From 5d3c6320d92c40613d2ce5d08df7d0268d6e61c7 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sun, 25 Jan 2026 09:03:05 -0700 Subject: [PATCH] Fix host disconnect --- App.tsx | 6 ++++-- hooks/useGame.ts | 36 +++++++++++++++++++++++------------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/App.tsx b/App.tsx index 503e242..0eff1fd 100644 --- a/App.tsx +++ b/App.tsx @@ -104,7 +104,8 @@ function App() { setPresenterPlayer, sendAdvance, kickPlayer, - leaveGame + leaveGame, + connectedPlayerIds } = useGame(defaultConfig); const handleSaveQuiz = async () => { @@ -386,7 +387,8 @@ function App() { quizTitle={quiz.title} currentQuestionIndex={currentQuestionIndex} totalQuestions={quiz.questions.length} - playerCount={players.filter(p => p.id !== 'host').length} + players={players} + connectedPlayerIds={connectedPlayerIds} onResume={resumeGame} onEndGame={endGame} /> diff --git a/hooks/useGame.ts b/hooks/useGame.ts index 03bd3b7..23b6b6f 100644 --- a/hooks/useGame.ts +++ b/hooks/useGame.ts @@ -129,6 +129,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => { const [hostSecret, setHostSecret] = useState(null); const [isReconnecting, setIsReconnecting] = useState(false); const [presenterId, setPresenterId] = useState(null); + const [connectedPlayerIds, setConnectedPlayerIds] = useState([]); const timerRef = useRef | null>(null); const syncTimerRef = useRef | null>(null); @@ -213,11 +214,19 @@ export const useGame = (defaultGameConfig?: GameConfig) => { return null; } + // Don't redirect away from game URLs during initial load - let initializeFromUrl handle it + const isGameUrl = /^\/host\/[A-Z0-9]+$/i.test(location.pathname) || + /^\/play\/[A-Z0-9]+$/i.test(location.pathname); + switch (gameState) { case 'LANDING': if (gamePin && location.pathname.startsWith('/play/')) { return `/play/${gamePin}`; } + // Don't navigate away from game URLs if we're in LANDING state (might be reconnecting) + if (isGameUrl) { + return null; + } return '/'; case 'CREATING': case 'GENERATING': @@ -236,6 +245,10 @@ export const useGame = (defaultGameConfig?: GameConfig) => { } return '/'; case 'DISCONNECTED': + if (gamePin) { + return role === 'HOST' ? `/host/${gamePin}` : `/play/${gamePin}`; + } + return '/'; case 'WAITING_TO_REJOIN': if (gamePin) { return `/play/${gamePin}`; @@ -411,6 +424,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => { }); conn.on('close', () => { connectionsRef.current.delete(conn.peer); + setConnectedPlayerIds(prev => prev.filter(id => id !== conn.peer)); }); }); @@ -504,6 +518,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => { setGamePin(session.pin); setGameState('DISCONNECTED'); // Show loading state on disconnected screen setCurrentPlayerName("Host"); + setConnectedPlayerIds([]); // Reset connected players - they will reconnect const hostData = await fetchHostSession(session.pin, session.hostSecret); @@ -546,24 +561,15 @@ export const useGame = (defaultGameConfig?: GameConfig) => { // Determine which state to restore to const savedState = hostData.gameState; - const restoredPlayers = hostData.players || []; - const allAnswered = restoredPlayers.length > 0 && restoredPlayers.every((p: Player) => p.lastAnswerCorrect !== null); if (savedState === 'LOBBY') { setGameState('LOBBY'); } else if (savedState === 'PODIUM') { setGameState('PODIUM'); - } else if (savedState === 'REVEAL') { - // Go directly to reveal screen - players may have already seen their results - setGameState('REVEAL'); - } else if (savedState === 'SCOREBOARD') { - // Go directly to scoreboard - setGameState('SCOREBOARD'); - } else if (savedState === 'QUESTION' && allAnswered) { - // All players answered while host was disconnected - go directly to reveal - setGameState('REVEAL'); } else { - // For QUESTION or COUNTDOWN states where not everyone answered, show HOST_RECONNECTED to let them resume + // For all mid-game states (QUESTION, COUNTDOWN, REVEAL, SCOREBOARD), + // show HOST_RECONNECTED first so host can wait for players to reconnect + // and then choose when to resume setGameState('HOST_RECONNECTED'); } @@ -853,6 +859,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => { const payload = data.payload as { name: string; reconnect?: boolean; previousId?: string }; connectionsRef.current.set(conn.peer, conn); + setConnectedPlayerIds(prev => prev.includes(conn.peer) ? prev : [...prev, conn.peer]); const existingByPreviousId = payload.previousId ? playersRef.current.find(p => p.id === payload.previousId) : null; const existingByName = playersRef.current.find(p => p.name === payload.name && p.id !== 'host'); @@ -899,6 +906,8 @@ export const useGame = (defaultGameConfig?: GameConfig) => { updatedPlayers = playersRef.current.map(p => p.id === reconnectedPlayer.id ? { ...p, id: conn.peer } : p); setPlayers(updatedPlayers); assignedName = reconnectedPlayer.name; + // Update connected player IDs - remove old ID, new ID already added above + setConnectedPlayerIds(prev => prev.filter(id => id !== reconnectedPlayer.id)); if (presenterIdRef.current === reconnectedPlayer.id) { setPresenterId(conn.peer); } @@ -1052,6 +1061,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => { playersRef.current = updatedPlayers; setPlayers(updatedPlayers); connectionsRef.current.delete(playerId); + setConnectedPlayerIds(prev => prev.filter(id => id !== playerId)); if (presenterIdRef.current === playerId) { const realPlayers = updatedPlayers.filter(p => p.id !== 'host'); @@ -1718,7 +1728,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => { return { role, gameState, quiz, players, currentQuestionIndex, timeLeft, error, gamePin, hasAnswered, lastPointsEarned, lastAnswerCorrect, currentCorrectShape, selectedOption, currentPlayerScore, currentStreak, currentPlayerId, gameConfig, - pendingQuizToSave, dismissSavePrompt, sourceQuizId, isReconnecting, currentPlayerName, presenterId, + pendingQuizToSave, dismissSavePrompt, sourceQuizId, isReconnecting, currentPlayerName, presenterId, connectedPlayerIds, startQuizGen, startManualCreation, cancelCreation, finalizeManualQuiz, loadSavedQuiz, joinGame, startGame: startHostGame, handleAnswer, nextQuestion, showScoreboard, updateQuizFromEditor, startGameFromEditor, backFromEditor, endGame, attemptReconnect, goHomeFromDisconnected, resumeGame, setPresenterPlayer, sendAdvance, kickPlayer, leaveGame };