From b77720c3da378d130d931decbd1239f860219d1a Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Wed, 23 Sep 2026 21:53:43 -0400 Subject: [PATCH] fix(calls): no stale "muted + deafened" render when a call starts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useCallControlState kept the previous control's state (the all-off default before a call) for one render after the control changed, until its effect caught up. The thumbbar, SMTC and tray hooks pushed that render to the OS, so every join flashed "muted + deafened" first (recorded: idle → active+muted+deafened → active). The state now remembers which control it belongs to and reads the new control directly when they differ (now: idle → active). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src/app/plugins/call/hooks.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/app/plugins/call/hooks.ts b/src/app/plugins/call/hooks.ts index e57534741..8df8a37c6 100644 --- a/src/app/plugins/call/hooks.ts +++ b/src/app/plugins/call/hooks.ts @@ -35,20 +35,29 @@ export const useSendClientWidgetApiAction = (api: ClientWidgetApi) => { const DEFAULT_CONTROL_STATE = new CallControlState(false, false, false); export const useCallControlState = (control: CallControl | undefined): CallControlState => { - const [state, setState] = useState(control?.getState() ?? DEFAULT_CONTROL_STATE); + // Remember which control the stored state came from. When a call starts the + // control changes, and the effect below only catches up after a render; until + // then the old (default: all off) state would be returned for the NEW call — + // one render of "muted + deafened" that the thumbbar, SMTC and tray (#4) + // pushed to the OS as a flicker. Read the new control directly instead. + const [entry, setEntry] = useState(() => ({ + control, + state: control?.getState() ?? DEFAULT_CONTROL_STATE, + })); useEffect(() => { if (!control) { - setState(DEFAULT_CONTROL_STATE); + setEntry({ control, state: DEFAULT_CONTROL_STATE }); return; } - setState(control.getState()); - const handleUpdate = () => setState(control.getState()); + setEntry({ control, state: control.getState() }); + const handleUpdate = () => setEntry({ control, state: control.getState() }); control.on(CallControlEvent.StateUpdate, handleUpdate); return () => { control.off(CallControlEvent.StateUpdate, handleUpdate); }; }, [control]); - return state; + if (entry.control !== control) return control?.getState() ?? DEFAULT_CONTROL_STATE; + return entry.state; };