fix: SoundController isSelf must check BOTH local ID and homeUserId

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.
This commit is contained in:
Jannis Braun
2026-04-08 15:33:51 +02:00
parent 5ab1f7b89d
commit b03df992dc
@@ -12,10 +12,14 @@ function getSfxVolume(): number {
export function SoundController() { export function SoundController() {
const audioManager = AudioManager.getInstance(); const audioManager = AudioManager.getInstance();
const currentUser = useAuthStore((s) => s.user); const currentUser = useAuthStore((s) => s.user);
// Federation-aware self-ID: in federated calls, LiveKit participant identity // Federation-aware self-check: in federated calls, the participant userId
// uses homeUserId (from the home instance), not the local snowflake ID. // can flip between localSnowflake (when activeDmCall resolves identity) and
// Without this, the SoundController thinks our own presence is a stranger. // homeUserId (when activeDmCall is cleared during disconnect). We must
const myId = currentUser?.homeUserId || currentUser?.id; // recognize BOTH as "self" to prevent phantom join/leave sounds.
const myIds = new Set<string>();
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 // Refs to track previous states
const isInitialMount = useRef(true); const isInitialMount = useRef(true);
@@ -90,28 +94,28 @@ export function SoundController() {
if (state.isLiveKitConnected && !justDisconnected) { if (state.isLiveKitConnected && !justDisconnected) {
// Someone joined voice (Others only) // Someone joined voice (Others only)
state.participants.forEach(p => { 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); audioManager.playSound('user_join', sfxOpts);
} }
}); });
// Someone left voice (Others only) // Someone left voice (Others only)
prevParticipantIds.current.forEach(userId => { prevParticipantIds.current.forEach(userId => {
if (!currentParticipantIds.has(userId) && userId !== myId) { if (!currentParticipantIds.has(userId) && !isSelf(userId)) {
audioManager.playSound('user_leave', sfxOpts); audioManager.playSound('user_leave', sfxOpts);
} }
}); });
// Someone started screen sharing (Others only) // Someone started screen sharing (Others only)
state.participants.forEach(p => { 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); audioManager.playSound('stream_user_joined', sfxOpts);
} }
}); });
// Someone stopped screen sharing (Others only) // Someone stopped screen sharing (Others only)
prevScreenShareUserIds.current.forEach(userId => { prevScreenShareUserIds.current.forEach(userId => {
if (!currentScreenShareUserIds.has(userId) && userId !== myId) { if (!currentScreenShareUserIds.has(userId) && !isSelf(userId)) {
audioManager.playSound('stream_user_left', sfxOpts); audioManager.playSound('stream_user_left', sfxOpts);
} }
}); });
@@ -183,7 +187,7 @@ export function SoundController() {
if (incomingCallLoop.current) incomingCallLoop.current.stop(); if (incomingCallLoop.current) incomingCallLoop.current.stop();
if (outgoingCallLoop.current) outgoingCallLoop.current.stop(); if (outgoingCallLoop.current) outgoingCallLoop.current.stop();
}; };
}, [audioManager, myId]); }, [audioManager, currentUser?.id, currentUser?.homeUserId]);
return null; return null;
} }