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:
@@ -55,6 +55,8 @@ interface UIState {
|
||||
toggleVoiceFullscreen: () => void;
|
||||
setVoiceFullscreen: (fullscreen: boolean) => void;
|
||||
setPipCollapsed: (collapsed: boolean) => void;
|
||||
floatingPanelHeight: number;
|
||||
setFloatingPanelHeight: (height: number) => void;
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIState>()(
|
||||
@@ -124,6 +126,8 @@ export const useUIStore = create<UIState>()(
|
||||
toggleVoiceFullscreen: () => set((state) => ({ voiceFullscreen: !state.voiceFullscreen })),
|
||||
setVoiceFullscreen: (fullscreen) => set({ voiceFullscreen: fullscreen }),
|
||||
setPipCollapsed: (collapsed) => set({ pipCollapsed: collapsed }),
|
||||
floatingPanelHeight: 140,
|
||||
setFloatingPanelHeight: (height) => set({ floatingPanelHeight: height }),
|
||||
}),
|
||||
{
|
||||
name: 'backspace-ui-settings',
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user