Compare commits

...
2 Commits
Author SHA1 Message Date
Lotus CIandClaude Opus 5.5 25fa0f6ef9 feat(desktop): mirror call state onto the tray icon (cinny-desktop #4)
Sends `set_tray_call_state` from the same effect as the taskbar thumbbar,
so tray and thumbbar can't disagree. Verified through a real local call:
idle → active → active+muted → active+muted+deafened → active+muted → idle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-23 21:53:48 -04:00
Lotus CIandClaude Opus 5.5 b77720c3da fix(calls): no stale "muted + deafened" render when a call starts
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-23 21:53:43 -04:00
2 changed files with 18 additions and 6 deletions
+4 -1
View File
@@ -10,7 +10,7 @@ type ThumbbarAction = { action: 'mute' | 'deafen' | 'end' };
* P5-44 — Taskbar thumbnail toolbar (call controls). While a call is active,
* mirrors the mic/sound state onto the native `set_thumbbar` command (three
* Mute / Deafen / End-Call buttons on the Windows taskbar thumbnail toolbar) and
* hides them when the call ends. Thumb-button clicks come back as the
* hides them when the call ends. The tray icon mirrors the same state. Thumb-button clicks come back as the
* `thumbbar-action` event and drive the real call controls. No-op in the browser.
*/
export function useTauriThumbbar(): void {
@@ -25,6 +25,9 @@ export function useTauriThumbbar(): void {
useEffect(() => {
invokeTauri('set_thumbbar', { active, muted, deafened });
// [cinny-desktop #4] Same state onto the tray icon + tooltip, from the same
// effect so the tray and the thumbbar can never disagree.
invokeTauri('set_tray_call_state', { active, muted, deafened });
}, [active, muted, deafened]);
useTauriEvent<ThumbbarAction>('thumbbar-action', ({ action }) => {
+14 -5
View File
@@ -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;
};