diff --git a/src/components/landing-page/join-production.tsx b/src/components/landing-page/join-production.tsx index 204e74a9..6f46c943 100644 --- a/src/components/landing-page/join-production.tsx +++ b/src/components/landing-page/join-production.tsx @@ -160,6 +160,8 @@ export const JoinProduction = ({ connectionState: null, audioElements: null, sessionId: null, + dataChannel: null, + isRemotelyMuted: false, hotkeys: { muteHotkey: "m", speakerHotkey: "n", diff --git a/src/components/modal/modal-confirmation-text.ts b/src/components/modal/modal-confirmation-text.ts index f68bcfd8..31d0dce8 100644 --- a/src/components/modal/modal-confirmation-text.ts +++ b/src/components/modal/modal-confirmation-text.ts @@ -2,4 +2,8 @@ import styled from "@emotion/styled"; export const ModalConfirmationText = styled.p` padding-bottom: 1rem; + + &.bold { + font-weight: bold; + } `; diff --git a/src/components/production-line/production-line.tsx b/src/components/production-line/production-line.tsx index 31bc667d..d6f631f0 100644 --- a/src/components/production-line/production-line.tsx +++ b/src/components/production-line/production-line.tsx @@ -161,6 +161,10 @@ export const ProductionLine = ({ const [showDeviceSettings, setShowDeviceSettings] = useState(false); const [confirmExitModalOpen, setConfirmExitModalOpen] = useState(false); const [value, setValue] = useState(0.75); + const [confirmModalOpen, setConfirmModalOpen] = useState(false); + const [muteError, setMuteError] = useState(false); + const [userId, setUserId] = useState(""); + const [userName, setUserName] = useState(""); const { joinProductionOptions, dominantSpeaker, @@ -169,6 +173,8 @@ export const ProductionLine = ({ audioElements, sessionId, hotkeys: savedHotkeys, + dataChannel, + isRemotelyMuted, } = callState; const { @@ -252,10 +258,33 @@ export const ProductionLine = ({ }); setIsInputMuted(mute); } + if (mute) { + dispatch({ + type: "UPDATE_CALL", + payload: { + id, + updates: { + isRemotelyMuted: false, + }, + }, + }); + } }, - [inputAudioStream] + [dispatch, id, inputAudioStream] ); + useEffect(() => { + if (!confirmModalOpen) { + setMuteError(false); + } + }, [confirmModalOpen]); + + useEffect(() => { + if (isRemotelyMuted) { + muteInput(true); + } + }, [isRemotelyMuted, muteInput]); + const { playEnterSound, playExitSound } = useAudioCue(); const exit = useCallback(() => { @@ -293,7 +322,18 @@ export const ProductionLine = ({ }); setIsInputMuted(masterInputMute); } - }, [inputAudioStream, masterInputMute]); + if (masterInputMute) { + dispatch({ + type: "UPDATE_CALL", + payload: { + id, + updates: { + isRemotelyMuted: false, + }, + }, + }); + } + }, [dispatch, id, inputAudioStream, masterInputMute]); useEffect(() => { if (connectionState === "connected") { @@ -390,6 +430,25 @@ export const ProductionLine = ({ } }; + const muteParticipant = () => { + const msg = JSON.stringify({ + type: "EndpointMessage", + to: userId, + payload: { + muteParticipant: "mute", + }, + }); + + if (dataChannel && dataChannel.readyState === "open") { + dataChannel.send(msg); + setMuteError(false); + setConfirmModalOpen(false); + } else { + setMuteError(true); + console.error("Data channel is not open."); + } + }; + // TODO detect if browser back button is pressed and run exit(); return ( @@ -606,8 +665,30 @@ export const ProductionLine = ({ participants={line.participants} dominantSpeaker={dominantSpeaker} audioLevelAboveThreshold={audioLevelAboveThreshold} + setConfirmModalOpen={setConfirmModalOpen} + setUserId={setUserId} + setUserName={setUserName} /> )} + {confirmModalOpen && ( + setConfirmModalOpen(false)}> + Confirm + + {muteError + ? "Something went wrong, Please try again" + : `Are you sure you want to mute ${userName}?`} + + + {muteError + ? "" + : `This will mute ${userName} for everyone in the call.`} + + setConfirmModalOpen(false)} + /> + + )} )} diff --git a/src/components/production-line/use-rtc-connection.ts b/src/components/production-line/use-rtc-connection.ts index 031cc10a..09c078c7 100644 --- a/src/components/production-line/use-rtc-connection.ts +++ b/src/components/production-line/use-rtc-connection.ts @@ -127,6 +127,16 @@ const establishConnection = ({ } ); + dispatch({ + type: "UPDATE_CALL", + payload: { + id: callId, + updates: { + dataChannel, + }, + }, + }); + const onDataChannelMessage = ({ data }: MessageEvent) => { let message: unknown; @@ -153,6 +163,30 @@ const establishConnection = ({ }, }, }); + } else if ( + message && + typeof message === "object" && + "type" in message && + message.type === "EndpointMessage" && + "payload" in message && + "to" in message && + "from" in message && + message.payload && + typeof message.payload === "object" && + "muteParticipant" in message.payload && + typeof message.payload.muteParticipant === "string" + ) { + dispatch({ + type: "UPDATE_CALL", + payload: { + id: callId, + updates: { + isRemotelyMuted: + message.payload.muteParticipant === "mute" && + message.to !== message.from, + }, + }, + }); } else { console.error("Unexpected data channel message structure"); } diff --git a/src/components/production-line/user-list.tsx b/src/components/production-line/user-list.tsx index 08a44bda..9812d03d 100644 --- a/src/components/production-line/user-list.tsx +++ b/src/components/production-line/user-list.tsx @@ -1,7 +1,7 @@ import styled from "@emotion/styled"; import { DisplayContainerHeader } from "../landing-page/display-container-header.tsx"; import { TParticipant } from "./types.ts"; -import { UserIcon } from "../../assets/icons/icon.tsx"; +import { MicMuted, UserIcon } from "../../assets/icons/icon.tsx"; const Container = styled.div` width: 100%; @@ -26,11 +26,12 @@ type TIndicatorProps = { isActive: boolean; }; -const User = styled.div` +const UserWrapper = styled.div` display: flex; align-items: center; - background: #1a1a1a; + justify-content: space-between; padding: 1rem; + background: #1a1a1a; color: #ddd; border: transparent; border-bottom: 0.1rem solid #464646; @@ -47,6 +48,11 @@ const User = styled.div` ${({ isYou }) => (isYou ? `background: #353434;` : "")} `; +const User = styled.div` + display: flex; + align-items: center; +`; + const IsTalkingIndicator = styled.div` width: 4rem; height: 4rem; @@ -79,11 +85,24 @@ const IconWrapper = styled.div` width: 2rem; `; +const MuteParticipantButton = styled.button` + width: 3rem; + padding: 0.3rem; + margin: 0; + background: #302b2b; + border: 0.1rem solid #707070; + border-radius: 0.4rem; + cursor: pointer; +`; + type TUserListOptions = { participants: TParticipant[]; sessionId: string | null; dominantSpeaker: string | null; audioLevelAboveThreshold: boolean; + setConfirmModalOpen: (value: boolean) => void; + setUserId: (value: string) => void; + setUserName: (value: string) => void; }; export const UserList = ({ @@ -91,6 +110,9 @@ export const UserList = ({ sessionId, dominantSpeaker, audioLevelAboveThreshold, + setConfirmModalOpen, + setUserId, + setUserName, }: TUserListOptions) => { if (!participants) return null; @@ -98,22 +120,38 @@ export const UserList = ({ Participants - {participants.map((p) => ( - - - - - - - - - {p.name} {p.isActive ? "" : "(inactive)"} - - ))} + {participants.map((p) => { + const isYou = p.sessionId === sessionId; + return ( + + + + + + + + + + {p.name} {p.isActive ? "" : "(inactive)"} + + {!isYou && p.isActive && ( + { + setUserId(p.endpointId); + setUserName(p.name); + setConfirmModalOpen(true); + }} + > + + + )} + + ); + })} ); diff --git a/src/components/production-list/production-list-item.tsx b/src/components/production-list/production-list-item.tsx index bd83ea95..63bdfc23 100644 --- a/src/components/production-list/production-list-item.tsx +++ b/src/components/production-list/production-list-item.tsx @@ -162,6 +162,8 @@ export const ProductionsListItem = ({ connectionState: null, audioElements: null, sessionId: null, + dataChannel: null, + isRemotelyMuted: false, hotkeys: { muteHotkey: "m", speakerHotkey: "n", diff --git a/src/global-state/types.ts b/src/global-state/types.ts index 56bbac5b..c0cd4ae5 100644 --- a/src/global-state/types.ts +++ b/src/global-state/types.ts @@ -24,6 +24,8 @@ export interface CallState { audioElements: HTMLAudioElement[] | null; sessionId: string | null; hotkeys: Hotkeys; + dataChannel: RTCDataChannel | null; + isRemotelyMuted: boolean; } export type TGlobalState = {