Fix host disconnect

This commit is contained in:
Joey Yakimowich-Payne 2026-01-25 09:03:05 -07:00
commit 5d3c6320d9
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
2 changed files with 27 additions and 15 deletions

View file

@ -104,7 +104,8 @@ function App() {
setPresenterPlayer, setPresenterPlayer,
sendAdvance, sendAdvance,
kickPlayer, kickPlayer,
leaveGame leaveGame,
connectedPlayerIds
} = useGame(defaultConfig); } = useGame(defaultConfig);
const handleSaveQuiz = async () => { const handleSaveQuiz = async () => {
@ -386,7 +387,8 @@ function App() {
quizTitle={quiz.title} quizTitle={quiz.title}
currentQuestionIndex={currentQuestionIndex} currentQuestionIndex={currentQuestionIndex}
totalQuestions={quiz.questions.length} totalQuestions={quiz.questions.length}
playerCount={players.filter(p => p.id !== 'host').length} players={players}
connectedPlayerIds={connectedPlayerIds}
onResume={resumeGame} onResume={resumeGame}
onEndGame={endGame} onEndGame={endGame}
/> />

View file

@ -129,6 +129,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
const [hostSecret, setHostSecret] = useState<string | null>(null); const [hostSecret, setHostSecret] = useState<string | null>(null);
const [isReconnecting, setIsReconnecting] = useState(false); const [isReconnecting, setIsReconnecting] = useState(false);
const [presenterId, setPresenterId] = useState<string | null>(null); const [presenterId, setPresenterId] = useState<string | null>(null);
const [connectedPlayerIds, setConnectedPlayerIds] = useState<string[]>([]);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null); const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const syncTimerRef = useRef<ReturnType<typeof setInterval> | null>(null); const syncTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
@ -213,11 +214,19 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
return null; 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) { switch (gameState) {
case 'LANDING': case 'LANDING':
if (gamePin && location.pathname.startsWith('/play/')) { if (gamePin && location.pathname.startsWith('/play/')) {
return `/play/${gamePin}`; return `/play/${gamePin}`;
} }
// Don't navigate away from game URLs if we're in LANDING state (might be reconnecting)
if (isGameUrl) {
return null;
}
return '/'; return '/';
case 'CREATING': case 'CREATING':
case 'GENERATING': case 'GENERATING':
@ -236,6 +245,10 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
} }
return '/'; return '/';
case 'DISCONNECTED': case 'DISCONNECTED':
if (gamePin) {
return role === 'HOST' ? `/host/${gamePin}` : `/play/${gamePin}`;
}
return '/';
case 'WAITING_TO_REJOIN': case 'WAITING_TO_REJOIN':
if (gamePin) { if (gamePin) {
return `/play/${gamePin}`; return `/play/${gamePin}`;
@ -411,6 +424,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
}); });
conn.on('close', () => { conn.on('close', () => {
connectionsRef.current.delete(conn.peer); 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); setGamePin(session.pin);
setGameState('DISCONNECTED'); // Show loading state on disconnected screen setGameState('DISCONNECTED'); // Show loading state on disconnected screen
setCurrentPlayerName("Host"); setCurrentPlayerName("Host");
setConnectedPlayerIds([]); // Reset connected players - they will reconnect
const hostData = await fetchHostSession(session.pin, session.hostSecret); const hostData = await fetchHostSession(session.pin, session.hostSecret);
@ -546,24 +561,15 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
// Determine which state to restore to // Determine which state to restore to
const savedState = hostData.gameState; const savedState = hostData.gameState;
const restoredPlayers = hostData.players || [];
const allAnswered = restoredPlayers.length > 0 && restoredPlayers.every((p: Player) => p.lastAnswerCorrect !== null);
if (savedState === 'LOBBY') { if (savedState === 'LOBBY') {
setGameState('LOBBY'); setGameState('LOBBY');
} else if (savedState === 'PODIUM') { } else if (savedState === 'PODIUM') {
setGameState('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 { } 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'); setGameState('HOST_RECONNECTED');
} }
@ -853,6 +859,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
const payload = data.payload as { name: string; reconnect?: boolean; previousId?: string }; const payload = data.payload as { name: string; reconnect?: boolean; previousId?: string };
connectionsRef.current.set(conn.peer, conn); 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 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'); 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); updatedPlayers = playersRef.current.map(p => p.id === reconnectedPlayer.id ? { ...p, id: conn.peer } : p);
setPlayers(updatedPlayers); setPlayers(updatedPlayers);
assignedName = reconnectedPlayer.name; 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) { if (presenterIdRef.current === reconnectedPlayer.id) {
setPresenterId(conn.peer); setPresenterId(conn.peer);
} }
@ -1052,6 +1061,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
playersRef.current = updatedPlayers; playersRef.current = updatedPlayers;
setPlayers(updatedPlayers); setPlayers(updatedPlayers);
connectionsRef.current.delete(playerId); connectionsRef.current.delete(playerId);
setConnectedPlayerIds(prev => prev.filter(id => id !== playerId));
if (presenterIdRef.current === playerId) { if (presenterIdRef.current === playerId) {
const realPlayers = updatedPlayers.filter(p => p.id !== 'host'); const realPlayers = updatedPlayers.filter(p => p.id !== 'host');
@ -1718,7 +1728,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
return { return {
role, gameState, quiz, players, currentQuestionIndex, timeLeft, error, gamePin, hasAnswered, lastPointsEarned, lastAnswerCorrect, currentCorrectShape, selectedOption, currentPlayerScore, currentStreak, currentPlayerId, gameConfig, 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, startQuizGen, startManualCreation, cancelCreation, finalizeManualQuiz, loadSavedQuiz, joinGame, startGame: startHostGame, handleAnswer, nextQuestion, showScoreboard,
updateQuizFromEditor, startGameFromEditor, backFromEditor, endGame, attemptReconnect, goHomeFromDisconnected, resumeGame, setPresenterPlayer, sendAdvance, kickPlayer, leaveGame updateQuizFromEditor, startGameFromEditor, backFromEditor, endGame, attemptReconnect, goHomeFromDisconnected, resumeGame, setPresenterPlayer, sendAdvance, kickPlayer, leaveGame
}; };