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 = {