import React, { useState, useCallback } from 'react'; import { useVoiceStore } from '../../stores/voiceStore'; import { useSpaceStore, getChannelOrigin } from '../../stores/spaceStore'; import { useAuthStore } from '../../stores/authStore'; import { Avatar } from '../ui/Avatar'; import { VoiceModContextMenu } from './VoiceModContextMenu'; const EMPTY_VOICE_USERS: string[] = []; interface VoiceChannelProps { channelId: string; channelName: string; onClick: () => void; locked?: boolean; } export function VoiceChannel({ channelId, channelName, onClick, locked }: VoiceChannelProps) { const voiceUsers = useVoiceStore((s) => s.voiceUsers.get(channelId)) ?? EMPTY_VOICE_USERS; const currentVoiceChannel = useVoiceStore((s) => s.currentVoiceChannelId); const participants = useVoiceStore((s) => s.participants); const localIsDeafened = useVoiceStore((s) => s.isDeafened); const localIsMuted = useVoiceStore((s) => s.isMuted); const voiceUserStates = useVoiceStore((s) => s.voiceUserStates); const serverMutedUserIds = useVoiceStore((s) => s.serverMutedUserIds); const serverDeafenedUserIds = useVoiceStore((s) => s.serverDeafenedUserIds); const currentUserId = useVoiceStore((s) => { const local = s.participants.find(p => p.isLocal); 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; // Context menu state const [contextMenu, setContextMenu] = useState<{ x: number; y: number; userId: string } | null>(null); const handleContextMenu = useCallback( (e: React.MouseEvent, userId: string) => { if (userId === myUser?.id) return; e.preventDefault(); setContextMenu({ x: e.clientX, y: e.clientY, userId }); }, [myUser?.id], ); return (
{/* Connected users */} {voiceUsers.length > 0 && (
{voiceUsers.map((userId) => { const member = members.find(m => m.userId === userId); const participant = participants.find(p => p.userId === userId); const displayName = member?.user.displayName ?? member?.user.username ?? participant?.username ?? userId; const avatar = member?.user.avatar ?? null; const status = member?.user.status; const wsStatus = voiceUserStates.get(userId); const isParticipantDeafened = userId === currentUserId ? localIsDeafened : (participant?.isDeafened ?? wsStatus?.isDeafened ?? false); const isMuted = userId === currentUserId ? localIsMuted : (participant?.isMuted ?? wsStatus?.isMuted ?? false); const hasCamera = participant?.isCameraOn ?? wsStatus?.isCameraOn ?? false; const isScreenSharing = participant?.isScreenSharing ?? wsStatus?.isScreenSharing ?? false; const spaceId = channelToSpaceMap.get(channelId); const isServerMuted = serverMutedUserIds.has(`${spaceId}:${userId}`); const isServerDeafened = serverDeafenedUserIds.has(`${spaceId}:${userId}`); return (
handleContextMenu(e, userId)} > {displayName} {/* Status badges */}
{(isServerMuted || isServerDeafened) && ( )} {isServerDeafened && ( )} {!isServerMuted && !isServerDeafened && isMuted && ( )} {!isServerDeafened && isParticipantDeafened && ( )} {hasCamera && ( )} {isScreenSharing && ( LIVE )}
); })}
)} {/* Voice moderation context menu (portalled to body) */} {contextMenu && ( setContextMenu(null)} /> )}
); }