import React, { useCallback, useMemo } from 'react'; import { useVoiceStore } from '../../stores/voiceStore'; import { CallTimer } from './CallTimer'; import { useSpaceStore } from '../../stores/spaceStore'; import { useAuthStore } from '../../stores/authStore'; import { useContextMenuStore, type ContextMenuItem } from '../../stores/contextMenuStore'; import { buildVoiceModMenuItems, VolumeSliderItem } from './voiceMenuItems'; import { VoiceUserRow } from './VoiceUserRow'; const EMPTY_VOICE_USERS: string[] = []; interface VoiceChannelProps { channelId: string; channelName: string; onClick: () => void; locked?: boolean; canManage?: boolean; onSettingsClick?: () => void; voiceUserHandlers?: (userId: string, channelId: string) => { draggable: boolean; isBeingDragged: boolean; onDragStart: (e: React.DragEvent) => void; onDragEnd: (e: React.DragEvent) => void; }; dropZone?: { onDragOver: (e: React.DragEvent) => void; onDragEnter: (e: React.DragEvent) => void; onDragLeave: (e: React.DragEvent) => void; onDrop: (e: React.DragEvent) => void; isDragOver: boolean; isValidTarget: boolean; }; } /** Wrapper component for the volume slider so it can use hooks (useState). */ export function VoiceChannel({ channelId, channelName, onClick, locked, canManage, onSettingsClick, voiceUserHandlers, dropZone }: VoiceChannelProps) { // Present only while someone is in the channel; the server drops the room // when it empties, which is what makes the next call start from zero. const callStartedAt = useVoiceStore((s) => s.voiceRoomStarts.get(channelId)); const serverVoiceUsers = useVoiceStore((s) => s.voiceUsers.get(channelId)) ?? EMPTY_VOICE_USERS; const currentVoiceChannel = useVoiceStore((s) => s.currentVoiceChannelId); const participants = useVoiceStore((s) => s.participants); const isLiveKitConnected = useVoiceStore((s) => s.isLiveKitConnected); // For OUR channel: LiveKit participants are the single source of truth. // For other channels: use server-provided voiceUsers (only available source). const voiceUsers = useMemo(() => { if (currentVoiceChannel === channelId && isLiveKitConnected && participants.length > 0) { return [...new Set(participants.map(p => p.userId))]; } return serverVoiceUsers; }, [currentVoiceChannel, channelId, isLiveKitConnected, participants, serverVoiceUsers]); const localIsDeafened = useVoiceStore((s) => s.isDeafened); const localIsMuted = useVoiceStore((s) => s.isMuted); const voiceUserStates = useVoiceStore((s) => s.voiceUserStates); const spaceMutedUserIds = useVoiceStore((s) => s.spaceMutedUserIds); const spaceDeafenedUserIds = useVoiceStore((s) => s.spaceDeafenedUserIds); const permissionMutedUserIds = useVoiceStore((s) => s.permissionMutedUserIds); const participantMutes = useVoiceStore((s) => s.participantMutes); const unwatchedCameras = useVoiceStore((s) => s.unwatchedCameras); const speakingUserIds = useVoiceStore((s) => s.speakingUserIds); 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; const openContextMenu = useContextMenuStore((s) => s.open); const handleContextMenu = useCallback( (e: React.MouseEvent, userId: string) => { if (userId === myUser?.id) return; e.preventDefault(); e.stopPropagation(); // Build moderation items const modItems = buildVoiceModMenuItems(userId, channelId); const items: ContextMenuItem[] = [...modItems]; // Separator after mod items if (modItems.length > 0) { items.push({ key: 'mod-end-sep', type: 'separator' }); } // Mute User checkbox items.push({ key: 'mute-user', type: 'checkbox', label: 'Mute User', subscribe: useVoiceStore.subscribe, getChecked: () => useVoiceStore.getState().participantMutes.get(userId) ?? false, onChange: (checked) => useVoiceStore.getState().setParticipantMute(userId, checked), }); items.push({ key: 'vol-sep', type: 'separator' }); // Volume slider items.push({ key: 'volume', type: 'custom', render: () => React.createElement(VolumeSliderItem, { userId }), }); openContextMenu({ x: e.clientX, y: e.clientY }, items); }, [myUser?.id, channelId, openContextMenu], ); return (