feat(ui): app-shell behaviour — restore validation, return-to-lobby, push
- A restored game that no longer exists (cancelled/removed/revoked) drops to
the lobby with a toast instead of the in-game error state: game-state
exposes a `notFound` flag and the shell redirects via appScreen.go("lobby").
- Add a visible "return to lobby" control to the in-game header.
- Push/toast deep-links use activeView.select(...) (no URL); fix a latent
visibility-listener double-install on in-place game switches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -118,6 +118,17 @@ export class GameStateStore {
|
||||
*/
|
||||
mapToggles: MapToggles = $state({ ...DEFAULT_MAP_TOGGLES });
|
||||
error: string | null = $state(null);
|
||||
/**
|
||||
* notFound is the distinct "this game is not in the player's list"
|
||||
* signal, set when `findGame` returns null (cancelled, removed, or
|
||||
* access revoked). It is a clean flag the app-shell reads after
|
||||
* `init` to drop a restored/stale game back to the lobby with a
|
||||
* toast, rather than string-matching the `error` message. Transient
|
||||
* network failures keep `notFound` false (they take the catch path)
|
||||
* so they still surface the in-game error state for a retry. Reset
|
||||
* to false at the start of every `setGame` / `advanceToPending`.
|
||||
*/
|
||||
notFound = $state(false);
|
||||
/**
|
||||
* currentTurn mirrors the engine's turn number for the running
|
||||
* game (lifted from the lobby record on `setGame`). Phase 14
|
||||
@@ -218,6 +229,7 @@ export class GameStateStore {
|
||||
this.gameId = gameId;
|
||||
this.status = "loading";
|
||||
this.error = null;
|
||||
this.notFound = false;
|
||||
this.report = null;
|
||||
|
||||
this.wrapMode = await readWrapMode(this.cache, gameId);
|
||||
@@ -229,6 +241,7 @@ export class GameStateStore {
|
||||
if (summary === null) {
|
||||
this.status = "error";
|
||||
this.error = `game ${gameId} is not in your list`;
|
||||
this.notFound = true;
|
||||
return;
|
||||
}
|
||||
this.gameName = summary.gameName;
|
||||
@@ -306,11 +319,13 @@ export class GameStateStore {
|
||||
}
|
||||
this.status = "loading";
|
||||
this.error = null;
|
||||
this.notFound = false;
|
||||
try {
|
||||
const summary = await this.findGame(this.gameId);
|
||||
if (summary === null) {
|
||||
this.status = "error";
|
||||
this.error = `game ${this.gameId} is not in your list`;
|
||||
this.notFound = true;
|
||||
return;
|
||||
}
|
||||
this.gameName = summary.gameName;
|
||||
@@ -558,6 +573,18 @@ export class GameStateStore {
|
||||
|
||||
private installVisibilityListener(): void {
|
||||
if (typeof document === "undefined") return;
|
||||
// Idempotent on re-init. In the single-URL app-shell a direct
|
||||
// game → game switch (a push deep-link or a refresh-restore)
|
||||
// re-runs `init` without unmounting the shell, so a naive
|
||||
// `addEventListener` here would stack a second listener on every
|
||||
// switch. Drop any previously-registered one first so exactly one
|
||||
// stays live.
|
||||
if (this.visibilityListener !== null) {
|
||||
document.removeEventListener(
|
||||
"visibilitychange",
|
||||
this.visibilityListener,
|
||||
);
|
||||
}
|
||||
const listener = (): void => {
|
||||
if (document.visibilityState === "visible" && this.status === "ready") {
|
||||
void this.refresh();
|
||||
|
||||
Reference in New Issue
Block a user