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

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