collapse the dual-world type layer and delete the widening shims

The types/bsky post/profile/starterPack unions drop their @atproto/api arms,
and dangerousIsType/validate go with the old-world guards they wrapped. The
moderation subjects.ts widening shim and rich-text-helpers' asSdkFacets both
existed only to bridge branded and unbranded views, so their 55 and 14 callers
now go straight to @bsky.app/sdk/moderation and the raw facets.

Boundary fallout: lexicon token defs are camelCase schema objects needing
.value, and the branded string slots that the widening used to absorb are now
cast or branded at their producers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-08-04 04:53:44 +03:00
parent e9a67f2dea
commit ae0750099f
114 changed files with 591 additions and 899 deletions
+6 -3
View File
@@ -44,7 +44,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {scheduleOnUI} from 'react-native-worklets'
import * as FileSystem from 'expo-file-system'
import {type ImagePickerAsset} from 'expo-image-picker'
import {type Client} from '@atproto/lex'
import {type Client, type UriString} from '@atproto/lex'
import {type AtUriString, AtUri} from '@atproto/syntax'
import {type RichText} from '@bsky.app/sdk/richtext'
import {plural} from '@lingui/core/macro'
@@ -52,6 +52,7 @@ import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
import {useQueries, useQueryClient} from '@tanstack/react-query'
import {matchXrpcError} from '#/lib/xrpc-error'
import * as bsky from '#/types/bsky'
import * as apilib from '#/lib/api/index'
import {EmbeddingDisabledError} from '#/lib/api/resolve'
@@ -766,7 +767,9 @@ export const ComposePost = ({
const getDraftSaveError = useCallback(
(e: unknown): string => {
if (e instanceof app.bsky.draft.createDraft.DraftLimitReachedError) {
if (
matchXrpcError(e, app.bsky.draft.createDraft) === 'DraftLimitReached'
) {
return l`You've reached the maximum number of drafts`
}
return l`Failed to save draft`
@@ -1676,7 +1679,7 @@ let ComposerPost = memo(function ComposerPost({
const onNewLink = useCallback(
(uri: string) => {
dispatchPost({type: 'embed_add_uri', uri})
dispatchPost({type: 'embed_add_uri', uri: uri as UriString})
},
[dispatchPost],
)
+7 -5
View File
@@ -1,3 +1,4 @@
import {type UriString} from '@atproto/lex'
import {useMemo} from 'react'
import {type StyleProp, View, type ViewStyle} from 'react-native'
@@ -32,9 +33,9 @@ export const ExternalEmbedGif = ({
() =>
data && {
title: data.title ?? data.uri,
uri: data.uri,
uri: data.uri as UriString,
description: data.description ?? '',
thumb: data.thumb?.source.path,
thumb: data.thumb?.source.path as UriString | undefined,
},
[data],
)
@@ -80,7 +81,7 @@ export const ExternalEmbedLink = ({
hasQuote,
onRemove,
}: {
uri: string
uri: UriString
hasQuote: boolean
onRemove: () => void
}) => {
@@ -100,7 +101,8 @@ export const ExternalEmbedLink = ({
description:
data.view?.external?.description || data.description,
// prefer opengraph data to atproto record-derived image
thumb: data.thumb?.source.path || data.view?.external?.thumb,
thumb: (data.thumb?.source.path ||
data.view?.external?.thumb) as UriString | undefined,
}}
/>
)
@@ -111,7 +113,7 @@ export const ExternalEmbedLink = ({
title: data.title || uri,
uri,
description: data.description,
thumb: data.thumb?.source.path,
thumb: data.thumb?.source.path as UriString | undefined,
}}
hideAlt
/>
+3 -2
View File
@@ -1,6 +1,7 @@
/**
* Type converters for Draft API - convert between ComposerState and server Draft types.
*/
import {type UriString} from '@atproto/lex'
import {RichText} from '@bsky.app/sdk/richtext'
import {nanoid} from 'nanoid/non-secure'
@@ -275,7 +276,7 @@ function serializeGif(gifMedia: {
return {
$type: 'app.bsky.draft.defs#draftEmbedExternal',
uri: url.toString(),
uri: url.toString() as UriString,
}
}
@@ -607,7 +608,7 @@ export async function draftToComposerPosts(
const record = post.embedRecords[0]
const urip = new AtUri(record.record.uri)
const url = `https://bsky.app/profile/${urip.host}/post/${urip.rkey}`
embed.quote = {type: 'link', uri: url}
embed.quote = {type: 'link', uri: url as UriString}
}
// Restore link embed (only if not a GIF)
+11 -5
View File
@@ -1,3 +1,4 @@
import {type UriString} from '@atproto/lex'
import {type ImagePickerAsset} from 'expo-image-picker'
import {type AtUriString, toDatetimeString} from '@atproto/syntax'
import {RichText} from '@bsky.app/sdk/richtext'
@@ -55,7 +56,12 @@ type GifMedia = {
type Link = {
type: 'link'
uri: string
/*
* A URL the user typed or pasted, validated by the link resolver rather than
* at construction, so it carries lex's `uri` brand for the record and view
* slots it flows into.
*/
uri: UriString
}
// This structure doesn't exactly correspond to the data model.
@@ -90,7 +96,7 @@ export type PostAction =
}
| {type: 'embed_remove_video'}
| {type: 'embed_update_video'; videoAction: VideoAction}
| {type: 'embed_add_uri'; uri: string}
| {type: 'embed_add_uri'; uri: UriString}
| {type: 'embed_remove_quote'}
| {type: 'embed_remove_link'}
| {type: 'embed_add_gif'; gif: Gif}
@@ -645,7 +651,7 @@ export function createComposerState({
if (path) {
quote = {
type: 'link',
uri: toBskyAppUrl(path),
uri: toBskyAppUrl(path) as UriString,
}
}
}
@@ -699,7 +705,7 @@ export function createComposerState({
if (suggestedExtUri) {
link = {
type: 'link',
uri: suggestedExtUri,
uri: suggestedExtUri as UriString,
}
}
const suggestedPostUri = suggestLinkCardUri(
@@ -716,7 +722,7 @@ export function createComposerState({
if (!quote) {
quote = {
type: 'link',
uri: suggestedPostUri,
uri: suggestedPostUri as UriString,
}
}
}