From 640d81d919ca6bb39410080a5e93bbba762cdce0 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Tue, 21 Jul 2026 22:50:09 +0200 Subject: [PATCH] fix(call-to-play): explain unavailable game calls Calls for a game missing from the local catalog still contributed to the badge but were discarded by both the ticker and overlay. Opening the badge could therefore show a blank modal with no explanation. Render those calls with an unavailable-game label, the sender and game ID, the normal roster and chat, and safe coordination actions. The creator can mark the match started, but automatic launch remains disabled until catalog data exists. Test Plan: - `just fmt` -- passed - `just frontend-test` -- passed, 21 tests - `just build` -- passed - `git diff --cached --check` -- passed --- .../calltoplay/CallToPlayOverlay.tsx | 5 ++-- .../calltoplay/CallToPlayTicker.tsx | 6 ++-- .../components/calltoplay/NominationCard.tsx | 28 +++++++++++++------ .../src/styles/launcher.css | 8 ++++++ 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/crates/lanspread-tauri-deno-ts/src/components/calltoplay/CallToPlayOverlay.tsx b/crates/lanspread-tauri-deno-ts/src/components/calltoplay/CallToPlayOverlay.tsx index 4af3c06..14b0693 100644 --- a/crates/lanspread-tauri-deno-ts/src/components/calltoplay/CallToPlayOverlay.tsx +++ b/crates/lanspread-tauri-deno-ts/src/components/calltoplay/CallToPlayOverlay.tsx @@ -81,8 +81,7 @@ export const CallToPlayOverlay = ({ )} {sorted.map(nomination => { - const game = gameById.get(nomination.gameId); - if (!game) return null; + const game = gameById.get(nomination.gameId) ?? null; return ( diff --git a/crates/lanspread-tauri-deno-ts/src/components/calltoplay/CallToPlayTicker.tsx b/crates/lanspread-tauri-deno-ts/src/components/calltoplay/CallToPlayTicker.tsx index 288817d..65a4df5 100644 --- a/crates/lanspread-tauri-deno-ts/src/components/calltoplay/CallToPlayTicker.tsx +++ b/crates/lanspread-tauri-deno-ts/src/components/calltoplay/CallToPlayTicker.tsx @@ -75,7 +75,6 @@ export const CallToPlayTicker = ({ nominations, games, accent, onOpen }: Props)
{active.map(nomination => { const game = gameById.get(nomination.gameId); - if (!game) return null; const status = statusOf(nomination, now); if (status === 'started') return null; const ready = readyCountOf(nomination, now); @@ -102,7 +101,10 @@ export const CallToPlayTicker = ({ nominations, games, accent, onOpen }: Props) > {LABEL[status]} - {game.name} + {game?.name ?? 'Game unavailable here'} by {nomination.creator} {count} {time} diff --git a/crates/lanspread-tauri-deno-ts/src/components/calltoplay/NominationCard.tsx b/crates/lanspread-tauri-deno-ts/src/components/calltoplay/NominationCard.tsx index ec890b9..20da08f 100644 --- a/crates/lanspread-tauri-deno-ts/src/components/calltoplay/NominationCard.tsx +++ b/crates/lanspread-tauri-deno-ts/src/components/calltoplay/NominationCard.tsx @@ -20,7 +20,7 @@ import { CallToPlayParticipant, Game, Nomination } from '../../lib/types'; interface Props { nomination: Nomination; - game: Game; + game: Game | null; actorId: string | null; actions: CallToPlayActions; focused: boolean; @@ -94,7 +94,9 @@ export const NominationCard = ({ Math.min(100, (remaining / Math.max(1, nomination.deadline - windowStart)) * 100), ); const urgency = percentage < 20 ? 'high' : percentage < 50 ? 'mid' : 'low'; - const installedCount = (game.installed_peer_count ?? game.peer_count) + (game.installed ? 1 : 0); + const installedCount = game === null + ? 0 + : (game.installed_peer_count ?? game.peer_count) + (game.installed ? 1 : 0); const lanCount = Math.max(totalPeerCount + 1, installedCount); const timer = isStarted @@ -130,15 +132,19 @@ export const NominationCard = ({ >
- + {game + ? + :
}
-
{game.name}
+
{game?.name ?? 'Game unavailable here'}
{nomination.scheduledFor === null ? 'Called' : 'Scheduled'} by{' '} {nomination.creator} {nomination.scheduledFor !== null && ` · starts at ${formatClock(nomination.scheduledFor)}`} - {` · ${installedCount}/${lanCount} peers have it installed`} + {game + ? ` · ${installedCount}/${lanCount} peers have it installed` + : ` · this game is not in your current library (ID: ${nomination.gameId})`}
{timer} @@ -229,7 +235,7 @@ export const NominationCard = ({ interface CardActionsProps { nomination: Nomination; - game: Game; + game: Game | null; actorId: string | null; actions: CallToPlayActions; onLaunch: (game: Game) => void; @@ -274,7 +280,11 @@ const CardActions = ({ const readyCount = readyCountOf(nomination, now); if (isStarted) { - return
Launching {game.name}…
; + return ( +
+ {game ? `Launching ${game.name}…` : 'The match is starting.'} +
+ ); } if (scheduled) { if (isCreator) { @@ -310,9 +320,9 @@ const CardActions = ({ + >{game ? 'Start now' : 'Mark as started'} diff --git a/crates/lanspread-tauri-deno-ts/src/styles/launcher.css b/crates/lanspread-tauri-deno-ts/src/styles/launcher.css index 6093935..45f84ab 100644 --- a/crates/lanspread-tauri-deno-ts/src/styles/launcher.css +++ b/crates/lanspread-tauri-deno-ts/src/styles/launcher.css @@ -1933,6 +1933,14 @@ border-radius: 8px; overflow: hidden; } +.ctp-card-cover-missing { + width: 100%; height: 100%; + display: grid; place-items: center; + color: var(--t-3); + background: color-mix(in srgb, var(--bd-2) 55%, transparent); + border: 1px dashed var(--bd-2); +} +.ctp-card-cover-missing svg { width: 22px; height: 22px; } .ctp-card-info { flex: 1; min-width: 0; } .ctp-card-title { font-size: 14.5px; font-weight: 700; color: var(--t-1); } .ctp-card-sub { margin-top: 3px; font-size: 11.5px; color: var(--t-3); }