Repository navigation
feat: propagate global mute state to all participants via data channel - #749
Conversation
…el (#678) Extend the existing advisory remote-mute directive so an admin's mute is reflected in every participant's list, not just the mute target. When an admin mutes a participant, in addition to the existing targeted self-mute directive we broadcast an EndpointMessage mute-state message (muteState + participantId) to all endpoints. Each client parses it and stores per-endpointId advisory mute state in call state (participantMuteState), which the participant list renders as a muted indicator. Enforcement stays advisory, matching the already-merged behaviour: the broadcast only mirrors what an admin asked for and is not server-enforced. Authoritative/server-enforced mute with backend persistence and self-unmute prevention (option (b) in #678) is a deferred follow-up product decision and is documented as out of scope in the code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
Verdict: LGTM
Summary: Clean, well-tested extension of the advisory data-channel mute model. The new broadcast message is correctly distinguished from the existing targeted self-mute/kick directives: getMuteStateBroadcast is evaluated first in the receive chain and keys only off muteState+participantId, so a broadcast can never flip the receiver's own isRemotelyMuted or be read as a force-disconnect (asserted by the new use-rtc-connection test). Reducer action is wired into the TGlobalStateAction union and switch, all CallState construction sites gained participantMuteState, no new dependencies, style consistent. One non-blocking scope note and two minor suggestions.
Blocking
- None.
Warnings
src/components/ui/production-line/production-line.tsx(muteParticipant sender) — The only sender currently emitsmuteState: "mute"; nothing broadcasts"unmute"yet, so the advisory muted badge, once set in everyone's list, cannot be cleared from the UI (a participant unmuting their own mic locally does not broadcast). This is expected scope separation: the remote-unmute action belongs to the dependent admin-panel issue #676, and #678's job is the state-propagation mechanism (the receive/reducer side already handles both mute and unmute payloads). Flagging so the limitation is explicit; it is documented in-code.
Suggestions
src/global-state/global-state-reducer.ts(UPDATE_PARTICIPANT_MUTE_STATE) —participantMuteStateentries keyed byendpointIdare not pruned when a participant leaves; low impact (badge only renders for current participants andREMOVE_CALLdrops the whole map) but a long-lived, high-churn call accumulates dead keys. Optional cleanup on participant removal.src/components/ui/production-line/data-channel-parser.ts(getMuteStateBroadcast) — Re-validatesmuteState/participantIdas strings already guaranteed upstream; harmless/defensive given the looseEndpointMessagepayload type. Fine to keep.
Domain Note
The feature depends on SMB relaying an EndpointMessage with empty to: "" to all other endpoints without echoing to the sender (hence the acting admin's local self-dispatch). That broadcast semantic can't be verified from the frontend repo. If SMB does not fan out empty-to endpoint messages, the badge simply never appears on remote clients (silent no-op, not a crash) — worth a quick manual confirmation against a live SMB conference.
Reviewed by code-reviewer (separate invocation from the implementer) via daily-backlog-pr Phase 3.
Summary
EndpointMessage{ muteState, participantId }over the existing data channel whenever an admin mutes/unmutes a participant.callState.participantMuteState.Test plan
npm test)npm run typecheck)npm run lint)What a human does next
Nothing to configure. To verify manually: join one SMB line from two+ browsers, have an admin mute a participant from the participant list, and confirm a muted indicator appears next to that participant in every connected client's list within the normal refresh interval (advisory — the muted user can still unmute themselves locally).
Closes #678
🤖 Generated with Claude Code