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,
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}
/>

View file

@ -129,6 +129,7 @@ export const useGame = (defaultGameConfig?: GameConfig) => {
const [hostSecret, setHostSecret] = useState<string | null>(null);
const [isReconnecting, setIsReconnecting] = useState(false);
const [presenterId, setPresenterId] = useState<string | null>(null);
const [connectedPlayerIds, setConnectedPlayerIds] = useState<string[]>([]);
const timerRef = 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;
}
// 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
};