From 81d366286a8e00416518e6e591a01efd7b59358e Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Tue, 7 Oct 2025 18:21:09 -0700 Subject: [PATCH] Building but users still not listing --- client/src/Activities.tsx | 36 +++----- client/src/Chat.tsx | 46 +++------- client/src/ChooseCard.tsx | 26 ++---- client/src/GameOrder.tsx | 28 +----- client/src/Hand.tsx | 26 ++---- client/src/HouseRules.tsx | 27 ++---- client/src/MediaControl.tsx | 9 +- client/src/PingPong.tsx | 46 ---------- client/src/Placard.tsx | 16 +--- client/src/PlayerList.tsx | 71 +++++++-------- client/src/PlayersStatus.tsx | 26 ++---- client/src/SelectPlayer.tsx | 25 ++--- client/src/ViewCard.tsx | 25 ++--- client/src/Winner.tsx | 26 ++---- server/routes/games.ts | 172 +++++++++++++++++++++++++++++++++-- 15 files changed, 296 insertions(+), 309 deletions(-) delete mode 100644 client/src/PingPong.tsx diff --git a/client/src/Activities.tsx b/client/src/Activities.tsx index 6a0f480..447bf4a 100644 --- a/client/src/Activities.tsx +++ b/client/src/Activities.tsx @@ -87,7 +87,7 @@ const Activity: React.FC = ({ keep, activity }) => { }; const Activities: React.FC = () => { - const { ws, sendJsonMessage } = useContext(GlobalContext); + const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext); const [activities, setActivities] = useState([]); const [turn, setTurn] = useState(undefined); const [color, setColor] = useState(undefined); @@ -103,15 +103,16 @@ const Activities: React.FC = () => { } else { request = fields; } - ws?.send( - JSON.stringify({ - type: "get", - fields: request, - }) - ); + sendJsonMessage({ + type: "get", + fields: request, + }); }; - const onWsMessage = (event: MessageEvent) => { - const data = JSON.parse(event.data) as { type: string; update?: Record }; + useEffect(() => { + if (!lastJsonMessage) { + return; + } + const data = lastJsonMessage; switch (data.type) { case "game-update": { const ignoring: string[] = [], @@ -153,21 +154,8 @@ const Activities: React.FC = () => { default: break; } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { - refWsMessage.current = onWsMessage; - }); - useEffect(() => { - if (!ws) { - return; - } - const cbMessage = (e: MessageEvent) => refWsMessage.current(e); - ws.addEventListener("message", cbMessage as EventListener); - return () => { - ws.removeEventListener("message", cbMessage as EventListener); - }; - }, [ws, refWsMessage]); + }, [lastJsonMessage, activities, turn, players, timestamp, color, state, fields]); + useEffect(() => { if (!sendJsonMessage) { return; diff --git a/client/src/Chat.tsx b/client/src/Chat.tsx index 76fe1fb..264be35 100644 --- a/client/src/Chat.tsx +++ b/client/src/Chat.tsx @@ -3,7 +3,7 @@ import Paper from "@mui/material/Paper"; import List from "@mui/material/List"; import ListItem from "@mui/material/ListItem"; import ListItemText from "@mui/material/ListItemText"; -import { formatDistanceToNow, formatDuration, intervalToDuration } from 'date-fns'; +import { formatDistanceToNow, formatDuration, intervalToDuration } from "date-fns"; import TextField from "@mui/material/TextField"; import equal from "fast-deep-equal"; @@ -34,10 +34,13 @@ const Chat: React.FC = () => { return () => clearInterval(timer); }, []); - const { ws, name, sendJsonMessage } = useContext(GlobalContext); + const { lastJsonMessage, name, sendJsonMessage } = useContext(GlobalContext); const fields = useMemo(() => ["chat", "startTime"], []); - const onWsMessage = (event: MessageEvent) => { - const data = JSON.parse(event.data); + useEffect(() => { + if (!lastJsonMessage) { + return; + } + const data = lastJsonMessage; switch (data.type) { case "game-update": console.log(`chat - game update`); @@ -52,30 +55,13 @@ const Chat: React.FC = () => { default: break; } - }; - const refWsMessage = useRef(onWsMessage); + }, [lastJsonMessage, chat, startTime]); useEffect(() => { - refWsMessage.current = onWsMessage; - }); - useEffect(() => { - if (!ws) { - return; - } - const cbMessage = (e: MessageEvent) => refWsMessage.current(e); - ws.addEventListener("message", cbMessage); - return () => { - ws.removeEventListener("message", cbMessage); - }; - }, [ws, refWsMessage]); - useEffect(() => { - if (!ws) { - return; - } sendJsonMessage({ type: "get", fields, }); - }, [ws, fields, sendJsonMessage]); + }, [fields, sendJsonMessage]); const chatKeyPress = useCallback( (event: React.KeyboardEvent) => { @@ -84,13 +70,11 @@ const Chat: React.FC = () => { setAutoScroll(true); } - if (ws) { - sendJsonMessage({ type: "chat", message: (event.target as HTMLInputElement).value }); - (event.target as HTMLInputElement).value = ""; - } + sendJsonMessage({ type: "chat", message: (event.target as HTMLInputElement).value }); + (event.target as HTMLInputElement).value = ""; } }, - [ws, setAutoScroll, autoScroll] + [setAutoScroll, autoScroll] ); const chatScroll = (event: React.UIEvent) => { @@ -217,9 +201,7 @@ const Chat: React.FC = () => { {item.color && } now ? now : item.date)) - } + secondary={item.color && formatDistanceToNow(new Date(item.date > now ? now : item.date))} /> ); @@ -248,7 +230,7 @@ const Chat: React.FC = () => { const hours = duration.hours || 0; const minutes = duration.minutes || 0; const seconds = duration.seconds || 0; - return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; })()} ) diff --git a/client/src/ChooseCard.tsx b/client/src/ChooseCard.tsx index 118ff75..01a62e4 100644 --- a/client/src/ChooseCard.tsx +++ b/client/src/ChooseCard.tsx @@ -12,15 +12,19 @@ import { GlobalContext } from "./GlobalContext"; /* eslint-disable @typescript-eslint/no-explicit-any */ const ChooseCard: React.FC = () => { - const { ws, sendJsonMessage } = useContext(GlobalContext); + const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext); const [turn, setTurn] = useState(undefined); const [color, setColor] = useState(undefined); const [state, setState] = useState(undefined); const [cards, setCards] = useState([]); const fields = useMemo(() => ["turn", "color", "state"], []); - const onWsMessage = (event: MessageEvent) => { - const data = JSON.parse(event.data); + useEffect(() => { + if (!lastJsonMessage) { + return; + } + + const data = lastJsonMessage; switch (data.type) { case "game-update": console.log(`choose-card - game-update: `, data.update); @@ -37,21 +41,7 @@ const ChooseCard: React.FC = () => { default: break; } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { - refWsMessage.current = onWsMessage; - }); - useEffect(() => { - if (!ws) { - return; - } - const cbMessage = (e: MessageEvent) => refWsMessage.current(e); - ws.addEventListener("message", cbMessage); - return () => { - ws.removeEventListener("message", cbMessage); - }; - }, [ws, refWsMessage]); + }, [lastJsonMessage, turn, color, state]); useEffect(() => { if (!sendJsonMessage) { return; diff --git a/client/src/GameOrder.tsx b/client/src/GameOrder.tsx index 44cd845..0f04986 100644 --- a/client/src/GameOrder.tsx +++ b/client/src/GameOrder.tsx @@ -21,13 +21,13 @@ interface PlayerItem { } const GameOrder: React.FC = () => { - const { ws, sendJsonMessage } = useContext(GlobalContext); + const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext); const [players, setPlayers] = useState<{ [key: string]: any }>({}); const [color, setColor] = useState(undefined); const fields = useMemo(() => ["players", "color"], []); - const onWsMessage = (event: MessageEvent) => { - const data = JSON.parse(event.data); + useEffect(() => { + const data = lastJsonMessage; switch (data.type) { case "game-update": console.log(`GameOrder game-update: `, data.update); @@ -41,21 +41,7 @@ const GameOrder: React.FC = () => { default: break; } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { - refWsMessage.current = onWsMessage; - }); - useEffect(() => { - if (!ws) { - return; - } - const cbMessage = (e: MessageEvent) => refWsMessage.current(e); - ws.addEventListener("message", cbMessage); - return () => { - ws.removeEventListener("message", cbMessage); - }; - }, [ws, refWsMessage]); + }, [lastJsonMessage, players, color]); useEffect(() => { if (!sendJsonMessage) { return; @@ -66,12 +52,8 @@ const GameOrder: React.FC = () => { }); }, [sendJsonMessage, fields]); - const sendMessage = (data: any) => { - ws!.send(JSON.stringify(data)); - }; - const rollClick = () => { - sendMessage({ type: "roll" }); + sendJsonMessage({ type: "roll" }); }; let hasRolled = true; diff --git a/client/src/Hand.tsx b/client/src/Hand.tsx index 55a83ee..66fec70 100644 --- a/client/src/Hand.tsx +++ b/client/src/Hand.tsx @@ -34,7 +34,7 @@ interface HandProps { } const Hand: React.FC = ({ buildActive, setBuildActive, setCardActive }) => { - const { ws, sendJsonMessage } = useContext(GlobalContext); + const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext); const [priv, setPriv] = useState(undefined); const [color, setColor] = useState(undefined); const [turn, setTurn] = useState(undefined); @@ -49,8 +49,12 @@ const Hand: React.FC = ({ buildActive, setBuildActive, setCardActive () => ["private", "turn", "color", "longestRoad", "largestArmy", "mostPorts", "mostDeveloped"], [] ); - const onWsMessage = (event: MessageEvent) => { - const data = JSON.parse(event.data); + useEffect(() => { + if (!lastJsonMessage) { + return; + } + + const data = lastJsonMessage; switch (data.type) { case "game-update": console.log(`hand - game-update: `, data.update); @@ -79,21 +83,7 @@ const Hand: React.FC = ({ buildActive, setBuildActive, setCardActive default: break; } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { - refWsMessage.current = onWsMessage; - }); - useEffect(() => { - if (!ws) { - return; - } - const cbMessage = (e: MessageEvent) => refWsMessage.current(e); - ws.addEventListener("message", cbMessage); - return () => { - ws.removeEventListener("message", cbMessage); - }; - }, [ws, refWsMessage]); + }, [lastJsonMessage, priv, turn, color, longestRoad, largestArmy, mostPorts, mostDeveloped]); useEffect(() => { if (!sendJsonMessage) { return; diff --git a/client/src/HouseRules.tsx b/client/src/HouseRules.tsx index 5045611..596db0a 100644 --- a/client/src/HouseRules.tsx +++ b/client/src/HouseRules.tsx @@ -247,14 +247,17 @@ interface HouseRulesProps { } const HouseRules: React.FC = ({ houseRulesActive, setHouseRulesActive }) => { - const { ws, name, sendJsonMessage } = useContext(GlobalContext); + const { lastJsonMessage, name, sendJsonMessage } = useContext(GlobalContext); const [rules, setRules] = useState({}); const [state, setState] = useState({}); const [gameState, setGameState] = useState(""); const fields = useMemo(() => ["state", "rules"], []); - const onWsMessage = (event: MessageEvent) => { - const data = JSON.parse(event.data); + useEffect(() => { + if (!lastJsonMessage) { + return; + } + const data = lastJsonMessage; switch (data.type) { case "game-update": console.log(`house-rules - game-update: `, data.update); @@ -268,21 +271,7 @@ const HouseRules: React.FC = ({ houseRulesActive, setHouseRules default: break; } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { - refWsMessage.current = onWsMessage; - }); - useEffect(() => { - if (!ws) { - return; - } - const cbMessage = (e: MessageEvent) => refWsMessage.current(e); - ws.addEventListener("message", cbMessage); - return () => { - ws.removeEventListener("message", cbMessage); - }; - }, [ws, refWsMessage]); + }, [lastJsonMessage, rules, gameState]); useEffect(() => { if (!sendJsonMessage) { return; @@ -477,7 +466,7 @@ const HouseRules: React.FC = ({ houseRulesActive, setHouseRules ), }, ].sort((a, b) => a.category.localeCompare(b.category)), - [rules, setRules, state, ws, setRule, name, gameState] + [rules, setRules, state, setRule, name, gameState] ); if (!houseRulesActive) { diff --git a/client/src/MediaControl.tsx b/client/src/MediaControl.tsx index 4ccc53d..957603d 100644 --- a/client/src/MediaControl.tsx +++ b/client/src/MediaControl.tsx @@ -1123,9 +1123,14 @@ const MediaAgent = (props: MediaAgentProps) => { if (media && joinStatus.status === "Not joined" && readyState === ReadyState.OPEN) { console.log(`media-agent - Initiating media join for ${session.name}`); setJoinStatus({ status: "Joining" }); - sendJsonMessage({ type: "join", data: {} }); + sendJsonMessage({ + type: "join", + data: { + has_media: session.has_media !== false, // Default to true for backward compatibility + } + }); } - }, [media, joinStatus.status, sendJsonMessage, readyState, session.name]); + }, [media, joinStatus.status, sendJsonMessage, readyState, session.name, session.has_media]); // Update local peer in peers list useEffect(() => { diff --git a/client/src/PingPong.tsx b/client/src/PingPong.tsx deleted file mode 100644 index da4c3db..0000000 --- a/client/src/PingPong.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { useState, useContext, useEffect, useRef } from "react"; -import { GlobalContext } from "./GlobalContext"; -import "./PingPong.css"; - -const PingPong: React.FC = () => { - const [count, setCount] = useState(0); - const global = useContext(GlobalContext); - - const onWsMessage = (event: MessageEvent) => { - const data = JSON.parse(event.data as string); - switch (data.type) { - case "ping": - if (global.ws) { - global.ws.send(JSON.stringify({ type: "pong", timestamp: data.ping })); - } - setCount(count + 1); - break; - default: - break; - } - }; - const refWsMessage = useRef(onWsMessage); - - useEffect(() => { - refWsMessage.current = onWsMessage; - }); - - useEffect(() => { - if (!global.ws) { - return; - } - const cbMessage = (e: MessageEvent) => refWsMessage.current(e); - global.ws.addEventListener("message", cbMessage); - return () => { - global.ws.removeEventListener("message", cbMessage); - }; - }, [global.ws, refWsMessage]); - - return ( -
- Game {global.gameId}: {global.name} {global.ws ? "has socket" : "no socket"} {count} pings -
- ); -}; - -export { PingPong }; diff --git a/client/src/Placard.tsx b/client/src/Placard.tsx index 851d0bf..adad631 100644 --- a/client/src/Placard.tsx +++ b/client/src/Placard.tsx @@ -16,13 +16,7 @@ type PlacardProps = { }; const Placard: React.FC = ({ type, disabled, count, buildActive, setBuildActive, className, sx }) => { - const { ws, sendJsonMessage } = useContext(GlobalContext); - const sendMessage = useCallback( - (data: Record) => { - sendJsonMessage(data); - }, - [sendJsonMessage] - ); + const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext); const dismissClicked = () => { setBuildActive && setBuildActive(false); @@ -37,19 +31,19 @@ const Placard: React.FC = ({ type, disabled, count, buildActive, s }; const roadClicked = () => { - sendMessage({ type: "buy-road" }); + sendJsonMessage({ type: "buy-road" }); setBuildActive && setBuildActive(false); }; const settlementClicked = () => { - sendMessage({ type: "buy-settlement" }); + sendJsonMessage({ type: "buy-settlement" }); setBuildActive && setBuildActive(false); }; const cityClicked = () => { - sendMessage({ type: "buy-city" }); + sendJsonMessage({ type: "buy-city" }); setBuildActive && setBuildActive(false); }; const developmentClicked = () => { - sendMessage({ type: "buy-development" }); + sendJsonMessage({ type: "buy-development" }); setBuildActive && setBuildActive(false); }; diff --git a/client/src/PlayerList.tsx b/client/src/PlayerList.tsx index cdecb45..d9b5255 100644 --- a/client/src/PlayerList.tsx +++ b/client/src/PlayerList.tsx @@ -60,38 +60,35 @@ const PlayerList: React.FC = () => { } const data: any = lastJsonMessage; switch (data.type) { - case "players": { - type RoomStateData = { - participants: Player[]; - }; - const room_state = data as RoomStateData; - console.log(`Players - room_state`, room_state.participants); - room_state.participants.forEach((player) => { - player.local = player.session_id === session.id; - }); - room_state.participants.sort(sortPlayers); - setPlayers(room_state.participants); - // Initialize peers with remote mute/video state - setPeers((prevPeers) => { - const updated: Record = { ...prevPeers }; - room_state.participants.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, - }; - } + case "game-update": { + console.log(`PlayerList - game-update:`, data.update); + + // Handle participants list + if ("participants" in data.update && data.update.participants) { + const participantsList: Player[] = data.update.participants; + console.log(`PlayerList - participants:`, participantsList); + + participantsList.forEach((player) => { + player.local = player.session_id === session?.id; + }); + participantsList.sort(sortPlayers); + 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; }); - return updated; - }); - break; - } - case "update_name": { - // Update local session name immediately - if (data && typeof data.name === "string") { - session.name = data.name; } break; } @@ -99,12 +96,12 @@ const PlayerList: React.FC = () => { // Update peer state in peers, but do not override local mute setPeers((prevPeers) => { const updated = { ...prevPeers }; - const peerId = data.peer_id; + const peerId = data.data?.peer_id || data.peer_id; if (peerId && updated[peerId]) { updated[peerId] = { ...updated[peerId], - muted: data.muted, - video_on: data.video_on, + muted: data.data?.muted ?? data.muted, + video_on: data.data?.video_on ?? data.video_on, }; } return updated; @@ -117,11 +114,13 @@ const PlayerList: React.FC = () => { }, [lastJsonMessage, session, sortPlayers, setPeers, setPlayers]); useEffect(() => { - if (players !== null) { + if (players !== null || !sendJsonMessage) { return; } + // Request participants list sendJsonMessage({ - type: "list_Players", + type: "get", + fields: ["participants"], }); }, [players, sendJsonMessage]); diff --git a/client/src/PlayersStatus.tsx b/client/src/PlayersStatus.tsx index 35bfa20..ce22856 100644 --- a/client/src/PlayersStatus.tsx +++ b/client/src/PlayersStatus.tsx @@ -133,7 +133,7 @@ interface PlayersStatusProps { } const PlayersStatus: React.FC = ({ active }) => { - const { ws, sendJsonMessage } = useContext(GlobalContext); + const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext); const [players, setPlayers] = useState(undefined); const [color, setColor] = useState(undefined); const [largestArmy, setLargestArmy] = useState(undefined); @@ -141,8 +141,12 @@ const PlayersStatus: React.FC = ({ active }) => { const [mostPorts, setMostPorts] = useState(undefined); const [mostDeveloped, setMostDeveloped] = useState(undefined); const fields = useMemo(() => ["players", "color", "longestRoad", "largestArmy", "mostPorts", "mostDeveloped"], []); - const onWsMessage = (event: MessageEvent) => { - const data = JSON.parse(event.data); + useEffect(() => { + if (!lastJsonMessage) { + return; + } + + const data = lastJsonMessage; switch (data.type) { case "game-update": console.log(`players-status - game-update: `, data.update); @@ -168,21 +172,7 @@ const PlayersStatus: React.FC = ({ active }) => { default: break; } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { - refWsMessage.current = onWsMessage; - }); - useEffect(() => { - if (!ws) { - return; - } - const cbMessage = (e: MessageEvent) => refWsMessage.current(e); - ws.addEventListener("message", cbMessage); - return () => { - ws.removeEventListener("message", cbMessage); - }; - }, [ws, refWsMessage]); + }, [lastJsonMessage, players, color, longestRoad, largestArmy, mostPorts, mostDeveloped]); useEffect(() => { if (!sendJsonMessage) { return; diff --git a/client/src/SelectPlayer.tsx b/client/src/SelectPlayer.tsx index 40dd09f..f8fe849 100644 --- a/client/src/SelectPlayer.tsx +++ b/client/src/SelectPlayer.tsx @@ -11,13 +11,16 @@ import { GlobalContext } from "./GlobalContext"; /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ const SelectPlayer: React.FC = () => { - const { ws, sendJsonMessage } = useContext(GlobalContext); + const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext); const [turn, setTurn] = useState(undefined); const [color, setColor] = useState(undefined); const fields = useMemo(() => ["turn", "color"], []); - const onWsMessage = (event: MessageEvent) => { - const data = JSON.parse(event.data); + useEffect(() => { + if (!lastJsonMessage) { + return; + } + const data = lastJsonMessage; switch (data.type) { case "game-update": console.log(`select-players - game-update: `, data.update); @@ -31,21 +34,7 @@ const SelectPlayer: React.FC = () => { default: break; } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { - refWsMessage.current = onWsMessage; - }); - useEffect(() => { - if (!ws) { - return; - } - const cbMessage = (e: MessageEvent) => refWsMessage.current(e); - ws.addEventListener("message", cbMessage); - return () => { - ws.removeEventListener("message", cbMessage); - }; - }, [ws, refWsMessage]); + }, [lastJsonMessage, turn, color]); useEffect(() => { if (!sendJsonMessage) { return; diff --git a/client/src/ViewCard.tsx b/client/src/ViewCard.tsx index 4c010da..75eb14f 100644 --- a/client/src/ViewCard.tsx +++ b/client/src/ViewCard.tsx @@ -16,13 +16,16 @@ interface ViewCardProps { } const ViewCard: React.FC = ({ cardActive, setCardActive }) => { - const { ws, sendJsonMessage } = useContext(GlobalContext); + const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext); const [priv, setPriv] = useState(undefined); const [turns, setTurns] = useState(0); const [rules, setRules] = useState({}); const fields = useMemo(() => ["private", "turns", "rules"], []); - const onWsMessage = (event: MessageEvent) => { - const data = JSON.parse(event.data as string); + useEffect(() => { + if (!lastJsonMessage) { + return; + } + const data = lastJsonMessage; switch (data.type) { case "game-update": console.log(`view-card - game update`); @@ -39,21 +42,7 @@ const ViewCard: React.FC = ({ cardActive, setCardActive }) => { default: break; } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { - refWsMessage.current = onWsMessage; - }); - useEffect(() => { - if (!ws) { - return; - } - const cbMessage = (e: MessageEvent) => refWsMessage.current(e); - ws.addEventListener("message", cbMessage); - return () => { - ws.removeEventListener("message", cbMessage); - }; - }, [ws, refWsMessage]); + }, [lastJsonMessage, priv, turns, rules]); useEffect(() => { if (!sendJsonMessage) { return; diff --git a/client/src/Winner.tsx b/client/src/Winner.tsx index b343619..b2957d6 100644 --- a/client/src/Winner.tsx +++ b/client/src/Winner.tsx @@ -17,12 +17,16 @@ interface WinnerProps { /* eslint-disable @typescript-eslint/no-explicit-any */ const Winner: React.FC = ({ winnerDismissed, setWinnerDismissed }) => { - const { ws, sendJsonMessage } = useContext(GlobalContext); + const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext); const [winner, setWinner] = useState(undefined); const [state, setState] = useState(undefined); const fields = useMemo(() => ["winner", "state"], []); - const onWsMessage = (event: MessageEvent) => { - const data: { type: string; update: any } = JSON.parse(event.data); + useEffect(() => { + if (!lastJsonMessage) { + return; + } + + const data = lastJsonMessage; switch (data.type) { case "game-update": console.log(`winner - game update`, data.update); @@ -40,21 +44,7 @@ const Winner: React.FC = ({ winnerDismissed, setWinnerDismissed }) default: break; } - }; - const refWsMessage = useRef(onWsMessage); - useEffect(() => { - refWsMessage.current = onWsMessage; - }); - useEffect(() => { - if (!ws) { - return; - } - const cbMessage = (e: MessageEvent) => refWsMessage.current(e); - ws.addEventListener("message", cbMessage); - return () => { - ws.removeEventListener("message", cbMessage); - }; - }, [ws, refWsMessage]); + }, [lastJsonMessage, winner, state, setWinnerDismissed]); useEffect(() => { if (!sendJsonMessage) { return; diff --git a/server/routes/games.ts b/server/routes/games.ts index 32f8576..84c1529 100755 --- a/server/routes/games.ts +++ b/server/routes/games.ts @@ -1163,6 +1163,7 @@ const setPlayerName = (game: Game, session: Session, name: string): string | und }); sendUpdateToPlayers(game, { players: getFilteredPlayers(game), + participants: getParticipants(game), unselected: getFilteredUnselected(game), chat: game.chat, }); @@ -1282,6 +1283,7 @@ const setPlayerColor = (game: Game, session: Session, color: string): string | u const update: any = { players: getFilteredPlayers(game), + participants: getParticipants(game), chat: game.chat, }; @@ -3407,19 +3409,79 @@ const resetDisconnectCheck = (_game: any, req: any): void => { //req.disconnectCheck = setTimeout(() => { wsInactive(game, req) }, 20000); }; -const join = (peers: any, session: any, { hasVideo, hasAudio }: { hasVideo?: boolean; hasAudio?: boolean }): void => { +const join = (peers: any, session: any, { hasVideo, hasAudio, has_media }: { hasVideo?: boolean; hasAudio?: boolean; has_media?: boolean }): void => { const ws = session.ws; if (!session.name) { console.error(`${session.id}: <- join - No name set yet. Audio not available.`); + ws.send(JSON.stringify({ + type: "join_status", + status: "Error", + message: "No name set yet. Audio not available." + })); return; } console.log(`${session.id}: <- join - ${session.name}`); console.log(`${all}: -> addPeer - ${session.name}`); + // Determine media capability - prefer has_media if provided, otherwise derive from hasVideo/hasAudio + const peerHasMedia = has_media !== undefined ? has_media : (hasVideo || hasAudio); + if (session.name in peers) { - console.log(`${session.id}:${session.name} - Already joined to Audio.`); + console.log(`${session.id}:${session.name} - Already joined to Audio, updating WebSocket reference.`); + + // Update the WebSocket reference in case of reconnection + peers[session.name].ws = ws; + peers[session.name].has_media = peerHasMedia; + peers[session.name].hasAudio = hasAudio; + peers[session.name].hasVideo = hasVideo; + + // Send join status to reconnected client + ws.send(JSON.stringify({ + type: "join_status", + status: "Joined", + message: "Reconnected" + })); + + // Notify the reconnected client about all existing peers + for (const peer in peers) { + if (peer === session.name) continue; // Skip self + + ws.send( + JSON.stringify({ + type: "addPeer", + data: { + peer_id: peer, + peer_name: peer, + has_media: peers[peer].has_media, + should_create_offer: true, + hasAudio: peers[peer].hasAudio, + hasVideo: peers[peer].hasVideo, + }, + }) + ); + } + + // Notify all other peers about the reconnected peer (with updated connection) + for (const peer in peers) { + if (peer === session.name) continue; // Skip self + + peers[peer].ws.send( + JSON.stringify({ + type: "addPeer", + data: { + peer_id: session.name, + peer_name: session.name, + has_media: peerHasMedia, + should_create_offer: false, + hasAudio, + hasVideo, + }, + }) + ); + } + return; } @@ -3430,6 +3492,8 @@ const join = (peers: any, session: any, { hasVideo, hasAudio }: { hasVideo?: boo type: "addPeer", data: { peer_id: session.name, + peer_name: session.name, + has_media: peers[session.name]?.has_media ?? peerHasMedia, should_create_offer: false, hasAudio, hasVideo, @@ -3443,6 +3507,8 @@ const join = (peers: any, session: any, { hasVideo, hasAudio }: { hasVideo?: boo type: "addPeer", data: { peer_id: peer, + peer_name: peer, + has_media: peers[peer].has_media, should_create_offer: true, hasAudio: peers[peer].hasAudio, hasVideo: peers[peer].hasVideo, @@ -3456,7 +3522,15 @@ const join = (peers: any, session: any, { hasVideo, hasAudio }: { hasVideo?: boo ws, hasAudio, hasVideo, + has_media: peerHasMedia, }; + + /* Send join success status */ + ws.send(JSON.stringify({ + type: "join_status", + status: "Joined", + message: "Successfully joined" + })); }; const part = (peers: any, session: any): void => { @@ -3483,13 +3557,19 @@ const part = (peers: any, session: any): void => { peers[peer].ws.send( JSON.stringify({ type: "removePeer", - data: { peer_id: session.name }, + data: { + peer_id: session.name, + peer_name: session.name + }, }) ); ws.send( JSON.stringify({ type: "removePeer", - data: { peer_id: session.name }, + data: { + peer_id: peer, + peer_name: peer + }, }) ); } @@ -3856,6 +3936,49 @@ const getFilteredPlayers = (game: any): Record => { return filtered; }; +/** + * Get participants list for the game room + * Uses the reusable room helper and adds game-specific data (color) + * + * This demonstrates how to extend the base participant list with app-specific data + */ +const getParticipants = (game: any): any[] => { + // Use the reusable room helper for base participant data + // If you were using the new architecture, this would be: + // import { getParticipants as getBaseParticipants } from './room/helpers'; + // const baseParticipants = getBaseParticipants(game.sessions); + + const participants: any[] = []; + for (let id in game.sessions) { + const session = game.sessions[id]; + if (!session) continue; + + // Base participant data (reusable across any application) + const baseParticipant = { + name: session.name || null, + session_id: session.id, + live: session.live || false, + protected: session.protected || false, + has_media: session.has_media !== false, + bot_run_id: session.bot_run_id || null, + bot_provider_id: session.bot_provider_id || null, + bot_instance_id: session.bot_instance_id || null, + muted: session.muted || false, + video_on: session.video_on !== false, + }; + + // Game-specific data (in metadata layer) + // This is the ONLY game-specific code in this function + const gameSpecific = { + color: session.color || null, // Game-specific: player color + // In the new architecture, this would be: session.metadata?.color + }; + + participants.push({ ...baseParticipant, ...gameSpecific }); + } + return participants; +}; + const calculatePoints = (game: any, update: any): void => { if (game.state === "winner") { return; @@ -4225,6 +4348,16 @@ router.ws("/ws/:id", async (ws, req) => { // If there was a previous websocket and it's a different object, try to // close it to avoid stale sockets lingering in memory. if (previousWs && previousWs !== ws) { + // Clean up peer from audio registry before replacing WebSocket + if (gameId in audio) { + try { + part(audio[gameId], session); + console.log(`${short}: Cleaned up peer ${session.name} from audio registry during reconnection`); + } catch (e) { + console.warn(`${short}: Error cleaning up peer during reconnection:`, e); + } + } + try { previousWs.close(); } catch (e) { @@ -4282,7 +4415,11 @@ router.ws("/ws/:id", async (ws, req) => { message = JSON.stringify({ type: "iceCandidate", - data: { peer_id: getName(session), candidate: candidate }, + data: { + peer_id: getName(session), + peer_name: getName(session), + candidate: candidate + }, }) as any; if (peer_id in audio[gameId]) { @@ -4312,7 +4449,11 @@ router.ws("/ws/:id", async (ws, req) => { ); message = JSON.stringify({ type: "sessionDescription", - data: { peer_id: getName(session), session_description: session_description }, + data: { + peer_id: getName(session), + peer_name: getName(session), + session_description: session_description + }, }) as any; if (peer_id in audio[gameId]) { try { @@ -4350,7 +4491,12 @@ router.ws("/ws/:id", async (ws, req) => { const messagePayload = JSON.stringify({ type: "peer_state_update", - data: { peer_id: session.name, muted, video_on }, + data: { + peer_id: session.name, + peer_name: session.name, + muted, + video_on + }, }); // Send to all other peers @@ -4488,6 +4634,9 @@ router.ws("/ws/:id", async (ws, req) => { case "players": batchedUpdate.players = getFilteredPlayers(game); break; + case "participants": + batchedUpdate.participants = getParticipants(game); + break; case "color": console.log(`${session.id}: -> Returning color as ${session.color} for ${getName(session)}`); batchedUpdate.color = session.color; @@ -4771,6 +4920,7 @@ router.ws("/ws/:id", async (ws, req) => { if (session.name) { sendUpdateToPlayers(game, { players: getFilteredPlayers(game), + participants: getParticipants(game), unselected: getFilteredUnselected(game), }); } @@ -5326,7 +5476,13 @@ router.get("/", (req, res /*, next*/) => { // Mark this response as coming from the backend API to aid debugging res.setHeader("X-Backend", "games"); - return res.status(200).send({ player: playerId }); + return res.status(200).send({ + id: playerId, + player: playerId, + name: null, + lobbies: [], + has_media: true // Default to true for regular users + }); }); router.post("/:id?", async (req, res /*, next*/) => {