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