import React, { useState, useEffect, useCallback, useContext, useMemo } from 'react'; import Paper from '@mui/material/Paper'; import List from '@mui/material/List'; import './PlayerList.css'; import { MediaControl, MediaAgent, Peer } from './MediaControl'; import { PlayerColor } from './PlayerColor'; import Box from '@mui/material/Box'; import { GlobalContext } from './GlobalContext'; import { styles } from './Styles'; type Player = { name: string; session_id: string; live: boolean; local: boolean /* Client side variable */; protected?: boolean; has_media?: boolean; // Whether this Player provides audio/video streams color?: string; bot_run_id?: string; bot_provider_id?: string; bot_instance_id?: string; // For bot instances muted?: boolean; video_on?: boolean; }; const PlayerList: React.FC = () => { const { session, socketUrl, lastJsonMessage, sendJsonMessage } = useContext(GlobalContext); const [players, setPlayers] = useState([]); const [player, setPlayer] = useState(null); const [peers, setPeers] = useState>({}); const [gameState, setGameState] = useState(undefined); useEffect(() => { console.log('player-list - Mounted - requesting fields'); if (sendJsonMessage) { sendJsonMessage({ type: 'get', fields: ['participants'], }); } }, [sendJsonMessage]); const sortPlayers = useCallback( (A: any, B: any) => { if (!session) { return 0; } /* active Player first */ if (A.name === session.name) { return -1; } if (B.name === session.name) { return +1; } /* Sort active Players first */ if (A.name && !B.name) { return -1; } if (B.name && !A.name) { return +1; } /* Otherwise, sort by color */ if (A.color && B.color) { return A.color.localeCompare(B.color); } return 0; }, [session] ); // Use the WebSocket hook for room events with automatic reconnection useEffect(() => { if (!lastJsonMessage) { return; } const data: any = lastJsonMessage; switch (data.type) { case 'game-update': { console.log(`player-list - game-update:`, data.update); // Track game state if provided if ('state' in data.update) { setGameState(data.update.state); } // Handle participants list if ('participants' in data.update && data.update.participants) { const participantsList: Player[] = data.update.participants; console.log(`player-list - participants:`, participantsList); participantsList.forEach(player => { player.local = player.session_id === session?.id; if (player.local) { setPlayer(player); } }); participantsList.sort(sortPlayers); console.log(`player-list - sorted participants:`, participantsList); setPlayers(participantsList); // Initialize peers with remote mute/video state setPeers(prevPeers => { const updated: Record = { ...prevPeers }; participantsList.forEach(player => { // Only update remote peers, never overwrite local peer object if (!player.local && updated[player.session_id]) { updated[player.session_id] = { ...updated[player.session_id], muted: player.muted ?? false, video_on: player.video_on ?? true, }; } }); return updated; }); } break; } case 'peer_state_update': { // Update peer state in peers, but do not override local mute setPeers(prevPeers => { const updated = { ...prevPeers }; const peerId = data.data?.peer_id || data.peer_id; if (peerId && updated[peerId]) { updated[peerId] = { ...updated[peerId], muted: data.data?.muted ?? data.muted, video_on: data.data?.video_on ?? data.video_on, }; } return updated; }); break; } default: // console.log(`player-list - ignoring message: ${data.type}`); break; } }, [lastJsonMessage, session, sortPlayers, setPeers, setPlayers]); // Compute which colors are already taken const availableColors = useMemo(() => { const assignedColors = new Set(players.filter(p => p.color !== 'unassigned').map(p => p.color)); return ['O', 'R', 'W', 'B'].filter(color => !assignedColors.has(color)); }, [players]); useEffect(() => { if (players.length !== 0 || !sendJsonMessage) { return; } // Request participants list sendJsonMessage({ type: 'get', fields: ['participants'], }); }, [players, sendJsonMessage]); return ( {players .filter(p => p.color && p.color !== 'unassigned') .map(player => { const peerObj = peers[player.session_id] || peers[player.name]; const playerStyle = player.color !== 'unassigned' ? styles[player.color] : {}; return ( {player.bot_instance_id && <>🤖} {player.name ? player.name : player.session_id} {player.name && !player.live && } {player.name && player.live ? ( ) : (
💬 Chat Only
)}
); })}
{gameState === 'lobby' && (
{player && player.color !== 'unassigned' ? 'Change' : 'Pick'} your color:
{availableColors.map(c => { return ( { sendJsonMessage({ type: 'set', field: 'color', value: c }); }} > ); })}
)} {players .filter(p => !p.color || p.color === 'unassigned') .map(player => { const peerObj = peers[player.session_id] || peers[player.name]; return ( {player.bot_instance_id && <>🤖} {player.name ? player.name : player.session_id} {player.name && !player.live && } {/* Show media control if available */} {player.name && player.live && peerObj && (player.local || player.has_media !== false) && ( )} ); })}
); }; export { PlayerList };