From 29fed223976804572b169955f90c9a1aa8c6c355 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Fri, 20 Feb 2026 01:46:32 +0100 Subject: [PATCH 1/3] fix(voice): fix race conditions, broken icons, and null dereference in call UI - Fix mic enable race: move setMicrophoneEnabled outside batch and guard the .then() callback against stale room reference - Add reconnecting/reconnected event listeners so status indicator shows correct state during network interruptions - Fix camera icon: camera_video is not a valid Material Symbol, use videocam - Fix PiP snap: use innerWidth/innerHeight instead of outerWidth/outerHeight so corner detection matches clientX/clientY coordinate space - Guard user().user null dereference in voice participant tile to prevent crash when participant User object hasn't been fetched yet --- packages/client/components/rtc/state.tsx | 16 +++++++++----- .../features/voice/VoiceStatefulUserIcons.tsx | 2 +- .../features/voice/callCard/VoiceCallCard.tsx | 4 ++-- .../voice/callCard/VoiceCallCardActions.tsx | 2 +- .../callCard/VoiceCallCardActiveRoom.tsx | 22 +++++++++++++------ 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/packages/client/components/rtc/state.tsx b/packages/client/components/rtc/state.tsx index 95a1a0f095..92b6939008 100644 --- a/packages/client/components/rtc/state.tsx +++ b/packages/client/components/rtc/state.tsx @@ -147,15 +147,19 @@ class Voice { this.#setAudioOnly(false); this.#setVideoWatchDisabled({}); this.#setScreenshareWatch({}); - - if (this.speakingPermission) - room.localParticipant - .setMicrophoneEnabled(true) - .then((track) => this.#setMicrophone(typeof track !== "undefined")); }); - room.addListener("connected", () => this.#setState("CONNECTED")); + if (this.speakingPermission) + room.localParticipant + .setMicrophoneEnabled(true) + .then((track) => { + if (this.room() === room) + this.#setMicrophone(typeof track !== "undefined"); + }); + room.addListener("connected", () => this.#setState("CONNECTED")); + room.addListener("reconnecting", () => this.#setState("RECONNECTING")); + room.addListener("reconnected", () => this.#setState("CONNECTED")); room.addListener("disconnected", () => this.#setState("DISCONNECTED")); if (!auth) { diff --git a/packages/client/components/ui/components/features/voice/VoiceStatefulUserIcons.tsx b/packages/client/components/ui/components/features/voice/VoiceStatefulUserIcons.tsx index 9276e161c8..b652ce36eb 100644 --- a/packages/client/components/ui/components/features/voice/VoiceStatefulUserIcons.tsx +++ b/packages/client/components/ui/components/features/voice/VoiceStatefulUserIcons.tsx @@ -47,7 +47,7 @@ export function VoiceStatefulUserIcons(props: { headset_off - camera_video + videocam screen_share diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx index 601bbe1e37..d9e275860d 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCard.tsx @@ -113,8 +113,8 @@ export function VoiceCallCardContext(props: { children: JSX.Element }) { batch(() => { setMoving(false); - const left = event.clientX < window.outerWidth / 2; - const top = event.clientY < window.outerHeight / 2; + const left = event.clientX < window.innerWidth / 2; + const top = event.clientY < window.innerHeight / 2; setState({ type: "floating", diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx index a80e56d7bb..7b5c081497 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActions.tsx @@ -110,7 +110,7 @@ export function VoiceCallCardActions(props: { size: "xs" | "sm" }) { onPress={() => voice.toggleCamera()} > videocam_off}> - camera_video + videocam diff --git a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActiveRoom.tsx b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActiveRoom.tsx index 34331bba68..b1ee5489a5 100644 --- a/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActiveRoom.tsx +++ b/packages/client/components/ui/components/features/voice/callCard/VoiceCallCardActiveRoom.tsx @@ -422,13 +422,21 @@ function UserTile(props: { tileId: string; isMaximized: boolean }) { })} classList={{ "voice-tile": true, group: true }} use:floating={{ - userCard: { - user: user().user!, - member: user().member, - }, - contextMenu: () => ( - - ), + ...(user().user + ? { + userCard: { + user: user().user, + member: user().member, + }, + contextMenu: () => ( + + ), + } + : {}), }} > From 9130ace92a96717f4bb1495c30ce32a73671b495 Mon Sep 17 00:00:00 2001 From: Bryan White Date: Fri, 20 Feb 2026 02:04:17 +0100 Subject: [PATCH 2/3] style: fix prettier formatting in state.tsx --- packages/client/components/rtc/state.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/client/components/rtc/state.tsx b/packages/client/components/rtc/state.tsx index 92b6939008..1f79f241eb 100644 --- a/packages/client/components/rtc/state.tsx +++ b/packages/client/components/rtc/state.tsx @@ -150,12 +150,10 @@ class Voice { }); if (this.speakingPermission) - room.localParticipant - .setMicrophoneEnabled(true) - .then((track) => { - if (this.room() === room) - this.#setMicrophone(typeof track !== "undefined"); - }); + room.localParticipant.setMicrophoneEnabled(true).then((track) => { + if (this.room() === room) + this.#setMicrophone(typeof track !== "undefined"); + }); room.addListener("connected", () => this.#setState("CONNECTED")); room.addListener("reconnecting", () => this.#setState("RECONNECTING")); From 4ebad53df059c127efb1dacba28f0deb5c64339e Mon Sep 17 00:00:00 2001 From: Bryan White Date: Fri, 20 Feb 2026 02:40:06 +0100 Subject: [PATCH 3/3] fix(voice): add stale room guard to mock connection path Address review feedback: the mock path's setTimeout callback could set mic state on a stale room if disconnect/reconnect happens within the 10ms window. Add the same room() === room check used in the non-mock path. --- packages/client/components/rtc/state.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/client/components/rtc/state.tsx b/packages/client/components/rtc/state.tsx index 889accb9ac..f76ca752b2 100644 --- a/packages/client/components/rtc/state.tsx +++ b/packages/client/components/rtc/state.tsx @@ -187,11 +187,13 @@ class Voice { if (isMock) { // Instant mock connection setTimeout(() => { - this.#setState("CONNECTED"); - if (this.speakingPermission) { - void room.localParticipant.setMicrophoneEnabled(true).then(() => { - this.#setMicrophone(true); - }); + if (this.room() === room) { + this.#setState("CONNECTED"); + if (this.speakingPermission) { + void room.localParticipant.setMicrophoneEnabled(true).then(() => { + if (this.room() === room) this.#setMicrophone(true); + }); + } } }, 10); }