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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-08-04 04:53:44 +03:00
parent e9a67f2dea
commit ae0750099f
114 changed files with 591 additions and 899 deletions
+87 -116
View File
@@ -1,10 +1,7 @@
import type * as AgeRange from 'expo-age-range' import type * as AgeRange from 'expo-age-range'
import { import {toDatetimeString} from '@atproto/syntax'
ageAssuranceRuleIDs as ids,
type AppBskyAgeassuranceDefs,
type AppBskyAgeassuranceGetState,
} from '@atproto/api'
import {app} from '#/lexicons'
import {type OtherRequiredData} from '#/ageAssurance/data' import {type OtherRequiredData} from '#/ageAssurance/data'
import {IS_DEV, IS_E2E} from '#/env' import {IS_DEV, IS_E2E} from '#/env'
import {type Geolocation} from '#/geolocation' import {type Geolocation} from '#/geolocation'
@@ -31,33 +28,38 @@ export const otherRequiredData: OtherRequiredData = {
} }
const serverStateEnabled = false || IS_E2E const serverStateEnabled = false || IS_E2E
export const serverState: AppBskyAgeassuranceGetState.OutputSchema | undefined = export const serverState:
serverStateEnabled | app.bsky.ageassurance.getState.$OutputBody
? { | undefined = serverStateEnabled
state: { ? {
lastInitiatedAt: undefined, // new Date(2025, 1, 1).toISOString(), state: {
status: 'unknown', lastInitiatedAt: undefined, // new Date(2025, 1, 1).toISOString(),
access: 'unknown', status: 'unknown',
}, access: 'unknown',
metadata: { },
accountCreatedAt: new Date(2023, 1, 1).toISOString(), metadata: {
}, accountCreatedAt: toDatetimeString(new Date(2023, 1, 1)),
} },
: undefined }
: undefined
export const config: AppBskyAgeassuranceDefs.Config = { export const config: app.bsky.ageassurance.defs.Config = {
regions: [ regions: [
{ {
countryCode: 'AA', countryCode: 'AA',
regionCode: undefined, regionCode: undefined,
minAccessAge: 13, minAccessAge: 13,
rules: [ rules: [
{ app.bsky.ageassurance.defs.configRegionRuleDefault.build({
$type: ids.Default,
access: 'full', 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 // On-device verification region, native-only (web users in TX are not
// age assured). KWS is included as a fallback for when the device // age assured). KWS is included as a fallback for when the device
@@ -68,65 +70,55 @@ export const config: AppBskyAgeassuranceDefs.Config = {
minAccessAge: 18, minAccessAge: 18,
additionalVerificationMethods: ['device'], additionalVerificationMethods: ['device'],
rules: [ rules: [
{ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18, age: 18,
access: 'full', access: 'full',
$type: ids.IfAssuredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleDefault.build({
{
access: 'none', access: 'none',
$type: ids.Default, }),
},
], ],
}, } as app.bsky.ageassurance.defs.ConfigRegion,
{ {
countryCode: 'GB', countryCode: 'GB',
minAccessAge: 13, minAccessAge: 13,
rules: [ rules: [
{ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18, age: 18,
access: 'full', access: 'full',
$type: ids.IfAssuredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
{
age: 13, age: 13,
access: 'safe', access: 'safe',
$type: ids.IfDeclaredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleDefault.build({
{
access: 'none', access: 'none',
$type: ids.Default, }),
},
], ],
}, },
{ {
countryCode: 'AU', countryCode: 'AU',
minAccessAge: 16, minAccessAge: 16,
rules: [ rules: [
{ app.bsky.ageassurance.defs.configRegionRuleIfAccountNewerThan.build({
date: '2025-12-10T00:00:00Z', date: '2025-12-10T00:00:00Z',
access: 'none', access: 'none',
$type: ids.IfAccountNewerThan, }),
}, app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
{
age: 18, age: 18,
access: 'full', access: 'full',
$type: ids.IfAssuredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
{
age: 16, age: 16,
access: 'safe', access: 'safe',
$type: ids.IfAssuredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
{
age: 16, age: 16,
access: 'safe', access: 'safe',
$type: ids.IfDeclaredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleDefault.build({
{
access: 'none', access: 'none',
$type: ids.Default, }),
},
], ],
}, },
{ {
@@ -134,20 +126,17 @@ export const config: AppBskyAgeassuranceDefs.Config = {
regionCode: 'SD', regionCode: 'SD',
minAccessAge: 13, minAccessAge: 13,
rules: [ rules: [
{ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18, age: 18,
access: 'full', access: 'full',
$type: ids.IfAssuredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
{
age: 13, age: 13,
access: 'safe', access: 'safe',
$type: ids.IfDeclaredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleDefault.build({
{
access: 'none', access: 'none',
$type: ids.Default, }),
},
], ],
}, },
{ {
@@ -155,20 +144,17 @@ export const config: AppBskyAgeassuranceDefs.Config = {
regionCode: 'WY', regionCode: 'WY',
minAccessAge: 13, minAccessAge: 13,
rules: [ rules: [
{ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18, age: 18,
access: 'full', access: 'full',
$type: ids.IfAssuredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
{
age: 13, age: 13,
access: 'safe', access: 'safe',
$type: ids.IfDeclaredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleDefault.build({
{
access: 'none', access: 'none',
$type: ids.Default, }),
},
], ],
}, },
{ {
@@ -176,20 +162,17 @@ export const config: AppBskyAgeassuranceDefs.Config = {
regionCode: 'OH', regionCode: 'OH',
minAccessAge: 13, minAccessAge: 13,
rules: [ rules: [
{ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18, age: 18,
access: 'full', access: 'full',
$type: ids.IfAssuredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
{
age: 13, age: 13,
access: 'safe', access: 'safe',
$type: ids.IfDeclaredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleDefault.build({
{
access: 'none', access: 'none',
$type: ids.Default, }),
},
], ],
}, },
{ {
@@ -197,15 +180,13 @@ export const config: AppBskyAgeassuranceDefs.Config = {
regionCode: 'MS', regionCode: 'MS',
minAccessAge: 18, minAccessAge: 18,
rules: [ rules: [
{ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18, age: 18,
access: 'full', access: 'full',
$type: ids.IfAssuredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleDefault.build({
{
access: 'none', access: 'none',
$type: ids.Default, }),
},
], ],
}, },
{ {
@@ -213,20 +194,17 @@ export const config: AppBskyAgeassuranceDefs.Config = {
regionCode: 'VA', regionCode: 'VA',
minAccessAge: 16, minAccessAge: 16,
rules: [ rules: [
{ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 16, age: 16,
access: 'full', access: 'full',
$type: ids.IfAssuredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
{
age: 16, age: 16,
access: 'full', access: 'full',
$type: ids.IfDeclaredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleDefault.build({
{
access: 'none', access: 'none',
$type: ids.Default, }),
},
], ],
}, },
{ {
@@ -234,45 +212,38 @@ export const config: AppBskyAgeassuranceDefs.Config = {
regionCode: 'TN', regionCode: 'TN',
minAccessAge: 18, minAccessAge: 18,
rules: [ rules: [
{ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18, age: 18,
access: 'full', access: 'full',
$type: ids.IfAssuredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
{
age: 18, age: 18,
access: 'full', access: 'full',
$type: ids.IfDeclaredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleDefault.build({
{
access: 'none', access: 'none',
$type: ids.Default, }),
},
], ],
}, },
{ {
countryCode: 'BR', countryCode: 'BR',
minAccessAge: 13, minAccessAge: 13,
rules: [ rules: [
{ app.bsky.ageassurance.defs.configRegionRuleIfAssuredOverAge.build({
age: 18, age: 18,
access: 'full', access: 'full',
$type: ids.IfAssuredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
{
age: 18, age: 18,
access: 'full', access: 'full',
$type: ids.IfDeclaredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleIfDeclaredOverAge.build({
{
age: 13, age: 13,
access: 'safe', access: 'safe',
$type: ids.IfDeclaredOverAge, }),
}, app.bsky.ageassurance.defs.configRegionRuleDefault.build({
{
access: 'none', access: 'none',
$type: ids.Default, }),
},
], ],
}, },
], ],
@@ -1,9 +1,8 @@
import {useCallback, useMemo} from 'react' 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 {keepPreviousData, useQuery} from '@tanstack/react-query'
import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation' import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
import {moderateProfile} from '#/lib/moderation/subjects'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences' import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences'
+1 -1
View File
@@ -9,7 +9,7 @@ import Animated, {
withTiming, withTiming,
} from 'react-native-reanimated' } 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 {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
+1 -1
View File
@@ -1,6 +1,6 @@
import {View} from 'react-native' import {View} from 'react-native'
import {moderateProfile} from '#/lib/moderation/subjects' import {moderateProfile} from '@bsky.app/sdk/moderation'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useProfilesQuery} from '#/state/queries/profile' import {useProfilesQuery} from '#/state/queries/profile'
+1 -2
View File
@@ -1,10 +1,9 @@
import {useRef} from 'react' import {useRef} from 'react'
import {View} from 'react-native' 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 {Plural, Trans, useLingui} from '@lingui/react/macro'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {moderateProfile} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {UserAvatar} from '#/view/com/util/UserAvatar' import {UserAvatar} from '#/view/com/util/UserAvatar'
+1 -2
View File
@@ -1,6 +1,6 @@
import {useEffect, useMemo} from 'react' import {useEffect, useMemo} from 'react'
import {View} from 'react-native' 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 {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
@@ -8,7 +8,6 @@ import {useQueryClient} from '@tanstack/react-query'
import {AtUri} from '@atproto/syntax' import {AtUri} from '@atproto/syntax'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {moderateUserList} from '#/lib/moderation/subjects'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {precacheList} from '#/state/queries/feed' import {precacheList} from '#/state/queries/feed'
+11 -1
View File
@@ -103,7 +103,17 @@ export function Embed({
// ignore any unknowns // ignore any unknowns
e.media.view !== null e.media.view !== null
) { ) {
return <Embed embed={e.media.view} style={style} /> /*
* 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 (
<Embed
embed={e.media.view as app.bsky.feed.defs.PostView['embed']}
style={style}
/>
)
} }
return null return null
+1 -1
View File
@@ -1,5 +1,6 @@
import {useMemo, useState} from 'react' import {useMemo, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {moderateProfile} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
@@ -8,7 +9,6 @@ import {differenceInSeconds} from 'date-fns'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {HITSLOP_10} from '#/lib/constants' import {HITSLOP_10} from '#/lib/constants'
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
+1 -1
View File
@@ -1,6 +1,6 @@
import {useMemo} from 'react' 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 {useModerationOpts} from '#/state/preferences/moderation-opts'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import * as FeedCard from '#/components/FeedCard' import * as FeedCard from '#/components/FeedCard'
+1 -1
View File
@@ -1,6 +1,6 @@
import {useMemo} from 'react' 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 {useModerationOpts} from '#/state/preferences/moderation-opts'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import * as ListCard from '#/components/ListCard' import * as ListCard from '#/components/ListCard'
+2 -3
View File
@@ -1,15 +1,14 @@
import {useCallback, useMemo} from 'react' import {useCallback, useMemo} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {moderatePost} from '@bsky.app/sdk/moderation'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {type $Typed} from '@atproto/lex' import {type $Typed} from '@atproto/lex'
import {AtUri} from '@atproto/syntax' import {AtUri} from '@atproto/syntax'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {moderatePost} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {getChatInviteCodeFromUrl} from '#/lib/strings/url-helpers' import {getChatInviteCodeFromUrl} from '#/lib/strings/url-helpers'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {unstableCacheProfileView} from '#/state/queries/profile' import {unstableCacheProfileView} from '#/state/queries/profile'
@@ -287,7 +286,7 @@ export function QuoteEmbed({
if (!bsky.isType(app.bsky.feed.post, quote.record)) return undefined if (!bsky.isType(app.bsky.feed.post, quote.record)) return undefined
const {text, facets} = quote.record const {text, facets} = quote.record
return text.trim() return text.trim()
? new RichTextAPI({text: text, facets: asSdkFacets(facets)}) ? new RichTextAPI({text: text, facets: facets})
: undefined : undefined
}, [quote.record]) }, [quote.record])
@@ -1,5 +1,5 @@
import {ScrollView, View} from 'react-native' 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 {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
@@ -8,7 +8,6 @@ import {useNavigation} from '@react-navigation/native'
import {chat} from '#/lexicons' import {chat} from '#/lexicons'
import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted' import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {moderateProfile} from '#/lib/moderation/subjects'
import {type NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
+1 -2
View File
@@ -6,12 +6,11 @@ import {
View, View,
type ViewStyle, type ViewStyle,
} from 'react-native' } 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 {RichText as RichTextApi} from '@bsky.app/sdk/richtext'
import {useLingui} from '@lingui/react/macro' import {useLingui} from '@lingui/react/macro'
import {getModerationCauseKey} from '#/lib/moderation' import {getModerationCauseKey} from '#/lib/moderation'
import {moderateProfile} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {forceLTR} from '#/lib/strings/bidi' import {forceLTR} from '#/lib/strings/bidi'
import {NON_BREAKING_SPACE} from '#/lib/strings/constants' import {NON_BREAKING_SPACE} from '#/lib/strings/constants'
@@ -1,6 +1,6 @@
import {memo, useCallback, useEffect, useMemo, useReducer, useRef} from 'react' import {memo, useCallback, useEffect, useMemo, useReducer, useRef} from 'react'
import {View} from 'react-native' 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 {flip, offset, shift, size, useFloating} from '@floating-ui/react-dom'
import {msg, plural} from '@lingui/core/macro' import {msg, plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -8,7 +8,6 @@ import {useNavigation} from '@react-navigation/native'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {getModerationCauseKey} from '#/lib/moderation' import {getModerationCauseKey} from '#/lib/moderation'
import {moderateProfile} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {type NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
@@ -1,12 +1,16 @@
import {Keyboard, View} from 'react-native' 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 {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from '#/lib/constants' 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 {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
@@ -59,7 +59,7 @@ function Inner({control}: {control: Dialog.DialogControlProps}) {
await client.call( await client.call(
com.atproto.moderation.createReport, com.atproto.moderation.createReport,
{ {
reasonType: tools.ozone.report.defs.reasonAppeal, reasonType: tools.ozone.report.defs.reasonAppeal.value,
subject: { subject: {
$type: 'com.atproto.admin.defs#repoRef', $type: 'com.atproto.admin.defs#repoRef',
// the persisted account did is already resolved // the persisted account did is already resolved
@@ -7,11 +7,10 @@ import {
useState, useState,
} from 'react' } from 'react'
import {TextInput, View} from 'react-native' 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 {Plural, Trans, useLingui} from '@lingui/react/macro'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
@@ -8,7 +8,7 @@ import {Plural, Trans} from '@lingui/react/macro'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {isOverMaxGraphemeCount} from '#/lib/strings/helpers' import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
import {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 {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
import {logger} from '#/logger' import {logger} from '#/logger'
import {type ImageMeta} from '#/state/gallery' import {type ImageMeta} from '#/state/gallery'
@@ -169,7 +169,7 @@ function DialogInner({
// We want to be working with a blank state here, so let's get the // We want to be working with a blank state here, so let's get the
// serialized version and turn it back into a RichText // serialized version and turn it back into a RichText
const serialized = richTextToString( const serialized = richTextToString(
new RichTextAPI({text, facets: asSdkFacets(facets)}), new RichTextAPI({text, facets: facets}),
false, false,
) )
+1 -1
View File
@@ -1,10 +1,10 @@
import {useCallback, useEffect} from 'react' import {useCallback, useEffect} from 'react'
import {type ScrollView, View} from 'react-native' import {type ScrollView, View} from 'react-native'
import Animated, {useAnimatedRef, useSharedValue} from 'react-native-reanimated' import Animated, {useAnimatedRef, useSharedValue} from 'react-native-reanimated'
import {moderateProfile} from '@bsky.app/sdk/moderation'
import {useLingui} from '@lingui/react/macro' import {useLingui} from '@lingui/react/macro'
import {HITSLOP_10} from '#/lib/constants' import {HITSLOP_10} from '#/lib/constants'
import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
+1 -2
View File
@@ -7,12 +7,11 @@ import {
useState, useState,
} from 'react' } from 'react'
import {LayoutAnimation, type TextInput, View} from 'react-native' 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 {Plural, Trans, useLingui} from '@lingui/react/macro'
import {MAX_GROUP_NAME_GRAPHEME_LENGTH} from '#/lib/constants' import {MAX_GROUP_NAME_GRAPHEME_LENGTH} from '#/lib/constants'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {isOverMaxGraphemeCount} from '#/lib/strings/helpers' import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
+2 -2
View File
@@ -10,7 +10,7 @@ import {useLingui} from '@lingui/react/macro'
import {chat} from '#/lexicons' import {chat} from '#/lexicons'
import {EMOJI_REACTION_LIMIT} from '#/lib/constants' import {EMOJI_REACTION_LIMIT} from '#/lib/constants'
import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate' import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'
import {asSdkFacets, richTextToString} from '#/lib/strings/rich-text-helpers' import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
import {useConvoActive} from '#/state/messages/convo' import {useConvoActive} from '#/state/messages/convo'
import {useLanguagePrefs} from '#/state/preferences' import {useLanguagePrefs} from '#/state/preferences'
@@ -86,7 +86,7 @@ export let MessageContextMenu = ({
const str = richTextToString( const str = richTextToString(
new RichText({ new RichText({
text: message.text, text: message.text,
facets: asSdkFacets(message.facets), facets: message.facets,
}), }),
true, true,
) )
+2 -3
View File
@@ -23,6 +23,7 @@ import Animated, {
ZoomOut, ZoomOut,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {moderateProfile} from '@bsky.app/sdk/moderation'
import {plural} from '@lingui/core/macro' import {plural} from '@lingui/core/macro'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
@@ -31,9 +32,7 @@ import * as bsky from '#/types/bsky'
import {app, chat} from '#/lexicons' import {app, chat} from '#/lexicons'
import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted' import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
import {type Shadow} from '#/state/cache/types' import {type Shadow} from '#/state/cache/types'
import {type ConvoItem} from '#/state/messages/convo/types' import {type ConvoItem} from '#/state/messages/convo/types'
@@ -257,7 +256,7 @@ let MessageItem = ({
const rt = new RichTextAPI({ const rt = new RichTextAPI({
text: message.text, text: message.text,
facets: asSdkFacets(message.facets), facets: message.facets,
}) })
const isEmojiOnly = isOnlyEmoji(message.text) const isEmojiOnly = isOnlyEmoji(message.text)
+1 -2
View File
@@ -1,10 +1,9 @@
import {useMemo} from 'react' import {useMemo} from 'react'
import {View} from 'react-native' 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 {useLingui} from '@lingui/react/macro'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {moderateProfile} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -1,8 +1,7 @@
import {View} from 'react-native' 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 {Trans} from '@lingui/react/macro'
import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
+1 -2
View File
@@ -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 {type $Typed} from '@atproto/lex'
import {chat} from '#/lexicons' import {chat} from '#/lexicons'
import {EMOJI_REACTION_LIMIT} from '#/lib/constants' import {EMOJI_REACTION_LIMIT} from '#/lib/constants'
import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted' import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted'
import {moderateProfile} from '#/lib/moderation/subjects'
import {logger} from '#/logger' import {logger} from '#/logger'
import {type Shadow} from '#/state/cache/profile-shadow' import {type Shadow} from '#/state/cache/profile-shadow'
import {type ConvoState, ConvoStatus} from '#/state/messages/convo/types' import {type ConvoState, ConvoStatus} from '#/state/messages/convo/types'
@@ -1,12 +1,12 @@
import {useEffect} from 'react' import {useEffect} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Plural, Trans, useLingui} from '@lingui/react/macro' import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import * as bsky from '#/types/bsky' import * as bsky from '#/types/bsky'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {moderateProfile} from '#/lib/moderation/subjects'
import {useCallOnce} from '#/lib/once' import {useCallOnce} from '#/lib/once'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {type NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
@@ -1,9 +1,9 @@
import {useMemo} from 'react' import {useMemo} from 'react'
import {Pressable, View} from 'react-native' import {Pressable, View} from 'react-native'
import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Plural, Trans, useLingui} from '@lingui/react/macro' import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {moderateProfile} from '#/lib/moderation/subjects'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import { import {
useTrendingSettings, useTrendingSettings,
+1 -1
View File
@@ -50,7 +50,7 @@ export function AppealForm({
await client.call( await client.call(
com.atproto.moderation.createReport, 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 * `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 * `cid`: an at-uri plus cid for a record, or the label's `uri` reused
+106 -106
View File
@@ -30,93 +30,93 @@ export const NEW_TO_OLD_REASONS_MAP: Record<
tools.ozone.report.defs.ReasonType, tools.ozone.report.defs.ReasonType,
com.atproto.moderation.defs.ReasonType com.atproto.moderation.defs.ReasonType
> = { > = {
[tools.ozone.report.defs.reasonAppeal]: [tools.ozone.report.defs.reasonAppeal.value]:
com.atproto.moderation.defs.reasonAppeal, com.atproto.moderation.defs.reasonAppeal.value,
[tools.ozone.report.defs.reasonOther]: [tools.ozone.report.defs.reasonOther.value]:
com.atproto.moderation.defs.reasonOther, com.atproto.moderation.defs.reasonOther.value,
[tools.ozone.report.defs.reasonViolenceAnimal]: [tools.ozone.report.defs.reasonViolenceAnimal.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonViolenceThreats]: [tools.ozone.report.defs.reasonViolenceThreats.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonViolenceGraphicContent]: [tools.ozone.report.defs.reasonViolenceGraphicContent.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonViolenceGlorification]: [tools.ozone.report.defs.reasonViolenceGlorification.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonViolenceExtremistContent]: [tools.ozone.report.defs.reasonViolenceExtremistContent.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonViolenceTrafficking]: [tools.ozone.report.defs.reasonViolenceTrafficking.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonViolenceOther]: [tools.ozone.report.defs.reasonViolenceOther.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonSexualAbuseContent]: [tools.ozone.report.defs.reasonSexualAbuseContent.value]:
com.atproto.moderation.defs.reasonSexual, com.atproto.moderation.defs.reasonSexual.value,
[tools.ozone.report.defs.reasonSexualNCII]: [tools.ozone.report.defs.reasonSexualNCII.value]:
com.atproto.moderation.defs.reasonSexual, com.atproto.moderation.defs.reasonSexual.value,
[tools.ozone.report.defs.reasonSexualDeepfake]: [tools.ozone.report.defs.reasonSexualDeepfake.value]:
com.atproto.moderation.defs.reasonSexual, com.atproto.moderation.defs.reasonSexual.value,
[tools.ozone.report.defs.reasonSexualAnimal]: [tools.ozone.report.defs.reasonSexualAnimal.value]:
com.atproto.moderation.defs.reasonSexual, com.atproto.moderation.defs.reasonSexual.value,
[tools.ozone.report.defs.reasonSexualUnlabeled]: [tools.ozone.report.defs.reasonSexualUnlabeled.value]:
com.atproto.moderation.defs.reasonSexual, com.atproto.moderation.defs.reasonSexual.value,
[tools.ozone.report.defs.reasonSexualOther]: [tools.ozone.report.defs.reasonSexualOther.value]:
com.atproto.moderation.defs.reasonSexual, com.atproto.moderation.defs.reasonSexual.value,
[tools.ozone.report.defs.reasonChildSafetyCSAM]: [tools.ozone.report.defs.reasonChildSafetyCSAM.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonChildSafetyGroom]: [tools.ozone.report.defs.reasonChildSafetyGroom.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonChildSafetyPrivacy]: [tools.ozone.report.defs.reasonChildSafetyPrivacy.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonChildSafetyHarassment]: [tools.ozone.report.defs.reasonChildSafetyHarassment.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonChildSafetyOther]: [tools.ozone.report.defs.reasonChildSafetyOther.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonHarassmentTroll]: [tools.ozone.report.defs.reasonHarassmentTroll.value]:
com.atproto.moderation.defs.reasonRude, com.atproto.moderation.defs.reasonRude.value,
[tools.ozone.report.defs.reasonHarassmentTargeted]: [tools.ozone.report.defs.reasonHarassmentTargeted.value]:
com.atproto.moderation.defs.reasonRude, com.atproto.moderation.defs.reasonRude.value,
[tools.ozone.report.defs.reasonHarassmentHateSpeech]: [tools.ozone.report.defs.reasonHarassmentHateSpeech.value]:
com.atproto.moderation.defs.reasonRude, com.atproto.moderation.defs.reasonRude.value,
[tools.ozone.report.defs.reasonHarassmentDoxxing]: [tools.ozone.report.defs.reasonHarassmentDoxxing.value]:
com.atproto.moderation.defs.reasonRude, com.atproto.moderation.defs.reasonRude.value,
[tools.ozone.report.defs.reasonHarassmentOther]: [tools.ozone.report.defs.reasonHarassmentOther.value]:
com.atproto.moderation.defs.reasonRude, com.atproto.moderation.defs.reasonRude.value,
[tools.ozone.report.defs.reasonMisleadingBot]: [tools.ozone.report.defs.reasonMisleadingBot.value]:
com.atproto.moderation.defs.reasonMisleading, com.atproto.moderation.defs.reasonMisleading.value,
[tools.ozone.report.defs.reasonMisleadingImpersonation]: [tools.ozone.report.defs.reasonMisleadingImpersonation.value]:
com.atproto.moderation.defs.reasonMisleading, com.atproto.moderation.defs.reasonMisleading.value,
[tools.ozone.report.defs.reasonMisleadingSpam]: [tools.ozone.report.defs.reasonMisleadingSpam.value]:
com.atproto.moderation.defs.reasonSpam, com.atproto.moderation.defs.reasonSpam.value,
[tools.ozone.report.defs.reasonMisleadingScam]: [tools.ozone.report.defs.reasonMisleadingScam.value]:
com.atproto.moderation.defs.reasonMisleading, com.atproto.moderation.defs.reasonMisleading.value,
[tools.ozone.report.defs.reasonMisleadingElections]: [tools.ozone.report.defs.reasonMisleadingElections.value]:
com.atproto.moderation.defs.reasonMisleading, com.atproto.moderation.defs.reasonMisleading.value,
[tools.ozone.report.defs.reasonMisleadingOther]: [tools.ozone.report.defs.reasonMisleadingOther.value]:
com.atproto.moderation.defs.reasonMisleading, com.atproto.moderation.defs.reasonMisleading.value,
[tools.ozone.report.defs.reasonRuleSiteSecurity]: [tools.ozone.report.defs.reasonRuleSiteSecurity.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonRuleProhibitedSales]: [tools.ozone.report.defs.reasonRuleProhibitedSales.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonRuleBanEvasion]: [tools.ozone.report.defs.reasonRuleBanEvasion.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonRuleOther]: [tools.ozone.report.defs.reasonRuleOther.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonSelfHarmContent]: [tools.ozone.report.defs.reasonSelfHarmContent.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonSelfHarmED]: [tools.ozone.report.defs.reasonSelfHarmED.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonSelfHarmStunts]: [tools.ozone.report.defs.reasonSelfHarmStunts.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonSelfHarmSubstances]: [tools.ozone.report.defs.reasonSelfHarmSubstances.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
[tools.ozone.report.defs.reasonSelfHarmOther]: [tools.ozone.report.defs.reasonSelfHarmOther.value]:
com.atproto.moderation.defs.reasonViolation, com.atproto.moderation.defs.reasonViolation.value,
} }
/** /**
@@ -130,26 +130,26 @@ export const OLD_TO_NEW_REASONS_MAP: Record<
>, >,
tools.ozone.report.defs.ReasonType tools.ozone.report.defs.ReasonType
> = { > = {
[com.atproto.moderation.defs.reasonSpam]: [ [com.atproto.moderation.defs.reasonSpam.value]: [
tools.ozone.report.defs.reasonMisleadingSpam, tools.ozone.report.defs.reasonMisleadingSpam.value,
], ],
[com.atproto.moderation.defs.reasonViolation]: [ [com.atproto.moderation.defs.reasonViolation.value]: [
tools.ozone.report.defs.reasonRuleOther, tools.ozone.report.defs.reasonRuleOther.value,
], ],
[com.atproto.moderation.defs.reasonMisleading]: [ [com.atproto.moderation.defs.reasonMisleading.value]: [
tools.ozone.report.defs.reasonMisleadingOther, tools.ozone.report.defs.reasonMisleadingOther.value,
], ],
[com.atproto.moderation.defs.reasonSexual]: [ [com.atproto.moderation.defs.reasonSexual.value]: [
tools.ozone.report.defs.reasonSexualUnlabeled, tools.ozone.report.defs.reasonSexualUnlabeled.value,
], ],
[com.atproto.moderation.defs.reasonRude]: [ [com.atproto.moderation.defs.reasonRude.value]: [
tools.ozone.report.defs.reasonHarassmentOther, tools.ozone.report.defs.reasonHarassmentOther.value,
], ],
[com.atproto.moderation.defs.reasonOther]: [ [com.atproto.moderation.defs.reasonOther.value]: [
tools.ozone.report.defs.reasonOther, tools.ozone.report.defs.reasonOther.value,
], ],
[com.atproto.moderation.defs.reasonAppeal]: [ [com.atproto.moderation.defs.reasonAppeal.value]: [
tools.ozone.report.defs.reasonAppeal, tools.ozone.report.defs.reasonAppeal.value,
], ],
} }
@@ -159,14 +159,14 @@ export const OLD_TO_NEW_REASONS_MAP: Record<
*/ */
export const OTHER_REPORT_REASONS: Set<tools.ozone.report.defs.ReasonType> = export const OTHER_REPORT_REASONS: Set<tools.ozone.report.defs.ReasonType> =
new Set([ new Set([
tools.ozone.report.defs.reasonViolenceOther, tools.ozone.report.defs.reasonViolenceOther.value,
tools.ozone.report.defs.reasonSexualOther, tools.ozone.report.defs.reasonSexualOther.value,
tools.ozone.report.defs.reasonChildSafetyOther, tools.ozone.report.defs.reasonChildSafetyOther.value,
tools.ozone.report.defs.reasonHarassmentOther, tools.ozone.report.defs.reasonHarassmentOther.value,
tools.ozone.report.defs.reasonMisleadingOther, tools.ozone.report.defs.reasonMisleadingOther.value,
tools.ozone.report.defs.reasonRuleOther, tools.ozone.report.defs.reasonRuleOther.value,
tools.ozone.report.defs.reasonSelfHarmOther, tools.ozone.report.defs.reasonSelfHarmOther.value,
tools.ozone.report.defs.reasonOther, tools.ozone.report.defs.reasonOther.value,
]) ])
/** /**
@@ -174,10 +174,10 @@ export const OTHER_REPORT_REASONS: Set<tools.ozone.report.defs.ReasonType> =
*/ */
export const BSKY_LABELER_ONLY_REPORT_REASONS: Set<tools.ozone.report.defs.ReasonType> = export const BSKY_LABELER_ONLY_REPORT_REASONS: Set<tools.ozone.report.defs.ReasonType> =
new Set([ new Set([
tools.ozone.report.defs.reasonChildSafetyCSAM, tools.ozone.report.defs.reasonChildSafetyCSAM.value,
tools.ozone.report.defs.reasonChildSafetyGroom, tools.ozone.report.defs.reasonChildSafetyGroom.value,
tools.ozone.report.defs.reasonChildSafetyOther, tools.ozone.report.defs.reasonChildSafetyOther.value,
tools.ozone.report.defs.reasonViolenceExtremistContent, tools.ozone.report.defs.reasonViolenceExtremistContent.value,
]) ])
/** /**
@@ -8,12 +8,12 @@ import {
const nciiOption = { const nciiOption = {
title: 'Non-consensual intimate imagery', title: 'Non-consensual intimate imagery',
reason: tools.ozone.report.defs.reasonSexualNCII, reason: tools.ozone.report.defs.reasonSexualNCII.value,
} }
const otherOption = { const otherOption = {
title: 'Unlabeled adult content', title: 'Unlabeled adult content',
reason: tools.ozone.report.defs.reasonSexualUnlabeled, reason: tools.ozone.report.defs.reasonSexualUnlabeled.value,
} }
function selectNciiOption(state: ReportState = initialState) { function selectNciiOption(state: ReportState = initialState) {
@@ -124,7 +124,7 @@ export function reducer(state: ReportState, action: ReportAction): ReportState {
} }
case 'selectOption': { case 'selectOption': {
const isNcii = const isNcii =
action.option.reason === tools.ozone.report.defs.reasonSexualNCII action.option.reason === tools.ozone.report.defs.reasonSexualNCII.value
return { return {
...state, ...state,
selectedOption: action.option, selectedOption: action.option,
@@ -37,27 +37,27 @@ export function useReportOptions() {
options: [ options: [
{ {
title: _(msg`Spam`), title: _(msg`Spam`),
reason: tools.ozone.report.defs.reasonMisleadingSpam, reason: tools.ozone.report.defs.reasonMisleadingSpam.value,
}, },
{ {
title: _(msg`Scam`), title: _(msg`Scam`),
reason: tools.ozone.report.defs.reasonMisleadingScam, reason: tools.ozone.report.defs.reasonMisleadingScam.value,
}, },
{ {
title: _(msg`Fake account or bot`), title: _(msg`Fake account or bot`),
reason: tools.ozone.report.defs.reasonMisleadingBot, reason: tools.ozone.report.defs.reasonMisleadingBot.value,
}, },
{ {
title: _(msg`Impersonation`), title: _(msg`Impersonation`),
reason: tools.ozone.report.defs.reasonMisleadingImpersonation, reason: tools.ozone.report.defs.reasonMisleadingImpersonation.value,
}, },
{ {
title: _(msg`False information about elections`), title: _(msg`False information about elections`),
reason: tools.ozone.report.defs.reasonMisleadingElections, reason: tools.ozone.report.defs.reasonMisleadingElections.value,
}, },
{ {
title: _(msg`Other misleading content`), 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: [ options: [
{ {
title: _(msg`Unlabeled adult content`), title: _(msg`Unlabeled adult content`),
reason: tools.ozone.report.defs.reasonSexualUnlabeled, reason: tools.ozone.report.defs.reasonSexualUnlabeled.value,
}, },
{ {
title: _(msg`Adult sexual abuse content`), 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`), title: _(msg`Non-consensual intimate imagery`),
reason: tools.ozone.report.defs.reasonSexualNCII, reason: tools.ozone.report.defs.reasonSexualNCII.value,
}, },
{ {
title: _(msg`Deepfake adult content`), title: _(msg`Deepfake adult content`),
reason: tools.ozone.report.defs.reasonSexualDeepfake, reason: tools.ozone.report.defs.reasonSexualDeepfake.value,
}, },
{ {
title: _(msg`Animal sexual abuse`), title: _(msg`Animal sexual abuse`),
reason: tools.ozone.report.defs.reasonSexualAnimal, reason: tools.ozone.report.defs.reasonSexualAnimal.value,
}, },
{ {
title: _(msg`Other sexual violence content`), 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: [ options: [
{ {
title: _(msg`Trolling`), title: _(msg`Trolling`),
reason: tools.ozone.report.defs.reasonHarassmentTroll, reason: tools.ozone.report.defs.reasonHarassmentTroll.value,
}, },
{ {
title: _(msg`Targeted harassment`), title: _(msg`Targeted harassment`),
reason: tools.ozone.report.defs.reasonHarassmentTargeted, reason: tools.ozone.report.defs.reasonHarassmentTargeted.value,
}, },
{ {
title: _(msg`Hate speech`), title: _(msg`Hate speech`),
reason: tools.ozone.report.defs.reasonHarassmentHateSpeech, reason: tools.ozone.report.defs.reasonHarassmentHateSpeech.value,
}, },
{ {
title: _(msg`Doxxing`), title: _(msg`Doxxing`),
reason: tools.ozone.report.defs.reasonHarassmentDoxxing, reason: tools.ozone.report.defs.reasonHarassmentDoxxing.value,
}, },
{ {
title: _(msg`Other harassing or hateful content`), 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: [ options: [
{ {
title: _(msg`Animal welfare`), title: _(msg`Animal welfare`),
reason: tools.ozone.report.defs.reasonViolenceAnimal, reason: tools.ozone.report.defs.reasonViolenceAnimal.value,
}, },
{ {
title: _(msg`Threats or incitement`), title: _(msg`Threats or incitement`),
reason: tools.ozone.report.defs.reasonViolenceThreats, reason: tools.ozone.report.defs.reasonViolenceThreats.value,
}, },
{ {
title: _(msg`Graphic violent content`), title: _(msg`Graphic violent content`),
reason: tools.ozone.report.defs.reasonViolenceGraphicContent, reason: tools.ozone.report.defs.reasonViolenceGraphicContent.value,
}, },
{ {
title: _(msg`Glorification of violence`), title: _(msg`Glorification of violence`),
reason: tools.ozone.report.defs.reasonViolenceGlorification, reason: tools.ozone.report.defs.reasonViolenceGlorification.value,
}, },
{ {
title: _(msg`Extremist content`), title: _(msg`Extremist content`),
reason: tools.ozone.report.defs.reasonViolenceExtremistContent, reason:
tools.ozone.report.defs.reasonViolenceExtremistContent.value,
}, },
{ {
title: _(msg`Human trafficking`), title: _(msg`Human trafficking`),
reason: tools.ozone.report.defs.reasonViolenceTrafficking, reason: tools.ozone.report.defs.reasonViolenceTrafficking.value,
}, },
{ {
title: _(msg`Other violent content`), 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: [ options: [
{ {
title: _(msg`Child Sexual Abuse Material (CSAM)`), 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`), 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`), 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`), 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`), 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: [ options: [
{ {
title: _(msg`Content promoting or depicting self-harm`), 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`), title: _(msg`Eating disorders`),
reason: tools.ozone.report.defs.reasonSelfHarmED, reason: tools.ozone.report.defs.reasonSelfHarmED.value,
}, },
{ {
title: _(msg`Dangerous challenges or activities`), 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`), 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`), 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: [ options: [
{ {
title: _(msg`Hacking or system attacks`), 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`), 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`), title: _(msg`Banned user returning`),
reason: tools.ozone.report.defs.reasonRuleBanEvasion, reason: tools.ozone.report.defs.reasonRuleBanEvasion.value,
}, },
{ {
title: _(msg`Other network rule-breaking`), 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: [ options: [
{ {
title: _(msg`Other`), title: _(msg`Other`),
reason: tools.ozone.report.defs.reasonOther, reason: tools.ozone.report.defs.reasonOther.value,
}, },
], ],
}, },
+7 -5
View File
@@ -1,4 +1,4 @@
import {type Did} from '@atproto/syntax' import {type DidString} from '@atproto/syntax'
import packageJson from '#/../package.json' 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 * The DID of the Bluesky appview to proxy to
*/ */
export const BLUESKY_PROXY_DID: Did = export const BLUESKY_PROXY_DID: DidString =
process.env.EXPO_PUBLIC_BLUESKY_PROXY_DID || 'did:web:api.bsky.app' (process.env.EXPO_PUBLIC_BLUESKY_PROXY_DID as DidString) ||
'did:web:api.bsky.app'
/** /**
* The DID of the chat service to proxy to * The DID of the chat service to proxy to
*/ */
export const CHAT_PROXY_DID: Did = export const CHAT_PROXY_DID: DidString =
process.env.EXPO_PUBLIC_CHAT_PROXY_DID || 'did:web:api.bsky.chat' (process.env.EXPO_PUBLIC_CHAT_PROXY_DID as DidString) ||
'did:web:api.bsky.chat'
/** /**
* Metrics API host * Metrics API host
@@ -55,7 +55,9 @@ function DialogInner({
const {_, i18n} = useLingui() const {_, i18n} = useLingui()
const t = useTheme() 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<string>(embed.external.uri)
const [liveLinkError, setLiveLinkError] = useState('') const [liveLinkError, setLiveLinkError] = useState('')
const tick = useTickEveryMinute() const tick = useTickEveryMinute()
@@ -60,7 +60,7 @@ export function DialogInner({
await client.call( await client.call(
com.atproto.moderation.createReport, com.atproto.moderation.createReport,
{ {
reasonType: tools.ozone.report.defs.reasonAppeal, reasonType: tools.ozone.report.defs.reasonAppeal.value,
subject: { subject: {
$type: 'com.atproto.repo.strongRef', $type: 'com.atproto.repo.strongRef',
// a status view's uri is an at-uri produced by the appview // a status view's uri is an at-uri produced by the appview
@@ -1,13 +1,13 @@
import {useCallback, useMemo} from 'react' import {useCallback, useMemo} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import {moderateStatus} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {useOpenLink} from '#/lib/hooks/useOpenLink' import {useOpenLink} from '#/lib/hooks/useOpenLink'
import {moderateStatus} from '#/lib/moderation/subjects'
import {type NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {toNiceDomain} from '#/lib/strings/url-helpers' import {toNiceDomain} from '#/lib/strings/url-helpers'
+5 -4
View File
@@ -1,7 +1,8 @@
import {useMemo} from 'react' import {useMemo} from 'react'
import {retry} from '@atproto/common-web' 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 {type AtIdentifierString, AtUri, toDatetimeString} from '@atproto/syntax'
import {moderateStatus} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
@@ -10,7 +11,6 @@ import {isAfter, parseISO} from 'date-fns'
import {uploadBlob} from '#/lib/api' import {uploadBlob} from '#/lib/api'
import {imageToThumb} from '#/lib/api/resolve' import {imageToThumb} from '#/lib/api/resolve'
import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta' import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta'
import {moderateStatus} from '#/lib/moderation/subjects'
import {matchXrpcError} from '#/lib/xrpc-error' import {matchXrpcError} from '#/lib/xrpc-error'
import {useAppConfig} from '#/state/appConfig' import {useAppConfig} from '#/state/appConfig'
import { import {
@@ -335,7 +335,7 @@ export function useUpsertLiveStatusMutation(
$type: 'app.bsky.actor.defs#statusView', $type: 'app.bsky.actor.defs#statusView',
status: 'app.bsky.actor.status#live', status: 'app.bsky.actor.status#live',
isActive: true, isActive: true,
expiresAt: expiresAt.toISOString(), expiresAt: toDatetimeString(expiresAt),
embed: embed:
record.embed && image record.embed && image
? { ? {
@@ -343,7 +343,8 @@ export function useUpsertLiveStatusMutation(
external: { external: {
...record.embed.external, ...record.embed.external,
$type: 'app.bsky.embed.external#viewExternal', $type: 'app.bsky.embed.external#viewExternal',
thumb: image, // an opengraph image URL from link resolution
thumb: image as UriString,
}, },
} }
: undefined, : undefined,
+7 -2
View File
@@ -15,7 +15,12 @@ import {type FeedAPI, type FeedAPIResponse} from './types'
// we use this fallback marker post to drive this instead. see Feed.tsx // we use this fallback marker post to drive this instead. see Feed.tsx
// for the usage. // for the usage.
// -prf // -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: { post: {
uri: 'fallback-marker-post', uri: 'fallback-marker-post',
cid: 'fake', cid: 'fake',
@@ -26,7 +31,7 @@ export const FALLBACK_MARKER_POST: app.bsky.feed.defs.FeedViewPost = {
}, },
indexedAt: new Date().toISOString(), indexedAt: new Date().toISOString(),
}, },
} } as unknown as app.bsky.feed.defs.FeedViewPost
export class HomeFeedAPI implements FeedAPI { export class HomeFeedAPI implements FeedAPI {
client: Client client: Client
+7 -5
View File
@@ -1,11 +1,13 @@
import {toDatetimeString} from '@atproto/syntax'
import {subDays, subMinutes} from 'date-fns' import {subDays, subMinutes} from 'date-fns'
import {app} from '#/lexicons'
import {type app} from '#/lexicons'
const DID = `did:plc:z72i7hdynmk6r22z27h6tvur` const DID = `did:plc:z72i7hdynmk6r22z27h6tvur`
const NOW = new Date() const NOW = new Date()
const POST_1_DATE = subMinutes(NOW, 2).toISOString() const POST_1_DATE = toDatetimeString(subMinutes(NOW, 2))
const POST_2_DATE = subMinutes(NOW, 4).toISOString() const POST_2_DATE = toDatetimeString(subMinutes(NOW, 4))
const POST_3_DATE = subMinutes(NOW, 5).toISOString() const POST_3_DATE = toDatetimeString(subMinutes(NOW, 5))
export const DEMO_FEED = { export const DEMO_FEED = {
feed: [ feed: [
@@ -31,7 +33,7 @@ export const DEMO_FEED = {
issuer: DID, issuer: DID,
uri: `at://${DID}/app.bsky.graph.verification/post1`, uri: `at://${DID}/app.bsky.graph.verification/post1`,
isValid: true, isValid: true,
createdAt: subDays(NOW, 11).toISOString(), createdAt: toDatetimeString(subDays(NOW, 11)),
}, },
], ],
verifiedStatus: 'valid', verifiedStatus: 'valid',
-125
View File
@@ -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,
})
}
-23
View File
@@ -30,26 +30,3 @@ export function richTextToString(rt: RichText, loose: boolean): string {
return result return result
} }
/**
* Widens facets typed by the legacy `@atproto/api` codegen into the shape the
* SDK's `RichText` accepts.
*
* The two are the same lexicon and identical at runtime; they differ only in
* that the SDK brands `did`/`uri` as template literal types, which makes the
* legacy `string` versions unassignable. Call this where facets read off an
* `@atproto/api` view type are handed to `new RichText(...)`.
*
* Transitional: it goes away once the view types come from the SDK too.
*/
export function asSdkFacets(
facets: {index: {byteStart: number; byteEnd: number}; features: unknown[]}[],
): app.bsky.richtext.facet.Main[]
export function asSdkFacets(
facets:
| {index: {byteStart: number; byteEnd: number}; features: unknown[]}[]
| undefined,
): app.bsky.richtext.facet.Main[] | undefined
export function asSdkFacets(facets: unknown) {
return facets as app.bsky.richtext.facet.Main[] | undefined
}
+14
View File
@@ -1,6 +1,7 @@
import { import {
getMain, getMain,
type InferMethodError, type InferMethodError,
LexError,
type Main, type Main,
type Procedure, type Procedure,
type Query, type Query,
@@ -27,6 +28,19 @@ export function getErrorStatus(e: unknown): number | undefined {
return e instanceof XrpcResponseError ? e.status : 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 * Same nsid means `e` was thrown for this method schema, so `e` can be
* treated as an `XrpcResponseError<M>` - which is what lets the SDK's * treated as an `XrpcResponseError<M>` - which is what lets the SDK's
+1 -1
View File
@@ -175,7 +175,7 @@ export function CustomFeedScreenInner({
const isVideoFeed = useMemo(() => { const isVideoFeed = useMemo(() => {
const isBskyVideoFeed = VIDEO_FEED_URIS.includes(feedInfo.uri) const isBskyVideoFeed = VIDEO_FEED_URIS.includes(feedInfo.uri)
const feedIsVideoMode = const feedIsVideoMode =
feedInfo.contentMode === app.bsky.feed.defs.contentModeVideo feedInfo.contentMode === app.bsky.feed.defs.contentModeVideo.value
const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
return IS_NATIVE && _isVideoFeed return IS_NATIVE && _isVideoFeed
}, [feedInfo]) }, [feedInfo])
+1 -1
View File
@@ -43,7 +43,7 @@ export function ListHiddenScreen({
const goBack = useGoBack() const goBack = useGoBack()
const queryClient = useQueryClient() 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 [isProcessing, setIsProcessing] = useState(false)
const listBlockMutation = useListBlockMutation() const listBlockMutation = useListBlockMutation()
+1 -1
View File
@@ -5,6 +5,7 @@ import {
ScrollEdgeEffect, ScrollEdgeEffect,
ScrollEdgeEffectProvider, ScrollEdgeEffectProvider,
} from '@bsky.app/expo-scroll-edge-effect' } from '@bsky.app/expo-scroll-edge-effect'
import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import { import {
type RouteProp, type RouteProp,
@@ -19,7 +20,6 @@ import * as bsky from '#/types/bsky'
import {chat} from '#/lexicons' import {chat} from '#/lexicons'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useViewportZoomLock} from '#/lib/hooks/useViewportZoomLock' import {useViewportZoomLock} from '#/lib/hooks/useViewportZoomLock'
import {moderateProfile} from '#/lib/moderation/subjects'
import { import {
type CommonNavigatorParams, type CommonNavigatorParams,
type NavigationProp, type NavigationProp,
@@ -1,9 +1,9 @@
import {View} from 'react-native' import {View} from 'react-native'
import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted' import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {moderateProfile} from '#/lib/moderation/subjects'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
+1 -1
View File
@@ -2,12 +2,12 @@ import {useEffect} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {ImageBackground} from 'expo-image' import {ImageBackground} from 'expo-image'
import {type ThemeName} from '@bsky.app/alf' import {type ThemeName} from '@bsky.app/alf'
import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import * as bsky from '#/types/bsky' import * as bsky from '#/types/bsky'
import {chat} from '#/lexicons' import {chat} from '#/lexicons'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useJoinLinkPreviewsQuery} from '#/state/queries/join-links' import {useJoinLinkPreviewsQuery} from '#/state/queries/join-links'
@@ -103,7 +103,7 @@ function DialogInner() {
await client.call( await client.call(
com.atproto.moderation.createReport, com.atproto.moderation.createReport,
{ {
reasonType: tools.ozone.report.defs.reasonAppeal, reasonType: tools.ozone.report.defs.reasonAppeal.value,
subject: { subject: {
$type: 'com.atproto.admin.defs#repoRef', $type: 'com.atproto.admin.defs#repoRef',
// the persisted account did is already resolved // the persisted account did is already resolved
@@ -1,6 +1,7 @@
import {useCallback, useMemo, useState} from 'react' import {useCallback, useMemo, useState} from 'react'
import {type GestureResponderEvent, View} from 'react-native' import {type GestureResponderEvent, View} from 'react-native'
import { import {
moderateProfile,
type ModerationDecision, type ModerationDecision,
type ModerationOpts, type ModerationOpts,
} from '@bsky.app/sdk/moderation' } from '@bsky.app/sdk/moderation'
@@ -12,7 +13,6 @@ import {chat} from '#/lexicons'
import {GestureActionView} from '#/lib/custom-animations/GestureActionView' import {GestureActionView} from '#/lib/custom-animations/GestureActionView'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {moderateProfile} from '#/lib/moderation/subjects'
import {decrementBadgeCount} from '#/lib/notifications/notifications' import {decrementBadgeCount} from '#/lib/notifications/notifications'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import { import {
@@ -1,10 +1,10 @@
import {useCallback, useMemo} from 'react' import {useCallback, useMemo} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {LinearGradient} from 'expo-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {type ActiveConvoStates} from '#/state/messages/convo' import {type ActiveConvoStates} from '#/state/messages/convo'
@@ -1,13 +1,12 @@
import {useState} from 'react' import {useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {Image} from 'expo-image' 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 {Plural, Trans, useLingui} from '@lingui/react/macro'
import {chat} from '#/lexicons' import {chat} from '#/lexicons'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {moderateProfile} from '#/lib/moderation/subjects'
import {shareUrl} from '#/lib/sharing' import {shareUrl} from '#/lib/sharing'
import {useCreateJoinLink} from '#/state/queries/messages/create-join-link' import {useCreateJoinLink} from '#/state/queries/messages/create-join-link'
import {useDisableJoinLink} from '#/state/queries/messages/disable-join-link' import {useDisableJoinLink} from '#/state/queries/messages/disable-join-link'
@@ -1,18 +1,17 @@
import {useCallback, useEffect, useMemo, useState} from 'react' import {useCallback, useEffect, useMemo, useState} from 'react'
import {LayoutAnimation, View} from 'react-native' import {LayoutAnimation, View} from 'react-native'
import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {moderatePost} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {type RouteProp, useNavigation, useRoute} from '@react-navigation/native' import {type RouteProp, useNavigation, useRoute} from '@react-navigation/native'
import {AtUri} from '@atproto/syntax' import {AtUri} from '@atproto/syntax'
import {HITSLOP_20} from '#/lib/constants' import {HITSLOP_20} from '#/lib/constants'
import {moderatePost} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import { import {
type CommonNavigatorParams, type CommonNavigatorParams,
type NavigationProp, type NavigationProp,
} from '#/lib/routes/types' } from '#/lib/routes/types'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import { import {
convertBskyAppUrlIfNeeded, convertBskyAppUrlIfNeeded,
getChatInviteCodeFromUrl, getChatInviteCodeFromUrl,
@@ -168,7 +167,7 @@ function MessageInputPostEmbed({
return { return {
rt: new RichTextAPI({ rt: new RichTextAPI({
text: post.record.text, text: post.record.text,
facets: asSdkFacets(post.record.facets), facets: post.record.facets,
}), }),
record: post.record, record: post.record,
} }
@@ -1,9 +1,9 @@
import {View} from 'react-native' import {View} from 'react-native'
import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {moderateProfile} from '#/lib/moderation/subjects'
import {type NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles' import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -1,9 +1,9 @@
import {View} from 'react-native' import {View} from 'react-native'
import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Plural, Trans, useLingui} from '@lingui/react/macro' import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {AtUri} from '@atproto/syntax' import {AtUri} from '@atproto/syntax'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {moderateProfile} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -10,7 +10,6 @@ import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {niceDate} from '#/lib/strings/time' import {niceDate} from '#/lib/strings/time'
import { import {
POST_TOMBSTONE, POST_TOMBSTONE,
@@ -196,7 +195,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
() => () =>
new RichTextAPI({ new RichTextAPI({
text: record.text, text: record.text,
facets: asSdkFacets(record.facets), facets: record.facets,
}), }),
[record], [record],
) )
@@ -9,7 +9,6 @@ import {MAX_POST_LINES} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {countLines} from '#/lib/strings/helpers' import {countLines} from '#/lib/strings/helpers'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import { import {
POST_TOMBSTONE, POST_TOMBSTONE,
type Shadow, type Shadow,
@@ -209,7 +208,7 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({
() => () =>
new RichTextAPI({ new RichTextAPI({
text: record.text, text: record.text,
facets: asSdkFacets(record.facets), facets: record.facets,
}), }),
[record], [record],
) )
@@ -9,7 +9,6 @@ import {MAX_POST_LINES} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {countLines} from '#/lib/strings/helpers' import {countLines} from '#/lib/strings/helpers'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import { import {
POST_TOMBSTONE, POST_TOMBSTONE,
type Shadow, type Shadow,
@@ -269,7 +268,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
() => () =>
new RichTextAPI({ new RichTextAPI({
text: record.text, text: record.text,
facets: asSdkFacets(record.facets), facets: record.facets,
}), }),
[record], [record],
) )
@@ -1,6 +1,6 @@
import {memo, useCallback, useMemo, useState} from 'react' import {memo, useCallback, useMemo, useState} from 'react'
import {View} from 'react-native' 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 {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {msg, plural} from '@lingui/core/macro' import {msg, plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -10,7 +10,6 @@ import {Plural, Trans} from '@lingui/react/macro'
import {MAX_LABELERS} from '#/lib/constants' import {MAX_LABELERS} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {isAppLabeler} from '#/lib/moderation' import {isAppLabeler} from '#/lib/moderation'
import {moderateProfile} from '#/lib/moderation/subjects'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {type Shadow} from '#/state/cache/types' import {type Shadow} from '#/state/cache/types'
import {useLabelerSubscriptionMutation} from '#/state/queries/labeler' import {useLabelerSubscriptionMutation} from '#/state/queries/labeler'
@@ -1,6 +1,7 @@
import {memo, useMemo, useState} from 'react' import {memo, useMemo, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import { import {
moderateProfile,
type ModerationDecision, type ModerationDecision,
type ModerationOpts, type ModerationOpts,
} from '@bsky.app/sdk/moderation' } from '@bsky.app/sdk/moderation'
@@ -11,7 +12,6 @@ import {Trans} from '@lingui/react/macro'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {logger} from '#/logger' import {logger} from '#/logger'
import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow' import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
+1 -2
View File
@@ -7,11 +7,10 @@ import Animated, {
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {scheduleOnRN} from 'react-native-worklets' 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 {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext'
import {useIsFocused} from '@react-navigation/native' import {useIsFocused} from '@react-navigation/native'
import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -8,7 +8,6 @@ import {Trans} from '@lingui/react/macro'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {makeListLink} from '#/lib/routes/links' import {makeListLink} from '#/lib/routes/links'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useListBlockMutation, useListMuteMutation} from '#/state/queries/list' import {useListBlockMutation, useListMuteMutation} from '#/state/queries/list'
import { import {
@@ -40,8 +39,8 @@ export function Header({
const {_} = useLingui() const {_} = useLingui()
const ax = useAnalytics() const ax = useAnalytics()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const isCurateList = list.purpose === app.bsky.graph.defs.curatelist const isCurateList = list.purpose === app.bsky.graph.defs.curatelist.value
const isModList = list.purpose === app.bsky.graph.defs.modlist const isModList = list.purpose === app.bsky.graph.defs.modlist.value
const isBlocking = !!list.viewer?.blocked const isBlocking = !!list.viewer?.blocked
const isMuting = !!list.viewer?.muted const isMuting = !!list.viewer?.muted
const playHaptic = useHaptics() const playHaptic = useHaptics()
@@ -130,7 +129,7 @@ export function Header({
list.description list.description
? new RichTextAPI({ ? new RichTextAPI({
text: list.description, text: list.description,
facets: asSdkFacets(list.descriptionFacets), facets: list.descriptionFacets,
}) })
: undefined, : undefined,
[list], [list],
@@ -58,8 +58,8 @@ export function MoreOptionsMenu({
const {mutateAsync: muteList} = useListMuteMutation() const {mutateAsync: muteList} = useListMuteMutation()
const {mutateAsync: blockList} = useListBlockMutation() const {mutateAsync: blockList} = useListBlockMutation()
const isCurateList = list.purpose === app.bsky.graph.defs.curatelist const isCurateList = list.purpose === app.bsky.graph.defs.curatelist.value
const isModList = list.purpose === app.bsky.graph.defs.modlist const isModList = list.purpose === app.bsky.graph.defs.modlist.value
const isBlocking = !!list.viewer?.blocked const isBlocking = !!list.viewer?.blocked
const isMuting = !!list.viewer?.muted const isMuting = !!list.viewer?.muted
const isPinned = Boolean(savedFeedConfig?.pinned) const isPinned = Boolean(savedFeedConfig?.pinned)
+2 -3
View File
@@ -1,7 +1,7 @@
import {useCallback, useMemo, useRef, useState} from 'react' import {useCallback, useMemo, useRef, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useAnimatedRef} from 'react-native-reanimated' 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 {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
@@ -12,7 +12,6 @@ import {AtUri} from '@atproto/syntax'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useSetTitle} from '#/lib/hooks/useSetTitle' import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {moderateUserList} from '#/lib/moderation/subjects'
import { import {
type CommonNavigatorParams, type CommonNavigatorParams,
type NativeStackScreenProps, type NativeStackScreenProps,
@@ -156,7 +155,7 @@ function ProfileListScreenLoaded({
const {rkey} = route.params const {rkey} = route.params
const feedSectionRef = useRef<SectionRef>(null) const feedSectionRef = useRef<SectionRef>(null)
const aboutSectionRef = useRef<SectionRef>(null) const aboutSectionRef = useRef<SectionRef>(null)
const isCurateList = list.purpose === app.bsky.graph.defs.curatelist const isCurateList = list.purpose === app.bsky.graph.defs.curatelist.value
const isScreenFocused = useIsFocused() const isScreenFocused = useIsFocused()
const isHidden = list.labels?.findIndex(l => l.val === '!hide') !== -1 const isHidden = list.labels?.findIndex(l => l.val === '!hide') !== -1
const isOwner = currentAccount?.did === list.creator.did const isOwner = currentAccount?.did === list.creator.did
@@ -1,9 +1,8 @@
import {ScrollView, View} from 'react-native' 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 {Plural, Trans, useLingui} from '@lingui/react/macro'
import {createHitslop} from '#/lib/constants' import {createHitslop} from '#/lib/constants'
import {moderateProfile} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
@@ -1,11 +1,11 @@
import {useState} from 'react' import {useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {moderateProfile} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {moderateProfile} from '#/lib/moderation/subjects'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
@@ -2,10 +2,10 @@ import {useMemo} from 'react'
import {Pressable, View} from 'react-native' import {Pressable, View} from 'react-native'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import {RichText as RichTextApi} from '@bsky.app/sdk/richtext' 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 {Plural, Trans, useLingui} from '@lingui/react/macro'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {moderateProfile} from '#/lib/moderation/subjects'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import { import {
useTrendingSettings, useTrendingSettings,
@@ -79,8 +79,8 @@ export function AutomationLabelSettingsScreen({}: Props) {
return existing return existing
}, },
checkCommitted: res => { checkCommitted: profile => {
const exists = !!res.data.labels?.some(l => l.val === 'bot') const exists = !!profile.labels?.some(l => l.val === 'bot')
return exists === wasAdded return exists === wasAdded
}, },
}, },
+1 -1
View File
@@ -2,6 +2,7 @@ import {useState} from 'react'
import {Alert, LayoutAnimation, Linking, Pressable, View} from 'react-native' import {Alert, LayoutAnimation, Linking, Pressable, View} from 'react-native'
import {useReducedMotion} from 'react-native-reanimated' import {useReducedMotion} from 'react-native-reanimated'
import {removeNuxs} from '@bsky.app/sdk' import {removeNuxs} from '@bsky.app/sdk'
import {moderateProfile} from '@bsky.app/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {type NativeStackScreenProps} from '@react-navigation/native-stack' 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 {HELP_DESK_URL, HITSLOP_10} from '#/lib/constants'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates' import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates'
import {moderateProfile} from '#/lib/moderation/subjects'
import { import {
type CommonNavigatorParams, type CommonNavigatorParams,
type NavigationProp, type NavigationProp,
@@ -69,8 +69,8 @@ export function PwiOptOut() {
return existing return existing
}, },
checkCommitted: res => { checkCommitted: profile => {
const exists = !!res.data.labels?.some( const exists = !!profile.labels?.some(
l => l.val === '!no-unauthenticated', l => l.val === '!no-unauthenticated',
) )
return exists === wasAdded return exists === wasAdded
@@ -21,7 +21,6 @@ import {
type NavigationProp, type NavigationProp,
} from '#/lib/routes/types' } from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {getStarterPackOgCard} from '#/lib/strings/starter-pack' import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
import {logger} from '#/logger' import {logger} from '#/logger'
import {updateProfileShadow} from '#/state/cache/profile-shadow' import {updateProfileShadow} from '#/state/cache/profile-shadow'
@@ -413,7 +412,7 @@ function Header({
const richText = record.description const richText = record.description
? new RichTextAPI({ ? new RichTextAPI({
text: record.description, text: record.description,
facets: asSdkFacets(record.descriptionFacets), facets: record.descriptionFacets,
}) })
: undefined : undefined
+1 -1
View File
@@ -54,7 +54,7 @@ export function Takendown() {
await client.call( await client.call(
com.atproto.moderation.createReport, com.atproto.moderation.createReport,
{ {
reasonType: tools.ozone.report.defs.reasonAppeal, reasonType: tools.ozone.report.defs.reasonAppeal.value,
subject: { subject: {
$type: 'com.atproto.admin.defs#repoRef', $type: 'com.atproto.admin.defs#repoRef',
// the persisted account did is already resolved // the persisted account did is already resolved
+1 -2
View File
@@ -54,7 +54,6 @@ import {
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useA11y} from '#/state/a11y' import {useA11y} from '#/state/a11y'
import { import {
@@ -831,7 +830,7 @@ function Overlay({
: undefined : undefined
const richText = new RichTextAPI({ const richText = new RichTextAPI({
text: record?.text || '', text: record?.text || '',
facets: asSdkFacets(record?.facets), facets: record?.facets,
}) })
const handle = sanitizeHandle(post.author.handle, '@') const handle = sanitizeHandle(post.author.handle, '@')
+22 -5
View File
@@ -1,3 +1,4 @@
import {type AtUriString} from '@atproto/syntax'
import {useEffect, useMemo, useState} from 'react' import {useEffect, useMemo, useState} from 'react'
import {type QueryClient} from '@tanstack/react-query' import {type QueryClient} from '@tanstack/react-query'
import {EventEmitter} from 'eventemitter3' import {EventEmitter} from 'eventemitter3'
@@ -15,9 +16,13 @@ import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/stat
import {castAsShadow, type Shadow} from './types' import {castAsShadow, type Shadow} from './types'
export 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 { export interface PostShadow {
likeUri: string | undefined likeUri: AtUriString | 'pending' | undefined
repostUri: string | undefined repostUri: AtUriString | 'pending' | undefined
isDeleted: boolean isDeleted: boolean
embed: embed:
| app.bsky.embed.record.View | 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, post.embed) &&
bsky.isType(app.bsky.embed.recordWithMedia.view, shadow.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, bookmarkCount: bookmarkCount,
viewer: { viewer: {
...(post.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, pinned: 'pinned' in shadow ? shadow.pinned : post.viewer?.pinned,
bookmarked: bookmarked:
'bookmarked' in shadow ? shadow.bookmarked : post.viewer?.bookmarked, 'bookmarked' in shadow ? shadow.bookmarked : post.viewer?.bookmarked,
+5 -4
View File
@@ -1,3 +1,4 @@
import {type DidString, toDatetimeString} from '@atproto/syntax'
import {type Client, XrpcResponseError, type $Typed} from '@atproto/lex' import {type Client, XrpcResponseError, type $Typed} from '@atproto/lex'
import {EventEmitter} from 'eventemitter3' import {EventEmitter} from 'eventemitter3'
import {nanoid} from 'nanoid/non-secure' import {nanoid} from 'nanoid/non-secure'
@@ -97,7 +98,7 @@ export class Convo {
private chatClient: Client private chatClient: Client
private events: MessagesEventBus private events: MessagesEventBus
private senderUserDid: string private senderUserDid: DidString
private status: ConvoStatus = ConvoStatus.Uninitialized private status: ConvoStatus = ConvoStatus.Uninitialized
private error: ConvoError | undefined private error: ConvoError | undefined
@@ -1420,7 +1421,7 @@ export class Convo {
$type: 'chat.bsky.convo.defs#messageView', $type: 'chat.bsky.convo.defs#messageView',
id: nanoid(), id: nanoid(),
rev: '__fake__', rev: '__fake__',
sentAt: new Date().toISOString(), sentAt: toDatetimeString(new Date()),
sender: { sender: {
$type: 'chat.bsky.convo.defs#messageViewSender', $type: 'chat.bsky.convo.defs#messageViewSender',
did: this.senderUserDid, did: this.senderUserDid,
@@ -1462,10 +1463,10 @@ export class Convo {
* @param emoji - must be one grapheme * @param emoji - must be one grapheme
*/ */
async addReaction(messageId: string, emoji: string) { async addReaction(messageId: string, emoji: string) {
const optimisticReaction = { const optimisticReaction: chat.bsky.convo.defs.ReactionView = {
value: emoji, value: emoji,
sender: {did: this.senderUserDid}, sender: {did: this.senderUserDid},
createdAt: new Date().toISOString(), createdAt: toDatetimeString(new Date()),
} }
let restore: null | (() => void) = null let restore: null | (() => void) = null
if (this.pastMessages.has(messageId)) { if (this.pastMessages.has(messageId)) {
+1 -2
View File
@@ -1,9 +1,8 @@
import {useCallback} from 'react' 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 {keepPreviousData, useQuery, useQueryClient} from '@tanstack/react-query'
import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation' import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
import {moderateProfile} from '#/lib/moderation/subjects'
import {logger} from '#/logger' import {logger} from '#/logger'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {useAppviewClient} from '#/state/session' import {useAppviewClient} from '#/state/session'
+1 -1
View File
@@ -1,5 +1,6 @@
import {useMemo, useRef} from 'react' import {useMemo, useRef} from 'react'
import {type AtUriString, AtUri} from '@atproto/syntax' import {type AtUriString, AtUri} from '@atproto/syntax'
import {moderatePost} from '@bsky.app/sdk/moderation'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import { import {
@@ -13,7 +14,6 @@ import {app} from '#/lexicons'
import {CustomFeedAPI} from '#/lib/api/feed/custom' import {CustomFeedAPI} from '#/lib/api/feed/custom'
import {aggregateUserInterests} from '#/lib/api/feed/utils' import {aggregateUserInterests} from '#/lib/api/feed/utils'
import {FeedTuner} from '#/lib/api/feed-manip' import {FeedTuner} from '#/lib/api/feed-manip'
import {moderatePost} from '#/lib/moderation/subjects'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import { import {
+3 -4
View File
@@ -1,6 +1,7 @@
import {useCallback, useEffect, useMemo, useRef} from 'react' import {useCallback, useEffect, useMemo, useRef} from 'react'
import {type AtUriString, AtUri} from '@atproto/syntax' import {type AtUriString, AtUri} from '@atproto/syntax'
import {RichText} from '@bsky.app/sdk/richtext' import {RichText} from '@bsky.app/sdk/richtext'
import {moderateFeedGenerator} from '@bsky.app/sdk/moderation'
import {t} from '@lingui/core/macro' import {t} from '@lingui/core/macro'
import { import {
type InfiniteData, type InfiniteData,
@@ -13,10 +14,8 @@ import {
} from '@tanstack/react-query' } from '@tanstack/react-query'
import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants' import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants'
import {moderateFeedGenerator} from '#/lib/moderation/subjects'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {asSdkFacets} from '#/lib/strings/rich-text-helpers'
import {GCTIME, STALE} from '#/state/queries' import {GCTIME, STALE} from '#/state/queries'
import {RQKEY as listQueryKey} from '#/state/queries/list' import {RQKEY as listQueryKey} from '#/state/queries/list'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
@@ -99,7 +98,7 @@ export function hydrateFeedGenerator(
const description = new RichText({ const description = new RichText({
text: view.description || '', text: view.description || '',
facets: asSdkFacets((view.descriptionFacets || [])?.slice()), facets: (view.descriptionFacets || [])?.slice(),
}) })
if (!view.descriptionFacets) { if (!view.descriptionFacets) {
@@ -142,7 +141,7 @@ export function hydrateList(
const description = new RichText({ const description = new RichText({
text: view.description || '', text: view.description || '',
facets: asSdkFacets((view.descriptionFacets || [])?.slice()), facets: (view.descriptionFacets || [])?.slice(),
}) })
if (!view.descriptionFacets) { if (!view.descriptionFacets) {
+8 -5
View File
@@ -1,5 +1,6 @@
import {toDatetimeString} from '@atproto/syntax'
import {useCallback} from 'react' 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 {type QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
import * as bsky from '#/types/bsky' import * as bsky from '#/types/bsky'
@@ -22,10 +23,10 @@ export type KnownChatInvitePreview =
| $Typed<chat.bsky.group.defs.InvalidJoinLinkPreviewView> | $Typed<chat.bsky.group.defs.InvalidJoinLinkPreviewView>
/** /**
* The full open-union shape, including the `{$type: string}` fallback for * The full open-union shape, including lex's `Unknown$TypedObject` fallback for
* future variants. * 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 * Narrows a preview to one of the three known variants, filtering out the
@@ -112,7 +113,9 @@ export function setJoinLinkPreviewRequestedForCode(
...preview, ...preview,
viewer: { viewer: {
...preview.viewer, ...preview.viewer,
requestedAt: requested ? new Date().toISOString() : undefined, requestedAt: requested
? toDatetimeString(new Date())
: undefined,
}, },
} }
} }
+2 -2
View File
@@ -28,14 +28,14 @@ export function useListMembershipAddMutation({
* Needed for optimistic update of starter pack query * Needed for optimistic update of starter pack query
*/ */
subject?: bsky.profile.AnyProfileView subject?: bsky.profile.AnyProfileView
onSuccess?: (data: {uri: string; cid: string}) => void onSuccess?: (data: {uri: AtUriString; cid: string}) => void
onError?: (error: Error) => void onError?: (error: Error) => void
} = {}) { } = {}) {
const {currentAccount} = useSession() const {currentAccount} = useSession()
const pdsClient = usePdsClient() const pdsClient = usePdsClient()
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useMutation< return useMutation<
{uri: string; cid: string}, {uri: AtUriString; cid: string},
Error, Error,
{listUri: string; actorDid: string} {listUri: string; actorDid: string}
>({ >({
@@ -1,3 +1,4 @@
import {toDatetimeString} from '@atproto/syntax'
import {useMutation, useQueryClient} from '@tanstack/react-query' import {useMutation, useQueryClient} from '@tanstack/react-query'
import * as bsky from '#/types/bsky' import * as bsky from '#/types/bsky'
@@ -52,7 +53,7 @@ export function useCreateJoinLink(
enabledStatus: 'enabled', enabledStatus: 'enabled',
joinRule, joinRule,
requireApproval, requireApproval,
createdAt: new Date().toISOString(), createdAt: toDatetimeString(new Date()),
}, },
}, },
} }
@@ -742,11 +742,18 @@ export function ListConvosProviderInner({
return { return {
...convo, ...convo,
members: [...convo.members, ...relatedProfilesSansMembers], 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: { lastReaction: {
$type: 'chat.bsky.convo.defs#messageAndReactionView', $type: 'chat.bsky.convo.defs#messageAndReactionView',
reaction: log.reaction, reaction: log.reaction,
message: log.message, message: log.message,
}, } as chat.bsky.convo.defs.ConvoView['lastReaction'],
rev: log.rev, rev: log.rev,
} }
}), }),
@@ -1,14 +1,19 @@
import {type DidString} from '@atproto/syntax'
import {describe, expect, it, jest} from '@jest/globals' import {describe, expect, it, jest} from '@jest/globals'
import {groupNotifications} from '../util'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {type Notification} from '../types'
import {groupNotifications} from '../util'
jest.mock('#/state/queries/profile', () => ({precacheProfile: jest.fn()})) 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( function makeFollowNotification(
did: string, did: DidString,
starterPackUri?: string, starterPackUri?: string,
): Notification { ): Notification {
return { return {
@@ -31,7 +36,7 @@ function makeFollowNotification(
: undefined, : undefined,
isRead: false, isRead: false,
indexedAt: '2026-07-28T12:00:00.000Z', indexedAt: '2026-07-28T12:00:00.000Z',
} } as Notification
} }
describe('groupNotifications', () => { describe('groupNotifications', () => {
+1 -1
View File
@@ -26,8 +26,8 @@ import {
} from '@tanstack/react-query' } from '@tanstack/react-query'
import {AtUri} from '@atproto/syntax' import {AtUri} from '@atproto/syntax'
import {moderatePost} from '@bsky.app/sdk/moderation'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {moderatePost} from '#/lib/moderation/subjects'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {useAppviewClient} from '#/state/session' import {useAppviewClient} from '#/state/session'
+13 -2
View File
@@ -1,5 +1,16 @@
import {app} from '#/lexicons' 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 = export type NotificationType =
| StarterPackNotificationType | StarterPackNotificationType
| OtherNotificationType | OtherNotificationType
@@ -50,8 +61,8 @@ type OtherNotificationType =
type FeedNotificationBase = { type FeedNotificationBase = {
_reactKey: string _reactKey: string
notification: app.bsky.notification.listNotifications.Notification notification: Notification
additional?: app.bsky.notification.listNotifications.Notification[] additional?: Notification[]
subjectUri?: string subjectUri?: string
subject?: subject?:
| app.bsky.feed.defs.PostView | app.bsky.feed.defs.PostView
+10 -10
View File
@@ -1,17 +1,21 @@
import {type Client} from '@atproto/lex' import {type Client} from '@atproto/lex'
import {type AtUriString} from '@atproto/syntax' import {type AtUriString} from '@atproto/syntax'
import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {
hasMutedWord,
moderateNotification,
type ModerationOpts,
} from '@bsky.app/sdk/moderation'
import {type QueryClient} from '@tanstack/react-query' import {type QueryClient} from '@tanstack/react-query'
import chunk from 'lodash.chunk' import chunk from 'lodash.chunk'
import {labelIsHideableOffense} from '#/lib/moderation' import {labelIsHideableOffense} from '#/lib/moderation'
import {hasMutedWord, moderateNotification} from '#/lib/moderation/subjects'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import * as bsky from '#/types/bsky' import * as bsky from '#/types/bsky'
import {precacheProfile} from '../profile' import {precacheProfile} from '../profile'
import { import {
type FeedNotification, type FeedNotification,
type FeedPage, type FeedPage,
type Notification,
type NotificationType, type NotificationType,
} from './types' } from './types'
@@ -108,7 +112,7 @@ export async function fetchPage({
// = // =
export function shouldFilterNotif( export function shouldFilterNotif(
notif: app.bsky.notification.listNotifications.Notification, notif: Notification,
moderationOpts: ModerationOpts | undefined, moderationOpts: ModerationOpts | undefined,
): boolean { ): boolean {
const containsImperative = !!notif.author.labels?.some(labelIsHideableOffense) const containsImperative = !!notif.author.labels?.some(labelIsHideableOffense)
@@ -138,9 +142,7 @@ export function shouldFilterNotif(
return moderateNotification(notif, moderationOpts).ui('contentList').filter return moderateNotification(notif, moderationOpts).ui('contentList').filter
} }
export function groupNotifications( export function groupNotifications(notifs: Notification[]): FeedNotification[] {
notifs: app.bsky.notification.listNotifications.Notification[],
): FeedNotification[] {
const groupedNotifs: FeedNotification[] = [] const groupedNotifs: FeedNotification[] = []
for (const notif of notifs) { for (const notif of notifs) {
const ts = +new Date(notif.indexedAt) const ts = +new Date(notif.indexedAt)
@@ -249,9 +251,7 @@ async function fetchSubjects(
} }
} }
function toKnownType( function toKnownType(notif: Notification): NotificationType {
notif: app.bsky.notification.listNotifications.Notification,
): NotificationType {
if (notif.reason === 'like') { if (notif.reason === 'like') {
if (notif.reasonSubject?.includes('feed.generator')) { if (notif.reasonSubject?.includes('feed.generator')) {
return 'feedgen-like' return 'feedgen-like'
@@ -279,7 +279,7 @@ function toKnownType(
function getSubjectUri( function getSubjectUri(
type: NotificationType, type: NotificationType,
notif: app.bsky.notification.listNotifications.Notification, notif: Notification,
): string | undefined { ): string | undefined {
if ( if (
type === 'reply' || type === 'reply' ||
+3 -3
View File
@@ -55,10 +55,10 @@ export function usePinnedPostMutation() {
: undefined : undefined
return existing return existing
}, },
checkCommitted: res => checkCommitted: profile =>
pinCurrentPost pinCurrentPost
? res.data.pinnedPost?.uri === postUri ? profile.pinnedPost?.uri === postUri
: !res.data.pinnedPost, : !profile.pinnedPost,
}) })
if (pinCurrentPost) { if (pinCurrentPost) {
+2 -2
View File
@@ -3,6 +3,7 @@ import {AppState} from 'react-native'
import {type Client} from '@atproto/lex' import {type Client} from '@atproto/lex'
import {type AtIdentifierString, type AtUriString, AtUri} from '@atproto/syntax' import {type AtIdentifierString, type AtUriString, AtUri} from '@atproto/syntax'
import { import {
moderatePost,
type ModerationDecision, type ModerationDecision,
type ModerationPrefs, type ModerationPrefs,
} from '@bsky.app/sdk/moderation' } 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 {aggregateUserInterests} from '#/lib/api/feed/utils'
import {FeedTuner, type FeedTunerFn} from '#/lib/api/feed-manip' import {FeedTuner, type FeedTunerFn} from '#/lib/api/feed-manip'
import {DISCOVER_FEED_URI} from '#/lib/constants' import {DISCOVER_FEED_URI} from '#/lib/constants'
import {moderatePost} from '#/lib/moderation/subjects'
import {logger} from '#/logger' import {logger} from '#/logger'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const' import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
@@ -78,7 +78,7 @@ export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) {
export interface FeedPostSliceItem { export interface FeedPostSliceItem {
_reactKey: string _reactKey: string
uri: string uri: AtUriString
post: app.bsky.feed.defs.PostView post: app.bsky.feed.defs.PostView
record: app.bsky.feed.post.Main record: app.bsky.feed.post.Main
moderation: ModerationDecision moderation: ModerationDecision
+2 -2
View File
@@ -190,7 +190,7 @@ function usePostLikeMutation(
const pdsClient = usePdsClient() const pdsClient = usePdsClient()
const ax = useAnalytics() const ax = useAnalytics()
return useMutation< return useMutation<
{uri: string}, // responds with the uri of the like {uri: AtUriString}, // responds with the uri of the like
Error, Error,
{uri: string; cid: string; via?: {uri: string; cid: string}} // the post's uri and cid, and the repost uri/cid if present {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 pdsClient = usePdsClient()
const ax = useAnalytics() const ax = useAnalytics()
return useMutation< return useMutation<
{uri: string}, // responds with the uri of the repost {uri: AtUriString}, // responds with the uri of the repost
Error, Error,
{uri: string; cid: string; via?: {uri: string; cid: string}} // the post's uri and cid, and the repost uri/cid if present {uri: string; cid: string; via?: {uri: string; cid: string}} // the post's uri and cid, and the repost uri/cid if present
>({ >({
+2 -2
View File
@@ -1,6 +1,6 @@
import {useRef} from 'react' import {useRef} from 'react'
import {type Client} from '@atproto/lex' 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 {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {networkRetry, retry} from '#/lib/async/retry' import {networkRetry, retry} from '#/lib/async/retry'
@@ -177,7 +177,7 @@ export function useToggleQuoteDetachmentMutation() {
action, action,
}: { }: {
post: app.bsky.feed.defs.PostView post: app.bsky.feed.defs.PostView
quoteUri: string quoteUri: AtUriString
action: 'detach' | 'reattach' action: 'detach' | 'reattach'
}) => { }) => {
// cache here since post shadow mutates original object // cache here since post shadow mutates original object
+2 -2
View File
@@ -58,7 +58,7 @@ export function mergePostgateRecords(
export function createEmbedViewDetachedRecord({ export function createEmbedViewDetachedRecord({
uri, uri,
}: { }: {
uri: string uri: AtUriString
}): $Typed<app.bsky.embed.record.View> { }): $Typed<app.bsky.embed.record.View> {
const record: $Typed<app.bsky.embed.record.ViewDetached> = { const record: $Typed<app.bsky.embed.record.ViewDetached> = {
$type: 'app.bsky.embed.record#viewDetached', $type: 'app.bsky.embed.record#viewDetached',
@@ -86,7 +86,7 @@ export function createMaybeDetachedQuoteEmbed({
| { | {
post: app.bsky.feed.defs.PostView post: app.bsky.feed.defs.PostView
quote: undefined quote: undefined
quoteUri: string quoteUri: AtUriString
detached: true detached: true
}): }):
| app.bsky.embed.record.View | app.bsky.embed.record.View
+1 -1
View File
@@ -5,7 +5,7 @@ import {
useInfiniteQuery, useInfiniteQuery,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import {moderateFeedGenerator} from '#/lib/moderation/subjects' import {moderateFeedGenerator} from '@bsky.app/sdk/moderation'
import {useAppviewClient} from '#/state/session' import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {useModerationOpts} from '../preferences/moderation-opts' import {useModerationOpts} from '../preferences/moderation-opts'
+1 -1
View File
@@ -5,7 +5,7 @@ import {
useInfiniteQuery, useInfiniteQuery,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import {moderateUserList} from '#/lib/moderation/subjects' import {moderateUserList} from '@bsky.app/sdk/moderation'
import {useAppviewClient} from '#/state/session' import {useAppviewClient} from '#/state/session'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {useModerationOpts} from '../preferences/moderation-opts' import {useModerationOpts} from '../preferences/moderation-opts'
+13 -20
View File
@@ -146,7 +146,7 @@ interface ProfileUpdateParams {
) => Un$Typed<app.bsky.actor.profile.Main>) ) => Un$Typed<app.bsky.actor.profile.Main>)
newUserAvatar?: ImageMeta | undefined | null newUserAvatar?: ImageMeta | undefined | null
newUserBanner?: 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() { export function useProfileUpdateMutation() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -206,21 +206,21 @@ export function useProfileUpdateMutation() {
appviewClient, appviewClient,
profile.did, profile.did,
checkCommitted || checkCommitted ||
(res => { (profile => {
if (typeof newUserAvatar !== 'undefined') { if (typeof newUserAvatar !== 'undefined') {
if (newUserAvatar === null && res.data.avatar) { if (newUserAvatar === null && profile.avatar) {
// url hasn't cleared yet // url hasn't cleared yet
return false return false
} else if (res.data.avatar === profile.avatar) { } else if (profile.avatar === profile.avatar) {
// url hasn't changed yet // url hasn't changed yet
return false return false
} }
} }
if (typeof newUserBanner !== 'undefined') { if (typeof newUserBanner !== 'undefined') {
if (newUserBanner === null && res.data.banner) { if (newUserBanner === null && profile.banner) {
// url hasn't cleared yet // url hasn't cleared yet
return false return false
} else if (res.data.banner === profile.banner) { } else if (profile.banner === profile.banner) {
// url hasn't changed yet // url hasn't changed yet
return false return false
} }
@@ -229,8 +229,8 @@ export function useProfileUpdateMutation() {
return true return true
} }
return ( return (
res.data.displayName === updates.displayName && profile.displayName === updates.displayName &&
res.data.description === updates.description profile.description === updates.description
) )
}), }),
) )
@@ -381,7 +381,7 @@ function useProfileFollowMutation(
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {captureAction} = useProgressGuideControls() const {captureAction} = useProgressGuideControls()
return useMutation<{uri: string; cid: string}, Error, {did: string}>({ return useMutation<{uri: AtUriString; cid: string}, Error, {did: string}>({
mutationFn: async ({did}) => { mutationFn: async ({did}) => {
let ownProfile: app.bsky.actor.defs.ProfileViewDetailed | undefined let ownProfile: app.bsky.actor.defs.ProfileViewDetailed | undefined
if (currentAccount) { if (currentAccount) {
@@ -637,7 +637,7 @@ function useProfileBlockMutation() {
const {currentAccount} = useSession() const {currentAccount} = useSession()
const pdsClient = usePdsClient() const pdsClient = usePdsClient()
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useMutation<{uri: string; cid: string}, Error, {did: string}>({ return useMutation<{uri: AtUriString; cid: string}, Error, {did: string}>({
mutationFn: async ({did}) => { mutationFn: async ({did}) => {
if (!currentAccount) { if (!currentAccount) {
throw new Error('Not signed in') throw new Error('Not signed in')
@@ -679,23 +679,16 @@ function useProfileUnblockMutation() {
async function whenAppViewReady( async function whenAppViewReady(
client: Client, client: Client,
actor: string, actor: string,
fn: (res: app.bsky.actor.getProfile.Response) => boolean, fn: (profile: app.bsky.actor.getProfile.$OutputBody) => boolean,
) { ) {
await until( await until(
5, // 5 tries 5, // 5 tries
1e3, // 1s delay between tries 1e3, // 1s delay between tries
fn, fn,
/* async () =>
* `checkCommitted` callbacks are still written against the legacy await client.call(app.bsky.actor.getProfile, {
* `Response` envelope, so wrap the lex output until those consumers move.
*/
async () => ({
success: true,
headers: {},
data: await client.call(app.bsky.actor.getProfile, {
actor: actor as AtIdentifierString, actor: actor as AtIdentifierString,
}), }),
}),
) )
} }
+21 -2
View File
@@ -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 * operator. v2 renames v1's singular operators to plural arrays, and `lang` to
* `language`. * `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( export function buildSearchPostsV2Filters(
embedded: Omit<ExtractedSearchParams, 'q'>, embedded: Omit<ExtractedSearchParams, 'q'>,
filters?: SearchFilters, filters?: SearchFilters,
): app.bsky.feed.searchPostsV2.$Params { ): SearchPostsV2FilterParams {
const apiFilters = filters ? filtersToApiParams(filters) : {} const apiFilters = filters ? filtersToApiParams(filters) : {}
const params: app.bsky.feed.searchPostsV2.$Params = {} const params: SearchPostsV2FilterParams = {}
const authors = mergeList( const authors = mergeList(
embedded.author ? [embedded.author] : undefined, embedded.author ? [embedded.author] : undefined,
+1 -1
View File
@@ -7,7 +7,7 @@ import {
} from '@tanstack/react-query' } from '@tanstack/react-query'
import {AtUri} from '@atproto/syntax' 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 {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useAppviewClient} from '#/state/session' import {useAppviewClient} from '#/state/session'
import {type SearchFilters} from '#/screens/Search/searchParams' import {type SearchFilters} from '#/screens/Search/searchParams'
+1 -2
View File
@@ -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 {type $Typed} from '@atproto/lex'
import {AtUri} from '@atproto/syntax' import {AtUri} from '@atproto/syntax'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import {moderatePost} from '#/lib/moderation/subjects'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import { import {
type ApiThreadItem, type ApiThreadItem,
-41
View File
@@ -1,5 +1,3 @@
import {AppBskyFeedPost} from '@atproto/api'
import {app} from '#/lexicons' import {app} from '#/lexicons'
import * as bsky from '#/types/bsky' 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<AppBskyFeedPost.Record>(
validPost,
AppBskyFeedPost.isRecord,
),
).toBe(true)
// Right $type, invalid body - still passes the fast guard.
expect(
bsky.dangerousIsType<AppBskyFeedPost.Record>(
invalidPost,
AppBskyFeedPost.isRecord,
),
).toBe(true)
expect(
bsky.dangerousIsType<AppBskyFeedPost.Record>(
wrongType,
AppBskyFeedPost.isRecord,
),
).toBe(false)
})
it('validate fully validates the body', () => {
expect(
bsky.validate<AppBskyFeedPost.Record>(
validPost,
AppBskyFeedPost.validateRecord,
),
).toBe(true)
expect(
bsky.validate<AppBskyFeedPost.Record>(
invalidPost,
AppBskyFeedPost.validateRecord,
),
).toBe(false)
})
})
+5 -23
View File
@@ -1,8 +1,3 @@
import {
type $Typed as $TypedApi,
type AppBskyEmbedRecord,
type AppBskyFeedDefs,
} from '@atproto/api'
import {type $Typed} from '@atproto/lex' import {type $Typed} from '@atproto/lex'
import {type app} from '#/lexicons' import {type app} from '#/lexicons'
@@ -109,38 +104,25 @@ const starterPackViewBasic = {
const asEmbed = (v: unknown) => v as app.bsky.feed.defs.PostView['embed'] const asEmbed = (v: unknown) => v as app.bsky.feed.defs.PostView['embed']
/* /*
* Type-level assertions for the dual-world widening. These are compile-time * Type-level assertions for the embed union. Compile-time only: each arm must
* only: each widened arm must accept both the `#/lexicons` view and the * accept the `#/lexicons` view of its def, and `parseEmbed` must accept a
* `@atproto/api` view, and `parseEmbed` must accept a `PostView.embed` from * `PostView.embed`.
* either world, because both worlds have live producers.
*/ */
type Assignable<From, To> = From extends To ? true : false type Assignable<From, To> = From extends To ? true : false
type Expect<T extends true> = T type Expect<T extends true> = T
type _PostArmAcceptsNewWorld = Expect< type _PostArmAcceptsView = Expect<
Assignable< Assignable<
{type: 'post'; view: $Typed<app.bsky.embed.record.ViewRecord>}, {type: 'post'; view: $Typed<app.bsky.embed.record.ViewRecord>},
EmbedType<'post'> EmbedType<'post'>
> >
> >
type _PostArmAcceptsOldWorld = Expect< type _ParseEmbedAcceptsPostViewEmbed = Expect<
Assignable<
{type: 'post'; view: $TypedApi<AppBskyEmbedRecord.ViewRecord>},
EmbedType<'post'>
>
>
type _ParseEmbedAcceptsNewWorld = Expect<
Assignable< Assignable<
app.bsky.feed.defs.PostView['embed'], app.bsky.feed.defs.PostView['embed'],
Parameters<typeof parseEmbed>[0] Parameters<typeof parseEmbed>[0]
> >
> >
type _ParseEmbedAcceptsOldWorld = Expect<
Assignable<
AppBskyFeedDefs.PostView['embed'],
Parameters<typeof parseEmbed>[0]
>
>
type _ParseEmbedReturnsEmbed = Expect< type _ParseEmbedReturnsEmbed = Expect<
Assignable<ReturnType<typeof parseEmbed>, Embed> Assignable<ReturnType<typeof parseEmbed>, Embed>
> >
+5 -13
View File
@@ -1,5 +1,3 @@
import {type AppBskyGraphDefs} from '@atproto/api'
import {type app} from '#/lexicons' import {type app} from '#/lexicons'
import { import {
type AnyStarterPackView, type AnyStarterPackView,
@@ -30,25 +28,19 @@ const fullView = {
} }
/* /*
* Type-level assertions for the dual-world widening: the alias must accept a * Type-level assertions for the view alias: it must accept both the basic and
* starter pack view from either world, because both have live producers. These * the full starter pack view. Compile-time only - a failure surfaces as a
* are compile-time only - a failure surfaces as a typecheck error. * typecheck error.
*/ */
type Assignable<From, To> = From extends To ? true : false type Assignable<From, To> = From extends To ? true : false
type Expect<T extends true> = T type Expect<T extends true> = T
type _AcceptsNewBasicView = Expect< type _AcceptsBasicView = Expect<
Assignable<app.bsky.graph.defs.StarterPackViewBasic, AnyStarterPackView> Assignable<app.bsky.graph.defs.StarterPackViewBasic, AnyStarterPackView>
> >
type _AcceptsNewFullView = Expect< type _AcceptsFullView = Expect<
Assignable<app.bsky.graph.defs.StarterPackView, AnyStarterPackView> Assignable<app.bsky.graph.defs.StarterPackView, AnyStarterPackView>
> >
type _AcceptsOldBasicView = Expect<
Assignable<AppBskyGraphDefs.StarterPackViewBasic, AnyStarterPackView>
>
type _AcceptsOldFullView = Expect<
Assignable<AppBskyGraphDefs.StarterPackView, AnyStarterPackView>
>
describe('types/bsky/starterPack guards', () => { describe('types/bsky/starterPack guards', () => {
describe('isBasicView', () => { describe('isBasicView', () => {
+11 -70
View File
@@ -10,71 +10,15 @@ import {
type ValidateOptions, type ValidateOptions,
type ValidationResult, type ValidationResult,
} from '@atproto/lex' } from '@atproto/lex'
import {type ValidationResult as LegacyValidationResult} from '@atproto/lexicon'
export * as post from '#/types/bsky/post' export * as post from '#/types/bsky/post'
export * as profile from '#/types/bsky/profile' export * as profile from '#/types/bsky/profile'
export * as starterPack from '#/types/bsky/starterPack' 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<AppBskyFeedPost.Record>(item, AppBskyFeedPost.isRecord)) {
* // `item` has type `$Typed<AppBskyFeedPost.Record>` here
* }
* ```
*/
export function dangerousIsType<R extends {$type?: string}>(
record: unknown,
identity: <V>(v: V) => v is V & {$type: NonNullable<R['$type']>},
): 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<AppBskyFeedPost.Record>` here
* }
* ```
*/
export function validate<R extends {$type?: string}>(
record: unknown,
validator: (v: unknown) => LegacyValidationResult<R>,
): record is R {
return validator(record).success
}
/* /*
* Two families of helpers live in this file, distinguished by the validation * These helpers operate on the generated lexicon schema objects from
* surface they consume. * '#/lexicons'. The codegen attaches the validation surface directly to each
* * schema, so these helpers take the schema object itself:
* `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:
* *
* ```ts * ```ts
* import {app} from '#/lexicons' * 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 * string; it does NOT assert the rest of the object matches the schema. An
* invalid record with the right `$type` will pass. * invalid record with the right `$type` will pass.
* *
* This is the '#/lexicons' equivalent of {@link dangerousIsType}. For full * For full validation of the object schema, use {@link matches}, {@link parse},
* validation of the object schema, use {@link matches}, {@link parse}, or * or {@link safeParse} from this same file.
* {@link safeParse} from this same file.
* *
* Usage: * Usage:
* ```ts * ```ts
@@ -126,10 +69,9 @@ export function isType<S extends TypedSchema>(
* Deliberately NOT delegating to the schema's `isTypeOf`: the generated * Deliberately NOT delegating to the schema's `isTypeOf`: the generated
* `TypedObjectSchema.isTypeOf` treats a MISSING `$type` as a match * `TypedObjectSchema.isTypeOf` treats a MISSING `$type` as a match
* (maybe-typed semantics), which would let any plain object satisfy any * (maybe-typed semantics), which would let any plain object satisfy any
* def-schema check and break `$type`-discriminated unions. The old * def-schema check and break `$type`-discriminated unions. We require a
* `dangerousIsType` required a present, matching `$type`, and so do we. * present, matching `$type`. The nullish/object guard also returns false
* The nullish/object guard also mirrors the old `is$typed` behavior of * (not throws) for null/undefined input.
* returning false (not throwing) for null/undefined input.
*/ */
return ( return (
value != null && value != null &&
@@ -142,9 +84,8 @@ export function isType<S extends TypedSchema>(
* Fully validates the object against the schema (strict, no coercion), which * Fully validates the object against the schema (strict, no coercion), which
* has a performance cost, and narrows the value on success. * has a performance cost, and narrows the value on success.
* *
* This is the '#/lexicons' equivalent of {@link validate}. For faster checks * For faster checks with data we trust, like that from our app view, use
* with data we trust, like that from our app view, use {@link isType} from this * {@link isType} from this same file.
* same file.
* *
* Usage: * Usage:
* ```ts * ```ts
@@ -166,7 +107,7 @@ export function matches<S extends Schema>(
/** /**
* Fully validates and parses the value against the schema, returning the typed * 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. * schema transformations such as default values.
* *
* Prefer {@link safeParse} where you want to branch on failure without a * Prefer {@link safeParse} where you want to branch on failure without a

Some files were not shown because too many files have changed in this diff Show More