From f6a22c4f05a01c76e0b97f8118ae4ad55fac131f Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:18 +0300 Subject: [PATCH] [SDK] Adopt @bsky.app/sdk and migrate RichText (#11369) --- package.json | 3 +- pnpm-lock.yaml | 27 +++++++++++++++++ pnpm-workspace.yaml | 2 ++ src/components/FeedCard.tsx | 8 ++--- src/components/Post/Embed/index.tsx | 5 ++-- .../PostControls/PostMenu/PostMenuItems.tsx | 2 +- .../PostControls/PostMenu/index.tsx | 2 +- .../ShareMenu/ShareMenuItems.types.tsx | 2 +- .../PostControls/ShareMenu/index.tsx | 2 +- src/components/PostControls/index.tsx | 2 +- src/components/ProfileCard.tsx | 7 ++--- src/components/RichText.tsx | 10 ++++--- .../dialogs/lists/CreateOrEditListDialog.tsx | 22 +++++++++----- src/components/dms/MessageContextMenu.tsx | 11 +++---- src/components/dms/MessageItem.tsx | 8 +++-- src/components/hooks/useRichText.ts | 15 ++++++---- src/lib/api/index.ts | 15 +++++++--- src/lib/strings/helpers.ts | 2 +- src/lib/strings/rich-text-helpers.ts | 29 +++++++++++++++++-- src/lib/strings/rich-text-manip.ts | 12 ++++++-- .../Messages/components/MessageInputEmbed.tsx | 15 ++++------ .../Messages/components/MessagesList.tsx | 20 ++++++++----- .../components/ThreadItemAnchor.tsx | 5 ++-- .../PostThread/components/ThreadItemPost.tsx | 5 ++-- .../components/ThreadItemTreePost.tsx | 5 ++-- .../Profile/Header/ProfileHeaderLabeler.tsx | 2 +- .../Profile/Header/ProfileHeaderStandard.tsx | 2 +- src/screens/Profile/Header/index.tsx | 2 +- src/screens/ProfileList/components/Header.tsx | 6 ++-- .../Search/modules/ExploreTrendingTopics.tsx | 7 ++--- src/screens/StarterPack/StarterPackScreen.tsx | 5 ++-- src/screens/VideoFeed/index.tsx | 5 ++-- src/state/queries/feed.ts | 7 +++-- src/state/queries/starter-packs.ts | 20 ++++++++----- src/view/com/composer/Composer.tsx | 3 +- src/view/com/composer/drafts/state/api.ts | 3 +- .../select-language/SuggestedLanguage.tsx | 2 +- src/view/com/composer/state/composer.ts | 7 +++-- .../com/composer/text-input/TextInput.tsx | 6 ++-- .../composer/text-input/TextInput.types.ts | 2 +- .../com/composer/text-input/TextInput.web.tsx | 6 ++-- .../composer/text-input/text-input-util.ts | 6 ++-- src/view/com/post/Post.tsx | 5 ++-- src/view/com/posts/PostFeed.tsx | 2 +- src/view/com/posts/PostFeedItem.tsx | 5 ++-- src/view/screens/DebugMod.tsx | 2 +- src/view/screens/Profile.tsx | 14 +++++---- 47 files changed, 231 insertions(+), 124 deletions(-) diff --git a/package.json b/package.json index e084d19641..510955abbf 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "@bsky.app/expo-translate-text": "^0.2.9", "@bsky.app/peek-menu": "^0.3.2", "@bsky.app/react-native-uitextview": "^2.7.0", + "@bsky.app/sdk": "npm:bsky-sdk-preview@0.0.1-preview.2", "@bsky.app/sift": "^0.3.9", "@bsky.app/tapper": "^0.6.1", "@bsky.app/video": "0.3.6", @@ -339,7 +340,7 @@ "^await-lock$": "/node_modules/await-lock/build/AwaitLock.js" }, "transformIgnorePatterns": [ - "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|nanoid|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo|bcp-47-match|@atproto/.*|multiformats|uint8arrays|@ipld/.*|cborg|await-lock)" + "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|nanoid|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo|bcp-47-match|@atproto/.*|@bsky.app/sdk|tlds|multiformats|uint8arrays|@ipld/.*|cborg|await-lock)" ], "modulePathIgnorePatterns": [ "__tests__/.*/__mocks__", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa725d0bff..3b8bcb5f36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -287,6 +287,9 @@ importers: '@bsky.app/react-native-uitextview': specifier: ^2.7.0 version: 2.7.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) + '@bsky.app/sdk': + specifier: npm:bsky-sdk-preview@0.0.1-preview.2 + version: bsky-sdk-preview@0.0.1-preview.2(@atproto/lex@0.3.6) '@bsky.app/sift': specifier: ^0.3.9 version: 0.3.9(2d5c14016450c7e55c8cd0dffcca6f69) @@ -889,6 +892,10 @@ packages: resolution: {integrity: sha512-iDFZEoNqfL17EVKE+uNzdUD7YF8LWhEhxeBtP6x+PNuAxoXv3ip9vLmB8nNzNxFwVn++FipfDtSiPqmziv1bdA==} engines: {node: '>=22'} + '@atproto-labs/handle-resolver@0.4.8': + resolution: {integrity: sha512-38I+j8Efs+cCgW/NX9RFKKu7qv/AF+FqxlKS8sWjXlNZSHQZqPj5cnKVQhSsJCs4ypPB84iKch8w4/STd33bLg==} + engines: {node: '>=22'} + '@atproto-labs/pipe@0.2.4': resolution: {integrity: sha512-n67jCcrC+ouAeO10cWkpPzzLMlDi/lDCU30Us+LGqhOPhT6c4t5ASdBLQi9W3jUQtRzQBt3G9zipF+xKWNvVbw==} engines: {node: '>=22'} @@ -4505,6 +4512,12 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + bsky-sdk-preview@0.0.1-preview.2: + resolution: {integrity: sha512-LOcuvzc6UASiI5os1BUIo9dxAeVPIFiplcVSYbaDCgpH+tkpv2mqSqqsCRTP0g2CaVCwozqaTVy+jnsTO6/9BQ==} + engines: {node: '>=22.12.0'} + peerDependencies: + '@atproto/lex': ^0.3.0 + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -9725,6 +9738,13 @@ snapshots: dependencies: '@atproto-labs/pipe': 0.2.4 + '@atproto-labs/handle-resolver@0.4.8': + dependencies: + '@atproto-labs/simple-store': 0.5.1 + '@atproto-labs/simple-store-memory': 0.2.6 + '@atproto/did': 0.5.4 + zod: 3.25.76 + '@atproto-labs/pipe@0.2.4': {} '@atproto-labs/simple-store-memory@0.2.6': @@ -14063,6 +14083,13 @@ snapshots: dependencies: node-int64: 0.4.0 + bsky-sdk-preview@0.0.1-preview.2(@atproto/lex@0.3.6): + dependencies: + '@atproto-labs/handle-resolver': 0.4.8 + '@atproto/lex': 0.3.6 + '@atproto/syntax': 0.7.4 + tlds: 1.261.0 + buffer-crc32@0.2.13: {} buffer-from@1.1.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 372e071c73..cbb815b851 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -44,6 +44,7 @@ patchedDependencies: 'react-native@0.81.5': patches/react-native@0.81.5.patch minimumReleaseAgeExclude: - '@atproto/*' + - '@atproto-labs/*' - '@bsky.app/*' # todo: remove when old enough - '@oxlint-tsgolint/darwin-arm64@7.0.2001' @@ -53,3 +54,4 @@ minimumReleaseAgeExclude: - '@oxlint-tsgolint/win32-arm64@7.0.2001' - '@oxlint-tsgolint/win32-x64@7.0.2001' - oxlint-tsgolint@7.0.2001 + - bsky-sdk-preview@0.0.1-preview.2 diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx index efd14be4ba..2e7b070fb1 100644 --- a/src/components/FeedCard.tsx +++ b/src/components/FeedCard.tsx @@ -1,11 +1,7 @@ import {useCallback, useEffect, useMemo} from 'react' import {type GestureResponderEvent, View} from 'react-native' -import { - type AppBskyFeedDefs, - type AppBskyGraphDefs, - AtUri, - RichText as RichTextApi, -} from '@atproto/api' +import {type AppBskyFeedDefs, type AppBskyGraphDefs, AtUri} from '@atproto/api' +import {RichText as RichTextApi} from '@bsky.app/sdk/richtext' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx index da930dc6fb..0b143a0df3 100644 --- a/src/components/Post/Embed/index.tsx +++ b/src/components/Post/Embed/index.tsx @@ -6,12 +6,13 @@ import { AppBskyFeedPost, AtUri, moderatePost, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Trans} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {makeProfileLink} from '#/lib/routes/links' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import {getChatInviteCodeFromUrl} from '#/lib/strings/url-helpers' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {unstableCacheProfileView} from '#/state/queries/profile' @@ -295,7 +296,7 @@ export function QuoteEmbed({ return undefined const {text, facets} = quote.record return text.trim() - ? new RichTextAPI({text: text, facets: facets}) + ? new RichTextAPI({text: text, facets: asSdkFacets(facets)}) : undefined }, [quote.record]) diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx index b51f6cb7cb..43fdaaf1cd 100644 --- a/src/components/PostControls/PostMenu/PostMenuItems.tsx +++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx @@ -11,8 +11,8 @@ import { type AppBskyFeedPost, type AppBskyFeedThreadgate, AtUri, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' diff --git a/src/components/PostControls/PostMenu/index.tsx b/src/components/PostControls/PostMenu/index.tsx index a744efbaa8..c0826683b6 100644 --- a/src/components/PostControls/PostMenu/index.tsx +++ b/src/components/PostControls/PostMenu/index.tsx @@ -4,8 +4,8 @@ import { type AppBskyFeedDefs, type AppBskyFeedPost, type AppBskyFeedThreadgate, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {useLingui} from '@lingui/react/macro' import {type Shadow} from '#/state/cache/post-shadow' diff --git a/src/components/PostControls/ShareMenu/ShareMenuItems.types.tsx b/src/components/PostControls/ShareMenu/ShareMenuItems.types.tsx index 5bc2a8fb6e..ef93865513 100644 --- a/src/components/PostControls/ShareMenu/ShareMenuItems.types.tsx +++ b/src/components/PostControls/ShareMenu/ShareMenuItems.types.tsx @@ -3,8 +3,8 @@ import { type AppBskyFeedDefs, type AppBskyFeedPost, type AppBskyFeedThreadgate, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {type Shadow} from '#/state/cache/post-shadow' diff --git a/src/components/PostControls/ShareMenu/index.tsx b/src/components/PostControls/ShareMenu/index.tsx index efac6a3f01..121cd2a779 100644 --- a/src/components/PostControls/ShareMenu/index.tsx +++ b/src/components/PostControls/ShareMenu/index.tsx @@ -5,8 +5,8 @@ import { type AppBskyFeedPost, type AppBskyFeedThreadgate, AtUri, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' diff --git a/src/components/PostControls/index.tsx b/src/components/PostControls/index.tsx index 1f6be5ab6c..88f30b0561 100644 --- a/src/components/PostControls/index.tsx +++ b/src/components/PostControls/index.tsx @@ -4,8 +4,8 @@ import { type AppBskyFeedDefs, type AppBskyFeedPost, type AppBskyFeedThreadgate, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro' diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index c37f99c075..8508fecc71 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -6,11 +6,8 @@ import { View, type ViewStyle, } from 'react-native' -import { - moderateProfile, - type ModerationOpts, - RichText as RichTextApi, -} from '@atproto/api' +import {moderateProfile, type ModerationOpts} from '@atproto/api' +import {RichText as RichTextApi} from '@bsky.app/sdk/richtext' import {useLingui} from '@lingui/react/macro' import {getModerationCauseKey} from '#/lib/moderation' diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 5501193faf..ce73990570 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -1,6 +1,6 @@ import {useMemo} from 'react' import {type StyleProp, type TextStyle} from 'react-native' -import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {toShortUrl} from '#/lib/strings/url-helpers' import {android, atoms as a, flatten, type TextStyleProp} from '#/alf' @@ -9,6 +9,8 @@ import {InlineLinkText, type LinkProps} from '#/components/Link' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {RichTextTag} from '#/components/RichTextTag' import {Text, type TextProps} from '#/components/Typography' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' const WORD_WRAP = {wordWrap: 1} // lifted from facet detection in `RichText` impl, _without_ `gm` flags @@ -139,7 +141,7 @@ export function RichText({ if ( mention && (disableMentionFacetValidation || - AppBskyRichtextFacet.validateMention(mention).success) && + bsky.matches(app.bsky.richtext.facet.mention, mention)) && !disableLinks ) { els.push( @@ -156,7 +158,7 @@ export function RichText({ , ) - } else if (link && AppBskyRichtextFacet.validateLink(link).success) { + } else if (link && bsky.matches(app.bsky.richtext.facet.link, link)) { const isValidLink = URL_REGEX.test(link.uri) if (!isValidLink || disableLinks) { els.push(toShortUrl(segment.text)) @@ -181,7 +183,7 @@ export function RichText({ !disableLinks && enableTags && tag && - AppBskyRichtextFacet.validateTag(tag).success + bsky.matches(app.bsky.richtext.facet.tag, tag) ) { els.push( new RichTextAPI({text})) const [resolvedRT, setResolvedRT] = useState(null) - const agent = useAgent() + /* + * Facet/mention resolution is an appview job - it resolves handles via + * `com.atproto.identity.resolveHandle` through the appview. The public + * fallback keeps mentions working on logged-out surfaces. + */ + const client = useAppviewClient() if (text !== prevText) { setPrevText(text) setRawRT(new RichTextAPI({text})) @@ -19,7 +24,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] { async function resolveRTFacets() { // new each time const resolvedRT = new RichTextAPI({text}) - await resolvedRT.detectFacets(agent) + await resolvedRT.detectFacets(client) if (!ignore) { setResolvedRT(resolvedRT) } @@ -28,7 +33,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] { return () => { ignore = true } - }, [text, agent]) + }, [text, client]) const isResolving = resolvedRT === null return [resolvedRT ?? rawRT, isResolving] } diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 69cc2bf03e..9f81abb77a 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -13,9 +13,10 @@ import { type ComAtprotoLabelDefs, type ComAtprotoRepoApplyWrites, type ComAtprotoRepoStrongRef, - RichText, } from '@atproto/api' import {TID} from '@atproto/common-web' +import {type Client} from '@atproto/lex' +import {RichText} from '@bsky.app/sdk/richtext' import {t} from '@lingui/core/macro' import {type QueryClient} from '@tanstack/react-query' import {sha256} from 'js-sha256' @@ -51,6 +52,12 @@ interface PostOpts { replyTo?: string onStateChange?: (state: string) => void langs?: string[] + /* + * Facet/mention resolution is an appview job - it resolves handles through + * the appview, and the public fallback keeps it working when logged out. + * The rest of this pipeline still writes through the agent. + */ + appviewClient: Client } export async function post( @@ -87,7 +94,7 @@ export async function post( const draft = thread.posts[i] // Not awaited to avoid waterfalls. - const rtPromise = resolveRT(agent, draft.richtext) + const rtPromise = resolveRT(opts.appviewClient, draft.richtext) const embedPromise = resolveEmbed( agent, queryClient, @@ -196,14 +203,14 @@ export async function post( return {uris} } -async function resolveRT(agent: AtpAgent, richtext: RichText) { +async function resolveRT(appviewClient: Client, richtext: RichText) { const trimmedText = richtext.text // Trim leading whitespace-only lines (but don't break ASCII art). .replace(/^(\s*\n)+/, '') // Trim any trailing whitespace. .trimEnd() let rt = new RichText({text: trimmedText}, {cleanNewlines: true}) - await rt.detectFacets(agent) + await rt.detectFacets(appviewClient) rt = shortenLinks(rt) rt = stripInvalidMentions(rt) diff --git a/src/lib/strings/helpers.ts b/src/lib/strings/helpers.ts index 77ef18cb55..402807f50f 100644 --- a/src/lib/strings/helpers.ts +++ b/src/lib/strings/helpers.ts @@ -1,4 +1,4 @@ -import {type RichText} from '@atproto/api' +import {type RichText} from '@bsky.app/sdk/richtext' import {countGraphemes} from 'unicode-segmenter/grapheme' import {shortenLinks} from './rich-text-manip' diff --git a/src/lib/strings/rich-text-helpers.ts b/src/lib/strings/rich-text-helpers.ts index c2b2ceac52..a6341025ff 100644 --- a/src/lib/strings/rich-text-helpers.ts +++ b/src/lib/strings/rich-text-helpers.ts @@ -1,5 +1,7 @@ -import {AppBskyRichtextFacet, type RichText} from '@atproto/api' +import {type RichText} from '@bsky.app/sdk/richtext' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import {linkRequiresWarning} from './url-helpers' export function richTextToString(rt: RichText, loose: boolean): string { @@ -14,7 +16,7 @@ export function richTextToString(rt: RichText, loose: boolean): string { for (const segment of rt.segments()) { const link = segment.link - if (link && AppBskyRichtextFacet.validateLink(link).success) { + if (link && bsky.matches(app.bsky.richtext.facet.link, link)) { const href = link.uri const text = segment.text @@ -28,3 +30,26 @@ export function richTextToString(rt: RichText, loose: boolean): string { return result } + +/** + * Widens facets typed by the legacy `@atproto/api` codegen into the shape the + * SDK's `RichText` accepts. + * + * The two are the same lexicon and identical at runtime; they differ only in + * that the SDK brands `did`/`uri` as template literal types, which makes the + * legacy `string` versions unassignable. Call this where facets read off an + * `@atproto/api` view type are handed to `new RichText(...)`. + * + * Transitional: it goes away once the view types come from the SDK too. + */ +export function asSdkFacets( + facets: {index: {byteStart: number; byteEnd: number}; features: unknown[]}[], +): app.bsky.richtext.facet.Main[] +export function asSdkFacets( + facets: + | {index: {byteStart: number; byteEnd: number}; features: unknown[]}[] + | undefined, +): app.bsky.richtext.facet.Main[] | undefined +export function asSdkFacets(facets: unknown) { + return facets as app.bsky.richtext.facet.Main[] | undefined +} diff --git a/src/lib/strings/rich-text-manip.ts b/src/lib/strings/rich-text-manip.ts index 099fbffb0a..fa7a9b4a82 100644 --- a/src/lib/strings/rich-text-manip.ts +++ b/src/lib/strings/rich-text-manip.ts @@ -1,5 +1,7 @@ -import {AppBskyRichtextFacet, type RichText, UnicodeString} from '@atproto/api' +import {type RichText, UnicodeString} from '@bsky.app/sdk/richtext' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import {toShortUrl} from './url-helpers' export function shortenLinks(rt: RichText): RichText { @@ -10,7 +12,9 @@ export function shortenLinks(rt: RichText): RichText { // enumerate the link facets if (rt.facets) { for (const facet of rt.facets) { - const isLink = !!facet.features.find(AppBskyRichtextFacet.isLink) + const isLink = !!facet.features.find(f => + bsky.isType(app.bsky.richtext.facet.link, f), + ) if (!isLink) { continue } @@ -40,7 +44,9 @@ export function stripInvalidMentions(rt: RichText): RichText { rt = rt.clone() if (rt.facets) { rt.facets = rt.facets?.filter(facet => { - const mention = facet.features.find(AppBskyRichtextFacet.isMention) + const mention = facet.features.find(f => + bsky.isType(app.bsky.richtext.facet.mention, f), + ) if (mention && !mention.did) { return false } diff --git a/src/screens/Messages/components/MessageInputEmbed.tsx b/src/screens/Messages/components/MessageInputEmbed.tsx index 477939cd3e..9d1e4c32f6 100644 --- a/src/screens/Messages/components/MessageInputEmbed.tsx +++ b/src/screens/Messages/components/MessageInputEmbed.tsx @@ -1,12 +1,7 @@ import {useCallback, useEffect, useMemo, useState} from 'react' import {LayoutAnimation, View} from 'react-native' -import { - AppBskyFeedPost, - AppBskyRichtextFacet, - AtUri, - moderatePost, - RichText as RichTextAPI, -} from '@atproto/api' +import {AppBskyFeedPost, AtUri, moderatePost} from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Trans, useLingui} from '@lingui/react/macro' import {type RouteProp, useNavigation, useRoute} from '@react-navigation/native' @@ -16,6 +11,7 @@ import { type CommonNavigatorParams, type NavigationProp, } from '#/lib/routes/types' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import { convertBskyAppUrlIfNeeded, getChatInviteCodeFromUrl, @@ -36,6 +32,7 @@ import {ContentHider} from '#/components/moderation/ContentHider' import {PostAlerts} from '#/components/moderation/PostAlerts' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' /** @@ -106,7 +103,7 @@ export function useExtractEmbedFromFacets( for (const facet of rt.facets ?? []) { for (const feature of facet.features) { if ( - AppBskyRichtextFacet.isLink(feature) && + bsky.isType(app.bsky.richtext.facet.link, feature) && (isBskyPostUrl(feature.uri) || isBskyChatInviteUrl(feature.uri)) ) { uriFromFacet = feature.uri @@ -176,7 +173,7 @@ function MessageInputPostEmbed({ return { rt: new RichTextAPI({ text: post.record.text, - facets: post.record.facets, + facets: asSdkFacets(post.record.facets), }), record: post.record, } diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index 312a2277ad..e9d336a67a 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -25,13 +25,12 @@ import {scheduleOnRN} from 'react-native-worklets' import { type $Typed, type AppBskyEmbedRecord, - AppBskyRichtextFacet, ChatBskyConvoDefs, type ChatBskyEmbedJoinLink, ChatBskyGroupDefs, - RichText, } from '@atproto/api' import {useScrollEdgeEffectRef} from '@bsky.app/expo-scroll-edge-effect' +import {RichText} from '@bsky.app/sdk/richtext' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {mergeRefs} from '#/lib/merge-refs' @@ -52,7 +51,7 @@ import {type ConvoState, ConvoStatus} from '#/state/messages/convo/types' import {useGetJoinLinkPreview} from '#/state/queries/join-links' import {useGetPost} from '#/state/queries/post' import {createEmbedViewRecordFromPost} from '#/state/queries/postgate/util' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' import {List, type ListMethods} from '#/view/com/util/List' import {MessageComposer} from '#/screens/Messages/components/MessageComposer' import {MessageListError} from '#/screens/Messages/components/MessageListError' @@ -71,6 +70,8 @@ import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_ANDROID, IS_NATIVE, IS_WEB} from '#/env' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import {ChatStatusInfo} from './ChatStatusInfo' import {groupSystemMessages, type RenderItem} from './groupSystemMessages' import {InviteLinkDialogProvider} from './InviteLinkDialogProvider' @@ -144,7 +145,11 @@ export function MessagesList({ }) { const ax = useAnalytics() const convoState = useConvoActive() - const agent = useAgent() + /* + * Facet/mention resolution is an appview job - it resolves handles through + * the appview, and the public fallback keeps it working when logged out. + */ + const appviewClient = useAppviewClient() const {hasSession, currentAccount} = useSession() const getPost = useGetPost() const getJoinLinkPreview = useGetJoinLinkPreview() @@ -550,7 +555,8 @@ export function MessagesList({ const linkFacet = rt.facets?.find(facet => facet.features.find( feature => - AppBskyRichtextFacet.isLink(feature) && predicate(feature.uri), + bsky.isType(app.bsky.richtext.facet.link, feature) && + predicate(feature.uri), ), ) if (linkFacet) { @@ -617,7 +623,7 @@ export function MessagesList({ replyTo = {messageId: reply.id} } - await rt.detectFacets(agent) + await rt.detectFacets(appviewClient) rt = shortenLinks(rt) rt = stripInvalidMentions(rt) @@ -667,7 +673,7 @@ export function MessagesList({ } }, [ - agent, + appviewClient, convoState, getPost, getJoinLinkPreview, diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index c4d664238b..4a2f6513e5 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -5,8 +5,8 @@ import { AppBskyFeedPost, type AppBskyFeedThreadgate, AtUri, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' @@ -14,6 +14,7 @@ import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {makeProfileLink} from '#/lib/routes/links' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import {niceDate} from '#/lib/strings/time' import { POST_TOMBSTONE, @@ -199,7 +200,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ () => new RichTextAPI({ text: record.text, - facets: record.facets, + facets: asSdkFacets(record.facets), }), [record], ) diff --git a/src/screens/PostThread/components/ThreadItemPost.tsx b/src/screens/PostThread/components/ThreadItemPost.tsx index c7f5b3b5f7..bd256e43b0 100644 --- a/src/screens/PostThread/components/ThreadItemPost.tsx +++ b/src/screens/PostThread/components/ThreadItemPost.tsx @@ -4,14 +4,15 @@ import { type AppBskyFeedDefs, type AppBskyFeedThreadgate, AtUri, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Trans} from '@lingui/react/macro' import {MAX_POST_LINES} from '#/lib/constants' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {makeProfileLink} from '#/lib/routes/links' import {countLines} from '#/lib/strings/helpers' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import { POST_TOMBSTONE, type Shadow, @@ -211,7 +212,7 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({ () => new RichTextAPI({ text: record.text, - facets: record.facets, + facets: asSdkFacets(record.facets), }), [record], ) diff --git a/src/screens/PostThread/components/ThreadItemTreePost.tsx b/src/screens/PostThread/components/ThreadItemTreePost.tsx index 1560550329..3f4ae03741 100644 --- a/src/screens/PostThread/components/ThreadItemTreePost.tsx +++ b/src/screens/PostThread/components/ThreadItemTreePost.tsx @@ -4,14 +4,15 @@ import { type AppBskyFeedDefs, type AppBskyFeedThreadgate, AtUri, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Trans} from '@lingui/react/macro' import {MAX_POST_LINES} from '#/lib/constants' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {makeProfileLink} from '#/lib/routes/links' import {countLines} from '#/lib/strings/helpers' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import { POST_TOMBSTONE, type Shadow, @@ -271,7 +272,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({ () => new RichTextAPI({ text: record.text, - facets: record.facets, + facets: asSdkFacets(record.facets), }), [record], ) diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index 96e136da5c..812e65b6e2 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -5,8 +5,8 @@ import { type AppBskyLabelerDefs, moderateProfile, type ModerationOpts, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg, plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx index da01ded772..3070997b98 100644 --- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -5,8 +5,8 @@ import { moderateProfile, type ModerationDecision, type ModerationOpts, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/screens/Profile/Header/index.tsx b/src/screens/Profile/Header/index.tsx index 0b7ab16ec4..dbd8bafcab 100644 --- a/src/screens/Profile/Header/index.tsx +++ b/src/screens/Profile/Header/index.tsx @@ -12,8 +12,8 @@ import { type AppBskyLabelerDefs, moderateProfile, type ModerationOpts, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {useIsFocused} from '@react-navigation/native' import {sanitizeHandle} from '#/lib/strings/handles' diff --git a/src/screens/ProfileList/components/Header.tsx b/src/screens/ProfileList/components/Header.tsx index 9d6c4f3ff7..55f572c554 100644 --- a/src/screens/ProfileList/components/Header.tsx +++ b/src/screens/ProfileList/components/Header.tsx @@ -1,12 +1,14 @@ import {useMemo} from 'react' import {View} from 'react-native' -import {AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api' +import {AppBskyGraphDefs} from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useHaptics} from '#/lib/haptics' import {makeListLink} from '#/lib/routes/links' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import {logger} from '#/logger' import {useListBlockMutation, useListMuteMutation} from '#/state/queries/list' import { @@ -128,7 +130,7 @@ export function Header({ list.description ? new RichTextAPI({ text: list.description, - facets: list.descriptionFacets, + facets: asSdkFacets(list.descriptionFacets), }) : undefined, [list], diff --git a/src/screens/Search/modules/ExploreTrendingTopics.tsx b/src/screens/Search/modules/ExploreTrendingTopics.tsx index 38c8cac0a8..60c42323c3 100644 --- a/src/screens/Search/modules/ExploreTrendingTopics.tsx +++ b/src/screens/Search/modules/ExploreTrendingTopics.tsx @@ -1,11 +1,8 @@ import {useMemo} from 'react' import {Pressable, View} from 'react-native' import {Image} from 'expo-image' -import { - type AppBskyUnspeccedDefs, - moderateProfile, - RichText as RichTextApi, -} from '@atproto/api' +import {type AppBskyUnspeccedDefs, moderateProfile} from '@atproto/api' +import {RichText as RichTextApi} from '@bsky.app/sdk/richtext' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useModerationOpts} from '#/state/preferences/moderation-opts' diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx index ddbd5cd840..bb9c6506e9 100644 --- a/src/screens/StarterPack/StarterPackScreen.tsx +++ b/src/screens/StarterPack/StarterPackScreen.tsx @@ -6,8 +6,8 @@ import { AppBskyGraphStarterpack, AtUri, type ModerationOpts, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' @@ -24,6 +24,7 @@ import { type NavigationProp, } from '#/lib/routes/types' import {cleanError} from '#/lib/strings/errors' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import {getStarterPackOgCard} from '#/lib/strings/starter-pack' import {logger} from '#/logger' import {updateProfileShadow} from '#/state/cache/profile-shadow' @@ -418,7 +419,7 @@ function Header({ const richText = record.description ? new RichTextAPI({ text: record.description, - facets: record.descriptionFacets, + facets: asSdkFacets(record.descriptionFacets), }) : undefined diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx index 6e7bb4139a..38c2e5a89d 100644 --- a/src/screens/VideoFeed/index.tsx +++ b/src/screens/VideoFeed/index.tsx @@ -31,8 +31,8 @@ import { AppBskyFeedPost, AtUri, type ModerationDecision, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Trans, useLingui} from '@lingui/react/macro' import { type RouteProp, @@ -58,6 +58,7 @@ import { import {sanitizeDisplayName} from '#/lib/strings/display-names' import {cleanError} from '#/lib/strings/errors' import {sanitizeHandle} from '#/lib/strings/handles' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import {logger} from '#/logger' import {useA11y} from '#/state/a11y' import { @@ -834,7 +835,7 @@ function Overlay({ : undefined const richText = new RichTextAPI({ text: record?.text || '', - facets: record?.facets, + facets: asSdkFacets(record?.facets), }) const handle = sanitizeHandle(post.author.handle, '@') diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index 9a9a8c09b9..a3b90de32b 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -5,9 +5,9 @@ import { type AppBskyGraphDefs, AtUri, moderateFeedGenerator, - RichText, } from '@atproto/api' import {type AtUriString} from '@atproto/syntax' +import {RichText} from '@bsky.app/sdk/richtext' import {t} from '@lingui/core/macro' import { type InfiniteData, @@ -22,6 +22,7 @@ import { import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import {GCTIME, STALE} from '#/state/queries' import {RQKEY as listQueryKey} from '#/state/queries/list' import {usePreferencesQuery} from '#/state/queries/preferences' @@ -104,7 +105,7 @@ export function hydrateFeedGenerator( const description = new RichText({ text: view.description || '', - facets: (view.descriptionFacets || [])?.slice(), + facets: asSdkFacets((view.descriptionFacets || [])?.slice()), }) if (!view.descriptionFacets) { @@ -145,7 +146,7 @@ export function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo { const description = new RichText({ text: view.description || '', - facets: (view.descriptionFacets || [])?.slice(), + facets: asSdkFacets((view.descriptionFacets || [])?.slice()), }) if (!view.descriptionFacets) { diff --git a/src/state/queries/starter-packs.ts b/src/state/queries/starter-packs.ts index ddf8365dda..6a1aecef0e 100644 --- a/src/state/queries/starter-packs.ts +++ b/src/state/queries/starter-packs.ts @@ -3,11 +3,10 @@ import { AppBskyGraphDefs, type AppBskyGraphGetStarterPack, AppBskyGraphStarterpack, - type AppBskyRichtextFacet, type AtpAgent, AtUri, - RichText, } from '@atproto/api' +import {RichText} from '@bsky.app/sdk/richtext' import { type QueryClient, useMutation, @@ -26,7 +25,8 @@ import { import {invalidateActorStarterPacksQuery} from '#/state/queries/actor-starter-packs' import {STALE} from '#/state/queries/index' import {invalidateListMembersQuery} from '#/state/queries/list-members' -import {useAgent} from '#/state/session' +import {useAgent, useAppviewClient} from '#/state/session' +import {type app} from '#/lexicons' import * as bsky from '#/types/bsky' const RQKEY_ROOT = 'starter-pack' @@ -105,6 +105,11 @@ export function useCreateStarterPackMutation({ }) { const queryClient = useQueryClient() const agent = useAgent() + /* + * Facet/mention resolution is an appview job - it resolves handles through + * the appview, and the public fallback keeps it working when logged out. + */ + const appviewClient = useAppviewClient() return useMutation< {uri: string; cid: string}, @@ -112,10 +117,10 @@ export function useCreateStarterPackMutation({ UseCreateStarterPackMutationParams >({ mutationFn: async ({name, description, feeds, profiles}) => { - let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined + let descriptionFacets: app.bsky.richtext.facet.Main[] | undefined if (description) { const rt = new RichText({text: description}) - await rt.detectFacets(agent) + await rt.detectFacets(appviewClient) descriptionFacets = rt.facets } @@ -167,6 +172,7 @@ export function useEditStarterPackMutation({ }) { const queryClient = useQueryClient() const agent = useAgent() + const appviewClient = useAppviewClient() return useMutation< void, @@ -184,10 +190,10 @@ export function useEditStarterPackMutation({ currentStarterPack, currentListItems, }) => { - let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined + let descriptionFacets: app.bsky.richtext.facet.Main[] | undefined if (description) { const rt = new RichText({text: description}) - await rt.detectFacets(agent) + await rt.detectFacets(appviewClient) descriptionFacets = rt.facets } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 6c7c2280c2..c2e651556d 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -49,10 +49,10 @@ import { AppBskyUnspeccedDefs, AtUri, ChatBskyGroupDefs, - type RichText, } from '@atproto/api' import {type Client} from '@atproto/lex' import {type AtUriString} from '@atproto/syntax' +import {type RichText} from '@bsky.app/sdk/richtext' import {plural} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' @@ -1083,6 +1083,7 @@ export const ComposePost = ({ replyTo: replyTo?.uri, onStateChange: setPublishingStage, langs: currentLanguages, + appviewClient: client, }) ).uris[0] diff --git a/src/view/com/composer/drafts/state/api.ts b/src/view/com/composer/drafts/state/api.ts index b8fddc618f..3b5ff9cdf2 100644 --- a/src/view/com/composer/drafts/state/api.ts +++ b/src/view/com/composer/drafts/state/api.ts @@ -1,7 +1,8 @@ /** * Type converters for Draft API - convert between ComposerState and server Draft types. */ -import {AppBskyDraftDefs, AtUri, RichText} from '@atproto/api' +import {AppBskyDraftDefs, AtUri} from '@atproto/api' +import {RichText} from '@bsky.app/sdk/richtext' import {nanoid} from 'nanoid/non-secure' import {resolveLink} from '#/lib/api/resolve' diff --git a/src/view/com/composer/select-language/SuggestedLanguage.tsx b/src/view/com/composer/select-language/SuggestedLanguage.tsx index ab731cda22..71a45ec796 100644 --- a/src/view/com/composer/select-language/SuggestedLanguage.tsx +++ b/src/view/com/composer/select-language/SuggestedLanguage.tsx @@ -1,11 +1,11 @@ import {useEffect, useMemo, useRef, useState} from 'react' import {Platform, Text as RNText, View} from 'react-native' -import {RichText} from '@atproto/api' import {parseLanguageString} from '@atproto/syntax' import { guessLanguageAsync, type LanguageResult, } from '@bsky.app/expo-guess-language' +import {RichText} from '@bsky.app/sdk/richtext' import {Trans, useLingui} from '@lingui/react/macro' import debounce from 'lodash.debounce' diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index fc8b341270..3d2aa45992 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -3,9 +3,8 @@ import { type AppBskyActorDefs, type AppBskyDraftDefs, type AppBskyFeedPostgate, - AppBskyRichtextFacet, - RichText, } from '@atproto/api' +import {RichText} from '@bsky.app/sdk/richtext' import {nanoid} from 'nanoid/non-secure' import {type VideoTelemetry} from '#/lib/media/video/telemetry' @@ -28,6 +27,8 @@ import { suggestLinkCardUri, } from '#/view/com/composer/text-input/text-input-util' import {type Gif} from '#/features/gifPicker/types' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import { createVideoState, type VideoAction, @@ -677,7 +678,7 @@ export function createComposerState({ if (initRichText.facets) { for (const facet of initRichText.facets) { for (const feature of facet.features) { - if (AppBskyRichtextFacet.isLink(feature)) { + if (bsky.isType(app.bsky.richtext.facet.link, feature)) { if (isBskyPostUrl(feature.uri)) { detectedPostUris.set(feature.uri, {facet, rt: initRichText}) } else { diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 88389ffd56..d230b01545 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -12,7 +12,7 @@ import { View, } from 'react-native' import {type PasteEventPayload, TextInputWrapper} from 'expo-paste-input' -import {AppBskyRichtextFacet, RichText} from '@atproto/api' +import {RichText} from '@bsky.app/sdk/richtext' import {useLingui} from '@lingui/react/macro' import {IMAGE_SIZE_CONFIG_POSTS} from '#/lib/constants' @@ -27,6 +27,8 @@ import { import {atoms as a, useAlf} from '#/alf' import {normalizeTextStyles} from '#/alf/typography' import {IS_ANDROID} from '#/env' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import {Autocomplete} from './mobile/Autocomplete' import {type TextInputProps} from './TextInput.types' @@ -88,7 +90,7 @@ export function TextInput({ if (newRt.facets) { for (const facet of newRt.facets) { for (const feature of facet.features) { - if (AppBskyRichtextFacet.isLink(feature)) { + if (bsky.isType(app.bsky.richtext.facet.link, feature)) { if (isUriImage(feature.uri)) { const res = await downloadAndResize({ uri: feature.uri, diff --git a/src/view/com/composer/text-input/TextInput.types.ts b/src/view/com/composer/text-input/TextInput.types.ts index fab2bc32f8..e60432b41e 100644 --- a/src/view/com/composer/text-input/TextInput.types.ts +++ b/src/view/com/composer/text-input/TextInput.types.ts @@ -1,5 +1,5 @@ import {type TextInput} from 'react-native' -import {type RichText} from '@atproto/api' +import {type RichText} from '@bsky.app/sdk/richtext' export type TextInputRef = { focus: () => void diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index e76d9e3f3d..f75d60f4bd 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -8,7 +8,7 @@ import { } from 'react' import {StyleSheet, View} from 'react-native' import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' -import {AppBskyRichtextFacet, RichText} from '@atproto/api' +import {RichText} from '@bsky.app/sdk/richtext' import {Trans} from '@lingui/react/macro' import {getSchema} from '@tiptap/core' import {Document} from '@tiptap/extension-document' @@ -40,6 +40,8 @@ import {normalizeTextStyles} from '#/alf/typography' import {type Emoji} from '#/components/EmojiPicker' import {Portal} from '#/components/Portal' import {Text} from '#/components/Typography' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import {type TextInputProps} from './TextInput.types' import {type AutocompleteRef, createSuggestion} from './web/Autocomplete' import {LinkDecorator} from './web/LinkDecorator' @@ -265,7 +267,7 @@ export function TextInput({ if (newRt.facets) { for (const facet of newRt.facets) { for (const feature of facet.features) { - if (AppBskyRichtextFacet.isLink(feature)) { + if (bsky.isType(app.bsky.richtext.facet.link, feature)) { nextDetectedUris.set(feature.uri, {facet, rt: newRt}) } } diff --git a/src/view/com/composer/text-input/text-input-util.ts b/src/view/com/composer/text-input/text-input-util.ts index 71db8c3238..998d7074d9 100644 --- a/src/view/com/composer/text-input/text-input-util.ts +++ b/src/view/com/composer/text-input/text-input-util.ts @@ -1,8 +1,10 @@ -import {type AppBskyRichtextFacet, type RichText} from '@atproto/api' +import {type RichText} from '@bsky.app/sdk/richtext' + +import {type app} from '#/lexicons' export type LinkFacetMatch = { rt: RichText - facet: AppBskyRichtextFacet.Main + facet: app.bsky.richtext.facet.Main } export function suggestLinkCardUri( diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index dc17bff725..b67133df65 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -6,14 +6,15 @@ import { AtUri, moderatePost, type ModerationDecision, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {useQueryClient} from '@tanstack/react-query' import {MAX_POST_LINES} from '#/lib/constants' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {makeProfileLink} from '#/lib/routes/links' import {countLines} from '#/lib/strings/helpers' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import { POST_TOMBSTONE, type Shadow, @@ -68,7 +69,7 @@ export function Post({ record ? new RichTextAPI({ text: record.text, - facets: record.facets, + facets: asSdkFacets(record.facets), }) : undefined, [record], diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index eea2453739..2ccf3872dc 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -25,8 +25,8 @@ import { AppBskyEmbedImages, AppBskyEmbedVideo, type AppBskyFeedDefs, - type RichText as RichTextType, } from '@atproto/api' +import {type RichText as RichTextType} from '@bsky.app/sdk/richtext' import {useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx index 57f506c5d1..a95b41635b 100644 --- a/src/view/com/posts/PostFeedItem.tsx +++ b/src/view/com/posts/PostFeedItem.tsx @@ -7,8 +7,8 @@ import { AppBskyFeedThreadgate, AtUri, type ModerationDecision, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {useQueryClient} from '@tanstack/react-query' import {type ReasonFeedSource} from '#/lib/api/feed/types' @@ -17,6 +17,7 @@ import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {usePalette} from '#/lib/hooks/usePalette' import {makeProfileLink} from '#/lib/routes/links' import {countLines} from '#/lib/strings/helpers' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import { POST_TOMBSTONE, type Shadow, @@ -104,7 +105,7 @@ export function PostFeedItem({ () => new RichTextAPI({ text: record.text, - facets: record.facets, + facets: asSdkFacets(record.facets), }), [record], ) diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx index c3fe8fba8e..d92043966c 100644 --- a/src/view/screens/DebugMod.tsx +++ b/src/view/screens/DebugMod.tsx @@ -15,8 +15,8 @@ import { type ModerationBehavior, type ModerationDecision, type ModerationOpts, - RichText, } from '@atproto/api' +import {RichText} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index ead74bae94..c796bf9586 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -6,8 +6,8 @@ import { type AppBskyActorDefs, moderateProfile, type ModerationOpts, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -33,7 +33,7 @@ import {useLabelerInfoQuery} from '#/state/queries/labeler' import {resetProfilePostsQueries} from '#/state/queries/post-feed' import {useProfileQuery} from '#/state/queries/profile' import {useResolveDidQuery} from '#/state/queries/resolve-uri' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' import {ProfileFeedgens} from '#/view/com/feeds/ProfileFeedgens' import {ProfileLists} from '#/view/com/lists/ProfileLists' import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader' @@ -613,7 +613,11 @@ function ProfileScreenLoaded({ } function useRichText(text: string): [RichTextAPI, boolean] { - const agent = useAgent() + /* + * Facet/mention resolution is an appview job - it resolves handles through + * the appview, and the public fallback keeps it working when logged out. + */ + const client = useAppviewClient() const [prevText, setPrevText] = useState(text) const [rawRT, setRawRT] = useState(() => new RichTextAPI({text})) const [resolvedRT, setResolvedRT] = useState(null) @@ -628,7 +632,7 @@ function useRichText(text: string): [RichTextAPI, boolean] { async function resolveRTFacets() { // new each time const resolvedRT = new RichTextAPI({text}) - await resolvedRT.detectFacets(agent) + await resolvedRT.detectFacets(client) if (!ignore) { setResolvedRT(resolvedRT) } @@ -637,7 +641,7 @@ function useRichText(text: string): [RichTextAPI, boolean] { return () => { ignore = true } - }, [text, agent]) + }, [text, client]) const isResolving = resolvedRT === null return [resolvedRT ?? rawRT, isResolving] }