From 5e0ebcefd28ee9713d8548c9b001d8ef11bc1895 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 8 Jun 2026 15:38:18 -0500 Subject: [PATCH] Resolve chat invites --- .../ComposerV2/store/__tests__/embeds.test.ts | 22 ++++++++++++++++++- src/components/ComposerV2/store/index.ts | 8 +++++++ src/components/ComposerV2/store/types.ts | 10 ++++++++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/components/ComposerV2/store/__tests__/embeds.test.ts b/src/components/ComposerV2/store/__tests__/embeds.test.ts index 035bcfa12e..18af37ae5b 100644 --- a/src/components/ComposerV2/store/__tests__/embeds.test.ts +++ b/src/components/ComposerV2/store/__tests__/embeds.test.ts @@ -69,7 +69,7 @@ function makeStore() { return createThreadStore({ agent, __createId: makeIdGenerator(), - __resolveLink: mockResolveLink as unknown as typeof resolveLink, + __resolveLink: mockResolveLink, }) } @@ -118,6 +118,13 @@ const externalLink: ResolvedLink = { thumb: undefined, } +const chatInviteLink: ResolvedLink = { + type: 'chat-invite', + uri: 'https://bsky.app/messages/join/abc123', + code: 'abc123', + view: undefined, +} + describe('addUri pre-classifies bsky post URLs to the quote slot', () => { test('post URL → quote.pending → quote.resolved (with view)', async () => { const d = deferred() @@ -258,6 +265,19 @@ describe('addUri pre-classifies non-post URLs to the embed slot', () => { expect(embed.title).toBe('Example') }) + test('chat-invite → embed["chat-invite"]', async () => { + const d = deferred() + mockResolveLink.mockReturnValueOnce(d.promise) + const store = makeStore() + const root = rootId(store) + store.actions.addUri(root, EXTERNAL_URL) + d.resolve(chatInviteLink) + await flushPromises() + const embed = store.getState().posts[root].embed + if (embed?.state !== 'chat-invite') throw new Error('expected chat-invite') + expect(embed.code).toBe('abc123') + }) + test('addUri is a no-op when embed has settled (resolved)', async () => { const d = deferred() mockResolveLink.mockReturnValueOnce(d.promise) diff --git a/src/components/ComposerV2/store/index.ts b/src/components/ComposerV2/store/index.ts index d454b54a85..7e7d60c3c7 100644 --- a/src/components/ComposerV2/store/index.ts +++ b/src/components/ComposerV2/store/index.ts @@ -608,6 +608,14 @@ export function createThreadStore(options: { thumb: link.thumb, } } + if (link.type === 'chat-invite') { + return { + state: 'chat-invite', + uri: link.uri, + code: link.code, + view: link.view, + } + } switch (link.kind) { case 'feed': return {state: 'feed', record: link.record, view: link.view} diff --git a/src/components/ComposerV2/store/types.ts b/src/components/ComposerV2/store/types.ts index 810f33fbac..224f6e2bf0 100644 --- a/src/components/ComposerV2/store/types.ts +++ b/src/components/ComposerV2/store/types.ts @@ -2,6 +2,7 @@ import { type AppBskyFeedDefs, type AppBskyGraphDefs, type BlobRef, + type ChatBskyGroupDefs, type ComAtprotoRepoStrongRef, } from '@atproto/api' @@ -97,7 +98,8 @@ export type LinkResolutionFailureCode = 'embedding-disabled' | 'unknown' * `embedding-disabled`) `retry` is omitted so UI can detect that case and * surface a non-retryable message. The pending variant is set synchronously * by addUri while resolution is in flight; the resolved variants - * (external/feed/list/starter-pack) land when the worker reports back. + * (external/feed/list/starter-pack/chat-invite) land when the worker + * reports back. * * Note: `retry` is a function reference and won't survive JSON serialization. * On restore (OS-resume / draft load), the store re-attaches it. @@ -135,6 +137,12 @@ export type PostEmbed = record: ComAtprotoRepoStrongRef.Main view: AppBskyGraphDefs.StarterPackView } + | { + state: 'chat-invite' + uri: string + code: string + view: ChatBskyGroupDefs.JoinLinkPreviewView | undefined + } /** * What's stored on a post's `quote` field. Mirrors `PostEmbed`'s shape: the