Chess on the ATmosphere checkmate.blue
chess
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix game join flow and add login prompts

- Add initialCursor option to JetstreamConnection to prevent missed
events when opponent joins during WebSocket connection setup
- Show login prompt to unauthenticated users on game pages so they
can sign in to join or play
- Load user games from PDS directly instead of Constellation to
avoid indexing delay on pending games
- Add listGamesForDid helper for direct PDS game listing

authored by

Scott Hadfield and committed by tangled.org 988a2768 7e0c1b94

+46 -36
+21
src/lib/atproto.ts
··· 145 145 } 146 146 } 147 147 148 + export async function listGamesForDid( 149 + did: string 150 + ): Promise<{ did: string; rkey: string; record: GameRecord }[]> { 151 + try { 152 + const publicAgent = await getPublicAgent(did); 153 + const response = await publicAgent.com.atproto.repo.listRecords({ 154 + repo: did, 155 + collection: COLLECTIONS.game, 156 + limit: 100, 157 + }); 158 + return response.data.records.map((rec) => ({ 159 + did, 160 + rkey: rec.uri.split('/').pop()!, 161 + record: rec.value as unknown as GameRecord, 162 + })); 163 + } catch (e) { 164 + console.error('listGamesForDid failed:', did, e); 165 + return []; 166 + } 167 + } 168 + 148 169 export async function createChallenge( 149 170 agent: Agent, 150 171 options: { opponent?: string }
+2
src/lib/jetstream.ts
··· 17 17 type JetstreamOptions = { 18 18 opponentDid: string; 19 19 agent?: Agent; 20 + initialCursor?: number; 20 21 onGameUpdate: (record: Record<string, unknown>) => void; 21 22 onConnectionChange?: (connected: boolean) => void; 22 23 }; ··· 40 41 constructor(options: JetstreamOptions) { 41 42 this.options = options; 42 43 if (options.agent) this.agent = options.agent; 44 + if (options.initialCursor) this.cursor = options.initialCursor; 43 45 } 44 46 45 47 connect(): void {
+7 -26
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { onMount } from 'svelte'; 3 3 import { auth } from '$lib/stores/auth.svelte'; 4 - import { findGamesForPlayer } from '$lib/microcosm'; 5 - import { getGamePublic, COLLECTIONS } from '$lib/atproto'; 4 + import { listGamesForDid, COLLECTIONS } from '$lib/atproto'; 6 5 import type { GameRecord } from '$lib/types'; 7 6 import GameCard from '$lib/components/GameCard.svelte'; 8 7 import LoginButton from '$lib/components/LoginButton.svelte'; ··· 39 38 }; 40 39 }); 41 40 42 - function parseAtUri(uri: string): { did: string; rkey: string } | null { 43 - const match = uri.match(/^at:\/\/([^/]+)\/[^/]+\/(.+)$/); 44 - if (!match) return null; 45 - return { did: match[1], rkey: match[2] }; 46 - } 47 - 48 41 async function loadUserGames(did: string) { 49 42 loadingUserGames = true; 50 43 try { 51 - const links = await findGamesForPlayer(did); 52 - const results = await Promise.all( 53 - links.slice(0, 20).map(async (link) => { 54 - const parsed = parseAtUri(link.uri); 55 - if (!parsed) return null; 56 - const record = await getGamePublic(parsed.did, parsed.rkey); 57 - if (!record || record.parentGameUri) return null; 58 - return { 59 - did: parsed.did, 60 - rkey: parsed.rkey, 61 - record, 62 - timestamp: new Date(record.createdAt).getTime(), 63 - }; 64 - }) 65 - ); 66 - userGames = results 67 - .filter((g): g is GameEntry => g !== null) 68 - .sort((a, b) => b.timestamp - a.timestamp); 44 + const allGames = await listGamesForDid(did); 45 + userGames = allGames 46 + .filter((g) => !g.record.parentGameUri) 47 + .map((g) => ({ ...g, timestamp: new Date(g.record.createdAt).getTime() })) 48 + .sort((a, b) => b.timestamp - a.timestamp) 49 + .slice(0, 20); 69 50 } catch { 70 51 userGames = []; 71 52 }
+16 -10
src/routes/game/[did]/[rkey]/+page.svelte
··· 161 161 const js = new JetstreamConnection({ 162 162 opponentDid: '', 163 163 agent: auth.agent ?? undefined, 164 + initialCursor: (Date.now() - 60_000) * 1000, 164 165 onGameUpdate: async (record) => { 165 166 const white = record.white as string; 166 167 const black = record.black as string; ··· 464 465 {/if} 465 466 466 467 {#if isSpectator && isWaiting} 467 - <div class="rounded-lg bg-bg-secondary px-4 py-2 text-sm text-text-secondary"> 468 - Waiting for game to start 469 - </div> 468 + {#if !auth.isLoggedIn} 469 + <div class="w-full max-w-sm rounded-lg border border-border bg-bg-secondary p-4 text-center"> 470 + <p class="mb-3 text-sm text-text-secondary">Sign in to join this game</p> 471 + <LoginButton /> 472 + </div> 473 + {:else} 474 + <div class="rounded-lg bg-bg-secondary px-4 py-2 text-sm text-text-secondary"> 475 + Waiting for game to start 476 + </div> 477 + {/if} 470 478 {/if} 471 479 472 480 <PlayerBar ··· 501 509 Flip board 502 510 </button> 503 511 </div> 504 - {/if} 505 - 506 - {#if !isSpectator && !auth.isLoggedIn} 507 - <div class="w-full max-w-sm rounded-lg border border-border bg-bg-secondary p-4 text-center"> 508 - <p class="mb-3 text-sm text-text-secondary">Sign in to play</p> 509 - <LoginButton /> 510 - </div> 512 + {#if !auth.isLoggedIn && !game.result} 513 + <div class="w-full max-w-sm"> 514 + <LoginButton /> 515 + </div> 516 + {/if} 511 517 {/if} 512 518 513 519 {#if game.result}