feat: bans system, voice moderation, and federated space settings fixes

Add ban/unban functionality with BansPanel in space settings, voice
moderation context menu (mute/deafen/disconnect), and fix federated
space settings panels to use origin-aware API client. Show domain
indicators for federated members in MembersPanel.
This commit is contained in:
Jannis Braun
2026-03-09 15:56:46 +01:00
parent 7e2986ca01
commit e2c18ad2b0
24 changed files with 1224 additions and 159 deletions
@@ -1,6 +1,7 @@
import React, { useEffect, useState, useRef } from 'react';
import { useVoiceStore } from '../../stores/voiceStore';
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';
@@ -25,17 +26,22 @@ export function VoiceControlBar() {
const voiceFullscreen = useUIStore((s) => s.voiceFullscreen);
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 voiceOrigin = currentVoiceChannelId ? getChannelOrigin(currentVoiceChannelId) : '';
const [qualityOpen, setQualityOpen] = useState(false);
const qualityBtnRef = useRef<HTMLButtonElement>(null);
const handleMute = React.useCallback(async () => {
if (isServerMuted || isServerDeafened) return;
toggleMic();
// Broadcast via WebSocket so sidebar shows status without joining
wsSend({ type: 'voice_status', isMuted: !isMuted, isDeafened, isCameraOn, isScreenSharing }, voiceOrigin);
}, [isMuted, isDeafened, isCameraOn, isScreenSharing, toggleMic, voiceOrigin]);
}, [isMuted, isDeafened, isCameraOn, isScreenSharing, toggleMic, voiceOrigin, isServerMuted, isServerDeafened]);
const handleDeafen = React.useCallback(async () => {
if (isServerDeafened) return;
const room = getActiveRoom();
const willDeafen = !isDeafened;
// Update store FIRST so updateParticipants reads correct state
@@ -149,31 +155,35 @@ export function VoiceControlBar() {
{/* Mute */}
<button
onClick={handleMute}
className={isMuted || isDeafened
? `${btnBase} bg-accent-rose/20 text-txt-danger hover:bg-accent-rose/30`
: btnDefault
className={isServerMuted
? `${btnBase} bg-accent-amber/20 text-accent-amber cursor-not-allowed`
: isMuted || isDeafened
? `${btnBase} bg-accent-rose/20 text-txt-danger hover:bg-accent-rose/30`
: btnDefault
}
title={isMuted ? 'Unmute (M)' : 'Mute (M)'}
title={isServerMuted ? 'Server Muted' : isMuted ? 'Unmute (M)' : 'Mute (M)'}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z" />
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" />
{(isMuted || isDeafened) && <line x1="3" y1="3" x2="21" y2="21" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />}
{(isMuted || isDeafened || isServerMuted) && <line x1="3" y1="3" x2="21" y2="21" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />}
</svg>
</button>
{/* Deafen */}
<button
onClick={handleDeafen}
className={isDeafened
? `${btnBase} bg-accent-rose/20 text-txt-danger hover:bg-accent-rose/30`
: btnDefault
className={isServerDeafened
? `${btnBase} bg-accent-amber/20 text-accent-amber cursor-not-allowed`
: isDeafened
? `${btnBase} bg-accent-rose/20 text-txt-danger hover:bg-accent-rose/30`
: btnDefault
}
title={isDeafened ? 'Undeafen (D)' : 'Deafen (D)'}
title={isServerDeafened ? 'Server Deafened' : isDeafened ? 'Undeafen (D)' : 'Deafen (D)'}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 3c-4.97 0-9 4.03-9 9v7c0 1.1.9 2 2 2h2v-7H5v-2c0-3.87 3.13-7 7-7s7 3.13 7 7v2h-2v7h2c1.1 0 2-.9 2-2v-7c0-4.97-4.03-9-9-9z" />
{isDeafened && <line x1="3" y1="3" x2="21" y2="21" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />}
{(isDeafened || isServerDeafened) && <line x1="3" y1="3" x2="21" y2="21" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />}
</svg>
</button>