From b03df992dcd52a62fd92409e4ffb2a9d43ae1ce7 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:33:51 +0200 Subject: [PATCH] fix: SoundController isSelf must check BOTH local ID and homeUserId MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The identity flip during disconnect: updateParticipants resolves homeUserId → localSnowflake when activeDmCall is set, but reverts to raw homeUserId when activeDmCall is cleared (before LiveKit disconnect completes). SoundController sees the snowflake "leave" and the homeUserId "join" — two phantom events for the same person. Previous fix only checked homeUserId OR id. Now checks BOTH via isSelf(id) which matches against a Set of {id, homeUserId}. This recognizes the user as "self" regardless of which identity format the participant currently has. --- .../src/components/voice/SoundController.tsx | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/web/src/components/voice/SoundController.tsx b/packages/web/src/components/voice/SoundController.tsx index 5514db5b..51ab7a05 100644 --- a/packages/web/src/components/voice/SoundController.tsx +++ b/packages/web/src/components/voice/SoundController.tsx @@ -12,10 +12,14 @@ function getSfxVolume(): number { export function SoundController() { const audioManager = AudioManager.getInstance(); const currentUser = useAuthStore((s) => s.user); - // Federation-aware self-ID: in federated calls, LiveKit participant identity - // uses homeUserId (from the home instance), not the local snowflake ID. - // Without this, the SoundController thinks our own presence is a stranger. - const myId = currentUser?.homeUserId || currentUser?.id; + // Federation-aware self-check: in federated calls, the participant userId + // can flip between localSnowflake (when activeDmCall resolves identity) and + // homeUserId (when activeDmCall is cleared during disconnect). We must + // recognize BOTH as "self" to prevent phantom join/leave sounds. + const myIds = new Set(); + if (currentUser?.id) myIds.add(currentUser.id); + if (currentUser?.homeUserId) myIds.add(currentUser.homeUserId); + const isSelf = (id: string) => myIds.has(id); // Refs to track previous states const isInitialMount = useRef(true); @@ -90,28 +94,28 @@ export function SoundController() { if (state.isLiveKitConnected && !justDisconnected) { // Someone joined voice (Others only) state.participants.forEach(p => { - if (!prevParticipantIds.current.has(p.userId) && p.userId !== myId) { + if (!prevParticipantIds.current.has(p.userId) && !isSelf(p.userId)) { audioManager.playSound('user_join', sfxOpts); } }); // Someone left voice (Others only) prevParticipantIds.current.forEach(userId => { - if (!currentParticipantIds.has(userId) && userId !== myId) { + if (!currentParticipantIds.has(userId) && !isSelf(userId)) { audioManager.playSound('user_leave', sfxOpts); } }); // Someone started screen sharing (Others only) state.participants.forEach(p => { - if (p.isScreenSharing && !prevScreenShareUserIds.current.has(p.userId) && p.userId !== myId) { + if (p.isScreenSharing && !prevScreenShareUserIds.current.has(p.userId) && !isSelf(p.userId)) { audioManager.playSound('stream_user_joined', sfxOpts); } }); // Someone stopped screen sharing (Others only) prevScreenShareUserIds.current.forEach(userId => { - if (!currentScreenShareUserIds.has(userId) && userId !== myId) { + if (!currentScreenShareUserIds.has(userId) && !isSelf(userId)) { audioManager.playSound('stream_user_left', sfxOpts); } }); @@ -183,7 +187,7 @@ export function SoundController() { if (incomingCallLoop.current) incomingCallLoop.current.stop(); if (outgoingCallLoop.current) outgoingCallLoop.current.stop(); }; - }, [audioManager, myId]); + }, [audioManager, currentUser?.id, currentUser?.homeUserId]); return null; }