Chess on the ATmosphere checkmate.blue
chess
13
fork

Configure Feed

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

Phase 1: Challenge & Invite Flow#

Status: Implemented (branch: phase-1-challenge-invite-flow)


What Was Implemented#

1a. Post-Based Challenges -- DONE#

"Post to Bluesky" button on the game waiting screen. Creates a app.bsky.feed.post in the challenger's repo with:

  • Text mentioning the opponent: "I'm challenging @{handle} to a game of chess on checkmate.blue!"
  • Mention facet with correct UTF-8 byte offsets
  • Link facet for the game URL
  • app.bsky.embed.external with title and description for the link card

Only shows when a specific opponent is set (not for open games). Button disables after posting to prevent double-posts.

Files created:

  • src/lib/bluesky.ts -- buildFacets(), postToBluesky(), composeChallengePost()
  • tests/lib/bluesky.test.ts -- 9 tests covering facet construction, byte offsets with unicode, handle edge cases

Files modified:

  • src/routes/game/[did]/[rkey]/+page.svelte -- added post button in waiting state

1b. DM-Based Challenges -- DONE (simplified)#

Originally planned as a chat.bsky.convo API integration. Investigation revealed that transition:generic scope does NOT cover chat.bsky.* operations -- adding DM support would require the transition:chat.bsky scope, broadening the permission grant.

Implemented instead: "Send via DM" button that copies the game link to clipboard and opens https://bsky.app/messages in a new tab. The user pastes the link in the conversation. Not as seamless as API-driven DMs, but works without additional OAuth scope.

Files modified:

  • src/routes/game/[did]/[rkey]/+page.svelte -- added DM button in waiting state

1c. Color Selection -- DONE#

White/Black/Random segmented control on the /play form. Game page logic fully decoupled from record ownership:

  • Owner creates canonical record regardless of their color
  • white/black fields determine color, independent of who owns the record
  • Non-owner creates child record with parentGameUri regardless of their color
  • Jetstream, record pairing, and join logic all work for either color assignment
  • createGame now accepts optional white param (was required)

Files modified:

  • src/routes/play/+page.svelte -- color selector UI, resolveColors() helper
  • src/routes/game/[did]/[rkey]/+page.svelte -- loadGame() rewritten for color-agnostic logic, joinAsBlack renamed to joinGame, waitForOpponent handles either color
  • src/lib/atproto.ts -- white param made optional in createGame
  • src/lib/types.ts -- challengerColor added to ChallengeRecord
  • lexicons/blue.checkmate.challenge.json -- challengerColor field added

Also: Check-Before-Join Guard#

Before creating a child record to join a game, the game page re-fetches the owner's record to verify the slot is still empty. If it's been filled (race condition), the viewer falls into spectator mode instead of creating an orphaned record.


What Was NOT Implemented#

  • Editable post text -- the Bluesky post uses a fixed template. An editable textarea (planned for Phase 5's game result sharing) was deferred.
  • Bluesky post text adjusting for color choice -- the post always says "I'm challenging @handle to a game of chess." It doesn't mention which color the challenger picked. Could be added later.
  • API-driven DM sending -- requires transition:chat.bsky scope. Deferred unless there's a strong reason to broaden permissions.

Decisions Made During Implementation#

  • OAuth scope is already minimal. atproto transition:generic is the tightest scope available today. There are no collection-level scopes yet. The transition: prefix signals these are temporary and will be replaced by granular permissions eventually.
  • DM deep linking doesn't work. Bluesky message URLs use conversation IDs (bsky.app/messages/{convoId}), not DIDs. Resolving the conversation ID requires chat.bsky scope. The fallback (open inbox) is acceptable.
  • Record ownership and color are now fully independent. The game URL always uses the creator's DID/rkey. The white/black fields determine who plays what. This is a meaningful architectural change from the original design where owner=White was assumed.