migrate RichText to the SDK and resolve facets via the appview
The two RichText classes are not mutually assignable - `UnicodeString` has a private field and the SDK brands `did`/`uri` as template literal types - so every producer and consumer of a RichText instance has to move in one step. `detectFacets` now takes a lex client instead of the legacy agent, which is what removes the last hard agent dependency from these files. Handle resolution is an appview job, so the appview client is threaded in: through `useAppviewClient` in the hooks and dialogs, and through a new `appviewClient` option on `apilib.post` (Composer already had the client to hand). The rest of the post pipeline still writes through the agent. Facet feature checks move from the `AppBskyRichtextFacet` validators to the generated `#/lexicons` schemas, matching how the rest of the app narrows lexicon types. Display sinks still read facets off `@atproto/api` view types, which are the same lexicon but typed with plain strings. `asSdkFacets` widens them at those call sites and goes away once the view types come from the SDK too.
This commit is contained in:
@@ -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'
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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({
|
||||
</InlineLinkText>
|
||||
</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)
|
||||
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(
|
||||
<RichTextTag
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||
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 {useLingui} from '@lingui/react'
|
||||
import {Plural, Trans} from '@lingui/react/macro'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
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 {logger} from '#/logger'
|
||||
import {type ImageMeta} from '#/state/gallery'
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
useListCreateMutation,
|
||||
useListMetadataMutation,
|
||||
} from '#/state/queries/list'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {useAppviewClient} from '#/state/session'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {EditableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
@@ -133,7 +134,11 @@ function DialogInner({
|
||||
|
||||
const {_} = useLingui()
|
||||
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 {
|
||||
mutateAsync: createListMutation,
|
||||
@@ -163,7 +168,10 @@ function DialogInner({
|
||||
|
||||
// We want to be working with a blank state here, so let's get the
|
||||
// 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})
|
||||
richText.detectFacetsWithoutResolution()
|
||||
@@ -224,7 +232,7 @@ function DialogInner({
|
||||
{cleanNewlines: true},
|
||||
)
|
||||
|
||||
await richText.detectFacets(agent)
|
||||
await richText.detectFacets(appviewClient)
|
||||
richText = shortenLinks(richText)
|
||||
richText = stripInvalidMentions(richText)
|
||||
|
||||
@@ -272,7 +280,7 @@ function DialogInner({
|
||||
setImageError,
|
||||
activePurpose,
|
||||
isCurateList,
|
||||
agent,
|
||||
appviewClient,
|
||||
_,
|
||||
])
|
||||
|
||||
|
||||
@@ -2,17 +2,14 @@ import {memo, useCallback} from 'react'
|
||||
import {Platform} from 'react-native'
|
||||
import {type GestureType} from 'react-native-gesture-handler'
|
||||
import * as Clipboard from 'expo-clipboard'
|
||||
import {
|
||||
type ChatBskyConvoDefs,
|
||||
type ModerationOpts,
|
||||
RichText,
|
||||
} from '@atproto/api'
|
||||
import {type ChatBskyConvoDefs, type ModerationOpts} from '@atproto/api'
|
||||
import {RichText} from '@bsky.app/sdk/richtext'
|
||||
import {plural} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {EMOJI_REACTION_LIMIT} from '#/lib/constants'
|
||||
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 {useConvoActive} from '#/state/messages/convo'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
@@ -88,7 +85,7 @@ export let MessageContextMenu = ({
|
||||
const str = richTextToString(
|
||||
new RichText({
|
||||
text: message.text,
|
||||
facets: message.facets,
|
||||
facets: asSdkFacets(message.facets),
|
||||
}),
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -28,8 +28,8 @@ import {
|
||||
ChatBskyConvoDefs,
|
||||
ChatBskyEmbedJoinLink,
|
||||
moderateProfile,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
|
||||
import {plural} from '@lingui/core/macro'
|
||||
import {Trans, useLingui} from '@lingui/react/macro'
|
||||
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 {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
|
||||
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {type Shadow} from '#/state/cache/types'
|
||||
import {type ConvoItem} from '#/state/messages/convo/types'
|
||||
@@ -245,7 +246,10 @@ let MessageItem = ({
|
||||
? t.palette.primary_300
|
||||
: 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)
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
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] {
|
||||
const [prevText, setPrevText] = useState(text)
|
||||
const [rawRT, setRawRT] = useState(() => new RichTextAPI({text}))
|
||||
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) {
|
||||
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]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user