fix: rearchitect server mute/deafen pipeline to scope restrictions by spaceId
- Replaces global `userId` tracking with `spaceId:userId` composite keys across both backend and frontend, fixing the issue where server-muting a user in one space bled into others. - Modifies client-side `ready` event hydration to merge voice states per-origin instead of completely overwriting the store, preventing federated connections from wiping out home instance mutes. - Excludes server voice restrictions from `zustand/persist` so stale client caches don't override the server's authority on reload. - Fixes React component reactivity by using reactive store selections for `spaceId` instead of imperative `getState()` calls, ensuring UI lockdown indicators accurately reflect the initial websocket handshake.
This commit is contained in:
@@ -34,8 +34,11 @@ export function ChannelSidebar() {
|
||||
const isDeafened = useVoiceStore((s) => s.isDeafened);
|
||||
const toggleMic = useVoiceStore((s) => s.toggleMic);
|
||||
const toggleDeafen = useVoiceStore((s) => s.toggleDeafen);
|
||||
const isServerMuted = useVoiceStore((s) => user ? s.serverMutedUserIds.has(user.id) : false);
|
||||
const isServerDeafened = useVoiceStore((s) => user ? s.serverDeafenedUserIds.has(user.id) : false);
|
||||
const spaceId = useSpaceStore((s) => currentVoiceChannelId ? s.channelToSpaceMap.get(currentVoiceChannelId) : null);
|
||||
const serverMutedUserIds = useVoiceStore((s) => s.serverMutedUserIds);
|
||||
const serverDeafenedUserIds = useVoiceStore((s) => s.serverDeafenedUserIds);
|
||||
const isServerMuted = !!(user && spaceId && serverMutedUserIds.has(`${spaceId}:${user.id}`));
|
||||
const isServerDeafened = !!(user && spaceId && serverDeafenedUserIds.has(`${spaceId}:${user.id}`));
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useVoiceStore } from '../../stores/voiceStore';
|
||||
import { useSpaceStore } from '../../stores/spaceStore';
|
||||
import { useSpaceStore, getChannelOrigin } from '../../stores/spaceStore';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { Avatar } from '../ui/Avatar';
|
||||
import { VoiceModContextMenu } from './VoiceModContextMenu';
|
||||
@@ -28,6 +28,7 @@ export function VoiceChannel({ channelId, channelName, onClick, locked }: VoiceC
|
||||
return local?.userId ?? null;
|
||||
});
|
||||
const members = useSpaceStore((s) => s.members);
|
||||
const channelToSpaceMap = useSpaceStore((s) => s.channelToSpaceMap);
|
||||
const myUser = useAuthStore((s) => s.user);
|
||||
const isActive = currentVoiceChannel === channelId;
|
||||
|
||||
@@ -92,8 +93,9 @@ export function VoiceChannel({ channelId, channelName, onClick, locked }: VoiceC
|
||||
: (participant?.isMuted ?? wsStatus?.isMuted ?? false);
|
||||
const hasCamera = participant?.isCameraOn ?? wsStatus?.isCameraOn ?? false;
|
||||
const isScreenSharing = participant?.isScreenSharing ?? wsStatus?.isScreenSharing ?? false;
|
||||
const isServerMuted = serverMutedUserIds.has(userId);
|
||||
const isServerDeafened = serverDeafenedUserIds.has(userId);
|
||||
const spaceId = channelToSpaceMap.get(channelId);
|
||||
const isServerMuted = serverMutedUserIds.has(`${spaceId}:${userId}`);
|
||||
const isServerDeafened = serverDeafenedUserIds.has(`${spaceId}:${userId}`);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useUIStore } from '../../stores/uiStore';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
import { getActiveRoom } from '../../hooks/useLiveKit';
|
||||
import { wsSend } from '../../hooks/useWebSocket';
|
||||
import { getChannelOrigin } from '../../stores/spaceStore';
|
||||
import { useSpaceStore, getChannelOrigin } from '../../stores/spaceStore';
|
||||
import { ScreenShareSettingsPopover } from './ScreenShareSettingsPopover';
|
||||
import { CAMERA_PRESET, startScreenShare, stopScreenShare } from '../../utils/screenShare';
|
||||
|
||||
@@ -27,8 +27,11 @@ export function VoiceControlBar() {
|
||||
const toggleVoiceFullscreen = useUIStore((s) => s.toggleVoiceFullscreen);
|
||||
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
||||
const myUser = useAuthStore((s) => s.user);
|
||||
const isServerMuted = useVoiceStore((s) => myUser ? s.serverMutedUserIds.has(myUser.id) : false);
|
||||
const isServerDeafened = useVoiceStore((s) => myUser ? s.serverDeafenedUserIds.has(myUser.id) : false);
|
||||
const spaceId = useSpaceStore((s) => currentVoiceChannelId ? s.channelToSpaceMap.get(currentVoiceChannelId) : null);
|
||||
const serverMutedUserIds = useVoiceStore((s) => s.serverMutedUserIds);
|
||||
const serverDeafenedUserIds = useVoiceStore((s) => s.serverDeafenedUserIds);
|
||||
const isServerMuted = !!(myUser && spaceId && serverMutedUserIds.has(`${spaceId}:${myUser.id}`));
|
||||
const isServerDeafened = !!(myUser && spaceId && serverDeafenedUserIds.has(`${spaceId}:${myUser.id}`));
|
||||
const voiceOrigin = currentVoiceChannelId ? getChannelOrigin(currentVoiceChannelId) : '';
|
||||
const [qualityOpen, setQualityOpen] = useState(false);
|
||||
const qualityBtnRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
@@ -34,9 +34,10 @@ export function VoiceModMenuItems({ targetUserId, channelId, onAction }: VoiceMo
|
||||
);
|
||||
|
||||
const voiceOrigin = getChannelOrigin(channelId);
|
||||
const spaceId = useSpaceStore((s) => s.channelToSpaceMap.get(channelId));
|
||||
|
||||
const isServerMuted = serverMutedUserIds.has(targetUserId);
|
||||
const isServerDeafened = serverDeafenedUserIds.has(targetUserId);
|
||||
const isServerMuted = serverMutedUserIds.has(`${spaceId}:${targetUserId}`);
|
||||
const isServerDeafened = serverDeafenedUserIds.has(`${spaceId}:${targetUserId}`);
|
||||
|
||||
if (!canMuteMembers && !canDeafenMembers && !canMoveMembers) return null;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { VoiceModMenuItems } from './VoiceModContextMenu';
|
||||
import { useSpaceStore } from '../../stores/spaceStore';
|
||||
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
|
||||
import type { UserTile } from '../../hooks/useLiveKit';
|
||||
import { getChannelOrigin } from '../../stores/spaceStore';
|
||||
|
||||
interface VoiceUserProps {
|
||||
tile: UserTile;
|
||||
@@ -22,6 +23,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
|
||||
const isSpeaking = useVoiceStore((s) => s.speakingParticipantIds.has(participant.identity));
|
||||
const serverMutedUserIds = useVoiceStore((s) => s.serverMutedUserIds);
|
||||
const serverDeafenedUserIds = useVoiceStore((s) => s.serverDeafenedUserIds);
|
||||
const spaceId = useSpaceStore((s) => currentVoiceChannelId ? s.channelToSpaceMap.get(currentVoiceChannelId) : null);
|
||||
|
||||
const [, forceUpdate] = useState(0);
|
||||
|
||||
@@ -149,8 +151,8 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{participant.isMuted && (() => {
|
||||
const isServerMutedUser = serverMutedUserIds.has(participant.userId);
|
||||
const isServerDeafenedUser = serverDeafenedUserIds.has(participant.userId);
|
||||
const isServerMutedUser = spaceId ? serverMutedUserIds.has(`${spaceId}:${participant.userId}`) : false;
|
||||
const isServerDeafenedUser = spaceId ? serverDeafenedUserIds.has(`${spaceId}:${participant.userId}`) : false;
|
||||
const badgeBg = (isServerMutedUser || isServerDeafenedUser) ? 'bg-accent-amber/90' : 'bg-accent-rose/90';
|
||||
return (
|
||||
<div className={`w-5 h-5 ${badgeBg} rounded-full flex items-center justify-center`}>
|
||||
@@ -169,7 +171,7 @@ export function VoiceUser({ tile, large }: VoiceUserProps) {
|
||||
);
|
||||
})()}
|
||||
{(isLocal ? isDeafened : participant.isDeafened) && (() => {
|
||||
const isServerDeafenedUser = serverDeafenedUserIds.has(participant.userId);
|
||||
const isServerDeafenedUser = spaceId ? serverDeafenedUserIds.has(`${spaceId}:${participant.userId}`) : false;
|
||||
const badgeBg = isServerDeafenedUser ? 'bg-accent-amber/90' : 'bg-accent-rose/90';
|
||||
return (
|
||||
<div className={`w-5 h-5 ${badgeBg} rounded-full flex items-center justify-center`}>
|
||||
|
||||
Reference in New Issue
Block a user