feat: real-time SPEAK permission enforcement in voice channels

Permission changes now take effect immediately without requiring
disconnect/reconnect. Modeled as "permission mute" parallel to
server mute — server recomputes SPEAK for all voice participants
on role/override changes and broadcasts state via WebSocket.
Includes amber UI indicators and mic toggle blocking.
This commit is contained in:
Jannis Braun
2026-03-10 14:50:01 +01:00
parent 4d9454382c
commit ce63c5ed36
13 changed files with 173 additions and 28 deletions
+16 -1
View File
@@ -99,6 +99,9 @@ interface VoiceState {
setServerMutedUser: (spaceId: string, userId: string, muted: boolean) => void;
setServerDeafenedUser: (spaceId: string, userId: string, deafened: boolean) => void;
clearServerVoiceStates: () => void;
// Permission mute state (SPEAK permission revoked while in voice)
permissionMutedUserIds: Set<string>; // Stores "spaceId:userId"
setPermissionMutedUser: (spaceId: string, userId: string, muted: boolean) => void;
getVoiceUsers: (channelId: string) => string[];
clearAllVoiceUsers: () => void;
clearVoiceUsersForOrigin: (origin: string) => void;
@@ -346,7 +349,17 @@ export const useVoiceStore = create<VoiceState>()(
return { serverDeafenedUserIds: newSet };
});
},
clearServerVoiceStates: () => set({ serverMutedUserIds: new Set(), serverDeafenedUserIds: new Set() }),
clearServerVoiceStates: () => set({ serverMutedUserIds: new Set(), serverDeafenedUserIds: new Set(), permissionMutedUserIds: new Set() }),
permissionMutedUserIds: new Set(),
setPermissionMutedUser: (spaceId, userId, muted) => {
set((state) => {
const newSet = new Set(state.permissionMutedUserIds);
const key = `${spaceId}:${userId}`;
if (muted) newSet.add(key); else newSet.delete(key);
return { permissionMutedUserIds: newSet };
});
},
getVoiceUsers: (channelId) => get().voiceUsers.get(channelId) ?? [],
@@ -458,6 +471,7 @@ export const useVoiceStore = create<VoiceState>()(
unwatchedCameras: new Set(),
serverMutedUserIds: new Set(),
serverDeafenedUserIds: new Set(),
permissionMutedUserIds: new Set(),
}),
}),
{
@@ -521,6 +535,7 @@ export const useVoiceStore = create<VoiceState>()(
// Reconstruct non-persisted Sets/Maps to their defaults
merged.serverMutedUserIds = currentState.serverMutedUserIds;
merged.serverDeafenedUserIds = currentState.serverDeafenedUserIds;
merged.permissionMutedUserIds = currentState.permissionMutedUserIds;
merged.voiceUsers = currentState.voiceUsers;
merged.participants = currentState.participants;
merged.speakingParticipantIds = currentState.speakingParticipantIds;