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
+65
View File
@@ -167,6 +167,14 @@ function handleEvent(origin: string, event: ServerEvent): void {
setVoiceUserStatus(uid, status.isMuted, status.isDeafened, status.isCameraOn, status.isScreenSharing);
}
}
// Populate server mute/deafen states
if (event.serverVoiceStates) {
const { setServerMutedUser, setServerDeafenedUser } = useVoiceStore.getState();
for (const [uid, state] of Object.entries(event.serverVoiceStates as Record<string, { serverMuted: boolean; serverDeafened: boolean }>)) {
if (state.serverMuted) setServerMutedUser(uid, true);
if (state.serverDeafened) setServerDeafenedUser(uid, true);
}
}
// Re-register in voice channel after WS reconnect — the server lost
// voice state on restart, so we must tell it we're still connected.
@@ -265,6 +273,56 @@ function handleEvent(origin: string, event: ServerEvent): void {
setVoiceUserStatus(event.userId, event.isMuted, event.isDeafened, event.isCameraOn, event.isScreenSharing);
break;
case 'voice_server_muted': {
const { setServerMutedUser } = useVoiceStore.getState();
setServerMutedUser(event.userId, event.muted);
// If the local user was server-muted, force-mute the mic
const myUserId = useAuthStore.getState().user?.id;
if (event.userId === myUserId && event.muted) {
const vs = useVoiceStore.getState();
if (!vs.isMuted) {
vs.toggleMic();
const voiceOrigin = vs.currentVoiceChannelId ? getChannelOrigin(vs.currentVoiceChannelId) : '';
wsSend({ type: 'voice_status', isMuted: true, isDeafened: vs.isDeafened, isCameraOn: vs.isCameraOn, isScreenSharing: vs.isScreenSharing }, voiceOrigin);
}
}
break;
}
case 'voice_server_deafened': {
const { setServerDeafenedUser } = useVoiceStore.getState();
setServerDeafenedUser(event.userId, event.deafened);
// If the local user was server-deafened, force-deafen and force-mute
const myUid = useAuthStore.getState().user?.id;
if (event.userId === myUid && event.deafened) {
const vs = useVoiceStore.getState();
if (!vs.isDeafened) {
vs.toggleDeafen();
if (!vs.isMuted) vs.toggleMic();
const voiceOrigin = vs.currentVoiceChannelId ? getChannelOrigin(vs.currentVoiceChannelId) : '';
wsSend({ type: 'voice_status', isMuted: true, isDeafened: true, isCameraOn: vs.isCameraOn, isScreenSharing: vs.isScreenSharing }, voiceOrigin);
}
}
break;
}
case 'voice_moved': {
// The local user was moved to a different channel by a moderator
const myMovedId = useAuthStore.getState().user?.id;
if (event.userId === myMovedId) {
// Import dynamically to avoid circular deps — joinVoiceChannel handles
// leaving old channel, setting new channel, and triggering LiveKit reconnect
import('../utils/voice').then(({ joinVoiceChannel }) => {
// Force-set the channel (joinVoiceChannel skips if same channel)
const vs = useVoiceStore.getState();
// Clear current channel first so joinVoiceChannel doesn't bail
vs.setCurrentVoiceChannel(null);
joinVoiceChannel(event.newChannelId);
});
}
break;
}
case 'member_joined':
if (!isHome) normalizeUserAssets(event.member.user, origin);
addMember(event.member);
@@ -274,6 +332,13 @@ function handleEvent(origin: string, event: ServerEvent): void {
removeMember(event.userId);
break;
case 'member_banned': {
// The current user has been banned from a space — remove it from the sidebar
const { removeSpace: rmSpace } = useSpaceStore.getState();
rmSpace(event.spaceId);
break;
}
// ─── DM events (home-only) ──────────────────────────────────────────────
case 'dm_message_created': {