import { useEffect, useMemo, useRef, useState, useCallback } from 'react'; import { useSession } from '../context/SessionContext.jsx'; import { useSettingsNamespace } from '../settings/index.js'; import { useSocket } from '../context/SocketContext.jsx'; import NicknameForm from './NicknameForm.jsx'; function roleColors(role) { switch (role) { case 'admin': case 'lockdown': case 'lockdown-admin': return 'text-amber-300'; case 'spectator': return 'text-slate-400'; default: return 'text-sky-300'; } } function formatLabel(user, selfId) { if (!user) return ''; const base = user.nickname || user.socketId?.slice(0, 6) || 'unknown'; if (user.socketId && user.socketId === selfId) { return `${base} (you)`; } return base; } export default function UserListPanel({ hideNicknameForm = false, hideHeader = false, className = '', fillHeight = false }) { const { session, setNickname } = useSession(); const { value } = useSettingsNamespace('profile', { nickname: '' }); const lastSyncedSocketRef = useRef(null); const socket = useSocket(); const canSetNickname = session?.role !== 'spectator'; const users = session?.users ?? []; const selfId = session?.socketId || null; const isTurnsMode = session?.mode === 'turns'; const turnQueues = session?.turnQueues || {}; const roster = session?.roster || []; useEffect(() => { if (!canSetNickname) return; if (!session?.socketId) return; const nicknameInput = value.nickname || ''; if (!nicknameInput) return; if (session.socketId === lastSyncedSocketRef.current) return; const currentId = session.socketId; setNickname(nicknameInput).then(() => { lastSyncedSocketRef.current = currentId; }).catch(() => {}); }, [canSetNickname, session?.socketId, setNickname, value.nickname]); useEffect(() => { if (!socket) return undefined; const handleConnect = () => { if (!canSetNickname) return; const nick = (value.nickname || '').trim(); if (!nick) return; setNickname(nick).then(() => { lastSyncedSocketRef.current = session?.socketId || null; }).catch(() => {}); }; socket.on('connect', handleConnect); return () => socket.off('connect', handleConnect); }, [canSetNickname, setNickname, socket, value.nickname, session?.socketId]); const sorted = useMemo( () => [...users].sort((a, b) => { if (a.socketId === selfId) return -1; if (b.socketId === selfId) return 1; return (a.nickname || '').localeCompare(b.nickname || ''); }), [selfId, users], ); const rosterName = useCallback( (roverId) => roster.find((r) => String(r.id) === String(roverId))?.name || roverId, [roster], ); const lookupUser = useCallback( (socketId) => users.find((u) => u.socketId === socketId) || { socketId, nickname: null, role: null }, [users], ); const secondsRemaining = useCallback((deadline) => { if (!deadline) return null; const ms = deadline - Date.now(); if (ms <= 0) return 0; return Math.ceil(ms / 1000); }, []); const listClass = fillHeight ? 'flex-1 min-h-0 overflow-y-auto' : 'h-48 overflow-y-auto'; return (
{!hideNicknameForm && (
{!canSetNickname &&

Spectators cannot set nicknames.

}
)}
{!hideHeader && (
{isTurnsMode ? 'Turn queues' : 'Users'} {isTurnsMode ? Object.keys(turnQueues || {}).length : sorted.length}
)}
{isTurnsMode ? ( Object.keys(turnQueues || {}).length === 0 ? (

No turn queues yet.

) : ( Object.entries(turnQueues).map(([roverId, info]) => { const queue = info?.queue || []; const deadline = info?.deadline || null; const remaining = secondsRemaining(deadline); return (

{rosterName(roverId)}

{remaining != null && ( {remaining}s left )}
{queue.length === 0 ? (

No drivers queued.

) : (
{queue.map((socketId, idx) => { const user = lookupUser(socketId); const isCurrent = socketId === info?.current; const isAdmin = user.role === 'admin' || user.role === 'lockdown' || user.role === 'lockdown-admin'; return ( {formatLabel(user, selfId)} {isAdmin && } {isCurrent && now} ); })}
)}
); }) ) ) : sorted.length === 0 ? (

Waiting for users…

) : ( sorted.map((user) => { const isAdmin = user.role === 'admin' || user.role === 'lockdown' || user.role === 'lockdown-admin'; return (

{formatLabel(user, selfId)}

{user.roverId ? ( rover {user.roverId} ) : ( no rover )} {isAdmin && ( Admin )}
); }) )}
); }