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] }