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:
+11
-4
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user