From 8674579b1968550248856f58dcf5de344c4b6eb7 Mon Sep 17 00:00:00 2001 From: Myx Date: Sat, 4 Apr 2026 03:09:44 +0200 Subject: [PATCH] fix: leave and reconnect sound randomly playing, also fix leave sound when muting --- toju-app/src/app/core/constants.ts | 1 + toju-app/src/app/store/rooms/rooms.effects.ts | 54 ++++++++++++++++--- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/toju-app/src/app/core/constants.ts b/toju-app/src/app/core/constants.ts index fc2ab57..1f28577 100644 --- a/toju-app/src/app/core/constants.ts +++ b/toju-app/src/app/core/constants.ts @@ -15,3 +15,4 @@ export const DEFAULT_MAX_USERS = 50; export const DEFAULT_AUDIO_BITRATE_KBPS = 96; export const DEFAULT_VOLUME = 100; export const SEARCH_DEBOUNCE_MS = 300; +export const RECONNECT_SOUND_GRACE_MS = 15_000; diff --git a/toju-app/src/app/store/rooms/rooms.effects.ts b/toju-app/src/app/store/rooms/rooms.effects.ts index 06bb1c1..048b98c 100644 --- a/toju-app/src/app/store/rooms/rooms.effects.ts +++ b/toju-app/src/app/store/rooms/rooms.effects.ts @@ -64,7 +64,7 @@ import { } from '../../shared-kernel'; import { NotificationAudioService, AppSound } from '../../core/services/notification-audio.service'; import { hasRoomBanForUser } from '../../core/helpers/room-ban.helpers'; -import { ROOM_URL_PATTERN } from '../../core/constants'; +import { RECONNECT_SOUND_GRACE_MS, ROOM_URL_PATTERN } from '../../core/constants'; import { VoiceSessionFacade } from '../../domains/voice-session'; import { findRoomMember, @@ -190,6 +190,12 @@ export class RoomsEffects { * preventing false join/leave sounds during state refreshes. */ private knownVoiceUsers = new Set(); + /** + * When a user leaves (e.g. socket drops), record the timestamp so + * that a rapid re-join (reconnect) does not trigger a false + * join/leave sound within {@link RECONNECT_SOUND_GRACE_MS}. + */ + private recentlyLeftVoiceTimestamps = new Map(); private roomNavigationRequestVersion = 0; private latestNavigatedRoomId: string | null = null; @@ -1287,6 +1293,10 @@ export class RoomsEffects { : undefined; if (!remainingServerIds || remainingServerIds.length === 0) { + if (this.knownVoiceUsers.has(signalingMessage.oderId)) { + this.recentlyLeftVoiceTimestamps.set(signalingMessage.oderId, Date.now()); + } + this.knownVoiceUsers.delete(signalingMessage.oderId); } @@ -1452,13 +1462,20 @@ export class RoomsEffects { const nowConnected = vs.isConnected ?? false; const wasKnown = this.knownVoiceUsers.has(userId); const wasInCurrentVoiceRoom = this.isSameVoiceRoom(existingUser?.voiceState, currentUser?.voiceState); - const isInCurrentVoiceRoom = this.isSameVoiceRoom(vs, currentUser?.voiceState); + // Merge with existing state so partial updates (e.g. mute toggle + // that omits roomId/serverId) don't look like a room change. + const mergedVoiceState = { ...existingUser?.voiceState, ...vs }; + const isInCurrentVoiceRoom = this.isSameVoiceRoom(mergedVoiceState, currentUser?.voiceState); if (weAreInVoice) { - if (((!wasKnown && isInCurrentVoiceRoom) || (userExists && !wasInCurrentVoiceRoom && isInCurrentVoiceRoom)) && nowConnected) { - this.audioService.play(AppSound.Joining); - } else if (wasInCurrentVoiceRoom && !isInCurrentVoiceRoom) { - this.audioService.play(AppSound.Leave); + const isReconnect = this.consumeRecentLeave(userId); + + if (!isReconnect) { + if (((!wasKnown && isInCurrentVoiceRoom) || (userExists && !wasInCurrentVoiceRoom && isInCurrentVoiceRoom)) && nowConnected) { + this.audioService.play(AppSound.Joining); + } else if (wasInCurrentVoiceRoom && !isInCurrentVoiceRoom) { + this.audioService.play(AppSound.Leave); + } } } @@ -1630,6 +1647,31 @@ export class RoomsEffects { && voiceState.serverId === currentUserVoiceState.serverId; } + /** + * Returns `true` and cleans up the entry if the given user left + * recently enough to be considered a reconnect. Also prunes any + * stale entries older than the grace window. + */ + private consumeRecentLeave(userId: string): boolean { + const now = Date.now(); + + // Prune stale entries while iterating. + for (const [id, ts] of this.recentlyLeftVoiceTimestamps) { + if (now - ts > RECONNECT_SOUND_GRACE_MS) { + this.recentlyLeftVoiceTimestamps.delete(id); + } + } + + const leaveTs = this.recentlyLeftVoiceTimestamps.get(userId); + + if (leaveTs !== undefined && now - leaveTs <= RECONNECT_SOUND_GRACE_MS) { + this.recentlyLeftVoiceTimestamps.delete(userId); + return true; + } + + return false; + } + private resolveRoom(roomId: string | undefined, currentRoom: Room | null, savedRooms: Room[]): Room | null { if (!roomId) return currentRoom;