diff --git a/src/ageAssurance/debug.ts b/src/ageAssurance/debug.ts
index 74121976ca..3360747d4e 100644
--- a/src/ageAssurance/debug.ts
+++ b/src/ageAssurance/debug.ts
@@ -1,10 +1,7 @@
import type * as AgeRange from 'expo-age-range'
-import {
- ageAssuranceRuleIDs as ids,
- type AppBskyAgeassuranceDefs,
- type AppBskyAgeassuranceGetState,
-} from '@atproto/api'
+import {toDatetimeString} from '@atproto/syntax'
+import {app} from '#/lexicons'
import {type OtherRequiredData} from '#/ageAssurance/data'
import {IS_DEV, IS_E2E} from '#/env'
import {type Geolocation} from '#/geolocation'
@@ -31,33 +28,38 @@ export const otherRequiredData: OtherRequiredData = {
}
const serverStateEnabled = false || IS_E2E
-export const serverState: AppBskyAgeassuranceGetState.OutputSchema | undefined =
- serverStateEnabled
- ? {
- state: {
- lastInitiatedAt: undefined, // new Date(2025, 1, 1).toISOString(),
- status: 'unknown',
- access: 'unknown',
- },
- metadata: {
- accountCreatedAt: new Date(2023, 1, 1).toISOString(),
- },
- }
- : undefined
+export const serverState:
+ | app.bsky.ageassurance.getState.$OutputBody
+ | undefined = serverStateEnabled
+ ? {
+ state: {
+ lastInitiatedAt: undefined, // new Date(2025, 1, 1).toISOString(),
+ status: 'unknown',
+ access: 'unknown',
+ },
+ metadata: {
+ accountCreatedAt: toDatetimeString(new Date(2023, 1, 1)),
+ },
+ }
+ : undefined
-export const config: AppBskyAgeassuranceDefs.Config = {
+export const config: app.bsky.ageassurance.defs.Config = {
regions: [
{
countryCode: 'AA',
regionCode: undefined,
minAccessAge: 13,
rules: [
- {
- $type: ids.Default,
+ app.bsky.ageassurance.defs.configRegionRuleDefault.build({
access: 'full',
- },
+ }),
],
},
+ /*
+ * `platforms` is not in the generated `ConfigRegion` yet (the server sends
+ * it and `getAgeAssuranceRegionConfigForGeolocation` reads it), so this
+ * fixture asserts the shape until the lexicon carries the field.
+ */
{
// On-device verification region, native-only (web users in TX are not
// age assured). KWS is included as a fallback for when the device
@@ -68,65 +70,55 @@ export const config: AppBskyAgeassuranceDefs.Config = {
minAccessAge: 18,
additionalVerificationMethods: ['device'],
rules: [
- {
+ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18,
access: 'full',
- $type: ids.IfAssuredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleDefault.build({
access: 'none',
- $type: ids.Default,
- },
+ }),
],
- },
+ } as app.bsky.ageassurance.defs.ConfigRegion,
{
countryCode: 'GB',
minAccessAge: 13,
rules: [
- {
+ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18,
access: 'full',
- $type: ids.IfAssuredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
age: 13,
access: 'safe',
- $type: ids.IfDeclaredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleDefault.build({
access: 'none',
- $type: ids.Default,
- },
+ }),
],
},
{
countryCode: 'AU',
minAccessAge: 16,
rules: [
- {
+ app.bsky.ageassurance.defs.configRegionRuleIfAccountNewerThan.build({
date: '2025-12-10T00:00:00Z',
access: 'none',
- $type: ids.IfAccountNewerThan,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18,
access: 'full',
- $type: ids.IfAssuredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 16,
access: 'safe',
- $type: ids.IfAssuredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
age: 16,
access: 'safe',
- $type: ids.IfDeclaredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleDefault.build({
access: 'none',
- $type: ids.Default,
- },
+ }),
],
},
{
@@ -134,20 +126,17 @@ export const config: AppBskyAgeassuranceDefs.Config = {
regionCode: 'SD',
minAccessAge: 13,
rules: [
- {
+ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18,
access: 'full',
- $type: ids.IfAssuredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
age: 13,
access: 'safe',
- $type: ids.IfDeclaredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleDefault.build({
access: 'none',
- $type: ids.Default,
- },
+ }),
],
},
{
@@ -155,20 +144,17 @@ export const config: AppBskyAgeassuranceDefs.Config = {
regionCode: 'WY',
minAccessAge: 13,
rules: [
- {
+ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18,
access: 'full',
- $type: ids.IfAssuredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
age: 13,
access: 'safe',
- $type: ids.IfDeclaredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleDefault.build({
access: 'none',
- $type: ids.Default,
- },
+ }),
],
},
{
@@ -176,20 +162,17 @@ export const config: AppBskyAgeassuranceDefs.Config = {
regionCode: 'OH',
minAccessAge: 13,
rules: [
- {
+ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18,
access: 'full',
- $type: ids.IfAssuredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
age: 13,
access: 'safe',
- $type: ids.IfDeclaredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleDefault.build({
access: 'none',
- $type: ids.Default,
- },
+ }),
],
},
{
@@ -197,15 +180,13 @@ export const config: AppBskyAgeassuranceDefs.Config = {
regionCode: 'MS',
minAccessAge: 18,
rules: [
- {
+ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18,
access: 'full',
- $type: ids.IfAssuredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleDefault.build({
access: 'none',
- $type: ids.Default,
- },
+ }),
],
},
{
@@ -213,20 +194,17 @@ export const config: AppBskyAgeassuranceDefs.Config = {
regionCode: 'VA',
minAccessAge: 16,
rules: [
- {
+ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 16,
access: 'full',
- $type: ids.IfAssuredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
age: 16,
access: 'full',
- $type: ids.IfDeclaredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleDefault.build({
access: 'none',
- $type: ids.Default,
- },
+ }),
],
},
{
@@ -234,45 +212,38 @@ export const config: AppBskyAgeassuranceDefs.Config = {
regionCode: 'TN',
minAccessAge: 18,
rules: [
- {
+ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18,
access: 'full',
- $type: ids.IfAssuredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
age: 18,
access: 'full',
- $type: ids.IfDeclaredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleDefault.build({
access: 'none',
- $type: ids.Default,
- },
+ }),
],
},
{
countryCode: 'BR',
minAccessAge: 13,
rules: [
- {
+ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18,
access: 'full',
- $type: ids.IfAssuredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
age: 18,
access: 'full',
- $type: ids.IfDeclaredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
age: 13,
access: 'safe',
- $type: ids.IfDeclaredOverAge,
- },
- {
+ }),
+ app.bsky.ageassurance.defs.configRegionRuleDefault.build({
access: 'none',
- $type: ids.Default,
- },
+ }),
],
},
],
diff --git a/src/components/Autocomplete/useAutocomplete/index.ts b/src/components/Autocomplete/useAutocomplete/index.ts
index b310876b84..de5009a600 100644
--- a/src/components/Autocomplete/useAutocomplete/index.ts
+++ b/src/components/Autocomplete/useAutocomplete/index.ts
@@ -1,9 +1,8 @@
import {useCallback, useMemo} from 'react'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {keepPreviousData, useQuery} from '@tanstack/react-query'
import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences'
diff --git a/src/components/AvatarBubbles.tsx b/src/components/AvatarBubbles.tsx
index 9b59a2555f..0f68952c59 100644
--- a/src/components/AvatarBubbles.tsx
+++ b/src/components/AvatarBubbles.tsx
@@ -9,7 +9,7 @@ import Animated, {
withTiming,
} from 'react-native-reanimated'
-import {moderateProfile} from '#/lib/moderation/subjects'
+import {moderateProfile} from '@bsky.app/sdk/moderation'
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSession} from '#/state/session'
diff --git a/src/components/AvatarStack.tsx b/src/components/AvatarStack.tsx
index f98148f52c..136d40acb4 100644
--- a/src/components/AvatarStack.tsx
+++ b/src/components/AvatarStack.tsx
@@ -1,6 +1,6 @@
import {View} from 'react-native'
-import {moderateProfile} from '#/lib/moderation/subjects'
+import {moderateProfile} from '@bsky.app/sdk/moderation'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useProfilesQuery} from '#/state/queries/profile'
diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx
index 164e25ec84..f309369993 100644
--- a/src/components/KnownFollowers.tsx
+++ b/src/components/KnownFollowers.tsx
@@ -1,10 +1,9 @@
import {useRef} from 'react'
import {View} from 'react-native'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {app} from '#/lexicons'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {UserAvatar} from '#/view/com/util/UserAvatar'
diff --git a/src/components/ListCard.tsx b/src/components/ListCard.tsx
index f2c85216ae..aa89f31acb 100644
--- a/src/components/ListCard.tsx
+++ b/src/components/ListCard.tsx
@@ -1,6 +1,6 @@
import {useEffect, useMemo} from 'react'
import {View} from 'react-native'
-import {type ModerationUI} from '@bsky.app/sdk/moderation'
+import {moderateUserList, type ModerationUI} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
@@ -8,7 +8,6 @@ import {useQueryClient} from '@tanstack/react-query'
import {AtUri} from '@atproto/syntax'
import {app} from '#/lexicons'
-import {moderateUserList} from '#/lib/moderation/subjects'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {precacheList} from '#/state/queries/feed'
diff --git a/src/components/MediaPreview.tsx b/src/components/MediaPreview.tsx
index 1059bae9a3..2178dad193 100644
--- a/src/components/MediaPreview.tsx
+++ b/src/components/MediaPreview.tsx
@@ -103,7 +103,17 @@ export function Embed({
// ignore any unknowns
e.media.view !== null
) {
- return
+ /*
+ * Every media arm's view is a `$Typed<...>` of a def that `PostView.embed`
+ * also admits, but TS will not narrow the parsed union back into the raw
+ * embed union, so the arm is re-asserted here.
+ */
+ return (
+
+ )
}
return null
diff --git a/src/components/NewskieDialog.tsx b/src/components/NewskieDialog.tsx
index fda20c2a58..84a69fe694 100644
--- a/src/components/NewskieDialog.tsx
+++ b/src/components/NewskieDialog.tsx
@@ -1,5 +1,6 @@
import {useMemo, useState} from 'react'
import {View} from 'react-native'
+import {moderateProfile} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
@@ -8,7 +9,6 @@ import {differenceInSeconds} from 'date-fns'
import {app} from '#/lexicons'
import {HITSLOP_10} from '#/lib/constants'
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSession} from '#/state/session'
diff --git a/src/components/Post/Embed/FeedEmbed.tsx b/src/components/Post/Embed/FeedEmbed.tsx
index 8871a63b59..70d59c3adb 100644
--- a/src/components/Post/Embed/FeedEmbed.tsx
+++ b/src/components/Post/Embed/FeedEmbed.tsx
@@ -1,6 +1,6 @@
import {useMemo} from 'react'
-import {moderateFeedGenerator} from '#/lib/moderation/subjects'
+import {moderateFeedGenerator} from '@bsky.app/sdk/moderation'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {atoms as a, useTheme} from '#/alf'
import * as FeedCard from '#/components/FeedCard'
diff --git a/src/components/Post/Embed/ListEmbed.tsx b/src/components/Post/Embed/ListEmbed.tsx
index d6daca265c..f7f354dd53 100644
--- a/src/components/Post/Embed/ListEmbed.tsx
+++ b/src/components/Post/Embed/ListEmbed.tsx
@@ -1,6 +1,6 @@
import {useMemo} from 'react'
-import {moderateUserList} from '#/lib/moderation/subjects'
+import {moderateUserList} from '@bsky.app/sdk/moderation'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {atoms as a, useTheme} from '#/alf'
import * as ListCard from '#/components/ListCard'
diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx
index 4f4a5ae05f..6a196ce881 100644
--- a/src/components/Post/Embed/index.tsx
+++ b/src/components/Post/Embed/index.tsx
@@ -1,15 +1,14 @@
import {useCallback, useMemo} from 'react'
import {View} from 'react-native'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
+import {moderatePost} from '@bsky.app/sdk/moderation'
import {Trans} from '@lingui/react/macro'
import {useQueryClient} from '@tanstack/react-query'
import {type $Typed} from '@atproto/lex'
import {AtUri} from '@atproto/syntax'
import {app} from '#/lexicons'
-import {moderatePost} from '#/lib/moderation/subjects'
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'
@@ -287,7 +286,7 @@ export function QuoteEmbed({
if (!bsky.isType(app.bsky.feed.post, quote.record)) return undefined
const {text, facets} = quote.record
return text.trim()
- ? new RichTextAPI({text: text, facets: asSdkFacets(facets)})
+ ? new RichTextAPI({text: text, facets: facets})
: undefined
}, [quote.record])
diff --git a/src/components/PostControls/ShareMenu/RecentChats.tsx b/src/components/PostControls/ShareMenu/RecentChats.tsx
index 9a7f0c4923..ce6dc144c9 100644
--- a/src/components/PostControls/ShareMenu/RecentChats.tsx
+++ b/src/components/PostControls/ShareMenu/RecentChats.tsx
@@ -1,5 +1,5 @@
import {ScrollView, View} from 'react-native'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
@@ -8,7 +8,6 @@ import {useNavigation} from '@react-navigation/native'
import {chat} from '#/lexicons'
import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {type NavigationProp} from '#/lib/routes/types'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx
index cdc8d194a1..9db85d2749 100644
--- a/src/components/ProfileCard.tsx
+++ b/src/components/ProfileCard.tsx
@@ -6,12 +6,11 @@ import {
View,
type ViewStyle,
} from 'react-native'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {RichText as RichTextApi} from '@bsky.app/sdk/richtext'
import {useLingui} from '@lingui/react/macro'
import {getModerationCauseKey} from '#/lib/moderation'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links'
import {forceLTR} from '#/lib/strings/bidi'
import {NON_BREAKING_SPACE} from '#/lib/strings/constants'
diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx
index 39fa40233a..d1580d891d 100644
--- a/src/components/ProfileHoverCard/index.web.tsx
+++ b/src/components/ProfileHoverCard/index.web.tsx
@@ -1,6 +1,6 @@
import {memo, useCallback, useEffect, useMemo, useReducer, useRef} from 'react'
import {View} from 'react-native'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {flip, offset, shift, size, useFloating} from '@floating-ui/react-dom'
import {msg, plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
@@ -8,7 +8,6 @@ import {useNavigation} from '@react-navigation/native'
import {app} from '#/lexicons'
import {getModerationCauseKey} from '#/lib/moderation'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links'
import {type NavigationProp} from '#/lib/routes/types'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
diff --git a/src/components/StarterPack/Wizard/WizardListCard.tsx b/src/components/StarterPack/Wizard/WizardListCard.tsx
index ef9c647b54..2bd9deba50 100644
--- a/src/components/StarterPack/Wizard/WizardListCard.tsx
+++ b/src/components/StarterPack/Wizard/WizardListCard.tsx
@@ -1,12 +1,16 @@
import {Keyboard, View} from 'react-native'
-import {type ModerationOpts, type ModerationUI} from '@bsky.app/sdk/moderation'
+import {
+ moderateFeedGenerator,
+ moderateProfile,
+ type ModerationOpts,
+ type ModerationUI,
+} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {app} from '#/lexicons'
import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from '#/lib/constants'
-import {moderateFeedGenerator, moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useSession} from '#/state/session'
diff --git a/src/components/ageAssurance/AgeAssuranceAppealDialog.tsx b/src/components/ageAssurance/AgeAssuranceAppealDialog.tsx
index db8b169bcc..e3a9dd41a7 100644
--- a/src/components/ageAssurance/AgeAssuranceAppealDialog.tsx
+++ b/src/components/ageAssurance/AgeAssuranceAppealDialog.tsx
@@ -59,7 +59,7 @@ function Inner({control}: {control: Dialog.DialogControlProps}) {
await client.call(
com.atproto.moderation.createReport,
{
- reasonType: tools.ozone.report.defs.reasonAppeal,
+ reasonType: tools.ozone.report.defs.reasonAppeal.value,
subject: {
$type: 'com.atproto.admin.defs#repoRef',
// the persisted account did is already resolved
diff --git a/src/components/dialogs/SearchablePeopleList.tsx b/src/components/dialogs/SearchablePeopleList.tsx
index 221beb9350..3813a3c745 100644
--- a/src/components/dialogs/SearchablePeopleList.tsx
+++ b/src/components/dialogs/SearchablePeopleList.tsx
@@ -7,11 +7,10 @@ import {
useState,
} from 'react'
import {TextInput, View} from 'react-native'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
diff --git a/src/components/dialogs/lists/CreateOrEditListDialog.tsx b/src/components/dialogs/lists/CreateOrEditListDialog.tsx
index 32e52fb18c..17d34f0ac2 100644
--- a/src/components/dialogs/lists/CreateOrEditListDialog.tsx
+++ b/src/components/dialogs/lists/CreateOrEditListDialog.tsx
@@ -8,7 +8,7 @@ import {Plural, Trans} from '@lingui/react/macro'
import {app} from '#/lexicons'
import {cleanError} from '#/lib/strings/errors'
import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
-import {asSdkFacets, richTextToString} from '#/lib/strings/rich-text-helpers'
+import {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'
@@ -169,7 +169,7 @@ 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: asSdkFacets(facets)}),
+ new RichTextAPI({text, facets: facets}),
false,
)
diff --git a/src/components/dms/ChatProfileTabs.tsx b/src/components/dms/ChatProfileTabs.tsx
index 341d94392d..5a0e7a8fb3 100644
--- a/src/components/dms/ChatProfileTabs.tsx
+++ b/src/components/dms/ChatProfileTabs.tsx
@@ -1,10 +1,10 @@
import {useCallback, useEffect} from 'react'
import {type ScrollView, View} from 'react-native'
import Animated, {useAnimatedRef, useSharedValue} from 'react-native-reanimated'
+import {moderateProfile} from '@bsky.app/sdk/moderation'
import {useLingui} from '@lingui/react/macro'
import {HITSLOP_10} from '#/lib/constants'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
diff --git a/src/components/dms/InitiateChatFlow.tsx b/src/components/dms/InitiateChatFlow.tsx
index 3a93b966b0..271016a5d5 100644
--- a/src/components/dms/InitiateChatFlow.tsx
+++ b/src/components/dms/InitiateChatFlow.tsx
@@ -7,12 +7,11 @@ import {
useState,
} from 'react'
import {LayoutAnimation, type TextInput, View} from 'react-native'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {MAX_GROUP_NAME_GRAPHEME_LENGTH} from '#/lib/constants'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
diff --git a/src/components/dms/MessageContextMenu.tsx b/src/components/dms/MessageContextMenu.tsx
index e080ed49e1..d513be0564 100644
--- a/src/components/dms/MessageContextMenu.tsx
+++ b/src/components/dms/MessageContextMenu.tsx
@@ -10,7 +10,7 @@ import {useLingui} from '@lingui/react/macro'
import {chat} from '#/lexicons'
import {EMOJI_REACTION_LIMIT} from '#/lib/constants'
import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'
-import {asSdkFacets, richTextToString} from '#/lib/strings/rich-text-helpers'
+import {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'
@@ -86,7 +86,7 @@ export let MessageContextMenu = ({
const str = richTextToString(
new RichText({
text: message.text,
- facets: asSdkFacets(message.facets),
+ facets: message.facets,
}),
true,
)
diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx
index 4c33d772b1..5add07e564 100644
--- a/src/components/dms/MessageItem.tsx
+++ b/src/components/dms/MessageItem.tsx
@@ -23,6 +23,7 @@ import Animated, {
ZoomOut,
} from 'react-native-reanimated'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
+import {moderateProfile} from '@bsky.app/sdk/moderation'
import {plural} from '@lingui/core/macro'
import {Trans, useLingui} from '@lingui/react/macro'
import {useQueryClient} from '@tanstack/react-query'
@@ -31,9 +32,7 @@ import * as bsky from '#/types/bsky'
import {app, chat} from '#/lexicons'
import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
-import {moderateProfile} from '#/lib/moderation/subjects'
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'
@@ -257,7 +256,7 @@ let MessageItem = ({
const rt = new RichTextAPI({
text: message.text,
- facets: asSdkFacets(message.facets),
+ facets: message.facets,
})
const isEmojiOnly = isOnlyEmoji(message.text)
diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx
index 75a534b790..adf8e3b1f5 100644
--- a/src/components/dms/MessagesListHeader.tsx
+++ b/src/components/dms/MessagesListHeader.tsx
@@ -1,10 +1,9 @@
import {useMemo} from 'react'
import {View} from 'react-native'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {useLingui} from '@lingui/react/macro'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useProfileShadow} from '#/state/cache/profile-shadow'
diff --git a/src/components/dms/components/GroupChatProfileCard.tsx b/src/components/dms/components/GroupChatProfileCard.tsx
index 63fbbbd20c..90d15c4b5f 100644
--- a/src/components/dms/components/GroupChatProfileCard.tsx
+++ b/src/components/dms/components/GroupChatProfileCard.tsx
@@ -1,8 +1,7 @@
import {View} from 'react-native'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {Trans} from '@lingui/react/macro'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {atoms as a, useTheme} from '#/alf'
diff --git a/src/components/dms/util.ts b/src/components/dms/util.ts
index be2f821182..23d4fb3178 100644
--- a/src/components/dms/util.ts
+++ b/src/components/dms/util.ts
@@ -1,10 +1,9 @@
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {type $Typed} from '@atproto/lex'
import {chat} from '#/lexicons'
import {EMOJI_REACTION_LIMIT} from '#/lib/constants'
import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {logger} from '#/logger'
import {type Shadow} from '#/state/cache/profile-shadow'
import {type ConvoState, ConvoStatus} from '#/state/messages/convo/types'
diff --git a/src/components/intents/GroupChatJoinDialog.tsx b/src/components/intents/GroupChatJoinDialog.tsx
index c8d5b34b33..5a7f4edaff 100644
--- a/src/components/intents/GroupChatJoinDialog.tsx
+++ b/src/components/intents/GroupChatJoinDialog.tsx
@@ -1,12 +1,12 @@
import {useEffect} from 'react'
import {View} from 'react-native'
+import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import * as bsky from '#/types/bsky'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {useCallOnce} from '#/lib/once'
import {makeProfileLink} from '#/lib/routes/links'
import {type NavigationProp} from '#/lib/routes/types'
diff --git a/src/components/interstitials/FeedTrendingTopics.tsx b/src/components/interstitials/FeedTrendingTopics.tsx
index 965d47f45e..e75045f025 100644
--- a/src/components/interstitials/FeedTrendingTopics.tsx
+++ b/src/components/interstitials/FeedTrendingTopics.tsx
@@ -1,9 +1,9 @@
import {useMemo} from 'react'
import {Pressable, View} from 'react-native'
+import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {app} from '#/lexicons'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {
useTrendingSettings,
diff --git a/src/components/moderation/AppealForm.tsx b/src/components/moderation/AppealForm.tsx
index 0511825ea7..271a19fc59 100644
--- a/src/components/moderation/AppealForm.tsx
+++ b/src/components/moderation/AppealForm.tsx
@@ -50,7 +50,7 @@ export function AppealForm({
await client.call(
com.atproto.moderation.createReport,
{
- reasonType: tools.ozone.report.defs.reasonAppeal,
+ reasonType: tools.ozone.report.defs.reasonAppeal.value,
/*
* `useLabelSubject` derives one shape or the other from the label's
* `cid`: an at-uri plus cid for a record, or the label's `uri` reused
diff --git a/src/components/moderation/ReportDialog/const.ts b/src/components/moderation/ReportDialog/const.ts
index 5707f62635..a265da56e1 100644
--- a/src/components/moderation/ReportDialog/const.ts
+++ b/src/components/moderation/ReportDialog/const.ts
@@ -30,93 +30,93 @@ export const NEW_TO_OLD_REASONS_MAP: Record<
tools.ozone.report.defs.ReasonType,
com.atproto.moderation.defs.ReasonType
> = {
- [tools.ozone.report.defs.reasonAppeal]:
- com.atproto.moderation.defs.reasonAppeal,
- [tools.ozone.report.defs.reasonOther]:
- com.atproto.moderation.defs.reasonOther,
+ [tools.ozone.report.defs.reasonAppeal.value]:
+ com.atproto.moderation.defs.reasonAppeal.value,
+ [tools.ozone.report.defs.reasonOther.value]:
+ com.atproto.moderation.defs.reasonOther.value,
- [tools.ozone.report.defs.reasonViolenceAnimal]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonViolenceThreats]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonViolenceGraphicContent]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonViolenceGlorification]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonViolenceExtremistContent]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonViolenceTrafficking]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonViolenceOther]:
- com.atproto.moderation.defs.reasonViolation,
+ [tools.ozone.report.defs.reasonViolenceAnimal.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonViolenceThreats.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonViolenceGraphicContent.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonViolenceGlorification.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonViolenceExtremistContent.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonViolenceTrafficking.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonViolenceOther.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
- [tools.ozone.report.defs.reasonSexualAbuseContent]:
- com.atproto.moderation.defs.reasonSexual,
- [tools.ozone.report.defs.reasonSexualNCII]:
- com.atproto.moderation.defs.reasonSexual,
- [tools.ozone.report.defs.reasonSexualDeepfake]:
- com.atproto.moderation.defs.reasonSexual,
- [tools.ozone.report.defs.reasonSexualAnimal]:
- com.atproto.moderation.defs.reasonSexual,
- [tools.ozone.report.defs.reasonSexualUnlabeled]:
- com.atproto.moderation.defs.reasonSexual,
- [tools.ozone.report.defs.reasonSexualOther]:
- com.atproto.moderation.defs.reasonSexual,
+ [tools.ozone.report.defs.reasonSexualAbuseContent.value]:
+ com.atproto.moderation.defs.reasonSexual.value,
+ [tools.ozone.report.defs.reasonSexualNCII.value]:
+ com.atproto.moderation.defs.reasonSexual.value,
+ [tools.ozone.report.defs.reasonSexualDeepfake.value]:
+ com.atproto.moderation.defs.reasonSexual.value,
+ [tools.ozone.report.defs.reasonSexualAnimal.value]:
+ com.atproto.moderation.defs.reasonSexual.value,
+ [tools.ozone.report.defs.reasonSexualUnlabeled.value]:
+ com.atproto.moderation.defs.reasonSexual.value,
+ [tools.ozone.report.defs.reasonSexualOther.value]:
+ com.atproto.moderation.defs.reasonSexual.value,
- [tools.ozone.report.defs.reasonChildSafetyCSAM]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonChildSafetyGroom]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonChildSafetyPrivacy]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonChildSafetyHarassment]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonChildSafetyOther]:
- com.atproto.moderation.defs.reasonViolation,
+ [tools.ozone.report.defs.reasonChildSafetyCSAM.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonChildSafetyGroom.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonChildSafetyPrivacy.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonChildSafetyHarassment.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonChildSafetyOther.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
- [tools.ozone.report.defs.reasonHarassmentTroll]:
- com.atproto.moderation.defs.reasonRude,
- [tools.ozone.report.defs.reasonHarassmentTargeted]:
- com.atproto.moderation.defs.reasonRude,
- [tools.ozone.report.defs.reasonHarassmentHateSpeech]:
- com.atproto.moderation.defs.reasonRude,
- [tools.ozone.report.defs.reasonHarassmentDoxxing]:
- com.atproto.moderation.defs.reasonRude,
- [tools.ozone.report.defs.reasonHarassmentOther]:
- com.atproto.moderation.defs.reasonRude,
+ [tools.ozone.report.defs.reasonHarassmentTroll.value]:
+ com.atproto.moderation.defs.reasonRude.value,
+ [tools.ozone.report.defs.reasonHarassmentTargeted.value]:
+ com.atproto.moderation.defs.reasonRude.value,
+ [tools.ozone.report.defs.reasonHarassmentHateSpeech.value]:
+ com.atproto.moderation.defs.reasonRude.value,
+ [tools.ozone.report.defs.reasonHarassmentDoxxing.value]:
+ com.atproto.moderation.defs.reasonRude.value,
+ [tools.ozone.report.defs.reasonHarassmentOther.value]:
+ com.atproto.moderation.defs.reasonRude.value,
- [tools.ozone.report.defs.reasonMisleadingBot]:
- com.atproto.moderation.defs.reasonMisleading,
- [tools.ozone.report.defs.reasonMisleadingImpersonation]:
- com.atproto.moderation.defs.reasonMisleading,
- [tools.ozone.report.defs.reasonMisleadingSpam]:
- com.atproto.moderation.defs.reasonSpam,
- [tools.ozone.report.defs.reasonMisleadingScam]:
- com.atproto.moderation.defs.reasonMisleading,
- [tools.ozone.report.defs.reasonMisleadingElections]:
- com.atproto.moderation.defs.reasonMisleading,
- [tools.ozone.report.defs.reasonMisleadingOther]:
- com.atproto.moderation.defs.reasonMisleading,
+ [tools.ozone.report.defs.reasonMisleadingBot.value]:
+ com.atproto.moderation.defs.reasonMisleading.value,
+ [tools.ozone.report.defs.reasonMisleadingImpersonation.value]:
+ com.atproto.moderation.defs.reasonMisleading.value,
+ [tools.ozone.report.defs.reasonMisleadingSpam.value]:
+ com.atproto.moderation.defs.reasonSpam.value,
+ [tools.ozone.report.defs.reasonMisleadingScam.value]:
+ com.atproto.moderation.defs.reasonMisleading.value,
+ [tools.ozone.report.defs.reasonMisleadingElections.value]:
+ com.atproto.moderation.defs.reasonMisleading.value,
+ [tools.ozone.report.defs.reasonMisleadingOther.value]:
+ com.atproto.moderation.defs.reasonMisleading.value,
- [tools.ozone.report.defs.reasonRuleSiteSecurity]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonRuleProhibitedSales]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonRuleBanEvasion]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonRuleOther]:
- com.atproto.moderation.defs.reasonViolation,
+ [tools.ozone.report.defs.reasonRuleSiteSecurity.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonRuleProhibitedSales.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonRuleBanEvasion.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonRuleOther.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
- [tools.ozone.report.defs.reasonSelfHarmContent]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonSelfHarmED]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonSelfHarmStunts]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonSelfHarmSubstances]:
- com.atproto.moderation.defs.reasonViolation,
- [tools.ozone.report.defs.reasonSelfHarmOther]:
- com.atproto.moderation.defs.reasonViolation,
+ [tools.ozone.report.defs.reasonSelfHarmContent.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonSelfHarmED.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonSelfHarmStunts.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonSelfHarmSubstances.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
+ [tools.ozone.report.defs.reasonSelfHarmOther.value]:
+ com.atproto.moderation.defs.reasonViolation.value,
}
/**
@@ -130,26 +130,26 @@ export const OLD_TO_NEW_REASONS_MAP: Record<
>,
tools.ozone.report.defs.ReasonType
> = {
- [com.atproto.moderation.defs.reasonSpam]: [
- tools.ozone.report.defs.reasonMisleadingSpam,
+ [com.atproto.moderation.defs.reasonSpam.value]: [
+ tools.ozone.report.defs.reasonMisleadingSpam.value,
],
- [com.atproto.moderation.defs.reasonViolation]: [
- tools.ozone.report.defs.reasonRuleOther,
+ [com.atproto.moderation.defs.reasonViolation.value]: [
+ tools.ozone.report.defs.reasonRuleOther.value,
],
- [com.atproto.moderation.defs.reasonMisleading]: [
- tools.ozone.report.defs.reasonMisleadingOther,
+ [com.atproto.moderation.defs.reasonMisleading.value]: [
+ tools.ozone.report.defs.reasonMisleadingOther.value,
],
- [com.atproto.moderation.defs.reasonSexual]: [
- tools.ozone.report.defs.reasonSexualUnlabeled,
+ [com.atproto.moderation.defs.reasonSexual.value]: [
+ tools.ozone.report.defs.reasonSexualUnlabeled.value,
],
- [com.atproto.moderation.defs.reasonRude]: [
- tools.ozone.report.defs.reasonHarassmentOther,
+ [com.atproto.moderation.defs.reasonRude.value]: [
+ tools.ozone.report.defs.reasonHarassmentOther.value,
],
- [com.atproto.moderation.defs.reasonOther]: [
- tools.ozone.report.defs.reasonOther,
+ [com.atproto.moderation.defs.reasonOther.value]: [
+ tools.ozone.report.defs.reasonOther.value,
],
- [com.atproto.moderation.defs.reasonAppeal]: [
- tools.ozone.report.defs.reasonAppeal,
+ [com.atproto.moderation.defs.reasonAppeal.value]: [
+ tools.ozone.report.defs.reasonAppeal.value,
],
}
@@ -159,14 +159,14 @@ export const OLD_TO_NEW_REASONS_MAP: Record<
*/
export const OTHER_REPORT_REASONS: Set =
new Set([
- tools.ozone.report.defs.reasonViolenceOther,
- tools.ozone.report.defs.reasonSexualOther,
- tools.ozone.report.defs.reasonChildSafetyOther,
- tools.ozone.report.defs.reasonHarassmentOther,
- tools.ozone.report.defs.reasonMisleadingOther,
- tools.ozone.report.defs.reasonRuleOther,
- tools.ozone.report.defs.reasonSelfHarmOther,
- tools.ozone.report.defs.reasonOther,
+ tools.ozone.report.defs.reasonViolenceOther.value,
+ tools.ozone.report.defs.reasonSexualOther.value,
+ tools.ozone.report.defs.reasonChildSafetyOther.value,
+ tools.ozone.report.defs.reasonHarassmentOther.value,
+ tools.ozone.report.defs.reasonMisleadingOther.value,
+ tools.ozone.report.defs.reasonRuleOther.value,
+ tools.ozone.report.defs.reasonSelfHarmOther.value,
+ tools.ozone.report.defs.reasonOther.value,
])
/**
@@ -174,10 +174,10 @@ export const OTHER_REPORT_REASONS: Set =
*/
export const BSKY_LABELER_ONLY_REPORT_REASONS: Set =
new Set([
- tools.ozone.report.defs.reasonChildSafetyCSAM,
- tools.ozone.report.defs.reasonChildSafetyGroom,
- tools.ozone.report.defs.reasonChildSafetyOther,
- tools.ozone.report.defs.reasonViolenceExtremistContent,
+ tools.ozone.report.defs.reasonChildSafetyCSAM.value,
+ tools.ozone.report.defs.reasonChildSafetyGroom.value,
+ tools.ozone.report.defs.reasonChildSafetyOther.value,
+ tools.ozone.report.defs.reasonViolenceExtremistContent.value,
])
/**
diff --git a/src/components/moderation/ReportDialog/state.test.ts b/src/components/moderation/ReportDialog/state.test.ts
index ccea65f72d..6f64c1a7c3 100644
--- a/src/components/moderation/ReportDialog/state.test.ts
+++ b/src/components/moderation/ReportDialog/state.test.ts
@@ -8,12 +8,12 @@ import {
const nciiOption = {
title: 'Non-consensual intimate imagery',
- reason: tools.ozone.report.defs.reasonSexualNCII,
+ reason: tools.ozone.report.defs.reasonSexualNCII.value,
}
const otherOption = {
title: 'Unlabeled adult content',
- reason: tools.ozone.report.defs.reasonSexualUnlabeled,
+ reason: tools.ozone.report.defs.reasonSexualUnlabeled.value,
}
function selectNciiOption(state: ReportState = initialState) {
diff --git a/src/components/moderation/ReportDialog/state.ts b/src/components/moderation/ReportDialog/state.ts
index feb5e01339..9be5145ce7 100644
--- a/src/components/moderation/ReportDialog/state.ts
+++ b/src/components/moderation/ReportDialog/state.ts
@@ -124,7 +124,7 @@ export function reducer(state: ReportState, action: ReportAction): ReportState {
}
case 'selectOption': {
const isNcii =
- action.option.reason === tools.ozone.report.defs.reasonSexualNCII
+ action.option.reason === tools.ozone.report.defs.reasonSexualNCII.value
return {
...state,
selectedOption: action.option,
diff --git a/src/components/moderation/ReportDialog/utils/useReportOptions.ts b/src/components/moderation/ReportDialog/utils/useReportOptions.ts
index 581e8b7b52..075a0ec582 100644
--- a/src/components/moderation/ReportDialog/utils/useReportOptions.ts
+++ b/src/components/moderation/ReportDialog/utils/useReportOptions.ts
@@ -37,27 +37,27 @@ export function useReportOptions() {
options: [
{
title: _(msg`Spam`),
- reason: tools.ozone.report.defs.reasonMisleadingSpam,
+ reason: tools.ozone.report.defs.reasonMisleadingSpam.value,
},
{
title: _(msg`Scam`),
- reason: tools.ozone.report.defs.reasonMisleadingScam,
+ reason: tools.ozone.report.defs.reasonMisleadingScam.value,
},
{
title: _(msg`Fake account or bot`),
- reason: tools.ozone.report.defs.reasonMisleadingBot,
+ reason: tools.ozone.report.defs.reasonMisleadingBot.value,
},
{
title: _(msg`Impersonation`),
- reason: tools.ozone.report.defs.reasonMisleadingImpersonation,
+ reason: tools.ozone.report.defs.reasonMisleadingImpersonation.value,
},
{
title: _(msg`False information about elections`),
- reason: tools.ozone.report.defs.reasonMisleadingElections,
+ reason: tools.ozone.report.defs.reasonMisleadingElections.value,
},
{
title: _(msg`Other misleading content`),
- reason: tools.ozone.report.defs.reasonMisleadingOther,
+ reason: tools.ozone.report.defs.reasonMisleadingOther.value,
},
],
},
@@ -70,27 +70,27 @@ export function useReportOptions() {
options: [
{
title: _(msg`Unlabeled adult content`),
- reason: tools.ozone.report.defs.reasonSexualUnlabeled,
+ reason: tools.ozone.report.defs.reasonSexualUnlabeled.value,
},
{
title: _(msg`Adult sexual abuse content`),
- reason: tools.ozone.report.defs.reasonSexualAbuseContent,
+ reason: tools.ozone.report.defs.reasonSexualAbuseContent.value,
},
{
title: _(msg`Non-consensual intimate imagery`),
- reason: tools.ozone.report.defs.reasonSexualNCII,
+ reason: tools.ozone.report.defs.reasonSexualNCII.value,
},
{
title: _(msg`Deepfake adult content`),
- reason: tools.ozone.report.defs.reasonSexualDeepfake,
+ reason: tools.ozone.report.defs.reasonSexualDeepfake.value,
},
{
title: _(msg`Animal sexual abuse`),
- reason: tools.ozone.report.defs.reasonSexualAnimal,
+ reason: tools.ozone.report.defs.reasonSexualAnimal.value,
},
{
title: _(msg`Other sexual violence content`),
- reason: tools.ozone.report.defs.reasonSexualOther,
+ reason: tools.ozone.report.defs.reasonSexualOther.value,
},
],
},
@@ -101,23 +101,23 @@ export function useReportOptions() {
options: [
{
title: _(msg`Trolling`),
- reason: tools.ozone.report.defs.reasonHarassmentTroll,
+ reason: tools.ozone.report.defs.reasonHarassmentTroll.value,
},
{
title: _(msg`Targeted harassment`),
- reason: tools.ozone.report.defs.reasonHarassmentTargeted,
+ reason: tools.ozone.report.defs.reasonHarassmentTargeted.value,
},
{
title: _(msg`Hate speech`),
- reason: tools.ozone.report.defs.reasonHarassmentHateSpeech,
+ reason: tools.ozone.report.defs.reasonHarassmentHateSpeech.value,
},
{
title: _(msg`Doxxing`),
- reason: tools.ozone.report.defs.reasonHarassmentDoxxing,
+ reason: tools.ozone.report.defs.reasonHarassmentDoxxing.value,
},
{
title: _(msg`Other harassing or hateful content`),
- reason: tools.ozone.report.defs.reasonHarassmentOther,
+ reason: tools.ozone.report.defs.reasonHarassmentOther.value,
},
],
},
@@ -128,31 +128,32 @@ export function useReportOptions() {
options: [
{
title: _(msg`Animal welfare`),
- reason: tools.ozone.report.defs.reasonViolenceAnimal,
+ reason: tools.ozone.report.defs.reasonViolenceAnimal.value,
},
{
title: _(msg`Threats or incitement`),
- reason: tools.ozone.report.defs.reasonViolenceThreats,
+ reason: tools.ozone.report.defs.reasonViolenceThreats.value,
},
{
title: _(msg`Graphic violent content`),
- reason: tools.ozone.report.defs.reasonViolenceGraphicContent,
+ reason: tools.ozone.report.defs.reasonViolenceGraphicContent.value,
},
{
title: _(msg`Glorification of violence`),
- reason: tools.ozone.report.defs.reasonViolenceGlorification,
+ reason: tools.ozone.report.defs.reasonViolenceGlorification.value,
},
{
title: _(msg`Extremist content`),
- reason: tools.ozone.report.defs.reasonViolenceExtremistContent,
+ reason:
+ tools.ozone.report.defs.reasonViolenceExtremistContent.value,
},
{
title: _(msg`Human trafficking`),
- reason: tools.ozone.report.defs.reasonViolenceTrafficking,
+ reason: tools.ozone.report.defs.reasonViolenceTrafficking.value,
},
{
title: _(msg`Other violent content`),
- reason: tools.ozone.report.defs.reasonViolenceOther,
+ reason: tools.ozone.report.defs.reasonViolenceOther.value,
},
],
},
@@ -163,23 +164,23 @@ export function useReportOptions() {
options: [
{
title: _(msg`Child Sexual Abuse Material (CSAM)`),
- reason: tools.ozone.report.defs.reasonChildSafetyCSAM,
+ reason: tools.ozone.report.defs.reasonChildSafetyCSAM.value,
},
{
title: _(msg`Grooming or predatory behavior`),
- reason: tools.ozone.report.defs.reasonChildSafetyGroom,
+ reason: tools.ozone.report.defs.reasonChildSafetyGroom.value,
},
{
title: _(msg`Privacy violation of a minor`),
- reason: tools.ozone.report.defs.reasonChildSafetyPrivacy,
+ reason: tools.ozone.report.defs.reasonChildSafetyPrivacy.value,
},
{
title: _(msg`Minor harassment or bullying`),
- reason: tools.ozone.report.defs.reasonChildSafetyHarassment,
+ reason: tools.ozone.report.defs.reasonChildSafetyHarassment.value,
},
{
title: _(msg`Other child safety issue`),
- reason: tools.ozone.report.defs.reasonChildSafetyOther,
+ reason: tools.ozone.report.defs.reasonChildSafetyOther.value,
},
],
},
@@ -190,23 +191,23 @@ export function useReportOptions() {
options: [
{
title: _(msg`Content promoting or depicting self-harm`),
- reason: tools.ozone.report.defs.reasonSelfHarmContent,
+ reason: tools.ozone.report.defs.reasonSelfHarmContent.value,
},
{
title: _(msg`Eating disorders`),
- reason: tools.ozone.report.defs.reasonSelfHarmED,
+ reason: tools.ozone.report.defs.reasonSelfHarmED.value,
},
{
title: _(msg`Dangerous challenges or activities`),
- reason: tools.ozone.report.defs.reasonSelfHarmStunts,
+ reason: tools.ozone.report.defs.reasonSelfHarmStunts.value,
},
{
title: _(msg`Dangerous substances or drug abuse`),
- reason: tools.ozone.report.defs.reasonSelfHarmSubstances,
+ reason: tools.ozone.report.defs.reasonSelfHarmSubstances.value,
},
{
title: _(msg`Other dangerous content`),
- reason: tools.ozone.report.defs.reasonSelfHarmOther,
+ reason: tools.ozone.report.defs.reasonSelfHarmOther.value,
},
],
},
@@ -217,19 +218,19 @@ export function useReportOptions() {
options: [
{
title: _(msg`Hacking or system attacks`),
- reason: tools.ozone.report.defs.reasonRuleSiteSecurity,
+ reason: tools.ozone.report.defs.reasonRuleSiteSecurity.value,
},
{
title: _(msg`Promoting or selling prohibited items or services`),
- reason: tools.ozone.report.defs.reasonRuleProhibitedSales,
+ reason: tools.ozone.report.defs.reasonRuleProhibitedSales.value,
},
{
title: _(msg`Banned user returning`),
- reason: tools.ozone.report.defs.reasonRuleBanEvasion,
+ reason: tools.ozone.report.defs.reasonRuleBanEvasion.value,
},
{
title: _(msg`Other network rule-breaking`),
- reason: tools.ozone.report.defs.reasonRuleOther,
+ reason: tools.ozone.report.defs.reasonRuleOther.value,
},
],
},
@@ -240,7 +241,7 @@ export function useReportOptions() {
options: [
{
title: _(msg`Other`),
- reason: tools.ozone.report.defs.reasonOther,
+ reason: tools.ozone.report.defs.reasonOther.value,
},
],
},
diff --git a/src/env/common.ts b/src/env/common.ts
index f36f671695..897c1fd3a9 100644
--- a/src/env/common.ts
+++ b/src/env/common.ts
@@ -1,4 +1,4 @@
-import {type Did} from '@atproto/syntax'
+import {type DidString} from '@atproto/syntax'
import packageJson from '#/../package.json'
/**
@@ -74,14 +74,16 @@ export const LOG_DEBUG: string = process.env.EXPO_PUBLIC_LOG_DEBUG || ''
/**
* The DID of the Bluesky appview to proxy to
*/
-export const BLUESKY_PROXY_DID: Did =
- process.env.EXPO_PUBLIC_BLUESKY_PROXY_DID || 'did:web:api.bsky.app'
+export const BLUESKY_PROXY_DID: DidString =
+ (process.env.EXPO_PUBLIC_BLUESKY_PROXY_DID as DidString) ||
+ 'did:web:api.bsky.app'
/**
* The DID of the chat service to proxy to
*/
-export const CHAT_PROXY_DID: Did =
- process.env.EXPO_PUBLIC_CHAT_PROXY_DID || 'did:web:api.bsky.chat'
+export const CHAT_PROXY_DID: DidString =
+ (process.env.EXPO_PUBLIC_CHAT_PROXY_DID as DidString) ||
+ 'did:web:api.bsky.chat'
/**
* Metrics API host
diff --git a/src/features/liveNow/components/EditLiveDialog.tsx b/src/features/liveNow/components/EditLiveDialog.tsx
index 6c9dff3092..0da9d06210 100644
--- a/src/features/liveNow/components/EditLiveDialog.tsx
+++ b/src/features/liveNow/components/EditLiveDialog.tsx
@@ -55,7 +55,9 @@ function DialogInner({
const {_, i18n} = useLingui()
const t = useTheme()
- const [liveLink, setLiveLink] = useState(embed.external.uri)
+ /* Holds the user-editable link text, so it is a plain string rather than
+ * the branded `uri` the view it was seeded from carries. */
+ const [liveLink, setLiveLink] = useState(embed.external.uri)
const [liveLinkError, setLiveLinkError] = useState('')
const tick = useTickEveryMinute()
diff --git a/src/features/liveNow/components/GoLiveDisabledDialog.tsx b/src/features/liveNow/components/GoLiveDisabledDialog.tsx
index 24324817fc..63eddbf55f 100644
--- a/src/features/liveNow/components/GoLiveDisabledDialog.tsx
+++ b/src/features/liveNow/components/GoLiveDisabledDialog.tsx
@@ -60,7 +60,7 @@ export function DialogInner({
await client.call(
com.atproto.moderation.createReport,
{
- reasonType: tools.ozone.report.defs.reasonAppeal,
+ reasonType: tools.ozone.report.defs.reasonAppeal.value,
subject: {
$type: 'com.atproto.repo.strongRef',
// a status view's uri is an at-uri produced by the appview
diff --git a/src/features/liveNow/components/LiveStatusDialog.tsx b/src/features/liveNow/components/LiveStatusDialog.tsx
index 65b757bcf0..bc03d9da21 100644
--- a/src/features/liveNow/components/LiveStatusDialog.tsx
+++ b/src/features/liveNow/components/LiveStatusDialog.tsx
@@ -1,13 +1,13 @@
import {useCallback, useMemo} from 'react'
import {View} from 'react-native'
import {Image} from 'expo-image'
+import {moderateStatus} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {app} from '#/lexicons'
import {useOpenLink} from '#/lib/hooks/useOpenLink'
-import {moderateStatus} from '#/lib/moderation/subjects'
import {type NavigationProp} from '#/lib/routes/types'
import {sanitizeHandle} from '#/lib/strings/handles'
import {toNiceDomain} from '#/lib/strings/url-helpers'
diff --git a/src/features/liveNow/index.tsx b/src/features/liveNow/index.tsx
index 9303e734f6..88daad4df7 100644
--- a/src/features/liveNow/index.tsx
+++ b/src/features/liveNow/index.tsx
@@ -1,7 +1,8 @@
import {useMemo} from 'react'
import {retry} from '@atproto/common-web'
-import {type l, type $Typed} from '@atproto/lex'
+import {type $Typed, type l, type UriString} from '@atproto/lex'
import {type AtIdentifierString, AtUri, toDatetimeString} from '@atproto/syntax'
+import {moderateStatus} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
@@ -10,7 +11,6 @@ import {isAfter, parseISO} from 'date-fns'
import {uploadBlob} from '#/lib/api'
import {imageToThumb} from '#/lib/api/resolve'
import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta'
-import {moderateStatus} from '#/lib/moderation/subjects'
import {matchXrpcError} from '#/lib/xrpc-error'
import {useAppConfig} from '#/state/appConfig'
import {
@@ -335,7 +335,7 @@ export function useUpsertLiveStatusMutation(
$type: 'app.bsky.actor.defs#statusView',
status: 'app.bsky.actor.status#live',
isActive: true,
- expiresAt: expiresAt.toISOString(),
+ expiresAt: toDatetimeString(expiresAt),
embed:
record.embed && image
? {
@@ -343,7 +343,8 @@ export function useUpsertLiveStatusMutation(
external: {
...record.embed.external,
$type: 'app.bsky.embed.external#viewExternal',
- thumb: image,
+ // an opengraph image URL from link resolution
+ thumb: image as UriString,
},
}
: undefined,
diff --git a/src/lib/api/feed/home.ts b/src/lib/api/feed/home.ts
index 6e15b75530..db66810dbe 100644
--- a/src/lib/api/feed/home.ts
+++ b/src/lib/api/feed/home.ts
@@ -15,7 +15,12 @@ import {type FeedAPI, type FeedAPIResponse} from './types'
// we use this fallback marker post to drive this instead. see Feed.tsx
// for the usage.
// -prf
-export const FALLBACK_MARKER_POST: app.bsky.feed.defs.FeedViewPost = {
+/*
+ * A synthetic marker, not a real view: its `uri`/`did`/`indexedAt` are
+ * deliberately not well-formed, so the literal is asserted rather than branded.
+ * Only `post.uri` is ever read (see Feed.tsx).
+ */
+export const FALLBACK_MARKER_POST = {
post: {
uri: 'fallback-marker-post',
cid: 'fake',
@@ -26,7 +31,7 @@ export const FALLBACK_MARKER_POST: app.bsky.feed.defs.FeedViewPost = {
},
indexedAt: new Date().toISOString(),
},
-}
+} as unknown as app.bsky.feed.defs.FeedViewPost
export class HomeFeedAPI implements FeedAPI {
client: Client
diff --git a/src/lib/demo.ts b/src/lib/demo.ts
index b83b4b9a68..826049045a 100644
--- a/src/lib/demo.ts
+++ b/src/lib/demo.ts
@@ -1,11 +1,13 @@
+import {toDatetimeString} from '@atproto/syntax'
import {subDays, subMinutes} from 'date-fns'
-import {app} from '#/lexicons'
+
+import {type app} from '#/lexicons'
const DID = `did:plc:z72i7hdynmk6r22z27h6tvur`
const NOW = new Date()
-const POST_1_DATE = subMinutes(NOW, 2).toISOString()
-const POST_2_DATE = subMinutes(NOW, 4).toISOString()
-const POST_3_DATE = subMinutes(NOW, 5).toISOString()
+const POST_1_DATE = toDatetimeString(subMinutes(NOW, 2))
+const POST_2_DATE = toDatetimeString(subMinutes(NOW, 4))
+const POST_3_DATE = toDatetimeString(subMinutes(NOW, 5))
export const DEMO_FEED = {
feed: [
@@ -31,7 +33,7 @@ export const DEMO_FEED = {
issuer: DID,
uri: `at://${DID}/app.bsky.graph.verification/post1`,
isValid: true,
- createdAt: subDays(NOW, 11).toISOString(),
+ createdAt: toDatetimeString(subDays(NOW, 11)),
},
],
verifiedStatus: 'valid',
diff --git a/src/lib/moderation/subjects.ts b/src/lib/moderation/subjects.ts
deleted file mode 100644
index fa6de1366a..0000000000
--- a/src/lib/moderation/subjects.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-import {
- hasMutedWord as sdkHasMutedWord,
- moderateFeedGenerator as sdkModerateFeedGenerator,
- moderateNotification as sdkModerateNotification,
- moderatePost as sdkModeratePost,
- moderateProfile as sdkModerateProfile,
- moderateStatus as sdkModerateStatus,
- moderateUserList as sdkModerateUserList,
- type ModerationDecision,
- type ModerationOpts,
-} from '@bsky.app/sdk/moderation'
-
-import {app, chat} from '#/lexicons'
-
-/*
- * TRANSITIONAL. The moderation implementation now comes from
- * `@bsky.app/sdk/moderation`, whose subject types are the generated
- * `#/lexicons` views - so their `did`/`uri`/`cid` fields are branded
- * (`DidString`, `AtUriString`). Many read paths still emit the identically
- * shaped `@atproto/api` views, whose same fields are plain `string`.
- *
- * A plain `string` is not assignable to a branded template-literal type, so
- * every `moderate*` call taking an unmigrated view fails to typecheck even
- * though the value is byte-identical - the runtime only ever reads `.did`,
- * `.labels` and `.viewer`, none of which the brand affects.
- *
- * These wrappers widen each subject parameter to accept a view from either
- * world and drop the brand on the way in. Delete this module once every
- * producer emits `#/lexicons` views (the `@atproto/api` removal pass) and point
- * callers back at `@bsky.app/sdk/moderation` directly.
- */
-
-type AnyProfileSubject =
- | app.bsky.actor.defs.ProfileViewBasic
- | app.bsky.actor.defs.ProfileView
- | app.bsky.actor.defs.ProfileViewDetailed
- | chat.bsky.actor.defs.ProfileViewBasic
- | app.bsky.actor.defs.ProfileViewBasic
- | app.bsky.actor.defs.ProfileView
- | app.bsky.actor.defs.ProfileViewDetailed
- | chat.bsky.actor.defs.ProfileViewBasic
-
-type AnyPostSubject = app.bsky.feed.defs.PostView | app.bsky.feed.defs.PostView
-
-type AnyUserListSubject =
- | app.bsky.graph.defs.ListViewBasic
- | app.bsky.graph.defs.ListView
- | app.bsky.graph.defs.ListViewBasic
- | app.bsky.graph.defs.ListView
-
-type AnyFeedGeneratorSubject =
- | app.bsky.feed.defs.GeneratorView
- | app.bsky.feed.defs.GeneratorView
-
-type AnyNotificationSubject =
- | app.bsky.notification.listNotifications.Notification
- | app.bsky.notification.listNotifications.Notification
-
-export function moderateProfile(
- subject: AnyProfileSubject,
- opts: ModerationOpts,
-): ModerationDecision {
- return sdkModerateProfile(subject as app.bsky.actor.defs.ProfileView, opts)
-}
-
-export function moderateStatus(
- subject: AnyProfileSubject,
- opts: ModerationOpts,
-): ModerationDecision {
- return sdkModerateStatus(subject as app.bsky.actor.defs.ProfileView, opts)
-}
-
-export function moderatePost(
- subject: AnyPostSubject,
- opts: ModerationOpts,
-): ModerationDecision {
- return sdkModeratePost(subject as app.bsky.feed.defs.PostView, opts)
-}
-
-export function moderateUserList(
- subject: AnyUserListSubject,
- opts: ModerationOpts,
-): ModerationDecision {
- return sdkModerateUserList(subject as app.bsky.graph.defs.ListView, opts)
-}
-
-export function moderateFeedGenerator(
- subject: AnyFeedGeneratorSubject,
- opts: ModerationOpts,
-): ModerationDecision {
- return sdkModerateFeedGenerator(
- subject as app.bsky.feed.defs.GeneratorView,
- opts,
- )
-}
-
-export function moderateNotification(
- subject: AnyNotificationSubject,
- opts: ModerationOpts,
-): ModerationDecision {
- return sdkModerateNotification(
- subject as app.bsky.notification.listNotifications.Notification,
- opts,
- )
-}
-
-/**
- * Widens `facets`/`actor` for the same reason the `moderate*` wrappers widen
- * their subjects: mute-word matching reads only `text`/`features`/`langs`, none
- * of which the brand affects.
- */
-export function hasMutedWord(params: {
- mutedWords: app.bsky.actor.defs.MutedWord[]
- text: string
- facets?: app.bsky.richtext.facet.Main[] | app.bsky.richtext.facet.Main[]
- outlineTags?: string[]
- languages?: string[]
- actor?: AnyProfileSubject
-}): boolean {
- return sdkHasMutedWord({
- ...params,
- facets: params.facets as app.bsky.richtext.facet.Main[] | undefined,
- actor: params.actor as app.bsky.actor.defs.ProfileView | undefined,
- })
-}
diff --git a/src/lib/strings/rich-text-helpers.ts b/src/lib/strings/rich-text-helpers.ts
index a6341025ff..078b91e400 100644
--- a/src/lib/strings/rich-text-helpers.ts
+++ b/src/lib/strings/rich-text-helpers.ts
@@ -30,26 +30,3 @@ export function richTextToString(rt: RichText, loose: boolean): string {
return result
}
-
-/**
- * Widens facets typed by the legacy `@atproto/api` codegen into the shape the
- * SDK's `RichText` accepts.
- *
- * The two are the same lexicon and identical at runtime; they differ only in
- * that the SDK brands `did`/`uri` as template literal types, which makes the
- * legacy `string` versions unassignable. Call this where facets read off an
- * `@atproto/api` view type are handed to `new RichText(...)`.
- *
- * Transitional: it goes away once the view types come from the SDK too.
- */
-export function asSdkFacets(
- facets: {index: {byteStart: number; byteEnd: number}; features: unknown[]}[],
-): app.bsky.richtext.facet.Main[]
-export function asSdkFacets(
- facets:
- | {index: {byteStart: number; byteEnd: number}; features: unknown[]}[]
- | undefined,
-): app.bsky.richtext.facet.Main[] | undefined
-export function asSdkFacets(facets: unknown) {
- return facets as app.bsky.richtext.facet.Main[] | undefined
-}
diff --git a/src/lib/xrpc-error.ts b/src/lib/xrpc-error.ts
index ab4af7a6b1..ec4fd7e872 100644
--- a/src/lib/xrpc-error.ts
+++ b/src/lib/xrpc-error.ts
@@ -1,6 +1,7 @@
import {
getMain,
type InferMethodError,
+ LexError,
type Main,
type Procedure,
type Query,
@@ -27,6 +28,19 @@ export function getErrorStatus(e: unknown): number | undefined {
return e instanceof XrpcResponseError ? e.status : undefined
}
+/**
+ * The lexicon error code (`err.error`), or undefined when `e` carries none.
+ * Gated on `LexError` (the base of every `XrpcError`) rather than `XrpcError`,
+ * so the non-XRPC lex errors are covered too.
+ *
+ * Prefer {@link matchXrpcError} where the method that threw is known: it
+ * constrains the compared name to that method's declared errors. Use this only
+ * where the source method is genuinely ambiguous.
+ */
+export function getErrorName(e: unknown): string | undefined {
+ return e instanceof LexError ? e.error : undefined
+}
+
/**
* Same nsid means `e` was thrown for this method schema, so `e` can be
* treated as an `XrpcResponseError` - which is what lets the SDK's
diff --git a/src/screens/CustomFeed/index.tsx b/src/screens/CustomFeed/index.tsx
index 0c884c2fda..72269bde03 100644
--- a/src/screens/CustomFeed/index.tsx
+++ b/src/screens/CustomFeed/index.tsx
@@ -175,7 +175,7 @@ export function CustomFeedScreenInner({
const isVideoFeed = useMemo(() => {
const isBskyVideoFeed = VIDEO_FEED_URIS.includes(feedInfo.uri)
const feedIsVideoMode =
- feedInfo.contentMode === app.bsky.feed.defs.contentModeVideo
+ feedInfo.contentMode === app.bsky.feed.defs.contentModeVideo.value
const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
return IS_NATIVE && _isVideoFeed
}, [feedInfo])
diff --git a/src/screens/List/ListHiddenScreen.tsx b/src/screens/List/ListHiddenScreen.tsx
index c48983cd56..fcc0acc71c 100644
--- a/src/screens/List/ListHiddenScreen.tsx
+++ b/src/screens/List/ListHiddenScreen.tsx
@@ -43,7 +43,7 @@ export function ListHiddenScreen({
const goBack = useGoBack()
const queryClient = useQueryClient()
- const isModList = list.purpose === app.bsky.graph.defs.modlist
+ const isModList = list.purpose === app.bsky.graph.defs.modlist.value
const [isProcessing, setIsProcessing] = useState(false)
const listBlockMutation = useListBlockMutation()
diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx
index f973dbe44b..11db727688 100644
--- a/src/screens/Messages/Conversation.tsx
+++ b/src/screens/Messages/Conversation.tsx
@@ -5,6 +5,7 @@ import {
ScrollEdgeEffect,
ScrollEdgeEffectProvider,
} from '@bsky.app/expo-scroll-edge-effect'
+import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro'
import {
type RouteProp,
@@ -19,7 +20,6 @@ import * as bsky from '#/types/bsky'
import {chat} from '#/lexicons'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useViewportZoomLock} from '#/lib/hooks/useViewportZoomLock'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {
type CommonNavigatorParams,
type NavigationProp,
diff --git a/src/screens/Messages/ConversationSettings/Member.tsx b/src/screens/Messages/ConversationSettings/Member.tsx
index 8fe07978e8..ad6fdc6136 100644
--- a/src/screens/Messages/ConversationSettings/Member.tsx
+++ b/src/screens/Messages/ConversationSettings/Member.tsx
@@ -1,9 +1,9 @@
import {View} from 'react-native'
+import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro'
import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {logger} from '#/logger'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
diff --git a/src/screens/Messages/JoinRequest.tsx b/src/screens/Messages/JoinRequest.tsx
index ed00626304..d5caea9419 100644
--- a/src/screens/Messages/JoinRequest.tsx
+++ b/src/screens/Messages/JoinRequest.tsx
@@ -2,12 +2,12 @@ import {useEffect} from 'react'
import {View} from 'react-native'
import {ImageBackground} from 'expo-image'
import {type ThemeName} from '@bsky.app/alf'
+import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro'
import * as bsky from '#/types/bsky'
import {chat} from '#/lexicons'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useJoinLinkPreviewsQuery} from '#/state/queries/join-links'
diff --git a/src/screens/Messages/components/ChatDisabled.tsx b/src/screens/Messages/components/ChatDisabled.tsx
index e8e69af722..31bdebc4d2 100644
--- a/src/screens/Messages/components/ChatDisabled.tsx
+++ b/src/screens/Messages/components/ChatDisabled.tsx
@@ -103,7 +103,7 @@ function DialogInner() {
await client.call(
com.atproto.moderation.createReport,
{
- reasonType: tools.ozone.report.defs.reasonAppeal,
+ reasonType: tools.ozone.report.defs.reasonAppeal.value,
subject: {
$type: 'com.atproto.admin.defs#repoRef',
// the persisted account did is already resolved
diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx
index 48553cc6c1..05c383e596 100644
--- a/src/screens/Messages/components/ChatListItem.tsx
+++ b/src/screens/Messages/components/ChatListItem.tsx
@@ -1,6 +1,7 @@
import {useCallback, useMemo, useState} from 'react'
import {type GestureResponderEvent, View} from 'react-native'
import {
+ moderateProfile,
type ModerationDecision,
type ModerationOpts,
} from '@bsky.app/sdk/moderation'
@@ -12,7 +13,6 @@ import {chat} from '#/lexicons'
import {GestureActionView} from '#/lib/custom-animations/GestureActionView'
import {useHaptics} from '#/lib/haptics'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {decrementBadgeCount} from '#/lib/notifications/notifications'
import {sanitizeHandle} from '#/lib/strings/handles'
import {
diff --git a/src/screens/Messages/components/ChatStatusInfo.tsx b/src/screens/Messages/components/ChatStatusInfo.tsx
index 4d3ebe2ab8..1910159a49 100644
--- a/src/screens/Messages/components/ChatStatusInfo.tsx
+++ b/src/screens/Messages/components/ChatStatusInfo.tsx
@@ -1,10 +1,10 @@
import {useCallback, useMemo} from 'react'
import {View} from 'react-native'
import {LinearGradient} from 'expo-linear-gradient'
+import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {type ActiveConvoStates} from '#/state/messages/convo'
diff --git a/src/screens/Messages/components/InviteLinkDialog.tsx b/src/screens/Messages/components/InviteLinkDialog.tsx
index 6fcfed1d23..959dc4434c 100644
--- a/src/screens/Messages/components/InviteLinkDialog.tsx
+++ b/src/screens/Messages/components/InviteLinkDialog.tsx
@@ -1,13 +1,12 @@
import {useState} from 'react'
import {View} from 'react-native'
import {Image} from 'expo-image'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {chat} from '#/lexicons'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {shareUrl} from '#/lib/sharing'
import {useCreateJoinLink} from '#/state/queries/messages/create-join-link'
import {useDisableJoinLink} from '#/state/queries/messages/disable-join-link'
diff --git a/src/screens/Messages/components/MessageInputEmbed.tsx b/src/screens/Messages/components/MessageInputEmbed.tsx
index 469a463972..b711e15289 100644
--- a/src/screens/Messages/components/MessageInputEmbed.tsx
+++ b/src/screens/Messages/components/MessageInputEmbed.tsx
@@ -1,18 +1,17 @@
import {useCallback, useEffect, useMemo, useState} from 'react'
import {LayoutAnimation, View} from 'react-native'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
+import {moderatePost} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro'
import {type RouteProp, useNavigation, useRoute} from '@react-navigation/native'
import {AtUri} from '@atproto/syntax'
import {HITSLOP_20} from '#/lib/constants'
-import {moderatePost} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links'
import {
type CommonNavigatorParams,
type NavigationProp,
} from '#/lib/routes/types'
-import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {
convertBskyAppUrlIfNeeded,
getChatInviteCodeFromUrl,
@@ -168,7 +167,7 @@ function MessageInputPostEmbed({
return {
rt: new RichTextAPI({
text: post.record.text,
- facets: asSdkFacets(post.record.facets),
+ facets: post.record.facets,
}),
record: post.record,
}
diff --git a/src/screens/Messages/components/MessagesListInfoPanel.tsx b/src/screens/Messages/components/MessagesListInfoPanel.tsx
index 297ef8632f..e0e5545336 100644
--- a/src/screens/Messages/components/MessagesListInfoPanel.tsx
+++ b/src/screens/Messages/components/MessagesListInfoPanel.tsx
@@ -1,9 +1,9 @@
import {View} from 'react-native'
+import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {type NavigationProp} from '#/lib/routes/types'
import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
diff --git a/src/screens/PostThread/components/LikesStat.tsx b/src/screens/PostThread/components/LikesStat.tsx
index bd9bef3af7..f32d91c91c 100644
--- a/src/screens/PostThread/components/LikesStat.tsx
+++ b/src/screens/PostThread/components/LikesStat.tsx
@@ -1,9 +1,9 @@
import {View} from 'react-native'
+import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {AtUri} from '@atproto/syntax'
import {app} from '#/lexicons'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx
index 44cf8d5bb0..f4604f52ea 100644
--- a/src/screens/PostThread/components/ThreadItemAnchor.tsx
+++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx
@@ -10,7 +10,6 @@ import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
-import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {niceDate} from '#/lib/strings/time'
import {
POST_TOMBSTONE,
@@ -196,7 +195,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
() =>
new RichTextAPI({
text: record.text,
- facets: asSdkFacets(record.facets),
+ facets: record.facets,
}),
[record],
)
diff --git a/src/screens/PostThread/components/ThreadItemPost.tsx b/src/screens/PostThread/components/ThreadItemPost.tsx
index d2ace186c4..e599d33bcb 100644
--- a/src/screens/PostThread/components/ThreadItemPost.tsx
+++ b/src/screens/PostThread/components/ThreadItemPost.tsx
@@ -9,7 +9,6 @@ import {MAX_POST_LINES} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {makeProfileLink} from '#/lib/routes/links'
import {countLines} from '#/lib/strings/helpers'
-import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {
POST_TOMBSTONE,
type Shadow,
@@ -209,7 +208,7 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({
() =>
new RichTextAPI({
text: record.text,
- facets: asSdkFacets(record.facets),
+ facets: record.facets,
}),
[record],
)
diff --git a/src/screens/PostThread/components/ThreadItemTreePost.tsx b/src/screens/PostThread/components/ThreadItemTreePost.tsx
index 0b653037f0..1b23cc18fc 100644
--- a/src/screens/PostThread/components/ThreadItemTreePost.tsx
+++ b/src/screens/PostThread/components/ThreadItemTreePost.tsx
@@ -9,7 +9,6 @@ import {MAX_POST_LINES} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {makeProfileLink} from '#/lib/routes/links'
import {countLines} from '#/lib/strings/helpers'
-import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {
POST_TOMBSTONE,
type Shadow,
@@ -269,7 +268,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
() =>
new RichTextAPI({
text: record.text,
- facets: asSdkFacets(record.facets),
+ facets: record.facets,
}),
[record],
)
diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx
index 950d1d5cc1..b29cf824b1 100644
--- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx
+++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx
@@ -1,6 +1,6 @@
import {memo, useCallback, useMemo, useState} from 'react'
import {View} from 'react-native'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {msg, plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
@@ -10,7 +10,6 @@ import {Plural, Trans} from '@lingui/react/macro'
import {MAX_LABELERS} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics'
import {isAppLabeler} from '#/lib/moderation'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {type Shadow} from '#/state/cache/types'
import {useLabelerSubscriptionMutation} from '#/state/queries/labeler'
diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
index 7157c96dba..0f3eaab88f 100644
--- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx
+++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
@@ -1,6 +1,7 @@
import {memo, useMemo, useState} from 'react'
import {View} from 'react-native'
import {
+ moderateProfile,
type ModerationDecision,
type ModerationOpts,
} from '@bsky.app/sdk/moderation'
@@ -11,7 +12,6 @@ import {Trans} from '@lingui/react/macro'
import {app} from '#/lexicons'
import {useHaptics} from '#/lib/haptics'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {logger} from '#/logger'
import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
diff --git a/src/screens/Profile/Header/index.tsx b/src/screens/Profile/Header/index.tsx
index ef48ba3e65..29eb1092cc 100644
--- a/src/screens/Profile/Header/index.tsx
+++ b/src/screens/Profile/Header/index.tsx
@@ -7,11 +7,10 @@ import Animated, {
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {scheduleOnRN} from 'react-native-worklets'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {useIsFocused} from '@react-navigation/native'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
diff --git a/src/screens/ProfileList/components/Header.tsx b/src/screens/ProfileList/components/Header.tsx
index e2916feab7..39e2119012 100644
--- a/src/screens/ProfileList/components/Header.tsx
+++ b/src/screens/ProfileList/components/Header.tsx
@@ -8,7 +8,6 @@ import {Trans} from '@lingui/react/macro'
import {app} from '#/lexicons'
import {useHaptics} from '#/lib/haptics'
import {makeListLink} from '#/lib/routes/links'
-import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {logger} from '#/logger'
import {useListBlockMutation, useListMuteMutation} from '#/state/queries/list'
import {
@@ -40,8 +39,8 @@ export function Header({
const {_} = useLingui()
const ax = useAnalytics()
const {currentAccount} = useSession()
- const isCurateList = list.purpose === app.bsky.graph.defs.curatelist
- const isModList = list.purpose === app.bsky.graph.defs.modlist
+ const isCurateList = list.purpose === app.bsky.graph.defs.curatelist.value
+ const isModList = list.purpose === app.bsky.graph.defs.modlist.value
const isBlocking = !!list.viewer?.blocked
const isMuting = !!list.viewer?.muted
const playHaptic = useHaptics()
@@ -130,7 +129,7 @@ export function Header({
list.description
? new RichTextAPI({
text: list.description,
- facets: asSdkFacets(list.descriptionFacets),
+ facets: list.descriptionFacets,
})
: undefined,
[list],
diff --git a/src/screens/ProfileList/components/MoreOptionsMenu.tsx b/src/screens/ProfileList/components/MoreOptionsMenu.tsx
index f9e32a1c76..0394475cce 100644
--- a/src/screens/ProfileList/components/MoreOptionsMenu.tsx
+++ b/src/screens/ProfileList/components/MoreOptionsMenu.tsx
@@ -58,8 +58,8 @@ export function MoreOptionsMenu({
const {mutateAsync: muteList} = useListMuteMutation()
const {mutateAsync: blockList} = useListBlockMutation()
- const isCurateList = list.purpose === app.bsky.graph.defs.curatelist
- const isModList = list.purpose === app.bsky.graph.defs.modlist
+ const isCurateList = list.purpose === app.bsky.graph.defs.curatelist.value
+ const isModList = list.purpose === app.bsky.graph.defs.modlist.value
const isBlocking = !!list.viewer?.blocked
const isMuting = !!list.viewer?.muted
const isPinned = Boolean(savedFeedConfig?.pinned)
diff --git a/src/screens/ProfileList/index.tsx b/src/screens/ProfileList/index.tsx
index 0d261c2edf..de361fb57d 100644
--- a/src/screens/ProfileList/index.tsx
+++ b/src/screens/ProfileList/index.tsx
@@ -1,7 +1,7 @@
import {useCallback, useMemo, useRef, useState} from 'react'
import {View} from 'react-native'
import {useAnimatedRef} from 'react-native-reanimated'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateUserList, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
@@ -12,7 +12,6 @@ import {AtUri} from '@atproto/syntax'
import {app} from '#/lexicons'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useSetTitle} from '#/lib/hooks/useSetTitle'
-import {moderateUserList} from '#/lib/moderation/subjects'
import {
type CommonNavigatorParams,
type NativeStackScreenProps,
@@ -156,7 +155,7 @@ function ProfileListScreenLoaded({
const {rkey} = route.params
const feedSectionRef = useRef(null)
const aboutSectionRef = useRef(null)
- const isCurateList = list.purpose === app.bsky.graph.defs.curatelist
+ const isCurateList = list.purpose === app.bsky.graph.defs.curatelist.value
const isScreenFocused = useIsFocused()
const isHidden = list.labels?.findIndex(l => l.val === '!hide') !== -1
const isOwner = currentAccount?.did === list.creator.did
diff --git a/src/screens/Search/components/SearchHistory.tsx b/src/screens/Search/components/SearchHistory.tsx
index b71f5ef312..4a23cf4956 100644
--- a/src/screens/Search/components/SearchHistory.tsx
+++ b/src/screens/Search/components/SearchHistory.tsx
@@ -1,9 +1,8 @@
import {ScrollView, View} from 'react-native'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {createHitslop} from '#/lib/constants'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
diff --git a/src/screens/Search/components/StarterPackCard.tsx b/src/screens/Search/components/StarterPackCard.tsx
index 07687d001c..6eea0fb134 100644
--- a/src/screens/Search/components/StarterPackCard.tsx
+++ b/src/screens/Search/components/StarterPackCard.tsx
@@ -1,11 +1,11 @@
import {useState} from 'react'
import {View} from 'react-native'
+import {moderateProfile} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {app} from '#/lexicons'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSession} from '#/state/session'
diff --git a/src/screens/Search/modules/ExploreTrendingTopics.tsx b/src/screens/Search/modules/ExploreTrendingTopics.tsx
index 6ebc5c55d0..cca16fe685 100644
--- a/src/screens/Search/modules/ExploreTrendingTopics.tsx
+++ b/src/screens/Search/modules/ExploreTrendingTopics.tsx
@@ -2,10 +2,10 @@ import {useMemo} from 'react'
import {Pressable, View} from 'react-native'
import {Image} from 'expo-image'
import {RichText as RichTextApi} from '@bsky.app/sdk/richtext'
+import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {app} from '#/lexicons'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {
useTrendingSettings,
diff --git a/src/screens/Settings/AutomationLabelSettings.tsx b/src/screens/Settings/AutomationLabelSettings.tsx
index 0504bd438f..65ce732a0e 100644
--- a/src/screens/Settings/AutomationLabelSettings.tsx
+++ b/src/screens/Settings/AutomationLabelSettings.tsx
@@ -79,8 +79,8 @@ export function AutomationLabelSettingsScreen({}: Props) {
return existing
},
- checkCommitted: res => {
- const exists = !!res.data.labels?.some(l => l.val === 'bot')
+ checkCommitted: profile => {
+ const exists = !!profile.labels?.some(l => l.val === 'bot')
return exists === wasAdded
},
},
diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx
index c4f726050e..b2b9c878c6 100644
--- a/src/screens/Settings/Settings.tsx
+++ b/src/screens/Settings/Settings.tsx
@@ -2,6 +2,7 @@ import {useState} from 'react'
import {Alert, LayoutAnimation, Linking, Pressable, View} from 'react-native'
import {useReducedMotion} from 'react-native-reanimated'
import {removeNuxs} from '@bsky.app/sdk'
+import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
@@ -10,7 +11,6 @@ import {app} from '#/lexicons'
import {HELP_DESK_URL, HITSLOP_10} from '#/lib/constants'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {
type CommonNavigatorParams,
type NavigationProp,
diff --git a/src/screens/Settings/components/PwiOptOut.tsx b/src/screens/Settings/components/PwiOptOut.tsx
index 96ac34beed..7229622ad0 100644
--- a/src/screens/Settings/components/PwiOptOut.tsx
+++ b/src/screens/Settings/components/PwiOptOut.tsx
@@ -69,8 +69,8 @@ export function PwiOptOut() {
return existing
},
- checkCommitted: res => {
- const exists = !!res.data.labels?.some(
+ checkCommitted: profile => {
+ const exists = !!profile.labels?.some(
l => l.val === '!no-unauthenticated',
)
return exists === wasAdded
diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx
index 7bd270cc5b..9ecb2095a4 100644
--- a/src/screens/StarterPack/StarterPackScreen.tsx
+++ b/src/screens/StarterPack/StarterPackScreen.tsx
@@ -21,7 +21,6 @@ import {
type NavigationProp,
} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors'
-import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
import {logger} from '#/logger'
import {updateProfileShadow} from '#/state/cache/profile-shadow'
@@ -413,7 +412,7 @@ function Header({
const richText = record.description
? new RichTextAPI({
text: record.description,
- facets: asSdkFacets(record.descriptionFacets),
+ facets: record.descriptionFacets,
})
: undefined
diff --git a/src/screens/Takendown.tsx b/src/screens/Takendown.tsx
index 1f654a0f22..9843e9337d 100644
--- a/src/screens/Takendown.tsx
+++ b/src/screens/Takendown.tsx
@@ -54,7 +54,7 @@ export function Takendown() {
await client.call(
com.atproto.moderation.createReport,
{
- reasonType: tools.ozone.report.defs.reasonAppeal,
+ reasonType: tools.ozone.report.defs.reasonAppeal.value,
subject: {
$type: 'com.atproto.admin.defs#repoRef',
// the persisted account did is already resolved
diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx
index 7526d32409..1891eb2912 100644
--- a/src/screens/VideoFeed/index.tsx
+++ b/src/screens/VideoFeed/index.tsx
@@ -54,7 +54,6 @@ import {
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {cleanError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles'
-import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {logger} from '#/logger'
import {useA11y} from '#/state/a11y'
import {
@@ -831,7 +830,7 @@ function Overlay({
: undefined
const richText = new RichTextAPI({
text: record?.text || '',
- facets: asSdkFacets(record?.facets),
+ facets: record?.facets,
})
const handle = sanitizeHandle(post.author.handle, '@')
diff --git a/src/state/cache/post-shadow.ts b/src/state/cache/post-shadow.ts
index 77ef7e5811..59b60a79f4 100644
--- a/src/state/cache/post-shadow.ts
+++ b/src/state/cache/post-shadow.ts
@@ -1,3 +1,4 @@
+import {type AtUriString} from '@atproto/syntax'
import {useEffect, useMemo, useState} from 'react'
import {type QueryClient} from '@tanstack/react-query'
import {EventEmitter} from 'eventemitter3'
@@ -15,9 +16,13 @@ import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/stat
import {castAsShadow, type Shadow} from './types'
export type {Shadow} from './types'
+/**
+ * `'pending'` is the optimistic sentinel written before the real record uri
+ * comes back, so the two uri slots admit it alongside a real at-uri.
+ */
export interface PostShadow {
- likeUri: string | undefined
- repostUri: string | undefined
+ likeUri: AtUriString | 'pending' | undefined
+ repostUri: AtUriString | 'pending' | undefined
isDeleted: boolean
embed:
| app.bsky.embed.record.View
@@ -130,7 +135,12 @@ function mergeShadow(
(bsky.isType(app.bsky.embed.recordWithMedia.view, post.embed) &&
bsky.isType(app.bsky.embed.recordWithMedia.view, shadow.embed))
) {
- embed = shadow.embed
+ /*
+ * `isType` asserts a present, matching `$type` at runtime but narrows to
+ * the schema's input type, whose `$type` is optional - so the value does
+ * not satisfy the `$Typed` arm of `PostView['embed']` without this cast.
+ */
+ embed = shadow.embed as typeof post.embed
}
}
@@ -143,8 +153,15 @@ function mergeShadow(
bookmarkCount: bookmarkCount,
viewer: {
...(post.viewer || {}),
- like: 'likeUri' in shadow ? shadow.likeUri : post.viewer?.like,
- repost: 'repostUri' in shadow ? shadow.repostUri : post.viewer?.repost,
+ /*
+ * The optimistic `'pending'` sentinel is not a real at-uri; consumers only
+ * test these for presence while the write is in flight.
+ */
+ like: (('likeUri' in shadow ? shadow.likeUri : post.viewer?.like) ??
+ undefined) as AtUriString | undefined,
+ repost: (('repostUri' in shadow
+ ? shadow.repostUri
+ : post.viewer?.repost) ?? undefined) as AtUriString | undefined,
pinned: 'pinned' in shadow ? shadow.pinned : post.viewer?.pinned,
bookmarked:
'bookmarked' in shadow ? shadow.bookmarked : post.viewer?.bookmarked,
diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts
index 5618f96dcb..9e54c9c6a6 100644
--- a/src/state/messages/convo/agent.ts
+++ b/src/state/messages/convo/agent.ts
@@ -1,3 +1,4 @@
+import {type DidString, toDatetimeString} from '@atproto/syntax'
import {type Client, XrpcResponseError, type $Typed} from '@atproto/lex'
import {EventEmitter} from 'eventemitter3'
import {nanoid} from 'nanoid/non-secure'
@@ -97,7 +98,7 @@ export class Convo {
private chatClient: Client
private events: MessagesEventBus
- private senderUserDid: string
+ private senderUserDid: DidString
private status: ConvoStatus = ConvoStatus.Uninitialized
private error: ConvoError | undefined
@@ -1420,7 +1421,7 @@ export class Convo {
$type: 'chat.bsky.convo.defs#messageView',
id: nanoid(),
rev: '__fake__',
- sentAt: new Date().toISOString(),
+ sentAt: toDatetimeString(new Date()),
sender: {
$type: 'chat.bsky.convo.defs#messageViewSender',
did: this.senderUserDid,
@@ -1462,10 +1463,10 @@ export class Convo {
* @param emoji - must be one grapheme
*/
async addReaction(messageId: string, emoji: string) {
- const optimisticReaction = {
+ const optimisticReaction: chat.bsky.convo.defs.ReactionView = {
value: emoji,
sender: {did: this.senderUserDid},
- createdAt: new Date().toISOString(),
+ createdAt: toDatetimeString(new Date()),
}
let restore: null | (() => void) = null
if (this.pastMessages.has(messageId)) {
diff --git a/src/state/queries/actor-autocomplete.ts b/src/state/queries/actor-autocomplete.ts
index e764242097..cdb2584803 100644
--- a/src/state/queries/actor-autocomplete.ts
+++ b/src/state/queries/actor-autocomplete.ts
@@ -1,9 +1,8 @@
import {useCallback} from 'react'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {keepPreviousData, useQuery, useQueryClient} from '@tanstack/react-query'
import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {useAppviewClient} from '#/state/session'
diff --git a/src/state/queries/explore-feed-previews.tsx b/src/state/queries/explore-feed-previews.tsx
index a2d755c162..4b91477491 100644
--- a/src/state/queries/explore-feed-previews.tsx
+++ b/src/state/queries/explore-feed-previews.tsx
@@ -1,5 +1,6 @@
import {useMemo, useRef} from 'react'
import {type AtUriString, AtUri} from '@atproto/syntax'
+import {moderatePost} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {
@@ -13,7 +14,6 @@ import {app} from '#/lexicons'
import {CustomFeedAPI} from '#/lib/api/feed/custom'
import {aggregateUserInterests} from '#/lib/api/feed/utils'
import {FeedTuner} from '#/lib/api/feed-manip'
-import {moderatePost} from '#/lib/moderation/subjects'
import {cleanError} from '#/lib/strings/errors'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {
diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts
index c2ddb14660..f6ca1b6f26 100644
--- a/src/state/queries/feed.ts
+++ b/src/state/queries/feed.ts
@@ -1,6 +1,7 @@
import {useCallback, useEffect, useMemo, useRef} from 'react'
import {type AtUriString, AtUri} from '@atproto/syntax'
import {RichText} from '@bsky.app/sdk/richtext'
+import {moderateFeedGenerator} from '@bsky.app/sdk/moderation'
import {t} from '@lingui/core/macro'
import {
type InfiniteData,
@@ -13,10 +14,8 @@ import {
} from '@tanstack/react-query'
import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants'
-import {moderateFeedGenerator} from '#/lib/moderation/subjects'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
-import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {GCTIME, STALE} from '#/state/queries'
import {RQKEY as listQueryKey} from '#/state/queries/list'
import {usePreferencesQuery} from '#/state/queries/preferences'
@@ -99,7 +98,7 @@ export function hydrateFeedGenerator(
const description = new RichText({
text: view.description || '',
- facets: asSdkFacets((view.descriptionFacets || [])?.slice()),
+ facets: (view.descriptionFacets || [])?.slice(),
})
if (!view.descriptionFacets) {
@@ -142,7 +141,7 @@ export function hydrateList(
const description = new RichText({
text: view.description || '',
- facets: asSdkFacets((view.descriptionFacets || [])?.slice()),
+ facets: (view.descriptionFacets || [])?.slice(),
})
if (!view.descriptionFacets) {
diff --git a/src/state/queries/join-links.ts b/src/state/queries/join-links.ts
index 9fba112ae3..41bf754122 100644
--- a/src/state/queries/join-links.ts
+++ b/src/state/queries/join-links.ts
@@ -1,5 +1,6 @@
+import {toDatetimeString} from '@atproto/syntax'
import {useCallback} from 'react'
-import {type Client, type $Typed} from '@atproto/lex'
+import {type $Typed, type Client, type Unknown$TypedObject} from '@atproto/lex'
import {type QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
import * as bsky from '#/types/bsky'
@@ -22,10 +23,10 @@ export type KnownChatInvitePreview =
| $Typed
/**
- * The full open-union shape, including the `{$type: string}` fallback for
- * future variants.
+ * The full open-union shape, including lex's `Unknown$TypedObject` fallback for
+ * unrecognized future variants.
*/
-export type ChatInvitePreview = KnownChatInvitePreview | {$type: string}
+export type ChatInvitePreview = KnownChatInvitePreview | Unknown$TypedObject
/**
* Narrows a preview to one of the three known variants, filtering out the
@@ -112,7 +113,9 @@ export function setJoinLinkPreviewRequestedForCode(
...preview,
viewer: {
...preview.viewer,
- requestedAt: requested ? new Date().toISOString() : undefined,
+ requestedAt: requested
+ ? toDatetimeString(new Date())
+ : undefined,
},
}
}
diff --git a/src/state/queries/list-memberships.ts b/src/state/queries/list-memberships.ts
index f0fb0032cb..2bc62f3712 100644
--- a/src/state/queries/list-memberships.ts
+++ b/src/state/queries/list-memberships.ts
@@ -28,14 +28,14 @@ export function useListMembershipAddMutation({
* Needed for optimistic update of starter pack query
*/
subject?: bsky.profile.AnyProfileView
- onSuccess?: (data: {uri: string; cid: string}) => void
+ onSuccess?: (data: {uri: AtUriString; cid: string}) => void
onError?: (error: Error) => void
} = {}) {
const {currentAccount} = useSession()
const pdsClient = usePdsClient()
const queryClient = useQueryClient()
return useMutation<
- {uri: string; cid: string},
+ {uri: AtUriString; cid: string},
Error,
{listUri: string; actorDid: string}
>({
diff --git a/src/state/queries/messages/create-join-link.ts b/src/state/queries/messages/create-join-link.ts
index 36d0164c4e..8cda148502 100644
--- a/src/state/queries/messages/create-join-link.ts
+++ b/src/state/queries/messages/create-join-link.ts
@@ -1,3 +1,4 @@
+import {toDatetimeString} from '@atproto/syntax'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import * as bsky from '#/types/bsky'
@@ -52,7 +53,7 @@ export function useCreateJoinLink(
enabledStatus: 'enabled',
joinRule,
requireApproval,
- createdAt: new Date().toISOString(),
+ createdAt: toDatetimeString(new Date()),
},
},
}
diff --git a/src/state/queries/messages/list-conversations.tsx b/src/state/queries/messages/list-conversations.tsx
index 44e699b5f7..82e9a37c09 100644
--- a/src/state/queries/messages/list-conversations.tsx
+++ b/src/state/queries/messages/list-conversations.tsx
@@ -742,11 +742,18 @@ export function ListConvosProviderInner({
return {
...convo,
members: [...convo.members, ...relatedProfilesSansMembers],
+ /*
+ * `logAddReaction.message` is the wider message union (it can
+ * carry a deleted message too), while the reaction view's
+ * `message` slot is only the live `messageView` - the server
+ * never pairs a reaction with a deleted message, so the arm is
+ * asserted rather than re-narrowed.
+ */
lastReaction: {
$type: 'chat.bsky.convo.defs#messageAndReactionView',
reaction: log.reaction,
message: log.message,
- },
+ } as chat.bsky.convo.defs.ConvoView['lastReaction'],
rev: log.rev,
}
}),
diff --git a/src/state/queries/notifications/__tests__/util.test.ts b/src/state/queries/notifications/__tests__/util.test.ts
index 48eda7e411..f0a6f3aeba 100644
--- a/src/state/queries/notifications/__tests__/util.test.ts
+++ b/src/state/queries/notifications/__tests__/util.test.ts
@@ -1,14 +1,19 @@
+import {type DidString} from '@atproto/syntax'
import {describe, expect, it, jest} from '@jest/globals'
-import {groupNotifications} from '../util'
import {app} from '#/lexicons'
+import {type Notification} from '../types'
+import {groupNotifications} from '../util'
jest.mock('#/state/queries/profile', () => ({precacheProfile: jest.fn()}))
-type Notification = app.bsky.notification.listNotifications.Notification
-
+/*
+ * Fixture builder. The generated view brands `did`/`uri`/`cid`/`indexedAt`, and
+ * these are plain test strings, so the whole literal is asserted once - the
+ * grouping logic under test only compares them as strings.
+ */
function makeFollowNotification(
- did: string,
+ did: DidString,
starterPackUri?: string,
): Notification {
return {
@@ -31,7 +36,7 @@ function makeFollowNotification(
: undefined,
isRead: false,
indexedAt: '2026-07-28T12:00:00.000Z',
- }
+ } as Notification
}
describe('groupNotifications', () => {
diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts
index 6f185d644c..7c850c5b50 100644
--- a/src/state/queries/notifications/feed.ts
+++ b/src/state/queries/notifications/feed.ts
@@ -26,8 +26,8 @@ import {
} from '@tanstack/react-query'
import {AtUri} from '@atproto/syntax'
+import {moderatePost} from '@bsky.app/sdk/moderation'
import {app} from '#/lexicons'
-import {moderatePost} from '#/lib/moderation/subjects'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {STALE} from '#/state/queries'
import {useAppviewClient} from '#/state/session'
diff --git a/src/state/queries/notifications/types.ts b/src/state/queries/notifications/types.ts
index 703d4d1088..6fab4cbe32 100644
--- a/src/state/queries/notifications/types.ts
+++ b/src/state/queries/notifications/types.ts
@@ -1,5 +1,16 @@
import {app} from '#/lexicons'
+/**
+ * A `listNotifications` notification, widened with the `starterPack` the server
+ * attaches to a `follow` notification sourced from a starter pack. The pinned
+ * lexicon does not declare the field yet, so it is added here rather than read
+ * off an `unknown`. Drop this alias once the generated type carries it.
+ */
+export type Notification =
+ app.bsky.notification.listNotifications.Notification & {
+ starterPack?: app.bsky.graph.defs.StarterPackViewBasic
+ }
+
export type NotificationType =
| StarterPackNotificationType
| OtherNotificationType
@@ -50,8 +61,8 @@ type OtherNotificationType =
type FeedNotificationBase = {
_reactKey: string
- notification: app.bsky.notification.listNotifications.Notification
- additional?: app.bsky.notification.listNotifications.Notification[]
+ notification: Notification
+ additional?: Notification[]
subjectUri?: string
subject?:
| app.bsky.feed.defs.PostView
diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts
index db6882b384..83a028423b 100644
--- a/src/state/queries/notifications/util.ts
+++ b/src/state/queries/notifications/util.ts
@@ -1,17 +1,21 @@
import {type Client} from '@atproto/lex'
import {type AtUriString} from '@atproto/syntax'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {
+ hasMutedWord,
+ moderateNotification,
+ type ModerationOpts,
+} from '@bsky.app/sdk/moderation'
import {type QueryClient} from '@tanstack/react-query'
import chunk from 'lodash.chunk'
import {labelIsHideableOffense} from '#/lib/moderation'
-import {hasMutedWord, moderateNotification} from '#/lib/moderation/subjects'
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
import {precacheProfile} from '../profile'
import {
type FeedNotification,
type FeedPage,
+ type Notification,
type NotificationType,
} from './types'
@@ -108,7 +112,7 @@ export async function fetchPage({
// =
export function shouldFilterNotif(
- notif: app.bsky.notification.listNotifications.Notification,
+ notif: Notification,
moderationOpts: ModerationOpts | undefined,
): boolean {
const containsImperative = !!notif.author.labels?.some(labelIsHideableOffense)
@@ -138,9 +142,7 @@ export function shouldFilterNotif(
return moderateNotification(notif, moderationOpts).ui('contentList').filter
}
-export function groupNotifications(
- notifs: app.bsky.notification.listNotifications.Notification[],
-): FeedNotification[] {
+export function groupNotifications(notifs: Notification[]): FeedNotification[] {
const groupedNotifs: FeedNotification[] = []
for (const notif of notifs) {
const ts = +new Date(notif.indexedAt)
@@ -249,9 +251,7 @@ async function fetchSubjects(
}
}
-function toKnownType(
- notif: app.bsky.notification.listNotifications.Notification,
-): NotificationType {
+function toKnownType(notif: Notification): NotificationType {
if (notif.reason === 'like') {
if (notif.reasonSubject?.includes('feed.generator')) {
return 'feedgen-like'
@@ -279,7 +279,7 @@ function toKnownType(
function getSubjectUri(
type: NotificationType,
- notif: app.bsky.notification.listNotifications.Notification,
+ notif: Notification,
): string | undefined {
if (
type === 'reply' ||
diff --git a/src/state/queries/pinned-post.ts b/src/state/queries/pinned-post.ts
index cd3d0cb4a3..e53cefcbfe 100644
--- a/src/state/queries/pinned-post.ts
+++ b/src/state/queries/pinned-post.ts
@@ -55,10 +55,10 @@ export function usePinnedPostMutation() {
: undefined
return existing
},
- checkCommitted: res =>
+ checkCommitted: profile =>
pinCurrentPost
- ? res.data.pinnedPost?.uri === postUri
- : !res.data.pinnedPost,
+ ? profile.pinnedPost?.uri === postUri
+ : !profile.pinnedPost,
})
if (pinCurrentPost) {
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index 1639a45611..87b1ec43e5 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -3,6 +3,7 @@ import {AppState} from 'react-native'
import {type Client} from '@atproto/lex'
import {type AtIdentifierString, type AtUriString, AtUri} from '@atproto/syntax'
import {
+ moderatePost,
type ModerationDecision,
type ModerationPrefs,
} from '@bsky.app/sdk/moderation'
@@ -28,7 +29,6 @@ import {type FeedAPI, type ReasonFeedSource} from '#/lib/api/feed/types'
import {aggregateUserInterests} from '#/lib/api/feed/utils'
import {FeedTuner, type FeedTunerFn} from '#/lib/api/feed-manip'
import {DISCOVER_FEED_URI} from '#/lib/constants'
-import {moderatePost} from '#/lib/moderation/subjects'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
@@ -78,7 +78,7 @@ export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) {
export interface FeedPostSliceItem {
_reactKey: string
- uri: string
+ uri: AtUriString
post: app.bsky.feed.defs.PostView
record: app.bsky.feed.post.Main
moderation: ModerationDecision
diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts
index 2cd575c0e4..3b1b2b12a1 100644
--- a/src/state/queries/post.ts
+++ b/src/state/queries/post.ts
@@ -190,7 +190,7 @@ function usePostLikeMutation(
const pdsClient = usePdsClient()
const ax = useAnalytics()
return useMutation<
- {uri: string}, // responds with the uri of the like
+ {uri: AtUriString}, // responds with the uri of the like
Error,
{uri: string; cid: string; via?: {uri: string; cid: string}} // the post's uri and cid, and the repost uri/cid if present
>({
@@ -319,7 +319,7 @@ function usePostRepostMutation(
const pdsClient = usePdsClient()
const ax = useAnalytics()
return useMutation<
- {uri: string}, // responds with the uri of the repost
+ {uri: AtUriString}, // responds with the uri of the repost
Error,
{uri: string; cid: string; via?: {uri: string; cid: string}} // the post's uri and cid, and the repost uri/cid if present
>({
diff --git a/src/state/queries/postgate/index.ts b/src/state/queries/postgate/index.ts
index 889d692c5e..08dff1a45c 100644
--- a/src/state/queries/postgate/index.ts
+++ b/src/state/queries/postgate/index.ts
@@ -1,6 +1,6 @@
import {useRef} from 'react'
import {type Client} from '@atproto/lex'
-import {AtUri, type HandleString} from '@atproto/syntax'
+import {AtUri, type AtUriString, type HandleString} from '@atproto/syntax'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {networkRetry, retry} from '#/lib/async/retry'
@@ -177,7 +177,7 @@ export function useToggleQuoteDetachmentMutation() {
action,
}: {
post: app.bsky.feed.defs.PostView
- quoteUri: string
+ quoteUri: AtUriString
action: 'detach' | 'reattach'
}) => {
// cache here since post shadow mutates original object
diff --git a/src/state/queries/postgate/util.ts b/src/state/queries/postgate/util.ts
index b4c538e354..afbb41428c 100644
--- a/src/state/queries/postgate/util.ts
+++ b/src/state/queries/postgate/util.ts
@@ -58,7 +58,7 @@ export function mergePostgateRecords(
export function createEmbedViewDetachedRecord({
uri,
}: {
- uri: string
+ uri: AtUriString
}): $Typed {
const record: $Typed = {
$type: 'app.bsky.embed.record#viewDetached',
@@ -86,7 +86,7 @@ export function createMaybeDetachedQuoteEmbed({
| {
post: app.bsky.feed.defs.PostView
quote: undefined
- quoteUri: string
+ quoteUri: AtUriString
detached: true
}):
| app.bsky.embed.record.View
diff --git a/src/state/queries/profile-feedgens.ts b/src/state/queries/profile-feedgens.ts
index 7a78c0a6c6..89303554e6 100644
--- a/src/state/queries/profile-feedgens.ts
+++ b/src/state/queries/profile-feedgens.ts
@@ -5,7 +5,7 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
-import {moderateFeedGenerator} from '#/lib/moderation/subjects'
+import {moderateFeedGenerator} from '@bsky.app/sdk/moderation'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
import {useModerationOpts} from '../preferences/moderation-opts'
diff --git a/src/state/queries/profile-lists.ts b/src/state/queries/profile-lists.ts
index 3d795def0d..d6437c3e5b 100644
--- a/src/state/queries/profile-lists.ts
+++ b/src/state/queries/profile-lists.ts
@@ -5,7 +5,7 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
-import {moderateUserList} from '#/lib/moderation/subjects'
+import {moderateUserList} from '@bsky.app/sdk/moderation'
import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons'
import {useModerationOpts} from '../preferences/moderation-opts'
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index ee1b8b9c76..e5e66fc22e 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -146,7 +146,7 @@ interface ProfileUpdateParams {
) => Un$Typed)
newUserAvatar?: ImageMeta | undefined | null
newUserBanner?: ImageMeta | undefined | null
- checkCommitted?: (res: app.bsky.actor.getProfile.Response) => boolean
+ checkCommitted?: (profile: app.bsky.actor.getProfile.$OutputBody) => boolean
}
export function useProfileUpdateMutation() {
const queryClient = useQueryClient()
@@ -206,21 +206,21 @@ export function useProfileUpdateMutation() {
appviewClient,
profile.did,
checkCommitted ||
- (res => {
+ (profile => {
if (typeof newUserAvatar !== 'undefined') {
- if (newUserAvatar === null && res.data.avatar) {
+ if (newUserAvatar === null && profile.avatar) {
// url hasn't cleared yet
return false
- } else if (res.data.avatar === profile.avatar) {
+ } else if (profile.avatar === profile.avatar) {
// url hasn't changed yet
return false
}
}
if (typeof newUserBanner !== 'undefined') {
- if (newUserBanner === null && res.data.banner) {
+ if (newUserBanner === null && profile.banner) {
// url hasn't cleared yet
return false
- } else if (res.data.banner === profile.banner) {
+ } else if (profile.banner === profile.banner) {
// url hasn't changed yet
return false
}
@@ -229,8 +229,8 @@ export function useProfileUpdateMutation() {
return true
}
return (
- res.data.displayName === updates.displayName &&
- res.data.description === updates.description
+ profile.displayName === updates.displayName &&
+ profile.description === updates.description
)
}),
)
@@ -381,7 +381,7 @@ function useProfileFollowMutation(
const queryClient = useQueryClient()
const {captureAction} = useProgressGuideControls()
- return useMutation<{uri: string; cid: string}, Error, {did: string}>({
+ return useMutation<{uri: AtUriString; cid: string}, Error, {did: string}>({
mutationFn: async ({did}) => {
let ownProfile: app.bsky.actor.defs.ProfileViewDetailed | undefined
if (currentAccount) {
@@ -637,7 +637,7 @@ function useProfileBlockMutation() {
const {currentAccount} = useSession()
const pdsClient = usePdsClient()
const queryClient = useQueryClient()
- return useMutation<{uri: string; cid: string}, Error, {did: string}>({
+ return useMutation<{uri: AtUriString; cid: string}, Error, {did: string}>({
mutationFn: async ({did}) => {
if (!currentAccount) {
throw new Error('Not signed in')
@@ -679,23 +679,16 @@ function useProfileUnblockMutation() {
async function whenAppViewReady(
client: Client,
actor: string,
- fn: (res: app.bsky.actor.getProfile.Response) => boolean,
+ fn: (profile: app.bsky.actor.getProfile.$OutputBody) => boolean,
) {
await until(
5, // 5 tries
1e3, // 1s delay between tries
fn,
- /*
- * `checkCommitted` callbacks are still written against the legacy
- * `Response` envelope, so wrap the lex output until those consumers move.
- */
- async () => ({
- success: true,
- headers: {},
- data: await client.call(app.bsky.actor.getProfile, {
+ async () =>
+ await client.call(app.bsky.actor.getProfile, {
actor: actor as AtIdentifierString,
}),
- }),
)
}
diff --git a/src/state/queries/search-posts-params.ts b/src/state/queries/search-posts-params.ts
index c1991f919a..6284022a4e 100644
--- a/src/state/queries/search-posts-params.ts
+++ b/src/state/queries/search-posts-params.ts
@@ -200,12 +200,31 @@ function mergeList(a?: string[], b?: string[]): string[] | undefined {
* operator. v2 renames v1's singular operators to plural arrays, and `lang` to
* `language`.
*/
+/**
+ * The filter subset of the `searchPostsV2` params, with the format-constrained
+ * strings (uri, at-identifier, language, datetime) left unbranded: every value
+ * here is parsed from user input, so the brand is asserted by the caller rather
+ * than re-validated here. `q`, `limit`, `cursor` and `sort` are the caller's.
+ */
+type SearchPostsV2FilterParams = {
+ [K in Exclude<
+ keyof app.bsky.feed.searchPostsV2.$Params,
+ 'query' | 'limit' | 'cursor' | 'sort'
+ >]?: app.bsky.feed.searchPostsV2.$Params[K] extends
+ | readonly (infer _E)[]
+ | undefined
+ ? string[]
+ : app.bsky.feed.searchPostsV2.$Params[K] extends string | undefined
+ ? string
+ : app.bsky.feed.searchPostsV2.$Params[K]
+}
+
export function buildSearchPostsV2Filters(
embedded: Omit,
filters?: SearchFilters,
-): app.bsky.feed.searchPostsV2.$Params {
+): SearchPostsV2FilterParams {
const apiFilters = filters ? filtersToApiParams(filters) : {}
- const params: app.bsky.feed.searchPostsV2.$Params = {}
+ const params: SearchPostsV2FilterParams = {}
const authors = mergeList(
embedded.author ? [embedded.author] : undefined,
diff --git a/src/state/queries/search-posts-v2.ts b/src/state/queries/search-posts-v2.ts
index 841f55a593..a1e941879f 100644
--- a/src/state/queries/search-posts-v2.ts
+++ b/src/state/queries/search-posts-v2.ts
@@ -7,7 +7,7 @@ import {
} from '@tanstack/react-query'
import {AtUri} from '@atproto/syntax'
-import {moderatePost} from '#/lib/moderation/subjects'
+import {moderatePost} from '@bsky.app/sdk/moderation'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useAppviewClient} from '#/state/session'
import {type SearchFilters} from '#/screens/Search/searchParams'
diff --git a/src/state/queries/usePostThread/views.ts b/src/state/queries/usePostThread/views.ts
index 973f89a125..7fc21161be 100644
--- a/src/state/queries/usePostThread/views.ts
+++ b/src/state/queries/usePostThread/views.ts
@@ -1,9 +1,8 @@
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderatePost, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {type $Typed} from '@atproto/lex'
import {AtUri} from '@atproto/syntax'
import {app} from '#/lexicons'
-import {moderatePost} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links'
import {
type ApiThreadItem,
diff --git a/src/types/bsky/__tests__/index.test.ts b/src/types/bsky/__tests__/index.test.ts
index b0b4466b2c..ce7c98acab 100644
--- a/src/types/bsky/__tests__/index.test.ts
+++ b/src/types/bsky/__tests__/index.test.ts
@@ -1,5 +1,3 @@
-import {AppBskyFeedPost} from '@atproto/api'
-
import {app} from '#/lexicons'
import * as bsky from '#/types/bsky'
@@ -167,42 +165,3 @@ describe('types/bsky lexicon schema helpers (#/lexicons)', () => {
})
})
})
-
-describe('types/bsky validator helpers (@atproto/api)', () => {
- it('dangerousIsType accepts the right $type without validating the body', () => {
- expect(
- bsky.dangerousIsType(
- validPost,
- AppBskyFeedPost.isRecord,
- ),
- ).toBe(true)
- // Right $type, invalid body - still passes the fast guard.
- expect(
- bsky.dangerousIsType(
- invalidPost,
- AppBskyFeedPost.isRecord,
- ),
- ).toBe(true)
- expect(
- bsky.dangerousIsType(
- wrongType,
- AppBskyFeedPost.isRecord,
- ),
- ).toBe(false)
- })
-
- it('validate fully validates the body', () => {
- expect(
- bsky.validate(
- validPost,
- AppBskyFeedPost.validateRecord,
- ),
- ).toBe(true)
- expect(
- bsky.validate(
- invalidPost,
- AppBskyFeedPost.validateRecord,
- ),
- ).toBe(false)
- })
-})
diff --git a/src/types/bsky/__tests__/post.test.ts b/src/types/bsky/__tests__/post.test.ts
index 6db1d23878..a19d9dda27 100644
--- a/src/types/bsky/__tests__/post.test.ts
+++ b/src/types/bsky/__tests__/post.test.ts
@@ -1,8 +1,3 @@
-import {
- type $Typed as $TypedApi,
- type AppBskyEmbedRecord,
- type AppBskyFeedDefs,
-} from '@atproto/api'
import {type $Typed} from '@atproto/lex'
import {type app} from '#/lexicons'
@@ -109,38 +104,25 @@ const starterPackViewBasic = {
const asEmbed = (v: unknown) => v as app.bsky.feed.defs.PostView['embed']
/*
- * Type-level assertions for the dual-world widening. These are compile-time
- * only: each widened arm must accept both the `#/lexicons` view and the
- * `@atproto/api` view, and `parseEmbed` must accept a `PostView.embed` from
- * either world, because both worlds have live producers.
+ * Type-level assertions for the embed union. Compile-time only: each arm must
+ * accept the `#/lexicons` view of its def, and `parseEmbed` must accept a
+ * `PostView.embed`.
*/
type Assignable = From extends To ? true : false
type Expect = T
-type _PostArmAcceptsNewWorld = Expect<
+type _PostArmAcceptsView = Expect<
Assignable<
{type: 'post'; view: $Typed},
EmbedType<'post'>
>
>
-type _PostArmAcceptsOldWorld = Expect<
- Assignable<
- {type: 'post'; view: $TypedApi},
- EmbedType<'post'>
- >
->
-type _ParseEmbedAcceptsNewWorld = Expect<
+type _ParseEmbedAcceptsPostViewEmbed = Expect<
Assignable<
app.bsky.feed.defs.PostView['embed'],
Parameters[0]
>
>
-type _ParseEmbedAcceptsOldWorld = Expect<
- Assignable<
- AppBskyFeedDefs.PostView['embed'],
- Parameters[0]
- >
->
type _ParseEmbedReturnsEmbed = Expect<
Assignable, Embed>
>
diff --git a/src/types/bsky/__tests__/starterPack.test.ts b/src/types/bsky/__tests__/starterPack.test.ts
index b973766f2e..161de2a867 100644
--- a/src/types/bsky/__tests__/starterPack.test.ts
+++ b/src/types/bsky/__tests__/starterPack.test.ts
@@ -1,5 +1,3 @@
-import {type AppBskyGraphDefs} from '@atproto/api'
-
import {type app} from '#/lexicons'
import {
type AnyStarterPackView,
@@ -30,25 +28,19 @@ const fullView = {
}
/*
- * Type-level assertions for the dual-world widening: the alias must accept a
- * starter pack view from either world, because both have live producers. These
- * are compile-time only - a failure surfaces as a typecheck error.
+ * Type-level assertions for the view alias: it must accept both the basic and
+ * the full starter pack view. Compile-time only - a failure surfaces as a
+ * typecheck error.
*/
type Assignable = From extends To ? true : false
type Expect = T
-type _AcceptsNewBasicView = Expect<
+type _AcceptsBasicView = Expect<
Assignable
>
-type _AcceptsNewFullView = Expect<
+type _AcceptsFullView = Expect<
Assignable
>
-type _AcceptsOldBasicView = Expect<
- Assignable
->
-type _AcceptsOldFullView = Expect<
- Assignable
->
describe('types/bsky/starterPack guards', () => {
describe('isBasicView', () => {
diff --git a/src/types/bsky/index.ts b/src/types/bsky/index.ts
index 1d498bc130..278c1c1079 100644
--- a/src/types/bsky/index.ts
+++ b/src/types/bsky/index.ts
@@ -10,71 +10,15 @@ import {
type ValidateOptions,
type ValidationResult,
} from '@atproto/lex'
-import {type ValidationResult as LegacyValidationResult} from '@atproto/lexicon'
export * as post from '#/types/bsky/post'
export * as profile from '#/types/bsky/profile'
export * as starterPack from '#/types/bsky/starterPack'
-/**
- * Fast type checking without full schema validation, for use with data we
- * trust, or for non-critical path use cases. Why? Our SDK's `is*` identity
- * utils do not assert the type of the entire object, only the `$type` string.
- *
- * For full validation of the object schema, use the `validate` export from
- * this file.
- *
- * Usage:
- * ```ts
- * import * as bsky from '#/types/bsky'
- *
- * if (bsky.dangerousIsType(item, AppBskyFeedPost.isRecord)) {
- * // `item` has type `$Typed` here
- * }
- * ```
- */
-export function dangerousIsType(
- record: unknown,
- identity: (v: V) => v is V & {$type: NonNullable},
-): record is R {
- return identity(record)
-}
-
-/**
- * Fully validates the object schema, which has a performance cost.
- *
- * For faster checks with data we trust, like that from our app view, use the
- * `dangerousIsType` export from this same file.
- *
- * Usage:
- * ```ts
- * import * as bsky from '#/types/bsky'
- *
- * if (bsky.validate(item, AppBskyFeedPost.validateRecord)) {
- * // `item` has type `$Typed` here
- * }
- * ```
- */
-export function validate(
- record: unknown,
- validator: (v: unknown) => LegacyValidationResult,
-): record is R {
- return validator(record).success
-}
-
/*
- * Two families of helpers live in this file, distinguished by the validation
- * surface they consume.
- *
- * `dangerousIsType` and `validate` (above) operate on the standalone
- * `is*`/`validate*` functions exported by '@atproto/api' - the record and the
- * validator are passed separately, e.g.
- * `dangerousIsType(v, AppBskyFeedPost.isRecord)`.
- *
- * `isType`, `matches`, `parse`, and `safeParse` (below) operate on the
- * generated lexicon schema objects from '#/lexicons'. That codegen attaches
- * the validation surface directly to each schema, so these helpers take the
- * schema object itself:
+ * These helpers operate on the generated lexicon schema objects from
+ * '#/lexicons'. The codegen attaches the validation surface directly to each
+ * schema, so these helpers take the schema object itself:
*
* ```ts
* import {app} from '#/lexicons'
@@ -104,9 +48,8 @@ type TypedSchema = RecordSchema | TypedObjectSchema
* string; it does NOT assert the rest of the object matches the schema. An
* invalid record with the right `$type` will pass.
*
- * This is the '#/lexicons' equivalent of {@link dangerousIsType}. For full
- * validation of the object schema, use {@link matches}, {@link parse}, or
- * {@link safeParse} from this same file.
+ * For full validation of the object schema, use {@link matches}, {@link parse},
+ * or {@link safeParse} from this same file.
*
* Usage:
* ```ts
@@ -126,10 +69,9 @@ export function isType(
* Deliberately NOT delegating to the schema's `isTypeOf`: the generated
* `TypedObjectSchema.isTypeOf` treats a MISSING `$type` as a match
* (maybe-typed semantics), which would let any plain object satisfy any
- * def-schema check and break `$type`-discriminated unions. The old
- * `dangerousIsType` required a present, matching `$type`, and so do we.
- * The nullish/object guard also mirrors the old `is$typed` behavior of
- * returning false (not throwing) for null/undefined input.
+ * def-schema check and break `$type`-discriminated unions. We require a
+ * present, matching `$type`. The nullish/object guard also returns false
+ * (not throws) for null/undefined input.
*/
return (
value != null &&
@@ -142,9 +84,8 @@ export function isType(
* Fully validates the object against the schema (strict, no coercion), which
* has a performance cost, and narrows the value on success.
*
- * This is the '#/lexicons' equivalent of {@link validate}. For faster checks
- * with data we trust, like that from our app view, use {@link isType} from this
- * same file.
+ * For faster checks with data we trust, like that from our app view, use
+ * {@link isType} from this same file.
*
* Usage:
* ```ts
@@ -166,7 +107,7 @@ export function matches(
/**
* Fully validates and parses the value against the schema, returning the typed
- * value or throwing a `LexValidationError` on failure. Parsing may apply
+ * value or throwing an `LexValidationError` on failure. Parsing may apply
* schema transformations such as default values.
*
* Prefer {@link safeParse} where you want to branch on failure without a
diff --git a/src/types/bsky/post.ts b/src/types/bsky/post.ts
index 2c8e3f4d87..ed0d8a9d83 100644
--- a/src/types/bsky/post.ts
+++ b/src/types/bsky/post.ts
@@ -1,110 +1,56 @@
-import {
- type $Typed as $TypedApi,
- type AppBskyEmbedExternal,
- type AppBskyEmbedGallery,
- type AppBskyEmbedImages,
- type AppBskyEmbedRecord,
- type AppBskyEmbedVideo,
- type AppBskyFeedDefs,
- type AppBskyGraphDefs,
- type AppBskyLabelerDefs,
-} from '@atproto/api'
import {type $Typed} from '@atproto/lex'
import {app} from '#/lexicons'
import {isType} from '#/types/bsky'
-/*
- * Each `view` slot below accepts both the generated `#/lexicons` view and the
- * `@atproto/api` view of the same def, because both worlds have live producers:
- * `parseEmbed` narrows with the `#/lexicons` schemas and so returns new-world
- * views, while call sites that build an `Embed` by hand still pass views
- * produced through the `@atproto/api` agent.
- *
- * The two worlds share the same `$type` strings, so the guards in this file
- * narrow a value from either producer; only the static type differs.
- *
- * TODO: remove the @atproto/api arms once all producers emit #/lexicons views
- */
export type Embed =
| {
type: 'post'
- view:
- | $Typed
- | $TypedApi
+ view: $Typed
}
| {
type: 'post_not_found'
- view:
- | $Typed
- | $TypedApi
+ view: $Typed
}
| {
type: 'post_blocked'
- view:
- | $Typed
- | $TypedApi
+ view: $Typed
}
| {
type: 'post_detached'
- view:
- | $Typed
- | $TypedApi
+ view: $Typed
}
| {
type: 'feed'
- view:
- | $Typed
- | $TypedApi
+ view: $Typed
}
| {
type: 'list'
- view:
- | $Typed
- | $TypedApi
+ view: $Typed
}
| {
type: 'labeler'
- view:
- | $Typed
- | $TypedApi
+ view: $Typed
}
| {
type: 'starter_pack'
- view:
- | $Typed
- | $TypedApi
+ view: $Typed
}
| {
type: 'images'
- /*
- * Only the `@atproto/api` view, unlike the other arms: the ImageEmbed
- * consumer reads `view.images` directly, and the `#/lexicons` view is
- * assignable to this slot, so `parseEmbed`'s new-world value flows in
- * while the consumer keeps a single structural shape to read from.
- */
- view: $TypedApi
+ view: $Typed
}
| {
type: 'gallery'
- /*
- * Old-world only for the same reason as the `images` arm above: the
- * consumer narrows `view.items` with `AppBskyEmbedGallery.isViewImage`,
- * which cannot narrow the new view's `Unknown$TypedObject` arm.
- */
- view: $TypedApi
+ view: $Typed
}
| {
type: 'link'
- view:
- | $Typed
- | $TypedApi
+ view: $Typed
}
| {
type: 'video'
- view:
- | $Typed
- | $TypedApi
+ view: $Typed
}
| {
type: 'post_with_media'
@@ -169,15 +115,7 @@ export function parseEmbedRecordView({
}
}
-export function parseEmbed(
- /*
- * Accepts a `PostView.embed` from either world; the `#/lexicons` guards below
- * narrow on `$type`, which is world-independent.
- */
- embed:
- | app.bsky.feed.defs.PostView['embed']
- | AppBskyFeedDefs.PostView['embed'],
-): Embed {
+export function parseEmbed(embed: app.bsky.feed.defs.PostView['embed']): Embed {
if (isType(app.bsky.embed.images.view, embed)) {
return {
type: 'images',
diff --git a/src/types/bsky/profile.ts b/src/types/bsky/profile.ts
index 4231a6d0fe..edf1aa1c08 100644
--- a/src/types/bsky/profile.ts
+++ b/src/types/bsky/profile.ts
@@ -1,23 +1,10 @@
-import {type AppBskyActorDefs, type ChatBskyActorDefs} from '@atproto/api'
-
import {type app, type chat} from '#/lexicons'
/**
- * Matches any profile view exported by our SDK, in either world.
- *
- * Both the generated `#/lexicons` views and the `@atproto/api` views are
- * accepted because both are live producers: queries migrated to the lexicon
- * client emit the former, unmigrated ones emit the latter, and consumers of
- * this alias take profiles from both.
- *
- * TODO: remove the @atproto/api arms once all producers emit #/lexicons views
+ * Matches any profile view exported by our SDK.
*/
export type AnyProfileView =
| app.bsky.actor.defs.ProfileViewBasic
| app.bsky.actor.defs.ProfileView
| app.bsky.actor.defs.ProfileViewDetailed
| chat.bsky.actor.defs.ProfileViewBasic
- | AppBskyActorDefs.ProfileViewBasic
- | AppBskyActorDefs.ProfileView
- | AppBskyActorDefs.ProfileViewDetailed
- | ChatBskyActorDefs.ProfileViewBasic
diff --git a/src/types/bsky/starterPack.ts b/src/types/bsky/starterPack.ts
index 23df0e4631..21cb4634df 100644
--- a/src/types/bsky/starterPack.ts
+++ b/src/types/bsky/starterPack.ts
@@ -1,23 +1,14 @@
-import {type AppBskyGraphDefs} from '@atproto/api'
-
import {app} from '#/lexicons'
/*
- * `$type`-only guards for starter pack views. They compare against the
- * `#/lexicons` schema's `$type` string rather than delegating to the schema's
- * `isTypeOf` (which treats a missing `$type` as a match), matching the
- * present-and-equal semantics of the old `@atproto/api`
- * `AppBskyGraphDefs.isStarterPackView*` helpers.
- *
- * The `$type` string is identical in both worlds, so a single check narrows a
- * value from either producer; the narrowed type is the union of both worlds'
- * views for the same reason {@link AnyStarterPackView} is.
+ * The generated `$type`-only guards. These match on a present, matching
+ * `$type` against the `#/lexicons` schema's `$type` string rather than
+ * delegating to the schema's `isTypeOf` (which treats a missing `$type` as a
+ * match).
*/
export function isBasicView(
v: unknown,
-): v is
- | app.bsky.graph.defs.StarterPackViewBasic
- | AppBskyGraphDefs.StarterPackViewBasic {
+): v is app.bsky.graph.defs.StarterPackViewBasic {
return (
v != null &&
typeof v === 'object' &&
@@ -26,9 +17,7 @@ export function isBasicView(
)
}
-export function isView(
- v: unknown,
-): v is app.bsky.graph.defs.StarterPackView | AppBskyGraphDefs.StarterPackView {
+export function isView(v: unknown): v is app.bsky.graph.defs.StarterPackView {
return (
v != null &&
typeof v === 'object' &&
@@ -37,16 +26,8 @@ export function isView(
}
/**
- * Matches any starter pack view exported by our SDK, in either world.
- *
- * Both the generated `#/lexicons` views and the `@atproto/api` views are
- * accepted because both are live producers: queries migrated to the lexicon
- * client emit the former, unmigrated ones emit the latter.
- *
- * TODO: remove the @atproto/api arms once all producers emit #/lexicons views
+ * Matches any starter pack view exported by our SDK.
*/
export type AnyStarterPackView =
| app.bsky.graph.defs.StarterPackViewBasic
| app.bsky.graph.defs.StarterPackView
- | AppBskyGraphDefs.StarterPackViewBasic
- | AppBskyGraphDefs.StarterPackView
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index c297d9fd98..1d4c7adbcf 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -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],
)
diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx
index 0b14f16ed3..edd0a7e0ad 100644
--- a/src/view/com/composer/ExternalEmbed.tsx
+++ b/src/view/com/composer/ExternalEmbed.tsx
@@ -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
/>
diff --git a/src/view/com/composer/drafts/state/api.ts b/src/view/com/composer/drafts/state/api.ts
index 40ab2cd6de..6f2aa48910 100644
--- a/src/view/com/composer/drafts/state/api.ts
+++ b/src/view/com/composer/drafts/state/api.ts
@@ -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)
diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts
index 45a097ff83..44c0bcda69 100644
--- a/src/view/com/composer/state/composer.ts
+++ b/src/view/com/composer/state/composer.ts
@@ -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,
}
}
}
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx
index 55c4d08c9e..0f47eefa37 100644
--- a/src/view/com/feeds/FeedPage.tsx
+++ b/src/view/com/feeds/FeedPage.tsx
@@ -77,7 +77,7 @@ export function FeedPage({
const isVideoFeed = useMemo(() => {
const isBskyVideoFeed = VIDEO_FEED_URIS.includes(feedInfo.uri)
const feedIsVideoMode =
- feedInfo.contentMode === app.bsky.feed.defs.contentModeVideo
+ feedInfo.contentMode === app.bsky.feed.defs.contentModeVideo.value
const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
return IS_NATIVE && _isVideoFeed
}, [feedInfo])
diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx
index e15fd71572..02753c031a 100644
--- a/src/view/com/notifications/NotificationFeedItem.tsx
+++ b/src/view/com/notifications/NotificationFeedItem.tsx
@@ -11,6 +11,7 @@ import {
import {TID} from '@atproto/common-web'
import {type DidString, AtUri} from '@atproto/syntax'
import {
+ moderateProfile,
type ModerationDecision,
type ModerationOpts,
} from '@bsky.app/sdk/moderation'
@@ -21,7 +22,6 @@ import {useQueryClient} from '@tanstack/react-query'
import {MAX_POST_LINES} from '#/lib/constants'
import {useAnimatedValue} from '#/lib/hooks/useAnimatedValue'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links'
import {type NavigationProp} from '#/lib/routes/types'
import {forceLTR} from '#/lib/strings/bidi'
diff --git a/src/view/com/post-thread/PostQuotes.tsx b/src/view/com/post-thread/PostQuotes.tsx
index f14219dbf7..44ca1f6d3c 100644
--- a/src/view/com/post-thread/PostQuotes.tsx
+++ b/src/view/com/post-thread/PostQuotes.tsx
@@ -1,12 +1,11 @@
import {useCallback, useState} from 'react'
-import {type ModerationDecision} from '@bsky.app/sdk/moderation'
+import {moderatePost, type ModerationDecision} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {app} from '#/lexicons'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
-import {moderatePost} from '#/lib/moderation/subjects'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 025a18409d..f2f64d67eb 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -1,6 +1,6 @@
import {useCallback, useMemo, useState} from 'react'
import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native'
-import {type ModerationDecision} from '@bsky.app/sdk/moderation'
+import {moderatePost, type ModerationDecision} from '@bsky.app/sdk/moderation'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {useQueryClient} from '@tanstack/react-query'
@@ -8,10 +8,8 @@ import {AtUri} from '@atproto/syntax'
import {app} from '#/lexicons'
import {MAX_POST_LINES} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
-import {moderatePost} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links'
import {countLines} from '#/lib/strings/helpers'
-import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {
POST_TOMBSTONE,
type Shadow,
@@ -64,7 +62,7 @@ export function Post({
record
? new RichTextAPI({
text: record.text,
- facets: asSdkFacets(record.facets),
+ facets: record.facets,
})
: undefined,
[record],
diff --git a/src/view/com/posts/PostFeedErrorMessage.tsx b/src/view/com/posts/PostFeedErrorMessage.tsx
index 135fcc7074..54c3e8f8c0 100644
--- a/src/view/com/posts/PostFeedErrorMessage.tsx
+++ b/src/view/com/posts/PostFeedErrorMessage.tsx
@@ -6,6 +6,7 @@ import {Trans} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
import {AtUri} from '@atproto/syntax'
+import {getErrorName, getErrorStatus} from '#/lib/xrpc-error'
import {app} from '#/lexicons'
import {usePalette} from '#/lib/hooks/usePalette'
import {type NavigationProp} from '#/lib/routes/types'
@@ -238,15 +239,21 @@ function detectKnownError(
if (!error) {
return undefined
}
+ /*
+ * Both names are declared by `app.bsky.feed.getAuthorFeed` AND
+ * `.getActorLikes`, and this helper takes an error from an arbitrary feed
+ * descriptor, so the source method is ambiguous - hence the untyped
+ * `getErrorName` check rather than `matchXrpcError`.
+ */
if (
- error instanceof app.bsky.feed.getAuthorFeed.BlockedActorError ||
- error instanceof app.bsky.feed.getAuthorFeed.BlockedByActorError
+ getErrorName(error) === 'BlockedActor' ||
+ getErrorName(error) === 'BlockedByActor'
) {
return KnownError.Block
}
// check status codes
- if (error?.status === 429) {
+ if (getErrorStatus(error) === 429) {
return KnownError.FeedTooManyRequests
}
diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx
index 43679fd39c..dac8652365 100644
--- a/src/view/com/posts/PostFeedItem.tsx
+++ b/src/view/com/posts/PostFeedItem.tsx
@@ -12,7 +12,6 @@ import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette'
import {makeProfileLink} from '#/lib/routes/links'
import {countLines} from '#/lib/strings/helpers'
-import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {
POST_TOMBSTONE,
type Shadow,
@@ -100,7 +99,7 @@ export function PostFeedItem({
() =>
new RichTextAPI({
text: record.text,
- facets: asSdkFacets(record.facets),
+ facets: record.facets,
}),
[record],
)
@@ -253,8 +252,13 @@ let FeedItemInner = ({
feedSourceInfo,
post: {
post,
+ /*
+ * `isType` requires a present `$type` at runtime but narrows to the
+ * schema's input type, whose `$type` is optional, so the `$Typed` arm
+ * of `FeedViewPost['reason']` needs the assertion back.
+ */
reason: bsky.isType(app.bsky.feed.defs.reasonRepost, reason)
- ? reason
+ ? (reason as app.bsky.feed.defs.FeedViewPost['reason'])
: undefined,
feedContext,
reqId,
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 4f7be3e0e2..9780461f4c 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -2,7 +2,7 @@ import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {StyleSheet} from 'react-native'
import {SafeAreaView} from 'react-native-safe-area-context'
import {ScrollForwarderView} from 'react-native-scroll-forwarder'
-import {type ModerationOpts} from '@bsky.app/sdk/moderation'
+import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
@@ -14,7 +14,6 @@ import {app} from '#/lexicons'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {useSetTitle} from '#/lib/hooks/useSetTitle'
-import {moderateProfile} from '#/lib/moderation/subjects'
import {
type CommonNavigatorParams,
type NativeStackScreenProps,