fix: immediately clean up voice state when voice-active socket disconnects
Previously, voice cleanup only happened in finalizeDisconnect after a 5-second grace period, and only when ALL connections closed. If a user was logged in on multiple devices and closed the one in voice, the voice state was never cleaned up — creating a permanent ghost in the channel sidebar. Now removeConnection checks if the closing socket is the voice-active one (tracked via voiceWs map) and immediately cleans up voice state, broadcasts the leave event, and notifies remaining tabs.
This commit is contained in:
@@ -125,9 +125,71 @@ class ConnectionManager {
|
||||
const userConnections = this.connections.get(userId);
|
||||
if (userConnections) {
|
||||
userConnections.delete(ws);
|
||||
|
||||
// ── Immediate voice cleanup if this was the voice-active socket ──
|
||||
if (this.voiceWs.get(userId) === ws) {
|
||||
this.voiceWs.delete(userId);
|
||||
|
||||
// Leave voice room (space or DM)
|
||||
const left = this.leaveCurrentRoom(userId);
|
||||
this.clearVoiceUserStatus(userId);
|
||||
if (left) {
|
||||
if (left.room.roomType === 'space') {
|
||||
const meta = left.room.metadata as SpaceRoomMeta;
|
||||
this.sendToSpace(meta.spaceId, {
|
||||
type: 'voice_state_update',
|
||||
channelId: left.roomId,
|
||||
userId,
|
||||
action: 'leave',
|
||||
});
|
||||
} else {
|
||||
this.sendToDmMembers(left.roomId, {
|
||||
type: 'voice_state_update',
|
||||
channelId: left.roomId,
|
||||
userId,
|
||||
action: 'leave',
|
||||
});
|
||||
// Auto-end empty active DM calls
|
||||
const updatedRoom = this.voiceRooms.get(left.roomId);
|
||||
if (updatedRoom && updatedRoom.participants.size === 0
|
||||
&& (updatedRoom.metadata as DmRoomMeta).state === 'active') {
|
||||
this.destroyRoom(left.roomId);
|
||||
this.sendToDmMembers(left.roomId, {
|
||||
type: 'dm_call_ended',
|
||||
dmChannelId: left.roomId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up ringing DM rooms where this user is the caller
|
||||
for (const [roomId, room] of this.voiceRooms) {
|
||||
if (room.roomType === 'dm') {
|
||||
const meta = room.metadata as DmRoomMeta;
|
||||
if (meta.state === 'ringing' && meta.callerId === userId) {
|
||||
this.destroyRoom(roomId);
|
||||
this.sendToDmMembers(roomId, {
|
||||
type: 'dm_call_ended',
|
||||
dmChannelId: roomId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the user's remaining tabs so their UI updates
|
||||
if (userConnections.size > 0 && left) {
|
||||
this.sendToUser(userId, {
|
||||
type: 'voice_disconnected',
|
||||
userId,
|
||||
channelId: left.roomId,
|
||||
reason: 'session_closed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (userConnections.size === 0) {
|
||||
this.connections.delete(userId);
|
||||
// Schedule disconnect cleanup
|
||||
// Schedule disconnect cleanup (presence/offline, NOT voice — already handled above)
|
||||
this.scheduleDisconnect(userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,7 +395,7 @@ export type ServerEvent =
|
||||
| { type: 'voice_space_deafened'; userId: string; channelId: string; spaceId: string; deafened: boolean }
|
||||
| { type: 'voice_permission_muted'; userId: string; spaceId: string; muted: boolean }
|
||||
| { type: 'voice_moved'; userId: string; oldChannelId: string; newChannelId: string }
|
||||
| { type: 'voice_disconnected'; userId: string; channelId: string; reason?: 'displaced' }
|
||||
| { type: 'voice_disconnected'; userId: string; channelId: string; reason?: 'displaced' | 'session_closed' }
|
||||
| { type: 'user_updated'; user: User }
|
||||
| { type: 'member_banned'; spaceId: string; reason: string | null }
|
||||
| { type: 'category_created'; category: ChannelCategory; spaceId: string }
|
||||
|
||||
Reference in New Issue
Block a user