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:
@@ -34,10 +34,13 @@ 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 navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const handleMicToggle = async () => {
|
||||
if (isServerMuted || isServerDeafened) return; // Blocked by server mute/deafen
|
||||
toggleMic();
|
||||
// Broadcast mute status via WebSocket so non-joined users can see it
|
||||
const willBeMuted = !isMuted;
|
||||
@@ -47,6 +50,7 @@ export function ChannelSidebar() {
|
||||
};
|
||||
|
||||
const handleDeafenToggle = async () => {
|
||||
if (isServerDeafened) return; // Blocked by server deafen
|
||||
const room = getActiveRoom();
|
||||
const willDeafen = !isDeafened;
|
||||
// Update store FIRST so updateParticipants reads correct state when LiveKit events fire
|
||||
@@ -84,7 +88,9 @@ export function ChannelSidebar() {
|
||||
if (inst) return inst.label;
|
||||
try { return new URL(origin).host; } catch { return origin; }
|
||||
}, [space, federationInstances]);
|
||||
const channelPermissions = useSpaceStore((s) => s.channelPermissions);
|
||||
const canManageChannels = hasPermissionBit(mySpacePerms, PermissionBits.MANAGE_CHANNELS);
|
||||
const canCreateInvite = hasPermissionBit(mySpacePerms, PermissionBits.CREATE_INVITE);
|
||||
|
||||
const textChannels = channels.filter(c => c.type === 'text');
|
||||
const voiceChannels = channels.filter(c => c.type === 'voice' || c.type === 'video');
|
||||
@@ -122,6 +128,8 @@ export function ChannelSidebar() {
|
||||
user={user}
|
||||
isMuted={isMuted}
|
||||
isDeafened={isDeafened}
|
||||
isServerMuted={isServerMuted}
|
||||
isServerDeafened={isServerDeafened}
|
||||
isAdmin={!!user.isAdmin}
|
||||
onMicToggle={handleMicToggle}
|
||||
onDeafenToggle={handleDeafenToggle}
|
||||
@@ -307,15 +315,17 @@ export function ChannelSidebar() {
|
||||
<path d="M5.293 7.293a1 1 0 011.414 0L9 9.586l2.293-2.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openModal('invite')}
|
||||
className="w-10 h-full flex items-center justify-center text-txt-tertiary hover:text-txt-primary hover:bg-interactive-hover transition-all flex-shrink-0"
|
||||
title="Invite People"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M21 3H24V5H21V8H19V5H16V3H19V0H21V3ZM10 12C12.21 12 14 10.21 14 8C14 5.79 12.21 4 10 4C7.79 4 6 5.79 6 8C6 10.21 7.79 12 10 12ZM10 13C6.69 13 1 14.66 1 18V20H19V18C19 14.66 13.31 13 10 13Z" />
|
||||
</svg>
|
||||
</button>
|
||||
{canCreateInvite && (
|
||||
<button
|
||||
onClick={() => openModal('invite')}
|
||||
className="w-10 h-full flex items-center justify-center text-txt-tertiary hover:text-txt-primary hover:bg-interactive-hover transition-all flex-shrink-0"
|
||||
title="Invite People"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M21 3H24V5H21V8H19V5H16V3H19V0H21V3ZM10 12C12.21 12 14 10.21 14 8C14 5.79 12.21 4 10 4C7.79 4 6 5.79 6 8C6 10.21 7.79 12 10 12ZM10 13C6.69 13 1 14.66 1 18V20H19V18C19 14.66 13.31 13 10 13Z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Channels */}
|
||||
@@ -419,14 +429,19 @@ export function ChannelSidebar() {
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-[2px]">
|
||||
{voiceChannels.map((channel) => (
|
||||
<VoiceChannel
|
||||
key={channel.id}
|
||||
channelId={channel.id}
|
||||
channelName={channel.name}
|
||||
onClick={() => handleVoiceJoin(channel.id)}
|
||||
/>
|
||||
))}
|
||||
{voiceChannels.map((channel) => {
|
||||
const chPerms = channelPermissions.get(channel.id);
|
||||
const canConnect = hasPermissionBit(chPerms, PermissionBits.CONNECT);
|
||||
return (
|
||||
<VoiceChannel
|
||||
key={channel.id}
|
||||
channelId={channel.id}
|
||||
channelName={channel.name}
|
||||
onClick={() => canConnect && handleVoiceJoin(channel.id)}
|
||||
locked={!canConnect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -444,6 +459,8 @@ function UserAreaPanel({
|
||||
user,
|
||||
isMuted,
|
||||
isDeafened,
|
||||
isServerMuted,
|
||||
isServerDeafened,
|
||||
isAdmin,
|
||||
onMicToggle,
|
||||
onDeafenToggle,
|
||||
@@ -453,6 +470,8 @@ function UserAreaPanel({
|
||||
user: any;
|
||||
isMuted: boolean;
|
||||
isDeafened: boolean;
|
||||
isServerMuted: boolean;
|
||||
isServerDeafened: boolean;
|
||||
isAdmin: boolean;
|
||||
onMicToggle: () => void;
|
||||
onDeafenToggle: () => void;
|
||||
@@ -769,14 +788,15 @@ function UserAreaPanel({
|
||||
<button
|
||||
onClick={onMicToggle}
|
||||
className={`w-8 h-8 flex items-center justify-center hover:bg-interactive-hover rounded-l-[4px] transition-colors ${
|
||||
isMuted ? 'text-txt-danger' : 'text-txt-tertiary hover:text-txt-primary'
|
||||
isServerMuted ? 'text-accent-amber cursor-not-allowed'
|
||||
: isMuted ? 'text-txt-danger' : 'text-txt-tertiary hover:text-txt-primary'
|
||||
}`}
|
||||
title={isMuted ? 'Unmute' : 'Mute'}
|
||||
title={isServerMuted ? 'Server Muted' : isMuted ? 'Unmute' : 'Mute'}
|
||||
>
|
||||
<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 && <line x1="3" y1="3" x2="21" y2="21" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />}
|
||||
{(isMuted || isServerMuted) && <line x1="3" y1="3" x2="21" y2="21" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />}
|
||||
</svg>
|
||||
</button>
|
||||
{/* Input chevron */}
|
||||
@@ -796,13 +816,14 @@ function UserAreaPanel({
|
||||
<button
|
||||
onClick={onDeafenToggle}
|
||||
className={`w-8 h-8 flex items-center justify-center hover:bg-interactive-hover rounded-l-[4px] transition-colors ${
|
||||
isDeafened ? 'text-txt-danger' : 'text-txt-tertiary hover:text-txt-primary'
|
||||
isServerDeafened ? 'text-accent-amber cursor-not-allowed'
|
||||
: isDeafened ? 'text-txt-danger' : 'text-txt-tertiary hover:text-txt-primary'
|
||||
}`}
|
||||
title={isDeafened ? 'Undeafen' : 'Deafen'}
|
||||
title={isServerDeafened ? 'Server Deafened' : isDeafened ? 'Undeafen' : 'Deafen'}
|
||||
>
|
||||
<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>
|
||||
{/* Output chevron */}
|
||||
|
||||
Reference in New Issue
Block a user