Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
170a46d
FaB Bazaar single source decks
OotTheMonk Mar 22, 2026
debf5b5
Try to simplify gamestate handler code (SSE connection)
OotTheMonk Mar 22, 2026
370c8b0
Burger menu for mobile portrait view
OotTheMonk Mar 22, 2026
95a02a8
Clean-up mobile burger menu
PvtVoid Mar 22, 2026
9da5e43
Update draw/flick sound
PvtVoid Mar 22, 2026
c16da0a
Don't make the S key play any sound when a user shuffle his hand with…
PvtVoid Mar 23, 2026
8b10d6a
Update create game/refresh button
PvtVoid Mar 23, 2026
78b7784
Update index.scss
PvtVoid Mar 23, 2026
512a2d6
Update themes.ts
PvtVoid Mar 23, 2026
7b7a134
Update lobby inactive buttons
PvtVoid Mar 23, 2026
3cec208
Fix chat box replacing only the first occurence of Player 1 or Player…
PvtVoid Mar 23, 2026
c8031a6
More privateMessagingAPI clean-up
PvtVoid Mar 23, 2026
90e78a2
Try to reduce GetGameList request to DB with friendslist and blocked …
PvtVoid Mar 23, 2026
b3c3055
Try to fix Leave Game button not appearing....
PvtVoid Mar 23, 2026
1c48faa
Reverse selected deck order
PvtVoid Mar 23, 2026
a487c99
Manually close the SSE when you refresh/unload
OotTheMonk Mar 23, 2026
e974d19
Merge branch 'main' of https://github.com/Talishar/Talishar-FE
OotTheMonk Mar 23, 2026
18ac450
Temporarily remove chat typing updates to prevent spamming server wit…
OotTheMonk Mar 23, 2026
0f59e72
Client side rate limiting for lobby refresh
OotTheMonk Mar 23, 2026
95b82b5
Bring the dispatch interval way down
OotTheMonk Mar 23, 2026
70c1a18
Revert "Bring the dispatch interval way down"
OotTheMonk Mar 23, 2026
78fff1a
Revert "Client side rate limiting for lobby refresh"
OotTheMonk Mar 23, 2026
69f0c29
Temporarily disable inactivity timer
OotTheMonk Mar 23, 2026
57b2d70
Simplify game lobby code + fixes
OotTheMonk Mar 24, 2026
9d370ca
Revert "Temporarily disable inactivity timer"
PvtVoid Mar 24, 2026
55ae010
Revert "Try to fix Leave Game button not appearing...."
PvtVoid Mar 24, 2026
83d4e50
Replaces the old CheckTyping polling entirely with webshockets
PvtVoid Mar 24, 2026
b457684
Try to add spectator names again and without using DB/html calls
PvtVoid Mar 24, 2026
2a887f2
Update GameStateHandler.tsx
PvtVoid Mar 24, 2026
da9b25b
Fix trailing spaces on login form
PvtVoid Mar 24, 2026
81bf30b
REdesign the join button with hero image
PvtVoid Mar 24, 2026
9932a77
Update inactivity warning style
PvtVoid Mar 24, 2026
115595b
Fix join button on mobile
PvtVoid Mar 24, 2026
e0bb8ff
Update css for discord Release Notes
PvtVoid Mar 24, 2026
39f0ac3
Update RightColumn.module.css
PvtVoid Mar 24, 2026
b373040
Add an add under Create Game button
PvtVoid Mar 24, 2026
6f1237d
Remove unused console.log and update profile page css
PvtVoid Mar 25, 2026
42675e1
Add a remove Ads link
PvtVoid Mar 25, 2026
569ea7b
Remove useless ads code
PvtVoid Mar 25, 2026
d5f9fcd
TEst not working ad in EU
PvtVoid Mar 25, 2026
3bfd662
Revert "TEst not working ad in EU"
PvtVoid Mar 25, 2026
f888177
Update RightColumn.tsx
PvtVoid Mar 25, 2026
95a11a6
Test rightColumn ads
PvtVoid Mar 25, 2026
eab7ca9
Small fixes on ads
PvtVoid Mar 25, 2026
13deef4
min 250x250 ads
PvtVoid Mar 25, 2026
7752919
Fix Ad Play.tsx
PvtVoid Mar 25, 2026
c09224d
Another fix
PvtVoid Mar 25, 2026
f06178d
Update RightColumn.module.css
PvtVoid Mar 25, 2026
0d86f29
TEst not working ad in EU
PvtVoid Mar 25, 2026
81474f4
Merge branch 'main' of https://github.com/Talishar/Talishar-FE
PvtVoid Mar 25, 2026
4ccde41
Revert "TEst not working ad in EU"
PvtVoid Mar 25, 2026
f9bf541
Remove right column ad
PvtVoid Mar 25, 2026
49c809c
Try to fix 2+ copies played from hand playing the proper one
PvtVoid Mar 25, 2026
194691c
Pull all the wires on Player Activity Status
PvtVoid Mar 26, 2026
730775b
FaB Bazaar single source decks
OotTheMonk Mar 22, 2026
90f0477
Merge branch 'fab-bazaar-deck-single-source' of https://github.com/Ta…
OotTheMonk Mar 26, 2026
65382ac
FaB Bazaar deck integration testing
OotTheMonk Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions __previewjs__/Wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { render } from '@testing-library/react';
import type { RenderOptions } from '@testing-library/react';
import type { PreloadedState } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import { AppStore, RootState, setupStore } from '../src/app/Store';
import { AppStore, RootState, globalInitialState, setupStore } from '../src/app/Store';
import { OfflineTestingGameState } from '../src/features/game/InitialGameState';
import '../src/index.css';
import { MemoryRouter } from 'react-router-dom';
Expand All @@ -20,7 +20,7 @@ export const Wrapper: React.FC<ExtendedRenderOptions> = ({
store
}) => {
//console.log(OfflineTestingGameState);
store = setupStore({ game: OfflineTestingGameState });
store = setupStore({ ...globalInitialState, game: OfflineTestingGameState });
return (
<>
<Provider store={store}>
Expand Down
173 changes: 48 additions & 125 deletions src/app/GameStateHandler.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { useAppDispatch, useAppSelector } from './Hooks';
import {
getGameInfo,
receiveGameState,
setGameStart
setGameStart,
setOpponentTyping
} from 'features/game/GameSlice';
import { useKnownSearchParams } from 'hooks/useKnownSearchParams';
import { GameLocationState } from 'interface/GameLocationState';
Expand All @@ -21,6 +22,8 @@ import { toast } from 'react-hot-toast';
import { useGetFriendsListQuery } from 'features/api/apiSlice';
import useAuth from 'hooks/useAuth';

const MAX_RETRIES = 5;

const GameStateHandler = () => {
const { gameID } = useParams();
const gameInfo = useAppSelector(getGameInfo);
Expand All @@ -35,7 +38,6 @@ const GameStateHandler = () => {
);
const navigate = useNavigate();

// Fetch friends list to ensure it's available for hand visibility checks
const { isLoggedIn } = useAuth();
const { data: friendsData } = useGetFriendsListQuery(undefined, {
skip: !isLoggedIn
Expand All @@ -44,9 +46,7 @@ const GameStateHandler = () => {
const sourceRef = useRef<EventSource | null>(null);
const gameParamsRef = useRef({ gameID: 0, playerID: 0, authKey: '' });
const retryCountRef = useRef(0);
const maxRetriesRef = useRef(5);
const [forceRetry, setForceRetry] = useState(0);
const fatalErrorRef = useRef(false);

useEffect(() => {
const currentGameID = parseInt(gameID ?? gameName);
Expand All @@ -57,11 +57,9 @@ const GameStateHandler = () => {
currentAuthKey = gameInfo.authKey;
}
if (!currentAuthKey && currentGameID > 0) {
// Last resort: try to load from localStorage for same game
currentAuthKey = loadGameAuthKey(currentGameID);
}

// Only dispatch if values actually changed
if (
gameParamsRef.current.gameID !== currentGameID ||
gameParamsRef.current.playerID !== currentPlayerID ||
Expand Down Expand Up @@ -97,42 +95,30 @@ const GameStateHandler = () => {
dispatch
]);

// Sync friends data to sessionStorage whenever it's fetched
// Sync friends list to sessionStorage whenever it's fetched
useEffect(() => {
if (friendsData?.friends) {
try {
const friendsList = friendsData.friends.map((f) => f.username);
sessionStorage.setItem('friendsList', JSON.stringify(friendsList));
console.log(
'GameStateHandler synced friendsList to sessionStorage:',
friendsList
);
} catch (e) {
console.error('Failed to sync friendsList to sessionStorage:', e);
}
}
}, [friendsData?.friends]);

// SSE connection to game server
useEffect(() => {
// Use gameInfo from Redux state (which is already updated) rather than ref
const currentGameID = gameInfo.gameID;
const currentPlayerID = gameInfo.playerID;
const currentAuthKey = gameInfo.authKey;

// Don't create EventSource if authKey is empty for actual players (playerID 1 or 2)
// Spectators (playerID 3) don't need authKey, but players do
// Players 1 and 2 require an authKey; spectators (3) do not
if ((currentPlayerID === 1 || currentPlayerID === 2) && !currentAuthKey) {
// This is expected while authKey is loading, only log once per game change
if (gameParamsRef.current.gameID !== currentGameID) {
console.warn(
`⏳ AuthKey loading for game ${currentGameID}, player ${currentPlayerID}...`
);
}
// Wait for authKey to be available before connecting
return;
}

// Reset retry count when game changes
// Reset retry count when the game changes
if (gameParamsRef.current.gameID !== currentGameID) {
retryCountRef.current = 0;
gameParamsRef.current = {
Expand All @@ -142,73 +128,54 @@ const GameStateHandler = () => {
};
}

// Close existing connection before creating new one
if (sourceRef.current) {
sourceRef.current.close();
sourceRef.current = null;
}

// Add a small delay before connecting to ensure page is ready
// Small delay to ensure the page is ready before connecting
const connectionTimeout = setTimeout(() => {
try {
console.log(
`🔌 Connecting to EventSource (attempt ${retryCountRef.current + 1}/${
maxRetriesRef.current + 1
})...`
);
let friendsList: string[] = [];
try {
const stored = sessionStorage.getItem('friendsList');
if (stored) {
friendsList = JSON.parse(stored);
}
} catch (e) {
// sessionStorage parsing failed, continue without friendsList
} catch {
// Continue without friendsList
}
console.log(
'GameStateHandler SSE - sending friendsList:',
friendsList,
'playerID:',
currentPlayerID
);

const resolvedUserName = getCurrentUsername(currentUserName) ?? '';
const source = new EventSource(
`${BACKEND_URL}GetUpdateSSE.php?gameName=${currentGameID}&playerID=${currentPlayerID}&authKey=${currentAuthKey}&friendsList=${encodeURIComponent(
JSON.stringify(friendsList)
)}`
)}&userName=${encodeURIComponent(resolvedUserName)}`
);
sourceRef.current = source;

// Mark as successful when first message comes through
let hasConnected = false;

source.onmessage = (event) => {
hasConnected = true;
retryCountRef.current = 0; // Reset retry counter on successful message
retryCountRef.current = 0;

try {
const data = JSON.parse(event.data);

// Check for error messages from SSE
if (data.error) {
console.error('SSE Error:', data.error);
const errorMsg = data.error.toLowerCase();

// Handle game not found errors
if (
errorMsg.includes('game no longer exists') ||
errorMsg.includes('does not exist')
) {
toast.error(`Game Error: ${data.error}`);
window.sessionStorage.setItem(
'gameNotFound',
String(currentGameID)
);
source.close();
setTimeout(() => navigate('/'), 60000);
return;
}

// Handle auth errors
if (
errorMsg.includes('invalid auth') ||
errorMsg.includes('authkey')
Expand All @@ -218,61 +185,45 @@ const GameStateHandler = () => {
return;
}

// Display other errors
toast.error(`Server Error: ${data.error}`);
return;
}

// Parse the game state directly from SSE data
const parsedState = ParseGameState(data);

// Dispatch the parsed game state directly (no HTTP round-trip needed)
dispatch(receiveGameState(parsedState));
dispatch(receiveGameState(ParseGameState(data)));
} catch (parseError) {
console.error('Failed to parse SSE data:', parseError);
// Don't close connection on parse errors - wait for next update
}
};

source.onerror = () => {
// Only process error if connection was actually established (not just interruption during page load)
if (!hasConnected && retryCountRef.current === 0) {
console.warn(
'⚠️ EventSource connection interrupted during page load'
);
// Treat interruptions during load as transient, retry once quickly
setTimeout(() => {
setForceRetry((prev) => prev + 1);
}, 500);
return;
// This replaces the old CheckOpponentTyping polling entirely.
source.addEventListener('typing', (event: MessageEvent) => {
try {
const data = JSON.parse(event.data);
if (typeof data.opponentIsTyping === 'boolean') {
dispatch(setOpponentTyping(data.opponentIsTyping));
}
} catch {
}
});

console.error('❌ EventSource connection failed');
source.onerror = () => {
retryCountRef.current++;
source.close();
sourceRef.current = null;

// Retry with exponential backoff
if (retryCountRef.current < maxRetriesRef.current) {
retryCountRef.current++;
if (!hasConnected && retryCountRef.current === 1) {
// Transient interruption during page load — retry once quickly
setTimeout(() => setForceRetry((prev) => prev + 1), 500);
return;
}

if (retryCountRef.current <= MAX_RETRIES) {
const retryDelay = Math.min(
500 * Math.pow(2, retryCountRef.current),
5000
);
console.warn(
`⏳ Will retry EventSource connection in ${retryDelay}ms (attempt ${
retryCountRef.current + 1
}/${maxRetriesRef.current + 1})`
);

setTimeout(() => {
setForceRetry((prev) => prev + 1); // Trigger the effect again
}, retryDelay);
setTimeout(() => setForceRetry((prev) => prev + 1), retryDelay);
} else {
console.error(
`❌ EventSource failed after ${
maxRetriesRef.current + 1
} attempts.`
);
toast.error(
'Connection to game server lost. Please refresh the page.'
);
Expand All @@ -281,61 +232,33 @@ const GameStateHandler = () => {
} catch (error) {
console.error('Failed to create EventSource:', error);
}
}, 100); // Small delay to ensure page is ready
}, 100);

const handleBeforeUnload = () => {
if (sourceRef.current) {
sourceRef.current.close();
sourceRef.current = null;
}
};

window.addEventListener('beforeunload', handleBeforeUnload);

return () => {
clearTimeout(connectionTimeout);
window.removeEventListener('beforeunload', handleBeforeUnload);
if (sourceRef.current) {
sourceRef.current.close();
sourceRef.current = null;
}
};
}, [
gameInfo.gameID,
gameInfo.playerID,
gameInfo.authKey,
gameInfo.isPrivateLobby,
gameInfo.isRoguelike,
forceRetry,
dispatch
]);
}, [gameInfo.gameID, gameInfo.playerID, gameInfo.authKey, forceRetry, dispatch, navigate]);

useEffect(() => {
if (isFullRematch && gameID) {
navigate(`/game/lobby/${gameID}`);
}
}, [isFullRematch, gameID, navigate]);

// Check if game was reported as not found
const gameNotFoundTimeoutRef = useRef<NodeJS.Timeout | null>(null);

useEffect(() => {
const checkGameNotFound = () => {
const state = window.sessionStorage.getItem('gameNotFound');
if (state === String(gameInfo.gameID)) {
// Only schedule navigation once
if (!gameNotFoundTimeoutRef.current) {
console.log(
`Game ${gameInfo.gameID} no longer exists, will navigate to games list in 60s`
);
gameNotFoundTimeoutRef.current = setTimeout(() => {
window.sessionStorage.removeItem('gameNotFound');
navigate('/');
}, 60000);
}
}
};

// Check periodically since errors might come asynchronously
const interval = setInterval(checkGameNotFound, 1000);
return () => {
clearInterval(interval);
if (gameNotFoundTimeoutRef.current) {
clearTimeout(gameNotFoundTimeoutRef.current);
}
};
}, [gameInfo.gameID, navigate]);

return null;
};

Expand Down
4 changes: 1 addition & 3 deletions src/app/ParseGameState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@ export default function ParseGameState(input: any) {

// spectator count
result.gameDynamicInfo.spectatorCount = input.spectatorCount ?? 0;
result.gameDynamicInfo.spectatorNames = input.spectatorNames ?? [];
// player inventory
result.gameDynamicInfo.playerInventory = input.playerInventory
? input.playerInventory.map((card: any) => ParseCard(card))
Expand Down Expand Up @@ -590,9 +591,6 @@ export default function ParseGameState(input: any) {
// AI infinite HP status for manual mode
result.aiHasInfiniteHP = input.aiHasInfiniteHP ?? false;

// opponent activity status (0 = active, 2 = inactive)
result.opponentActivity = input.opponentActivity ?? 0;

// rematch acceptance status
result.isFullRematch = input.fullRematchAccepted ?? false;

Expand Down
Loading