1
0
peddlers-of-ketran/client/src/PlayerList.tsx

266 lines
9.2 KiB
TypeScript

import React, { useState, useEffect, useCallback, useContext } 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";
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<Player[] | null>(null);
const [peers, setPeers] = useState<Record<string, Peer>>({});
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]
);
useEffect(() => {
if (!players) {
return;
}
players.forEach((player) => {
console.log("rabbit - player:", {
name: player.name,
live: player.live,
in_peers: peers[player.session_id],
local_or_media: player.local || player.has_media !== false,
});
});
}, [players]);
// 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);
// 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;
});
participantsList.sort(sortPlayers);
console.log(`player-list - sorted participants:`, participantsList);
setPlayers(participantsList);
// Initialize peers with remote mute/video state
setPeers((prevPeers) => {
const updated: Record<string, Peer> = { ...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]);
useEffect(() => {
if (players !== null || !sendJsonMessage) {
return;
}
// Request participants list
sendJsonMessage({
type: "get",
fields: ["participants"],
});
}, [players, sendJsonMessage]);
return (
<Box sx={{ position: "relative", width: "100%" }}>
<Paper
className={`player-list Medium`}
sx={{
maxWidth: { xs: "100%", sm: 500 },
p: { xs: 1, sm: 2 },
m: { xs: 0, sm: 2 },
}}
>
<MediaAgent {...{ session, peers, setPeers }} />
<List className="PlayerSelector">
{players?.map((player) => {
const peerObj = peers[player.session_id] || peers[player.name];
return (
<Box
key={player.session_id}
sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}
className={`PlayerEntry ${player.local ? "PlayerSelf" : ""}`}
>
<Box>
<Box style={{ display: "flex-wrap", alignItems: "center", justifyContent: "space-between" }}>
<Box style={{ display: "flex-wrap", alignItems: "center" }}>
<div className="Name">{player.name ? player.name : player.session_id}</div>
{player.protected && (
<div
style={{ marginLeft: 8, fontSize: "0.8em", color: "#a00" }}
title="This name is protected with a password"
>
🔒
</div>
)}
{player.bot_instance_id && (
<div style={{ marginLeft: 8, fontSize: "0.8em", color: "#00a" }} title="This is a bot">
🤖
</div>
)}
</Box>
</Box>
{player.name && !player.live && <div className="NoNetwork"></div>}
</Box>
{player.name && player.live && peerObj && (player.local || player.has_media !== false) ? (
<>
<MediaControl
className="Medium"
key={player.session_id}
peer={peerObj}
isSelf={player.local}
sendJsonMessage={player.local ? sendJsonMessage : undefined}
remoteAudioMuted={peerObj?.muted}
remoteVideoOff={peerObj?.video_on === false}
/>
{/* If this is the local player and they haven't picked a color, show a picker */}
{player.local && player.color === "unassigned" && (
<div style={{ marginTop: 8, width: "100%" }}>
<div style={{ marginBottom: 6, fontSize: "0.9em" }}>Pick your color:</div>
<div style={{ display: "flex", gap: 8 }}>
{["orange", "red", "white", "blue"].map((c) => (
<Box
key={c}
sx={{
display: "flex",
alignItems: "center",
gap: 8,
padding: "6px 8px",
borderRadius: 6,
border: "1px solid #ccc",
background: "#fff",
cursor: sendJsonMessage ? "pointer" : "not-allowed",
}}
onClick={() => {
if (!sendJsonMessage) return;
sendJsonMessage({ type: "set", field: "color", value: c[0].toUpperCase() });
}}
>
<PlayerColor color={c} />
</Box>
))}
</div>
</div>
)}
</>
) : player.name && player.live && player.has_media === false ? (
<div
className="Video fade-in"
style={{
background: "#333",
color: "#fff",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
fontSize: "14px",
}}
>
💬 Chat Only
</div>
) : (
<video className="Video"></video>
)}
</Box>
);
})}
</List>
</Paper>
</Box>
);
};
export { PlayerList };