Skip to content

Commit

Permalink
Feat/mute participant (#265)
Browse files Browse the repository at this point in the history
  • Loading branch information
malmen237 authored Jan 13, 2025
1 parent 8c807be commit adf129c
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 21 deletions.
2 changes: 2 additions & 0 deletions src/components/landing-page/join-production.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ export const JoinProduction = ({
connectionState: null,
audioElements: null,
sessionId: null,
dataChannel: null,
isRemotelyMuted: false,
hotkeys: {
muteHotkey: "m",
speakerHotkey: "n",
Expand Down
4 changes: 4 additions & 0 deletions src/components/modal/modal-confirmation-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ import styled from "@emotion/styled";

export const ModalConfirmationText = styled.p`
padding-bottom: 1rem;
&.bold {
font-weight: bold;
}
`;
85 changes: 83 additions & 2 deletions src/components/production-line/production-line.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -169,6 +173,8 @@ export const ProductionLine = ({
audioElements,
sessionId,
hotkeys: savedHotkeys,
dataChannel,
isRemotelyMuted,
} = callState;

const {
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -606,8 +665,30 @@ export const ProductionLine = ({
participants={line.participants}
dominantSpeaker={dominantSpeaker}
audioLevelAboveThreshold={audioLevelAboveThreshold}
setConfirmModalOpen={setConfirmModalOpen}
setUserId={setUserId}
setUserName={setUserName}
/>
)}
{confirmModalOpen && (
<Modal onClose={() => setConfirmModalOpen(false)}>
<DisplayContainerHeader>Confirm</DisplayContainerHeader>
<ModalConfirmationText>
{muteError
? "Something went wrong, Please try again"
: `Are you sure you want to mute ${userName}?`}
</ModalConfirmationText>
<ModalConfirmationText className="bold">
{muteError
? ""
: `This will mute ${userName} for everyone in the call.`}
</ModalConfirmationText>
<VerifyDecision
confirm={muteParticipant}
abort={() => setConfirmModalOpen(false)}
/>
</Modal>
)}
</ListWrapper>
</FlexContainer>
)}
Expand Down
34 changes: 34 additions & 0 deletions src/components/production-line/use-rtc-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ const establishConnection = ({
}
);

dispatch({
type: "UPDATE_CALL",
payload: {
id: callId,
updates: {
dataChannel,
},
},
});

const onDataChannelMessage = ({ data }: MessageEvent) => {
let message: unknown;

Expand All @@ -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");
}
Expand Down
76 changes: 57 additions & 19 deletions src/components/production-line/user-list.tsx
Original file line number Diff line number Diff line change
@@ -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%;
Expand All @@ -26,11 +26,12 @@ type TIndicatorProps = {
isActive: boolean;
};

const User = styled.div<TUserProps>`
const UserWrapper = styled.div<TUserProps>`
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;
Expand All @@ -47,6 +48,11 @@ const User = styled.div<TUserProps>`
${({ isYou }) => (isYou ? `background: #353434;` : "")}
`;

const User = styled.div`
display: flex;
align-items: center;
`;

const IsTalkingIndicator = styled.div<TIsTalkingIndicator>`
width: 4rem;
height: 4rem;
Expand Down Expand Up @@ -79,41 +85,73 @@ 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 = ({
participants,
sessionId,
dominantSpeaker,
audioLevelAboveThreshold,
setConfirmModalOpen,
setUserId,
setUserName,
}: TUserListOptions) => {
if (!participants) return null;

return (
<Container>
<DisplayContainerHeader>Participants</DisplayContainerHeader>
<ListWrapper>
{participants.map((p) => (
<User key={p.sessionId} isYou={p.sessionId === sessionId}>
<IsTalkingIndicator
isTalking={
audioLevelAboveThreshold && p.endpointId === dominantSpeaker
}
>
<OnlineIndicator isActive={p.isActive}>
<IconWrapper>
<UserIcon />
</IconWrapper>
</OnlineIndicator>
</IsTalkingIndicator>
{p.name} {p.isActive ? "" : "(inactive)"}
</User>
))}
{participants.map((p) => {
const isYou = p.sessionId === sessionId;
return (
<UserWrapper key={p.sessionId} isYou={isYou}>
<User>
<IsTalkingIndicator
isTalking={
audioLevelAboveThreshold && p.endpointId === dominantSpeaker
}
>
<OnlineIndicator isActive={p.isActive}>
<IconWrapper>
<UserIcon />
</IconWrapper>
</OnlineIndicator>
</IsTalkingIndicator>
{p.name} {p.isActive ? "" : "(inactive)"}
</User>
{!isYou && p.isActive && (
<MuteParticipantButton
onClick={() => {
setUserId(p.endpointId);
setUserName(p.name);
setConfirmModalOpen(true);
}}
>
<MicMuted />
</MuteParticipantButton>
)}
</UserWrapper>
);
})}
</ListWrapper>
</Container>
);
Expand Down
2 changes: 2 additions & 0 deletions src/components/production-list/production-list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ export const ProductionsListItem = ({
connectionState: null,
audioElements: null,
sessionId: null,
dataChannel: null,
isRemotelyMuted: false,
hotkeys: {
muteHotkey: "m",
speakerHotkey: "n",
Expand Down
2 changes: 2 additions & 0 deletions src/global-state/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface CallState {
audioElements: HTMLAudioElement[] | null;
sessionId: string | null;
hotkeys: Hotkeys;
dataChannel: RTCDataChannel | null;
isRemotelyMuted: boolean;
}

export type TGlobalState = {
Expand Down

0 comments on commit adf129c

Please sign in to comment.