feat: voice status visibility + sidebar persistence fixes
- Add WebSocket voice_status/voice_status_update events so mute/deafen icons are visible in the sidebar without joining the voice channel - Server tracks voiceUserStates and includes them in the ready payload - Re-register voice channel on WebSocket reconnect to prevent sidebar users from disappearing after idle timeout - Re-broadcast deafen state to late joiners via LiveKit data channel - Fix black grid tile when video stops (enabled-flag guards) - Remove duplicate mute/deafen from VoiceControls (replaced with Video Quality + Noise Suppression) - Fix missing users in sidebar voice list (identity matching + fallback)
This commit is contained in:
@@ -156,6 +156,9 @@ export function handleClientEvent(
|
||||
case 'dm_call_end':
|
||||
handleDmCallEnd(event, userId);
|
||||
break;
|
||||
case 'voice_status':
|
||||
handleVoiceStatus(event, userId);
|
||||
break;
|
||||
default:
|
||||
connectionManager.sendToUser(userId, {
|
||||
type: 'error',
|
||||
@@ -738,6 +741,27 @@ function handleChannelAck(event: Record<string, unknown>, userId: string): void
|
||||
});
|
||||
}
|
||||
|
||||
function handleVoiceStatus(event: Record<string, unknown>, userId: string): void {
|
||||
const isMuted = event.isMuted === true;
|
||||
const isDeafened = event.isDeafened === true;
|
||||
|
||||
const channelId = connectionManager.getUserVoiceChannel(userId);
|
||||
if (!channelId) return;
|
||||
|
||||
const serverId = getChannelServerId(channelId);
|
||||
if (!serverId) return;
|
||||
|
||||
connectionManager.setVoiceUserStatus(userId, isMuted, isDeafened);
|
||||
|
||||
connectionManager.sendToServer(serverId, {
|
||||
type: 'voice_status_update',
|
||||
userId,
|
||||
channelId,
|
||||
isMuted,
|
||||
isDeafened,
|
||||
});
|
||||
}
|
||||
|
||||
// ─── DM Call Handlers ──────────────────────────────────────────────────────────
|
||||
|
||||
function handleDmCallStart(event: Record<string, unknown>, userId: string, username: string): void {
|
||||
|
||||
@@ -44,6 +44,8 @@ class ConnectionManager {
|
||||
private wsToUser: Map<WebSocket, string> = new Map();
|
||||
// dmChannelId → { callerId, startedAt } — active DM calls
|
||||
private activeCalls: Map<string, { callerId: string; startedAt: number }> = new Map();
|
||||
// userId → { isMuted, isDeafened } — voice user status (mute/deafen state)
|
||||
private voiceUserStates: Map<string, { isMuted: boolean; isDeafened: boolean }> = new Map();
|
||||
|
||||
addConnection(userId: string, ws: WebSocket): void {
|
||||
if (!this.connections.has(userId)) {
|
||||
@@ -110,9 +112,11 @@ class ConnectionManager {
|
||||
this.voiceStates.delete(channelId);
|
||||
}
|
||||
}
|
||||
this.voiceUserStates.delete(userId);
|
||||
}
|
||||
|
||||
leaveAllVoice(userId: string): string | null {
|
||||
this.voiceUserStates.delete(userId);
|
||||
for (const [channelId, users] of this.voiceStates) {
|
||||
if (users.has(userId)) {
|
||||
users.delete(userId);
|
||||
@@ -138,6 +142,23 @@ class ConnectionManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Voice user status management
|
||||
setVoiceUserStatus(userId: string, isMuted: boolean, isDeafened: boolean): void {
|
||||
this.voiceUserStates.set(userId, { isMuted, isDeafened });
|
||||
}
|
||||
|
||||
getVoiceUserStatus(userId: string): { isMuted: boolean; isDeafened: boolean } | undefined {
|
||||
return this.voiceUserStates.get(userId);
|
||||
}
|
||||
|
||||
clearVoiceUserStatus(userId: string): void {
|
||||
this.voiceUserStates.delete(userId);
|
||||
}
|
||||
|
||||
getAllVoiceUserStates(): Map<string, { isMuted: boolean; isDeafened: boolean }> {
|
||||
return this.voiceUserStates;
|
||||
}
|
||||
|
||||
// DM call management
|
||||
startCall(dmChannelId: string, callerId: string): boolean {
|
||||
if (this.activeCalls.has(dmChannelId)) return false; // Already in a call
|
||||
@@ -206,6 +227,7 @@ function buildReadyPayload(userId: string): {
|
||||
dmChannels: DmChannel[];
|
||||
folders: ServerFolder[];
|
||||
voiceStates: Record<string, string[]>;
|
||||
voiceUserStates: Record<string, { isMuted: boolean; isDeafened: boolean }>;
|
||||
readStates: ReadState[];
|
||||
} {
|
||||
const db = getDb();
|
||||
@@ -422,6 +444,20 @@ function buildReadyPayload(userId: string): {
|
||||
}
|
||||
}
|
||||
|
||||
// Build voice user states — tell the client mute/deafen status of voice users
|
||||
const voiceUserStates: Record<string, { isMuted: boolean; isDeafened: boolean }> = {};
|
||||
for (const chId of Object.keys(voiceStates)) {
|
||||
const usersInChannel = voiceStates[chId];
|
||||
if (usersInChannel) {
|
||||
for (const uid of usersInChannel) {
|
||||
const status = connectionManager.getVoiceUserStatus(uid);
|
||||
if (status) {
|
||||
voiceUserStates[uid] = status;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch read states for unread tracking
|
||||
const readStateRows = db.select()
|
||||
.from(schema.readStates)
|
||||
@@ -433,7 +469,7 @@ function buildReadyPayload(userId: string): {
|
||||
lastReadMessageId: rs.lastReadMessageId,
|
||||
}));
|
||||
|
||||
return { user, servers, dmChannels, folders, voiceStates, readStates };
|
||||
return { user, servers, dmChannels, folders, voiceStates, voiceUserStates, readStates };
|
||||
}
|
||||
|
||||
export async function registerWebSocket(app: FastifyInstance): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user