feat(mobile): implement "Rejoin Game" functionality and fix storage bugs

This commit is contained in:
Halit Aksoy 2026-07-16 02:00:25 +03:00
parent 8a485e2db3
commit 69664796ce
9 changed files with 59 additions and 6 deletions

View File

@ -4,4 +4,10 @@ export class LocalStorage implements Storage {
getString(key: string): string | undefined { getString(key: string): string | undefined {
return localStorage.getItem(key) as string; return localStorage.getItem(key) as string;
} }
setString(key: string, value: string): void {
localStorage.setItem(key, value);
}
deleteString(key: string): void {
localStorage.removeItem(key);
}
} }

View File

@ -55,6 +55,9 @@ export function GameScreen({ navigation, route }: GameScreenProps) {
pitAnimator.setUpdatedGame(newGame); pitAnimator.setUpdatedGame(newGame);
setHasOngoingAction(false); setHasOngoingAction(false);
setGameUsersConnectionInfo(newGame.gameUsersConnectionInfo); setGameUsersConnectionInfo(newGame.gameUsersConnectionInfo);
if (newGame.mancalaGame.state === 'ended') {
context.storage.deleteString('lastGameId');
}
} }
const isUserOnline = (userId: string) => { const isUserOnline = (userId: string) => {
@ -81,6 +84,7 @@ export function GameScreen({ navigation, route }: GameScreenProps) {
const userKeyWhoLeave = message; const userKeyWhoLeave = message;
setUserKeyWhoLeave(userKeyWhoLeave); setUserKeyWhoLeave(userKeyWhoLeave);
setHasOngoingAction(false); setHasOngoingAction(false);
context.storage.deleteString('lastGameId');
}; };
const onUserConnectionChange = (message: any) => { const onUserConnectionChange = (message: any) => {
const gameUsersConnectionInfo = message as GameUsersConnectionInfo; const gameUsersConnectionInfo = message as GameUsersConnectionInfo;
@ -131,6 +135,7 @@ export function GameScreen({ navigation, route }: GameScreenProps) {
text: t('Yes'), text: t('Yes'),
onPress: () => { onPress: () => {
context.rtmt.sendMessage(channel_leave_game, {}); context.rtmt.sendMessage(channel_leave_game, {});
context.storage.deleteString('lastGameId');
updateHeaderButton(); updateHeaderButton();
}, },
style: 'default', style: 'default',
@ -144,6 +149,7 @@ export function GameScreen({ navigation, route }: GameScreenProps) {
const onNewGameClick = () => { const onNewGameClick = () => {
if (Util.checkConnectionAndMaybeAlert(context, t("ConnectionLost"))) return; if (Util.checkConnectionAndMaybeAlert(context, t("ConnectionLost"))) return;
context.storage.deleteString('lastGameId');
navigation.replace("Loby", { context }) navigation.replace("Loby", { context })
}; };
@ -193,12 +199,16 @@ export function GameScreen({ navigation, route }: GameScreenProps) {
setLoadingStateGame(LoadingState.Loading()) setLoadingStateGame(LoadingState.Loading())
context.gameStore.get(gameId!!).then((game) => { context.gameStore.get(gameId!!).then((game) => {
if (game) { if (game) {
if (game.mancalaGame.state !== 'ended') {
context.storage.setString('lastGameId', game.id);
}
pitAnimator = new PitAnimator(context.themeManager, updateBoardViewModel); pitAnimator = new PitAnimator(context.themeManager, updateBoardViewModel);
setPitAnimator(pitAnimator); setPitAnimator(pitAnimator);
onGameUpdate(pitAnimator, game); onGameUpdate(pitAnimator, game);
unlistenMessages = listenMessages(game, pitAnimator); unlistenMessages = listenMessages(game, pitAnimator);
setLoadingStateGame(LoadingState.Loaded({ value: game })) setLoadingStateGame(LoadingState.Loaded({ value: game }))
} else { } else {
context.storage.deleteString('lastGameId');
setLoadingStateGame(LoadingState.Error({ errorMessage: t('GameNotFound') })) setLoadingStateGame(LoadingState.Error({ errorMessage: t('GameNotFound') }))
} }
}) })
@ -210,6 +220,7 @@ export function GameScreen({ navigation, route }: GameScreenProps) {
const updateHeaderButton = () => { const updateHeaderButton = () => {
const isGameEnded = game?.mancalaGame.state === "ended" || leftPlayer; const isGameEnded = game?.mancalaGame.state === "ended" || leftPlayer;
const isError = gameLoadingState.isError();
navigation.setOptions({ navigation.setOptions({
headerRight: () => ( headerRight: () => (
<View style={{ <View style={{
@ -219,13 +230,15 @@ export function GameScreen({ navigation, route }: GameScreenProps) {
gap: 5 gap: 5
}}> }}>
<Pressable onPress={() => { <Pressable onPress={() => {
if (isGameEnded) { if (isError) {
navigation.replace("Home", { context });
} else if (isGameEnded) {
onNewGameClick(); onNewGameClick();
} else { } else {
onLeaveGameClick(); onLeaveGameClick();
} }
}}> }}>
<CircularPanel color={context.themeManager.theme?.background} children={<Text style={{ color: context.themeManager.theme.textColor }}>{isGameEnded ? t('NewGame') : t('Leave')}</Text>} /> <CircularPanel color={context.themeManager.theme?.background} children={<Text style={{ color: context.themeManager.theme.textColor }}>{isError ? t('Home') : (isGameEnded ? t('NewGame') : t('Leave'))}</Text>} />
</Pressable> </Pressable>
<Pressable onPress={() => { <Pressable onPress={() => {
navigation.push("Help", { context }) navigation.push("Help", { context })
@ -240,7 +253,7 @@ export function GameScreen({ navigation, route }: GameScreenProps) {
React.useEffect(() => { React.useEffect(() => {
updateHeaderButton(); updateHeaderButton();
}, [navigation, game, userKeyWhoLeave]); }, [navigation, game, userKeyWhoLeave, gameLoadingState]);
const textColorOnAppBar = getColorByBrightness( const textColorOnAppBar = getColorByBrightness(
context.themeManager.theme.appBarBgColor, context.themeManager.theme.appBarBgColor,

View File

@ -11,6 +11,7 @@ export function HomeScreen({ navigation, route }: HomeScreenProps) {
const { context } = route.params; const { context } = route.params;
const { t } = useTranslation(); const { t } = useTranslation();
const [lastGameId, setLastGameId] = React.useState<string | undefined>(undefined);
const textColorOnAppBar = getColorByBrightness( const textColorOnAppBar = getColorByBrightness(
context.themeManager.theme.appBarBgColor, context.themeManager.theme.appBarBgColor,
@ -32,6 +33,10 @@ export function HomeScreen({ navigation, route }: HomeScreenProps) {
React.useEffect(() => { React.useEffect(() => {
updateHeaderButton(); updateHeaderButton();
const savedGameId = context.storage.getString('lastGameId');
if (savedGameId) {
setLastGameId(savedGameId);
}
}, []); }, []);
return ( return (
@ -39,6 +44,22 @@ export function HomeScreen({ navigation, route }: HomeScreenProps) {
<Pressable onPress={() => navigation.replace('Loby', { context })}> <Pressable onPress={() => navigation.replace('Loby', { context })}>
<CircularPanel color={context.themeManager.theme?.boardColor} children={<Text style={{ color: context.themeManager.theme.textColor }}>{t('NewGame')}</Text>} /> <CircularPanel color={context.themeManager.theme?.boardColor} children={<Text style={{ color: context.themeManager.theme.textColor }}>{t('NewGame')}</Text>} />
</Pressable> </Pressable>
{lastGameId && (
<>
<View style={{ height: 20 }} />
<Pressable onPress={async () => {
try {
const userKey = await context.userKeyStore.getUserKey();
navigation.replace('Game', { context, gameId: lastGameId, userKey });
} catch (error) {
context.storage.deleteString('lastGameId');
setLastGameId(undefined);
}
}}>
<CircularPanel color={context.themeManager.theme?.boardColor} children={<Text style={{ color: context.themeManager.theme.textColor }}>{t('RejoinGame')}</Text>} />
</Pressable>
</>
)}
</View> </View>
); );
} }

View File

@ -17,6 +17,7 @@ export default function LobyScreen({ navigation, route }: LobyScreenProps) {
const onGameStart = async (message: Object) => { const onGameStart = async (message: Object) => {
const newGame: CommonMancalaGame = message as CommonMancalaGame; const newGame: CommonMancalaGame = message as CommonMancalaGame;
context.storage.setString('lastGameId', newGame.id);
const userKey = await context.userKeyStore.getUserKey(); const userKey = await context.userKeyStore.getUserKey();
navigation.replace("Game", { context, gameId: newGame.id, userKey }) navigation.replace("Game", { context, gameId: newGame.id, userKey })
} }

View File

@ -8,4 +8,10 @@ export class LocalStorage implements Storage {
getString(key: string): string | undefined { getString(key: string): string | undefined {
return storage.getString(key); return storage.getString(key);
} }
setString(key: string, value: string): void {
storage.set(key, value);
}
deleteString(key: string): void {
storage.remove(key);
}
} }

View File

@ -21,7 +21,7 @@ export class GameStoreImpl implements GameStore {
return game; return game;
} catch (error) { } catch (error) {
// todo check error // todo check error
Promise.resolve(undefined); return undefined;
} }
} }

View File

@ -36,5 +36,7 @@ export const en = {
Cancel: "Cancel", Cancel: "Cancel",
Help: "Help", Help: "Help",
WebApp: "Goto WebApp", WebApp: "Goto WebApp",
Privacy: "Open Privacy Policy" Privacy: "Open Privacy Policy",
Home: "Home",
RejoinGame: "Rejoin Game"
} }

View File

@ -36,5 +36,7 @@ export const tr = {
Cancel: "İptal", Cancel: "İptal",
Help: "Yardım", Help: "Yardım",
WebApp: "Web Uygulamasına Git", WebApp: "Web Uygulamasına Git",
Privacy: "Gizlilik Politikasını Göster" Privacy: "Gizlilik Politikasını Göster",
Home: "Ana Sayfa",
RejoinGame: "Oyuna Geri Dön"
}; };

View File

@ -1,4 +1,6 @@
export interface Storage { export interface Storage {
getString(key: string): string | undefined; getString(key: string): string | undefined;
setString(key: string, value: string): void;
deleteString(key: string): void;
} }