[SDK] Adopt @bsky.app/sdk and migrate RichText (#11369)

This commit is contained in:
Samuel Newman
2026-08-13 22:26:18 +03:00
committed by GitHub
parent d991118215
commit f6a22c4f05
47 changed files with 231 additions and 124 deletions
+2 -1
View File
@@ -115,6 +115,7 @@
"@bsky.app/expo-translate-text": "^0.2.9", "@bsky.app/expo-translate-text": "^0.2.9",
"@bsky.app/peek-menu": "^0.3.2", "@bsky.app/peek-menu": "^0.3.2",
"@bsky.app/react-native-uitextview": "^2.7.0", "@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/sift": "^0.3.9",
"@bsky.app/tapper": "^0.6.1", "@bsky.app/tapper": "^0.6.1",
"@bsky.app/video": "0.3.6", "@bsky.app/video": "0.3.6",
@@ -339,7 +340,7 @@
"^await-lock$": "<rootDir>/node_modules/await-lock/build/AwaitLock.js" "^await-lock$": "<rootDir>/node_modules/await-lock/build/AwaitLock.js"
}, },
"transformIgnorePatterns": [ "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": [ "modulePathIgnorePatterns": [
"__tests__/.*/__mocks__", "__tests__/.*/__mocks__",
+27
View File
@@ -287,6 +287,9 @@ importers:
'@bsky.app/react-native-uitextview': '@bsky.app/react-native-uitextview':
specifier: ^2.7.0 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) 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': '@bsky.app/sift':
specifier: ^0.3.9 specifier: ^0.3.9
version: 0.3.9(2d5c14016450c7e55c8cd0dffcca6f69) version: 0.3.9(2d5c14016450c7e55c8cd0dffcca6f69)
@@ -889,6 +892,10 @@ packages:
resolution: {integrity: sha512-iDFZEoNqfL17EVKE+uNzdUD7YF8LWhEhxeBtP6x+PNuAxoXv3ip9vLmB8nNzNxFwVn++FipfDtSiPqmziv1bdA==} resolution: {integrity: sha512-iDFZEoNqfL17EVKE+uNzdUD7YF8LWhEhxeBtP6x+PNuAxoXv3ip9vLmB8nNzNxFwVn++FipfDtSiPqmziv1bdA==}
engines: {node: '>=22'} 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': '@atproto-labs/pipe@0.2.4':
resolution: {integrity: sha512-n67jCcrC+ouAeO10cWkpPzzLMlDi/lDCU30Us+LGqhOPhT6c4t5ASdBLQi9W3jUQtRzQBt3G9zipF+xKWNvVbw==} resolution: {integrity: sha512-n67jCcrC+ouAeO10cWkpPzzLMlDi/lDCU30Us+LGqhOPhT6c4t5ASdBLQi9W3jUQtRzQBt3G9zipF+xKWNvVbw==}
engines: {node: '>=22'} engines: {node: '>=22'}
@@ -4505,6 +4512,12 @@ packages:
bser@2.1.1: bser@2.1.1:
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} 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: buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
@@ -9725,6 +9738,13 @@ snapshots:
dependencies: dependencies:
'@atproto-labs/pipe': 0.2.4 '@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/pipe@0.2.4': {}
'@atproto-labs/simple-store-memory@0.2.6': '@atproto-labs/simple-store-memory@0.2.6':
@@ -14063,6 +14083,13 @@ snapshots:
dependencies: dependencies:
node-int64: 0.4.0 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-crc32@0.2.13: {}
buffer-from@1.1.2: {} buffer-from@1.1.2: {}
+2
View File
@@ -44,6 +44,7 @@ patchedDependencies:
'react-native@0.81.5': patches/react-native@0.81.5.patch 'react-native@0.81.5': patches/react-native@0.81.5.patch
minimumReleaseAgeExclude: minimumReleaseAgeExclude:
- '@atproto/*' - '@atproto/*'
- '@atproto-labs/*'
- '@bsky.app/*' - '@bsky.app/*'
# todo: remove when old enough # todo: remove when old enough
- '@oxlint-tsgolint/darwin-arm64@7.0.2001' - '@oxlint-tsgolint/darwin-arm64@7.0.2001'
@@ -53,3 +54,4 @@ minimumReleaseAgeExclude:
- '@oxlint-tsgolint/win32-arm64@7.0.2001' - '@oxlint-tsgolint/win32-arm64@7.0.2001'
- '@oxlint-tsgolint/win32-x64@7.0.2001' - '@oxlint-tsgolint/win32-x64@7.0.2001'
- oxlint-tsgolint@7.0.2001 - oxlint-tsgolint@7.0.2001
- bsky-sdk-preview@0.0.1-preview.2
+2 -6
View File
@@ -1,11 +1,7 @@
import {useCallback, useEffect, useMemo} from 'react' import {useCallback, useEffect, useMemo} from 'react'
import {type GestureResponderEvent, View} from 'react-native' import {type GestureResponderEvent, View} from 'react-native'
import { import {type AppBskyFeedDefs, type AppBskyGraphDefs, AtUri} from '@atproto/api'
type AppBskyFeedDefs, import {RichText as RichTextApi} from '@bsky.app/sdk/richtext'
type AppBskyGraphDefs,
AtUri,
RichText as RichTextApi,
} from '@atproto/api'
import {Plural, Trans, useLingui} from '@lingui/react/macro' import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
+3 -2
View File
@@ -6,12 +6,13 @@ import {
AppBskyFeedPost, AppBskyFeedPost,
AtUri, AtUri,
moderatePost, moderatePost,
RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {getChatInviteCodeFromUrl} from '#/lib/strings/url-helpers' import {getChatInviteCodeFromUrl} from '#/lib/strings/url-helpers'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {unstableCacheProfileView} from '#/state/queries/profile' import {unstableCacheProfileView} from '#/state/queries/profile'
@@ -295,7 +296,7 @@ export function QuoteEmbed({
return undefined return undefined
const {text, facets} = quote.record const {text, facets} = quote.record
return text.trim() return text.trim()
? new RichTextAPI({text: text, facets: facets}) ? new RichTextAPI({text: text, facets: asSdkFacets(facets)})
: undefined : undefined
}, [quote.record]) }, [quote.record])
@@ -11,8 +11,8 @@ import {
type AppBskyFeedPost, type AppBskyFeedPost,
type AppBskyFeedThreadgate, type AppBskyFeedThreadgate,
AtUri, AtUri,
type RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {plural} from '@lingui/core/macro' import {plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react/macro' import {useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
@@ -4,8 +4,8 @@ import {
type AppBskyFeedDefs, type AppBskyFeedDefs,
type AppBskyFeedPost, type AppBskyFeedPost,
type AppBskyFeedThreadgate, type AppBskyFeedThreadgate,
type RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {useLingui} from '@lingui/react/macro' import {useLingui} from '@lingui/react/macro'
import {type Shadow} from '#/state/cache/post-shadow' import {type Shadow} from '#/state/cache/post-shadow'
@@ -3,8 +3,8 @@ import {
type AppBskyFeedDefs, type AppBskyFeedDefs,
type AppBskyFeedPost, type AppBskyFeedPost,
type AppBskyFeedThreadgate, type AppBskyFeedThreadgate,
type RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {type Shadow} from '#/state/cache/post-shadow' import {type Shadow} from '#/state/cache/post-shadow'
@@ -5,8 +5,8 @@ import {
type AppBskyFeedPost, type AppBskyFeedPost,
type AppBskyFeedThreadgate, type AppBskyFeedThreadgate,
AtUri, AtUri,
type RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
+1 -1
View File
@@ -4,8 +4,8 @@ import {
type AppBskyFeedDefs, type AppBskyFeedDefs,
type AppBskyFeedPost, type AppBskyFeedPost,
type AppBskyFeedThreadgate, type AppBskyFeedThreadgate,
type RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {plural} from '@lingui/core/macro' import {plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react/macro' import {useLingui} from '@lingui/react/macro'
+2 -5
View File
@@ -6,11 +6,8 @@ import {
View, View,
type ViewStyle, type ViewStyle,
} from 'react-native' } from 'react-native'
import { import {moderateProfile, type ModerationOpts} from '@atproto/api'
moderateProfile, import {RichText as RichTextApi} from '@bsky.app/sdk/richtext'
type ModerationOpts,
RichText as RichTextApi,
} from '@atproto/api'
import {useLingui} from '@lingui/react/macro' import {useLingui} from '@lingui/react/macro'
import {getModerationCauseKey} from '#/lib/moderation' import {getModerationCauseKey} from '#/lib/moderation'
+6 -4
View File
@@ -1,6 +1,6 @@
import {useMemo} from 'react' import {useMemo} from 'react'
import {type StyleProp, type TextStyle} from 'react-native' 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 {toShortUrl} from '#/lib/strings/url-helpers'
import {android, atoms as a, flatten, type TextStyleProp} from '#/alf' 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 {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {RichTextTag} from '#/components/RichTextTag' import {RichTextTag} from '#/components/RichTextTag'
import {Text, type TextProps} from '#/components/Typography' import {Text, type TextProps} from '#/components/Typography'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
const WORD_WRAP = {wordWrap: 1} const WORD_WRAP = {wordWrap: 1}
// lifted from facet detection in `RichText` impl, _without_ `gm` flags // lifted from facet detection in `RichText` impl, _without_ `gm` flags
@@ -139,7 +141,7 @@ export function RichText({
if ( if (
mention && mention &&
(disableMentionFacetValidation || (disableMentionFacetValidation ||
AppBskyRichtextFacet.validateMention(mention).success) && bsky.matches(app.bsky.richtext.facet.mention, mention)) &&
!disableLinks !disableLinks
) { ) {
els.push( els.push(
@@ -156,7 +158,7 @@ export function RichText({
</InlineLinkText> </InlineLinkText>
</ProfileHoverCard>, </ProfileHoverCard>,
) )
} 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) const isValidLink = URL_REGEX.test(link.uri)
if (!isValidLink || disableLinks) { if (!isValidLink || disableLinks) {
els.push(toShortUrl(segment.text)) els.push(toShortUrl(segment.text))
@@ -181,7 +183,7 @@ export function RichText({
!disableLinks && !disableLinks &&
enableTags && enableTags &&
tag && tag &&
AppBskyRichtextFacet.validateTag(tag).success bsky.matches(app.bsky.richtext.facet.tag, tag)
) { ) {
els.push( els.push(
<RichTextTag <RichTextTag
@@ -1,13 +1,14 @@
import {useCallback, useEffect, useMemo, useState} from 'react' import {useCallback, useEffect, useMemo, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {type AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api' import {type AppBskyGraphDefs} from '@atproto/api'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/react/macro' import {Plural, Trans} from '@lingui/react/macro'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {isOverMaxGraphemeCount} from '#/lib/strings/helpers' import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
import {richTextToString} from '#/lib/strings/rich-text-helpers' import {asSdkFacets, richTextToString} from '#/lib/strings/rich-text-helpers'
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
import {logger} from '#/logger' import {logger} from '#/logger'
import {type ImageMeta} from '#/state/gallery' import {type ImageMeta} from '#/state/gallery'
@@ -15,7 +16,7 @@ import {
useListCreateMutation, useListCreateMutation,
useListMetadataMutation, useListMetadataMutation,
} from '#/state/queries/list' } from '#/state/queries/list'
import {useAgent} from '#/state/session' import {useAppviewClient} from '#/state/session'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {EditableUserAvatar} from '#/view/com/util/UserAvatar' import {EditableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme, web} from '#/alf' import {atoms as a, useTheme, web} from '#/alf'
@@ -133,7 +134,11 @@ function DialogInner({
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
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 control = Dialog.useDialogContext() const control = Dialog.useDialogContext()
const { const {
mutateAsync: createListMutation, mutateAsync: createListMutation,
@@ -163,7 +168,10 @@ function DialogInner({
// We want to be working with a blank state here, so let's get the // We want to be working with a blank state here, so let's get the
// serialized version and turn it back into a RichText // serialized version and turn it back into a RichText
const serialized = richTextToString(new RichTextAPI({text, facets}), false) const serialized = richTextToString(
new RichTextAPI({text, facets: asSdkFacets(facets)}),
false,
)
const richText = new RichTextAPI({text: serialized}) const richText = new RichTextAPI({text: serialized})
richText.detectFacetsWithoutResolution() richText.detectFacetsWithoutResolution()
@@ -224,7 +232,7 @@ function DialogInner({
{cleanNewlines: true}, {cleanNewlines: true},
) )
await richText.detectFacets(agent) await richText.detectFacets(appviewClient)
richText = shortenLinks(richText) richText = shortenLinks(richText)
richText = stripInvalidMentions(richText) richText = stripInvalidMentions(richText)
@@ -272,7 +280,7 @@ function DialogInner({
setImageError, setImageError,
activePurpose, activePurpose,
isCurateList, isCurateList,
agent, appviewClient,
_, _,
]) ])
+4 -7
View File
@@ -2,17 +2,14 @@ import {memo, useCallback} from 'react'
import {Platform} from 'react-native' import {Platform} from 'react-native'
import {type GestureType} from 'react-native-gesture-handler' import {type GestureType} from 'react-native-gesture-handler'
import * as Clipboard from 'expo-clipboard' import * as Clipboard from 'expo-clipboard'
import { import {type ChatBskyConvoDefs, type ModerationOpts} from '@atproto/api'
type ChatBskyConvoDefs, import {RichText} from '@bsky.app/sdk/richtext'
type ModerationOpts,
RichText,
} from '@atproto/api'
import {plural} from '@lingui/core/macro' import {plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react/macro' import {useLingui} from '@lingui/react/macro'
import {EMOJI_REACTION_LIMIT} from '#/lib/constants' import {EMOJI_REACTION_LIMIT} from '#/lib/constants'
import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate' import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'
import {richTextToString} from '#/lib/strings/rich-text-helpers' import {asSdkFacets, richTextToString} from '#/lib/strings/rich-text-helpers'
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
import {useConvoActive} from '#/state/messages/convo' import {useConvoActive} from '#/state/messages/convo'
import {useLanguagePrefs} from '#/state/preferences' import {useLanguagePrefs} from '#/state/preferences'
@@ -88,7 +85,7 @@ export let MessageContextMenu = ({
const str = richTextToString( const str = richTextToString(
new RichText({ new RichText({
text: message.text, text: message.text,
facets: message.facets, facets: asSdkFacets(message.facets),
}), }),
true, true,
) )
+6 -2
View File
@@ -28,8 +28,8 @@ import {
ChatBskyConvoDefs, ChatBskyConvoDefs,
ChatBskyEmbedJoinLink, ChatBskyEmbedJoinLink,
moderateProfile, moderateProfile,
RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {plural} from '@lingui/core/macro' import {plural} from '@lingui/core/macro'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
@@ -37,6 +37,7 @@ import {useQueryClient} from '@tanstack/react-query'
import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted' import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
import {type Shadow} from '#/state/cache/types' import {type Shadow} from '#/state/cache/types'
import {type ConvoItem} from '#/state/messages/convo/types' import {type ConvoItem} from '#/state/messages/convo/types'
@@ -245,7 +246,10 @@ let MessageItem = ({
? t.palette.primary_300 ? t.palette.primary_300
: t.palette.primary_100 : t.palette.primary_100
const rt = new RichTextAPI({text: message.text, facets: message.facets}) const rt = new RichTextAPI({
text: message.text,
facets: asSdkFacets(message.facets),
})
const isEmojiOnly = isOnlyEmoji(message.text) const isEmojiOnly = isOnlyEmoji(message.text)
+10 -5
View File
@@ -1,13 +1,18 @@
import {useEffect, useState} from 'react' import {useEffect, useState} from 'react'
import {RichText as RichTextAPI} from '@atproto/api' import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {useAgent} from '#/state/session' import {useAppviewClient} from '#/state/session'
export function useRichText(text: string): [RichTextAPI, boolean] { export function useRichText(text: string): [RichTextAPI, boolean] {
const [prevText, setPrevText] = useState(text) const [prevText, setPrevText] = useState(text)
const [rawRT, setRawRT] = useState(() => new RichTextAPI({text})) const [rawRT, setRawRT] = useState(() => new RichTextAPI({text}))
const [resolvedRT, setResolvedRT] = useState<RichTextAPI | null>(null) const [resolvedRT, setResolvedRT] = useState<RichTextAPI | null>(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) { if (text !== prevText) {
setPrevText(text) setPrevText(text)
setRawRT(new RichTextAPI({text})) setRawRT(new RichTextAPI({text}))
@@ -19,7 +24,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] {
async function resolveRTFacets() { async function resolveRTFacets() {
// new each time // new each time
const resolvedRT = new RichTextAPI({text}) const resolvedRT = new RichTextAPI({text})
await resolvedRT.detectFacets(agent) await resolvedRT.detectFacets(client)
if (!ignore) { if (!ignore) {
setResolvedRT(resolvedRT) setResolvedRT(resolvedRT)
} }
@@ -28,7 +33,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] {
return () => { return () => {
ignore = true ignore = true
} }
}, [text, agent]) }, [text, client])
const isResolving = resolvedRT === null const isResolving = resolvedRT === null
return [resolvedRT ?? rawRT, isResolving] return [resolvedRT ?? rawRT, isResolving]
} }
+11 -4
View File
@@ -13,9 +13,10 @@ import {
type ComAtprotoLabelDefs, type ComAtprotoLabelDefs,
type ComAtprotoRepoApplyWrites, type ComAtprotoRepoApplyWrites,
type ComAtprotoRepoStrongRef, type ComAtprotoRepoStrongRef,
RichText,
} from '@atproto/api' } from '@atproto/api'
import {TID} from '@atproto/common-web' 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 {t} from '@lingui/core/macro'
import {type QueryClient} from '@tanstack/react-query' import {type QueryClient} from '@tanstack/react-query'
import {sha256} from 'js-sha256' import {sha256} from 'js-sha256'
@@ -51,6 +52,12 @@ interface PostOpts {
replyTo?: string replyTo?: string
onStateChange?: (state: string) => void onStateChange?: (state: string) => void
langs?: string[] 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( export async function post(
@@ -87,7 +94,7 @@ export async function post(
const draft = thread.posts[i] const draft = thread.posts[i]
// Not awaited to avoid waterfalls. // Not awaited to avoid waterfalls.
const rtPromise = resolveRT(agent, draft.richtext) const rtPromise = resolveRT(opts.appviewClient, draft.richtext)
const embedPromise = resolveEmbed( const embedPromise = resolveEmbed(
agent, agent,
queryClient, queryClient,
@@ -196,14 +203,14 @@ export async function post(
return {uris} return {uris}
} }
async function resolveRT(agent: AtpAgent, richtext: RichText) { async function resolveRT(appviewClient: Client, richtext: RichText) {
const trimmedText = richtext.text const trimmedText = richtext.text
// Trim leading whitespace-only lines (but don't break ASCII art). // Trim leading whitespace-only lines (but don't break ASCII art).
.replace(/^(\s*\n)+/, '') .replace(/^(\s*\n)+/, '')
// Trim any trailing whitespace. // Trim any trailing whitespace.
.trimEnd() .trimEnd()
let rt = new RichText({text: trimmedText}, {cleanNewlines: true}) let rt = new RichText({text: trimmedText}, {cleanNewlines: true})
await rt.detectFacets(agent) await rt.detectFacets(appviewClient)
rt = shortenLinks(rt) rt = shortenLinks(rt)
rt = stripInvalidMentions(rt) rt = stripInvalidMentions(rt)
+1 -1
View File
@@ -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 {countGraphemes} from 'unicode-segmenter/grapheme'
import {shortenLinks} from './rich-text-manip' import {shortenLinks} from './rich-text-manip'
+27 -2
View File
@@ -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' import {linkRequiresWarning} from './url-helpers'
export function richTextToString(rt: RichText, loose: boolean): string { 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()) { for (const segment of rt.segments()) {
const link = segment.link 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 href = link.uri
const text = segment.text const text = segment.text
@@ -28,3 +30,26 @@ export function richTextToString(rt: RichText, loose: boolean): string {
return result 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
}
+9 -3
View File
@@ -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' import {toShortUrl} from './url-helpers'
export function shortenLinks(rt: RichText): RichText { export function shortenLinks(rt: RichText): RichText {
@@ -10,7 +12,9 @@ export function shortenLinks(rt: RichText): RichText {
// enumerate the link facets // enumerate the link facets
if (rt.facets) { if (rt.facets) {
for (const facet of 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) { if (!isLink) {
continue continue
} }
@@ -40,7 +44,9 @@ export function stripInvalidMentions(rt: RichText): RichText {
rt = rt.clone() rt = rt.clone()
if (rt.facets) { if (rt.facets) {
rt.facets = rt.facets?.filter(facet => { 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) { if (mention && !mention.did) {
return false return false
} }
@@ -1,12 +1,7 @@
import {useCallback, useEffect, useMemo, useState} from 'react' import {useCallback, useEffect, useMemo, useState} from 'react'
import {LayoutAnimation, View} from 'react-native' import {LayoutAnimation, View} from 'react-native'
import { import {AppBskyFeedPost, AtUri, moderatePost} from '@atproto/api'
AppBskyFeedPost, import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
AppBskyRichtextFacet,
AtUri,
moderatePost,
RichText as RichTextAPI,
} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {type RouteProp, useNavigation, useRoute} from '@react-navigation/native' import {type RouteProp, useNavigation, useRoute} from '@react-navigation/native'
@@ -16,6 +11,7 @@ import {
type CommonNavigatorParams, type CommonNavigatorParams,
type NavigationProp, type NavigationProp,
} from '#/lib/routes/types' } from '#/lib/routes/types'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import { import {
convertBskyAppUrlIfNeeded, convertBskyAppUrlIfNeeded,
getChatInviteCodeFromUrl, getChatInviteCodeFromUrl,
@@ -36,6 +32,7 @@ import {ContentHider} from '#/components/moderation/ContentHider'
import {PostAlerts} from '#/components/moderation/PostAlerts' import {PostAlerts} from '#/components/moderation/PostAlerts'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky' import * as bsky from '#/types/bsky'
/** /**
@@ -106,7 +103,7 @@ export function useExtractEmbedFromFacets(
for (const facet of rt.facets ?? []) { for (const facet of rt.facets ?? []) {
for (const feature of facet.features) { for (const feature of facet.features) {
if ( if (
AppBskyRichtextFacet.isLink(feature) && bsky.isType(app.bsky.richtext.facet.link, feature) &&
(isBskyPostUrl(feature.uri) || isBskyChatInviteUrl(feature.uri)) (isBskyPostUrl(feature.uri) || isBskyChatInviteUrl(feature.uri))
) { ) {
uriFromFacet = feature.uri uriFromFacet = feature.uri
@@ -176,7 +173,7 @@ function MessageInputPostEmbed({
return { return {
rt: new RichTextAPI({ rt: new RichTextAPI({
text: post.record.text, text: post.record.text,
facets: post.record.facets, facets: asSdkFacets(post.record.facets),
}), }),
record: post.record, record: post.record,
} }
@@ -25,13 +25,12 @@ import {scheduleOnRN} from 'react-native-worklets'
import { import {
type $Typed, type $Typed,
type AppBskyEmbedRecord, type AppBskyEmbedRecord,
AppBskyRichtextFacet,
ChatBskyConvoDefs, ChatBskyConvoDefs,
type ChatBskyEmbedJoinLink, type ChatBskyEmbedJoinLink,
ChatBskyGroupDefs, ChatBskyGroupDefs,
RichText,
} from '@atproto/api' } from '@atproto/api'
import {useScrollEdgeEffectRef} from '@bsky.app/expo-scroll-edge-effect' import {useScrollEdgeEffectRef} from '@bsky.app/expo-scroll-edge-effect'
import {RichText} from '@bsky.app/sdk/richtext'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {mergeRefs} from '#/lib/merge-refs' 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 {useGetJoinLinkPreview} from '#/state/queries/join-links'
import {useGetPost} from '#/state/queries/post' import {useGetPost} from '#/state/queries/post'
import {createEmbedViewRecordFromPost} from '#/state/queries/postgate/util' 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 {List, type ListMethods} from '#/view/com/util/List'
import {MessageComposer} from '#/screens/Messages/components/MessageComposer' import {MessageComposer} from '#/screens/Messages/components/MessageComposer'
import {MessageListError} from '#/screens/Messages/components/MessageListError' import {MessageListError} from '#/screens/Messages/components/MessageListError'
@@ -71,6 +70,8 @@ import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {IS_ANDROID, IS_NATIVE, IS_WEB} from '#/env' import {IS_ANDROID, IS_NATIVE, IS_WEB} from '#/env'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {ChatStatusInfo} from './ChatStatusInfo' import {ChatStatusInfo} from './ChatStatusInfo'
import {groupSystemMessages, type RenderItem} from './groupSystemMessages' import {groupSystemMessages, type RenderItem} from './groupSystemMessages'
import {InviteLinkDialogProvider} from './InviteLinkDialogProvider' import {InviteLinkDialogProvider} from './InviteLinkDialogProvider'
@@ -144,7 +145,11 @@ export function MessagesList({
}) { }) {
const ax = useAnalytics() const ax = useAnalytics()
const convoState = useConvoActive() 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 {hasSession, currentAccount} = useSession()
const getPost = useGetPost() const getPost = useGetPost()
const getJoinLinkPreview = useGetJoinLinkPreview() const getJoinLinkPreview = useGetJoinLinkPreview()
@@ -550,7 +555,8 @@ export function MessagesList({
const linkFacet = rt.facets?.find(facet => const linkFacet = rt.facets?.find(facet =>
facet.features.find( facet.features.find(
feature => feature =>
AppBskyRichtextFacet.isLink(feature) && predicate(feature.uri), bsky.isType(app.bsky.richtext.facet.link, feature) &&
predicate(feature.uri),
), ),
) )
if (linkFacet) { if (linkFacet) {
@@ -617,7 +623,7 @@ export function MessagesList({
replyTo = {messageId: reply.id} replyTo = {messageId: reply.id}
} }
await rt.detectFacets(agent) await rt.detectFacets(appviewClient)
rt = shortenLinks(rt) rt = shortenLinks(rt)
rt = stripInvalidMentions(rt) rt = stripInvalidMentions(rt)
@@ -667,7 +673,7 @@ export function MessagesList({
} }
}, },
[ [
agent, appviewClient,
convoState, convoState,
getPost, getPost,
getJoinLinkPreview, getJoinLinkPreview,
@@ -5,8 +5,8 @@ import {
AppBskyFeedPost, AppBskyFeedPost,
type AppBskyFeedThreadgate, type AppBskyFeedThreadgate,
AtUri, AtUri,
RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {Plural, Trans, useLingui} from '@lingui/react/macro' import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
@@ -14,6 +14,7 @@ import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {niceDate} from '#/lib/strings/time' import {niceDate} from '#/lib/strings/time'
import { import {
POST_TOMBSTONE, POST_TOMBSTONE,
@@ -199,7 +200,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
() => () =>
new RichTextAPI({ new RichTextAPI({
text: record.text, text: record.text,
facets: record.facets, facets: asSdkFacets(record.facets),
}), }),
[record], [record],
) )
@@ -4,14 +4,15 @@ import {
type AppBskyFeedDefs, type AppBskyFeedDefs,
type AppBskyFeedThreadgate, type AppBskyFeedThreadgate,
AtUri, AtUri,
RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
import {MAX_POST_LINES} from '#/lib/constants' import {MAX_POST_LINES} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {countLines} from '#/lib/strings/helpers' import {countLines} from '#/lib/strings/helpers'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import { import {
POST_TOMBSTONE, POST_TOMBSTONE,
type Shadow, type Shadow,
@@ -211,7 +212,7 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({
() => () =>
new RichTextAPI({ new RichTextAPI({
text: record.text, text: record.text,
facets: record.facets, facets: asSdkFacets(record.facets),
}), }),
[record], [record],
) )
@@ -4,14 +4,15 @@ import {
type AppBskyFeedDefs, type AppBskyFeedDefs,
type AppBskyFeedThreadgate, type AppBskyFeedThreadgate,
AtUri, AtUri,
RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
import {MAX_POST_LINES} from '#/lib/constants' import {MAX_POST_LINES} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {countLines} from '#/lib/strings/helpers' import {countLines} from '#/lib/strings/helpers'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import { import {
POST_TOMBSTONE, POST_TOMBSTONE,
type Shadow, type Shadow,
@@ -271,7 +272,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
() => () =>
new RichTextAPI({ new RichTextAPI({
text: record.text, text: record.text,
facets: record.facets, facets: asSdkFacets(record.facets),
}), }),
[record], [record],
) )
@@ -5,8 +5,8 @@ import {
type AppBskyLabelerDefs, type AppBskyLabelerDefs,
moderateProfile, moderateProfile,
type ModerationOpts, type ModerationOpts,
type RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {msg, plural} from '@lingui/core/macro' import {msg, plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/react/macro' import {Plural, Trans} from '@lingui/react/macro'
@@ -5,8 +5,8 @@ import {
moderateProfile, moderateProfile,
type ModerationDecision, type ModerationDecision,
type ModerationOpts, type ModerationOpts,
type RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
+1 -1
View File
@@ -12,8 +12,8 @@ import {
type AppBskyLabelerDefs, type AppBskyLabelerDefs,
moderateProfile, moderateProfile,
type ModerationOpts, type ModerationOpts,
type RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {useIsFocused} from '@react-navigation/native' import {useIsFocused} from '@react-navigation/native'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
@@ -1,12 +1,14 @@
import {useMemo} from 'react' import {useMemo} from 'react'
import {View} from 'react-native' 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 {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {makeListLink} from '#/lib/routes/links' import {makeListLink} from '#/lib/routes/links'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useListBlockMutation, useListMuteMutation} from '#/state/queries/list' import {useListBlockMutation, useListMuteMutation} from '#/state/queries/list'
import { import {
@@ -128,7 +130,7 @@ export function Header({
list.description list.description
? new RichTextAPI({ ? new RichTextAPI({
text: list.description, text: list.description,
facets: list.descriptionFacets, facets: asSdkFacets(list.descriptionFacets),
}) })
: undefined, : undefined,
[list], [list],
@@ -1,11 +1,8 @@
import {useMemo} from 'react' import {useMemo} from 'react'
import {Pressable, View} from 'react-native' import {Pressable, View} from 'react-native'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import { import {type AppBskyUnspeccedDefs, moderateProfile} from '@atproto/api'
type AppBskyUnspeccedDefs, import {RichText as RichTextApi} from '@bsky.app/sdk/richtext'
moderateProfile,
RichText as RichTextApi,
} from '@atproto/api'
import {Plural, Trans, useLingui} from '@lingui/react/macro' import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -6,8 +6,8 @@ import {
AppBskyGraphStarterpack, AppBskyGraphStarterpack,
AtUri, AtUri,
type ModerationOpts, type ModerationOpts,
RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/react/macro' import {Plural, Trans} from '@lingui/react/macro'
@@ -24,6 +24,7 @@ import {
type NavigationProp, type NavigationProp,
} from '#/lib/routes/types' } from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {getStarterPackOgCard} from '#/lib/strings/starter-pack' import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
import {logger} from '#/logger' import {logger} from '#/logger'
import {updateProfileShadow} from '#/state/cache/profile-shadow' import {updateProfileShadow} from '#/state/cache/profile-shadow'
@@ -418,7 +419,7 @@ function Header({
const richText = record.description const richText = record.description
? new RichTextAPI({ ? new RichTextAPI({
text: record.description, text: record.description,
facets: record.descriptionFacets, facets: asSdkFacets(record.descriptionFacets),
}) })
: undefined : undefined
+3 -2
View File
@@ -31,8 +31,8 @@ import {
AppBskyFeedPost, AppBskyFeedPost,
AtUri, AtUri,
type ModerationDecision, type ModerationDecision,
RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import { import {
type RouteProp, type RouteProp,
@@ -58,6 +58,7 @@ import {
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useA11y} from '#/state/a11y' import {useA11y} from '#/state/a11y'
import { import {
@@ -834,7 +835,7 @@ function Overlay({
: undefined : undefined
const richText = new RichTextAPI({ const richText = new RichTextAPI({
text: record?.text || '', text: record?.text || '',
facets: record?.facets, facets: asSdkFacets(record?.facets),
}) })
const handle = sanitizeHandle(post.author.handle, '@') const handle = sanitizeHandle(post.author.handle, '@')
+4 -3
View File
@@ -5,9 +5,9 @@ import {
type AppBskyGraphDefs, type AppBskyGraphDefs,
AtUri, AtUri,
moderateFeedGenerator, moderateFeedGenerator,
RichText,
} from '@atproto/api' } from '@atproto/api'
import {type AtUriString} from '@atproto/syntax' import {type AtUriString} from '@atproto/syntax'
import {RichText} from '@bsky.app/sdk/richtext'
import {t} from '@lingui/core/macro' import {t} from '@lingui/core/macro'
import { import {
type InfiniteData, type InfiniteData,
@@ -22,6 +22,7 @@ import {
import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants' import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {GCTIME, STALE} from '#/state/queries' import {GCTIME, STALE} from '#/state/queries'
import {RQKEY as listQueryKey} from '#/state/queries/list' import {RQKEY as listQueryKey} from '#/state/queries/list'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
@@ -104,7 +105,7 @@ export function hydrateFeedGenerator(
const description = new RichText({ const description = new RichText({
text: view.description || '', text: view.description || '',
facets: (view.descriptionFacets || [])?.slice(), facets: asSdkFacets((view.descriptionFacets || [])?.slice()),
}) })
if (!view.descriptionFacets) { if (!view.descriptionFacets) {
@@ -145,7 +146,7 @@ export function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo {
const description = new RichText({ const description = new RichText({
text: view.description || '', text: view.description || '',
facets: (view.descriptionFacets || [])?.slice(), facets: asSdkFacets((view.descriptionFacets || [])?.slice()),
}) })
if (!view.descriptionFacets) { if (!view.descriptionFacets) {
+13 -7
View File
@@ -3,11 +3,10 @@ import {
AppBskyGraphDefs, AppBskyGraphDefs,
type AppBskyGraphGetStarterPack, type AppBskyGraphGetStarterPack,
AppBskyGraphStarterpack, AppBskyGraphStarterpack,
type AppBskyRichtextFacet,
type AtpAgent, type AtpAgent,
AtUri, AtUri,
RichText,
} from '@atproto/api' } from '@atproto/api'
import {RichText} from '@bsky.app/sdk/richtext'
import { import {
type QueryClient, type QueryClient,
useMutation, useMutation,
@@ -26,7 +25,8 @@ import {
import {invalidateActorStarterPacksQuery} from '#/state/queries/actor-starter-packs' import {invalidateActorStarterPacksQuery} from '#/state/queries/actor-starter-packs'
import {STALE} from '#/state/queries/index' import {STALE} from '#/state/queries/index'
import {invalidateListMembersQuery} from '#/state/queries/list-members' 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' import * as bsky from '#/types/bsky'
const RQKEY_ROOT = 'starter-pack' const RQKEY_ROOT = 'starter-pack'
@@ -105,6 +105,11 @@ export function useCreateStarterPackMutation({
}) { }) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() 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< return useMutation<
{uri: string; cid: string}, {uri: string; cid: string},
@@ -112,10 +117,10 @@ export function useCreateStarterPackMutation({
UseCreateStarterPackMutationParams UseCreateStarterPackMutationParams
>({ >({
mutationFn: async ({name, description, feeds, profiles}) => { mutationFn: async ({name, description, feeds, profiles}) => {
let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined let descriptionFacets: app.bsky.richtext.facet.Main[] | undefined
if (description) { if (description) {
const rt = new RichText({text: description}) const rt = new RichText({text: description})
await rt.detectFacets(agent) await rt.detectFacets(appviewClient)
descriptionFacets = rt.facets descriptionFacets = rt.facets
} }
@@ -167,6 +172,7 @@ export function useEditStarterPackMutation({
}) { }) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const agent = useAgent()
const appviewClient = useAppviewClient()
return useMutation< return useMutation<
void, void,
@@ -184,10 +190,10 @@ export function useEditStarterPackMutation({
currentStarterPack, currentStarterPack,
currentListItems, currentListItems,
}) => { }) => {
let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined let descriptionFacets: app.bsky.richtext.facet.Main[] | undefined
if (description) { if (description) {
const rt = new RichText({text: description}) const rt = new RichText({text: description})
await rt.detectFacets(agent) await rt.detectFacets(appviewClient)
descriptionFacets = rt.facets descriptionFacets = rt.facets
} }
+2 -1
View File
@@ -49,10 +49,10 @@ import {
AppBskyUnspeccedDefs, AppBskyUnspeccedDefs,
AtUri, AtUri,
ChatBskyGroupDefs, ChatBskyGroupDefs,
type RichText,
} from '@atproto/api' } from '@atproto/api'
import {type Client} from '@atproto/lex' import {type Client} from '@atproto/lex'
import {type AtUriString} from '@atproto/syntax' import {type AtUriString} from '@atproto/syntax'
import {type RichText} from '@bsky.app/sdk/richtext'
import {plural} from '@lingui/core/macro' import {plural} from '@lingui/core/macro'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
@@ -1083,6 +1083,7 @@ export const ComposePost = ({
replyTo: replyTo?.uri, replyTo: replyTo?.uri,
onStateChange: setPublishingStage, onStateChange: setPublishingStage,
langs: currentLanguages, langs: currentLanguages,
appviewClient: client,
}) })
).uris[0] ).uris[0]
+2 -1
View File
@@ -1,7 +1,8 @@
/** /**
* Type converters for Draft API - convert between ComposerState and server Draft types. * 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 {nanoid} from 'nanoid/non-secure'
import {resolveLink} from '#/lib/api/resolve' import {resolveLink} from '#/lib/api/resolve'
@@ -1,11 +1,11 @@
import {useEffect, useMemo, useRef, useState} from 'react' import {useEffect, useMemo, useRef, useState} from 'react'
import {Platform, Text as RNText, View} from 'react-native' import {Platform, Text as RNText, View} from 'react-native'
import {RichText} from '@atproto/api'
import {parseLanguageString} from '@atproto/syntax' import {parseLanguageString} from '@atproto/syntax'
import { import {
guessLanguageAsync, guessLanguageAsync,
type LanguageResult, type LanguageResult,
} from '@bsky.app/expo-guess-language' } from '@bsky.app/expo-guess-language'
import {RichText} from '@bsky.app/sdk/richtext'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
+4 -3
View File
@@ -3,9 +3,8 @@ import {
type AppBskyActorDefs, type AppBskyActorDefs,
type AppBskyDraftDefs, type AppBskyDraftDefs,
type AppBskyFeedPostgate, type AppBskyFeedPostgate,
AppBskyRichtextFacet,
RichText,
} from '@atproto/api' } from '@atproto/api'
import {RichText} from '@bsky.app/sdk/richtext'
import {nanoid} from 'nanoid/non-secure' import {nanoid} from 'nanoid/non-secure'
import {type VideoTelemetry} from '#/lib/media/video/telemetry' import {type VideoTelemetry} from '#/lib/media/video/telemetry'
@@ -28,6 +27,8 @@ import {
suggestLinkCardUri, suggestLinkCardUri,
} from '#/view/com/composer/text-input/text-input-util' } from '#/view/com/composer/text-input/text-input-util'
import {type Gif} from '#/features/gifPicker/types' import {type Gif} from '#/features/gifPicker/types'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
import { import {
createVideoState, createVideoState,
type VideoAction, type VideoAction,
@@ -677,7 +678,7 @@ export function createComposerState({
if (initRichText.facets) { if (initRichText.facets) {
for (const facet of initRichText.facets) { for (const facet of initRichText.facets) {
for (const feature of facet.features) { for (const feature of facet.features) {
if (AppBskyRichtextFacet.isLink(feature)) { if (bsky.isType(app.bsky.richtext.facet.link, feature)) {
if (isBskyPostUrl(feature.uri)) { if (isBskyPostUrl(feature.uri)) {
detectedPostUris.set(feature.uri, {facet, rt: initRichText}) detectedPostUris.set(feature.uri, {facet, rt: initRichText})
} else { } else {
@@ -12,7 +12,7 @@ import {
View, View,
} from 'react-native' } from 'react-native'
import {type PasteEventPayload, TextInputWrapper} from 'expo-paste-input' 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 {useLingui} from '@lingui/react/macro'
import {IMAGE_SIZE_CONFIG_POSTS} from '#/lib/constants' import {IMAGE_SIZE_CONFIG_POSTS} from '#/lib/constants'
@@ -27,6 +27,8 @@ import {
import {atoms as a, useAlf} from '#/alf' import {atoms as a, useAlf} from '#/alf'
import {normalizeTextStyles} from '#/alf/typography' import {normalizeTextStyles} from '#/alf/typography'
import {IS_ANDROID} from '#/env' import {IS_ANDROID} from '#/env'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {Autocomplete} from './mobile/Autocomplete' import {Autocomplete} from './mobile/Autocomplete'
import {type TextInputProps} from './TextInput.types' import {type TextInputProps} from './TextInput.types'
@@ -88,7 +90,7 @@ export function TextInput({
if (newRt.facets) { if (newRt.facets) {
for (const facet of newRt.facets) { for (const facet of newRt.facets) {
for (const feature of facet.features) { for (const feature of facet.features) {
if (AppBskyRichtextFacet.isLink(feature)) { if (bsky.isType(app.bsky.richtext.facet.link, feature)) {
if (isUriImage(feature.uri)) { if (isUriImage(feature.uri)) {
const res = await downloadAndResize({ const res = await downloadAndResize({
uri: feature.uri, uri: feature.uri,
@@ -1,5 +1,5 @@
import {type TextInput} from 'react-native' import {type TextInput} from 'react-native'
import {type RichText} from '@atproto/api' import {type RichText} from '@bsky.app/sdk/richtext'
export type TextInputRef = { export type TextInputRef = {
focus: () => void focus: () => void
@@ -8,7 +8,7 @@ import {
} from 'react' } from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' 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 {Trans} from '@lingui/react/macro'
import {getSchema} from '@tiptap/core' import {getSchema} from '@tiptap/core'
import {Document} from '@tiptap/extension-document' import {Document} from '@tiptap/extension-document'
@@ -40,6 +40,8 @@ import {normalizeTextStyles} from '#/alf/typography'
import {type Emoji} from '#/components/EmojiPicker' import {type Emoji} from '#/components/EmojiPicker'
import {Portal} from '#/components/Portal' import {Portal} from '#/components/Portal'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {type TextInputProps} from './TextInput.types' import {type TextInputProps} from './TextInput.types'
import {type AutocompleteRef, createSuggestion} from './web/Autocomplete' import {type AutocompleteRef, createSuggestion} from './web/Autocomplete'
import {LinkDecorator} from './web/LinkDecorator' import {LinkDecorator} from './web/LinkDecorator'
@@ -265,7 +267,7 @@ export function TextInput({
if (newRt.facets) { if (newRt.facets) {
for (const facet of newRt.facets) { for (const facet of newRt.facets) {
for (const feature of facet.features) { 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}) nextDetectedUris.set(feature.uri, {facet, rt: newRt})
} }
} }
@@ -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 = { export type LinkFacetMatch = {
rt: RichText rt: RichText
facet: AppBskyRichtextFacet.Main facet: app.bsky.richtext.facet.Main
} }
export function suggestLinkCardUri( export function suggestLinkCardUri(
+3 -2
View File
@@ -6,14 +6,15 @@ import {
AtUri, AtUri,
moderatePost, moderatePost,
type ModerationDecision, type ModerationDecision,
RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {MAX_POST_LINES} from '#/lib/constants' import {MAX_POST_LINES} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {countLines} from '#/lib/strings/helpers' import {countLines} from '#/lib/strings/helpers'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import { import {
POST_TOMBSTONE, POST_TOMBSTONE,
type Shadow, type Shadow,
@@ -68,7 +69,7 @@ export function Post({
record record
? new RichTextAPI({ ? new RichTextAPI({
text: record.text, text: record.text,
facets: record.facets, facets: asSdkFacets(record.facets),
}) })
: undefined, : undefined,
[record], [record],
+1 -1
View File
@@ -25,8 +25,8 @@ import {
AppBskyEmbedImages, AppBskyEmbedImages,
AppBskyEmbedVideo, AppBskyEmbedVideo,
type AppBskyFeedDefs, type AppBskyFeedDefs,
type RichText as RichTextType,
} from '@atproto/api' } from '@atproto/api'
import {type RichText as RichTextType} from '@bsky.app/sdk/richtext'
import {useLingui} from '@lingui/react/macro' import {useLingui} from '@lingui/react/macro'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
+3 -2
View File
@@ -7,8 +7,8 @@ import {
AppBskyFeedThreadgate, AppBskyFeedThreadgate,
AtUri, AtUri,
type ModerationDecision, type ModerationDecision,
RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {type ReasonFeedSource} from '#/lib/api/feed/types' 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 {usePalette} from '#/lib/hooks/usePalette'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {countLines} from '#/lib/strings/helpers' import {countLines} from '#/lib/strings/helpers'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import { import {
POST_TOMBSTONE, POST_TOMBSTONE,
type Shadow, type Shadow,
@@ -104,7 +105,7 @@ export function PostFeedItem({
() => () =>
new RichTextAPI({ new RichTextAPI({
text: record.text, text: record.text,
facets: record.facets, facets: asSdkFacets(record.facets),
}), }),
[record], [record],
) )
+1 -1
View File
@@ -15,8 +15,8 @@ import {
type ModerationBehavior, type ModerationBehavior,
type ModerationDecision, type ModerationDecision,
type ModerationOpts, type ModerationOpts,
RichText,
} from '@atproto/api' } from '@atproto/api'
import {RichText} from '@bsky.app/sdk/richtext'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
+9 -5
View File
@@ -6,8 +6,8 @@ import {
type AppBskyActorDefs, type AppBskyActorDefs,
moderateProfile, moderateProfile,
type ModerationOpts, type ModerationOpts,
RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
@@ -33,7 +33,7 @@ import {useLabelerInfoQuery} from '#/state/queries/labeler'
import {resetProfilePostsQueries} from '#/state/queries/post-feed' import {resetProfilePostsQueries} from '#/state/queries/post-feed'
import {useProfileQuery} from '#/state/queries/profile' import {useProfileQuery} from '#/state/queries/profile'
import {useResolveDidQuery} from '#/state/queries/resolve-uri' 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 {ProfileFeedgens} from '#/view/com/feeds/ProfileFeedgens'
import {ProfileLists} from '#/view/com/lists/ProfileLists' import {ProfileLists} from '#/view/com/lists/ProfileLists'
import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader' import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
@@ -613,7 +613,11 @@ function ProfileScreenLoaded({
} }
function useRichText(text: string): [RichTextAPI, boolean] { 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 [prevText, setPrevText] = useState(text)
const [rawRT, setRawRT] = useState(() => new RichTextAPI({text})) const [rawRT, setRawRT] = useState(() => new RichTextAPI({text}))
const [resolvedRT, setResolvedRT] = useState<RichTextAPI | null>(null) const [resolvedRT, setResolvedRT] = useState<RichTextAPI | null>(null)
@@ -628,7 +632,7 @@ function useRichText(text: string): [RichTextAPI, boolean] {
async function resolveRTFacets() { async function resolveRTFacets() {
// new each time // new each time
const resolvedRT = new RichTextAPI({text}) const resolvedRT = new RichTextAPI({text})
await resolvedRT.detectFacets(agent) await resolvedRT.detectFacets(client)
if (!ignore) { if (!ignore) {
setResolvedRT(resolvedRT) setResolvedRT(resolvedRT)
} }
@@ -637,7 +641,7 @@ function useRichText(text: string): [RichTextAPI, boolean] {
return () => { return () => {
ignore = true ignore = true
} }
}, [text, agent]) }, [text, client])
const isResolving = resolvedRT === null const isResolving = resolvedRT === null
return [resolvedRT ?? rawRT, isResolving] return [resolvedRT ?? rawRT, isResolving]
} }