diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 7e0931c437..6df56e5f78 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -632,11 +632,6 @@ "count": 3 } }, - "src/lib/api/feed/demo.ts": { - "typescript/require-await": { - "count": 2 - } - }, "src/lib/api/feed/posts.ts": { "typescript/require-await": { "count": 1 @@ -1453,11 +1448,6 @@ "count": 1 } }, - "src/state/queries/suggested-follows.ts": { - "no-unused-vars": { - "count": 1 - } - }, "src/state/queries/threadgate/index.ts": { "typescript/no-explicit-any": { "count": 1 diff --git a/package.json b/package.json index 7c1a7756d9..5341b8b199 100644 --- a/package.json +++ b/package.json @@ -332,11 +332,14 @@ "^multiformats/cid$": "/node_modules/multiformats/dist/src/cid.js", "^multiformats/bases/base32$": "/node_modules/multiformats/dist/src/bases/base32.js", "^multiformats/hashes/digest$": "/node_modules/multiformats/dist/src/hashes/digest.js", + "^multiformats/hashes/hasher$": "/node_modules/multiformats/dist/src/hashes/hasher.js", "^multiformats/hashes/sha2$": "/node_modules/multiformats/dist/src/hashes/sha2.js", "^uint8arrays/from-string$": "/node_modules/uint8arrays/dist/src/from-string.js", "^uint8arrays/to-string$": "/node_modules/uint8arrays/dist/src/to-string.js", "^unicode-segmenter/grapheme$": "/node_modules/unicode-segmenter/grapheme.cjs", - "^await-lock$": "/node_modules/await-lock/build/AwaitLock.js" + "^await-lock$": "/node_modules/await-lock/build/AwaitLock.js", + "^@ipld/dag-cbor$": "/node_modules/@ipld/dag-cbor/src/index.js", + "^cborg$": "/node_modules/cborg/cborg.js" }, "transformIgnorePatterns": [ "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|nanoid|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo|bcp-47-match|@atproto/.*|@bsky.app/sdk|tlds|multiformats|uint8arrays|@ipld/.*|cborg|await-lock)" diff --git a/src/components/Autocomplete/useAutocomplete/index.ts b/src/components/Autocomplete/useAutocomplete/index.ts index 498cc766cd..211c202fd8 100644 --- a/src/components/Autocomplete/useAutocomplete/index.ts +++ b/src/components/Autocomplete/useAutocomplete/index.ts @@ -1,5 +1,5 @@ import {useCallback, useMemo} from 'react' -import {moderateProfile, type ModerationOpts} from '@atproto/api' +import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation' import {keepPreviousData, useQuery} from '@tanstack/react-query' import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation' @@ -13,6 +13,7 @@ import { type AutocompleteItemType, type AutocompleteProfile, } from '#/components/Autocomplete/types' +import {toLex} from '#/types/bsky' import {useEmojiSearch} from './useEmojiSearch' const DEFAULT_MOD_OPTS = { @@ -131,7 +132,10 @@ function moderateProfileItem({ item: AutocompleteProfile moderationOpts: ModerationOpts }) { - const modui = moderateProfile(item.profile, moderationOpts).ui('profileList') + // TODO(phase4): drop toLex once searchActorsTypeahead emits #/lexicons views + const modui = moderateProfile(toLex(item.profile), moderationOpts).ui( + 'profileList', + ) const isExactMatch = query && item.profile.handle.toLowerCase() === query if ( diff --git a/src/components/AvatarBubbles.tsx b/src/components/AvatarBubbles.tsx index 49ac4d006f..7cbfe1025f 100644 --- a/src/components/AvatarBubbles.tsx +++ b/src/components/AvatarBubbles.tsx @@ -8,7 +8,7 @@ import Animated, { withDelay, withTiming, } from 'react-native-reanimated' -import {moderateProfile} from '@atproto/api' +import {moderateProfile} from '@bsky.app/sdk/moderation' import {useMaybeProfileShadow} from '#/state/cache/profile-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -17,6 +17,7 @@ import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' import {Person_Filled_Corner2_Rounded as PersonIcon} from '#/components/icons/Person' import type * as bsky from '#/types/bsky' +import {toLex} from '#/types/bsky' type Layout = { size: number @@ -162,7 +163,10 @@ function AvatarBubble({ type="user" hideLiveBadge noBorder - moderation={moderateProfile(profile, moderationOpts).ui('avatar')} + // TODO(phase4): drop toLex once profile props emit #/lexicons views + moderation={moderateProfile(toLex(profile), moderationOpts).ui( + 'avatar', + )} /> ) : ( diff --git a/src/components/AvatarStack.tsx b/src/components/AvatarStack.tsx index ae42a7470b..c24f058c70 100644 --- a/src/components/AvatarStack.tsx +++ b/src/components/AvatarStack.tsx @@ -1,5 +1,5 @@ import {View} from 'react-native' -import {moderateProfile} from '@atproto/api' +import {moderateProfile} from '@bsky.app/sdk/moderation' import {logger} from '#/logger' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -7,6 +7,7 @@ import {useProfilesQuery} from '#/state/queries/profile' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' import type * as bsky from '#/types/bsky' +import {toLex} from '#/types/bsky' export function AvatarStack({ profiles, @@ -34,7 +35,8 @@ export function AvatarStack({ : profiles.map(item => ({ key: item.did, profile: item, - moderation: moderateProfile(item, moderationOpts), + // TODO(phase4): drop toLex once useProfilesQuery emits #/lexicons views + moderation: moderateProfile(toLex(item), moderationOpts), })) return ( diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx index efd14be4ba..c204bb1cb2 100644 --- a/src/components/FeedCard.tsx +++ b/src/components/FeedCard.tsx @@ -1,11 +1,8 @@ import {useCallback, useEffect, useMemo} from 'react' import {type GestureResponderEvent, View} from 'react-native' -import { - type AppBskyFeedDefs, - type AppBskyGraphDefs, - AtUri, - RichText as RichTextApi, -} from '@atproto/api' +import {type AppBskyFeedDefs, type AppBskyGraphDefs} from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {RichText as RichTextApi} from '@bsky.app/sdk/richtext' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx index 502b08252f..03ea92b0b2 100644 --- a/src/components/KnownFollowers.tsx +++ b/src/components/KnownFollowers.tsx @@ -1,10 +1,7 @@ import {useRef} from 'react' import {View} from 'react-native' -import { - type AppBskyActorDefs, - moderateProfile, - type ModerationOpts, -} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {makeProfileLink} from '#/lib/routes/links' @@ -14,6 +11,7 @@ import {atoms as a, useTheme} from '#/alf' import {Link, type LinkProps} from '#/components/Link' import {Text} from '#/components/Typography' import type * as bsky from '#/types/bsky' +import {toLex} from '#/types/bsky' const AVI_SIZE = 30 const AVI_SIZE_SMALL = 20 @@ -96,7 +94,8 @@ function KnownFollowersInner({ const textStyle = [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium] const slice = cachedKnownFollowers.followers.slice(0, 3).map(f => { - const moderation = moderateProfile(f, moderationOpts) + // TODO(phase4): drop toLex once KnownFollowers emits #/lexicons views + const moderation = moderateProfile(toLex(f), moderationOpts) return { profile: { ...f, diff --git a/src/components/ListCard.tsx b/src/components/ListCard.tsx index 805c365566..c6d945419c 100644 --- a/src/components/ListCard.tsx +++ b/src/components/ListCard.tsx @@ -1,11 +1,8 @@ import {useEffect, useMemo} from 'react' import {View} from 'react-native' -import { - type AppBskyGraphDefs, - AtUri, - moderateUserList, - type ModerationUI, -} from '@atproto/api' +import {type AppBskyGraphDefs} from '@atproto/api' +import {AtUri} from '@atproto/syntax' +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' @@ -27,6 +24,7 @@ import {Link as InternalLink, type LinkProps} from '#/components/Link' import * as Hider from '#/components/moderation/Hider' import {Text} from '#/components/Typography' import type * as bsky from '#/types/bsky' +import {toLex} from '#/types/bsky' /* * This component is based on `FeedCard` and is tightly coupled with that @@ -57,7 +55,8 @@ export function Default( const {view, showPinButton} = props const moderationOpts = useModerationOpts() const moderation = moderationOpts - ? moderateUserList(view, moderationOpts) + ? // TODO(phase4): drop toLex once ListView props emit #/lexicons views + moderateUserList(toLex(view), moderationOpts) : undefined return ( diff --git a/src/components/NewskieDialog.tsx b/src/components/NewskieDialog.tsx index 5b17ec4451..1ae53839ec 100644 --- a/src/components/NewskieDialog.tsx +++ b/src/components/NewskieDialog.tsx @@ -1,6 +1,7 @@ import {useMemo, useState} from 'react' import {View} from 'react-native' -import {type AppBskyActorDefs, moderateProfile} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {moderateProfile} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -19,6 +20,7 @@ import {Newskie} from '#/components/icons/Newskie' import * as StarterPackCard from '#/components/StarterPack/StarterPackCard' import {Text} from '#/components/Typography' import {IS_NATIVE} from '#/env' +import {toLex} from '#/types/bsky' export function NewskieDialog({ profile, @@ -88,7 +90,8 @@ function DialogInner({ const profileName = useMemo(() => { if (!moderationOpts) return profile.displayName || profile.handle - const moderation = moderateProfile(profile, moderationOpts) + // TODO(phase4): drop toLex once ProfileViewDetailed prop emits #/lexicons views + const moderation = moderateProfile(toLex(profile), moderationOpts) return sanitizeDisplayName( profile.displayName || profile.handle, moderation.ui('displayName'), diff --git a/src/components/Pills.tsx b/src/components/Pills.tsx index c24e51047c..4c96b4ca42 100644 --- a/src/components/Pills.tsx +++ b/src/components/Pills.tsx @@ -1,6 +1,7 @@ import {useMemo} from 'react' import {View} from 'react-native' -import {BSKY_LABELER_DID, type ModerationCause} from '@atproto/api' +import {api} from '@bsky.app/sdk' +import {type ModerationCause} from '@bsky.app/sdk/moderation' import {Trans} from '@lingui/react/macro' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' @@ -65,7 +66,7 @@ export function Label({ const desc = useModerationCauseDescription(cause) const isLabeler = Boolean(desc.sourceType && desc.sourceDid) const isBlueskyLabel = - desc.sourceType === 'labeler' && desc.sourceDid === BSKY_LABELER_DID + desc.sourceType === 'labeler' && desc.sourceDid === api.moderation.did const avi = size === 'lg' ? 16 : 12 return ( diff --git a/src/components/Post/Embed/FeedEmbed.tsx b/src/components/Post/Embed/FeedEmbed.tsx index a726c313e1..56de18c6c8 100644 --- a/src/components/Post/Embed/FeedEmbed.tsx +++ b/src/components/Post/Embed/FeedEmbed.tsx @@ -1,10 +1,11 @@ import {useMemo} from 'react' -import {moderateFeedGenerator} from '@atproto/api' +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' import {ContentHider} from '#/components/moderation/ContentHider' +import {toLex} from '#/types/bsky' import {type EmbedType} from '#/types/bsky/post' import {type CommonProps} from './types' @@ -40,7 +41,8 @@ export function ModeratedFeedEmbed({ const moderationOpts = useModerationOpts() const moderation = useMemo(() => { return moderationOpts - ? moderateFeedGenerator(embed.view, moderationOpts) + ? // TODO(phase4): drop toLex once feed embed view emits #/lexicons views + moderateFeedGenerator(toLex(embed.view), moderationOpts) : undefined }, [embed.view, moderationOpts]) return ( diff --git a/src/components/Post/Embed/ListEmbed.tsx b/src/components/Post/Embed/ListEmbed.tsx index 47c5b2c287..4e3bb58613 100644 --- a/src/components/Post/Embed/ListEmbed.tsx +++ b/src/components/Post/Embed/ListEmbed.tsx @@ -1,10 +1,11 @@ import {useMemo} from 'react' -import {moderateUserList} from '@atproto/api' +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' import {ContentHider} from '#/components/moderation/ContentHider' +import {toLex} from '#/types/bsky' import {type EmbedType} from '#/types/bsky/post' import {type CommonProps} from './types' @@ -30,7 +31,8 @@ export function ModeratedListEmbed({ const moderationOpts = useModerationOpts() const moderation = useMemo(() => { return moderationOpts - ? moderateUserList(embed.view, moderationOpts) + ? // TODO(phase4): drop toLex once list embed view emits #/lexicons views + moderateUserList(toLex(embed.view), moderationOpts) : undefined }, [embed.view, moderationOpts]) return ( diff --git a/src/components/Post/Embed/StandardSiteEmbed/StandardSiteMetaRow.tsx b/src/components/Post/Embed/StandardSiteEmbed/StandardSiteMetaRow.tsx index 837f1b4a05..d2ee4cf545 100644 --- a/src/components/Post/Embed/StandardSiteEmbed/StandardSiteMetaRow.tsx +++ b/src/components/Post/Embed/StandardSiteEmbed/StandardSiteMetaRow.tsx @@ -1,6 +1,6 @@ import {Fragment, type ReactNode} from 'react' import {View} from 'react-native' -import {AtUri} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {Trans} from '@lingui/react/macro' import {toNiceDomain} from '#/lib/strings/url-helpers' diff --git a/src/components/Post/Embed/StandardSiteEmbed/index.tsx b/src/components/Post/Embed/StandardSiteEmbed/index.tsx index 64e93e4c68..8f7ee892af 100644 --- a/src/components/Post/Embed/StandardSiteEmbed/index.tsx +++ b/src/components/Post/Embed/StandardSiteEmbed/index.tsx @@ -1,6 +1,6 @@ import {type StyleProp, View, type ViewStyle} from 'react-native' import {Image} from 'expo-image' -import {AtUri} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro' diff --git a/src/components/Post/Embed/StandardSiteEmbed/utils.ts b/src/components/Post/Embed/StandardSiteEmbed/utils.ts index 2338ca59a3..d969fa077e 100644 --- a/src/components/Post/Embed/StandardSiteEmbed/utils.ts +++ b/src/components/Post/Embed/StandardSiteEmbed/utils.ts @@ -1,8 +1,8 @@ import { type AppBskyEmbedExternal, - AtUri, type ComAtprotoRepoStrongRef, } from '@atproto/api' +import {AtUri} from '@atproto/syntax' export function isStandardSiteDocumentUri(ref: ComAtprotoRepoStrongRef.Main) { return new AtUri(ref.uri).collection.startsWith('site.standard.document') diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx index c715dbd3ad..1bf56e5197 100644 --- a/src/components/Post/Embed/index.tsx +++ b/src/components/Post/Embed/index.tsx @@ -1,13 +1,10 @@ import {useCallback, useMemo} from 'react' import {View} from 'react-native' -import { - type $Typed, - type AppBskyFeedDefs, - AppBskyFeedPost, - AtUri, - moderatePost, - RichText as RichTextAPI, -} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type $Typed} from '@atproto/lex' +import {AtUri} from '@atproto/syntax' +import {moderatePost} from '@bsky.app/sdk/moderation' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Trans} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' @@ -28,6 +25,7 @@ import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/uti import {RichText} from '#/components/RichText' import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard' import {SubtleHover} from '#/components/SubtleHover' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' import { type Embed as TEmbed, @@ -271,7 +269,9 @@ export function QuoteEmbed({ [embed], ) const moderation = useMemo(() => { - return moderationOpts ? moderatePost(quote, moderationOpts) : undefined + return moderationOpts + ? moderatePost(bsky.toLex(quote), moderationOpts) + : undefined }, [quote, moderationOpts]) const t = useTheme() @@ -281,13 +281,7 @@ export function QuoteEmbed({ const itemTitle = `Post by ${quote.author.handle}` const richText = useMemo(() => { - if ( - !bsky.dangerousIsType( - quote.record, - AppBskyFeedPost.isRecord, - ) - ) - return undefined + if (!bsky.isType(app.bsky.feed.post, quote.record)) return undefined const {text, facets} = quote.record return text.trim() ? new RichTextAPI({text: text, facets: facets}) diff --git a/src/components/Post/Embed/types.ts b/src/components/Post/Embed/types.ts index 0ef5569c7d..41031fdf05 100644 --- a/src/components/Post/Embed/types.ts +++ b/src/components/Post/Embed/types.ts @@ -1,5 +1,6 @@ import {type StyleProp, type ViewStyle} from 'react-native' -import {type AppBskyFeedDefs, type ModerationDecision} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type ModerationDecision} from '@bsky.app/sdk/moderation' export enum PostEmbedViewContext { ThreadHighlighted = 'ThreadHighlighted', diff --git a/src/components/Post/Translated/index.tsx b/src/components/Post/Translated/index.tsx index 94e0ea697a..6321a0d135 100644 --- a/src/components/Post/Translated/index.tsx +++ b/src/components/Post/Translated/index.tsx @@ -1,6 +1,6 @@ import {useCallback, useMemo} from 'react' import {Platform, type StyleProp, type TextStyle, View} from 'react-native' -import {type AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api' +import {type AppBskyFeedDefs, type AppBskyFeedPost} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {HITSLOP_30} from '#/lib/constants' @@ -28,6 +28,7 @@ import * as Select from '#/components/Select' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' const X_ICON_OFFSET = 16 @@ -47,10 +48,7 @@ export function TranslatedPost({ }) const record = useMemo(() => { - return bsky.dangerousIsType( - post.record, - AppBskyFeedPost.isRecord, - ) + return bsky.isType(app.bsky.feed.post, post.record) ? post.record : undefined }, [post]) diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx index 7ed925405a..108b220361 100644 --- a/src/components/PostControls/PostMenu/PostMenuItems.tsx +++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx @@ -10,9 +10,9 @@ import { type AppBskyFeedDefs, type AppBskyFeedPost, type AppBskyFeedThreadgate, - AtUri, - type RichText as RichTextAPI, } from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' diff --git a/src/components/PostControls/PostMenu/index.tsx b/src/components/PostControls/PostMenu/index.tsx index a744efbaa8..c0826683b6 100644 --- a/src/components/PostControls/PostMenu/index.tsx +++ b/src/components/PostControls/PostMenu/index.tsx @@ -4,8 +4,8 @@ import { type AppBskyFeedDefs, type AppBskyFeedPost, type AppBskyFeedThreadgate, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {useLingui} from '@lingui/react/macro' import {type Shadow} from '#/state/cache/post-shadow' diff --git a/src/components/PostControls/ShareMenu/RecentChats.tsx b/src/components/PostControls/ShareMenu/RecentChats.tsx index bab3cbfded..50afb298a1 100644 --- a/src/components/PostControls/ShareMenu/RecentChats.tsx +++ b/src/components/PostControls/ShareMenu/RecentChats.tsx @@ -1,9 +1,6 @@ import {ScrollView, View} from 'react-native' -import { - type ChatBskyActorDefs, - moderateProfile, - type ModerationOpts, -} from '@atproto/api' +import {type ChatBskyActorDefs} from '@atproto/api' +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' @@ -25,6 +22,7 @@ import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util' import {ProfileBadges} from '#/components/ProfileBadges' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +import {toLex} from '#/types/bsky' export function RecentChats({ postUri, @@ -123,7 +121,8 @@ function RecentChatItem({ const primaryProfile = useProfileShadow(primaryMember) - const moderation = moderateProfile(primaryProfile, moderationOpts) + // TODO(phase4): drop toLex once useProfileShadow emits #/lexicons views + const moderation = moderateProfile(toLex(primaryProfile), moderationOpts) const name = convo.kind === 'group' ? convo.details.name diff --git a/src/components/PostControls/ShareMenu/ShareMenuItems.tsx b/src/components/PostControls/ShareMenu/ShareMenuItems.tsx index dc66340fd3..716e1ec851 100644 --- a/src/components/PostControls/ShareMenu/ShareMenuItems.tsx +++ b/src/components/PostControls/ShareMenu/ShareMenuItems.tsx @@ -1,6 +1,6 @@ import {memo, useMemo} from 'react' import * as ExpoClipboard from 'expo-clipboard' -import {AtUri} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' diff --git a/src/components/PostControls/ShareMenu/ShareMenuItems.types.tsx b/src/components/PostControls/ShareMenu/ShareMenuItems.types.tsx index 5bc2a8fb6e..ef93865513 100644 --- a/src/components/PostControls/ShareMenu/ShareMenuItems.types.tsx +++ b/src/components/PostControls/ShareMenu/ShareMenuItems.types.tsx @@ -3,8 +3,8 @@ import { type AppBskyFeedDefs, type AppBskyFeedPost, type AppBskyFeedThreadgate, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {type Shadow} from '#/state/cache/post-shadow' diff --git a/src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx b/src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx index 1950be0c4b..fb2fd20988 100644 --- a/src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx +++ b/src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx @@ -1,5 +1,5 @@ import {memo, useMemo} from 'react' -import {AtUri} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' diff --git a/src/components/PostControls/ShareMenu/index.tsx b/src/components/PostControls/ShareMenu/index.tsx index efac6a3f01..5c354770c9 100644 --- a/src/components/PostControls/ShareMenu/index.tsx +++ b/src/components/PostControls/ShareMenu/index.tsx @@ -4,9 +4,9 @@ import { type AppBskyFeedDefs, type AppBskyFeedPost, type AppBskyFeedThreadgate, - AtUri, - type RichText as RichTextAPI, } from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' diff --git a/src/components/PostControls/index.tsx b/src/components/PostControls/index.tsx index 1f6be5ab6c..88f30b0561 100644 --- a/src/components/PostControls/index.tsx +++ b/src/components/PostControls/index.tsx @@ -4,8 +4,8 @@ import { type AppBskyFeedDefs, type AppBskyFeedPost, type AppBskyFeedThreadgate, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro' diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index 7b4d75164d..d2b18e7b1c 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -6,11 +6,8 @@ import { View, type ViewStyle, } from 'react-native' -import { - moderateProfile, - type ModerationOpts, - RichText as RichTextApi, -} from '@atproto/api' +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' @@ -47,7 +44,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {type Metrics} from '#/analytics' import {useActorStatus} from '#/features/liveNow' -import type * as bsky from '#/types/bsky' +import * as bsky from '#/types/bsky' export function Default({ profile, @@ -173,7 +170,7 @@ export function Avatar({ liveOverride?: boolean size?: number }) { - const moderation = moderateProfile(profile, moderationOpts) + const moderation = moderateProfile(bsky.toLex(profile), moderationOpts) const {isActive: live} = useActorStatus(profile) @@ -243,7 +240,7 @@ function InlineNameAndHandle({ moderationOpts: ModerationOpts }) { const t = useTheme() - const moderation = moderateProfile(profile, moderationOpts) + const moderation = moderateProfile(bsky.toLex(profile), moderationOpts) const name = sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName'), @@ -297,7 +294,7 @@ export function Name({ style?: StyleProp textStyle?: StyleProp }) { - const moderation = moderateProfile(profile, moderationOpts) + const moderation = moderateProfile(bsky.toLex(profile), moderationOpts) const name = sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName'), @@ -482,7 +479,7 @@ export function FollowButtonInner({ }: FollowButtonProps) { const {t: l} = useLingui() const profile = useProfileShadow(profileUnshadowed) - const moderation = moderateProfile(profile, moderationOpts) + const moderation = moderateProfile(bsky.toLex(profile), moderationOpts) const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( profile, logContext, @@ -620,7 +617,7 @@ export function Labels({ profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts }) { - const moderation = moderateProfile(profile, moderationOpts) + const moderation = moderateProfile(bsky.toLex(profile), moderationOpts) const modui = moderation.ui('profileList') const followedBy = profile.viewer?.followedBy diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx index 46dade016b..0fcd9fc113 100644 --- a/src/components/ProfileHoverCard/index.web.tsx +++ b/src/components/ProfileHoverCard/index.web.tsx @@ -1,10 +1,7 @@ import {memo, useCallback, useEffect, useMemo, useReducer, useRef} from 'react' import {View} from 'react-native' -import { - type AppBskyActorDefs, - moderateProfile, - type ModerationOpts, -} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +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' @@ -42,6 +39,7 @@ import {Text} from '#/components/Typography' import {IS_WEB_TOUCH_DEVICE} from '#/env' import {useActorStatus} from '#/features/liveNow' import {LiveStatus} from '#/features/liveNow/components/LiveStatusDialog' +import {toLex} from '#/types/bsky' import {type ProfileHoverCardProps} from './types' const floatingMiddlewares = [ @@ -426,7 +424,8 @@ function Inner({ const {_, i18n} = useLingui() const {currentAccount} = useSession() const moderation = useMemo( - () => moderateProfile(profile, moderationOpts), + // TODO(phase4): drop toLex once ProfileViewDetailed prop emits #/lexicons views + () => moderateProfile(toLex(profile), moderationOpts), [profile, moderationOpts], ) const [descriptionRT] = useRichText(profile.description ?? '') diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx index 0ee575fdec..3ff4569964 100644 --- a/src/components/ProgressGuide/FollowDialog.tsx +++ b/src/components/ProgressGuide/FollowDialog.tsx @@ -1,6 +1,6 @@ import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react' import {TextInput, View, type ViewToken} from 'react-native' -import {type ModerationOpts} from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {Trans, useLingui} from '@lingui/react/macro' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' diff --git a/src/components/StarterPack/Main/ProfilesList.tsx b/src/components/StarterPack/Main/ProfilesList.tsx index c087f5fa46..7c10cafa15 100644 --- a/src/components/StarterPack/Main/ProfilesList.tsx +++ b/src/components/StarterPack/Main/ProfilesList.tsx @@ -1,11 +1,8 @@ import {forwardRef, useCallback, useImperativeHandle, useState} from 'react' import {type ListRenderItemInfo, View} from 'react-native' -import { - type AppBskyActorDefs, - type AppBskyGraphGetList, - AtUri, - type ModerationOpts, -} from '@atproto/api' +import {type AppBskyActorDefs, type AppBskyGraphGetList} from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import { type InfiniteData, type UseInfiniteQueryResult, diff --git a/src/components/StarterPack/QrCode.tsx b/src/components/StarterPack/QrCode.tsx index 4b27ff6649..7a1d47660f 100644 --- a/src/components/StarterPack/QrCode.tsx +++ b/src/components/StarterPack/QrCode.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native' // @ts-expect-error missing types import QRCode from 'react-native-qrcode-styled' import type ViewShot from 'react-native-view-shot' -import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' +import {type AppBskyGraphDefs} from '@atproto/api' import {Trans} from '@lingui/react/macro' import {Logo} from '#/view/icons/Logo' @@ -12,6 +12,7 @@ import {atoms as a, useTheme} from '#/alf' import {LinearGradientBackground} from '#/components/LinearGradientBackground' import {Text} from '#/components/Typography' import {IS_WEB} from '#/env' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' const LazyViewShot = lazy( @@ -30,12 +31,7 @@ export function QrCode({ }) { const {record} = starterPack - if ( - !bsky.dangerousIsType( - record, - AppBskyGraphStarterpack.isRecord, - ) - ) { + if (!bsky.isType(app.bsky.graph.starterpack, record)) { return null } diff --git a/src/components/StarterPack/QrCodeDialog.tsx b/src/components/StarterPack/QrCodeDialog.tsx index e239eee9a4..f5e16835b6 100644 --- a/src/components/StarterPack/QrCodeDialog.tsx +++ b/src/components/StarterPack/QrCodeDialog.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native' import type ViewShot from 'react-native-view-shot' import {requestPermissionsAsync, saveToLibraryAsync} from 'expo-media-library' import * as Sharing from 'expo-sharing' -import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' +import {type AppBskyGraphDefs} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -21,6 +21,7 @@ import {QrCode} from '#/components/StarterPack/QrCode' import * as Toast from '#/components/Toast' import {useAnalytics} from '#/analytics' import {IS_NATIVE, IS_WEB} from '#/env' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' export function QrCodeDialog({ @@ -87,12 +88,7 @@ export function QrCodeDialog({ } else { setIsSaveProcessing(true) - if ( - !bsky.validate( - starterPack.record, - AppBskyGraphStarterpack.validateRecord, - ) - ) { + if (!bsky.matches(app.bsky.graph.starterpack, starterPack.record)) { return } diff --git a/src/components/StarterPack/StarterPackCard.tsx b/src/components/StarterPack/StarterPackCard.tsx index 2ddfe642a9..10d5342f51 100644 --- a/src/components/StarterPack/StarterPackCard.tsx +++ b/src/components/StarterPack/StarterPackCard.tsx @@ -1,7 +1,7 @@ import {useMemo} from 'react' import {View} from 'react-native' import {Image} from 'expo-image' -import {AppBskyGraphStarterpack, AtUri} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' @@ -19,6 +19,7 @@ import { type LinkProps as BaseLinkProps, } from '#/components/Link' import {Text} from '#/components/Typography' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' export function Default({ @@ -62,12 +63,7 @@ export function Card({ const t = useTheme() const {currentAccount} = useSession() - if ( - !bsky.dangerousIsType( - record, - AppBskyGraphStarterpack.isRecord, - ) - ) { + if (!bsky.isType(app.bsky.graph.starterpack, record)) { return null } @@ -127,10 +123,7 @@ export function useStarterPackLink({ return { to: `/starter-pack/${handleOrDid}/${rkey}`, - label: bsky.dangerousIsType( - view.record, - AppBskyGraphStarterpack.isRecord, - ) + label: bsky.isType(app.bsky.graph.starterpack, view.record) ? _(msg`Navigate to ${view.record.name}`) : _(msg`Navigate to starter pack`), precache, @@ -154,12 +147,7 @@ export function Link({ return {rkey, handleOrDid: creator.handle || creator.did} }, [starterPack]) - if ( - !bsky.dangerousIsType( - record, - AppBskyGraphStarterpack.isRecord, - ) - ) { + if (!bsky.isType(app.bsky.graph.starterpack, record)) { return null } diff --git a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx index d0c5d546db..498300491d 100644 --- a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx +++ b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx @@ -1,11 +1,8 @@ import {useRef} from 'react' import {type ListRenderItemInfo} from 'react-native' import {View} from 'react-native' -import { - type AppBskyActorDefs, - type AppBskyFeedDefs, - type ModerationOpts, -} from '@atproto/api' +import {type AppBskyActorDefs, type AppBskyFeedDefs} from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/components/StarterPack/Wizard/WizardListCard.tsx b/src/components/StarterPack/Wizard/WizardListCard.tsx index b48c656a08..0d145af1d9 100644 --- a/src/components/StarterPack/Wizard/WizardListCard.tsx +++ b/src/components/StarterPack/Wizard/WizardListCard.tsx @@ -1,12 +1,11 @@ import {Keyboard, View} from 'react-native' +import {type AppBskyActorDefs, type AppBskyFeedDefs} from '@atproto/api' import { - type AppBskyActorDefs, - type AppBskyFeedDefs, moderateFeedGenerator, moderateProfile, type ModerationOpts, type ModerationUI, -} from '@atproto/api' +} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -27,6 +26,7 @@ import {Checkbox} from '#/components/forms/Toggle' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import type * as bsky from '#/types/bsky' +import {toLex} from '#/types/bsky' function WizardListCard({ type, @@ -140,7 +140,10 @@ export function WizardProfileCard({ const included = isTarget || state.profiles.some(p => p.did === profile.did) const disabled = isTarget || (!included && state.profiles.length >= STARTER_PACK_MAX_SIZE) - const moderationUi = moderateProfile(profile, moderationOpts).ui('avatar') + // TODO(phase4): drop toLex once profile prop emits #/lexicons views + const moderationUi = moderateProfile(toLex(profile), moderationOpts).ui( + 'avatar', + ) const displayName = profile.displayName ? sanitizeDisplayName(profile.displayName) : `@${sanitizeHandle(profile.handle)}` @@ -191,9 +194,11 @@ export function WizardFeedCard({ const isDiscover = generator.uri === DISCOVER_FEED_URI const included = isDiscover || state.feeds.some(f => f.uri === generator.uri) const disabled = isDiscover || (!included && state.feeds.length >= 3) - const moderationUi = moderateFeedGenerator(generator, moderationOpts).ui( - 'avatar', - ) + // TODO(phase4): drop toLex once GeneratorView prop emits #/lexicons views + const moderationUi = moderateFeedGenerator( + toLex(generator), + moderationOpts, + ).ui('avatar') const onPress = () => { if (disabled) return diff --git a/src/components/TrendingTopics.tsx b/src/components/TrendingTopics.tsx index 56fe08ccc9..a067545369 100644 --- a/src/components/TrendingTopics.tsx +++ b/src/components/TrendingTopics.tsx @@ -1,5 +1,5 @@ import {useEffect, useMemo} from 'react' -import {type AppBskyUnspeccedDefs, type AtUri} from '@atproto/api' +import {type AtUri} from '@atproto/syntax' import {useLingui} from '@lingui/react/macro' import {PressableScale} from '#/lib/custom-animations/PressableScale' @@ -9,6 +9,7 @@ import {useCallOnce} from '#/lib/once' import {native} from '#/alf' import {Link as InternalLink, type LinkProps} from '#/components/Link' import {type Metrics, useAnalytics} from '#/analytics' +import {type app} from '#/lexicons' export function TrendingTopicLink({ topic: raw, @@ -18,7 +19,7 @@ export function TrendingTopicLink({ children, ...rest }: { - topic: AppBskyUnspeccedDefs.TrendView + topic: app.bsky.unspecced.defs.TrendView metricContext: Metrics['trendingTopic:seen']['context'] rank: number recId?: string @@ -75,7 +76,7 @@ type ParsedTrendingTopic = } export function useTopic( - raw: AppBskyUnspeccedDefs.TrendView, + raw: app.bsky.unspecced.defs.TrendView, ): ParsedTrendingTopic { const {t: l} = useLingui() return useMemo(() => { diff --git a/src/components/VideoPostCard.tsx b/src/components/VideoPostCard.tsx index ba3d83ec16..7a2a5ed02f 100644 --- a/src/components/VideoPostCard.tsx +++ b/src/components/VideoPostCard.tsx @@ -6,9 +6,8 @@ import { type AppBskyActorDefs, AppBskyEmbedVideo, type AppBskyFeedDefs, - AppBskyFeedPost, - type ModerationDecision, } from '@atproto/api' +import {type ModerationDecision} from '@bsky.app/sdk/moderation' import {useLingui} from '@lingui/react/macro' import {sanitizeHandle} from '#/lib/strings/handles' @@ -26,6 +25,7 @@ import {Link} from '#/components/Link' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import * as Hider from '#/components/moderation/Hider' import {Text} from '#/components/Typography' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' function getBlackColor(t: ReturnType) { @@ -78,10 +78,7 @@ export function VideoPostCard({ if (!AppBskyEmbedVideo.isView(embed)) return null const author = post.author - const text = bsky.dangerousIsType( - post.record, - AppBskyFeedPost.isRecord, - ) + const text = bsky.isType(app.bsky.feed.post, post.record) ? post.record?.text : '' const likeCount = post?.likeCount ?? 0 diff --git a/src/components/WhoCanReply.tsx b/src/components/WhoCanReply.tsx index be7636ff4b..f4353a92d1 100644 --- a/src/components/WhoCanReply.tsx +++ b/src/components/WhoCanReply.tsx @@ -6,12 +6,8 @@ import { View, type ViewStyle, } from 'react-native' -import { - type AppBskyFeedDefs, - AppBskyFeedPost, - type AppBskyGraphDefs, - AtUri, -} from '@atproto/api' +import {type AppBskyFeedDefs, type AppBskyGraphDefs} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -38,6 +34,7 @@ import {InlineLinkText} from '#/components/Link' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_NATIVE} from '#/env' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' interface WhoCanReplyProps { @@ -58,10 +55,7 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) { * unexpectedly, we should check to make sure it's for sure the root URI. */ const rootUri = - bsky.dangerousIsType( - post.record, - AppBskyFeedPost.isRecord, - ) && post.record.reply?.root + bsky.isType(app.bsky.feed.post, post.record) && post.record.reply?.root ? post.record.reply.root.uri : post.uri const settings = useMemo(() => { diff --git a/src/components/activity-notifications/SubscribeProfileButton.tsx b/src/components/activity-notifications/SubscribeProfileButton.tsx index ce9dcd05ae..ed05f8cba0 100644 --- a/src/components/activity-notifications/SubscribeProfileButton.tsx +++ b/src/components/activity-notifications/SubscribeProfileButton.tsx @@ -1,5 +1,5 @@ import {useCallback, useEffect, useState} from 'react' -import {type ModerationOpts} from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {Trans, useLingui} from '@lingui/react/macro' import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' diff --git a/src/components/activity-notifications/SubscribeProfileDialog.tsx b/src/components/activity-notifications/SubscribeProfileDialog.tsx index 8a6b8314cd..78c4866d4b 100644 --- a/src/components/activity-notifications/SubscribeProfileDialog.tsx +++ b/src/components/activity-notifications/SubscribeProfileDialog.tsx @@ -3,9 +3,9 @@ import {View} from 'react-native' import { type AppBskyNotificationDefs, type AppBskyNotificationListActivitySubscriptions, - type ModerationOpts, type Un$Typed, } from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/components/ageAssurance/AgeAssuranceAppealDialog.tsx b/src/components/ageAssurance/AgeAssuranceAppealDialog.tsx index 1ee126eb28..9a7db4cdfc 100644 --- a/src/components/ageAssurance/AgeAssuranceAppealDialog.tsx +++ b/src/components/ageAssurance/AgeAssuranceAppealDialog.tsx @@ -1,13 +1,12 @@ import {useState} from 'react' import {View} from 'react-native' -import {ToolsOzoneReportDefs} from '@atproto/api' +import {api} from '@bsky.app/sdk' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useMutation} from '@tanstack/react-query' -import {BLUESKY_MOD_SERVICE_HEADERS} from '#/lib/constants' -import {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession} from '#/state/session' import {atoms as a, useBreakpoints, web} from '#/alf' import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -17,6 +16,8 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {logger} from '#/ageAssurance' import {useAnalytics} from '#/analytics' +import {com, tools} from '#/lexicons' +import {toLex} from '#/types/bsky' export function AgeAssuranceAppealDialog({ control, @@ -42,7 +43,7 @@ function Inner({control}: {control: Dialog.DialogControlProps}) { const ax = useAnalytics() const {currentAccount} = useSession() const {gtPhone} = useBreakpoints() - const agent = useAgent() + const pdsClient = usePdsClient() const [details, setDetails] = useState('') const isInvalid = details.length > 1000 @@ -51,18 +52,18 @@ function Inner({control}: {control: Dialog.DialogControlProps}) { mutationFn: async () => { ax.metric('ageAssurance:appealDialogSubmit', {}) - await agent.createModerationReport( - { - reasonType: ToolsOzoneReportDefs.REASONAPPEAL, + await pdsClient.call( + com.atproto.moderation.createReport, + toLex({ + reasonType: tools.ozone.report.defs.reasonAppeal.value, subject: { $type: 'com.atproto.admin.defs#repoRef', did: currentAccount?.did, }, reason: `AGE_ASSURANCE_INQUIRY: ` + details, - }, + }), { - encoding: 'application/json', - headers: BLUESKY_MOD_SERVICE_HEADERS, + service: api.moderation.service, }, ) }, diff --git a/src/components/ageAssurance/AgeAssuranceInitDialog.tsx b/src/components/ageAssurance/AgeAssuranceInitDialog.tsx index 9d74b0c6dc..445c3e3eed 100644 --- a/src/components/ageAssurance/AgeAssuranceInitDialog.tsx +++ b/src/components/ageAssurance/AgeAssuranceInitDialog.tsx @@ -1,6 +1,5 @@ import {useState} from 'react' import {View} from 'react-native' -import {XRPCError} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -14,6 +13,7 @@ import { import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' import {useTLDs} from '#/lib/hooks/useTLDs' import {isEmailMaybeInvalid} from '#/lib/strings/email' +import {getErrorName, isXrpcError} from '#/lib/xrpc-error' import {type AppLanguage} from '#/locale/languages' import {useLanguagePrefs} from '#/state/preferences' import {useSession} from '#/state/session' @@ -139,13 +139,14 @@ function Inner() { msg`Something went wrong, please try again`, ) - if (e instanceof XRPCError) { - if (e.error === 'InvalidEmail') { + if (isXrpcError(e)) { + const errorName = getErrorName(e) + if (errorName === 'InvalidEmail') { error = _( msg`Please enter a valid, non-temporary email address. You may need to access this email in the future.`, ) ax.metric('ageAssurance:initDialogError', {code: 'InvalidEmail'}) - } else if (e.error === 'DidTooLong') { + } else if (errorName === 'DidTooLong') { error = ( <> diff --git a/src/components/contacts/screens/ViewMatches.tsx b/src/components/contacts/screens/ViewMatches.tsx index 3543c604f5..1b8ed60480 100644 --- a/src/components/contacts/screens/ViewMatches.tsx +++ b/src/components/contacts/screens/ViewMatches.tsx @@ -2,7 +2,7 @@ import {useCallback, useMemo, useRef, useState} from 'react' import {View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' import * as SMS from 'expo-sms' -import {type ModerationOpts} from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' diff --git a/src/components/dialogs/Embed.tsx b/src/components/dialogs/Embed.tsx index 105895ce3d..8a88b15c9d 100644 --- a/src/components/dialogs/Embed.tsx +++ b/src/components/dialogs/Embed.tsx @@ -1,6 +1,7 @@ import {memo, useEffect, useMemo, useState} from 'react' import {View} from 'react-native' -import {type AppBskyActorDefs, type AppBskyFeedPost, AtUri} from '@atproto/api' +import {type AppBskyActorDefs, type AppBskyFeedPost} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/components/dialogs/PostInteractionSettingsDialog.tsx b/src/components/dialogs/PostInteractionSettingsDialog.tsx index 8a5c7101cc..62d3e41ad3 100644 --- a/src/components/dialogs/PostInteractionSettingsDialog.tsx +++ b/src/components/dialogs/PostInteractionSettingsDialog.tsx @@ -1,10 +1,7 @@ import {useCallback, useMemo, useState} from 'react' import {LayoutAnimation, Text as NestedText, View} from 'react-native' -import { - type AppBskyFeedDefs, - type AppBskyFeedPostgate, - AtUri, -} from '@atproto/api' +import {type AppBskyFeedDefs, type AppBskyFeedPostgate} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' diff --git a/src/components/dialogs/SearchablePeopleList.tsx b/src/components/dialogs/SearchablePeopleList.tsx index cf106684a4..ed509d9d6c 100644 --- a/src/components/dialogs/SearchablePeopleList.tsx +++ b/src/components/dialogs/SearchablePeopleList.tsx @@ -7,7 +7,7 @@ import { useState, } from 'react' import {TextInput, View} from 'react-native' -import {moderateProfile, type ModerationOpts} from '@atproto/api' +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' @@ -33,6 +33,7 @@ import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' import {IS_WEB} from '#/env' import type * as bsky from '#/types/bsky' +import {toLex} from '#/types/bsky' import {AvatarBubbles} from '../AvatarBubbles' import {Error} from '../Error' import {ProfileBadges} from '../ProfileBadges' @@ -410,7 +411,8 @@ function DefaultProfileCard({ const t = useTheme() const {t: l} = useLingui() const enabled = canBeMessaged(profile) - const moderation = moderateProfile(profile, moderationOpts) + // TODO(phase4): drop toLex once profile prop emits #/lexicons views + const moderation = moderateProfile(toLex(profile), moderationOpts) const handle = sanitizeHandle(profile.handle, '@') const displayName = createSanitizedDisplayName( profile, @@ -486,7 +488,8 @@ function ExistingChatCard({ : createSanitizedDisplayName( convo.primaryMember, true, - moderateProfile(convo.primaryMember, moderationOpts).ui( + // TODO(phase4): drop toLex once convo primaryMember emits #/lexicons views + moderateProfile(toLex(convo.primaryMember), moderationOpts).ui( 'displayName', ), ) diff --git a/src/components/dialogs/StarterPackDialog.tsx b/src/components/dialogs/StarterPackDialog.tsx index 2a77292f42..26ac1672bf 100644 --- a/src/components/dialogs/StarterPackDialog.tsx +++ b/src/components/dialogs/StarterPackDialog.tsx @@ -1,9 +1,6 @@ import {useCallback} from 'react' import {View} from 'react-native' -import { - type AppBskyGraphGetStarterPacksWithMembership, - AppBskyGraphStarterpack, -} from '@atproto/api' +import {type AppBskyGraphGetStarterPacksWithMembership} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' @@ -33,6 +30,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' type StarterPackWithMembership = @@ -324,12 +322,7 @@ function StarterPackItem({ const {record} = starterPack - if ( - !bsky.dangerousIsType( - record, - AppBskyGraphStarterpack.isRecord, - ) - ) { + if (!bsky.isType(app.bsky.graph.starterpack, record)) { return null } diff --git a/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx b/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx index 3e74de5c67..4b1c01fa32 100644 --- a/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx +++ b/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx @@ -4,10 +4,10 @@ import { type AppBskyGraphDefs, type AppBskyGraphListitem, type AppBskyGraphStarterpack, - AtUri, type ComAtprotoRepoApplyWrites, } from '@atproto/api' import {TID} from '@atproto/common-web' +import {AtUri} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/components/dialogs/lists/CreateOrEditListDialog.tsx b/src/components/dialogs/lists/CreateOrEditListDialog.tsx index 7164c3ebeb..4ffc954ce5 100644 --- a/src/components/dialogs/lists/CreateOrEditListDialog.tsx +++ b/src/components/dialogs/lists/CreateOrEditListDialog.tsx @@ -1,6 +1,7 @@ import {useCallback, useEffect, useMemo, useState} from 'react' import {View} from 'react-native' -import {type AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api' +import {type AppBskyGraphDefs} from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' @@ -15,7 +16,7 @@ import { useListCreateMutation, useListMetadataMutation, } from '#/state/queries/list' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {EditableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme, web} from '#/alf' @@ -27,6 +28,7 @@ import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {IS_WEB} from '#/env' +import {toLex} from '#/types/bsky' const DISPLAY_NAME_MAX_GRAPHEMES = 64 const DESCRIPTION_MAX_GRAPHEMES = 300 @@ -133,7 +135,7 @@ function DialogInner({ const {_} = useLingui() const t = useTheme() - const agent = useAgent() + const pdsClient = usePdsClient() const control = Dialog.useDialogContext() const { mutateAsync: createListMutation, @@ -163,7 +165,11 @@ 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}), false) + // TODO(phase4): drop toLex once the list view producer emits #/lexicons facets + const serialized = richTextToString( + new RichTextAPI({text, facets: toLex(facets)}), + false, + ) const richText = new RichTextAPI({text: serialized}) richText.detectFacetsWithoutResolution() @@ -224,7 +230,7 @@ function DialogInner({ {cleanNewlines: true}, ) - await richText.detectFacets(agent) + await richText.detectFacets(pdsClient) richText = shortenLinks(richText) richText = stripInvalidMentions(richText) @@ -272,7 +278,7 @@ function DialogInner({ setImageError, activePurpose, isCurateList, - agent, + pdsClient, _, ]) diff --git a/src/components/dialogs/lists/ListAddRemoveUsersDialog.tsx b/src/components/dialogs/lists/ListAddRemoveUsersDialog.tsx index 8129e9bdb4..a8f04e2857 100644 --- a/src/components/dialogs/lists/ListAddRemoveUsersDialog.tsx +++ b/src/components/dialogs/lists/ListAddRemoveUsersDialog.tsx @@ -1,6 +1,7 @@ import {useCallback, useMemo} from 'react' import {View} from 'react-native' -import {type AppBskyGraphDefs, type ModerationOpts} from '@atproto/api' +import {type AppBskyGraphDefs} from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/components/dms/ActionsWrapper.tsx b/src/components/dms/ActionsWrapper.tsx index ce58dba31c..b9782949b6 100644 --- a/src/components/dms/ActionsWrapper.tsx +++ b/src/components/dms/ActionsWrapper.tsx @@ -1,5 +1,6 @@ import {View} from 'react-native' -import {type ChatBskyConvoDefs, type ModerationOpts} from '@atproto/api' +import {type ChatBskyConvoDefs} from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {useLingui} from '@lingui/react/macro' import {MessageContextMenu} from '#/components/dms/MessageContextMenu' diff --git a/src/components/dms/ActionsWrapper.web.tsx b/src/components/dms/ActionsWrapper.web.tsx index 8d67051535..0ef5503370 100644 --- a/src/components/dms/ActionsWrapper.web.tsx +++ b/src/components/dms/ActionsWrapper.web.tsx @@ -1,6 +1,7 @@ import {useCallback, useRef, useState} from 'react' import {Pressable, View} from 'react-native' -import {type ChatBskyConvoDefs, type ModerationOpts} from '@atproto/api' +import {type ChatBskyConvoDefs} from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro' diff --git a/src/components/dms/BlockedByListDialog.tsx b/src/components/dms/BlockedByListDialog.tsx index 2532122087..2521168d79 100644 --- a/src/components/dms/BlockedByListDialog.tsx +++ b/src/components/dms/BlockedByListDialog.tsx @@ -1,6 +1,6 @@ import {Fragment} from 'react' import {View} from 'react-native' -import {type ModerationCause} from '@atproto/api' +import {type ModerationCause} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' diff --git a/src/components/dms/ChatProfileTabs.tsx b/src/components/dms/ChatProfileTabs.tsx index bac128c819..1e6e88217e 100644 --- a/src/components/dms/ChatProfileTabs.tsx +++ b/src/components/dms/ChatProfileTabs.tsx @@ -1,7 +1,7 @@ import {useCallback, useEffect} from 'react' import {type ScrollView, View} from 'react-native' import Animated, {useAnimatedRef, useSharedValue} from 'react-native-reanimated' -import {moderateProfile} from '@atproto/api' +import {moderateProfile} from '@bsky.app/sdk/moderation' import {useLingui} from '@lingui/react/macro' import {HITSLOP_10} from '#/lib/constants' @@ -15,6 +15,7 @@ import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Ti import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' import type * as bsky from '#/types/bsky' +import {toLex} from '#/types/bsky' type Props = { testID?: string @@ -89,7 +90,8 @@ function Tab({ const {t: l} = useLingui() const moderationOpts = useModerationOpts() - const moderation = moderateProfile(profile, moderationOpts!) + // TODO(phase4): drop toLex once profile prop emits #/lexicons views + const moderation = moderateProfile(toLex(profile), moderationOpts!) const displayName = sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName'), diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx index 2eac8cdb3a..9aab1da8c9 100644 --- a/src/components/dms/ConvoMenu.tsx +++ b/src/components/dms/ConvoMenu.tsx @@ -1,6 +1,6 @@ import {memo, useCallback} from 'react' import {Keyboard, View} from 'react-native' -import {type ModerationCause} from '@atproto/api' +import {type ModerationCause} from '@bsky.app/sdk/moderation' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' diff --git a/src/components/dms/InitiateChatFlow.tsx b/src/components/dms/InitiateChatFlow.tsx index 4e0e71c2f4..95dd6045c9 100644 --- a/src/components/dms/InitiateChatFlow.tsx +++ b/src/components/dms/InitiateChatFlow.tsx @@ -7,7 +7,7 @@ import { useState, } from 'react' import {LayoutAnimation, type TextInput, View} from 'react-native' -import {moderateProfile, type ModerationOpts} from '@atproto/api' +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' @@ -54,6 +54,7 @@ import {Text} from '#/components/Typography' import {useAgeAssurance} from '#/ageAssurance' import {IS_NATIVE, IS_WEB} from '#/env' import type * as bsky from '#/types/bsky' +import {toLex} from '#/types/bsky' type NewGroupChatItem = { type: 'newGroupChat' @@ -1070,7 +1071,8 @@ function DefaultProfileCard({ const t = useTheme() const {t: l} = useLingui() const enabled = canBeMessaged(profile) - const moderation = moderateProfile(profile, moderationOpts) + // TODO(phase4): drop toLex once profile prop emits #/lexicons views + const moderation = moderateProfile(toLex(profile), moderationOpts) const handle = sanitizeHandle(profile.handle, '@') const displayName = sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), diff --git a/src/components/dms/MessageContextMenu.tsx b/src/components/dms/MessageContextMenu.tsx index 1640ecff6d..eff369e8dc 100644 --- a/src/components/dms/MessageContextMenu.tsx +++ b/src/components/dms/MessageContextMenu.tsx @@ -2,11 +2,9 @@ import {memo, useCallback} from 'react' import {Platform} from 'react-native' import {type GestureType} from 'react-native-gesture-handler' import * as Clipboard from 'expo-clipboard' -import { - type ChatBskyConvoDefs, - type ModerationOpts, - RichText, -} from '@atproto/api' +import {type ChatBskyConvoDefs} from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' +import {RichText} from '@bsky.app/sdk/richtext' import {plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro' @@ -31,6 +29,7 @@ import * as Toast from '#/components/Toast' import {useAnalytics} from '#/analytics' import {IS_NATIVE} from '#/env' import type * as bsky from '#/types/bsky' +import {toLex} from '#/types/bsky' import {EmojiReactionPicker} from './EmojiReactionPicker' import {canReact, hasReachedReactionLimit} from './util' @@ -88,7 +87,8 @@ export let MessageContextMenu = ({ const str = richTextToString( new RichText({ text: message.text, - facets: message.facets, + // TODO(phase4): drop toLex once the message producer emits #/lexicons facets + facets: toLex(message.facets), }), true, ) diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index 7cf3123cea..bb814f7d6f 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -27,9 +27,9 @@ import { type ChatBskyActorDefs, ChatBskyConvoDefs, ChatBskyEmbedJoinLink, - moderateProfile, - RichText as RichTextAPI, } from '@atproto/api' +import {moderateProfile} from '@bsky.app/sdk/moderation' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {plural} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' @@ -58,6 +58,7 @@ import * as ProfileCard from '#/components/ProfileCard' import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' +import {toLex} from '#/types/bsky' import {DateDivider} from './DateDivider' import {MessageItemEmbed} from './MessageItemEmbed' import {MessageItemInviteEmbed} from './MessageItemInviteEmbed' @@ -335,7 +336,9 @@ let MessageItem = ({ size={AVATAR_SIZE} type={profile.associated?.labeler ? 'labeler' : 'user'} onBeforePress={() => unstableCacheProfileView(queryClient, profile)} - moderation={moderateProfile(profile, moderationOpts).ui('avatar')} + moderation={moderateProfile(toLex(profile), moderationOpts).ui( + 'avatar', + )} /> ) : ( diff --git a/src/components/dms/MessagesListBlockedFooter.tsx b/src/components/dms/MessagesListBlockedFooter.tsx index 1fbee421b0..f239a66aa4 100644 --- a/src/components/dms/MessagesListBlockedFooter.tsx +++ b/src/components/dms/MessagesListBlockedFooter.tsx @@ -1,6 +1,6 @@ import {useCallback, useMemo} from 'react' import {View} from 'react-native' -import {type ModerationDecision} from '@atproto/api' +import {type ModerationDecision} from '@bsky.app/sdk/moderation' import {Trans, useLingui} from '@lingui/react/macro' import {useProfileShadow} from '#/state/cache/profile-shadow' diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index dc42965dac..c35fbc13da 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -1,6 +1,6 @@ import {useMemo} from 'react' import {View} from 'react-native' -import {moderateProfile, type ModerationOpts} from '@atproto/api' +import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation' import {useLingui} from '@lingui/react/macro' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' @@ -21,6 +21,7 @@ import {Link} from '#/components/Link' import {ProfileBadges} from '#/components/ProfileBadges' import {Text} from '#/components/Typography' import {IS_LIQUID_GLASS} from '#/env' +import {toLex} from '#/types/bsky' import {type ConvoWithDetails} from './util' const PFP_SIZE = 40 @@ -85,7 +86,8 @@ function ProfileHeaderReady({ const {t: l} = useLingui() const profile = useProfileShadow(convo.primaryMember) - const moderation = moderateProfile(profile, moderationOpts) + // TODO(phase4): drop toLex once useProfileShadow emits #/lexicons views + const moderation = moderateProfile(toLex(profile), moderationOpts) const blockInfo = useMemo(() => { const modui = moderation.ui('profileView') diff --git a/src/components/dms/components/GroupChatProfileCard.tsx b/src/components/dms/components/GroupChatProfileCard.tsx index 1d704766c2..fc7823f834 100644 --- a/src/components/dms/components/GroupChatProfileCard.tsx +++ b/src/components/dms/components/GroupChatProfileCard.tsx @@ -1,5 +1,5 @@ import {View} from 'react-native' -import {moderateProfile, type ModerationOpts} from '@atproto/api' +import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation' import {Trans} from '@lingui/react/macro' import {sanitizeDisplayName} from '#/lib/strings/display-names' @@ -10,6 +10,7 @@ import * as Toggle from '#/components/forms/Toggle' import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' import type * as bsky from '#/types/bsky' +import {toLex} from '#/types/bsky' export function GroupChatProfileCard({ profile, @@ -20,7 +21,8 @@ export function GroupChatProfileCard({ }) { const t = useTheme() const enabled = canBeAddedToGroup(profile) - const moderation = moderateProfile(profile, moderationOpts) + // TODO(phase4): drop toLex once profile prop emits #/lexicons views + const moderation = moderateProfile(toLex(profile), moderationOpts) const handle = sanitizeHandle(profile.handle, '@') const displayName = sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), diff --git a/src/components/dms/util.ts b/src/components/dms/util.ts index ce0d30a8fd..683b421275 100644 --- a/src/components/dms/util.ts +++ b/src/components/dms/util.ts @@ -1,10 +1,6 @@ -import { - type $Typed, - ChatBskyActorDefs, - ChatBskyConvoDefs, - moderateProfile, - type ModerationOpts, -} from '@atproto/api' +import {type ChatBskyActorDefs, type ChatBskyConvoDefs} from '@atproto/api' +import {type $Typed} from '@atproto/lex' +import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation' import {EMOJI_REACTION_LIMIT} from '#/lib/constants' import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted' @@ -13,6 +9,7 @@ import {type Shadow} from '#/state/cache/profile-shadow' import {type ConvoState, ConvoStatus} from '#/state/messages/convo/types' import {platform} from '#/alf' import {type ReportSubject} from '#/components/moderation/ReportDialog/types' +import {chat} from '#/lexicons' import * as bsky from '#/types/bsky' export const MESSAGE_GAP_THRESHOLD_MS = 60 * 60 * 1000 @@ -158,7 +155,10 @@ export function canReact({ } if (primaryMember && moderationOpts) { - const moderation = moderateProfile(primaryMember, moderationOpts) + const moderation = moderateProfile( + bsky.toLex(primaryMember), + moderationOpts, + ) if (convoState.convo.kind === 'direct') { // Either direction (blocking or blocked-by) hides reactions in 1-1s if (moderation.blocked) return false @@ -206,21 +206,11 @@ export function parseConvoView( convoView: ChatBskyConvoDefs.ConvoView, ownDid: string | undefined, ): ConvoWithDetails | null { - if ( - bsky.dangerousIsType( - convoView.kind, - ChatBskyConvoDefs.isGroupConvo, - ) - ) { + if (bsky.isType(chat.bsky.convo.defs.groupConvo, convoView.kind)) { let owner: GroupConvoMember | undefined = undefined for (const member of convoView.members) { - if ( - bsky.dangerousIsType( - member.kind, - ChatBskyActorDefs.isGroupConvoMember, - ) - ) { + if (bsky.isType(chat.bsky.actor.defs.groupConvoMember, member.kind)) { if (member.kind.role === 'owner') { // have to do a type assertion here // this works: {...member, kind: member.kind} @@ -242,12 +232,7 @@ export function parseConvoView( primaryMember: owner, members: convoView.members as Array, } - } else if ( - bsky.dangerousIsType( - convoView.kind, - ChatBskyConvoDefs.isDirectConvo, - ) - ) { + } else if (bsky.isType(chat.bsky.convo.defs.directConvo, convoView.kind)) { const otherUser = convoView.members.find(m => m.did !== ownDid) if (!otherUser) { @@ -290,7 +275,7 @@ export function getConvoReportSubject( const lastMessage = convo.view.lastMessage const reportableMessage = - ChatBskyConvoDefs.isMessageView(lastMessage) && + bsky.isType(chat.bsky.convo.defs.messageView, lastMessage) && lastMessage.sender?.did !== ownDid ? lastMessage : null diff --git a/src/components/images/Gallery/maybeApplyGalleryOffsetStyles.ts b/src/components/images/Gallery/maybeApplyGalleryOffsetStyles.ts index 6ac2c0086f..860960191c 100644 --- a/src/components/images/Gallery/maybeApplyGalleryOffsetStyles.ts +++ b/src/components/images/Gallery/maybeApplyGalleryOffsetStyles.ts @@ -1,16 +1,10 @@ -import { - AppBskyEmbedGallery, - AppBskyEmbedImages, - AppBskyEmbedRecordWithMedia, - type AppBskyFeedDefs, - AppBskyFeedPost, - type ModerationCause, - type ModerationUI, -} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type ModerationCause, type ModerationUI} from '@bsky.app/sdk/moderation' import {unique} from '#/lib/moderation' import {type AppModerationCause} from '#/components/Pills' import {Features, features} from '#/analytics/features' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' export const POST_META_NO_CONTENT_OFFSET = {paddingTop: 10} @@ -28,12 +22,7 @@ export function maybeApplyGalleryOffsetStyles( additionalCauses?: ModerationCause[] | AppModerationCause[] }, ) { - if ( - !bsky.dangerousIsType( - post.record, - AppBskyFeedPost.isRecord, - ) - ) { + if (!bsky.isType(app.bsky.feed.post, post.record)) { return } @@ -48,24 +37,10 @@ export function maybeApplyGalleryOffsetStyles( * First check if we even have images */ const embed = post.record.embed - const isImageEmbed = - embed && - bsky.dangerousIsType( - embed, - AppBskyEmbedImages.isMain, - ) - const isGalleryEmbed = - embed && - bsky.dangerousIsType( - embed, - AppBskyEmbedGallery.isMain, - ) + const isImageEmbed = embed && bsky.isType(app.bsky.embed.images, embed) + const isGalleryEmbed = embed && bsky.isType(app.bsky.embed.gallery, embed) const isRecordWithMedia = - embed && - bsky.dangerousIsType( - embed, - AppBskyEmbedRecordWithMedia.isMain, - ) + embed && bsky.isType(app.bsky.embed.recordWithMedia, embed) let hasImages = false if (isImageEmbed) { if (!isPostGalleryEmbedEnabled) return @@ -79,22 +54,12 @@ export function maybeApplyGalleryOffsetStyles( hasImages = true } if (isRecordWithMedia) { - if ( - bsky.dangerousIsType( - embed.media, - AppBskyEmbedImages.isMain, - ) - ) { + if (bsky.isType(app.bsky.embed.images, embed.media)) { if (!isPostGalleryEmbedEnabled) return // one image, not a gallery if (embed.media.images.length === 1) return } - if ( - bsky.dangerousIsType( - embed.media, - AppBskyEmbedGallery.isMain, - ) - ) { + if (bsky.isType(app.bsky.embed.gallery, embed.media)) { // single (or empty) gallery - no offset needed if (embed.media.items.length <= 1) return } diff --git a/src/components/intents/GroupChatJoinDialog.tsx b/src/components/intents/GroupChatJoinDialog.tsx index 7c506a634d..ae430b9364 100644 --- a/src/components/intents/GroupChatJoinDialog.tsx +++ b/src/components/intents/GroupChatJoinDialog.tsx @@ -4,8 +4,8 @@ import { ChatBskyGroupDefs, ChatBskyGroupRequestJoin, ChatBskyGroupWithdrawJoinRequest, - moderateProfile, } from '@atproto/api' +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' @@ -49,6 +49,7 @@ import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +import {toLex} from '#/types/bsky' import {ProfileBadges} from '../ProfileBadges' export function GroupChatJoinDialog() { @@ -406,9 +407,11 @@ function GroupChatJoinDialogContent({code}: {code?: string}) { {createSanitizedDisplayName( joinLinkPreview.owner, true, - moderateProfile(joinLinkPreview.owner, moderationOpts).ui( - 'displayName', - ), + // TODO(phase4): drop toLex once join link preview emits #/lexicons views + moderateProfile( + toLex(joinLinkPreview.owner), + moderationOpts, + ).ui('displayName'), )} diff --git a/src/components/interstitials/TrendingVideos.tsx b/src/components/interstitials/TrendingVideos.tsx index d3047001a4..abbbcd56b3 100644 --- a/src/components/interstitials/TrendingVideos.tsx +++ b/src/components/interstitials/TrendingVideos.tsx @@ -1,6 +1,7 @@ import {useCallback, useEffect, useMemo} from 'react' import {ScrollView, View} from 'react-native' -import {AppBskyEmbedVideo, AtUri} from '@atproto/api' +import {AppBskyEmbedVideo} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {Trans, useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' diff --git a/src/components/moderation/AppealForm.tsx b/src/components/moderation/AppealForm.tsx index 5432672d1e..ddf7080089 100644 --- a/src/components/moderation/AppealForm.tsx +++ b/src/components/moderation/AppealForm.tsx @@ -1,7 +1,6 @@ import {useState} from 'react' import {View} from 'react-native' -import {type ComAtprotoLabelDefs, ToolsOzoneReportDefs} from '@atproto/api' -import {XRPCError} from '@atproto/api' +import {type Service} from '@atproto/lex-client' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -11,8 +10,9 @@ import {useLabelSubject} from '#/lib/moderation' import {useLabelInfo} from '#/lib/moderation/useLabelInfo' import {makeProfileLink} from '#/lib/routes/links' import {sanitizeHandle} from '#/lib/strings/handles' +import {getErrorName, isXrpcError} from '#/lib/xrpc-error' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' import {atoms as a, useBreakpoints} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -22,13 +22,15 @@ import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {IS_ANDROID} from '#/env' +import {com, tools} from '#/lexicons' +import {toLex} from '#/types/bsky' export function AppealForm({ label, control, onPressBack, }: { - label: ComAtprotoLabelDefs.Label + label: com.atproto.label.defs.Label control: Dialog.DialogOuterProps['control'] onPressBack: () => void }) { @@ -38,7 +40,7 @@ export function AppealForm({ const [details, setDetails] = useState('') const {subject} = useLabelSubject({label}) const isAccountReport = 'did' in subject - const agent = useAgent() + const pdsClient = usePdsClient() const sourceName = labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src @@ -49,25 +51,23 @@ export function AppealForm({ const $type = !isAccountReport ? 'com.atproto.repo.strongRef' : 'com.atproto.admin.defs#repoRef' - await agent.createModerationReport( - { - reasonType: ToolsOzoneReportDefs.REASONAPPEAL, + await pdsClient.call( + com.atproto.moderation.createReport, + toLex({ + reasonType: tools.ozone.report.defs.reasonAppeal.value, subject: { $type, ...subject, }, reason: details, - }, + }), { - encoding: 'application/json', - headers: { - 'atproto-proxy': `${label.src}#atproto_labeler`, - }, + service: `${label.src}#atproto_labeler` as Service, }, ) }, onError: err => { - if (err instanceof XRPCError && err.error === 'AlreadyAppealed') { + if (isXrpcError(err) && getErrorName(err) === 'AlreadyAppealed') { setError( _( msg`You've already appealed this label and it's being reviewed by our moderation team.`, diff --git a/src/components/moderation/ContentHider.tsx b/src/components/moderation/ContentHider.tsx index cfdc6c7e12..b0f8dddf01 100644 --- a/src/components/moderation/ContentHider.tsx +++ b/src/components/moderation/ContentHider.tsx @@ -5,7 +5,7 @@ import { View, type ViewStyle, } from 'react-native' -import {type ModerationUI} from '@atproto/api' +import {type ModerationUI} from '@bsky.app/sdk/moderation' import {Trans, useLingui} from '@lingui/react/macro' import { diff --git a/src/components/moderation/Hider.tsx b/src/components/moderation/Hider.tsx index 74cc9bd1c8..85d063f088 100644 --- a/src/components/moderation/Hider.tsx +++ b/src/components/moderation/Hider.tsx @@ -1,5 +1,5 @@ import {createContext, useContext, useState} from 'react' -import {type ModerationUI} from '@atproto/api' +import {type ModerationUI} from '@bsky.app/sdk/moderation' import { type ModerationCauseDescription, diff --git a/src/components/moderation/LabelPreference.tsx b/src/components/moderation/LabelPreference.tsx index a654b46e24..80a83c5e6c 100644 --- a/src/components/moderation/LabelPreference.tsx +++ b/src/components/moderation/LabelPreference.tsx @@ -2,7 +2,7 @@ import {View} from 'react-native' import { type InterpretedLabelValueDefinition, type LabelPreference, -} from '@atproto/api' +} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/components/moderation/ModerationDetailsDialog.tsx b/src/components/moderation/ModerationDetailsDialog.tsx index bf19dc0363..1b2351432e 100644 --- a/src/components/moderation/ModerationDetailsDialog.tsx +++ b/src/components/moderation/ModerationDetailsDialog.tsx @@ -1,6 +1,6 @@ import {useState} from 'react' import {View} from 'react-native' -import {type ModerationCause} from '@atproto/api' +import {type ModerationCause} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/components/moderation/PostAlerts.tsx b/src/components/moderation/PostAlerts.tsx index af8329bec5..2bfd40705b 100644 --- a/src/components/moderation/PostAlerts.tsx +++ b/src/components/moderation/PostAlerts.tsx @@ -1,10 +1,6 @@ import {type StyleProp, View, type ViewStyle} from 'react-native' -import { - type AppBskyFeedDefs, - type ComAtprotoLabelDefs, - type ModerationCause, - type ModerationUI, -} from '@atproto/api' +import {type AppBskyFeedDefs, type ComAtprotoLabelDefs} from '@atproto/api' +import {type ModerationCause, type ModerationUI} from '@bsky.app/sdk/moderation' import {plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro' @@ -20,6 +16,7 @@ import { useLabelsOnMeDialogControl, } from '#/components/moderation/LabelsOnMeDialog' import * as Pills from '#/components/Pills' +import {toLex} from '#/types/bsky' export function PostAlerts({ post, @@ -71,7 +68,8 @@ export function PostAlerts({ */ const shownCauses = [...alerts, ...informs, ...modui.blurs] const additionalLabels = filterUserFacingLabels( - allLabels, + // TODO(phase4): drop toLex once PostView labels are #/lexicons-typed + toLex(allLabels), currentAccount?.did, ).filter(label => shownCauses.every( diff --git a/src/components/moderation/PostHider.tsx b/src/components/moderation/PostHider.tsx index 5019b7c3e3..54d4acf945 100644 --- a/src/components/moderation/PostHider.tsx +++ b/src/components/moderation/PostHider.tsx @@ -7,11 +7,8 @@ import { View, type ViewStyle, } from 'react-native' -import { - type AppBskyActorDefs, - type ModerationCause, - type ModerationUI, -} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type ModerationCause, type ModerationUI} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/components/moderation/ProfileHeaderAlerts.tsx b/src/components/moderation/ProfileHeaderAlerts.tsx index f35bf14fe1..b7d3249249 100644 --- a/src/components/moderation/ProfileHeaderAlerts.tsx +++ b/src/components/moderation/ProfileHeaderAlerts.tsx @@ -1,5 +1,5 @@ import {type StyleProp, type ViewStyle} from 'react-native' -import {type ModerationDecision} from '@atproto/api' +import {type ModerationDecision} from '@bsky.app/sdk/moderation' import {getModerationCauseKey, unique} from '#/lib/moderation' import * as Pills from '#/components/Pills' diff --git a/src/components/moderation/ReportDialog/action.ts b/src/components/moderation/ReportDialog/action.ts index ac702abd67..2e3177c47e 100644 --- a/src/components/moderation/ReportDialog/action.ts +++ b/src/components/moderation/ReportDialog/action.ts @@ -1,21 +1,21 @@ -import { - type $Typed, - type ChatBskyConvoDefs, - type ComAtprotoModerationCreateReport, -} from '@atproto/api' +import {type Service} from '@atproto/lex-client' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {useMutation} from '@tanstack/react-query' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' +import {com} from '#/lexicons' +import {toLex} from '#/types/bsky' import {NEW_TO_OLD_REASONS_MAP} from './const' import {type ReportState} from './state' import {type ParsedReportSubject} from './types' +type CreateReportBody = com.atproto.moderation.createReport.$InputBody + export function useSubmitReportMutation() { const {_} = useLingui() - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation({ async mutationFn({ @@ -52,13 +52,17 @@ export function useSubmitReportMutation() { reasonType = backwardsCompatibleReasonType } - let report: - | ComAtprotoModerationCreateReport.InputSchema - | (Omit & { - subject: - | $Typed - | $Typed - }) + /* + * The generated `createReport` subject union only declares repoRef and + * strongRef with branded did/uri strings; chat subjects (message/convo + * refs) are accepted on the wire but not in the lexicon, and the subject + * ids we hold here are plain strings. We build the body against a loose + * subject shape and `toLex` it to the schema body at the call boundary + * (matching the old widened-InputSchema shape). + */ + let report: Omit & { + subject: {$type: string} & Record + } switch (subject.type) { case 'account': { @@ -123,12 +127,19 @@ export function useSubmitReportMutation() { report, }) } else { - await agent.createModerationReport(report, { - encoding: 'application/json', - headers: { - 'atproto-proxy': `${labeler.creator.did}#atproto_labeler`, + /* + * Route the report through the selected labeler's moderation service + * via the `atproto-proxy` header, which lex-client sets from the + * per-call `service` option (previously an explicit header on the + * bridge agent). + */ + await pdsClient.call( + com.atproto.moderation.createReport, + toLex(report), + { + service: `${labeler.creator.did}#atproto_labeler` as Service, }, - }) + ) } }, }) diff --git a/src/components/moderation/ReportDialog/const.ts b/src/components/moderation/ReportDialog/const.ts index 8f5f16db9f..df1bd7d895 100644 --- a/src/components/moderation/ReportDialog/const.ts +++ b/src/components/moderation/ReportDialog/const.ts @@ -1,9 +1,11 @@ -import { - ComAtprotoModerationDefs as RootReportDefs, - ToolsOzoneReportDefs as OzoneReportDefs, -} from '@atproto/api' - import {type ParsedReportSubject} from '#/components/moderation/ReportDialog/types' +import {com, tools} from '#/lexicons' + +const OzoneReportDefs = tools.ozone.report.defs +const RootReportDefs = com.atproto.moderation.defs + +type OzoneReasonType = tools.ozone.report.defs.ReasonType +type RootReasonType = com.atproto.moderation.defs.ReasonType export const DMCA_LINK = 'https://bsky.social/about/support/copyright' export const SUPPORT_PAGE = 'https://bsky.social/about/support' @@ -17,60 +19,87 @@ export const NEW_TO_OLD_REASON_MAPPING: Record = {} * Matches the mapping defined in the Ozone codebase: * @see https://github.com/bluesky-social/atproto/blob/4c15fb47cec26060bff2e710e95869a90c9d7fdd/packages/ozone/src/mod-service/profile.ts#L16-L64 */ -export const NEW_TO_OLD_REASONS_MAP: Record< - OzoneReportDefs.ReasonType, - RootReportDefs.ReasonType -> = { - [OzoneReportDefs.REASONAPPEAL]: RootReportDefs.REASONAPPEAL, - [OzoneReportDefs.REASONOTHER]: RootReportDefs.REASONOTHER, +export const NEW_TO_OLD_REASONS_MAP: Record = { + [OzoneReportDefs.reasonAppeal.value]: RootReportDefs.reasonAppeal.value, + [OzoneReportDefs.reasonOther.value]: RootReportDefs.reasonOther.value, - [OzoneReportDefs.REASONVIOLENCEANIMAL]: RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONVIOLENCETHREATS]: RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONVIOLENCEGRAPHICCONTENT]: - RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONVIOLENCEGLORIFICATION]: RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT]: - RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONVIOLENCETRAFFICKING]: RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONVIOLENCEOTHER]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.reasonViolenceAnimal.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonViolenceThreats.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonViolenceGraphicContent.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonViolenceGlorification.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonViolenceExtremistContent.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonViolenceTrafficking.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonViolenceOther.value]: + RootReportDefs.reasonViolation.value, - [OzoneReportDefs.REASONSEXUALABUSECONTENT]: RootReportDefs.REASONSEXUAL, - [OzoneReportDefs.REASONSEXUALNCII]: RootReportDefs.REASONSEXUAL, - [OzoneReportDefs.REASONSEXUALDEEPFAKE]: RootReportDefs.REASONSEXUAL, - [OzoneReportDefs.REASONSEXUALANIMAL]: RootReportDefs.REASONSEXUAL, - [OzoneReportDefs.REASONSEXUALUNLABELED]: RootReportDefs.REASONSEXUAL, - [OzoneReportDefs.REASONSEXUALOTHER]: RootReportDefs.REASONSEXUAL, + [OzoneReportDefs.reasonSexualAbuseContent.value]: + RootReportDefs.reasonSexual.value, + [OzoneReportDefs.reasonSexualNCII.value]: RootReportDefs.reasonSexual.value, + [OzoneReportDefs.reasonSexualDeepfake.value]: + RootReportDefs.reasonSexual.value, + [OzoneReportDefs.reasonSexualAnimal.value]: RootReportDefs.reasonSexual.value, + [OzoneReportDefs.reasonSexualUnlabeled.value]: + RootReportDefs.reasonSexual.value, + [OzoneReportDefs.reasonSexualOther.value]: RootReportDefs.reasonSexual.value, - [OzoneReportDefs.REASONCHILDSAFETYCSAM]: RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONCHILDSAFETYGROOM]: RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONCHILDSAFETYPRIVACY]: RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONCHILDSAFETYHARASSMENT]: RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONCHILDSAFETYOTHER]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.reasonChildSafetyCSAM.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonChildSafetyGroom.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonChildSafetyPrivacy.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonChildSafetyHarassment.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonChildSafetyOther.value]: + RootReportDefs.reasonViolation.value, - [OzoneReportDefs.REASONHARASSMENTTROLL]: RootReportDefs.REASONRUDE, - [OzoneReportDefs.REASONHARASSMENTTARGETED]: RootReportDefs.REASONRUDE, - [OzoneReportDefs.REASONHARASSMENTHATESPEECH]: RootReportDefs.REASONRUDE, - [OzoneReportDefs.REASONHARASSMENTDOXXING]: RootReportDefs.REASONRUDE, - [OzoneReportDefs.REASONHARASSMENTOTHER]: RootReportDefs.REASONRUDE, + [OzoneReportDefs.reasonHarassmentTroll.value]: + RootReportDefs.reasonRude.value, + [OzoneReportDefs.reasonHarassmentTargeted.value]: + RootReportDefs.reasonRude.value, + [OzoneReportDefs.reasonHarassmentHateSpeech.value]: + RootReportDefs.reasonRude.value, + [OzoneReportDefs.reasonHarassmentDoxxing.value]: + RootReportDefs.reasonRude.value, + [OzoneReportDefs.reasonHarassmentOther.value]: + RootReportDefs.reasonRude.value, - [OzoneReportDefs.REASONMISLEADINGBOT]: RootReportDefs.REASONMISLEADING, - [OzoneReportDefs.REASONMISLEADINGIMPERSONATION]: - RootReportDefs.REASONMISLEADING, - [OzoneReportDefs.REASONMISLEADINGSPAM]: RootReportDefs.REASONSPAM, - [OzoneReportDefs.REASONMISLEADINGSCAM]: RootReportDefs.REASONMISLEADING, - [OzoneReportDefs.REASONMISLEADINGELECTIONS]: RootReportDefs.REASONMISLEADING, - [OzoneReportDefs.REASONMISLEADINGOTHER]: RootReportDefs.REASONMISLEADING, + [OzoneReportDefs.reasonMisleadingBot.value]: + RootReportDefs.reasonMisleading.value, + [OzoneReportDefs.reasonMisleadingImpersonation.value]: + RootReportDefs.reasonMisleading.value, + [OzoneReportDefs.reasonMisleadingSpam.value]: RootReportDefs.reasonSpam.value, + [OzoneReportDefs.reasonMisleadingScam.value]: + RootReportDefs.reasonMisleading.value, + [OzoneReportDefs.reasonMisleadingElections.value]: + RootReportDefs.reasonMisleading.value, + [OzoneReportDefs.reasonMisleadingOther.value]: + RootReportDefs.reasonMisleading.value, - [OzoneReportDefs.REASONRULESITESECURITY]: RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONRULEPROHIBITEDSALES]: RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONRULEBANEVASION]: RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONRULEOTHER]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.reasonRuleSiteSecurity.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonRuleProhibitedSales.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonRuleBanEvasion.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonRuleOther.value]: RootReportDefs.reasonViolation.value, - [OzoneReportDefs.REASONSELFHARMCONTENT]: RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONSELFHARMED]: RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONSELFHARMSTUNTS]: RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONSELFHARMSUBSTANCES]: RootReportDefs.REASONVIOLATION, - [OzoneReportDefs.REASONSELFHARMOTHER]: RootReportDefs.REASONVIOLATION, + [OzoneReportDefs.reasonSelfHarmContent.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonSelfHarmED.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonSelfHarmStunts.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonSelfHarmSubstances.value]: + RootReportDefs.reasonViolation.value, + [OzoneReportDefs.reasonSelfHarmOther.value]: + RootReportDefs.reasonViolation.value, } /** @@ -78,43 +107,45 @@ export const NEW_TO_OLD_REASONS_MAP: Record< * @see https://github.com/bluesky-social/proposals/tree/main/0009-mod-report-granularity#backwards-compatibility */ export const OLD_TO_NEW_REASONS_MAP: Record< - Exclude, - OzoneReportDefs.ReasonType + Exclude, + OzoneReasonType > = { - [RootReportDefs.REASONSPAM]: [OzoneReportDefs.REASONMISLEADINGSPAM], - [RootReportDefs.REASONVIOLATION]: [OzoneReportDefs.REASONRULEOTHER], - [RootReportDefs.REASONMISLEADING]: [OzoneReportDefs.REASONMISLEADINGOTHER], - [RootReportDefs.REASONSEXUAL]: [OzoneReportDefs.REASONSEXUALUNLABELED], - [RootReportDefs.REASONRUDE]: [OzoneReportDefs.REASONHARASSMENTOTHER], - [RootReportDefs.REASONOTHER]: [OzoneReportDefs.REASONOTHER], - [RootReportDefs.REASONAPPEAL]: [OzoneReportDefs.REASONAPPEAL], + [RootReportDefs.reasonSpam.value]: OzoneReportDefs.reasonMisleadingSpam.value, + [RootReportDefs.reasonViolation.value]: OzoneReportDefs.reasonRuleOther.value, + [RootReportDefs.reasonMisleading.value]: + OzoneReportDefs.reasonMisleadingOther.value, + [RootReportDefs.reasonSexual.value]: + OzoneReportDefs.reasonSexualUnlabeled.value, + [RootReportDefs.reasonRude.value]: + OzoneReportDefs.reasonHarassmentOther.value, + [RootReportDefs.reasonOther.value]: OzoneReportDefs.reasonOther.value, + [RootReportDefs.reasonAppeal.value]: OzoneReportDefs.reasonAppeal.value, } /** * Set of report reasons that should optionally include additional details from * the reporter. */ -export const OTHER_REPORT_REASONS: Set = new Set([ - OzoneReportDefs.REASONVIOLENCEOTHER, - OzoneReportDefs.REASONSEXUALOTHER, - OzoneReportDefs.REASONCHILDSAFETYOTHER, - OzoneReportDefs.REASONHARASSMENTOTHER, - OzoneReportDefs.REASONMISLEADINGOTHER, - OzoneReportDefs.REASONRULEOTHER, - OzoneReportDefs.REASONSELFHARMOTHER, - OzoneReportDefs.REASONOTHER, +export const OTHER_REPORT_REASONS: Set = new Set([ + OzoneReportDefs.reasonViolenceOther.value, + OzoneReportDefs.reasonSexualOther.value, + OzoneReportDefs.reasonChildSafetyOther.value, + OzoneReportDefs.reasonHarassmentOther.value, + OzoneReportDefs.reasonMisleadingOther.value, + OzoneReportDefs.reasonRuleOther.value, + OzoneReportDefs.reasonSelfHarmOther.value, + OzoneReportDefs.reasonOther.value, ]) /** * Set of report reasons that should only be sent to Bluesky's moderation service. */ -export const BSKY_LABELER_ONLY_REPORT_REASONS: Set = - new Set([ - OzoneReportDefs.REASONCHILDSAFETYCSAM, - OzoneReportDefs.REASONCHILDSAFETYGROOM, - OzoneReportDefs.REASONCHILDSAFETYOTHER, - OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT, - ]) +export const BSKY_LABELER_ONLY_REPORT_REASONS: Set = new Set([ + OzoneReportDefs.reasonChildSafetyCSAM.value, + OzoneReportDefs.reasonChildSafetyGroom.value, + OzoneReportDefs.reasonChildSafetyOther.value, + OzoneReportDefs.reasonViolenceExtremistContent.value, +]) /** * Set of _parsed_ subject types that should only be sent to Bluesky's diff --git a/src/components/moderation/ReportDialog/index.tsx b/src/components/moderation/ReportDialog/index.tsx index dfb3e781a9..137b389ea7 100644 --- a/src/components/moderation/ReportDialog/index.tsx +++ b/src/components/moderation/ReportDialog/index.tsx @@ -7,7 +7,7 @@ import { useState, } from 'react' import {Pressable, type ScrollView, View} from 'react-native' -import {type AppBskyLabelerDefs, BSKY_LABELER_DID} from '@atproto/api' +import {api} from '@bsky.app/sdk' import {Trans, useLingui} from '@lingui/react/macro' import {wait} from '#/lib/async/wait' @@ -36,6 +36,7 @@ import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_NATIVE} from '#/env' +import {type app} from '#/lexicons' import {useSubmitReportMutation} from './action' import { BSKY_LABELER_ONLY_REPORT_REASONS, @@ -184,7 +185,7 @@ function Inner(props: ReportDialogProps) { .filter(l => { if (!state.selectedOption) return false if (isBskyOnlyReason || isBskyOnlySubject) { - return l.creator.did === BSKY_LABELER_DID + return l.creator.did === api.moderation.did } const supportedReasonTypes: string[] | undefined = l.reasonTypes if (supportedReasonTypes === undefined) return true @@ -932,8 +933,8 @@ function LabelerCard({ labeler, onSelect, }: { - labeler: AppBskyLabelerDefs.LabelerViewDetailed - onSelect?: (option: AppBskyLabelerDefs.LabelerViewDetailed) => void + labeler: app.bsky.labeler.defs.LabelerViewDetailed + onSelect?: (option: app.bsky.labeler.defs.LabelerViewDetailed) => void }) { const t = useTheme() const {t: l} = useLingui() diff --git a/src/components/moderation/ReportDialog/state.ts b/src/components/moderation/ReportDialog/state.ts index d86b5e28b9..61d432e478 100644 --- a/src/components/moderation/ReportDialog/state.ts +++ b/src/components/moderation/ReportDialog/state.ts @@ -1,13 +1,11 @@ -import { - type AppBskyLabelerDefs, - ToolsOzoneReportDefs as OzoneReportDefs, -} from '@atproto/api' - import {OTHER_REPORT_REASONS} from '#/components/moderation/ReportDialog/const' import { type ReportCategoryConfig, type ReportOption, } from '#/components/moderation/ReportDialog/utils/useReportOptions' +import {type app, tools} from '#/lexicons' + +const OzoneReportDefs = tools.ozone.report.defs export type NciiQualification = { isDepicted?: boolean @@ -16,7 +14,7 @@ export type NciiQualification = { export type ReportState = { selectedCategory?: ReportCategoryConfig selectedOption?: ReportOption - selectedLabeler?: AppBskyLabelerDefs.LabelerViewDetailed + selectedLabeler?: app.bsky.labeler.defs.LabelerViewDetailed details?: string detailsOpen: boolean activeStepIndex1: number @@ -66,7 +64,7 @@ export type ReportAction = } | { type: 'selectLabeler' - labeler: AppBskyLabelerDefs.LabelerViewDetailed + labeler: app.bsky.labeler.defs.LabelerViewDetailed } | { type: 'clearLabeler' @@ -116,7 +114,8 @@ export function reducer(state: ReportState, action: ReportAction): ReportState { ncii: undefined, } case 'selectOption': { - const isNcii = action.option.reason === OzoneReportDefs.REASONSEXUALNCII + const isNcii = + action.option.reason === OzoneReportDefs.reasonSexualNCII.value return { ...state, selectedOption: action.option, diff --git a/src/components/moderation/ReportDialog/utils/parseReportSubject.ts b/src/components/moderation/ReportDialog/utils/parseReportSubject.ts index a7d4b94c32..f2243f322d 100644 --- a/src/components/moderation/ReportDialog/utils/parseReportSubject.ts +++ b/src/components/moderation/ReportDialog/utils/parseReportSubject.ts @@ -1,46 +1,56 @@ -import { - AppBskyActorDefs, - AppBskyFeedDefs, - AppBskyFeedPost, - AppBskyGraphDefs, -} from '@atproto/api' - import { type ParsedReportSubject, type ReportSubject, } from '#/components/moderation/ReportDialog/types' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' export function parseReportSubject( - subject: ReportSubject, + rawSubject: ReportSubject, ): ParsedReportSubject | undefined { - if (!subject) return + if (!rawSubject) return - if ('convoId' in subject) { - if ('message' in subject) { + if ('convoId' in rawSubject) { + if ('message' in rawSubject) { return { type: 'convoMessage', - ...subject, + ...rawSubject, } } return { type: 'convo', - convoId: subject.convoId, - did: subject.did, + convoId: rawSubject.convoId, + did: rawSubject.did, } } + /* + * TODO(phase4): drop toLex once ReportSubject is a #/lexicons union. The view + * subjects are structurally the #/lexicons shapes; we bridge here so the + * `isType` schema guards below narrow against the lexicon `$type`s. + */ + const subject = bsky.toLex< + | app.bsky.actor.defs.ProfileViewBasic + | app.bsky.actor.defs.ProfileView + | app.bsky.actor.defs.ProfileViewDetailed + | app.bsky.actor.defs.StatusView + | app.bsky.graph.defs.ListView + | app.bsky.feed.defs.GeneratorView + | app.bsky.graph.defs.StarterPackView + | app.bsky.feed.defs.PostView + >(rawSubject) + if ( - AppBskyActorDefs.isProfileViewBasic(subject) || - AppBskyActorDefs.isProfileView(subject) || - AppBskyActorDefs.isProfileViewDetailed(subject) + bsky.isType(app.bsky.actor.defs.profileViewBasic, subject) || + bsky.isType(app.bsky.actor.defs.profileView, subject) || + bsky.isType(app.bsky.actor.defs.profileViewDetailed, subject) ) { return { type: 'account', did: subject.did, nsid: 'app.bsky.actor.profile', } - } else if (AppBskyActorDefs.isStatusView(subject)) { + } else if (bsky.isType(app.bsky.actor.defs.statusView, subject)) { if (!subject.uri || !subject.cid) return return { type: 'status', @@ -48,36 +58,31 @@ export function parseReportSubject( cid: subject.cid, nsid: 'app.bsky.actor.status', } - } else if (AppBskyGraphDefs.isListView(subject)) { + } else if (bsky.isType(app.bsky.graph.defs.listView, subject)) { return { type: 'list', uri: subject.uri, cid: subject.cid, nsid: 'app.bsky.graph.list', } - } else if (AppBskyFeedDefs.isGeneratorView(subject)) { + } else if (bsky.isType(app.bsky.feed.defs.generatorView, subject)) { return { type: 'feed', uri: subject.uri, cid: subject.cid, nsid: 'app.bsky.feed.generator', } - } else if (AppBskyGraphDefs.isStarterPackView(subject)) { + } else if (bsky.isType(app.bsky.graph.defs.starterPackView, subject)) { return { type: 'starterPack', uri: subject.uri, cid: subject.cid, nsid: 'app.bsky.graph.starterPack', } - } else if (AppBskyFeedDefs.isPostView(subject)) { + } else if (bsky.isType(app.bsky.feed.defs.postView, subject)) { const record = subject.record const embed = bsky.post.parseEmbed(subject.embed) - if ( - bsky.dangerousIsType( - record, - AppBskyFeedPost.isRecord, - ) - ) { + if (bsky.isType(app.bsky.feed.post, record)) { return { type: 'post', uri: subject.uri, diff --git a/src/components/moderation/ReportDialog/utils/useReportOptions.ts b/src/components/moderation/ReportDialog/utils/useReportOptions.ts index 87553ce6ed..a88176fb1e 100644 --- a/src/components/moderation/ReportDialog/utils/useReportOptions.ts +++ b/src/components/moderation/ReportDialog/utils/useReportOptions.ts @@ -1,8 +1,11 @@ import {useMemo} from 'react' -import {ToolsOzoneReportDefs as OzoneReportDefs} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' +import {tools} from '#/lexicons' + +const OzoneReportDefs = tools.ozone.report.defs + export type ReportCategory = | 'childSafety' | 'violencePhysicalHarm' @@ -22,7 +25,7 @@ export type ReportCategoryConfig = { export type ReportOption = { title: string - reason: OzoneReportDefs.ReasonType + reason: tools.ozone.report.defs.ReasonType } export function useReportOptions() { @@ -37,27 +40,27 @@ export function useReportOptions() { options: [ { title: _(msg`Spam`), - reason: OzoneReportDefs.REASONMISLEADINGSPAM, + reason: OzoneReportDefs.reasonMisleadingSpam.value, }, { title: _(msg`Scam`), - reason: OzoneReportDefs.REASONMISLEADINGSCAM, + reason: OzoneReportDefs.reasonMisleadingScam.value, }, { title: _(msg`Fake account or bot`), - reason: OzoneReportDefs.REASONMISLEADINGBOT, + reason: OzoneReportDefs.reasonMisleadingBot.value, }, { title: _(msg`Impersonation`), - reason: OzoneReportDefs.REASONMISLEADINGIMPERSONATION, + reason: OzoneReportDefs.reasonMisleadingImpersonation.value, }, { title: _(msg`False information about elections`), - reason: OzoneReportDefs.REASONMISLEADINGELECTIONS, + reason: OzoneReportDefs.reasonMisleadingElections.value, }, { title: _(msg`Other misleading content`), - reason: OzoneReportDefs.REASONMISLEADINGOTHER, + reason: OzoneReportDefs.reasonMisleadingOther.value, }, ], }, @@ -70,27 +73,27 @@ export function useReportOptions() { options: [ { title: _(msg`Unlabeled adult content`), - reason: OzoneReportDefs.REASONSEXUALUNLABELED, + reason: OzoneReportDefs.reasonSexualUnlabeled.value, }, { title: _(msg`Adult sexual abuse content`), - reason: OzoneReportDefs.REASONSEXUALABUSECONTENT, + reason: OzoneReportDefs.reasonSexualAbuseContent.value, }, { title: _(msg`Non-consensual intimate imagery`), - reason: OzoneReportDefs.REASONSEXUALNCII, + reason: OzoneReportDefs.reasonSexualNCII.value, }, { title: _(msg`Deepfake adult content`), - reason: OzoneReportDefs.REASONSEXUALDEEPFAKE, + reason: OzoneReportDefs.reasonSexualDeepfake.value, }, { title: _(msg`Animal sexual abuse`), - reason: OzoneReportDefs.REASONSEXUALANIMAL, + reason: OzoneReportDefs.reasonSexualAnimal.value, }, { title: _(msg`Other sexual violence content`), - reason: OzoneReportDefs.REASONSEXUALOTHER, + reason: OzoneReportDefs.reasonSexualOther.value, }, ], }, @@ -101,23 +104,23 @@ export function useReportOptions() { options: [ { title: _(msg`Trolling`), - reason: OzoneReportDefs.REASONHARASSMENTTROLL, + reason: OzoneReportDefs.reasonHarassmentTroll.value, }, { title: _(msg`Targeted harassment`), - reason: OzoneReportDefs.REASONHARASSMENTTARGETED, + reason: OzoneReportDefs.reasonHarassmentTargeted.value, }, { title: _(msg`Hate speech`), - reason: OzoneReportDefs.REASONHARASSMENTHATESPEECH, + reason: OzoneReportDefs.reasonHarassmentHateSpeech.value, }, { title: _(msg`Doxxing`), - reason: OzoneReportDefs.REASONHARASSMENTDOXXING, + reason: OzoneReportDefs.reasonHarassmentDoxxing.value, }, { title: _(msg`Other harassing or hateful content`), - reason: OzoneReportDefs.REASONHARASSMENTOTHER, + reason: OzoneReportDefs.reasonHarassmentOther.value, }, ], }, @@ -128,31 +131,31 @@ export function useReportOptions() { options: [ { title: _(msg`Animal welfare`), - reason: OzoneReportDefs.REASONVIOLENCEANIMAL, + reason: OzoneReportDefs.reasonViolenceAnimal.value, }, { title: _(msg`Threats or incitement`), - reason: OzoneReportDefs.REASONVIOLENCETHREATS, + reason: OzoneReportDefs.reasonViolenceThreats.value, }, { title: _(msg`Graphic violent content`), - reason: OzoneReportDefs.REASONVIOLENCEGRAPHICCONTENT, + reason: OzoneReportDefs.reasonViolenceGraphicContent.value, }, { title: _(msg`Glorification of violence`), - reason: OzoneReportDefs.REASONVIOLENCEGLORIFICATION, + reason: OzoneReportDefs.reasonViolenceGlorification.value, }, { title: _(msg`Extremist content`), - reason: OzoneReportDefs.REASONVIOLENCEEXTREMISTCONTENT, + reason: OzoneReportDefs.reasonViolenceExtremistContent.value, }, { title: _(msg`Human trafficking`), - reason: OzoneReportDefs.REASONVIOLENCETRAFFICKING, + reason: OzoneReportDefs.reasonViolenceTrafficking.value, }, { title: _(msg`Other violent content`), - reason: OzoneReportDefs.REASONVIOLENCEOTHER, + reason: OzoneReportDefs.reasonViolenceOther.value, }, ], }, @@ -163,23 +166,23 @@ export function useReportOptions() { options: [ { title: _(msg`Child Sexual Abuse Material (CSAM)`), - reason: OzoneReportDefs.REASONCHILDSAFETYCSAM, + reason: OzoneReportDefs.reasonChildSafetyCSAM.value, }, { title: _(msg`Grooming or predatory behavior`), - reason: OzoneReportDefs.REASONCHILDSAFETYGROOM, + reason: OzoneReportDefs.reasonChildSafetyGroom.value, }, { title: _(msg`Privacy violation of a minor`), - reason: OzoneReportDefs.REASONCHILDSAFETYPRIVACY, + reason: OzoneReportDefs.reasonChildSafetyPrivacy.value, }, { title: _(msg`Minor harassment or bullying`), - reason: OzoneReportDefs.REASONCHILDSAFETYHARASSMENT, + reason: OzoneReportDefs.reasonChildSafetyHarassment.value, }, { title: _(msg`Other child safety issue`), - reason: OzoneReportDefs.REASONCHILDSAFETYOTHER, + reason: OzoneReportDefs.reasonChildSafetyOther.value, }, ], }, @@ -190,23 +193,23 @@ export function useReportOptions() { options: [ { title: _(msg`Content promoting or depicting self-harm`), - reason: OzoneReportDefs.REASONSELFHARMCONTENT, + reason: OzoneReportDefs.reasonSelfHarmContent.value, }, { title: _(msg`Eating disorders`), - reason: OzoneReportDefs.REASONSELFHARMED, + reason: OzoneReportDefs.reasonSelfHarmED.value, }, { title: _(msg`Dangerous challenges or activities`), - reason: OzoneReportDefs.REASONSELFHARMSTUNTS, + reason: OzoneReportDefs.reasonSelfHarmStunts.value, }, { title: _(msg`Dangerous substances or drug abuse`), - reason: OzoneReportDefs.REASONSELFHARMSUBSTANCES, + reason: OzoneReportDefs.reasonSelfHarmSubstances.value, }, { title: _(msg`Other dangerous content`), - reason: OzoneReportDefs.REASONSELFHARMOTHER, + reason: OzoneReportDefs.reasonSelfHarmOther.value, }, ], }, @@ -217,19 +220,19 @@ export function useReportOptions() { options: [ { title: _(msg`Hacking or system attacks`), - reason: OzoneReportDefs.REASONRULESITESECURITY, + reason: OzoneReportDefs.reasonRuleSiteSecurity.value, }, { title: _(msg`Promoting or selling prohibited items or services`), - reason: OzoneReportDefs.REASONRULEPROHIBITEDSALES, + reason: OzoneReportDefs.reasonRuleProhibitedSales.value, }, { title: _(msg`Banned user returning`), - reason: OzoneReportDefs.REASONRULEBANEVASION, + reason: OzoneReportDefs.reasonRuleBanEvasion.value, }, { title: _(msg`Other network rule-breaking`), - reason: OzoneReportDefs.REASONRULEOTHER, + reason: OzoneReportDefs.reasonRuleOther.value, }, ], }, @@ -240,7 +243,7 @@ export function useReportOptions() { options: [ { title: _(msg`Other`), - reason: OzoneReportDefs.REASONOTHER, + reason: OzoneReportDefs.reasonOther.value, }, ], }, diff --git a/src/components/moderation/ScreenHider.tsx b/src/components/moderation/ScreenHider.tsx index 3c0d4584b8..9557121a05 100644 --- a/src/components/moderation/ScreenHider.tsx +++ b/src/components/moderation/ScreenHider.tsx @@ -5,7 +5,7 @@ import { View, type ViewStyle, } from 'react-native' -import {type ModerationUI} from '@atproto/api' +import {type ModerationUI} from '@bsky.app/sdk/moderation' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' diff --git a/src/features/liveEvents/context.tsx b/src/features/liveEvents/context.tsx index 3de2534a8e..0d333ba3a1 100644 --- a/src/features/liveEvents/context.tsx +++ b/src/features/liveEvents/context.tsx @@ -1,5 +1,5 @@ import {createContext, useContext, useMemo} from 'react' -import {hasMutedWord} from '@atproto/api' +import {hasMutedWord} from '@bsky.app/sdk/moderation' import {QueryClient, useQuery} from '@tanstack/react-query' import {useOnAppStateChange} from '#/lib/appState' diff --git a/src/features/liveEvents/preferences.ts b/src/features/liveEvents/preferences.ts index 6fb2ac6d11..dd791327ff 100644 --- a/src/features/liveEvents/preferences.ts +++ b/src/features/liveEvents/preferences.ts @@ -1,12 +1,12 @@ import {useEffect} from 'react' -import {type Agent, AppBskyActorDefs, asPredicate} from '@atproto/api' +import {getPreferences, updateLiveEventPreferences} from '@bsky.app/sdk' import {useMutation, useQueryClient} from '@tanstack/react-query' import { preferencesQueryKey, usePreferencesQuery, } from '#/state/queries/preferences' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' import {useAnalytics} from '#/analytics' import * as env from '#/env' import {IS_WEB} from '#/env' @@ -14,10 +14,11 @@ import { type LiveEventFeed, type LiveEventFeedMetricContext, } from '#/features/liveEvents/types' +import {app} from '#/lexicons' export type LiveEventPreferencesAction = Parameters< - Agent['updateLiveEventPreferences'] ->[0] & { + typeof updateLiveEventPreferences +>[1] & { /** * Flag that is internal to this hook, do not set when updating prefs */ @@ -38,7 +39,7 @@ export function useLiveEventPreferences() { function useWebOnlyDebugLiveEventPreferences() { const queryClient = useQueryClient() - const agent = useAgent() + const pdsClient = usePdsClient() useEffect(() => { if (env.IS_DEV && IS_WEB && typeof window !== 'undefined') { @@ -46,14 +47,14 @@ function useWebOnlyDebugLiveEventPreferences() { window.__updateLiveEventPreferences = async ( action: LiveEventPreferencesAction, ) => { - await agent.updateLiveEventPreferences(action) + await pdsClient.call(updateLiveEventPreferences, action) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, }) } } - }, [agent, queryClient]) + }, [pdsClient, queryClient]) } export function useUpdateLiveEventPreferences(props: { @@ -65,10 +66,10 @@ export function useUpdateLiveEventPreferences(props: { }) { const ax = useAnalytics() const queryClient = useQueryClient() - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation< - AppBskyActorDefs.LiveEventPreferences, + app.bsky.actor.defs.LiveEventPreferences, Error, LiveEventPreferencesAction, {undoAction: LiveEventPreferencesAction | null} @@ -108,10 +109,14 @@ export function useUpdateLiveEventPreferences(props: { } }, mutationFn: async action => { - const updated = await agent.updateLiveEventPreferences(action) - const prefs = updated.find(p => - asPredicate(AppBskyActorDefs.validateLiveEventPreferences)(p), - ) + /* + * The SDK action returns void, so after applying the update we read the + * fresh, interpreted preferences back to obtain the updated + * `liveEventPreferences` (the SDK extracts it from the raw prefs array for + * us, replacing the old `asPredicate(...).find(...)` lookup). + */ + await pdsClient.call(updateLiveEventPreferences, action) + const {liveEventPreferences: prefs} = await pdsClient.call(getPreferences) switch (action.type) { case 'hideFeed': @@ -138,7 +143,7 @@ export function useUpdateLiveEventPreferences(props: { break } case 'toggleHideAllFeeds': { - if (prefs!.hideAllFeeds) { + if (prefs.hideAllFeeds) { ax.metric('liveEvents:hideAllFeedBanners', { context: props.metricContext, }) @@ -156,7 +161,7 @@ export function useUpdateLiveEventPreferences(props: { queryKey: preferencesQueryKey, }) - return prefs! + return prefs }, }) } diff --git a/src/features/liveNow/components/EditLiveDialog.tsx b/src/features/liveNow/components/EditLiveDialog.tsx index cf913541a1..b6ee781ea7 100644 --- a/src/features/liveNow/components/EditLiveDialog.tsx +++ b/src/features/liveNow/components/EditLiveDialog.tsx @@ -1,6 +1,5 @@ import {useMemo, useState} from 'react' import {View} from 'react-native' -import {type AppBskyActorDefs, type AppBskyEmbedExternal} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -26,6 +25,7 @@ import { useUpsertLiveStatusMutation, } from '#/features/liveNow' import {LinkPreview} from '#/features/liveNow/components/LinkPreview' +import {type app} from '#/lexicons' export function EditLiveDialog({ control, @@ -33,8 +33,8 @@ export function EditLiveDialog({ embed, }: { control: Dialog.DialogControlProps - status: AppBskyActorDefs.StatusView - embed: AppBskyEmbedExternal.View + status: app.bsky.actor.defs.StatusView + embed: app.bsky.embed.external.View }) { return ( @@ -48,14 +48,14 @@ function DialogInner({ status, embed, }: { - status: AppBskyActorDefs.StatusView - embed: AppBskyEmbedExternal.View + status: app.bsky.actor.defs.StatusView + embed: app.bsky.embed.external.View }) { const control = Dialog.useDialogContext() const {_, i18n} = useLingui() const t = useTheme() - const [liveLink, setLiveLink] = useState(embed.external.uri) + 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 7eeccdc22b..e25f7ab5c9 100644 --- a/src/features/liveNow/components/GoLiveDisabledDialog.tsx +++ b/src/features/liveNow/components/GoLiveDisabledDialog.tsx @@ -1,20 +1,22 @@ import {useCallback, useState} from 'react' import {View} from 'react-native' -import {type AppBskyActorDefs, ToolsOzoneReportDefs} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {api} from '@bsky.app/sdk' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useMutation} from '@tanstack/react-query' -import {BLUESKY_MOD_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useMaybePdsClient, useSession} from '#/state/session' import {atoms as a, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' +import {com, tools} from '#/lexicons' +import {toLex} from '#/types/bsky' export function GoLiveDisabledDialog({ control, @@ -39,12 +41,13 @@ export function DialogInner({ status: AppBskyActorDefs.StatusView }) { const {_} = useLingui() - const agent = useAgent() + const {currentAccount} = useSession() + const pdsClient = useMaybePdsClient() const [details, setDetails] = useState('') const {mutate, isPending} = useMutation({ mutationFn: async () => { - if (!agent.session?.did) { + if (!currentAccount?.did || !pdsClient) { throw new Error('Not logged in') } if (!status.uri || !status.cid) { @@ -56,19 +59,19 @@ export function DialogInner({ details, }) } else { - await agent.createModerationReport( - { - reasonType: ToolsOzoneReportDefs.REASONAPPEAL, + await pdsClient.call( + com.atproto.moderation.createReport, + toLex({ + reasonType: tools.ozone.report.defs.reasonAppeal.value, subject: { $type: 'com.atproto.repo.strongRef', uri: status.uri, cid: status.cid, }, reason: details, - }, + }), { - encoding: 'application/json', - headers: BLUESKY_MOD_SERVICE_HEADERS, + service: api.moderation.service, }, ) } diff --git a/src/features/liveNow/components/LiveStatusDialog.tsx b/src/features/liveNow/components/LiveStatusDialog.tsx index 9e916678f2..5bf090516f 100644 --- a/src/features/liveNow/components/LiveStatusDialog.tsx +++ b/src/features/liveNow/components/LiveStatusDialog.tsx @@ -1,11 +1,7 @@ import {useCallback, useMemo} from 'react' import {View} from 'react-native' import {Image} from 'expo-image' -import { - type AppBskyActorDefs, - type AppBskyEmbedExternal, - moderateStatus, -} from '@atproto/api' +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' @@ -30,7 +26,8 @@ import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {LiveIndicator} from '#/features/liveNow/components/LiveIndicator' -import type * as bsky from '#/types/bsky' +import {type app} from '#/lexicons' +import * as bsky from '#/types/bsky' export function LiveStatusDialog({ control, @@ -40,8 +37,8 @@ export function LiveStatusDialog({ }: { control: Dialog.DialogControlProps profile: bsky.profile.AnyProfileView - status: AppBskyActorDefs.StatusView - embed: AppBskyEmbedExternal.View + status: app.bsky.actor.defs.StatusView + embed: app.bsky.embed.external.View }) { const navigation = useNavigation() return ( @@ -64,9 +61,9 @@ function DialogInner({ status, }: { profile: bsky.profile.AnyProfileView - embed: AppBskyEmbedExternal.View + embed: app.bsky.embed.external.View navigation: NavigationProp - status: AppBskyActorDefs.StatusView + status: app.bsky.actor.defs.StatusView }) { const {t: l} = useLingui() const control = Dialog.useDialogContext() @@ -102,9 +99,9 @@ export function LiveStatus({ padding = 'xl', onPressOpenProfile, }: { - status: AppBskyActorDefs.StatusView + status: app.bsky.actor.defs.StatusView profile: bsky.profile.AnyProfileView - embed: AppBskyEmbedExternal.View + embed: app.bsky.embed.external.View padding?: 'lg' | 'xl' onPressOpenProfile: () => void }) { @@ -118,7 +115,8 @@ export function LiveStatus({ const dialogContext = Dialog.useDialogContext() const moderation = useMemo(() => { if (!moderationOpts) return undefined - return moderateStatus(profile, moderationOpts) + // TODO(phase4): drop toLex once profile producers emit #/lexicons views + return moderateStatus(bsky.toLex(profile), moderationOpts) }, [profile, moderationOpts]) return ( diff --git a/src/features/liveNow/index.tsx b/src/features/liveNow/index.tsx index fb1f04b728..2341a211d1 100644 --- a/src/features/liveNow/index.tsx +++ b/src/features/liveNow/index.tsx @@ -1,14 +1,13 @@ import {useMemo} from 'react' -import { - type $Typed, - type AppBskyActorDefs, - type AppBskyActorStatus, - AppBskyEmbedExternal, - AtUri, - ComAtprotoRepoPutRecord, - moderateStatus, -} from '@atproto/api' import {retry} from '@atproto/common-web' +import {type $Typed} from '@atproto/lex' +import { + type AtIdentifierString, + AtUri, + type DatetimeString, + type UriString, +} 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' @@ -17,19 +16,21 @@ 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 {getErrorName} from '#/lib/xrpc-error' import {useAppConfig} from '#/state/appConfig' import { updateProfileShadow, useMaybeProfileShadow, } from '#/state/cache/profile-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useAgent, useSession} from '#/state/session' +import {useAgent, usePdsClient, useSession} from '#/state/session' import {useTickEveryMinute} from '#/state/shell' import {useDialogContext} from '#/components/Dialog' import * as Toast from '#/components/Toast' import {useAnalytics} from '#/analytics' import {getLiveNowHost, getLiveServiceNames} from '#/features/liveNow/utils' -import type * as bsky from '#/types/bsky' +import {app, com} from '#/lexicons' +import * as bsky from '#/types/bsky' export * from '#/features/liveNow/utils' @@ -47,7 +48,7 @@ const DEFAULT_STATE = { isDisabled: false, isActive: false, record: {}, -} satisfies AppBskyActorDefs.StatusView +} satisfies app.bsky.actor.defs.StatusView export type LiveNowConfig = { canGoLive: boolean @@ -104,7 +105,8 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) { const moderation = useMemo(() => { if (!actor || !('status' in actor && actor.status)) return undefined if (!moderationOpts) return undefined - return moderateStatus(actor, moderationOpts) + // TODO(phase4): drop toLex once profile producers emit #/lexicons views + return moderateStatus(bsky.toLex(actor), moderationOpts) }, [actor, moderationOpts]) return useMemo(() => { @@ -118,31 +120,37 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) { } if (shadowed && 'status' in shadowed && shadowed.status) { - const isValid = isStatusValidForViewers(shadowed.status, config) - const isDisabled = shadowed.status.isDisabled || false - const isActive = isStatusStillActive(shadowed.status.expiresAt) + /* + * TODO(phase4): drop toLex once profile-shadow emits #/lexicons views. + * The shadow store still holds old `@atproto/api` StatusViews, so brand + * it once here to feed the new-world validation and returned view. + */ + const status = bsky.toLex(shadowed.status) + const isValid = isStatusValidForViewers(status, config) + const isDisabled = status.isDisabled || false + const isActive = isStatusStillActive(status.expiresAt) if (isValid && !isDisabled && isActive) { return { - uri: shadowed.status.uri, - cid: shadowed.status.cid, + uri: status.uri, + cid: status.cid, isDisabled: false, isActive: true, status: 'app.bsky.actor.status#live', - embed: shadowed.status.embed as $Typed, // temp_isStatusValid asserts this - expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this - record: shadowed.status.record, - } satisfies AppBskyActorDefs.StatusView + embed: status.embed as $Typed, // temp_isStatusValid asserts this + expiresAt: status.expiresAt!, // isStatusStillActive asserts this + record: status.record, + } satisfies app.bsky.actor.defs.StatusView } return { - uri: shadowed.status.uri, - cid: shadowed.status.cid, + uri: status.uri, + cid: status.cid, isDisabled, isActive: false, status: 'app.bsky.actor.status#live', - embed: shadowed.status.embed as $Typed, // temp_isStatusValid asserts this - expiresAt: shadowed.status.expiresAt!, // isStatusStillActive asserts this - record: shadowed.status.record, - } satisfies AppBskyActorDefs.StatusView + embed: status.embed as $Typed, // temp_isStatusValid asserts this + expiresAt: status.expiresAt!, // isStatusStillActive asserts this + record: status.record, + } satisfies app.bsky.actor.defs.StatusView } else { return DEFAULT_STATE } @@ -162,14 +170,14 @@ export function isStatusStillActive(timeStr: string | undefined) { * validate if the status is valid for the acting user e.g. as they go live. */ export function isStatusValidForViewers( - status: AppBskyActorDefs.StatusView, + status: app.bsky.actor.defs.StatusView, config: LiveNowConfig, ) { if (status.status !== 'app.bsky.actor.status#live') return false if (!status.uri) return false // should not happen, just backwards compat try { const {host: liveDid} = new AtUri(status.uri) - if (AppBskyEmbedExternal.isView(status.embed)) { + if (bsky.isType(app.bsky.embed.external.view, status.embed)) { const host = getLiveNowHost(status.embed.external.uri) const exception = config.allowedHostsExceptionsByDid.get(liveDid) const isValidException = exception ? exception.has(host) : false @@ -217,7 +225,7 @@ export function useUpsertLiveStatusMutation( ) { const ax = useAnalytics() const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const control = useDialogContext() const {_} = useLingui() @@ -226,7 +234,7 @@ export function useUpsertLiveStatusMutation( mutationFn: async () => { if (!currentAccount) throw new Error('Not logged in') - let embed: $Typed | undefined + let embed: $Typed | undefined if (linkMeta) { let thumb @@ -236,11 +244,11 @@ export function useUpsertLiveStatusMutation( const img = await imageToThumb(linkMeta.image) if (img) { const blob = await uploadBlob( - agent, + pdsClient, img.source.path, img.source.mime, ) - thumb = blob.data.blob + thumb = blob.blob } } catch (e: any) { ax.logger.error(`Failed to upload thumbnail for live status`, { @@ -257,7 +265,7 @@ export function useUpsertLiveStatusMutation( $type: 'app.bsky.embed.external#external', title: linkMeta.title ?? '', description: linkMeta.description ?? '', - uri: linkMeta.url, + uri: linkMeta.url as UriString, thumb, }, } @@ -265,32 +273,32 @@ export function useUpsertLiveStatusMutation( const record = { $type: 'app.bsky.actor.status', - createdAt: createdAt ?? new Date().toISOString(), + createdAt: (createdAt ?? new Date().toISOString()) as DatetimeString, status: 'app.bsky.actor.status#live', durationMinutes: duration, embed, - } satisfies AppBskyActorStatus.Record + } satisfies app.bsky.actor.status.Main const upsert = async () => { - const repo = currentAccount.did + const repo = currentAccount.did as AtIdentifierString const collection = 'app.bsky.actor.status' - const existing = await agent.com.atproto.repo - .getRecord({repo, collection, rkey: 'self'}) + const existing = await pdsClient + .call(com.atproto.repo.getRecord, {repo, collection, rkey: 'self'}) .catch(_e => undefined) - await agent.com.atproto.repo.putRecord({ + await pdsClient.call(com.atproto.repo.putRecord, { repo, collection, rkey: 'self', record, - swapRecord: existing?.data.cid || null, + swapRecord: existing?.cid || null, }) } await retry(upsert, { maxRetries: 5, - retryable: e => e instanceof ComAtprotoRepoPutRecord.InvalidSwapError, + retryable: e => getErrorName(e) === 'InvalidSwap', }) return { @@ -347,7 +355,7 @@ export function useUpsertLiveStatusMutation( export function useRemoveLiveStatusMutation() { const ax = useAnalytics() const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const control = useDialogContext() const {_} = useLingui() @@ -356,8 +364,8 @@ export function useRemoveLiveStatusMutation() { mutationFn: async () => { if (!currentAccount) throw new Error('Not logged in') - await agent.app.bsky.actor.status.delete({ - repo: currentAccount.did, + await pdsClient.delete(app.bsky.actor.status, { + repo: currentAccount.did as AtIdentifierString, rkey: 'self', }) }, diff --git a/src/features/liveNow/utils.ts b/src/features/liveNow/utils.ts index a7326cd8e9..9fe63b606c 100644 --- a/src/features/liveNow/utils.ts +++ b/src/features/liveNow/utils.ts @@ -1,19 +1,19 @@ -import {AppBskyActorStatus, AppBskyEmbedExternal} from '@atproto/api' import {type I18n} from '@lingui/core' import {plural} from '@lingui/core/macro' import psl from 'psl' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' + /** * Validates a raw status record and returns the typed record, or null if the * value is not a valid `app.bsky.actor.status` record. */ export function getValidLiveStatusRecord( statusRecord: unknown, -): AppBskyActorStatus.Record | null { - if (!AppBskyActorStatus.isRecord(statusRecord)) return null - const validation = AppBskyActorStatus.validateRecord(statusRecord) - if (!validation.success) return null - return validation.value +): app.bsky.actor.status.Main | null { + if (!bsky.matches(app.bsky.actor.status, statusRecord)) return null + return statusRecord } /** @@ -23,7 +23,7 @@ export function getValidLiveStatusRecord( export function getLiveLinkFromStatusRecord(statusRecord: unknown): string { const record = getValidLiveStatusRecord(statusRecord) if (!record) return '' - if (!AppBskyEmbedExternal.isMain(record.embed)) return '' + if (!bsky.isType(app.bsky.embed.external, record.embed)) return '' return record.embed.external.uri } diff --git a/src/lib/api/__tests__/computeCid.test.ts b/src/lib/api/__tests__/computeCid.test.ts new file mode 100644 index 0000000000..4082c6b553 --- /dev/null +++ b/src/lib/api/__tests__/computeCid.test.ts @@ -0,0 +1,121 @@ +/* + * The jest suite ships global manual mocks for `multiformats/cid` and + * `multiformats/hashes/hasher` (in root `__mocks__/`) so unrelated tests don't + * pull in real crypto. This test is precisely about the real CID hashing, so we + * opt back into the actual implementations here. + */ +jest.unmock('multiformats/cid') +jest.unmock('multiformats/hashes/hasher') + +import {BlobRef} from '@atproto/api' +import {CID} from 'multiformats/cid' + +import {computeCid} from '#/lib/api/computeCid' +import {type app, type com} from '#/lexicons' + +/* + * Golden-CID regression test for the composer post pipeline (design section F). + * + * `computeCid` hashes a post record in the client so a thread's later posts can + * reference earlier posts by CID before the server assigns them. The hash is + * byte-sensitive: any drift in how records (especially blobs) are serialized to + * DAG-CBOR silently produces the wrong CID and breaks reply chains with NO type + * error. These golden values were captured from the PRE-migration `computeCid` + * (the `instanceof BlobRef` path) and MUST remain byte-identical after the guard + * is changed to the structural `isBlobRef` shape check. + * + * The blob CID below is a fixed, deterministic CIDv1/raw/sha256 used purely as a + * stable fixture - it is not derived from any real upload. + */ +const BLOB_CID = 'bafkreieq5jui4j25lacwomsqgjeswwl3y5zcdrresptwgmfylxo2depppq' + +/** + * Build a post record with an image embed whose blob is the given value. Used to + * prove that a `@atproto/api` `BlobRef` class instance (the shape the not-yet + * -migrated video path still yields) and a plain-JSON lex blob (the shape lex + * `uploadBlob` returns) hash to the SAME CID. + */ +function postWithImageBlob(blob: unknown): app.bsky.feed.post.Main { + return { + $type: 'app.bsky.feed.post', + createdAt: '2024-01-01T00:00:00.001Z', + text: 'post with image', + embed: { + $type: 'app.bsky.embed.images', + images: [ + { + image: blob, + alt: 'alt text', + aspectRatio: {width: 100, height: 200}, + }, + ], + }, + } as app.bsky.feed.post.Main +} + +describe('computeCid', () => { + it('case 1: plain post record with no blob', async () => { + const record: app.bsky.feed.post.Main = { + $type: 'app.bsky.feed.post', + createdAt: '2024-01-01T00:00:00.000Z', + text: 'hello world', + } + expect(await computeCid(record)).toBe( + 'bafyreieawtmh7hwfrqpamqkodza5r62bbfhsepe2iyustgxhgbhi6b2lfi', + ) + }) + + it('case 2: record whose embed carries a BlobRef class instance', async () => { + const blob = new BlobRef(CID.parse(BLOB_CID), 'image/jpeg', 12345) + expect(await computeCid(postWithImageBlob(blob))).toBe( + 'bafyreiem7g6vja66nebr7he4fshfnlyndyldbvle2n265oixscmepjcbii', + ) + }) + + it('case 2b: a plain-JSON blob object hashes identically to the class instance', async () => { + /* + * This is the post-migration shape: lex `uploadBlob` returns a plain object + * `{$type: 'blob', ref, mimeType, size}` (with `ref` a parsed CID), not a + * `BlobRef` class instance. The structural `isBlobRef` guard must treat it + * exactly like the class instance so the CID is unchanged. Under the + * pre-change `instanceof` code this case already matches because the plain + * object walks through `prepareForHashing` unchanged and DAG-CBOR encodes + * its CID `ref` the same way `.ipld()` does. + */ + const blob = { + $type: 'blob' as const, + ref: CID.parse(BLOB_CID), + mimeType: 'image/jpeg', + size: 12345, + } + expect(await computeCid(postWithImageBlob(blob))).toBe( + 'bafyreiem7g6vja66nebr7he4fshfnlyndyldbvle2n265oixscmepjcbii', + ) + }) + + it('case 3: three-post thread chains reply StrongRef CIDs', async () => { + const did = 'did:plc:abc123' + const base = new Date('2024-01-01T00:00:00.000Z') + const golden = [ + 'bafyreig62rxs34h5rvznfrracwkjlfgad5b25qxglp2hcziqdfas2nw2ee', + 'bafyreicxcj2tq5jrh5jcaczg3eli5cvxitgzu7kpu3fm5v3njq2byjxirq', + 'bafyreigvaswuhlpd7dllja2xrqswhqbruyv2kar7mvbn7gdm5ldzu6vkti', + ] + + let reply: app.bsky.feed.post.Main['reply'] | undefined + for (let i = 0; i < 3; i++) { + const now = new Date(base.getTime() + i) + const uri = `at://${did}/app.bsky.feed.post/rkey${i}` + const record = { + $type: 'app.bsky.feed.post', + createdAt: now.toISOString(), + text: `post ${i}`, + reply, + } as app.bsky.feed.post.Main + const cid = await computeCid(record) + expect(cid).toBe(golden[i]) + const ref = {cid, uri} as com.atproto.repo.strongRef.Main + reply = {root: reply?.root ?? ref, parent: ref} + } + }) +}) diff --git a/src/lib/api/computeCid.ts b/src/lib/api/computeCid.ts new file mode 100644 index 0000000000..32c97a5225 --- /dev/null +++ b/src/lib/api/computeCid.ts @@ -0,0 +1,158 @@ +import {sha256} from 'js-sha256' +import {CID} from 'multiformats/cid' +import * as Hasher from 'multiformats/hashes/hasher' + +import {app} from '#/lexicons' + +/* + * Client-side CID computation for post records, extracted from the post + * pipeline so it can be unit-tested in isolation (importing the pipeline pulls + * in the native gallery/media chain). See `computeCid.test.ts` for the golden + * -CID regression fixtures that gate any change to this serialization. + */ + +// The built-in hashing functions from multiformats (`multiformats/hashes/sha2`) +// are meant for Node.js, this is the cross-platform equivalent. +const mf_sha256 = Hasher.from({ + name: 'sha2-256', + code: 0x12, + encode: input => { + const digest = sha256.arrayBuffer(input) + return new Uint8Array(digest) + }, +}) + +export async function computeCid( + record: app.bsky.feed.post.Main, +): Promise { + /* + * Lazily loaded since it's only needed when posting a thread, and its + * `cborg` dependency is ~190KB that would otherwise be in the initial + * web bundle. + */ + const dcbor = await importDagCbor() + // IMPORTANT: `prepareObject` prepares the record to be hashed by removing + // fields with undefined value, and converting BlobRef instances to the + // right IPLD representation. + const prepared = prepareForHashing(record) + // 1. Encode the record into DAG-CBOR format + const encoded = dcbor.encode(prepared) + // 2. Hash the record in SHA-256 (code 0x12) + const digest = await mf_sha256.digest(encoded) + // 3. Create a CIDv1, specifying DAG-CBOR as content (code 0x71) + const cid = CID.createV1(0x71, digest) + // 4. Get the Base32 representation of the CID (`b` prefix) + return cid.toString() +} + +/** + * True for a plain-JSON lexicon blob, the shape lex `uploadBlob` now returns + * (`{$type: 'blob', ref, mimeType, size}` with `ref` a parsed CID). Replaces + * the old `instanceof BlobRef` check, since lex blobs are plain objects, not + * class instances (design section F). + */ +function isBlobRef(v: unknown): boolean { + if (v == null || typeof v !== 'object') return false + const o = v as Record + return o.$type === 'blob' && 'ref' in o && 'mimeType' in o +} + +/** + * True for a legacy `@atproto/api` `BlobRef` class instance. During the + * migration the video embed path still yields these (its blob comes from the + * not-yet-migrated `app.bsky.video.getJobStatus` bridge call), so we must keep + * handling them here even though the composer's own uploads are now plain lex + * blobs. A class instance is duck-typed by its `ipld()` method plus the + * `ref`/`mimeType` fields; it has NO `$type` and a non-plain prototype, so it + * would otherwise slip past both `isBlobRef` and `isPlainObject` and be encoded + * wrong - silently breaking video reply-chain CIDs. + */ +function isBlobRefInstance( + v: unknown, +): v is {ipld: () => unknown; ref: unknown; mimeType: unknown} { + if (v == null || typeof v !== 'object') return false + const o = v as Record + return typeof o.ipld === 'function' && 'ref' in o && 'mimeType' in o +} + +// Returns a transformed version of the object for use in DAG-CBOR. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function prepareForHashing(v: any): any { + /* + * A plain-JSON lex blob is already in the right IPLD shape (its `ref` is a + * parsed CID that DAG-CBOR encodes as a CID link), so pass it through + * untouched. + */ + if (isBlobRef(v)) { + return v + } + + /* + * A legacy `BlobRef` class instance must be converted via `ipld()` to the + * plain `{$type, ref, mimeType, size}` object; encoding the instance directly + * would emit its internal `original` field and omit `$type`, producing the + * wrong CID. `ipld()` returns exactly what `isBlobRef` accepts above. + */ + if (isBlobRefInstance(v)) { + return v.ipld() + } + + // Walk through arrays + if (Array.isArray(v)) { + let pure = true + const mapped = v.map(value => { + if (value !== (value = prepareForHashing(value))) { + pure = false + } + return value + }) + return pure ? v : mapped + } + + // Walk through plain objects + if (isPlainObject(v)) { + const rec = v as Record + const obj: Record = {} + let pure = true + for (const key in rec) { + let value = rec[key] + // `value` is undefined + if (value === undefined) { + pure = false + continue + } + // `prepareObject` returned a value that's different from what we had before + if (value !== (value = prepareForHashing(value))) { + pure = false + } + obj[key] = value + } + // Return as is if we haven't needed to tamper with anything + return pure ? v : obj + } + return v +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function isPlainObject(v: any): boolean { + if (typeof v !== 'object' || v === null) { + return false + } + const proto = Object.getPrototypeOf(v) + return proto === Object.prototype || proto === null +} + +/** + * Load `@ipld/dag-cbor` on demand. The dynamic `import()` lets web bundlers + * emit it (and its ~190KB `cborg` dependency) as a separate chunk that only + * loads when posting a thread. Under jest (which runs without + * `--experimental-vm-modules`) dynamic import throws, so we fall back to a + * lazy `require`, which resolves through the test moduleNameMapper. + */ +function importDagCbor(): Promise { + if (process.env.NODE_ENV === 'test') { + // eslint-disable-next-line @typescript-eslint/no-require-imports + return Promise.resolve(require('@ipld/dag-cbor')) + } + return import('@ipld/dag-cbor') +} diff --git a/src/lib/api/feed/author.ts b/src/lib/api/feed/author.ts index a830065c0f..456c0d6a56 100644 --- a/src/lib/api/feed/author.ts +++ b/src/lib/api/feed/author.ts @@ -1,23 +1,21 @@ -import { - AppBskyFeedDefs, - type AppBskyFeedGetAuthorFeed as GetAuthorFeed, -} from '@atproto/api' +import {type Client} from '@atproto/lex-client' -import {type SessionAgent} from '#/state/session' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import {type FeedAPI, type FeedAPIResponse} from './types' export class AuthorFeedAPI implements FeedAPI { - agent: SessionAgent - _params: GetAuthorFeed.QueryParams + client: Client + _params: app.bsky.feed.getAuthorFeed.$Params constructor({ - agent, + client, feedParams, }: { - agent: SessionAgent - feedParams: GetAuthorFeed.QueryParams + client: Client + feedParams: app.bsky.feed.getAuthorFeed.$Params }) { - this.agent = agent + this.client = client this._params = feedParams } @@ -27,12 +25,12 @@ export class AuthorFeedAPI implements FeedAPI { return params } - async peekLatest(): Promise { - const res = await this.agent.getAuthorFeed({ + async peekLatest(): Promise { + const res = await this.client.call(app.bsky.feed.getAuthorFeed, { ...this.params, limit: 1, }) - return res.data.feed[0] + return res.feed[0] } async fetch({ @@ -42,28 +40,26 @@ export class AuthorFeedAPI implements FeedAPI { cursor: string | undefined limit: number }): Promise { - const res = await this.agent.getAuthorFeed({ + const res = await this.client.call(app.bsky.feed.getAuthorFeed, { ...this.params, cursor, limit, }) - if (res.success) { - return { - cursor: res.data.cursor, - feed: this._filter(res.data.feed), - } - } return { - feed: [], + cursor: res.cursor, + feed: this._filter(res.feed), } } - _filter(feed: AppBskyFeedDefs.FeedViewPost[]) { + _filter(feed: app.bsky.feed.defs.FeedViewPost[]) { if (this.params.filter === 'posts_and_author_threads') { return feed.filter(post => { const isReply = post.reply - const isRepost = AppBskyFeedDefs.isReasonRepost(post.reason) - const isPin = AppBskyFeedDefs.isReasonPin(post.reason) + const isRepost = bsky.isType( + app.bsky.feed.defs.reasonRepost, + post.reason, + ) + const isPin = bsky.isType(app.bsky.feed.defs.reasonPin, post.reason) if (!isReply) return true if (isRepost || isPin) return true return isReply && isAuthorReplyChain(this.params.actor, post, feed) @@ -76,15 +72,15 @@ export class AuthorFeedAPI implements FeedAPI { function isAuthorReplyChain( actor: string, - post: AppBskyFeedDefs.FeedViewPost, - posts: AppBskyFeedDefs.FeedViewPost[], + post: app.bsky.feed.defs.FeedViewPost, + posts: app.bsky.feed.defs.FeedViewPost[], ): boolean { // current post is by a different user (shouldn't happen) if (post.post.author.did !== actor) return false const replyParent = post.reply?.parent - if (AppBskyFeedDefs.isPostView(replyParent)) { + if (bsky.isType(app.bsky.feed.defs.postView, replyParent)) { // reply parent is by a different user if (replyParent.author.did !== actor) return false diff --git a/src/lib/api/feed/custom.ts b/src/lib/api/feed/custom.ts index 5a838c5354..038c76140c 100644 --- a/src/lib/api/feed/custom.ts +++ b/src/lib/api/feed/custom.ts @@ -1,47 +1,53 @@ -import { - type AppBskyFeedDefs, - type AppBskyFeedGetFeed as GetCustomFeed, - AtpAgent, - jsonStringToLex, -} from '@atproto/api' +import {lexParse} from '@atproto/lex' +import {Client} from '@atproto/lex-client' import { getAppLanguageAsContentLanguage, getContentLanguages, } from '#/state/preferences/languages' -import {type SessionAgent} from '#/state/session' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' import {createBskyTopicsHeader, isBlueskyOwnedFeed} from './utils' +/** + * Input params for {@link CustomFeedAPI}. The generated `$Params` type reflects + * post-parse output, where `limit` (which has a lexicon default) is required; + * callers supply only `feed` and let `limit`/`cursor` come from `fetch`. + */ +type CustomFeedParams = {feed: app.bsky.feed.getFeed.$Params['feed']} & Partial< + Omit +> + export class CustomFeedAPI implements FeedAPI { - agent: SessionAgent - params: GetCustomFeed.QueryParams + client: Client + params: CustomFeedParams userInterests?: string constructor({ - agent, + client, feedParams, userInterests, }: { - agent: SessionAgent - feedParams: GetCustomFeed.QueryParams + client: Client + feedParams: CustomFeedParams userInterests?: string }) { - this.agent = agent + this.client = client this.params = feedParams this.userInterests = userInterests } - async peekLatest(): Promise { + async peekLatest(): Promise { const contentLangs = getContentLanguages().join(',') - const res = await this.agent.app.bsky.feed.getFeed( + const res = await this.client.call( + app.bsky.feed.getFeed, { ...this.params, limit: 1, }, {headers: {'Accept-Language': contentLangs}}, ) - return res.data.feed[0] + return res.feed[0] } async fetch({ @@ -52,41 +58,49 @@ export class CustomFeedAPI implements FeedAPI { limit: number }): Promise { const contentLangs = getContentLanguages().join(',') - const agent = this.agent const isBlueskyOwned = isBlueskyOwnedFeed(this.params.feed) - const res = agent.did - ? await this.agent.app.bsky.feed.getFeed( - { - ...this.params, - cursor, - limit, + let feed: app.bsky.feed.defs.FeedViewPost[] + let resCursor: string | undefined + + if (this.client.did) { + const res = await this.client.call( + app.bsky.feed.getFeed, + { + ...this.params, + cursor, + limit, + }, + { + headers: { + ...(isBlueskyOwned + ? createBskyTopicsHeader(this.userInterests) + : {}), + 'Accept-Language': contentLangs, }, - { - headers: { - ...(isBlueskyOwned - ? createBskyTopicsHeader(this.userInterests) - : {}), - 'Accept-Language': contentLangs, - }, - }, - ) - : await loggedOutFetch({...this.params, cursor, limit}) - if (res.success) { - // NOTE - // some custom feeds fail to enforce the pagination limit - // so we manually truncate here - // -prf - if (res.data.feed.length > limit) { - res.data.feed = res.data.feed.slice(0, limit) - } - return { - cursor: res.data.feed.length ? res.data.cursor : undefined, - feed: res.data.feed, + }, + ) + feed = res.feed + resCursor = res.cursor + } else { + const res = await loggedOutFetch({...this.params, cursor, limit}) + if (!res.success) { + return {feed: []} } + feed = res.data.feed + resCursor = res.data.cursor + } + + // NOTE + // some custom feeds fail to enforce the pagination limit + // so we manually truncate here + // -prf + if (feed.length > limit) { + feed = feed.slice(0, limit) } return { - feed: [], + cursor: feed.length ? resCursor : undefined, + feed, } } } @@ -106,15 +120,16 @@ async function loggedOutFetch({ feed: string limit: number cursor?: string -}) { +}): Promise<{success: boolean; data: app.bsky.feed.getFeed.$OutputBody}> { let contentLangs = getAppLanguageAsContentLanguage() - /** - * Copied from our root `Agent` class - * @see https://github.com/bluesky-social/atproto/blob/60df3fc652b00cdff71dd9235d98a7a4bb828f05/packages/api/src/agent.ts#L120 + /* + * Copied from our root `Agent` class. The global (`;redact`-suffixed) app + * labelers are kept on the lex `Client` static (synced in + * `#/state/session/moderation`), replacing the old `AtpAgent.appLabelers`. */ const labelersHeader = { - 'atproto-accept-labelers': AtpAgent.appLabelers + 'atproto-accept-labelers': Client.appLabelers .map(l => `${l};redact`) .join(', '), } @@ -130,7 +145,7 @@ async function loggedOutFetch({ }, ) let data = res.ok - ? (jsonStringToLex(await res.text()) as GetCustomFeed.OutputSchema) + ? (lexParse(await res.text()) as app.bsky.feed.getFeed.$OutputBody) : null if (data?.feed?.length) { return { @@ -147,7 +162,7 @@ async function loggedOutFetch({ {method: 'GET', headers: {'Accept-Language': '', ...labelersHeader}}, ) data = res.ok - ? (jsonStringToLex(await res.text()) as GetCustomFeed.OutputSchema) + ? (lexParse(await res.text()) as app.bsky.feed.getFeed.$OutputBody) : null if (data?.feed?.length) { return { diff --git a/src/lib/api/feed/demo.ts b/src/lib/api/feed/demo.ts index 5399419038..72878917d5 100644 --- a/src/lib/api/feed/demo.ts +++ b/src/lib/api/feed/demo.ts @@ -1,21 +1,28 @@ -import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client} from '@atproto/lex-client' import {DEMO_FEED} from '#/lib/demo' -import {type SessionAgent} from '#/state/session' +import {type app} from '#/lexicons' +import {toLex} from '#/types/bsky' import {type FeedAPI, type FeedAPIResponse} from './types' export class DemoFeedAPI implements FeedAPI { - agent: SessionAgent + client: Client - constructor({agent}: {agent: SessionAgent}) { - this.agent = agent + constructor({client}: {client: Client}) { + this.client = client } - async peekLatest(): Promise { - return DEMO_FEED.feed[0] + async peekLatest(): Promise { + /* + * TODO(phase4): drop this cast once `#/lib/demo` (DEMO_FEED) sources its + * feed items from `#/lexicons`. Its records are still typed against the old + * `@atproto/api` FeedViewPost, which does not assign to the branded lexicon + * type this method must return. + */ + return toLex(DEMO_FEED.feed[0]) } async fetch(): Promise { - return DEMO_FEED + return toLex(DEMO_FEED) } } diff --git a/src/lib/api/feed/following.ts b/src/lib/api/feed/following.ts index 9e1e339bd5..56c96b777b 100644 --- a/src/lib/api/feed/following.ts +++ b/src/lib/api/feed/following.ts @@ -1,20 +1,20 @@ -import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client} from '@atproto/lex-client' -import {type SessionAgent} from '#/state/session' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' export class FollowingFeedAPI implements FeedAPI { - agent: SessionAgent + client: Client - constructor({agent}: {agent: SessionAgent}) { - this.agent = agent + constructor({client}: {client: Client}) { + this.client = client } - async peekLatest(): Promise { - const res = await this.agent.getTimeline({ + async peekLatest(): Promise { + const res = await this.client.call(app.bsky.feed.getTimeline, { limit: 1, }) - return res.data.feed[0] + return res.feed[0] } async fetch({ @@ -24,18 +24,13 @@ export class FollowingFeedAPI implements FeedAPI { cursor: string | undefined limit: number }): Promise { - const res = await this.agent.getTimeline({ + const res = await this.client.call(app.bsky.feed.getTimeline, { cursor, limit, }) - if (res.success) { - return { - cursor: res.data.cursor, - feed: res.data.feed, - } - } return { - feed: [], + cursor: res.cursor, + feed: res.feed, } } } diff --git a/src/lib/api/feed/home.ts b/src/lib/api/feed/home.ts index 1ebc7e28c3..ffac360ca9 100644 --- a/src/lib/api/feed/home.ts +++ b/src/lib/api/feed/home.ts @@ -1,7 +1,8 @@ -import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client} from '@atproto/lex-client' +import {type AtUriString} from '@atproto/syntax' import {PROD_DEFAULT_FEED} from '#/lib/constants' -import {type SessionAgent} from '#/state/session' +import {type app} from '#/lexicons' import {CustomFeedAPI} from './custom' import {FollowingFeedAPI} from './following' import {type FeedAPI, type FeedAPIResponse} from './types' @@ -14,7 +15,11 @@ 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: AppBskyFeedDefs.FeedViewPost = { +/* + * A sentinel post whose fields intentionally violate the branded lexicon + * formats (`uri`, `did`, `indexedAt`), so it is asserted into the view type. + */ +export const FALLBACK_MARKER_POST: app.bsky.feed.defs.FeedViewPost = { post: { uri: 'fallback-marker-post', cid: 'fake', @@ -25,10 +30,10 @@ export const FALLBACK_MARKER_POST: AppBskyFeedDefs.FeedViewPost = { }, indexedAt: new Date().toISOString(), }, -} +} as unknown as app.bsky.feed.defs.FeedViewPost export class HomeFeedAPI implements FeedAPI { - agent: SessionAgent + client: Client following: FollowingFeedAPI discover: CustomFeedAPI usingDiscover = false @@ -37,32 +42,32 @@ export class HomeFeedAPI implements FeedAPI { constructor({ userInterests, - agent, + client, }: { userInterests?: string - agent: SessionAgent + client: Client }) { - this.agent = agent - this.following = new FollowingFeedAPI({agent}) + this.client = client + this.following = new FollowingFeedAPI({client}) this.discover = new CustomFeedAPI({ - agent, - feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')}, + client, + feedParams: {feed: PROD_DEFAULT_FEED('whats-hot') as AtUriString}, }) this.userInterests = userInterests } reset() { - this.following = new FollowingFeedAPI({agent: this.agent}) + this.following = new FollowingFeedAPI({client: this.client}) this.discover = new CustomFeedAPI({ - agent: this.agent, - feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')}, + client: this.client, + feedParams: {feed: PROD_DEFAULT_FEED('whats-hot') as AtUriString}, userInterests: this.userInterests, }) this.usingDiscover = false this.itemCursor = 0 } - async peekLatest(): Promise { + async peekLatest(): Promise { if (this.usingDiscover) { return this.discover.peekLatest() } @@ -81,7 +86,7 @@ export class HomeFeedAPI implements FeedAPI { } let returnCursor - let posts: AppBskyFeedDefs.FeedViewPost[] = [] + let posts: app.bsky.feed.defs.FeedViewPost[] = [] if (!this.usingDiscover) { const res = await this.following.fetch({cursor, limit}) diff --git a/src/lib/api/feed/likes.ts b/src/lib/api/feed/likes.ts index ee2018f625..3531a58f44 100644 --- a/src/lib/api/feed/likes.ts +++ b/src/lib/api/feed/likes.ts @@ -1,32 +1,29 @@ -import { - type AppBskyFeedDefs, - type AppBskyFeedGetActorLikes as GetActorLikes, -} from '@atproto/api' +import {type Client} from '@atproto/lex-client' -import {type SessionAgent} from '#/state/session' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' export class LikesFeedAPI implements FeedAPI { - agent: SessionAgent - params: GetActorLikes.QueryParams + client: Client + params: app.bsky.feed.getActorLikes.$Params constructor({ - agent, + client, feedParams, }: { - agent: SessionAgent - feedParams: GetActorLikes.QueryParams + client: Client + feedParams: app.bsky.feed.getActorLikes.$Params }) { - this.agent = agent + this.client = client this.params = feedParams } - async peekLatest(): Promise { - const res = await this.agent.getActorLikes({ + async peekLatest(): Promise { + const res = await this.client.call(app.bsky.feed.getActorLikes, { ...this.params, limit: 1, }) - return res.data.feed[0] + return res.feed[0] } async fetch({ @@ -36,21 +33,16 @@ export class LikesFeedAPI implements FeedAPI { cursor: string | undefined limit: number }): Promise { - const res = await this.agent.getActorLikes({ + const res = await this.client.call(app.bsky.feed.getActorLikes, { ...this.params, cursor, limit, }) - if (res.success) { - // HACKFIX: the API incorrectly returns a cursor when there are no items -sfn - const isEmptyPage = res.data.feed.length === 0 - return { - cursor: isEmptyPage ? undefined : res.data.cursor, - feed: res.data.feed, - } - } + // HACKFIX: the API incorrectly returns a cursor when there are no items -sfn + const isEmptyPage = res.feed.length === 0 return { - feed: [], + cursor: isEmptyPage ? undefined : res.cursor, + feed: res.feed, } } } diff --git a/src/lib/api/feed/list.ts b/src/lib/api/feed/list.ts index 9697b0aaf3..5278e7dae4 100644 --- a/src/lib/api/feed/list.ts +++ b/src/lib/api/feed/list.ts @@ -1,32 +1,29 @@ -import { - type Agent, - type AppBskyFeedDefs, - type AppBskyFeedGetListFeed as GetListFeed, -} from '@atproto/api' +import {type Client} from '@atproto/lex-client' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' export class ListFeedAPI implements FeedAPI { - agent: Agent - params: GetListFeed.QueryParams + client: Client + params: app.bsky.feed.getListFeed.$Params constructor({ - agent, + client, feedParams, }: { - agent: Agent - feedParams: GetListFeed.QueryParams + client: Client + feedParams: app.bsky.feed.getListFeed.$Params }) { - this.agent = agent + this.client = client this.params = feedParams } - async peekLatest(): Promise { - const res = await this.agent.app.bsky.feed.getListFeed({ + async peekLatest(): Promise { + const res = await this.client.call(app.bsky.feed.getListFeed, { ...this.params, limit: 1, }) - return res.data.feed[0] + return res.feed[0] } async fetch({ @@ -36,19 +33,14 @@ export class ListFeedAPI implements FeedAPI { cursor: string | undefined limit: number }): Promise { - const res = await this.agent.app.bsky.feed.getListFeed({ + const res = await this.client.call(app.bsky.feed.getListFeed, { ...this.params, cursor, limit, }) - if (res.success) { - return { - cursor: res.data.cursor, - feed: res.data.feed, - } - } return { - feed: [], + cursor: res.cursor, + feed: res.feed, } } } diff --git a/src/lib/api/feed/merge.ts b/src/lib/api/feed/merge.ts index 354027a57d..49fbba0ae0 100644 --- a/src/lib/api/feed/merge.ts +++ b/src/lib/api/feed/merge.ts @@ -1,4 +1,5 @@ -import {type AppBskyFeedDefs, type AppBskyFeedGetTimeline} from '@atproto/api' +import {type Client} from '@atproto/lex-client' +import {type AtUriString} from '@atproto/syntax' import shuffle from 'lodash.shuffle' import {bundleAsync} from '#/lib/async/bundle' @@ -6,7 +7,8 @@ import {timeout} from '#/lib/async/timeout' import {feedUriToHref} from '#/lib/strings/url-helpers' import {getContentLanguages} from '#/state/preferences/languages' import {type FeedParams} from '#/state/queries/post-feed' -import {type SessionAgent} from '#/state/session' +import {app} from '#/lexicons' +import {toLex} from '#/types/bsky' import {FeedTuner} from '../feed-manip' import {type FeedTunerFn} from '../feed-manip' import { @@ -19,9 +21,21 @@ import {createBskyTopicsHeader, isBlueskyOwnedFeed} from './utils' const REQUEST_WAIT_MS = 500 // 500ms const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours +/** + * Internal result shape for a single feed page fetch. Lex `client.call` + * returns the response body directly (throwing on error), so we no longer + * carry the old `{success, headers, data}` wrapper - `success` here just + * distinguishes an empty/errored fetch from a populated one. + */ +type FeedPage = { + success: boolean + cursor?: string + feed: app.bsky.feed.defs.FeedViewPost[] +} + export class MergeFeedAPI implements FeedAPI { userInterests?: string - agent: SessionAgent + client: Client params: FeedParams feedTuners: FeedTunerFn[] following: MergeFeedSource_Following @@ -31,29 +45,29 @@ export class MergeFeedAPI implements FeedAPI { sampleCursor = 0 constructor({ - agent, + client, feedParams, feedTuners, userInterests, }: { - agent: SessionAgent + client: Client feedParams: FeedParams feedTuners: FeedTunerFn[] userInterests?: string }) { - this.agent = agent + this.client = client this.params = feedParams this.feedTuners = feedTuners this.userInterests = userInterests this.following = new MergeFeedSource_Following({ - agent: this.agent, + client: this.client, feedTuners: this.feedTuners, }) } reset() { this.following = new MergeFeedSource_Following({ - agent: this.agent, + client: this.client, feedTuners: this.feedTuners, }) this.customFeeds = [] @@ -65,7 +79,7 @@ export class MergeFeedAPI implements FeedAPI { this.params.mergeFeedSources.map( feedUri => new MergeFeedSource_Custom({ - agent: this.agent, + client: this.client, feedUri, feedTuners: this.feedTuners, userInterests: this.userInterests, @@ -77,11 +91,11 @@ export class MergeFeedAPI implements FeedAPI { } } - async peekLatest(): Promise { - const res = await this.agent.getTimeline({ + async peekLatest(): Promise { + const res = await this.client.call(app.bsky.feed.getTimeline, { limit: 1, }) - return res.data.feed[0] + return res.feed[0] } async fetch({ @@ -124,7 +138,7 @@ export class MergeFeedAPI implements FeedAPI { await Promise.all(promises) // assemble a response by sampling from feeds with content - const posts: AppBskyFeedDefs.FeedViewPost[] = [] + const posts: app.bsky.feed.defs.FeedViewPost[] = [] while (posts.length < limit) { let slice = this.sampleItem() if (slice[0]) { @@ -172,21 +186,21 @@ export class MergeFeedAPI implements FeedAPI { } class MergeFeedSource { - agent: SessionAgent + client: Client feedTuners: FeedTunerFn[] sourceInfo: ReasonFeedSource | undefined cursor: string | undefined = undefined - queue: AppBskyFeedDefs.FeedViewPost[] = [] + queue: app.bsky.feed.defs.FeedViewPost[] = [] hasMore = true constructor({ - agent, + client, feedTuners, }: { - agent: SessionAgent + client: Client feedTuners: FeedTunerFn[] }) { - this.agent = agent + this.client = client this.feedTuners = feedTuners } @@ -198,7 +212,7 @@ class MergeFeedSource { return this.hasMore && this.queue.length === 0 } - take(n: number): AppBskyFeedDefs.FeedViewPost[] { + take(n: number): app.bsky.feed.defs.FeedViewPost[] { return this.queue.splice(0, n) } @@ -209,9 +223,9 @@ class MergeFeedSource { _fetchNextInner = bundleAsync(async (n: number) => { const res = await this._getFeed(this.cursor, n) if (res.success) { - this.cursor = res.data.cursor - if (res.data.feed.length) { - this.queue = this.queue.concat(res.data.feed) + this.cursor = res.cursor + if (res.feed.length) { + this.queue = this.queue.concat(res.feed) } else { this.hasMore = false } @@ -223,7 +237,7 @@ class MergeFeedSource { protected _getFeed( _cursor: string | undefined, _limit: number, - ): Promise { + ): Promise { throw new Error('Must be overridden') } } @@ -238,39 +252,51 @@ class MergeFeedSource_Following extends MergeFeedSource { protected async _getFeed( cursor: string | undefined, limit: number, - ): Promise { - const res = await this.agent.getTimeline({cursor, limit}) + ): Promise { + const res = await this.client.call(app.bsky.feed.getTimeline, { + cursor, + limit, + }) // run the tuner pre-emptively to ensure better mixing - const slices = this.tuner.tune(res.data.feed, { + const slices = this.tuner.tune(res.feed, { dryRun: false, }) - res.data.feed = slices.map(slice => slice._feedPost) - return res + return { + success: true, + cursor: res.cursor, + /* + * TODO(phase4): drop the toLex once `#/lib/api/feed-manip` (FeedTuner) + * flips its FeedViewPost source from `@atproto/api` to `#/lexicons`. Its + * `_feedPost` is still the old-world view type, which does not assign to + * the branded lexicon type this page carries. + */ + feed: slices.map(slice => toLex(slice._feedPost)), + } } } class MergeFeedSource_Custom extends MergeFeedSource { - agent: SessionAgent + client: Client minDate: Date feedUri: string userInterests?: string constructor({ - agent, + client, feedUri, feedTuners, userInterests, }: { - agent: SessionAgent + client: Client feedUri: string feedTuners: FeedTunerFn[] userInterests?: string }) { super({ - agent, + client, feedTuners, }) - this.agent = agent + this.client = client this.feedUri = feedUri this.userInterests = userInterests this.sourceInfo = { @@ -284,15 +310,16 @@ class MergeFeedSource_Custom extends MergeFeedSource { protected async _getFeed( cursor: string | undefined, limit: number, - ): Promise { + ): Promise { try { const contentLangs = getContentLanguages().join(',') const isBlueskyOwned = isBlueskyOwnedFeed(this.feedUri) - const res = await this.agent.app.bsky.feed.getFeed( + const res = await this.client.call( + app.bsky.feed.getFeed, { cursor, limit, - feed: this.feedUri, + feed: this.feedUri as AtUriString, }, { headers: { @@ -303,26 +330,25 @@ class MergeFeedSource_Custom extends MergeFeedSource { }, }, ) + let feed = res.feed // NOTE // some custom feeds fail to enforce the pagination limit // so we manually truncate here // -prf - if (limit && res.data.feed.length > limit) { - res.data.feed = res.data.feed.slice(0, limit) + if (limit && feed.length > limit) { + feed = feed.slice(0, limit) } // filter out older posts - res.data.feed = res.data.feed.filter( - post => new Date(post.post.indexedAt) > this.minDate, - ) + feed = feed.filter(post => new Date(post.post.indexedAt) > this.minDate) // attach source info - for (const post of res.data.feed) { + for (const post of feed) { // @ts-ignore post.__source = this.sourceInfo } - return res + return {success: true, cursor: res.cursor, feed} } catch { // dont bubble custom-feed errors - return {success: false, headers: {}, data: {feed: []}} + return {success: false, feed: []} } } } diff --git a/src/lib/api/feed/posts.ts b/src/lib/api/feed/posts.ts index 33eff50997..6093ff6698 100644 --- a/src/lib/api/feed/posts.ts +++ b/src/lib/api/feed/posts.ts @@ -1,25 +1,22 @@ -import { - type Agent, - type AppBskyFeedDefs, - type AppBskyFeedGetPosts, -} from '@atproto/api' +import {type Client} from '@atproto/lex-client' import {logger} from '#/logger' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' export class PostListFeedAPI implements FeedAPI { - agent: Agent - params: AppBskyFeedGetPosts.QueryParams - peek: AppBskyFeedDefs.FeedViewPost | null = null + client: Client + params: app.bsky.feed.getPosts.$Params + peek: app.bsky.feed.defs.FeedViewPost | null = null constructor({ - agent, + client, feedParams, }: { - agent: Agent - feedParams: AppBskyFeedGetPosts.QueryParams + client: Client + feedParams: app.bsky.feed.getPosts.$Params }) { - this.agent = agent + this.client = client if (feedParams.uris.length > 25) { logger.warn( `Too many URIs provided - expected 25, got ${feedParams.uris.length}`, @@ -30,23 +27,18 @@ export class PostListFeedAPI implements FeedAPI { } } - async peekLatest(): Promise { + async peekLatest(): Promise { if (this.peek) return this.peek throw new Error('Has not fetched yet') } async fetch({}: {}): Promise { - const res = await this.agent.app.bsky.feed.getPosts({ + const res = await this.client.call(app.bsky.feed.getPosts, { ...this.params, }) - if (res.success) { - this.peek = {post: res.data.posts[0]} - return { - feed: res.data.posts.map(post => ({post})), - } - } + this.peek = {post: res.posts[0]} return { - feed: [], + feed: res.posts.map(post => ({post})), } } } diff --git a/src/lib/api/feed/types.ts b/src/lib/api/feed/types.ts index 27fa066fbd..59803a992a 100644 --- a/src/lib/api/feed/types.ts +++ b/src/lib/api/feed/types.ts @@ -1,12 +1,12 @@ -import {type AppBskyFeedDefs} from '@atproto/api' +import {type app} from '#/lexicons' export interface FeedAPIResponse { cursor?: string - feed: AppBskyFeedDefs.FeedViewPost[] + feed: app.bsky.feed.defs.FeedViewPost[] } export interface FeedAPI { - peekLatest(): Promise + peekLatest(): Promise fetch({ cursor, limit, diff --git a/src/lib/api/feed/utils.ts b/src/lib/api/feed/utils.ts index c52f402326..b6ef738fcc 100644 --- a/src/lib/api/feed/utils.ts +++ b/src/lib/api/feed/utils.ts @@ -1,4 +1,4 @@ -import {AtUri} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {BSKY_FEED_OWNER_DIDS} from '#/lib/constants' import {type UsePreferencesQueryResponse} from '#/state/queries/preferences' diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 2138b75a18..6999e3938e 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -1,25 +1,14 @@ -import { - type $Typed, - type AppBskyEmbedExternal, - type AppBskyEmbedGallery, - type AppBskyEmbedImages, - type AppBskyEmbedRecord, - type AppBskyEmbedRecordWithMedia, - type AppBskyEmbedVideo, - AppBskyFeedPost, - BlobRef, - ChatBskyGroupDefs, - type ComAtprotoLabelDefs, - type ComAtprotoRepoApplyWrites, - type ComAtprotoRepoStrongRef, - RichText, -} from '@atproto/api' import {TID} from '@atproto/common-web' +import {type $Typed} from '@atproto/lex' +import {type Client} from '@atproto/lex-client' +import { + type AtUriString, + toDatetimeString, + type UriString, +} from '@atproto/syntax' +import {RichText} from '@bsky.app/sdk/richtext' import {t} from '@lingui/core/macro' import {type QueryClient} from '@tanstack/react-query' -import {sha256} from 'js-sha256' -import {CID} from 'multiformats/cid' -import * as Hasher from 'multiformats/hashes/hasher' import {IMAGE_SIZE_CONFIG_POSTS} from '#/lib/constants' import {isNetworkError} from '#/lib/strings/errors' @@ -34,18 +23,33 @@ import { createThreadgateRecord, threadgateAllowUISettingToAllowRecordValue, } from '#/state/queries/threadgate' -import {type SessionAgent} from '#/state/session' import { type EmbedDraft, type PostDraft, type ThreadDraft, } from '#/view/com/composer/state/composer' +import {app, chat, com} from '#/lexicons' import * as bsky from '#/types/bsky' import {createGIFDescription} from '../gif-alt-text' +import {computeCid} from './computeCid' +import {type ResolveClients} from './resolve' import {uploadBlob} from './upload-blob' export {uploadBlob} +/** + * The lex clients the post pipeline needs. `pdsClient` handles writes to the + * user's repo (applyWrites, uploadBlob) - proxied to their PDS, never the + * appview. `appviewClient` handles the `app.bsky.*` reads (getPosts, when + * resolving a reply's root). `resolveClients` is threaded to the link/gif + * resolvers, which need the appview + chat + bridge agent (design section H). + */ +export type PostClients = { + pdsClient: Client + appviewClient: Client + resolveClients: ResolveClients +} + interface PostOpts { thread: ThreadDraft replyTo?: string @@ -54,20 +58,21 @@ interface PostOpts { } export async function post( - agent: SessionAgent, + clients: PostClients, queryClient: QueryClient, opts: PostOpts, ) { + const {pdsClient, appviewClient, resolveClients} = clients const thread = opts.thread opts.onStateChange?.(t`Processing...`) let replyPromise: - | Promise - | AppBskyFeedPost.Record['reply'] + | Promise + | app.bsky.feed.post.Main['reply'] | undefined if (opts.replyTo) { // Not awaited to avoid waterfalls. - replyPromise = resolveReply(agent, opts.replyTo) + replyPromise = resolveReply(appviewClient, opts.replyTo) } // add top 3 languages from user preferences if langs is provided @@ -76,8 +81,8 @@ export async function post( langs = opts.langs.slice(0, 3) } - const did = agent.assertDid - const writes: $Typed[] = [] + const did = pdsClient.assertDid + const writes: com.atproto.repo.applyWrites.$InputBody['writes'] = [] const uris: string[] = [] let now = new Date() @@ -86,15 +91,23 @@ export async function post( for (let i = 0; i < thread.posts.length; i++) { const draft = thread.posts[i] - // Not awaited to avoid waterfalls. - const rtPromise = resolveRT(agent, draft.richtext) + /* + * Not awaited to avoid waterfalls. `draft.richtext` is still an + * `@atproto/api` RichText (composer state is migrated by Task 7); the SDK + * RichText is structurally the same transplant, so bridge it here. + * TODO(phase4): drop toLex once composer state migrates to SDK RichText. + */ + const rtPromise = resolveRT( + resolveClients.appview, + bsky.toLex(draft.richtext), + ) const embedPromise = resolveEmbed( - agent, + clients, queryClient, draft, opts.onStateChange, ) - let labels: $Typed | undefined + let labels: $Typed | undefined if (draft.labels.length) { labels = { $type: 'com.atproto.label.defs#selfLabels', @@ -107,17 +120,17 @@ export async function post( now.setMilliseconds(now.getMilliseconds() + 1) tid = TID.next(tid) const rkey = tid.toString() - const uri = `at://${did}/app.bsky.feed.post/${rkey}` + const uri = `at://${did}/app.bsky.feed.post/${rkey}` as AtUriString uris.push(uri) const rt = await rtPromise const embed = await embedPromise const reply = await replyPromise - const record: AppBskyFeedPost.Record = { + const record: app.bsky.feed.post.Main = { // IMPORTANT: $type has to exist, CID is calculated with the `$type` field // present and will produce the wrong CID if you omit it. $type: 'app.bsky.feed.post', - createdAt: now.toISOString(), + createdAt: toDatetimeString(now), text: rt.text, facets: rt.facets, reply, @@ -125,11 +138,17 @@ export async function post( langs, labels, } + /* + * `value` is typed `LexMap` (a loose index-signature map). A generated + * record type is a valid LexMap at runtime but a strict interface is not + * assignable to an index-signature type, so widen with `toLex` at this + * boundary. Not a brand cast - the record is already fully typed. + */ writes.push({ $type: 'com.atproto.repo.applyWrites#create', collection: 'app.bsky.feed.post', rkey: rkey, - value: record, + value: bsky.toLex(record), }) if (i === 0 && thread.threadgate.some(tg => tg.type !== 'everybody')) { @@ -137,11 +156,14 @@ export async function post( $type: 'com.atproto.repo.applyWrites#create', collection: 'app.bsky.feed.threadgate', rkey: rkey, - value: createThreadgateRecord({ - createdAt: now.toISOString(), - post: uri, - allow: threadgateAllowUISettingToAllowRecordValue(thread.threadgate), - }), + value: bsky.toLex( + createThreadgateRecord({ + post: uri, + allow: threadgateAllowUISettingToAllowRecordValue( + thread.threadgate, + ), + }), + ), }) } @@ -153,12 +175,12 @@ export async function post( $type: 'com.atproto.repo.applyWrites#create', collection: 'app.bsky.feed.postgate', rkey: rkey, - value: { + value: bsky.toLex({ ...thread.postgate, $type: 'app.bsky.feed.postgate', createdAt: now.toISOString(), post: uri, - }, + }), }) } @@ -174,8 +196,8 @@ export async function post( } try { - await agent.com.atproto.repo.applyWrites({ - repo: agent.assertDid, + await pdsClient.call(com.atproto.repo.applyWrites, { + repo: did, writes: writes, validate: true, }) @@ -196,14 +218,18 @@ export async function post( return {uris} } -async function resolveRT(agent: SessionAgent, richtext: RichText) { +async function resolveRT(appviewClient: Client, richtext: RichText) { const trimmedText = richtext.text // Trim leading whitespace-only lines (but don't break ASCII art). .replace(/^(\s*\n)+/, '') // Trim any trailing whitespace. .trimEnd() let rt = new RichText({text: trimmedText}, {cleanNewlines: true}) - await rt.detectFacets(agent) + /* + * Facet detection resolves handles via `com.atproto.identity.resolveHandle`, + * which the appview client serves (design section B). + */ + await rt.detectFacets(appviewClient) rt = shortenLinks(rt) rt = stripInvalidMentions(rt) @@ -216,9 +242,9 @@ export class ReplyDeletedError extends Error { } } -async function resolveReply(agent: SessionAgent, replyTo: string) { - const {data} = await agent.app.bsky.feed.getPosts({ - uris: [replyTo], +async function resolveReply(appviewClient: Client, replyTo: string) { + const data = await appviewClient.call(app.bsky.feed.getPosts, { + uris: [replyTo as AtUriString], }) const parentPost = data.posts[0] if (!parentPost) { @@ -229,14 +255,9 @@ async function resolveReply(agent: SessionAgent, replyTo: string) { uri: parentPost.uri, cid: parentPost.cid, } - let rootRef = parentRef + let rootRef: com.atproto.repo.strongRef.Main = parentRef - if ( - bsky.dangerousIsType( - parentPost.record, - AppBskyFeedPost.isRecord, - ) - ) { + if (bsky.isType(app.bsky.feed.post, parentPost.record)) { if (parentPost.record.reply) { rootRef = parentPost.record.reply.root } @@ -249,23 +270,15 @@ async function resolveReply(agent: SessionAgent, replyTo: string) { } async function resolveEmbed( - agent: SessionAgent, + clients: PostClients, queryClient: QueryClient, draft: PostDraft, onStateChange: ((state: string) => void) | undefined, -): Promise< - | $Typed - | $Typed - | $Typed - | $Typed - | $Typed - | $Typed - | undefined -> { +): Promise { if (draft.embed.quote) { const [resolvedMedia, resolvedQuote] = await Promise.all([ - resolveMedia(agent, queryClient, draft.embed, onStateChange), - resolveRecord(agent, queryClient, draft.embed.quote.uri), + resolveMedia(clients, queryClient, draft.embed, onStateChange), + resolveRecord(clients, queryClient, draft.embed.quote.uri), ]) if (resolvedMedia) { return { @@ -283,7 +296,7 @@ async function resolveEmbed( } } const resolvedMedia = await resolveMedia( - agent, + clients, queryClient, draft.embed, onStateChange, @@ -294,7 +307,7 @@ async function resolveEmbed( if (draft.embed.link) { const resolvedLink = await fetchResolveLinkQuery( queryClient, - agent, + clients.resolveClients, draft.embed.link.uri, ) if (resolvedLink.type === 'record') { @@ -308,24 +321,25 @@ async function resolveEmbed( } async function resolveMedia( - agent: SessionAgent, + clients: PostClients, queryClient: QueryClient, embedDraft: EmbedDraft, onStateChange: ((state: string) => void) | undefined, ): Promise< - | $Typed - | $Typed - | $Typed - | $Typed + | $Typed + | $Typed + | $Typed + | $Typed | undefined > { + const {pdsClient, resolveClients} = clients if (embedDraft.media?.type === 'images') { const imagesDraft = embedDraft.media.images logger.debug(`Uploading images`, { count: imagesDraft.length, }) onStateChange?.(t`Uploading images...`) - const images: AppBskyEmbedImages.Image[] = await Promise.all( + const images: app.bsky.embed.images.Image[] = await Promise.all( imagesDraft.map(async (image, i) => { logger.debug(`Compressing image #${i}`) const {path, width, height, mime} = await compressImage( @@ -333,9 +347,9 @@ async function resolveMedia( IMAGE_SIZE_CONFIG_POSTS, ) logger.debug(`Uploading image #${i}`) - const res = await uploadBlob(agent, path, mime) + const res = await uploadBlob(pdsClient, path, mime) return { - image: res.data.blob, + image: res.blob, alt: image.alt, aspectRatio: {width, height}, } @@ -352,7 +366,7 @@ async function resolveMedia( count: imagesDraft.length, }) onStateChange?.(t`Uploading images...`) - const items: $Typed[] = await Promise.all( + const items: $Typed[] = await Promise.all( imagesDraft.map(async (image, i) => { logger.debug(`Compressing image #${i}`) const {path, width, height, mime} = await compressImage( @@ -360,10 +374,10 @@ async function resolveMedia( IMAGE_SIZE_CONFIG_POSTS, ) logger.debug(`Uploading image #${i}`) - const res = await uploadBlob(agent, path, mime) + const res = await uploadBlob(pdsClient, path, mime) return { $type: 'app.bsky.embed.gallery#image' as const, - image: res.data.blob, + image: res.blob, alt: image.alt, aspectRatio: {width, height}, } @@ -383,10 +397,10 @@ async function resolveMedia( videoDraft.captions .filter(caption => caption.lang !== '') .map(async caption => { - const {data} = await agent.uploadBlob(caption.file, { + const res = await pdsClient.uploadBlob(caption.file, { encoding: 'text/vtt', }) - return {lang: caption.lang, file: data.blob} + return {lang: caption.lang, file: res.body.blob} }), ) @@ -406,7 +420,13 @@ async function resolveMedia( return { $type: 'app.bsky.embed.video', - video: videoDraft.pendingPublish.blobRef, + /* + * The video blob is a legacy `@atproto/api` BlobRef from the not-yet + * -migrated video pipeline (getJobStatus, in composer state/video). Its + * structural shape matches the lexicon blob field; the CID hasher handles + * both class instances and plain lex blobs (see computeCid). + */ + video: bsky.toLex(videoDraft.pendingPublish.blobRef), alt: videoDraft.altText || undefined, captions: captions.length === 0 ? undefined : captions, aspectRatio, @@ -416,22 +436,18 @@ async function resolveMedia( } if (embedDraft.media?.type === 'gif') { const gifDraft = embedDraft.media - const resolvedGif = await fetchResolveGifQuery( - queryClient, - agent, - gifDraft.gif, - ) - let blob: BlobRef | undefined + const resolvedGif = await fetchResolveGifQuery(queryClient, gifDraft.gif) + let blob: app.bsky.embed.external.External['thumb'] if (resolvedGif.thumb) { onStateChange?.(t`Uploading link thumbnail...`) const {path, mime} = resolvedGif.thumb.source - const response = await uploadBlob(agent, path, mime) - blob = response.data.blob + const response = await uploadBlob(pdsClient, path, mime) + blob = response.blob } return { $type: 'app.bsky.embed.external', external: { - uri: resolvedGif.uri, + uri: resolvedGif.uri as UriString, title: resolvedGif.title, description: createGIFDescription(resolvedGif.title, gifDraft.alt), thumb: blob, @@ -441,36 +457,42 @@ async function resolveMedia( if (embedDraft.link) { const resolvedLink = await fetchResolveLinkQuery( queryClient, - agent, + resolveClients, embedDraft.link.uri, ) if (resolvedLink.type === 'external') { - let blob: BlobRef | undefined + let blob: app.bsky.embed.external.External['thumb'] if (resolvedLink.thumb) { onStateChange?.(t`Uploading link thumbnail...`) const {path, mime} = resolvedLink.thumb.source - const response = await uploadBlob(agent, path, mime) - blob = response.data.blob + const response = await uploadBlob(pdsClient, path, mime) + blob = response.blob } return { $type: 'app.bsky.embed.external', external: { - uri: resolvedLink.uri, + uri: resolvedLink.uri as UriString, title: resolvedLink.title, description: resolvedLink.description, thumb: blob, - associatedRefs: resolvedLink.associatedRefs, + /* + * associatedRefs comes from getLinkMeta, still typed with the old + * `@atproto/api` StrongRef (link-meta.ts is out of scope). The shape + * is identical; bridge with toLex. + * TODO(phase4): drop toLex once link-meta migrates. + */ + associatedRefs: bsky.toLex(resolvedLink.associatedRefs), }, } } if ( resolvedLink.type === 'chat-invite' && - ChatBskyGroupDefs.isJoinLinkPreviewView(resolvedLink.view) + bsky.isType(chat.bsky.group.defs.joinLinkPreviewView, resolvedLink.view) ) { return { $type: 'app.bsky.embed.external', external: { - uri: resolvedLink.uri, + uri: resolvedLink.uri as UriString, title: resolvedLink.view.name, description: `${resolvedLink.view.memberCount}/${resolvedLink.view.memberLimit}`, }, @@ -481,100 +503,17 @@ async function resolveMedia( } async function resolveRecord( - agent: SessionAgent, + clients: PostClients, queryClient: QueryClient, uri: string, -): Promise { - const resolvedLink = await fetchResolveLinkQuery(queryClient, agent, uri) +): Promise { + const resolvedLink = await fetchResolveLinkQuery( + queryClient, + clients.resolveClients, + uri, + ) if (resolvedLink.type !== 'record') { throw Error(t`Expected uri to resolve to a record`) } return resolvedLink.record } - -// The built-in hashing functions from multiformats (`multiformats/hashes/sha2`) -// are meant for Node.js, this is the cross-platform equivalent. -const mf_sha256 = Hasher.from({ - name: 'sha2-256', - code: 0x12, - encode: input => { - const digest = sha256.arrayBuffer(input) - return new Uint8Array(digest) - }, -}) - -async function computeCid(record: AppBskyFeedPost.Record): Promise { - /* - * Lazily loaded since it's only needed when posting a thread, and its - * `cborg` dependency is ~190KB that would otherwise be in the initial - * web bundle. - */ - const dcbor = await import('@ipld/dag-cbor') - // IMPORTANT: `prepareObject` prepares the record to be hashed by removing - // fields with undefined value, and converting BlobRef instances to the - // right IPLD representation. - const prepared = prepareForHashing(record) - // 1. Encode the record into DAG-CBOR format - const encoded = dcbor.encode(prepared) - // 2. Hash the record in SHA-256 (code 0x12) - const digest = await mf_sha256.digest(encoded) - // 3. Create a CIDv1, specifying DAG-CBOR as content (code 0x71) - const cid = CID.createV1(0x71, digest) - // 4. Get the Base32 representation of the CID (`b` prefix) - return cid.toString() -} - -// Returns a transformed version of the object for use in DAG-CBOR. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function prepareForHashing(v: any): any { - // IMPORTANT: BlobRef#ipld() returns the correct object we need for hashing, - // the API client will convert this for you but we're hashing in the client, - // so we need it *now*. - if (v instanceof BlobRef) { - return v.ipld() - } - - // Walk through arrays - if (Array.isArray(v)) { - let pure = true - const mapped = v.map(value => { - if (value !== (value = prepareForHashing(value))) { - pure = false - } - return value - }) - return pure ? v : mapped - } - - // Walk through plain objects - if (isPlainObject(v)) { - const obj: Record = {} - let pure = true - for (const key in v) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - let value = v[key] - // `value` is undefined - if (value === undefined) { - pure = false - continue - } - // `prepareObject` returned a value that's different from what we had before - if (value !== (value = prepareForHashing(value))) { - pure = false - } - obj[key] = value - } - // Return as is if we haven't needed to tamper with anything - return pure ? v : obj - } - return v -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function isPlainObject(v: any): boolean { - if (typeof v !== 'object' || v === null) { - return false - } - const proto = Object.getPrototypeOf(v) - return proto === Object.prototype || proto === null -} diff --git a/src/lib/api/resolve.ts b/src/lib/api/resolve.ts index 79fa50183c..d3c0ce2a50 100644 --- a/src/lib/api/resolve.ts +++ b/src/lib/api/resolve.ts @@ -1,11 +1,7 @@ -import { - type AppBskyFeedDefs, - type AppBskyGraphDefs, - type ComAtprotoRepoStrongRef, -} from '@atproto/api' -import {AtUri} from '@atproto/api' +import {type Client} from '@atproto/lex-client' +import {AtUri, type AtUriString, type HandleString} from '@atproto/syntax' -import {DM_SERVICE_HEADERS, IMAGE_SIZE_CONFIG_2K_1MB} from '#/lib/constants' +import {IMAGE_SIZE_CONFIG_2K_1MB} from '#/lib/constants' import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta' import {resolveShortLink} from '#/lib/link-meta/resolve-short-link' import {downloadAndResize} from '#/lib/media/manip' @@ -29,8 +25,21 @@ import {createComposerImage} from '#/state/gallery' import {type ChatInvitePreview} from '#/state/queries/join-links' import {type SessionAgent} from '#/state/session' import {type Gif} from '#/features/gifPicker/types' +import {app, chat, com} from '#/lexicons' import {createGIFDescription} from '../gif-alt-text' +/** + * Clients the link resolver needs. `appview` serves the `app.bsky.*` feed/graph + * reads plus handle resolution; `chat` serves the group join-link previews. + * `agent` is still threaded through to {@link getLinkMeta}, which has not yet + * migrated off the bridge - it only reads the (unused) service URL from it. + */ +export type ResolveClients = { + appview: Client + chat: Client + agent: SessionAgent +} + type ResolvedExternalLink = { type: 'external' uri: string @@ -47,30 +56,30 @@ type ResolvedExternalLink = { type ResolvedPostRecord = { type: 'record' - record: ComAtprotoRepoStrongRef.Main + record: com.atproto.repo.strongRef.Main kind: 'post' - view: AppBskyFeedDefs.PostView + view: app.bsky.feed.defs.PostView } type ResolvedFeedRecord = { type: 'record' - record: ComAtprotoRepoStrongRef.Main + record: com.atproto.repo.strongRef.Main kind: 'feed' - view: AppBskyFeedDefs.GeneratorView + view: app.bsky.feed.defs.GeneratorView } type ResolvedListRecord = { type: 'record' - record: ComAtprotoRepoStrongRef.Main + record: com.atproto.repo.strongRef.Main kind: 'list' - view: AppBskyGraphDefs.ListView + view: app.bsky.graph.defs.ListView } type ResolvedStarterPackRecord = { type: 'record' - record: ComAtprotoRepoStrongRef.Main + record: com.atproto.repo.strongRef.Main kind: 'starter-pack' - view: AppBskyGraphDefs.StarterPackView + view: app.bsky.graph.defs.StarterPackView } type ResolvedChatInvite = { @@ -95,9 +104,10 @@ export class EmbeddingDisabledError extends Error { } export async function resolveLink( - agent: SessionAgent, + clients: ResolveClients, uri: string, ): Promise { + const {appview, chat: chatClient} = clients if (isShortLink(uri)) { uri = await resolveShortLink(uri) } @@ -124,15 +134,15 @@ export async function resolveLink( const [_0, handleOrDid, _1, rkey] = uri.split('/').filter(Boolean) const did = await fetchDid(handleOrDid) const feed = makeRecordUri(did, 'app.bsky.feed.generator', rkey) - const res = await agent.app.bsky.feed.getFeedGenerator({feed}) + const {view} = await appview.call(app.bsky.feed.getFeedGenerator, {feed}) return { type: 'record', record: { - uri: res.data.view.uri, - cid: res.data.view.cid, + uri: view.uri, + cid: view.cid, }, kind: 'feed', - view: res.data.view, + view, } } if (isBskyListUrl(uri)) { @@ -140,28 +150,27 @@ export async function resolveLink( const [_0, handleOrDid, _1, rkey] = uri.split('/').filter(Boolean) const did = await fetchDid(handleOrDid) const list = makeRecordUri(did, 'app.bsky.graph.list', rkey) - const res = await agent.app.bsky.graph.getList({list}) + const res = await appview.call(app.bsky.graph.getList, {list}) return { type: 'record', record: { - uri: res.data.list.uri, - cid: res.data.list.cid, + uri: res.list.uri, + cid: res.list.cid, }, kind: 'list', - view: res.data.list, + view: res.list, } } const chatInviteCode = getChatInviteCodeFromUrl(uri) if (chatInviteCode) { - const res = await agent.chat.bsky.group.getJoinLinkPreviews( - {codes: [chatInviteCode]}, - {headers: DM_SERVICE_HEADERS}, - ) + const res = await chatClient.call(chat.bsky.group.getJoinLinkPreviews, { + codes: [chatInviteCode], + }) return { type: 'chat-invite', uri, code: chatInviteCode, - view: res.data.joinLinkPreviews[0], + view: res.joinLinkPreviews[0], } } if (isBskyStartUrl(uri) || isBskyStarterPackUrl(uri)) { @@ -173,34 +182,35 @@ export async function resolveLink( } const did = await fetchDid(parsed.name) const starterPack = createStarterPackUri({did, rkey: parsed.rkey}) - const res = await agent.app.bsky.graph.getStarterPack({starterPack}) + const res = await appview.call(app.bsky.graph.getStarterPack, { + starterPack: starterPack as AtUriString, + }) return { type: 'record', record: { - uri: res.data.starterPack.uri, - cid: res.data.starterPack.cid, + uri: res.starterPack.uri, + cid: res.starterPack.cid, }, kind: 'starter-pack', - view: res.data.starterPack, + view: res.starterPack, } } - return resolveExternal(agent, uri) + return resolveExternal(clients, uri) // Forked from useGetPost. TODO: move into RQ. async function getPost({uri}: {uri: string}) { const urip = new AtUri(uri) if (!urip.host.startsWith('did:')) { - const res = await agent.resolveHandle({ - handle: urip.host, + const {did} = await appview.call(com.atproto.identity.resolveHandle, { + handle: urip.host as HandleString, }) - // @ts-expect-error TODO new-sdk-migration - urip.host = res.data.did + urip.host = did } - const res = await agent.getPosts({ + const res = await appview.call(app.bsky.feed.getPosts, { uris: [urip.toString()], }) - if (res.success && res.data.posts[0]) { - return res.data.posts[0] + if (res.posts[0]) { + return res.posts[0] } throw new Error('getPost: post not found') } @@ -209,17 +219,16 @@ export async function resolveLink( async function fetchDid(handleOrDid: string) { let identifier = handleOrDid if (!identifier.startsWith('did:')) { - const res = await agent.resolveHandle({handle: identifier}) - identifier = res.data.did + const {did} = await appview.call(com.atproto.identity.resolveHandle, { + handle: identifier as HandleString, + }) + identifier = did } return identifier } } -export async function resolveGif( - agent: SessionAgent, - gif: Gif, -): Promise { +export async function resolveGif(gif: Gif): Promise { const gifUrl = gif.media_formats.gif.url const params = new URLSearchParams() params.set('hh', String(gif.media_formats.gif.dims[1])) @@ -259,10 +268,15 @@ function getFileSlug(url: string | undefined): string | undefined { } async function resolveExternal( - agent: SessionAgent, + clients: ResolveClients, uri: string, ): Promise { - const result = await getLinkMeta(agent, uri) + /* + * getLinkMeta still takes the bridge agent (not yet migrated); it only reads + * a service URL from it that LINK_META_PROXY ignores. Keep threading the + * agent here until link-meta migrates. + */ + const result = await getLinkMeta(clients.agent, uri) return { type: 'external', uri: result.url, diff --git a/src/lib/api/upload-blob.ts b/src/lib/api/upload-blob.ts index 1d91ab23e9..ff237d2ad9 100644 --- a/src/lib/api/upload-blob.ts +++ b/src/lib/api/upload-blob.ts @@ -1,38 +1,62 @@ import {copyAsync} from 'expo-file-system/legacy' -import {type Agent, type ComAtprotoRepoUploadBlob} from '@atproto/api' +import {type BlobRef} from '@atproto/lex' +import {type Client, type EncodingString} from '@atproto/lex-client' import {safeDeleteAsync} from '#/lib/media/manip' /** - * @param encoding Allows overriding the blob's type + * The blob-upload response body: `{blob}`. lex `Client.uploadBlob` returns the + * full XRPC response, so callers read `res.body.blob` (the parsed blob ref). + */ +type UploadBlobResult = {blob: BlobRef} + +/** + * @param encoding Allows overriding the blob's type. Passed as the lex upload + * option (NEVER a content-type header - lex-client throws if the encoding is + * set via headers). */ export async function uploadBlob( - agent: Agent, + client: Client, input: string | Blob, encoding?: string, -): Promise { +): Promise { if (typeof input === 'string' && input.startsWith('file:')) { const blob = await asBlob(input) - return agent.uploadBlob(blob, {encoding}) + return uploadBlobResult(client, blob, encoding) } if (typeof input === 'string' && input.startsWith('/')) { const blob = await asBlob(`file://${input}`) - return agent.uploadBlob(blob, {encoding}) + return uploadBlobResult(client, blob, encoding) } if (typeof input === 'string' && input.startsWith('data:')) { const blob = await fetch(input).then(r => r.blob()) - return agent.uploadBlob(blob, {encoding}) + return uploadBlobResult(client, blob, encoding) } if (input instanceof Blob) { - return agent.uploadBlob(input, {encoding}) + return uploadBlobResult(client, input, encoding) } throw new TypeError(`Invalid uploadBlob input: ${typeof input}`) } +async function uploadBlobResult( + client: Client, + blob: Blob, + encoding?: string, +): Promise { + /* + * The lex encoding option is a branded mime string (`${string}/${string}`); + * callers pass a plain mime string, so assert the brand here. + */ + const res = await client.uploadBlob(blob, { + encoding: encoding as EncodingString | undefined, + }) + return {blob: res.body.blob} +} + async function asBlob(uri: string): Promise { return withSafeFile(uri, async safeUri => { // Note diff --git a/src/lib/api/upload-blob.web.ts b/src/lib/api/upload-blob.web.ts index 5fd79be7f7..774d1fc1e5 100644 --- a/src/lib/api/upload-blob.web.ts +++ b/src/lib/api/upload-blob.web.ts @@ -1,28 +1,43 @@ -import {type Agent, type ComAtprotoRepoUploadBlob} from '@atproto/api' +import {type BlobRef} from '@atproto/lex' +import {type Client, type EncodingString} from '@atproto/lex-client' + +/** + * The blob-upload response body: `{blob}`. lex `Client.uploadBlob` returns the + * full XRPC response, so callers read `res.body.blob` (the parsed blob ref). + */ +type UploadBlobResult = {blob: BlobRef} /** * @note It is recommended, on web, to use the `file` instance of the file * selector input element, rather than a `data:` URL, to avoid * loading the file into memory. `File` extends `Blob` "file" instances can * be passed directly to this function. + * + * @param encoding Passed as the lex upload option (NEVER a content-type header + * - lex-client throws if the encoding is set via headers). */ export async function uploadBlob( - agent: Agent, + client: Client, input: string | Blob, encoding?: string, -): Promise { +): Promise { + /* + * The lex encoding option is a branded mime string (`${string}/${string}`); + * callers pass a plain mime string, so assert the brand here. + */ + const enc = encoding as EncodingString | undefined if ( typeof input === 'string' && (input.startsWith('data:') || input.startsWith('blob:')) ) { const blob = await fetch(input).then(r => r.blob()) - return agent.uploadBlob(blob, {encoding}) + const res = await client.uploadBlob(blob, {encoding: enc}) + return {blob: res.body.blob} } if (input instanceof Blob) { - return agent.uploadBlob(input, { - encoding, - }) + const res = await client.uploadBlob(input, {encoding: enc}) + return {blob: res.body.blob} } throw new TypeError(`Invalid uploadBlob input: ${typeof input}`) diff --git a/src/lib/generate-starterpack.ts b/src/lib/generate-starterpack.ts index 3bfafa8a61..d41ec6e9db 100644 --- a/src/lib/generate-starterpack.ts +++ b/src/lib/generate-starterpack.ts @@ -1,10 +1,5 @@ -import { - type $Typed, - type AppBskyActorDefs, - type AppBskyGraphGetStarterPack, - type ComAtprotoRepoApplyWrites, - type Facet, -} from '@atproto/api' +import {type Client} from '@atproto/lex-client' +import {type AtUriString, type DatetimeString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {useMutation} from '@tanstack/react-query' @@ -13,7 +8,8 @@ import {until} from '#/lib/async/until' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {enforceLen} from '#/lib/strings/helpers' -import {type SessionAgent, useAgent} from '#/state/session' +import {useAppviewClient, usePdsClient} from '#/state/session' +import {app, com} from '#/lexicons' import type * as bsky from '#/types/bsky' export const createStarterPackList = async ({ @@ -21,30 +17,27 @@ export const createStarterPackList = async ({ description, descriptionFacets, profiles, - agent, + client, }: { name: string description?: string - descriptionFacets?: Facet[] + descriptionFacets?: app.bsky.richtext.facet.Main[] profiles: bsky.profile.AnyProfileView[] - agent: SessionAgent + client: Client }): Promise<{uri: string; cid: string}> => { if (profiles.length === 0) throw new Error('No profiles given') - const list = await agent.app.bsky.graph.list.create( - {repo: agent.session!.did}, - { - name, - description, - descriptionFacets, - avatar: undefined, - createdAt: new Date().toISOString(), - purpose: 'app.bsky.graph.defs#referencelist', - }, - ) + const list = await client.create(app.bsky.graph.list, { + name, + description, + descriptionFacets, + avatar: undefined, + createdAt: new Date().toISOString() as DatetimeString, + purpose: 'app.bsky.graph.defs#referencelist', + }) if (!list) throw new Error('List creation failed') - await agent.com.atproto.repo.applyWrites({ - repo: agent.session!.did, + await client.call(com.atproto.repo.applyWrites, { + repo: client.assertDid, writes: profiles.map(p => createListItem({did: p.did, listUri: list.uri})), }) @@ -59,28 +52,27 @@ export function useGenerateStarterPackMutation({ onError: (e: Error) => void }) { const {_} = useLingui() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() return useMutation<{uri: string; cid: string}, Error, void>({ mutationFn: async () => { - let profile: AppBskyActorDefs.ProfileViewDetailed | undefined - let profiles: AppBskyActorDefs.ProfileView[] | undefined + let profile: app.bsky.actor.defs.ProfileViewDetailed | undefined + let profiles: app.bsky.actor.defs.ProfileView[] | undefined await Promise.all([ (async () => { - profile = ( - await agent.app.bsky.actor.getProfile({ - actor: agent.session!.did, - }) - ).data + profile = await appviewClient.call(app.bsky.actor.getProfile, { + actor: pdsClient.assertDid, + }) })(), (async () => { profiles = ( - await agent.app.bsky.actor.searchActors({ + await appviewClient.call(app.bsky.actor.searchActors, { q: encodeURIComponent('*'), limit: 49, }) - ).data.actors.filter(p => p.viewer?.following) + ).actors.filter(p => p.viewer?.following) })(), ]) @@ -105,23 +97,18 @@ export function useGenerateStarterPackMutation({ const list = await createStarterPackList({ name: starterPackName, profiles, - agent, + client: pdsClient, }) - return await agent.app.bsky.graph.starterpack.create( - { - repo: agent.session!.did, - }, - { - name: starterPackName, - list: list.uri, - createdAt: new Date().toISOString(), - }, - ) + return await pdsClient.create(app.bsky.graph.starterpack, { + name: starterPackName, + list: list.uri as AtUriString, + createdAt: new Date().toISOString() as DatetimeString, + }) }, onSuccess: async data => { - await whenAppViewReady(agent, data.uri, v => { - return typeof v?.data.starterPack.uri === 'string' + await whenAppViewReady(appviewClient, data.uri, v => { + return typeof v?.starterPack.uri === 'string' }) onSuccess(data) }, @@ -137,7 +124,7 @@ function createListItem({ }: { did: string listUri: string -}): $Typed { +}): com.atproto.repo.applyWrites.$InputBody['writes'][number] { return { $type: 'com.atproto.repo.applyWrites#create', collection: 'app.bsky.graph.listitem', @@ -151,14 +138,17 @@ function createListItem({ } async function whenAppViewReady( - agent: SessionAgent, + client: Client, uri: string, - fn: (res?: AppBskyGraphGetStarterPack.Response) => boolean, + fn: (res?: app.bsky.graph.getStarterPack.$OutputBody) => boolean, ) { await until( 5, // 5 tries 1e3, // 1s delay between tries fn, - () => agent.app.bsky.graph.getStarterPack({starterPack: uri}), + () => + client.call(app.bsky.graph.getStarterPack, { + starterPack: uri as AtUriString, + }), ) } diff --git a/src/lib/media/video/multipart/types.ts b/src/lib/media/video/multipart/types.ts index 16817778c3..757bd20dfa 100644 --- a/src/lib/media/video/multipart/types.ts +++ b/src/lib/media/video/multipart/types.ts @@ -57,16 +57,17 @@ export type UploadStatusResponse = { expiresAt: string state: UploadState completedJobId?: string - jobStatus?: import('@atproto/api').AppBskyVideoDefs.JobStatus + jobStatus?: app.bsky.video.defs.JobStatus failureReason?: string } export type FinishUploadResponse = { completedJobId: string - jobStatus: import('@atproto/api').AppBskyVideoDefs.JobStatus + jobStatus: app.bsky.video.defs.JobStatus } export type AbortUploadResponse = Pick< UploadStatusResponse, 'completedJobId' | 'failureReason' > & {state: 'aborted' | 'completed' | 'failed' | 'expired'} +import {type app} from '#/lexicons' diff --git a/src/lib/media/video/multipart/upload.ts b/src/lib/media/video/multipart/upload.ts index af2d71d596..4fbaa6b226 100644 --- a/src/lib/media/video/multipart/upload.ts +++ b/src/lib/media/video/multipart/upload.ts @@ -1,9 +1,10 @@ -import {type AppBskyVideoDefs, type AtpAgent} from '@atproto/api' +import {type Client} from '@atproto/lex-client' import {nanoid} from 'nanoid/non-secure' import {AbortError} from '#/lib/async/cancelable' import {type CompressedVideo} from '#/lib/media/video/types' import {shouldRetryError} from '#/lib/strings/errors' +import {type app} from '#/lexicons' import {getServiceAuthToken} from '../upload.shared' import {mimeToExt} from '../util' import { @@ -25,19 +26,21 @@ export class MultipartFallbackError extends Error {} export async function uploadVideoMultipart({ video, - agent, + client, + dispatchUrl, setProgress, signal, onStarted, }: { video: CompressedVideo - agent: AtpAgent + client: Client + dispatchUrl: string | URL setProgress: (progress: number) => void signal: AbortSignal onStarted?: () => void -}): Promise { +}): Promise { throwIfAborted(signal) - const tokenProvider = createTokenProvider(agent, signal) + const tokenProvider = createTokenProvider(client, dispatchUrl, signal) const token = await tokenProvider.get() const name = `${nanoid(12)}.${mimeToExt(video.mimeType)}` let session @@ -130,7 +133,7 @@ async function finishAndRecover({ getToken: (forceRefresh?: boolean) => Promise signal: AbortSignal resendMissingParts: (receivedPartNumbers: number[]) => Promise -}): Promise { +}): Promise { let createdFailures = 0 let forceTokenRefresh = true while (true) { @@ -220,7 +223,7 @@ async function abortThenFallbackOrResolve( jobId: string, token: string, cause: unknown, -): Promise { +): Promise { const result = await abortUpload(jobId, token) if (result.state === 'aborted') { throw new MultipartFallbackError( @@ -238,7 +241,11 @@ async function abortThenFallbackOrResolve( ) } -function createTokenProvider(agent: AtpAgent, signal: AbortSignal) { +function createTokenProvider( + client: Client, + dispatchUrl: string | URL, + signal: AbortSignal, +) { let token: string | undefined let expiresAt = 0 let refresh: Promise | undefined @@ -247,7 +254,12 @@ function createTokenProvider(agent: AtpAgent, signal: AbortSignal) { if (!forceRefresh && token && Date.now() < expiresAt - 60_000) return token if (!refresh) { const exp = Math.floor(Date.now() / 1000) + 60 * 30 - refresh = getServiceAuthTokenWithRetry(agent, exp, signal) + refresh = getServiceAuthTokenWithRetry( + client, + dispatchUrl, + exp, + signal, + ) .then(nextToken => { token = nextToken expiresAt = exp * 1000 @@ -264,7 +276,8 @@ function createTokenProvider(agent: AtpAgent, signal: AbortSignal) { } async function getServiceAuthTokenWithRetry( - agent: AtpAgent, + client: Client, + dispatchUrl: string | URL, exp: number, signal: AbortSignal, ) { @@ -273,7 +286,8 @@ async function getServiceAuthTokenWithRetry( throwIfAborted(signal) try { return await getServiceAuthToken({ - agent, + client, + dispatchUrl, lxm: 'com.atproto.repo.uploadBlob', exp, }) diff --git a/src/lib/media/video/upload.shared.ts b/src/lib/media/video/upload.shared.ts index 8317738d5b..4bf514c7ab 100644 --- a/src/lib/media/video/upload.shared.ts +++ b/src/lib/media/video/upload.shared.ts @@ -1,44 +1,62 @@ +import {type Client} from '@atproto/lex-client' +import {type NsidString} from '@atproto/syntax' import {type I18n} from '@lingui/core' import {msg} from '@lingui/core/macro' import {VIDEO_SERVICE_DID} from '#/lib/constants' import {UploadLimitError} from '#/lib/media/video/errors' import {getServiceAuthAudFromUrl} from '#/lib/strings/url-helpers' -import {type SessionAgent} from '#/state/session' -import {createVideoAgent} from './util' +import {app, com} from '#/lexicons' +import {createVideoServiceClient} from './util' export async function getServiceAuthToken({ - agent, + client, + dispatchUrl, aud, lxm, exp, }: { - agent: SessionAgent + client: Client + /** + * The account's dispatch URL (old `agent.dispatchUrl`: the PDS entryway, + * falling back to the service URL). Only required when `aud` is omitted, so + * the default audience can be derived from the PDS host. The lex `Client` + * does not expose this - it routes to the PDS per-request internally - so the + * caller (which holds the session) must pass it. + */ + dispatchUrl?: string | URL aud?: string - lxm: string + lxm: NsidString exp?: number }) { - const pdsAud = getServiceAuthAudFromUrl(agent.dispatchUrl) - if (!pdsAud) { - throw new Error('Agent does not have a PDS URL') + let resolvedAud = aud + if (!resolvedAud) { + if (!dispatchUrl) { + throw new Error('Missing service auth audience: no aud or dispatchUrl') + } + const pdsAud = getServiceAuthAudFromUrl(dispatchUrl) + if (!pdsAud) { + throw new Error('Agent does not have a PDS URL') + } + resolvedAud = pdsAud } - const {data: serviceAuth} = await agent.com.atproto.server.getServiceAuth({ - aud: aud ?? pdsAud, + const {token} = await client.call(com.atproto.server.getServiceAuth, { + aud: resolvedAud, lxm, exp, }) - return serviceAuth.token + return token } -export async function getVideoUploadLimits(agent: SessionAgent, i18n: I18n) { +export async function getVideoUploadLimits(client: Client, i18n: I18n) { const token = await getServiceAuthToken({ - agent, + client, lxm: 'app.bsky.video.getUploadLimits', aud: VIDEO_SERVICE_DID, }) - const videoAgent = createVideoAgent() - const {data: limits} = await videoAgent.app.bsky.video - .getUploadLimits({}, {headers: {Authorization: `Bearer ${token}`}}) + const videoClient = createVideoServiceClient(token) + const limits = await videoClient + .call(app.bsky.video.getUploadLimits) .catch(err => { if (err instanceof Error) { throw new UploadLimitError(err.message) diff --git a/src/lib/media/video/upload.ts b/src/lib/media/video/upload.ts index e1c992a7ee..9cc2194435 100644 --- a/src/lib/media/video/upload.ts +++ b/src/lib/media/video/upload.ts @@ -1,5 +1,5 @@ import {createUploadTask, FileSystemUploadType} from 'expo-file-system/legacy' -import {type AppBskyVideoDefs} from '@atproto/api' +import {type Client} from '@atproto/lex-client' import {type I18n} from '@lingui/core' import {msg} from '@lingui/core/macro' import {nanoid} from 'nanoid/non-secure' @@ -10,15 +10,16 @@ import { type CompressedVideo, type VideoUploadTransport, } from '#/lib/media/video/types' -import {type SessionAgent} from '#/state/session' import {Features, features} from '#/analytics/features' +import {app} from '#/lexicons' import {MultipartFallbackError, uploadVideoMultipart} from './multipart/upload' import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared' import {createVideoEndpointUrl, mimeToExt} from './util' export async function uploadVideo({ video, - agent, + client, + dispatchUrl, did, setProgress, signal, @@ -26,7 +27,9 @@ export async function uploadVideo({ onTransport, }: { video: CompressedVideo - agent: SessionAgent + client: Client + /** The account's PDS/dispatch URL, for the uploadBlob service-auth token. */ + dispatchUrl: string | URL did: string setProgress: (progress: number) => void signal: AbortSignal @@ -36,13 +39,14 @@ export async function uploadVideo({ if (signal.aborted) { throw new AbortError() } - await getVideoUploadLimits(agent, i18n) + await getVideoUploadLimits(client, i18n) if (features.isOn(Features.VideoMultipartUploadEnable)) { try { return await uploadVideoMultipart({ video, - agent, + client, + dispatchUrl, setProgress, signal, onStarted: () => onTransport?.('multipart'), @@ -65,7 +69,8 @@ export async function uploadVideo({ throw new AbortError() } const token = await getServiceAuthToken({ - agent, + client, + dispatchUrl, lxm: 'com.atproto.repo.uploadBlob', exp: Date.now() / 1000 + 60 * 30, // 30 minutes }) @@ -92,7 +97,7 @@ export async function uploadVideo({ throw new Error('No response') } - const responseBody = JSON.parse(res.body) as AppBskyVideoDefs.JobStatus + const responseBody = JSON.parse(res.body) as app.bsky.video.defs.JobStatus if (!responseBody.jobId) { throw new ServerError( diff --git a/src/lib/media/video/upload.web.ts b/src/lib/media/video/upload.web.ts index f70c9beba7..3042ed0565 100644 --- a/src/lib/media/video/upload.web.ts +++ b/src/lib/media/video/upload.web.ts @@ -1,4 +1,4 @@ -import {type AppBskyVideoDefs} from '@atproto/api' +import {type Client} from '@atproto/lex-client' import {type I18n} from '@lingui/core' import {msg} from '@lingui/core/macro' import {nanoid} from 'nanoid/non-secure' @@ -9,15 +9,16 @@ import { type CompressedVideo, type VideoUploadTransport, } from '#/lib/media/video/types' -import {type SessionAgent} from '#/state/session' import {Features, features} from '#/analytics/features' +import {app} from '#/lexicons' import {MultipartFallbackError, uploadVideoMultipart} from './multipart/upload' import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared' import {createVideoEndpointUrl, mimeToExt} from './util' export async function uploadVideo({ video, - agent, + client, + dispatchUrl, did, setProgress, signal, @@ -25,7 +26,9 @@ export async function uploadVideo({ onTransport, }: { video: CompressedVideo - agent: SessionAgent + client: Client + /** The account's PDS/dispatch URL, for the uploadBlob service-auth token. */ + dispatchUrl: string | URL did: string setProgress: (progress: number) => void signal: AbortSignal @@ -35,13 +38,14 @@ export async function uploadVideo({ if (signal.aborted) { throw new AbortError() } - await getVideoUploadLimits(agent, i18n) + await getVideoUploadLimits(client, i18n) if (features.isOn(Features.VideoMultipartUploadEnable)) { try { return await uploadVideoMultipart({ video, - agent, + client, + dispatchUrl, setProgress, signal, onStarted: () => onTransport?.('multipart'), @@ -72,7 +76,8 @@ export async function uploadVideo({ throw new AbortError() } const token = await getServiceAuthToken({ - agent, + client, + dispatchUrl, lxm: 'com.atproto.repo.uploadBlob', exp: Date.now() / 1000 + 60 * 30, // 30 minutes }) @@ -81,7 +86,7 @@ export async function uploadVideo({ throw new AbortError() } const xhr = new XMLHttpRequest() - const res = await new Promise( + const res = await new Promise( (resolve, reject) => { xhr.upload.addEventListener('progress', e => { const progress = e.loaded / e.total @@ -93,7 +98,7 @@ export async function uploadVideo({ } else if (xhr.readyState === 4) { const uploadRes = JSON.parse( xhr.responseText, - ) as AppBskyVideoDefs.JobStatus + ) as app.bsky.video.defs.JobStatus resolve(uploadRes) } else { reject(new ServerError(i18n._(msg`Failed to upload video`))) diff --git a/src/lib/media/video/util.ts b/src/lib/media/video/util.ts index 236f0cff3e..c57c86b0d7 100644 --- a/src/lib/media/video/util.ts +++ b/src/lib/media/video/util.ts @@ -1,4 +1,4 @@ -import {AtpAgent} from '@atproto/api' +import {Client} from '@atproto/lex-client' import {type SupportedMimeTypes, VIDEO_SERVICE} from '#/lib/constants' @@ -16,9 +16,18 @@ export const createVideoEndpointUrl = ( return url.href } -export function createVideoAgent() { - return new AtpAgent({ +/** + * A non-refreshing throwaway lex {@link Client} scoped to the video service, + * authenticated by a per-call service-auth token. It has no session, so nothing + * can refresh it: requests go straight to the video service with the token as a + * static Authorization header (a raw client, unlike a session, is allowed to + * preset that header). Mirrors the scoped-client pattern in + * `#/ageAssurance/useBeginAgeAssurance`. + */ +export function createVideoServiceClient(token: string) { + return new Client({ service: VIDEO_SERVICE, + headers: {authorization: `Bearer ${token}`}, }) } diff --git a/src/lib/moderation.ts b/src/lib/moderation.ts index 0d2e3c4dbd..f93d7fc1f9 100644 --- a/src/lib/moderation.ts +++ b/src/lib/moderation.ts @@ -1,18 +1,17 @@ import {useMemo} from 'react' +import {api} from '@bsky.app/sdk' import { - type AppBskyLabelerDefs, - AtpAgent, - type ComAtprotoLabelDefs, type InterpretedLabelValueDefinition, LABELS, type ModerationCause, type ModerationOpts, type ModerationUI, -} from '@atproto/api' +} from '@bsky.app/sdk/moderation' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {type AppModerationCause} from '#/components/Pills' +import {type app, type com} from '#/lexicons' export const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn'] as const export const OTHER_SELF_LABELS = ['graphic-media'] as const @@ -53,7 +52,7 @@ export function moduiContainsHideableOffense(modui: ModerationUI): boolean { } export function labelIsHideableOffense( - label: ComAtprotoLabelDefs.Label, + label: com.atproto.label.defs.Label, ): boolean { return ['!hide', '!takedown'].includes(label.val) } @@ -63,9 +62,9 @@ export function labelIsHideableOffense( * with `!`) and the user's own "bot" self-label. */ export function filterUserFacingLabels( - labels: ComAtprotoLabelDefs.Label[], + labels: com.atproto.label.defs.Label[], currentAccountDid: string | undefined, -): ComAtprotoLabelDefs.Label[] { +): com.atproto.label.defs.Label[] { return labels.filter( label => !label.val.startsWith('!') && @@ -102,20 +101,20 @@ export function lookupLabelValueDefinition( export function isAppLabeler( labeler: | string - | AppBskyLabelerDefs.LabelerView - | AppBskyLabelerDefs.LabelerViewDetailed, + | app.bsky.labeler.defs.LabelerView + | app.bsky.labeler.defs.LabelerViewDetailed, ): boolean { if (typeof labeler === 'string') { - return AtpAgent.appLabelers.includes(labeler) + return labeler === api.moderation.did } - return AtpAgent.appLabelers.includes(labeler.creator.did) + return labeler.creator.did === api.moderation.did } export function isLabelerSubscribed( labeler: | string - | AppBskyLabelerDefs.LabelerView - | AppBskyLabelerDefs.LabelerViewDetailed, + | app.bsky.labeler.defs.LabelerView + | app.bsky.labeler.defs.LabelerViewDetailed, modOpts: ModerationOpts, ) { labeler = typeof labeler === 'string' ? labeler : labeler.creator.did @@ -134,7 +133,11 @@ export type Subject = did: string } -export function useLabelSubject({label}: {label: ComAtprotoLabelDefs.Label}): { +export function useLabelSubject({ + label, +}: { + label: com.atproto.label.defs.Label +}): { subject: Subject } { return useMemo(() => { diff --git a/src/lib/moderation/create-sanitized-display-name.ts b/src/lib/moderation/create-sanitized-display-name.ts index d15564d385..1298afd7ef 100644 --- a/src/lib/moderation/create-sanitized-display-name.ts +++ b/src/lib/moderation/create-sanitized-display-name.ts @@ -1,4 +1,4 @@ -import {type ModerationUI} from '@atproto/api' +import {type ModerationUI} from '@bsky.app/sdk/moderation' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' diff --git a/src/lib/moderation/useLabelBehaviorDescription.ts b/src/lib/moderation/useLabelBehaviorDescription.ts index cdded25924..e97ecef33c 100644 --- a/src/lib/moderation/useLabelBehaviorDescription.ts +++ b/src/lib/moderation/useLabelBehaviorDescription.ts @@ -1,7 +1,7 @@ import { type InterpretedLabelValueDefinition, type LabelPreference, -} from '@atproto/api' +} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' diff --git a/src/lib/moderation/useLabelInfo.ts b/src/lib/moderation/useLabelInfo.ts index f398406078..a766154667 100644 --- a/src/lib/moderation/useLabelInfo.ts +++ b/src/lib/moderation/useLabelInfo.ts @@ -1,10 +1,8 @@ import { - type AppBskyLabelerDefs, - type ComAtprotoLabelDefs, type InterpretedLabelValueDefinition, interpretLabelValueDefinition, LABELS, -} from '@atproto/api' +} from '@bsky.app/sdk/moderation' import {useLingui} from '@lingui/react' import * as bcp47Match from 'bcp-47-match' @@ -13,30 +11,44 @@ import { useGlobalLabelStrings, } from '#/lib/moderation/useGlobalLabelStrings' import {useLabelDefinitions} from '#/state/preferences' +import {type app, type com} from '#/lexicons' +import {toLex} from '#/types/bsky' export interface LabelInfo { - label: ComAtprotoLabelDefs.Label + label: com.atproto.label.defs.Label def: InterpretedLabelValueDefinition - strings: ComAtprotoLabelDefs.LabelValueDefinitionStrings - labeler: AppBskyLabelerDefs.LabelerViewDetailed | undefined + strings: com.atproto.label.defs.LabelValueDefinitionStrings + labeler: app.bsky.labeler.defs.LabelerViewDetailed | undefined } -export function useLabelInfo(label: ComAtprotoLabelDefs.Label): LabelInfo { +export function useLabelInfo(label: com.atproto.label.defs.Label): LabelInfo { const {i18n} = useLingui() + /* + * TODO(phase4): drop the `toLex` casts once `useLabelDefinitions` + * (state/preferences/label-defs) emits `#/lexicons` / `@bsky.app/sdk` + * moderation types. It still returns old `@atproto/api` + * `InterpretedLabelValueDefinition` / `LabelerViewDetailed`, which are + * structurally identical to the SDK/lexicon ones modulo branded strings. + */ const {labelDefs, labelers} = useLabelDefinitions() + const def = getDefinition( + toLex>(labelDefs), + label, + ) const globalLabelStrings = useGlobalLabelStrings() - const def = getDefinition(labelDefs, label) return { label, def, strings: getLabelStrings(i18n.locale, globalLabelStrings, def), - labeler: labelers.find(labeler => label.src === labeler.creator.did), + labeler: toLex(labelers).find( + labeler => label.src === labeler.creator.did, + ), } } export function getDefinition( labelDefs: Record, - label: ComAtprotoLabelDefs.Label, + label: com.atproto.label.defs.Label, ): InterpretedLabelValueDefinition { // check local definitions const customDef = @@ -71,13 +83,13 @@ export function getLabelStrings( locale: string, globalLabelStrings: GlobalLabelStrings, def: InterpretedLabelValueDefinition, -): ComAtprotoLabelDefs.LabelValueDefinitionStrings { +): com.atproto.label.defs.LabelValueDefinitionStrings { if (!def.definedBy) { // global definition, look up strings if (def.identifier in globalLabelStrings) { return globalLabelStrings[ def.identifier - ] as ComAtprotoLabelDefs.LabelValueDefinitionStrings + ] as com.atproto.label.defs.LabelValueDefinitionStrings } } else { // try to find locale match in the definition's strings diff --git a/src/lib/moderation/useModerationCauseDescription.ts b/src/lib/moderation/useModerationCauseDescription.ts index d11d16dca3..71415da158 100644 --- a/src/lib/moderation/useModerationCauseDescription.ts +++ b/src/lib/moderation/useModerationCauseDescription.ts @@ -1,9 +1,6 @@ import {useMemo} from 'react' -import { - BSKY_LABELER_DID, - type ModerationCause, - type ModerationCauseSource, -} from '@atproto/api' +import {api} from '@bsky.app/sdk' +import {type ModerationCause} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' @@ -16,6 +13,8 @@ import {type Props as SVGIconProps} from '#/components/icons/common' import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash' import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' import {type AppModerationCause} from '#/components/Pills' +import {type com} from '#/lexicons' +import {toLex} from '#/types/bsky' import {useGlobalLabelStrings} from './useGlobalLabelStrings' import {getDefinition, getLabelStrings} from './useLabelInfo' @@ -25,7 +24,7 @@ export interface ModerationCauseDescription { description: string source?: string sourceDisplayName?: string - sourceType?: ModerationCauseSource['type'] + sourceType?: ModerationCause['source']['type'] sourceAvi?: string sourceDid?: string isSubjectAccount?: boolean @@ -130,7 +129,18 @@ export function useModerationCauseDescription( } } if (cause.type === 'label') { - const def = cause.labelDef || getDefinition(labelDefs, cause.label) + /* + * TODO(phase4): drop `toLex` once `useLabelDefinitions` and the label + * cause type share the `#/lexicons` label shape. `labelDefs` still comes + * from the old `@atproto/api`-typed producer and `cause.label` is the SDK + * label; both are structurally identical to the `#/lexicons` label. + */ + const def = + cause.labelDef || + getDefinition( + toLex[0]>(labelDefs), + toLex(cause.label), + ) const strings = getLabelStrings(i18n.locale, globalLabelStrings, def) const labeler = labelers.find(l => l.creator.did === cause.label.src) let source = labeler @@ -138,7 +148,7 @@ export function useModerationCauseDescription( : undefined let sourceDisplayName = labeler?.creator.displayName if (!source) { - if (cause.label.src === BSKY_LABELER_DID) { + if (cause.label.src === api.moderation.did) { source = 'moderation.bsky.app' sourceDisplayName = 'Bluesky Moderation Service' } else { diff --git a/src/lib/strings/display-names.ts b/src/lib/strings/display-names.ts index 612a317eaf..8ba2e1d215 100644 --- a/src/lib/strings/display-names.ts +++ b/src/lib/strings/display-names.ts @@ -1,4 +1,4 @@ -import {type ModerationUI} from '@atproto/api' +import {type ModerationUI} from '@bsky.app/sdk/moderation' // \u2705 = ✅ // \u2713 = ✓ diff --git a/src/lib/strings/helpers.ts b/src/lib/strings/helpers.ts index 77ef18cb55..402807f50f 100644 --- a/src/lib/strings/helpers.ts +++ b/src/lib/strings/helpers.ts @@ -1,4 +1,4 @@ -import {type RichText} from '@atproto/api' +import {type RichText} from '@bsky.app/sdk/richtext' import {countGraphemes} from 'unicode-segmenter/grapheme' import {shortenLinks} from './rich-text-manip' diff --git a/src/lib/strings/rich-text-helpers.ts b/src/lib/strings/rich-text-helpers.ts index c2b2ceac52..078b91e400 100644 --- a/src/lib/strings/rich-text-helpers.ts +++ b/src/lib/strings/rich-text-helpers.ts @@ -1,5 +1,7 @@ -import {AppBskyRichtextFacet, type RichText} from '@atproto/api' +import {type RichText} from '@bsky.app/sdk/richtext' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import {linkRequiresWarning} from './url-helpers' export function richTextToString(rt: RichText, loose: boolean): string { @@ -14,7 +16,7 @@ export function richTextToString(rt: RichText, loose: boolean): string { for (const segment of rt.segments()) { const link = segment.link - if (link && AppBskyRichtextFacet.validateLink(link).success) { + if (link && bsky.matches(app.bsky.richtext.facet.link, link)) { const href = link.uri const text = segment.text diff --git a/src/lib/strings/rich-text-manip.ts b/src/lib/strings/rich-text-manip.ts index 099fbffb0a..fa7a9b4a82 100644 --- a/src/lib/strings/rich-text-manip.ts +++ b/src/lib/strings/rich-text-manip.ts @@ -1,5 +1,7 @@ -import {AppBskyRichtextFacet, type RichText, UnicodeString} from '@atproto/api' +import {type RichText, UnicodeString} from '@bsky.app/sdk/richtext' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import {toShortUrl} from './url-helpers' export function shortenLinks(rt: RichText): RichText { @@ -10,7 +12,9 @@ export function shortenLinks(rt: RichText): RichText { // enumerate the link facets if (rt.facets) { for (const facet of rt.facets) { - const isLink = !!facet.features.find(AppBskyRichtextFacet.isLink) + const isLink = !!facet.features.find(f => + bsky.isType(app.bsky.richtext.facet.link, f), + ) if (!isLink) { continue } @@ -40,7 +44,9 @@ export function stripInvalidMentions(rt: RichText): RichText { rt = rt.clone() if (rt.facets) { rt.facets = rt.facets?.filter(facet => { - const mention = facet.features.find(AppBskyRichtextFacet.isMention) + const mention = facet.features.find(f => + bsky.isType(app.bsky.richtext.facet.mention, f), + ) if (mention && !mention.did) { return false } diff --git a/src/lib/strings/starter-pack.ts b/src/lib/strings/starter-pack.ts index 475b000336..0dc6218114 100644 --- a/src/lib/strings/starter-pack.ts +++ b/src/lib/strings/starter-pack.ts @@ -1,4 +1,4 @@ -import {AtUri} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import type * as bsky from '#/types/bsky' diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index 4fb8034a51..c90f1a6fb9 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -1,4 +1,4 @@ -import {AtUri} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {parse} from 'psl' import TLDs from 'tlds' diff --git a/src/screens/Messages/Conversation.tsx b/src/screens/Messages/Conversation.tsx index 1e2f4037a8..17fa79fe8a 100644 --- a/src/screens/Messages/Conversation.tsx +++ b/src/screens/Messages/Conversation.tsx @@ -1,11 +1,12 @@ import {useCallback, useEffect, useMemo, useState} from 'react' import {type LayoutChangeEvent, View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {ChatBskyConvoDefs, moderateProfile} from '@atproto/api' +import {ChatBskyConvoDefs} from '@atproto/api' 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, @@ -45,6 +46,7 @@ import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util' import {Error} from '#/components/Error' import * as Layout from '#/components/Layout' import {IS_LIQUID_GLASS} from '#/env' +import {toLex} from '#/types/bsky' import {ChatDisabled} from './components/ChatDisabled' import {ChatEnded} from './components/ChatEnded' import {ChatLocked} from './components/ChatLocked' @@ -225,7 +227,8 @@ function InnerReady({ const moderationOpts = useModerationOpts() const primaryMemberModeration = useMemo(() => { if (!primaryMember || !moderationOpts) return null - return moderateProfile(primaryMember, moderationOpts) + // TODO(phase4): drop toLex once useMaybeProfileShadow emits #/lexicons views + return moderateProfile(toLex(primaryMember), moderationOpts) }, [primaryMember, moderationOpts]) const header = diff --git a/src/screens/Messages/ConversationSettings/Member.tsx b/src/screens/Messages/ConversationSettings/Member.tsx index 20cc0fee52..823182e689 100644 --- a/src/screens/Messages/ConversationSettings/Member.tsx +++ b/src/screens/Messages/ConversationSettings/Member.tsx @@ -1,5 +1,5 @@ import {View} from 'react-native' -import {moderateProfile} from '@atproto/api' +import {moderateProfile} from '@bsky.app/sdk/moderation' import {Trans, useLingui} from '@lingui/react/macro' import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted' @@ -21,6 +21,7 @@ import * as ProfileCard from '#/components/ProfileCard' import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' +import {toLex} from '#/types/bsky' import {MemberMenu} from './MemberMenu' import {RemoveMemberPrompt} from './prompts' import {StatusBadge} from './StatusBadge' @@ -80,7 +81,8 @@ export function Member({ return } - const moderation = moderateProfile(profile, moderationOpts) + // TODO(phase4): drop toLex once useProfileShadow emits #/lexicons views + const moderation = moderateProfile(toLex(profile), moderationOpts) const isDeletedAccount = profile.handle === 'missing.invalid' const displayName = isDeletedAccount @@ -106,10 +108,13 @@ export function Member({ } const joinedReason = profile.kind?.addedBy - ? l`Added by ${createSanitizedDisplayName( + ? // TODO(phase4): drop toLex once addedBy emits #/lexicons views + l`Added by ${createSanitizedDisplayName( profile.kind.addedBy, true, - moderateProfile(profile.kind.addedBy, moderationOpts).ui('displayName'), + moderateProfile(toLex(profile.kind.addedBy), moderationOpts).ui( + 'displayName', + ), )}` : l`Added by invite link` diff --git a/src/screens/Messages/ConversationSettings/index.tsx b/src/screens/Messages/ConversationSettings/index.tsx index 5576f28674..74ce2df2f7 100644 --- a/src/screens/Messages/ConversationSettings/index.tsx +++ b/src/screens/Messages/ConversationSettings/index.tsx @@ -1,11 +1,6 @@ import {useEffect, useState} from 'react' import {Pressable, View} from 'react-native' -import { - ChatBskyActorDefs, - ChatBskyConvoDefs, - ChatBskyConvoUnlockConvo, - type ModerationOpts, -} from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' @@ -19,6 +14,7 @@ import { type NativeStackScreenProps, type NavigationProp, } from '#/lib/routes/types' +import {getErrorName} from '#/lib/xrpc-error' import {logger} from '#/logger' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useConvoQuery} from '#/state/queries/messages/conversation' @@ -59,6 +55,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {chat} from '#/lexicons' import * as bsky from '#/types/bsky' import {InviteLinkDialog} from '../components/InviteLinkDialog' import {AddMembersLink} from './AddMembersLink' @@ -172,16 +169,20 @@ function keyExtractor(item: Item) { return item.key } +/* + * The member list now comes from the migrated lexicon-typed query, but the + * narrowed `GroupConvoMember` target is still the old-typed shape from + * `#/components/dms/util` (migrates in a later task) - the guard doubles as + * the mixed-world bridge. TODO(phase4): retype to the lexicon member types + * once dms/util migrates. + */ function isGroupMember( - member: ChatBskyActorDefs.ProfileViewBasic, -): member is GroupConvoMember { + member: chat.bsky.actor.defs.ProfileViewBasic, +): member is chat.bsky.actor.defs.ProfileViewBasic & GroupConvoMember { // Kind is missing when the account has been deleted. return ( member.kind === undefined || - bsky.dangerousIsType( - member.kind, - ChatBskyActorDefs.isGroupConvoMember, - ) + bsky.isType(chat.bsky.actor.defs.groupConvoMember, member.kind) ) } @@ -204,7 +205,15 @@ function GroupSettings({ const {data: memberListData = [], refetch} = useListConvoMembersQuery({ convoId: convo.view.id, - placeholderData: convo.members, + /* + * `convo.members` comes from the still-old-typed `#/components/dms/util` + * (migrates in a later task) while the member-list query is now typed on + * the lexicon ProfileViewBasic. TODO(phase4): drop toLex once dms/util + * migrates. + */ + placeholderData: bsky.toLex( + convo.members, + ), }) const {data: joinRequestsData, hasNextPage: hasMoreRequests} = @@ -417,7 +426,7 @@ function SettingsHeader({ isPending: isLocking, } = useLockConvo(convoId, { onSuccess: (data, {silent}) => { - if (!ChatBskyConvoDefs.isGroupConvo(data.convo.kind)) return + if (!bsky.isType(chat.bsky.convo.defs.groupConvo, data.convo.kind)) return if (silent) return if (data.convo.kind.lockStatus === 'locked') { ax.metric('groupchat:owner:lock', {convoId}) @@ -431,9 +440,7 @@ function SettingsHeader({ if (lock) { logger.error('Failed to lock group chat', {message: e}) Toast.show(l`Failed to lock group chat`, {type: 'error'}) - } else if ( - e instanceof ChatBskyConvoUnlockConvo.ConvoLockedByModerationError - ) { + } else if (getErrorName(e) === 'ConvoLockedByModeration') { Toast.show(l`This chat is locked by a moderation action`, { type: 'error', }) diff --git a/src/screens/Messages/JoinRequest.tsx b/src/screens/Messages/JoinRequest.tsx index 6ce57a151b..d9593b11f5 100644 --- a/src/screens/Messages/JoinRequest.tsx +++ b/src/screens/Messages/JoinRequest.tsx @@ -1,8 +1,9 @@ import {useEffect} from 'react' import {View} from 'react-native' import {ImageBackground} from 'expo-image' -import {ChatBskyGroupDefs, moderateProfile} from '@atproto/api' +import {ChatBskyGroupDefs} from '@atproto/api' import {type ThemeName} from '@bsky.app/alf' +import {moderateProfile} from '@bsky.app/sdk/moderation' import {Trans, useLingui} from '@lingui/react/macro' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' @@ -20,6 +21,7 @@ import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/componen import {ProfileBadges} from '#/components/ProfileBadges' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +import {toLex} from '#/types/bsky' const desktopDarkBg = require('../../../assets/images/chat-desktop-bg-dark.webp') const desktopDimBg = require('../../../assets/images/chat-desktop-bg-dim.webp') @@ -179,8 +181,9 @@ export function JoinRequest({setScreenState}: Props) { {createSanitizedDisplayName( joinLinkPreview.owner, true, + // TODO(phase4): drop toLex once join link preview emits #/lexicons views moderateProfile( - joinLinkPreview.owner, + toLex(joinLinkPreview.owner), moderationOpts, ).ui('displayName'), )} @@ -215,12 +218,14 @@ export function JoinRequest({setScreenState}: Props) { ? l`Sign in to request access to this group chat.` : l`Sign in to accept invite.`}{' '} {joinLinkPreview.joinRule === 'followedByOwner' && + // TODO(phase4): drop toLex once join link preview emits #/lexicons views l`Only people ${createSanitizedDisplayName( joinLinkPreview.owner, true, - moderateProfile(joinLinkPreview.owner, moderationOpts).ui( - 'displayName', - ), + moderateProfile( + toLex(joinLinkPreview.owner), + moderationOpts, + ).ui('displayName'), )} follows can join.`} diff --git a/src/screens/Messages/JoinRequests.tsx b/src/screens/Messages/JoinRequests.tsx index ffe3fb219e..933766122a 100644 --- a/src/screens/Messages/JoinRequests.tsx +++ b/src/screens/Messages/JoinRequests.tsx @@ -1,10 +1,6 @@ import {useState} from 'react' import {View} from 'react-native' -import { - ChatBskyGroupApproveJoinRequest, - type ChatBskyGroupListJoinRequests, - ChatBskyGroupRejectJoinRequest, -} from '@atproto/api' +import {type ChatBskyGroupListJoinRequests} from '@atproto/api' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {type InfiniteData, useQueryClient} from '@tanstack/react-query' @@ -16,6 +12,7 @@ import { type NativeStackScreenProps, type NavigationProp, } from '#/lib/routes/types' +import {getErrorName} from '#/lib/xrpc-error' import {logger} from '#/logger' import {ConvoProvider, useConvo} from '#/state/messages/convo' import {ConvoStatus} from '#/state/messages/convo/types' @@ -190,18 +187,11 @@ function JoinRequestsList({ let errorMessage = l`Failed to accept join request` if (isNetworkError(error)) { errorMessage = l`A network error occurred. Please check your internet connection.` - } else if ( - error instanceof ChatBskyGroupApproveJoinRequest.InvalidConvoError - ) { + } else if (getErrorName(error) === 'InvalidConvo') { errorMessage = l`Conversation not found.` - } else if ( - error instanceof ChatBskyGroupApproveJoinRequest.InsufficientRoleError - ) { + } else if (getErrorName(error) === 'InsufficientRole') { errorMessage = l`Only admins can accept join requests.` - } else if ( - error instanceof - ChatBskyGroupApproveJoinRequest.MemberLimitReachedError - ) { + } else if (getErrorName(error) === 'MemberLimitReached') { errorMessage = l`The member limit has been reached.` } Toast.show(errorMessage, {type: 'error'}) @@ -223,13 +213,9 @@ function JoinRequestsList({ let errorMessage = l`Failed to reject join request` if (isNetworkError(error)) { errorMessage = l`A network error occurred. Please check your internet connection.` - } else if ( - error instanceof ChatBskyGroupRejectJoinRequest.InvalidConvoError - ) { + } else if (getErrorName(error) === 'InvalidConvo') { errorMessage = l`Conversation not found.` - } else if ( - error instanceof ChatBskyGroupRejectJoinRequest.InsufficientRoleError - ) { + } else if (getErrorName(error) === 'InsufficientRole') { errorMessage = l`Only admins can reject join requests.` } Toast.show(errorMessage, {type: 'error'}) diff --git a/src/screens/Messages/components/ChatDisabled.tsx b/src/screens/Messages/components/ChatDisabled.tsx index 8095c1e0b9..04aa3ab60a 100644 --- a/src/screens/Messages/components/ChatDisabled.tsx +++ b/src/screens/Messages/components/ChatDisabled.tsx @@ -1,12 +1,11 @@ import {useCallback, useState} from 'react' import {type StyleProp, View, type ViewStyle} from 'react-native' -import {ToolsOzoneReportDefs} from '@atproto/api' +import {api} from '@bsky.app/sdk' import {Trans, useLingui} from '@lingui/react/macro' import {useMutation} from '@tanstack/react-query' -import {BLUESKY_MOD_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession} from '#/state/session' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -14,6 +13,8 @@ import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' +import {com, tools} from '#/lexicons' +import {toLex} from '#/types/bsky' export function ChatDisabled({ shape = 'pill', @@ -92,25 +93,25 @@ function DialogInner() { const control = Dialog.useDialogContext() const [details, setDetails] = useState('') const {gtMobile} = useBreakpoints() - const agent = useAgent() + const pdsClient = usePdsClient() const {currentAccount} = useSession() const {mutate, isPending} = useMutation({ mutationFn: async () => { if (!currentAccount) throw new Error('No current account, should be unreachable') - await agent.createModerationReport( - { - reasonType: ToolsOzoneReportDefs.REASONAPPEAL, + await pdsClient.call( + com.atproto.moderation.createReport, + toLex({ + reasonType: tools.ozone.report.defs.reasonAppeal.value, subject: { $type: 'com.atproto.admin.defs#repoRef', did: currentAccount.did, }, reason: details, - }, + }), { - encoding: 'application/json', - headers: BLUESKY_MOD_SERVICE_HEADERS, + service: api.moderation.service, }, ) }, diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx index 0acd0c28e7..ee27931636 100644 --- a/src/screens/Messages/components/ChatListItem.tsx +++ b/src/screens/Messages/components/ChatListItem.tsx @@ -1,11 +1,11 @@ import {useCallback, useMemo, useState} from 'react' import {type GestureResponderEvent, View} from 'react-native' +import {type ChatBskyConvoDefs} from '@atproto/api' import { - ChatBskyConvoDefs, moderateProfile, type ModerationDecision, type ModerationOpts, -} from '@atproto/api' +} from '@bsky.app/sdk/moderation' import {plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' @@ -53,7 +53,8 @@ import {ProfileBadges} from '#/components/ProfileBadges' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_NATIVE} from '#/env' -import type * as bsky from '#/types/bsky' +import {chat} from '#/lexicons' +import * as bsky from '#/types/bsky' import {useIsWithinSplitView} from './splitView/context' export const ChatListItemPortal = createPortalGroup() @@ -125,7 +126,8 @@ function DirectChatItem({ const {isWithinLeftPanel} = useIsWithinSplitView() const moderation = useMemo( - () => moderateProfile(profile, moderationOpts), + // TODO(phase4): drop toLex once useProfileShadow emits #/lexicons views + () => moderateProfile(bsky.toLex(profile), moderationOpts), [profile, moderationOpts], ) @@ -192,7 +194,10 @@ function GroupChatItem({ const moderation = useMemo( () => - groupOwner ? moderateProfile(groupOwner, moderationOpts) : undefined, + groupOwner + ? // TODO(phase4): drop toLex once useMaybeProfileShadow emits #/lexicons views + moderateProfile(bsky.toLex(groupOwner), moderationOpts) + : undefined, [groupOwner, moderationOpts], ) @@ -314,7 +319,12 @@ function BaseChatItem({ let lastMessageSentAt: string | null = null // Deleted message - if (ChatBskyConvoDefs.isDeletedMessageView(convo.view.lastMessage)) { + if ( + bsky.isType( + chat.bsky.convo.defs.deletedMessageView, + convo.view.lastMessage, + ) + ) { lastMessageSentAt = convo.view.lastMessage.sentAt lastMessage = isDeletedAccount @@ -323,7 +333,7 @@ function BaseChatItem({ } // Message - if (ChatBskyConvoDefs.isMessageView(convo.view.lastMessage)) { + if (bsky.isType(chat.bsky.convo.defs.messageView, convo.view.lastMessage)) { const info = getMessageInfo({ convo: convo.view, currentAccountDid: currentAccount?.did, @@ -339,7 +349,12 @@ function BaseChatItem({ } // Reaction - if (ChatBskyConvoDefs.isMessageAndReactionView(convo.view.lastReaction)) { + if ( + bsky.isType( + chat.bsky.convo.defs.messageAndReactionView, + convo.view.lastReaction, + ) + ) { const info = getReactionInfo({ convo: convo.view, currentAccountDid: currentAccount?.did, @@ -358,7 +373,12 @@ function BaseChatItem({ } // System message - if (ChatBskyConvoDefs.isSystemMessageView(convo.view.lastMessage)) { + if ( + bsky.isType( + chat.bsky.convo.defs.systemMessageView, + convo.view.lastMessage, + ) + ) { const info = getSystemMessageInfo( convo.view.lastMessage.data, new Map(convo.view.members.map(m => [m.did, m])), @@ -404,7 +424,15 @@ function BaseChatItem({ for (const member of convo.view.members) { unstableCacheProfileView(queryClient, member) } - precacheConvoQuery(queryClient, convo.view) + /* + * `convo.view` comes from the still-old-typed `#/components/dms/util` + * (migrates in a later task) while the convo cache is now keyed on the + * lexicon ConvoView. TODO(phase4): drop toLex once dms/util migrates. + */ + precacheConvoQuery( + queryClient, + bsky.toLex(convo.view), + ) void decrementBadgeCount(convo.view.unreadCount) if (isDeletedAccount) { e.preventDefault() @@ -496,7 +524,13 @@ function BaseChatItem({ ] : undefined } - onPressIn={() => precacheConvoQuery(queryClient, convo.view)} + onPressIn={() => + // see onPress: old-typed dms/util view into the new-typed cache + precacheConvoQuery( + queryClient, + bsky.toLex(convo.view), + ) + } onPress={onPress} onLongPress={showMenu && IS_NATIVE ? onLongPress : undefined} onAccessibilityAction={showMenu ? onLongPress : undefined}> diff --git a/src/screens/Messages/components/ChatLocked.tsx b/src/screens/Messages/components/ChatLocked.tsx index 8718667e9c..03a3b128ba 100644 --- a/src/screens/Messages/components/ChatLocked.tsx +++ b/src/screens/Messages/components/ChatLocked.tsx @@ -1,10 +1,10 @@ import {Pressable} from 'react-native' -import {ChatBskyConvoUnlockConvo} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {HITSLOP_10} from '#/lib/constants' import {type NavigationProp} from '#/lib/routes/types' +import {getErrorName} from '#/lib/xrpc-error' import {logger} from '#/logger' import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' import {useLockConvo} from '#/state/queries/messages/lock-conversation' @@ -41,7 +41,7 @@ export function ChatLocked({ Toast.show(l({message: 'Group chat unlocked', context: 'toast'})) }, onError: e => { - if (e instanceof ChatBskyConvoUnlockConvo.ConvoLockedByModerationError) { + if (getErrorName(e) === 'ConvoLockedByModeration') { Toast.show(l`This chat is locked by a moderation action`, { type: 'error', }) diff --git a/src/screens/Messages/components/ChatStatusInfo.tsx b/src/screens/Messages/components/ChatStatusInfo.tsx index e2e9a75faa..b1e052f005 100644 --- a/src/screens/Messages/components/ChatStatusInfo.tsx +++ b/src/screens/Messages/components/ChatStatusInfo.tsx @@ -1,7 +1,7 @@ import {useCallback, useMemo} from 'react' import {View} from 'react-native' import {LinearGradient} from 'expo-linear-gradient' -import {moderateProfile} from '@atproto/api' +import {moderateProfile} from '@bsky.app/sdk/moderation' import {Trans, useLingui} from '@lingui/react/macro' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' @@ -17,6 +17,7 @@ import {ProfileBadges} from '#/components/ProfileBadges' import {usePromptControl} from '#/components/Prompt' import {Text} from '#/components/Typography' import type * as bsky from '#/types/bsky' +import {toLex} from '#/types/bsky' import {AcceptChatButton, DeleteChatButton, RejectMenu} from './RequestButtons' export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) { @@ -114,7 +115,8 @@ function InviterHeader({ const t = useTheme() const profile = useProfileShadow(profileUnshadowed) const moderation = useMemo( - () => moderateProfile(profile, moderationOpts), + // TODO(phase4): drop toLex once useProfileShadow emits #/lexicons views + () => moderateProfile(toLex(profile), moderationOpts), [profile, moderationOpts], ) const displayName = createSanitizedDisplayName( diff --git a/src/screens/Messages/components/InviteLinkDialog.tsx b/src/screens/Messages/components/InviteLinkDialog.tsx index 22acd577b0..7b028bb39b 100644 --- a/src/screens/Messages/components/InviteLinkDialog.tsx +++ b/src/screens/Messages/components/InviteLinkDialog.tsx @@ -1,11 +1,8 @@ import {useState} from 'react' import {View} from 'react-native' import {Image} from 'expo-image' -import { - type ChatBskyGroupDefs, - moderateProfile, - type ModerationOpts, -} from '@atproto/api' +import {type ChatBskyGroupDefs} from '@atproto/api' +import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' @@ -37,6 +34,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {toLex} from '#/types/bsky' import {CopyTextButton} from './CopyTextButton' import {EditTextButton} from './EditTextButton' @@ -71,7 +69,8 @@ export function InviteLinkDialog({ const ownerName = createSanitizedDisplayName( owner, false, - moderateProfile(owner, moderationOpts).ui('displayName'), + // TODO(phase4): drop toLex once GroupConvoMember emits #/lexicons views + moderateProfile(toLex(owner), moderationOpts).ui('displayName'), ) const {joinLink} = convo.details diff --git a/src/screens/Messages/components/MessageComposer.tsx b/src/screens/Messages/components/MessageComposer.tsx index 1bc5e20e1e..1e03d3f66e 100644 --- a/src/screens/Messages/components/MessageComposer.tsx +++ b/src/screens/Messages/components/MessageComposer.tsx @@ -13,7 +13,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context' import {scheduleOnRN} from 'react-native-worklets' import {GlassContainer} from 'expo-glass-effect' import {LinearGradient} from 'expo-linear-gradient' -import {type $Typed, type ChatBskyConvoDefs} from '@atproto/api' +import {type $Typed} from '@atproto/lex' import {ScrollEdgeEffect} from '@bsky.app/expo-scroll-edge-effect' import {useLingui} from '@lingui/react/macro' import {countGraphemes} from 'unicode-segmenter/grapheme' @@ -37,6 +37,8 @@ import {PaperPlaneVertical_Filled_Stroke2_Corner1_Rounded as PaperPlaneIcon} fro import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {IS_ANDROID, IS_IOS, IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env' +import {type chat} from '#/lexicons' +import * as bsky from '#/types/bsky' import {type MessageEmbedState} from './MessageInputEmbed' const MIN_HEIGHT = 40 @@ -53,7 +55,7 @@ export function MessageComposer({ onSendMessage: ( message: string, embed?: MessageEmbedState, - replyTo?: $Typed, + replyTo?: $Typed, ) => void messageEmbed: MessageEmbedState | undefined setEmbed: (embedUrl: string | undefined) => void @@ -67,7 +69,16 @@ export function MessageComposer({ const editable = !needsEmailVerification && !loading const {getDraft, clearDraft} = useMessageDraft() const composerInternalApiRef = useComposerInternalApiRef() - const {replyTo, clearReply} = useMessageReplies() + const {replyTo: replyToOld, clearReply} = useMessageReplies() + /* + * `useMessageReplies` (`#/components/dms`, migrates in a later task) still + * emits the old `@atproto/api` MessageView; the send path is typed on the + * lexicon view. Structurally identical modulo branded strings. + * TODO(phase4): drop toLex once dms/MessageReplies migrates. + */ + const replyTo = bsky.toLex( + replyToOld, + ) const [text, setText] = useState(getDraft) useSaveMessageDraft(text) @@ -106,7 +117,7 @@ export function MessageComposer({ const onSubmit = ( message: string, embed: MessageEmbedState | undefined, - replyTo: ChatBskyConvoDefs.MessageView | null, + replyTo: chat.bsky.convo.defs.MessageView | null, ) => { if (!editable) return if (!embed && message.trim() === '') return diff --git a/src/screens/Messages/components/MessageInputEmbed.tsx b/src/screens/Messages/components/MessageInputEmbed.tsx index 477939cd3e..f57b8d5837 100644 --- a/src/screens/Messages/components/MessageInputEmbed.tsx +++ b/src/screens/Messages/components/MessageInputEmbed.tsx @@ -1,12 +1,8 @@ import {useCallback, useEffect, useMemo, useState} from 'react' import {LayoutAnimation, View} from 'react-native' -import { - AppBskyFeedPost, - AppBskyRichtextFacet, - AtUri, - moderatePost, - RichText as RichTextAPI, -} from '@atproto/api' +import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {moderatePost} from '@bsky.app/sdk/moderation' import {Trans, useLingui} from '@lingui/react/macro' import {type RouteProp, useNavigation, useRoute} from '@react-navigation/native' @@ -36,6 +32,7 @@ import {ContentHider} from '#/components/moderation/ContentHider' import {PostAlerts} from '#/components/moderation/PostAlerts' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' /** @@ -166,13 +163,7 @@ function MessageInputPostEmbed({ ) const {rt, record} = useMemo(() => { - if ( - post && - bsky.dangerousIsType( - post.record, - AppBskyFeedPost.isRecord, - ) - ) { + if (post && bsky.isType(app.bsky.feed.post, post.record)) { return { rt: new RichTextAPI({ text: post.record.text, diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index 312a2277ad..a0f78b4178 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -22,16 +22,9 @@ import Animated, { } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {scheduleOnRN} from 'react-native-worklets' -import { - type $Typed, - type AppBskyEmbedRecord, - AppBskyRichtextFacet, - ChatBskyConvoDefs, - type ChatBskyEmbedJoinLink, - ChatBskyGroupDefs, - RichText, -} from '@atproto/api' +import {type $Typed} from '@atproto/lex' import {useScrollEdgeEffectRef} from '@bsky.app/expo-scroll-edge-effect' +import {RichText} from '@bsky.app/sdk/richtext' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {mergeRefs} from '#/lib/merge-refs' @@ -52,7 +45,7 @@ import {type ConvoState, ConvoStatus} from '#/state/messages/convo/types' import {useGetJoinLinkPreview} from '#/state/queries/join-links' import {useGetPost} from '#/state/queries/post' import {createEmbedViewRecordFromPost} from '#/state/queries/postgate/util' -import {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession} from '#/state/session' import {List, type ListMethods} from '#/view/com/util/List' import {MessageComposer} from '#/screens/Messages/components/MessageComposer' import {MessageListError} from '#/screens/Messages/components/MessageListError' @@ -71,6 +64,8 @@ import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_ANDROID, IS_NATIVE, IS_WEB} from '#/env' +import {app, chat} from '#/lexicons' +import * as bsky from '#/types/bsky' import {ChatStatusInfo} from './ChatStatusInfo' import {groupSystemMessages, type RenderItem} from './groupSystemMessages' import {InviteLinkDialogProvider} from './InviteLinkDialogProvider' @@ -116,8 +111,8 @@ function getNeighborMessage( neighbor.type === 'deleted-message' ) { if ( - ChatBskyConvoDefs.isMessageView(neighbor.message) || - ChatBskyConvoDefs.isDeletedMessageView(neighbor.message) + bsky.isType(chat.bsky.convo.defs.messageView, neighbor.message) || + bsky.isType(chat.bsky.convo.defs.deletedMessageView, neighbor.message) ) { return neighbor.message } @@ -144,7 +139,12 @@ export function MessagesList({ }) { const ax = useAnalytics() const convoState = useConvoActive() - const agent = useAgent() + /* + * Facet detection resolves handles via `com.atproto.identity.resolveHandle`, + * which the account (PDS) client serves - chat requires a session, so the + * client is always live here. + */ + const pdsClient = usePdsClient() const {hasSession, currentAccount} = useSession() const getPost = useGetPost() const getJoinLinkPreview = useGetJoinLinkPreview() @@ -520,7 +520,7 @@ export function MessagesList({ async ( text: string, embedState?: MessageEmbedState, - reply?: $Typed, + reply?: $Typed, ) => { let rt = new RichText({text: text.trimEnd()}, {cleanNewlines: true}) @@ -533,14 +533,14 @@ export function MessagesList({ rt.detectFacetsWithoutResolution() let embed: - | $Typed - | $Typed + | $Typed + | $Typed | undefined let embedView: - | $Typed - | $Typed + | $Typed + | $Typed | undefined - let replyTo: ChatBskyConvoDefs.ReplyRef | undefined + let replyTo: chat.bsky.convo.defs.ReplyRef | undefined /** * Find the embedded link facet and, if it's at the start or end of the @@ -550,7 +550,8 @@ export function MessagesList({ const linkFacet = rt.facets?.find(facet => facet.features.find( feature => - AppBskyRichtextFacet.isLink(feature) && predicate(feature.uri), + bsky.isType(app.bsky.richtext.facet.link, feature) && + predicate(feature.uri), ), ) if (linkFacet) { @@ -568,18 +569,24 @@ export function MessagesList({ try { const post = await getPost({uri: embedState.uri}) if (post) { - embed = { + /* + * `post` comes from the still-old-typed `useGetPost` producer + * (migrates in a later task), so its uri/view shapes carry plain + * strings where the lexicon types are branded. TODO(phase4): drop + * toLex once that producer migrates. + */ + embed = bsky.toLex<$Typed>({ $type: 'app.bsky.embed.record', record: { uri: post.uri, cid: post.cid, }, - } + }) - embedView = { + embedView = bsky.toLex<$Typed>({ $type: 'app.bsky.embed.record#view', record: createEmbedViewRecordFromPost(post), - } + }) stripLinkFacet(uri => { if (!isBskyPostUrl(uri)) return false @@ -604,10 +611,14 @@ export function MessagesList({ const joinLinkPreview = await getJoinLinkPreview({code, hasSession}) if (joinLinkPreview) { - embedView = { + /* + * The preview comes from the still-old-typed `join-links.ts` query + * (migrates in a later task). TODO(phase4): drop toLex once it does. + */ + embedView = bsky.toLex<$Typed>({ $type: 'chat.bsky.embed.joinLink#view', joinLinkPreview, - } + }) } stripLinkFacet(uri => getChatInviteCodeFromUrl(uri) === code) @@ -617,7 +628,7 @@ export function MessagesList({ replyTo = {messageId: reply.id} } - await rt.detectFacets(agent) + await rt.detectFacets(pdsClient) rt = shortenLinks(rt) rt = stripInvalidMentions(rt) @@ -658,7 +669,10 @@ export function MessagesList({ } if ( embedView?.$type === 'chat.bsky.embed.joinLink#view' && - ChatBskyGroupDefs.isJoinLinkPreviewView(embedView.joinLinkPreview) + bsky.isType( + chat.bsky.group.defs.joinLinkPreviewView, + embedView.joinLinkPreview, + ) ) { ax.metric('groupchat:inviteLink:shared', { convoId: embedView.joinLinkPreview.convoId, @@ -667,7 +681,7 @@ export function MessagesList({ } }, [ - agent, + pdsClient, convoState, getPost, getJoinLinkPreview, @@ -896,7 +910,7 @@ function Composer({ onSendMessage: ( message: string, embed?: MessageEmbedState, - replyTo?: $Typed, + replyTo?: $Typed, ) => Promise messageEmbed: MessageEmbedState | undefined setEmbed: (embedUrl: string | undefined) => void @@ -906,7 +920,7 @@ function Composer({ ( message: string, embed?: MessageEmbedState, - replyTo?: $Typed, + replyTo?: $Typed, ) => { void onSendMessage(message, embed, replyTo) }, diff --git a/src/screens/Messages/components/MessagesListInfoPanel.tsx b/src/screens/Messages/components/MessagesListInfoPanel.tsx index e1e620adee..c2e276879f 100644 --- a/src/screens/Messages/components/MessagesListInfoPanel.tsx +++ b/src/screens/Messages/components/MessagesListInfoPanel.tsx @@ -1,5 +1,5 @@ import {View} from 'react-native' -import {moderateProfile} from '@atproto/api' +import {moderateProfile} from '@bsky.app/sdk/moderation' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' @@ -16,6 +16,7 @@ import {Person_Stroke2_Corner2_Rounded as PersonIcon} from '#/components/icons/P import {ProfileBadges} from '#/components/ProfileBadges' import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' +import {toLex} from '#/types/bsky' export function MessagesListInfoPanel({ convo, @@ -34,10 +35,11 @@ export function MessagesListInfoPanel({ )[0] const handle = sanitizeHandle(profile.handle, '@') const displayName = moderationOpts - ? createSanitizedDisplayName( + ? // TODO(phase4): drop toLex once convo members emit #/lexicons views + createSanitizedDisplayName( profile, true, - moderateProfile(profile, moderationOpts).ui('displayName'), + moderateProfile(toLex(profile), moderationOpts).ui('displayName'), ) : handle const profileLink = diff --git a/src/screens/Messages/components/OutgoingRequestListItem.tsx b/src/screens/Messages/components/OutgoingRequestListItem.tsx index b5dcdf08df..ef280919ba 100644 --- a/src/screens/Messages/components/OutgoingRequestListItem.tsx +++ b/src/screens/Messages/components/OutgoingRequestListItem.tsx @@ -1,11 +1,9 @@ import {View} from 'react-native' -import { - type ChatBskyGroupDefs, - ChatBskyGroupWithdrawJoinRequest, -} from '@atproto/api' +import {type ChatBskyGroupDefs} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {isNetworkError} from '#/lib/strings/errors' +import {getErrorName} from '#/lib/xrpc-error' import {useWithdrawJoinGroupChatRequest} from '#/state/queries/messages/withdraw-join-group-chat' import {TimeElapsed} from '#/view/com/util/TimeElapsed' import {atoms as a, useTheme, web} from '#/alf' @@ -34,10 +32,7 @@ export function OutgoingRequestListItem({ let errorMessage = l`Failed to rescind your request. Please try again.` if (isNetworkError(error)) { errorMessage = l`There was a problem with your internet connection, please try again` - } else if ( - error instanceof - ChatBskyGroupWithdrawJoinRequest.InvalidJoinRequestError - ) { + } else if (getErrorName(error) === 'InvalidJoinRequest') { errorMessage = l`Invalid rescind request.` } Toast.show(errorMessage) diff --git a/src/screens/Messages/components/RequestButtons.tsx b/src/screens/Messages/components/RequestButtons.tsx index c5e0e82ece..5b316201b3 100644 --- a/src/screens/Messages/components/RequestButtons.tsx +++ b/src/screens/Messages/components/RequestButtons.tsx @@ -41,6 +41,8 @@ import {Loader} from '#/components/Loader' import * as Menu from '#/components/Menu' import {ReportDialog} from '#/components/moderation/ReportDialog' import * as Toast from '#/components/Toast' +import {type chat} from '#/lexicons' +import * as bsky from '#/types/bsky' export function RejectMenu({ convo, @@ -228,7 +230,18 @@ export function AcceptChatButton({ onMutate: () => { onAcceptConvo?.() if (currentScreen === 'list') { - precacheConvoQuery(queryClient, {...convo, status: 'accepted'}) + /* + * `convo` is the old-typed view threaded from `#/components/dms/util` + * consumers while the convo cache is now keyed on the lexicon + * ConvoView. TODO(phase4): drop toLex once this file's props flip. + */ + precacheConvoQuery( + queryClient, + bsky.toLex({ + ...convo, + status: 'accepted', + }), + ) navigation.navigate('MessagesConversation', { conversation: convo.id, accept: true, diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx index 9aea34b89d..416390f075 100644 --- a/src/screens/Moderation/index.tsx +++ b/src/screens/Moderation/index.tsx @@ -1,6 +1,6 @@ import {Fragment, useCallback} from 'react' import {Linking, View} from 'react-native' -import {LABELS} from '@atproto/api' +import {LABELS} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/screens/Onboarding/StepFinished/index.tsx b/src/screens/Onboarding/StepFinished/index.tsx index 11dfd35ceb..88cd651c3a 100644 --- a/src/screens/Onboarding/StepFinished/index.tsx +++ b/src/screens/Onboarding/StepFinished/index.tsx @@ -4,7 +4,6 @@ import { type AppBskyActorDefs, type AppBskyActorProfile, type AppBskyGraphDefs, - AppBskyGraphStarterpack, type Un$Typed, } from '@atproto/api' import {TID} from '@atproto/common-web' @@ -48,6 +47,7 @@ import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/ico import {Loader} from '#/components/Loader' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' import {ValuePropositionPager} from './ValuePropositionPager' @@ -209,10 +209,7 @@ export function StepFinished() { usedStarterPack: Boolean(starterPack), starterPackName: starterPack && - bsky.dangerousIsType( - starterPack.record, - AppBskyGraphStarterpack.isRecord, - ) + bsky.isType(app.bsky.graph.starterpack, starterPack.record) ? starterPack.record.name : undefined, starterPackCreator: starterPack?.creator.did, diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx index 514e892895..e4885c87f5 100644 --- a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx +++ b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx @@ -1,6 +1,6 @@ import {useCallback, useEffect, useMemo, useRef, useState} from 'react' import {View} from 'react-native' -import {type ModerationOpts} from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx b/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx index b016af65db..f6a82b7c70 100644 --- a/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx +++ b/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx @@ -1,6 +1,6 @@ import {useState} from 'react' import {View} from 'react-native' -import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' +import {type AppBskyGraphDefs} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -21,6 +21,7 @@ import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' const IGNORED_ACCOUNT = 'did:plc:pifkcjimdcfwaxkanzhwxufp' @@ -102,12 +103,7 @@ export function StarterPackCard({ }) } - if ( - !bsky.dangerousIsType( - record, - AppBskyGraphStarterpack.isRecord, - ) - ) { + if (!bsky.isType(app.bsky.graph.starterpack, record)) { return null } diff --git a/src/screens/PostThread/components/LikesStat.tsx b/src/screens/PostThread/components/LikesStat.tsx index eab32dc274..bde37ef920 100644 --- a/src/screens/PostThread/components/LikesStat.tsx +++ b/src/screens/PostThread/components/LikesStat.tsx @@ -1,5 +1,7 @@ import {View} from 'react-native' -import {type AppBskyFeedDefs, AtUri, moderateProfile} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {moderateProfile} from '@bsky.app/sdk/moderation' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {makeProfileLink} from '#/lib/routes/links' @@ -14,6 +16,7 @@ import {useFormatPostStatCount} from '#/components/PostControls/util' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +import {toLex} from '#/types/bsky' const AVI_SIZE = 20 @@ -91,7 +94,8 @@ export function KnownLikers({post}: {post: AppBskyFeedDefs.PostView}) { .map(like => like.actor) .map(actor => ({ actor, - moderation: moderateProfile(actor, moderationOpts), + // TODO(phase4): drop toLex once useLikedBySampleQuery emits #/lexicons views + moderation: moderateProfile(toLex(actor), moderationOpts), })) .filter(({actor, moderation}) => { const modui = moderation.ui('profileList') diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index 7973bc1a8a..8851fb488e 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -4,9 +4,9 @@ import { AppBskyFeedDefs, AppBskyFeedPost, type AppBskyFeedThreadgate, - AtUri, - RichText as RichTextAPI, } from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' @@ -58,6 +58,7 @@ import {Text} from '#/components/Typography' import {WhoCanReply} from '#/components/WhoCanReply' import {useAnalytics} from '#/analytics' import {useActorStatus} from '#/features/liveNow' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' export function ThreadItemAnchor({ @@ -563,10 +564,7 @@ function BackdatedPostIndicator({post}: {post: AppBskyFeedDefs.PostView}) { const control = Prompt.usePromptControl() const indexedAt = new Date(post.indexedAt) - const createdAt = bsky.dangerousIsType( - post.record, - AppBskyFeedPost.isRecord, - ) + const createdAt = bsky.isType(app.bsky.feed.post, post.record) ? new Date(post.record.createdAt) : new Date(post.indexedAt) diff --git a/src/screens/PostThread/components/ThreadItemPost.tsx b/src/screens/PostThread/components/ThreadItemPost.tsx index de0b266526..09a96f3247 100644 --- a/src/screens/PostThread/components/ThreadItemPost.tsx +++ b/src/screens/PostThread/components/ThreadItemPost.tsx @@ -1,11 +1,8 @@ import {memo, type ReactNode, useCallback, useMemo, useState} from 'react' import {View} from 'react-native' -import { - type AppBskyFeedDefs, - type AppBskyFeedThreadgate, - AtUri, - RichText as RichTextAPI, -} from '@atproto/api' +import {type AppBskyFeedDefs, type AppBskyFeedThreadgate} from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Trans} from '@lingui/react/macro' import {MAX_POST_LINES} from '#/lib/constants' diff --git a/src/screens/PostThread/components/ThreadItemTreePost.tsx b/src/screens/PostThread/components/ThreadItemTreePost.tsx index 83b77b4882..88ea0c12c3 100644 --- a/src/screens/PostThread/components/ThreadItemTreePost.tsx +++ b/src/screens/PostThread/components/ThreadItemTreePost.tsx @@ -1,11 +1,8 @@ import {memo, useCallback, useMemo, useState} from 'react' import {View} from 'react-native' -import { - type AppBskyFeedDefs, - type AppBskyFeedThreadgate, - AtUri, - RichText as RichTextAPI, -} from '@atproto/api' +import {type AppBskyFeedDefs, type AppBskyFeedThreadgate} from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Trans} from '@lingui/react/macro' import {MAX_POST_LINES} from '#/lib/constants' diff --git a/src/screens/Profile/Header/DisplayName.tsx b/src/screens/Profile/Header/DisplayName.tsx index 684a9fe53b..a835becbfe 100644 --- a/src/screens/Profile/Header/DisplayName.tsx +++ b/src/screens/Profile/Header/DisplayName.tsx @@ -1,5 +1,6 @@ import {View} from 'react-native' -import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type ModerationDecision} from '@bsky.app/sdk/moderation' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index 96e136da5c..2c44c3cc03 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -1,12 +1,8 @@ import {memo, useCallback, useMemo, useState} from 'react' import {View} from 'react-native' -import { - type AppBskyActorDefs, - type AppBskyLabelerDefs, - moderateProfile, - type ModerationOpts, - type RichText as RichTextAPI, -} from '@atproto/api' +import {type AppBskyActorDefs, type AppBskyLabelerDefs} from '@atproto/api' +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' import {Plural, Trans} from '@lingui/react/macro' @@ -37,6 +33,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_IOS} from '#/env' +import {toLex} from '#/types/bsky' import {ProfileHeaderDisplayName} from './DisplayName' import {EditProfileDialog} from './EditProfileDialog' import {ProfileHeaderHandle} from './Handle' @@ -70,7 +67,7 @@ let ProfileHeaderLabeler = ({ const isSelf = currentAccount?.did === profile.did const moderation = useMemo( - () => moderateProfile(profile, moderationOpts), + () => moderateProfile(toLex(profile), moderationOpts), [profile, moderationOpts], ) const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation() diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx index da01ded772..87cac9a3b5 100644 --- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -1,12 +1,12 @@ import {memo, useMemo, useState} from 'react' import {View} from 'react-native' +import {type AppBskyActorDefs} from '@atproto/api' import { - type AppBskyActorDefs, moderateProfile, type ModerationDecision, type ModerationOpts, - type RichText as RichTextAPI, -} from '@atproto/api' +} from '@bsky.app/sdk/moderation' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -40,6 +40,7 @@ import {useAnalytics} from '#/analytics' import {IS_IOS, IS_NATIVE} from '#/env' import {InviteFriendsDialog} from '#/features/inviteFriends' import {useActorStatus} from '#/features/liveNow' +import {toLex} from '#/types/bsky' import {GermButton} from '../components/GermButton' import {ProfileHeaderDisplayName} from './DisplayName' import {EditProfileDialog} from './EditProfileDialog' @@ -68,7 +69,7 @@ let ProfileHeaderStandard = ({ const {currentAccount} = useSession() const {_} = useLingui() const moderation = useMemo( - () => moderateProfile(profile, moderationOpts), + () => moderateProfile(toLex(profile), moderationOpts), [profile, moderationOpts], ) const [, queueUnblock] = useProfileBlockMutationQueue(profile) diff --git a/src/screens/Profile/Header/Shell.tsx b/src/screens/Profile/Header/Shell.tsx index 94a2a9bb18..07b2b659a3 100644 --- a/src/screens/Profile/Header/Shell.tsx +++ b/src/screens/Profile/Header/Shell.tsx @@ -5,8 +5,9 @@ import Animated, { useAnimatedRef, } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' import {utils} from '@bsky.app/alf' +import {type ModerationDecision} from '@bsky.app/sdk/moderation' import {useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' diff --git a/src/screens/Profile/Header/index.tsx b/src/screens/Profile/Header/index.tsx index 0b7ab16ec4..bc18109fcc 100644 --- a/src/screens/Profile/Header/index.tsx +++ b/src/screens/Profile/Header/index.tsx @@ -7,13 +7,9 @@ import Animated, { } from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {scheduleOnRN} from 'react-native-worklets' -import { - type AppBskyActorDefs, - type AppBskyLabelerDefs, - moderateProfile, - type ModerationOpts, - type RichText as RichTextAPI, -} from '@atproto/api' +import {type AppBskyActorDefs, type AppBskyLabelerDefs} from '@atproto/api' +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 {sanitizeHandle} from '#/lib/strings/handles' @@ -26,6 +22,7 @@ import {atoms as a, useTheme} from '#/alf' import {Header} from '#/components/Layout' import * as ProfileCard from '#/components/ProfileCard' import {IS_NATIVE} from '#/env' +import {toLex} from '#/types/bsky' import { HeaderLabelerButtons, ProfileHeaderLabeler, @@ -115,7 +112,8 @@ const MinimalHeader = memo(function MinimalHeader({ const profile = useProfileShadow(profileUnshadowed) const moderationOpts = useModerationOpts() const moderation = useMemo( - () => (moderationOpts ? moderateProfile(profile, moderationOpts) : null), + () => + moderationOpts ? moderateProfile(toLex(profile), moderationOpts) : null, [moderationOpts, profile], ) const [visible, setVisible] = useState(false) diff --git a/src/screens/Profile/Sections/Labels.tsx b/src/screens/Profile/Sections/Labels.tsx index 8c70d40392..854dd37ce2 100644 --- a/src/screens/Profile/Sections/Labels.tsx +++ b/src/screens/Profile/Sections/Labels.tsx @@ -1,11 +1,11 @@ import {useCallback, useEffect, useImperativeHandle, useMemo} from 'react' import {type ListRenderItemInfo, View} from 'react-native' +import {type AppBskyLabelerDefs} from '@atproto/api' import { - type AppBskyLabelerDefs, type InterpretedLabelValueDefinition, interpretLabelValueDefinitions, type ModerationOpts, -} from '@atproto/api' +} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/screens/Profile/components/GermButton.tsx b/src/screens/Profile/components/GermButton.tsx index 0b3b79e36a..04a31616c1 100644 --- a/src/screens/Profile/components/GermButton.tsx +++ b/src/screens/Profile/components/GermButton.tsx @@ -1,6 +1,7 @@ import {Platform, View} from 'react-native' import {Image} from 'expo-image' -import {type AppBskyActorDefs, type AppBskyActorGetProfile} from '@atproto/api' +import {type Client} from '@atproto/lex-client' +import {type DidString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -9,7 +10,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query' import {until} from '#/lib/async/until' import {isNetworkError} from '#/lib/strings/errors' import {RQKEY} from '#/state/queries/profile' -import {type SessionAgent, useAgent, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {atoms as a, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -20,13 +21,14 @@ import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +import {app, com} from '#/lexicons' import type * as bsky from '#/types/bsky' export function GermButton({ germ, profile, }: { - germ: AppBskyActorDefs.ProfileAssociatedGerm + germ: app.bsky.actor.defs.ProfileAssociatedGerm profile: bsky.profile.AnyProfileView }) { const t = useTheme() @@ -115,25 +117,26 @@ function GermSelfButton({did}: {did: string}) { const ax = useAnalytics() const {_} = useLingui() const selfExplanationDialogControl = Dialog.useDialogControl() - const agent = useAgent() + const pdsClient = usePdsClient() + const appviewClient = useAppviewClient() const queryClient = useQueryClient() const {mutate: deleteDeclaration, isPending} = useMutation({ mutationFn: async () => { - const previousRecord = await agent.com.germnetwork.declaration - .get({ - repo: did, + const previousRecord = await pdsClient + .get(com.germnetwork.declaration, { + repo: did as DidString, rkey: 'self', }) .then(res => res.value) .catch(() => null) - await agent.com.germnetwork.declaration.delete({ - repo: did, + await pdsClient.delete(com.germnetwork.declaration, { + repo: did as DidString, rkey: 'self', }) - await whenAppViewReady(agent, did, res => !res.data.associated?.germ) + await whenAppViewReady(appviewClient, did, res => !res.associated?.germ) return previousRecord }, @@ -143,14 +146,15 @@ function GermSelfButton({did}: {did: string}) { async function undo() { if (!previousRecord) return try { - await agent.com.germnetwork.declaration.put( - { - repo: did, - rkey: 'self', - }, - previousRecord, + await pdsClient.put(com.germnetwork.declaration, previousRecord, { + repo: did as DidString, + rkey: 'self', + }) + await whenAppViewReady( + appviewClient, + did, + res => !!res.associated?.germ, ) - await whenAppViewReady(agent, did, res => !!res.data.associated?.germ) await queryClient.refetchQueries({queryKey: RQKEY(did)}) Toast.show(_(msg`Germ DM reconnected`)) @@ -273,7 +277,7 @@ function GermSelfButton({did}: {did: string}) { } function constructGermUrl( - declaration: AppBskyActorDefs.ProfileAssociatedGerm, + declaration: app.bsky.actor.defs.ProfileAssociatedGerm, profile: bsky.profile.AnyProfileView, viewerDid?: string, ) { @@ -319,14 +323,17 @@ function platform() { } async function whenAppViewReady( - agent: SessionAgent, + appviewClient: Client, actor: string, - fn: (res: AppBskyActorGetProfile.Response) => boolean, + fn: (res: app.bsky.actor.getProfile.$OutputBody) => boolean, ) { await until( 5, // 5 tries 1e3, // 1s delay between tries fn, - () => agent.app.bsky.actor.getProfile({actor}), + () => + appviewClient.call(app.bsky.actor.getProfile, { + actor: actor as DidString, + }), ) } diff --git a/src/screens/Profile/components/ProfileFeedHeader.tsx b/src/screens/Profile/components/ProfileFeedHeader.tsx index 4cfe838e13..ef888885ae 100644 --- a/src/screens/Profile/components/ProfileFeedHeader.tsx +++ b/src/screens/Profile/components/ProfileFeedHeader.tsx @@ -1,6 +1,6 @@ import {useCallback, useMemo, useState} from 'react' import {View} from 'react-native' -import {AtUri} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {TRENDING_HANDLE} from '#/lib/constants' diff --git a/src/screens/ProfileList/components/Header.tsx b/src/screens/ProfileList/components/Header.tsx index 9d6c4f3ff7..41e0b02ca7 100644 --- a/src/screens/ProfileList/components/Header.tsx +++ b/src/screens/ProfileList/components/Header.tsx @@ -1,6 +1,7 @@ import {useMemo} from 'react' import {View} from 'react-native' -import {AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api' +import {AppBskyGraphDefs} from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -23,6 +24,7 @@ import {Loader} from '#/components/Loader' import {RichText} from '#/components/RichText' import * as Toast from '#/components/Toast' import {useAnalytics} from '#/analytics' +import {toLex} from '#/types/bsky' import {MoreOptionsMenu} from './MoreOptionsMenu' import {SubscribeMenu} from './SubscribeMenu' @@ -128,7 +130,8 @@ export function Header({ list.description ? new RichTextAPI({ text: list.description, - facets: list.descriptionFacets, + // TODO(phase4): drop toLex once the list view producer emits #/lexicons facets + facets: toLex(list.descriptionFacets), }) : undefined, [list], diff --git a/src/screens/ProfileList/components/MoreOptionsMenu.tsx b/src/screens/ProfileList/components/MoreOptionsMenu.tsx index 8bc40682d1..04974d62c6 100644 --- a/src/screens/ProfileList/components/MoreOptionsMenu.tsx +++ b/src/screens/ProfileList/components/MoreOptionsMenu.tsx @@ -1,4 +1,5 @@ -import {type AppBskyActorDefs, AppBskyGraphDefs, AtUri} from '@atproto/api' +import {type AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/screens/ProfileList/index.tsx b/src/screens/ProfileList/index.tsx index 77af470cd5..1467885b46 100644 --- a/src/screens/ProfileList/index.tsx +++ b/src/screens/ProfileList/index.tsx @@ -1,12 +1,9 @@ import {useCallback, useMemo, useRef, useState} from 'react' import {View} from 'react-native' import {useAnimatedRef} from 'react-native-reanimated' -import { - AppBskyGraphDefs, - AtUri, - moderateUserList, - type ModerationOpts, -} from '@atproto/api' +import {AppBskyGraphDefs} from '@atproto/api' +import {AtUri} from '@atproto/syntax' +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' @@ -42,6 +39,7 @@ import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' import * as Hider from '#/components/moderation/Hider' import {IS_WEB} from '#/env' +import {toLex} from '#/types/bsky' import {AboutSection} from './AboutSection' import {ErrorScreen} from './components/ErrorScreen' import {Header} from './components/Header' @@ -169,7 +167,8 @@ function ProfileListScreenLoaded({ const [headerHeight, setHeaderHeight] = useState(null) const moderation = useMemo(() => { - return moderateUserList(list, moderationOpts) + // TODO(phase4): drop toLex once ListView prop emits #/lexicons views + return moderateUserList(toLex(list), moderationOpts) }, [list, moderationOpts]) useSetTitle(isHidden ? _(msg`List Hidden`) : list.name) diff --git a/src/screens/Search/components/ModuleHeader.tsx b/src/screens/Search/components/ModuleHeader.tsx index ce40af98b8..65fbe8eb01 100644 --- a/src/screens/Search/components/ModuleHeader.tsx +++ b/src/screens/Search/components/ModuleHeader.tsx @@ -1,6 +1,7 @@ import {useMemo} from 'react' import {View} from 'react-native' -import {type AppBskyFeedDefs, AtUri} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {PressableScale} from '#/lib/custom-animations/PressableScale' import {makeCustomFeedLink} from '#/lib/routes/links' diff --git a/src/screens/Search/components/SearchHistory.tsx b/src/screens/Search/components/SearchHistory.tsx index 31186726b2..07086e48ef 100644 --- a/src/screens/Search/components/SearchHistory.tsx +++ b/src/screens/Search/components/SearchHistory.tsx @@ -1,5 +1,5 @@ import {ScrollView, View} from 'react-native' -import {moderateProfile, type ModerationOpts} from '@atproto/api' +import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {createHitslop} from '#/lib/constants' @@ -22,6 +22,7 @@ import {ProfileBadges} from '#/components/ProfileBadges' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import type * as bsky from '#/types/bsky' +import {toLex} from '#/types/bsky' export function SearchHistory({ searchHistory, @@ -206,7 +207,8 @@ function RecentProfileItem({ const {t: l} = useLingui() const width = 80 - const moderation = moderateProfile(profile, moderationOpts) + // TODO(phase4): drop toLex once profile prop emits #/lexicons views + const moderation = moderateProfile(toLex(profile), moderationOpts) const name = sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName'), diff --git a/src/screens/Search/components/SearchProfileCard.tsx b/src/screens/Search/components/SearchProfileCard.tsx index a5ca18ee13..b848739ed3 100644 --- a/src/screens/Search/components/SearchProfileCard.tsx +++ b/src/screens/Search/components/SearchProfileCard.tsx @@ -1,6 +1,6 @@ import {useCallback} from 'react' import {View} from 'react-native' -import {type ModerationOpts} from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' diff --git a/src/screens/Search/components/StarterPackCard.tsx b/src/screens/Search/components/StarterPackCard.tsx index 9831746b42..c3316e8b1c 100644 --- a/src/screens/Search/components/StarterPackCard.tsx +++ b/src/screens/Search/components/StarterPackCard.tsx @@ -1,10 +1,7 @@ import {useState} from 'react' import {View} from 'react-native' -import { - type AppBskyGraphDefs, - AppBskyGraphStarterpack, - moderateProfile, -} from '@atproto/api' +import {type AppBskyGraphDefs} from '@atproto/api' +import {moderateProfile} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -22,6 +19,7 @@ import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {useStarterPackLink} from '#/components/StarterPack/StarterPackCard' import {SubtleHover} from '#/components/SubtleHover' import {Text} from '#/components/Typography' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' export function StarterPackCard({ @@ -38,12 +36,7 @@ export function StarterPackCard({ const link = useStarterPackLink({view}) const record = view.record - if ( - !bsky.dangerousIsType( - record, - AppBskyGraphStarterpack.isRecord, - ) - ) { + if (!bsky.isType(app.bsky.graph.starterpack, record)) { return null } diff --git a/src/screens/Search/modules/ExploreSuggestedAccounts.tsx b/src/screens/Search/modules/ExploreSuggestedAccounts.tsx index c32d4eefd3..ef6dc0cba9 100644 --- a/src/screens/Search/modules/ExploreSuggestedAccounts.tsx +++ b/src/screens/Search/modules/ExploreSuggestedAccounts.tsx @@ -1,6 +1,7 @@ import {memo, useEffect} from 'react' import {View} from 'react-native' -import {type AppBskyActorSearchActors, type ModerationOpts} from '@atproto/api' +import {type AppBskyActorSearchActors} from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {type InfiniteData} from '@tanstack/react-query' diff --git a/src/screens/Search/modules/ExploreTrendingTopics.tsx b/src/screens/Search/modules/ExploreTrendingTopics.tsx index 38c8cac0a8..b20da3524c 100644 --- a/src/screens/Search/modules/ExploreTrendingTopics.tsx +++ b/src/screens/Search/modules/ExploreTrendingTopics.tsx @@ -1,11 +1,9 @@ import {useMemo} from 'react' import {Pressable, View} from 'react-native' import {Image} from 'expo-image' -import { - type AppBskyUnspeccedDefs, - moderateProfile, - RichText as RichTextApi, -} from '@atproto/api' +import {type AppBskyUnspeccedDefs} from '@atproto/api' +import {moderateProfile} from '@bsky.app/sdk/moderation' +import {RichText as RichTextApi} from '@bsky.app/sdk/richtext' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -31,6 +29,7 @@ import {useTrendingTopicSeen} from '#/components/TrendingTopics' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import * as ModuleHeader from '../components/ModuleHeader' +import {toLex} from '#/types/bsky' const IMAGE_SIZE = 56 @@ -308,7 +307,8 @@ function useModerateTrendingActors( return actors .filter(actor => { - const decision = moderateProfile(actor, moderationOpts) + // TODO(phase4): drop toLex once TrendView actors emit #/lexicons views + const decision = moderateProfile(toLex(actor), moderationOpts) return !decision.ui('avatar').filter && !decision.ui('avatar').blur }) .slice(0, 3) diff --git a/src/screens/Search/modules/ExploreTrendingVideos.tsx b/src/screens/Search/modules/ExploreTrendingVideos.tsx index 6f173a7825..a015108951 100644 --- a/src/screens/Search/modules/ExploreTrendingVideos.tsx +++ b/src/screens/Search/modules/ExploreTrendingVideos.tsx @@ -1,6 +1,7 @@ import {useMemo} from 'react' import {ScrollView, View} from 'react-native' -import {AppBskyEmbedVideo, AtUri} from '@atproto/api' +import {AppBskyEmbedVideo} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/screens/Settings/AutomationLabelSettings.tsx b/src/screens/Settings/AutomationLabelSettings.tsx index 423bc0073a..f2009e4e12 100644 --- a/src/screens/Settings/AutomationLabelSettings.tsx +++ b/src/screens/Settings/AutomationLabelSettings.tsx @@ -1,5 +1,5 @@ import {View} from 'react-native' -import {type $Typed, ComAtprotoLabelDefs} from '@atproto/api' +import {type $Typed, type ComAtprotoLabelDefs} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {useQueryClient} from '@tanstack/react-query' @@ -22,6 +22,7 @@ import {Text} from '#/components/Typography' import {useSimpleVerificationState} from '#/components/verification' import {VerificationCheck} from '#/components/verification/VerificationCheck' import {useAnalytics} from '#/analytics' +import {com} from '#/lexicons' import * as bsky from '#/types/bsky' type Props = NativeStackScreenProps< @@ -53,9 +54,9 @@ export function AutomationLabelSettingsScreen({}: Props) { { profile, updates: existing => { - const labels: $Typed = bsky.validate( + const labels: $Typed = bsky.matches( + com.atproto.label.defs.selfLabels, existing.labels, - ComAtprotoLabelDefs.validateSelfLabels, ) ? existing.labels : { diff --git a/src/screens/Settings/FindContactsSettings.tsx b/src/screens/Settings/FindContactsSettings.tsx index 57f1edde90..a7d9af7812 100644 --- a/src/screens/Settings/FindContactsSettings.tsx +++ b/src/screens/Settings/FindContactsSettings.tsx @@ -4,8 +4,8 @@ import * as Contacts from 'expo-contacts' import { type AppBskyContactDefs, type AppBskyContactGetSyncStatus, - type ModerationOpts, } from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index 37d0e9b5fa..b8d23b7324 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -1,7 +1,8 @@ import {useState} from 'react' import {Alert, LayoutAnimation, Linking, Pressable, View} from 'react-native' import {useReducedMotion} from 'react-native-reanimated' -import {type AppBskyActorDefs, moderateProfile} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +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' @@ -69,6 +70,7 @@ import {IS_INTERNAL, IS_IOS, IS_NATIVE} from '#/env' import {useActorStatus} from '#/features/liveNow' import {device, useStorage} from '#/storage' import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged' +import {toLex} from '#/types/bsky' type Props = NativeStackScreenProps export function SettingsScreen({}: Props) { @@ -329,7 +331,8 @@ function ProfilePreview({ if (!moderationOpts) return null - const moderation = moderateProfile(profile, moderationOpts) + // TODO(phase4): drop toLex once ProfileViewDetailed prop emits #/lexicons views + const moderation = moderateProfile(toLex(profile), moderationOpts) const displayName = sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName'), @@ -636,7 +639,10 @@ function AccountRow({ { // create labels attr if needed - const labels: $Typed = bsky.validate( + const labels: $Typed = bsky.matches( + com.atproto.label.defs.selfLabels, existing.labels, - ComAtprotoLabelDefs.validateSelfLabels, ) ? existing.labels : { diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx index 4404384fb5..ab56eea29d 100644 --- a/src/screens/Signup/index.tsx +++ b/src/screens/Signup/index.tsx @@ -3,7 +3,6 @@ import {AppState, type AppStateStatus, View} from 'react-native' import ReactNativeDeviceAttest from 'react-native-device-attest' import {KeyboardAvoidingView} from 'react-native-keyboard-controller' import Animated, {FadeIn, LayoutAnimationConfig} from 'react-native-reanimated' -import {AppBskyGraphStarterpack} from '@atproto/api' import {tokens} from '@bsky.app/alf' import {Trans, useLingui} from '@lingui/react/macro' @@ -32,6 +31,7 @@ import {ScreenTransition} from '#/components/ScreenTransition' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {GCP_PROJECT_ID, IS_ANDROID} from '#/env' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' export function Signup({onPressBack}: {onPressBack: () => void}) { @@ -141,10 +141,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) { scrollable> {showStarterPackCard && - bsky.dangerousIsType( - starterPack.record, - AppBskyGraphStarterpack.isRecord, - ) ? ( + bsky.isType(app.bsky.graph.starterpack, starterPack.record) ? ( ( - starterPack.record, - AppBskyGraphStarterpack.isRecord, - ) - ) { + if (!bsky.isType(app.bsky.graph.starterpack, starterPack.record)) { return null } diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx index ddbd5cd840..4272d069bc 100644 --- a/src/screens/StarterPack/StarterPackScreen.tsx +++ b/src/screens/StarterPack/StarterPackScreen.tsx @@ -1,13 +1,10 @@ import {useCallback, useEffect, useState} from 'react' import {View} from 'react-native' import {Image} from 'expo-image' -import { - AppBskyGraphDefs, - AppBskyGraphStarterpack, - AtUri, - type ModerationOpts, - RichText as RichTextAPI, -} from '@atproto/api' +import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {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' import {Plural, Trans} from '@lingui/react/macro' @@ -77,6 +74,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' type StarterPackScreeProps = NativeStackScreenProps< @@ -406,19 +404,15 @@ function Header({ }) } - if ( - !bsky.dangerousIsType( - record, - AppBskyGraphStarterpack.isRecord, - ) - ) { + if (!bsky.isType(app.bsky.graph.starterpack, record)) { return null } const richText = record.description ? new RichTextAPI({ text: record.description, - facets: record.descriptionFacets, + // TODO(phase4): drop toLex once the starterpack record producer emits #/lexicons facets + facets: bsky.toLex(record.descriptionFacets), }) : undefined diff --git a/src/screens/StarterPack/Wizard/State.tsx b/src/screens/StarterPack/Wizard/State.tsx index 0ec8700057..5e27fc012d 100644 --- a/src/screens/StarterPack/Wizard/State.tsx +++ b/src/screens/StarterPack/Wizard/State.tsx @@ -1,13 +1,10 @@ import {createContext, useContext, useReducer} from 'react' -import { - type AppBskyFeedDefs, - type AppBskyGraphDefs, - AppBskyGraphStarterpack, -} from '@atproto/api' +import {type AppBskyFeedDefs, type AppBskyGraphDefs} from '@atproto/api' import {msg, plural} from '@lingui/core/macro' import {STARTER_PACK_MAX_SIZE} from '#/lib/constants' import * as Toast from '#/components/Toast' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' const steps = ['Details', 'Profiles', 'Feeds'] as const @@ -135,7 +132,7 @@ export function Provider({ if ( starterPack && - bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord) + bsky.matches(app.bsky.graph.starterpack, starterPack.record) ) { return { canNext: true, diff --git a/src/screens/StarterPack/Wizard/StepFeeds.tsx b/src/screens/StarterPack/Wizard/StepFeeds.tsx index 470d5a55ee..183f9fb843 100644 --- a/src/screens/StarterPack/Wizard/StepFeeds.tsx +++ b/src/screens/StarterPack/Wizard/StepFeeds.tsx @@ -1,7 +1,8 @@ import {useState} from 'react' import {type ListRenderItemInfo, View} from 'react-native' import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' -import {type AppBskyFeedDefs, type ModerationOpts} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {Trans} from '@lingui/react/macro' import {DISCOVER_FEED_URI} from '#/lib/constants' diff --git a/src/screens/StarterPack/Wizard/StepProfiles.tsx b/src/screens/StarterPack/Wizard/StepProfiles.tsx index 9f3b7076fd..45e52fa14b 100644 --- a/src/screens/StarterPack/Wizard/StepProfiles.tsx +++ b/src/screens/StarterPack/Wizard/StepProfiles.tsx @@ -1,7 +1,7 @@ import {useState} from 'react' import {type ListRenderItemInfo, View} from 'react-native' import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' -import {type ModerationOpts} from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {Trans} from '@lingui/react/macro' import {useA11y} from '#/state/a11y' diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx index c579f09d67..fcec3fd803 100644 --- a/src/screens/StarterPack/Wizard/index.tsx +++ b/src/screens/StarterPack/Wizard/index.tsx @@ -7,9 +7,9 @@ import { type AppBskyActorDefs, type AppBskyFeedDefs, type AppBskyGraphDefs, - AtUri, - type ModerationOpts, } from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' diff --git a/src/screens/Takendown.tsx b/src/screens/Takendown.tsx index 5ad254fd1c..3a08d8e2a4 100644 --- a/src/screens/Takendown.tsx +++ b/src/screens/Takendown.tsx @@ -2,19 +2,16 @@ import {useState} from 'react' import {View} from 'react-native' import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {type ComAtprotoAdminDefs, ToolsOzoneReportDefs} from '@atproto/api' +import {api} from '@bsky.app/sdk' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useMutation} from '@tanstack/react-query' import {countGraphemes} from 'unicode-segmenter/grapheme' -import { - BLUESKY_MOD_SERVICE_HEADERS, - MAX_REPORT_REASON_GRAPHEME_LENGTH, -} from '#/lib/constants' +import {MAX_REPORT_REASON_GRAPHEME_LENGTH} from '#/lib/constants' import {cleanError} from '#/lib/strings/errors' -import {useAgent, useSession, useSessionApi} from '#/state/session' +import {usePdsClient, useSession, useSessionApi} from '#/state/session' import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' import {Logo} from '#/view/icons/Logo' import {atoms as a, useBreakpoints, useTheme} from '#/alf' @@ -24,6 +21,8 @@ import {SimpleInlineLinkText} from '#/components/Link' import {Loader} from '#/components/Loader' import {P, Text} from '#/components/Typography' import {IS_WEB} from '#/env' +import {com, tools} from '#/lexicons' +import {toLex} from '#/types/bsky' const COL_WIDTH = 400 @@ -34,7 +33,7 @@ export function Takendown() { const {gtMobile} = useBreakpoints() const {currentAccount} = useSession() const {logoutCurrentAccount} = useSessionApi() - const agent = useAgent() + const pdsClient = usePdsClient() const [isAppealling, setIsAppealling] = useState(false) const [reason, setReason] = useState('') @@ -50,18 +49,18 @@ export function Takendown() { } = useMutation({ mutationFn: async (appealText: string) => { if (!currentAccount) throw new Error('No session') - await agent.com.atproto.moderation.createReport( - { - reasonType: ToolsOzoneReportDefs.REASONAPPEAL, + await pdsClient.call( + com.atproto.moderation.createReport, + toLex({ + reasonType: tools.ozone.report.defs.reasonAppeal.value, subject: { $type: 'com.atproto.admin.defs#repoRef', did: currentAccount.did, - } satisfies ComAtprotoAdminDefs.RepoRef, + }, reason: appealText, - }, + }), { - encoding: 'application/json', - headers: BLUESKY_MOD_SERVICE_HEADERS, + service: api.moderation.service, }, ) }, diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx index 2949c6a777..47b92362a3 100644 --- a/src/screens/VideoFeed/index.tsx +++ b/src/screens/VideoFeed/index.tsx @@ -25,14 +25,10 @@ import {useEvent, useEventListener} from 'expo' import {Image, type ImageStyle} from 'expo-image' import {LinearGradient} from 'expo-linear-gradient' import {createVideoPlayer, type VideoPlayer, VideoView} from 'expo-video' -import { - AppBskyEmbedVideo, - type AppBskyFeedDefs, - AppBskyFeedPost, - AtUri, - type ModerationDecision, - RichText as RichTextAPI, -} from '@atproto/api' +import {AppBskyEmbedVideo, type AppBskyFeedDefs} from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {type ModerationDecision} from '@bsky.app/sdk/moderation' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Trans, useLingui} from '@lingui/react/macro' import { type RouteProp, @@ -102,6 +98,7 @@ import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_ANDROID} from '#/env' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' import {Scrubber, VIDEO_PLAYER_BOTTOM_INSET} from './components/Scrubber' @@ -804,15 +801,13 @@ function Overlay({ ) const rkey = new AtUri(post.uri).rkey - const record = bsky.dangerousIsType( - post.record, - AppBskyFeedPost.isRecord, - ) + const record = bsky.isType(app.bsky.feed.post, post.record) ? post.record : undefined const richText = new RichTextAPI({ text: record?.text || '', - facets: record?.facets, + // TODO(phase4): drop toLex once the post record producer emits #/lexicons facets + facets: bsky.toLex(record?.facets), }) const handle = sanitizeHandle(post.author.handle, '@') diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts index 587021992e..c20d62352d 100644 --- a/src/state/messages/convo/agent.ts +++ b/src/state/messages/convo/agent.ts @@ -1,23 +1,15 @@ -import { - type $Typed, - type AppBskyEmbedRecord, - type ChatBskyActorDefs, - ChatBskyConvoDefs, - type ChatBskyConvoGetLog, - type ChatBskyConvoSendMessage, - type ChatBskyEmbedJoinLink, - type ChatBskyGroupDefs, -} from '@atproto/api' -import {XRPCError} from '@atproto/api' +import {type $Typed} from '@atproto/lex' +import {type Client} from '@atproto/lex-client' +import {type DatetimeString, type DidString} from '@atproto/syntax' import {EventEmitter} from 'eventemitter3' import {nanoid} from 'nanoid/non-secure' import {networkRetry} from '#/lib/async/retry' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import { isErrorMaybeAppPasswordPermissions, isNetworkError, } from '#/lib/strings/errors' +import {getErrorStatus, isXrpcError} from '#/lib/xrpc-error' import {Logger} from '#/logger' import { isProfileShadowApplied, @@ -45,13 +37,14 @@ import { } from '#/state/messages/convo/types' import {type MessagesEventBus} from '#/state/messages/events/agent' import {type MessagesEventBusError} from '#/state/messages/events/types' -import {type SessionAgent} from '#/state/session' import { type ConvoWithDetails, type GroupConvoMember, parseConvoView, } from '#/components/dms/util' import {IS_NATIVE} from '#/env' +import {app, chat} from '#/lexicons' +import * as bsky from '#/types/bsky' const logger = Logger.create(Logger.Context.ConversationAgent) @@ -67,23 +60,32 @@ export function isConvoItemMessage( } function toSystemMessageView( - ev: ChatBskyConvoGetLog.OutputSchema['logs'][number], -): ChatBskyConvoDefs.SystemMessageView | null { - const isSystem = - ChatBskyConvoDefs.isLogAddMember(ev) || - ChatBskyConvoDefs.isLogRemoveMember(ev) || - ChatBskyConvoDefs.isLogMemberJoin(ev) || - ChatBskyConvoDefs.isLogMemberLeave(ev) || - ChatBskyConvoDefs.isLogLockConvo(ev) || - ChatBskyConvoDefs.isLogUnlockConvo(ev) || - ChatBskyConvoDefs.isLogLockConvoPermanently(ev) || - ChatBskyConvoDefs.isLogEditGroup(ev) || - ChatBskyConvoDefs.isLogCreateJoinLink(ev) || - ChatBskyConvoDefs.isLogEditJoinLink(ev) || - ChatBskyConvoDefs.isLogEnableJoinLink(ev) || - ChatBskyConvoDefs.isLogDisableJoinLink(ev) - if (!isSystem) return null - return ev.message + ev: chat.bsky.convo.getLog.$OutputBody['logs'][number], +): chat.bsky.convo.defs.SystemMessageView | null { + /* + * The guard disjunction is kept inline in the `if` (rather than a separate + * boolean) so TS narrows `ev` to the union of system-message log types, all + * of which carry a `message: SystemMessageView`. The generated lexicon types + * are stricter than the old `@atproto/api` ones (no `[k: string]: unknown` + * index signature), so a separate boolean would not narrow the access. + */ + if ( + bsky.isType(chat.bsky.convo.defs.logAddMember, ev) || + bsky.isType(chat.bsky.convo.defs.logRemoveMember, ev) || + bsky.isType(chat.bsky.convo.defs.logMemberJoin, ev) || + bsky.isType(chat.bsky.convo.defs.logMemberLeave, ev) || + bsky.isType(chat.bsky.convo.defs.logLockConvo, ev) || + bsky.isType(chat.bsky.convo.defs.logUnlockConvo, ev) || + bsky.isType(chat.bsky.convo.defs.logLockConvoPermanently, ev) || + bsky.isType(chat.bsky.convo.defs.logEditGroup, ev) || + bsky.isType(chat.bsky.convo.defs.logCreateJoinLink, ev) || + bsky.isType(chat.bsky.convo.defs.logEditJoinLink, ev) || + bsky.isType(chat.bsky.convo.defs.logEnableJoinLink, ev) || + bsky.isType(chat.bsky.convo.defs.logDisableJoinLink, ev) + ) { + return ev.message + } + return null } /** @@ -91,8 +93,8 @@ function toSystemMessageView( * the fields the deleted view carries so a reply can render it as deleted. */ function toDeletedMessageView( - m: ChatBskyConvoDefs.MessageView, -): $Typed { + m: chat.bsky.convo.defs.MessageView, +): $Typed { return { $type: 'chat.bsky.convo.defs#deletedMessageView', id: m.id, @@ -105,9 +107,9 @@ function toDeletedMessageView( export class Convo { private id: string - private agent: SessionAgent + private chatClient: Client private events: MessagesEventBus - private senderUserDid: string + private senderUserDid: DidString private status: ConvoStatus = ConvoStatus.Uninitialized private error: ConvoError | undefined @@ -117,29 +119,29 @@ export class Convo { private pastMessages: Map< string, - | ChatBskyConvoDefs.MessageView - | ChatBskyConvoDefs.DeletedMessageView - | ChatBskyConvoDefs.SystemMessageView + | chat.bsky.convo.defs.MessageView + | chat.bsky.convo.defs.DeletedMessageView + | chat.bsky.convo.defs.SystemMessageView > = new Map() private newMessages: Map< string, - | ChatBskyConvoDefs.MessageView - | ChatBskyConvoDefs.DeletedMessageView - | ChatBskyConvoDefs.SystemMessageView + | chat.bsky.convo.defs.MessageView + | chat.bsky.convo.defs.DeletedMessageView + | chat.bsky.convo.defs.SystemMessageView > = new Map() private pendingMessages: Map< string, { id: string - message: ChatBskyConvoSendMessage.InputSchema['message'] + message: chat.bsky.convo.sendMessage.$InputBody['message'] optimisticEmbedView?: - | $Typed - | $Typed - optimisticReplyTo?: $Typed + | $Typed + | $Typed + optimisticReplyTo?: $Typed } > = new Map() private deletedMessages: Set = new Set() - private relatedProfiles: Map = + private relatedProfiles: Map = new Map() /** * Accumulated profile shadow state, keyed by did. The profiles this agent @@ -159,16 +161,16 @@ export class Convo { convoId: string convo: ConvoWithDetails | undefined - sender: ChatBskyActorDefs.ProfileViewBasic | undefined - recipients: ChatBskyActorDefs.ProfileViewBasic[] | undefined + sender: chat.bsky.actor.defs.ProfileViewBasic | undefined + recipients: chat.bsky.actor.defs.ProfileViewBasic[] | undefined snapshot: ConvoState | undefined constructor(params: ConvoParams) { this.id = nanoid(3) this.convoId = params.convoId - this.agent = params.agent + this.chatClient = params.chatClient this.events = params.events - this.senderUserDid = params.agent.assertDid + this.senderUserDid = params.chatClient.assertDid if (params.placeholderData) { this.setupPlaceholderData(params.placeholderData) @@ -589,23 +591,46 @@ export class Convo { } } - private setConvo(convo: ChatBskyConvoDefs.ConvoView) { + private setConvo(convo: chat.bsky.convo.defs.ConvoView) { this.convo = parseConvoView(convo, this.senderUserDid) ?? this.convo if (this.convo) { for (const member of this.convo.members) { - this.relatedProfiles.set(member.did, member) + // `this.convo` comes from `parseConvoView` in the still-old-typed + // `#/components/dms/util` (migrates in a later task); bridge its member + // shape to the lexicon `ProfileViewBasic` we store. TODO(phase4): drop + // toLex once dms/util migrates. + this.relatedProfiles.set( + member.did, + bsky.toLex(member), + ) } } this.applyProfileShadows() } - private updateConvo(convo: Partial) { + /* + * The partial merges into `this.convo.view` and is re-parsed by the + * still-old-typed `parseConvoView` (`#/components/dms/util`, migrates in a + * later task), and its callers build it from old-typed `this.convo.details` / + * members. So this boundary stays in the old view world - typing the param + * off `ConvoWithDetails['view']` keeps it internally consistent without a + * per-call `toLex`. TODO(phase4): flip to `chat.bsky.convo.defs.ConvoView` + * once dms/util migrates. + */ + private updateConvo(convo: Partial) { if (this.convo) { this.convo = parseConvoView({...this.convo.view, ...convo}, this.senderUserDid) ?? this.convo for (const member of this.convo.members) { - this.relatedProfiles.set(member.did, member) + // `this.convo` comes from `parseConvoView` in the still-old-typed + // `#/components/dms/util` (migrates in a later task); bridge its member + // shape to the lexicon `ProfileViewBasic` we store. TODO(phase4): drop + // toLex once dms/util migrates. + this.relatedProfiles.set( + member.did, + bsky.toLex(member), + ) } this.applyProfileShadows() } @@ -709,7 +734,7 @@ export class Convo { } private pendingFetchConvo: - | Promise<{convo: ChatBskyConvoDefs.ConvoView}> + | Promise<{convo: chat.bsky.convo.defs.ConvoView}> | undefined async fetchConvo() { if (this.pendingFetchConvo) return this.pendingFetchConvo @@ -720,13 +745,12 @@ export class Convo { this.pendingFetchConvo = (async () => { try { const response = await networkRetry(2, () => { - return this.agent.chat.bsky.convo.getConvo( - {convoId: this.convoId}, - {headers: DM_SERVICE_HEADERS}, - ) + return this.chatClient.call(chat.bsky.convo.getConvo, { + convoId: this.convoId, + }) }) - const convo = response.data.convo + const convo = response.convo return { convo, @@ -763,18 +787,15 @@ export class Convo { let cursor: string | undefined do { const result = await networkRetry(2, () => { - return this.agent.chat.bsky.convo.getConvoMembers( - { - convoId: this.convoId, - limit: 50, - cursor, - }, - {headers: DM_SERVICE_HEADERS}, - ) + return this.chatClient.call(chat.bsky.convo.getConvoMembers, { + convoId: this.convoId, + limit: 50, + cursor, + }) }) - cursor = result.data.cursor + cursor = result.cursor - for (const member of result.data.members) { + for (const member of result.members) { this.relatedProfiles.set(member.did, member) } } while (cursor) @@ -808,16 +829,13 @@ export class Convo { const nextCursor = this.oldestRev // for TS const response = await networkRetry(2, () => { - return this.agent.chat.bsky.convo.getMessages( - { - cursor: nextCursor, - convoId: this.convoId, - limit: IS_NATIVE ? 30 : 60, - }, - {headers: DM_SERVICE_HEADERS}, - ) + return this.chatClient.call(chat.bsky.convo.getMessages, { + cursor: nextCursor, + convoId: this.convoId, + limit: IS_NATIVE ? 30 : 60, + }) }) - const {cursor, messages, relatedProfiles} = response.data + const {cursor, messages, relatedProfiles} = response // Trust the cursor for pagination. We can't infer "no more pages" from a // short page: the server pages by raw rows but strips deleted messages @@ -836,9 +854,9 @@ export class Convo { for (const message of messages) { if ( - ChatBskyConvoDefs.isMessageView(message) || - ChatBskyConvoDefs.isDeletedMessageView(message) || - ChatBskyConvoDefs.isSystemMessageView(message) + bsky.isType(chat.bsky.convo.defs.messageView, message) || + bsky.isType(chat.bsky.convo.defs.deletedMessageView, message) || + bsky.isType(chat.bsky.convo.defs.systemMessageView, message) ) { /* * If this message is already in new messages, it was added by the @@ -913,7 +931,7 @@ export class Convo { this.commit() } - ingestFirehose(events: ChatBskyConvoGetLog.OutputSchema['logs']) { + ingestFirehose(events: chat.bsky.convo.getLog.$OutputBody['logs']) { let needsCommit = false for (const ev of events) { @@ -950,8 +968,8 @@ export class Convo { } if ( - ChatBskyConvoDefs.isLogCreateMessage(ev) && - ChatBskyConvoDefs.isMessageView(ev.message) + bsky.isType(chat.bsky.convo.defs.logCreateMessage, ev) && + bsky.isType(chat.bsky.convo.defs.messageView, ev.message) ) { /* * If this message is already in past messages, the initial @@ -976,8 +994,8 @@ export class Convo { } needsCommit = true } else if ( - ChatBskyConvoDefs.isLogDeleteMessage(ev) && - ChatBskyConvoDefs.isDeletedMessageView(ev.message) + bsky.isType(chat.bsky.convo.defs.logDeleteMessage, ev) && + bsky.isType(chat.bsky.convo.defs.deletedMessageView, ev.message) ) { /* * Remove the message itself, and keep its id in `deletedMessages` @@ -992,9 +1010,9 @@ export class Convo { this.deletedMessages.add(ev.message.id) needsCommit = true } else if ( - (ChatBskyConvoDefs.isLogAddReaction(ev) || - ChatBskyConvoDefs.isLogRemoveReaction(ev)) && - ChatBskyConvoDefs.isMessageView(ev.message) + (bsky.isType(chat.bsky.convo.defs.logAddReaction, ev) || + bsky.isType(chat.bsky.convo.defs.logRemoveReaction, ev)) && + bsky.isType(chat.bsky.convo.defs.messageView, ev.message) ) { /* * Update if we have this in state - replace message wholesale. If we don't, don't worry about it. @@ -1031,11 +1049,11 @@ export class Convo { private pendingMessageFailure: 'recoverable' | 'unrecoverable' | null = null sendMessage( - message: ChatBskyConvoSendMessage.InputSchema['message'], + message: chat.bsky.convo.sendMessage.$InputBody['message'], optimisticEmbedView?: - | $Typed - | $Typed, - optimisticReplyTo?: $Typed, + | $Typed + | $Typed, + optimisticReplyTo?: $Typed, ) { // Ignore empty messages for now since they have no other purpose atm if (!message.text.trim() && !message.embed) return @@ -1110,7 +1128,7 @@ export class Convo { this.commit() } - updateJoinLink(joinLink: ChatBskyGroupDefs.JoinLinkView | undefined) { + updateJoinLink(joinLink: chat.bsky.group.defs.JoinLinkView | undefined) { if (this.convo?.kind !== 'group') { throw new Error('updateJoinLink can only be called on group convo') } @@ -1126,7 +1144,7 @@ export class Convo { } updateLockStatus( - lockStatus: ChatBskyConvoDefs.ConvoLockStatus, + lockStatus: chat.bsky.convo.defs.ConvoLockStatus, lockStatusModerationOverride: boolean, ) { if (this.convo?.kind !== 'group') { @@ -1165,14 +1183,10 @@ export class Convo { const {id, message} = pendingMessage - const response = await this.agent.chat.bsky.convo.sendMessage( - { - convoId: this.convoId, - message, - }, - {encoding: 'application/json', headers: DM_SERVICE_HEADERS}, - ) - const res = response.data + const res = await this.chatClient.call(chat.bsky.convo.sendMessage, { + convoId: this.convoId, + message, + }) // remove from queue this.pendingMessages.delete(id) @@ -1197,9 +1211,18 @@ export class Convo { } } - private handleSendMessageFailure(e: Error | XRPCError) { - if (e instanceof XRPCError) { - if (NETWORK_FAILURE_STATUSES.includes(e.status)) { + private handleSendMessageFailure(e: Error) { + const status = getErrorStatus(e) + if (isXrpcError(e)) { + /* + * A status-less xrpc error is a network/transport failure (lex throws + * `XrpcInternalError`, which carries no HTTP status). The old bridge + * represented the same case with a sentinel `status` of `1`, which is a + * member of `NETWORK_FAILURE_STATUSES` - so a network failure was + * `recoverable`. Preserve that by treating `undefined` status the same + * as a network-failure status here. + */ + if (status === undefined || NETWORK_FAILURE_STATUSES.includes(status)) { this.pendingMessageFailure = 'recoverable' } else { this.pendingMessageFailure = 'unrecoverable' @@ -1226,7 +1249,7 @@ export class Convo { default: if (!isNetworkError(e)) { logger.warn(`handleSendMessageFailure could not handle error`, { - status: e.status, + status, message: e.message, }) } @@ -1261,16 +1284,15 @@ export class Convo { ) try { - const {data} = await this.agent.chat.bsky.convo.sendMessageBatch( + const {items} = await this.chatClient.call( + chat.bsky.convo.sendMessageBatch, { items: messageArray.map(({message}) => ({ convoId: this.convoId, message, })), }, - {encoding: 'application/json', headers: DM_SERVICE_HEADERS}, ) - const {items} = data /* * Insert into `newMessages` as soon as we have a real ID. That way, when @@ -1304,13 +1326,10 @@ export class Convo { try { await networkRetry(2, () => { - return this.agent.chat.bsky.convo.deleteMessageForSelf( - { - convoId: this.convoId, - messageId, - }, - {encoding: 'application/json', headers: DM_SERVICE_HEADERS}, - ) + return this.chatClient.call(chat.bsky.convo.deleteMessageForSelf, { + convoId: this.convoId, + messageId, + }) }) } catch (err) { const e = err as Error @@ -1341,11 +1360,11 @@ export class Convo { * matching what the server returns on refresh. */ private tombstoneDeletedReplyTo( - m: ChatBskyConvoDefs.MessageView, - ): ChatBskyConvoDefs.MessageView { + m: chat.bsky.convo.defs.MessageView, + ): chat.bsky.convo.defs.MessageView { const {replyTo} = m if ( - !ChatBskyConvoDefs.isMessageView(replyTo) || + !bsky.isType(chat.bsky.convo.defs.messageView, replyTo) || !this.deletedMessages.has(replyTo.id) ) { return m @@ -1360,19 +1379,19 @@ export class Convo { const items: ConvoItem[] = [] this.pastMessages.forEach(m => { - if (ChatBskyConvoDefs.isMessageView(m)) { + if (bsky.isType(chat.bsky.convo.defs.messageView, m)) { items.unshift({ type: 'message', key: m.id, message: this.tombstoneDeletedReplyTo(m), }) - } else if (ChatBskyConvoDefs.isDeletedMessageView(m)) { + } else if (bsky.isType(chat.bsky.convo.defs.deletedMessageView, m)) { items.unshift({ type: 'deleted-message', key: m.id, message: m, }) - } else if (ChatBskyConvoDefs.isSystemMessageView(m)) { + } else if (bsky.isType(chat.bsky.convo.defs.systemMessageView, m)) { items.unshift({ type: 'system-message', key: m.id, @@ -1393,19 +1412,19 @@ export class Convo { } this.newMessages.forEach(m => { - if (ChatBskyConvoDefs.isMessageView(m)) { + if (bsky.isType(chat.bsky.convo.defs.messageView, m)) { items.push({ type: 'message', key: m.id, message: this.tombstoneDeletedReplyTo(m), }) - } else if (ChatBskyConvoDefs.isDeletedMessageView(m)) { + } else if (bsky.isType(chat.bsky.convo.defs.deletedMessageView, m)) { items.push({ type: 'deleted-message', key: m.id, message: m, }) - } else if (ChatBskyConvoDefs.isSystemMessageView(m)) { + } else if (bsky.isType(chat.bsky.convo.defs.systemMessageView, m)) { items.push({ type: 'system-message', key: m.id, @@ -1429,7 +1448,9 @@ export class Convo { $type: 'chat.bsky.convo.defs#messageView', id: nanoid(), rev: '__fake__', - sentAt: new Date().toISOString(), + // ISO string is a valid datetime; assert the branded type the + // generated MessageView expects for this optimistic-only value. + sentAt: new Date().toISOString() as DatetimeString, sender: { $type: 'chat.bsky.convo.defs#messageViewSender', did: this.senderUserDid, @@ -1471,16 +1492,18 @@ 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(), + // ISO string is a valid datetime; assert the branded type the generated + // ReactionView expects for this optimistic-only value. + createdAt: new Date().toISOString() as DatetimeString, } let restore: null | (() => void) = null if (this.pastMessages.has(messageId)) { const prevMessage = this.pastMessages.get(messageId) if ( - ChatBskyConvoDefs.isMessageView(prevMessage) && + bsky.isType(chat.bsky.convo.defs.messageView, prevMessage) && // skip optimistic update if reaction already exists !prevMessage.reactions?.find( reaction => @@ -1510,7 +1533,7 @@ export class Convo { } else if (this.newMessages.has(messageId)) { const prevMessage = this.newMessages.get(messageId) if ( - ChatBskyConvoDefs.isMessageView(prevMessage) && + bsky.isType(chat.bsky.convo.defs.messageView, prevMessage) && !prevMessage.reactions?.find(reaction => reaction.value === emoji) ) { if (prevMessage.reactions && prevMessage.reactions.length >= 5) @@ -1529,11 +1552,12 @@ export class Convo { try { logger.debug(`Adding reaction ${emoji} to message ${messageId}`) - const {data} = await this.agent.chat.bsky.convo.addReaction( - {messageId, value: emoji, convoId: this.convoId}, - {encoding: 'application/json', headers: DM_SERVICE_HEADERS}, - ) - if (ChatBskyConvoDefs.isMessageView(data.message)) { + const data = await this.chatClient.call(chat.bsky.convo.addReaction, { + messageId, + value: emoji, + convoId: this.convoId, + }) + if (bsky.isType(chat.bsky.convo.defs.messageView, data.message)) { if (this.pastMessages.has(messageId)) { this.pastMessages.set(messageId, data.message) this.commit() @@ -1558,7 +1582,7 @@ export class Convo { let restore: null | (() => void) = null if (this.pastMessages.has(messageId)) { const prevMessage = this.pastMessages.get(messageId) - if (ChatBskyConvoDefs.isMessageView(prevMessage)) { + if (bsky.isType(chat.bsky.convo.defs.messageView, prevMessage)) { this.pastMessages.set(messageId, { ...prevMessage, reactions: prevMessage.reactions?.filter( @@ -1575,7 +1599,7 @@ export class Convo { } } else if (this.newMessages.has(messageId)) { const prevMessage = this.newMessages.get(messageId) - if (ChatBskyConvoDefs.isMessageView(prevMessage)) { + if (bsky.isType(chat.bsky.convo.defs.messageView, prevMessage)) { this.newMessages.set(messageId, { ...prevMessage, reactions: prevMessage.reactions?.filter( @@ -1594,10 +1618,11 @@ export class Convo { try { logger.debug(`Removing reaction ${emoji} from message ${messageId}`) - await this.agent.chat.bsky.convo.removeReaction( - {messageId, value: emoji, convoId: this.convoId}, - {encoding: 'application/json', headers: DM_SERVICE_HEADERS}, - ) + await this.chatClient.call(chat.bsky.convo.removeReaction, { + messageId, + value: emoji, + convoId: this.convoId, + }) } catch (error) { if (restore) restore() throw error diff --git a/src/state/messages/convo/index.tsx b/src/state/messages/convo/index.tsx index 69ad1e237c..2d2ca5e108 100644 --- a/src/state/messages/convo/index.tsx +++ b/src/state/messages/convo/index.tsx @@ -6,7 +6,6 @@ import { useState, useSyncExternalStore, } from 'react' -import {ChatBskyConvoDefs} from '@atproto/api' import {useFocusEffect} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' @@ -28,15 +27,20 @@ import { } from '#/state/queries/messages/conversation' import {RQKEY_ROOT as ListConvosQueryKeyRoot} from '#/state/queries/messages/list-conversations' import {RQKEY as createProfileQueryKey} from '#/state/queries/profile' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' import {type GroupConvoMember} from '#/components/dms/util' +import {chat} from '#/lexicons' +import * as bsky from '#/types/bsky' export * from '#/state/messages/convo/util' -function membersChanged( - a: ChatBskyConvoDefs.ConvoView['members'], - b: ChatBskyConvoDefs.ConvoView['members'], -) { +/* + * Only the member dids are compared, so accept any member-shaped arrays. This + * also lets the old-typed `convo.convo.members` (from `#/components/dms/util`, + * migrates in a later task) flow in alongside the new lexicon ConvoView + * members. + */ +function membersChanged(a: Array<{did: string}>, b: Array<{did: string}>) { if (a.length !== b.length) return true const aDids = new Set(a.map(m => m.did)) return b.some(m => !aDids.has(m.did)) @@ -80,15 +84,16 @@ export function ConvoProvider({ convoId, }: Pick & {children: React.ReactNode}) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() const events = useMessagesEventBus() const [convo] = useState(() => { - const placeholder = queryClient.getQueryData( - getConvoKey(convoId), - ) + const placeholder = + queryClient.getQueryData( + getConvoKey(convoId), + ) return new Convo({ convoId, - agent, + chatClient, events, placeholderData: placeholder ? {convo: placeholder} : undefined, }) @@ -141,14 +146,14 @@ export function ConvoProvider({ const queryKey = event.query.queryKey as string[] if (queryKey[0] === root && queryKey[1] === id) { const data = event.query.state.data as - | ChatBskyConvoDefs.ConvoView + | chat.bsky.convo.defs.ConvoView | undefined if (data && convo.convo && data.muted !== convo.convo.view.muted) { convo.updateMuted(data.muted) } if ( data && - ChatBskyConvoDefs.isGroupConvo(data.kind) && + bsky.isType(chat.bsky.convo.defs.groupConvo, data.kind) && convo.convo?.kind === 'group' ) { if (data.kind.name !== convo.convo.details.name) { @@ -170,7 +175,7 @@ export function ConvoProvider({ } if ( data && - ChatBskyConvoDefs.isGroupConvo(data.kind) && + bsky.isType(chat.bsky.convo.defs.groupConvo, data.kind) && convo.convo?.kind === 'group' && (membersChanged(data.members, convo.convo.members) || data.kind.memberCount !== convo.convo.details.memberCount) diff --git a/src/state/messages/convo/types.ts b/src/state/messages/convo/types.ts index 0155f10891..720df61acb 100644 --- a/src/state/messages/convo/types.ts +++ b/src/state/messages/convo/types.ts @@ -1,22 +1,16 @@ -import { - type $Typed, - type AppBskyEmbedRecord, - type ChatBskyActorDefs, - type ChatBskyConvoDefs, - type ChatBskyConvoSendMessage, - type ChatBskyEmbedJoinLink, -} from '@atproto/api' +import {type $Typed} from '@atproto/lex' +import {type Client} from '@atproto/lex-client' import {type MessagesEventBus} from '#/state/messages/events/agent' -import {type SessionAgent} from '#/state/session' import {type ConvoWithDetails} from '#/components/dms/util' +import {app, type chat} from '#/lexicons' export type ConvoParams = { convoId: string - agent: SessionAgent + chatClient: Client events: MessagesEventBus placeholderData?: { - convo: ChatBskyConvoDefs.ConvoView + convo: chat.bsky.convo.defs.ConvoView } } @@ -74,12 +68,12 @@ export type ConvoItem = | { type: 'message' key: string - message: ChatBskyConvoDefs.MessageView + message: chat.bsky.convo.defs.MessageView } | { type: 'pending-message' key: string - message: ChatBskyConvoDefs.MessageView + message: chat.bsky.convo.defs.MessageView failed: boolean /** * Retry sending the message. If present, the message is in a failed state. @@ -89,12 +83,12 @@ export type ConvoItem = | { type: 'deleted-message' key: string - message: ChatBskyConvoDefs.DeletedMessageView + message: chat.bsky.convo.defs.DeletedMessageView } | { type: 'system-message' key: string - message: ChatBskyConvoDefs.SystemMessageView + message: chat.bsky.convo.defs.SystemMessageView } | { type: 'error' @@ -108,12 +102,12 @@ export type ConvoItem = type DeleteMessage = (messageId: string) => Promise type SendMessage = ( - message: ChatBskyConvoSendMessage.InputSchema['message'], + message: chat.bsky.convo.sendMessage.$InputBody['message'], optimisticEmbedView: - | $Typed - | $Typed + | $Typed + | $Typed | undefined, - optimisticReplyTo?: $Typed, + optimisticReplyTo?: $Typed, ) => void type FetchMessageHistory = () => Promise type MarkConvoAccepted = () => void @@ -152,7 +146,7 @@ export type ConvoStateReady = { status: ConvoStatus.Ready items: ConvoItem[] convo: ConvoWithDetails - relatedProfiles: Map + relatedProfiles: Map error: undefined isFetchingHistory: boolean hasAllHistory: boolean @@ -167,7 +161,7 @@ export type ConvoStateBackgrounded = { status: ConvoStatus.Backgrounded items: ConvoItem[] convo: ConvoWithDetails - relatedProfiles: Map + relatedProfiles: Map error: undefined isFetchingHistory: boolean hasAllHistory: boolean @@ -182,7 +176,7 @@ export type ConvoStateSuspended = { status: ConvoStatus.Suspended items: ConvoItem[] convo: ConvoWithDetails - relatedProfiles: Map + relatedProfiles: Map error: undefined isFetchingHistory: boolean hasAllHistory: boolean @@ -211,7 +205,7 @@ export type ConvoStateDisabled = { status: ConvoStatus.Disabled items: ConvoItem[] convo: ConvoWithDetails - relatedProfiles: Map + relatedProfiles: Map error: undefined isFetchingHistory: boolean hasAllHistory: boolean diff --git a/src/state/messages/events/agent.ts b/src/state/messages/events/agent.ts index b7d1501461..82282e10ef 100644 --- a/src/state/messages/events/agent.ts +++ b/src/state/messages/events/agent.ts @@ -1,9 +1,8 @@ -import {type ChatBskyConvoGetLog} from '@atproto/api' +import {type Client} from '@atproto/lex-client' import {EventEmitter} from 'eventemitter3' import {nanoid} from 'nanoid/non-secure' import {networkRetry} from '#/lib/async/retry' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import { isErrorMaybeAppPasswordPermissions, isNetworkError, @@ -21,14 +20,14 @@ import { type MessagesEventBusParams, MessagesEventBusStatus, } from '#/state/messages/events/types' -import {type SessionAgent} from '#/state/session' +import {chat} from '#/lexicons' const logger = Logger.create(Logger.Context.DMsAgent) export class MessagesEventBus { private id: string - private agent: SessionAgent + private chatClient: Client private emitter = new EventEmitter<{event: [MessagesEventBusEvent]}>() private status: MessagesEventBusStatus = MessagesEventBusStatus.Initializing @@ -38,7 +37,7 @@ export class MessagesEventBus { constructor(params: MessagesEventBusParams) { this.id = nanoid(3) - this.agent = params.agent + this.chatClient = params.chatClient this.init() } @@ -261,14 +260,11 @@ export class MessagesEventBus { try { const response = await networkRetry(2, () => { - return this.agent.chat.bsky.convo.getLog( - {}, - {headers: DM_SERVICE_HEADERS}, - ) + return this.chatClient.call(chat.bsky.convo.getLog, {}) }) // throw new Error('UNCOMMENT TO TEST INIT FAILURE') - const {cursor} = response.data + const {cursor} = response // should always be defined if (cursor) { @@ -356,21 +352,18 @@ export class MessagesEventBus { // ) let needsEmit = false - let batch: ChatBskyConvoGetLog.OutputSchema['logs'] = [] + let batch: chat.bsky.convo.getLog.$OutputBody['logs'] = [] try { const response = await networkRetry(2, () => { - return this.agent.chat.bsky.convo.getLog( - { - cursor: this.latestRev, - }, - {headers: DM_SERVICE_HEADERS}, - ) + return this.chatClient.call(chat.bsky.convo.getLog, { + cursor: this.latestRev, + }) }) // throw new Error('UNCOMMENT TO TEST POLL FAILURE') - const {logs: events} = response.data + const {logs: events} = response for (const ev of events) { /* diff --git a/src/state/messages/events/index.tsx b/src/state/messages/events/index.tsx index 5d1e2ec505..2e58f48494 100644 --- a/src/state/messages/events/index.tsx +++ b/src/state/messages/events/index.tsx @@ -2,7 +2,7 @@ import {createContext, useContext, useEffect, useState} from 'react' import {AppState} from 'react-native' import {MessagesEventBus} from '#/state/messages/events/agent' -import {useAgent, useSession} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' const MessagesEventBusContext = createContext(null) MessagesEventBusContext.displayName = 'MessagesEventBusContext' @@ -42,11 +42,11 @@ export function MessagesEventBusProviderInner({ }: { children: React.ReactNode }) { - const agent = useAgent() + const chatClient = useChatClient() const [bus] = useState( () => new MessagesEventBus({ - agent, + chatClient, }), ) diff --git a/src/state/messages/events/types.ts b/src/state/messages/events/types.ts index a85bc2b197..d05c8689db 100644 --- a/src/state/messages/events/types.ts +++ b/src/state/messages/events/types.ts @@ -1,9 +1,9 @@ -import {type ChatBskyConvoGetLog} from '@atproto/api' +import {type Client} from '@atproto/lex-client' -import {type SessionAgent} from '#/state/session' +import {type chat} from '#/lexicons' export type MessagesEventBusParams = { - agent: SessionAgent + chatClient: Client } export enum MessagesEventBusStatus { @@ -66,5 +66,5 @@ export type MessagesEventBusEvent = } | { type: 'logs' - logs: ChatBskyConvoGetLog.OutputSchema['logs'] + logs: chat.bsky.convo.getLog.$OutputBody['logs'] } diff --git a/src/state/preferences/moderation-opts.tsx b/src/state/preferences/moderation-opts.tsx index 119c9008db..cfa2638a5f 100644 --- a/src/state/preferences/moderation-opts.tsx +++ b/src/state/preferences/moderation-opts.tsx @@ -1,5 +1,6 @@ import {createContext, useContext, useMemo} from 'react' -import {AtpAgent, type ModerationOpts} from '@atproto/api' +import {Client} from '@atproto/lex-client' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {useHiddenPosts, useLabelDefinitions} from '#/state/preferences' import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/const' @@ -38,18 +39,30 @@ export function Provider({children}: React.PropsWithChildren<{}>) { return undefined } return { - userDid, + /* + * `did`/`hiddenPosts` come from persisted storage typed as plain + * `string`, so brand them to the SDK's `DidString`/`AtUriString` slots. + */ + userDid: userDid as ModerationOpts['userDid'], prefs: { ...moderationPrefs, labelers: moderationPrefs.labelers.length ? moderationPrefs.labelers - : AtpAgent.appLabelers.map(did => ({ + : Client.appLabelers.map(did => ({ did, labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES, })), - hiddenPosts: hiddenPosts || [], + hiddenPosts: (hiddenPosts || + []) as ModerationOpts['prefs']['hiddenPosts'], }, - labelDefs, + /* + * TODO(phase4): drop this cast once `#/state/preferences/label-defs` + * flips its `InterpretedLabelValueDefinition` source from `@atproto/api` + * to `@bsky.app/sdk/moderation`. The value is already produced by the + * SDK's `interpretLabelValueDefinitions` (see `../queries/preferences`); + * only the intermediate context type is still old-world. + */ + labelDefs: labelDefs, } }, [override, userDid, labelDefs, moderationPrefs, hiddenPosts]) diff --git a/src/state/queries/activity-subscriptions.ts b/src/state/queries/activity-subscriptions.ts index 30964fe7d6..199b065d0e 100644 --- a/src/state/queries/activity-subscriptions.ts +++ b/src/state/queries/activity-subscriptions.ts @@ -1,8 +1,4 @@ -import { - type AppBskyActorDefs, - type AppBskyNotificationDeclaration, - type AppBskyNotificationListActivitySubscriptions, -} from '@atproto/api' +import {type AtIdentifierString} from '@atproto/syntax' import {t} from '@lingui/core/macro' import { type InfiniteData, @@ -13,23 +9,25 @@ import { useQueryClient, } from '@tanstack/react-query' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import * as Toast from '#/components/Toast' +import {app} from '#/lexicons' export const RQKEY_getActivitySubscriptions = ['activity-subscriptions'] export const RQKEY_getNotificationDeclaration = ['notification-declaration'] export function useActivitySubscriptionsQuery() { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery({ queryKey: RQKEY_getActivitySubscriptions, queryFn: async ({pageParam}) => { - const response = - await agent.app.bsky.notification.listActivitySubscriptions({ + return await client.call( + app.bsky.notification.listActivitySubscriptions, + { cursor: pageParam, - }) - return response.data + }, + ) }, initialPageParam: undefined as string | undefined, getNextPageParam: prev => prev.cursor, @@ -37,14 +35,14 @@ export function useActivitySubscriptionsQuery() { } export function useNotificationDeclarationQuery() { - const agent = useAgent() + const client = usePdsClient() const {currentAccount} = useSession() return useQuery({ queryKey: RQKEY_getNotificationDeclaration, queryFn: async () => { try { - const response = await agent.app.bsky.notification.declaration.get({ - repo: currentAccount!.did, + const response = await client.get(app.bsky.notification.declaration, { + repo: currentAccount!.did as AtIdentifierString, rkey: 'self', }) return response @@ -57,7 +55,7 @@ export function useNotificationDeclarationQuery() { value: { $type: 'app.bsky.notification.declaration', allowSubscriptions: 'followers', - } satisfies AppBskyNotificationDeclaration.Record, + } satisfies app.bsky.notification.declaration.Main, } } else { throw err @@ -68,17 +66,18 @@ export function useNotificationDeclarationQuery() { } export function useNotificationDeclarationMutation() { - const agent = useAgent() + const client = usePdsClient() const {currentAccount} = useSession() const queryClient = useQueryClient() return useMutation({ - mutationFn: async (record: AppBskyNotificationDeclaration.Record) => { - const response = await agent.app.bsky.notification.declaration.put( + mutationFn: async (record: app.bsky.notification.declaration.Main) => { + const response = await client.put( + app.bsky.notification.declaration, + record, { - repo: currentAccount!.did, + repo: currentAccount!.did as AtIdentifierString, rkey: 'self', }, - record, ) return response }, @@ -88,7 +87,7 @@ export function useNotificationDeclarationMutation() { (old?: { uri: string cid: string - value: AppBskyNotificationDeclaration.Record + value: app.bsky.notification.declaration.Main }) => { if (!old) return old return { @@ -109,9 +108,9 @@ export function useNotificationDeclarationMutation() { export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: RQKEY_getActivitySubscriptions, }) diff --git a/src/state/queries/actor-autocomplete.ts b/src/state/queries/actor-autocomplete.ts index c8acb39f96..201c599832 100644 --- a/src/state/queries/actor-autocomplete.ts +++ b/src/state/queries/actor-autocomplete.ts @@ -1,15 +1,12 @@ import {useCallback} from 'react' -import { - type AppBskyActorDefs, - moderateProfile, - type ModerationOpts, -} from '@atproto/api' +import {moderateProfile, type ModerationOpts} from '@bsky.app/sdk/moderation' import {keepPreviousData, useQuery, useQueryClient} from '@tanstack/react-query' import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation' import {logger} from '#/logger' import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' import {useModerationOpts} from '../preferences/moderation-opts' import {DEFAULT_LOGGED_OUT_PREFERENCES} from './preferences' @@ -27,7 +24,7 @@ export function useActorAutocompleteQuery( limit?: number, ) { const moderationOpts = useModerationOpts() - const agent = useAgent() + const client = useAppviewClient() prefix = prefix.toLowerCase().trim() if (prefix.endsWith('.')) { @@ -35,20 +32,20 @@ export function useActorAutocompleteQuery( prefix = prefix.slice(0, -1) } - return useQuery({ + return useQuery({ staleTime: STALE.MINUTES.ONE, queryKey: RQKEY(prefix || ''), async queryFn() { const res = prefix - ? await agent.searchActorsTypeahead({ + ? await client.call(app.bsky.actor.searchActorsTypeahead, { q: prefix, limit: limit || 8, }) : undefined - return res?.data.actors || [] + return res?.actors || [] }, select: useCallback( - (data: AppBskyActorDefs.ProfileViewBasic[]) => { + (data: app.bsky.actor.defs.ProfileViewBasic[]) => { return computeSuggestions({ q: prefix, searched: data, @@ -65,7 +62,7 @@ export type ActorAutocompleteFn = ReturnType export function useActorAutocompleteFn() { const queryClient = useQueryClient() const moderationOpts = useModerationOpts() - const agent = useAgent() + const client = useAppviewClient() return useCallback( async ({query, limit = 8}: {query: string; limit?: number}) => { @@ -77,7 +74,7 @@ export function useActorAutocompleteFn() { staleTime: STALE.MINUTES.ONE, queryKey: RQKEY(query || ''), queryFn: () => - agent.searchActorsTypeahead({ + client.call(app.bsky.actor.searchActorsTypeahead, { q: query, limit, }), @@ -91,11 +88,11 @@ export function useActorAutocompleteFn() { return computeSuggestions({ q: query, - searched: res?.data.actors, + searched: res?.actors, moderationOpts: moderationOpts || DEFAULT_MOD_OPTS, }) }, - [queryClient, moderationOpts, agent], + [queryClient, moderationOpts, client], ) } @@ -105,10 +102,10 @@ function computeSuggestions({ moderationOpts, }: { q?: string - searched?: AppBskyActorDefs.ProfileViewBasic[] + searched?: app.bsky.actor.defs.ProfileViewBasic[] moderationOpts: ModerationOpts }) { - let items: AppBskyActorDefs.ProfileViewBasic[] = [] + let items: app.bsky.actor.defs.ProfileViewBasic[] = [] for (const item of searched) { if (!items.find(item2 => item2.handle === item.handle)) { items.push(item) diff --git a/src/state/queries/actor-search.ts b/src/state/queries/actor-search.ts index 84e6d88f29..547bf39b0f 100644 --- a/src/state/queries/actor-search.ts +++ b/src/state/queries/actor-search.ts @@ -1,4 +1,3 @@ -import {type AppBskyActorSearchActors} from '@atproto/api' import { type InfiniteData, keepPreviousData, @@ -8,7 +7,8 @@ import { } from '@tanstack/react-query' import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' export const RQKEY_ROOT = 'actor-search' export const RQKEY = (query: string, limit?: number) => [ @@ -28,23 +28,22 @@ export function useActorSearch({ maintainData?: boolean limit?: number }) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyActorSearchActors.OutputSchema, + app.bsky.actor.searchActors.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, string | undefined >({ staleTime: STALE.MINUTES.FIVE, queryKey: RQKEY(query, limit), queryFn: async ({pageParam}) => { - const res = await agent.searchActors({ + return await client.call(app.bsky.actor.searchActors, { q: query, limit, cursor: pageParam, }) - return res.data }, enabled: enabled && !!query, initialPageParam: undefined, @@ -54,7 +53,7 @@ export function useActorSearch({ }) } -function select(data: InfiniteData) { +function select(data: InfiniteData) { // enforce uniqueness const dids = new Set() @@ -77,7 +76,7 @@ export function* findAllProfilesInQueryData( did: string, ) { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/app-passwords.ts b/src/state/queries/app-passwords.ts index 12d66dd2cd..c1326d0a00 100644 --- a/src/state/queries/app-passwords.ts +++ b/src/state/queries/app-passwords.ts @@ -1,39 +1,37 @@ -import {type ComAtprotoServerCreateAppPassword} from '@atproto/api' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {STALE} from '#/state/queries' -import {useAgent} from '../session' +import {com} from '#/lexicons' +import {usePdsClient} from '../session' const RQKEY_ROOT = 'app-passwords' export const RQKEY = () => [RQKEY_ROOT] export function useAppPasswordsQuery() { - const agent = useAgent() + const pdsClient = usePdsClient() return useQuery({ staleTime: STALE.MINUTES.FIVE, queryKey: RQKEY(), queryFn: async () => { - const res = await agent.com.atproto.server.listAppPasswords({}) - return res.data.passwords + const data = await pdsClient.call(com.atproto.server.listAppPasswords, {}) + return data.passwords }, }) } export function useAppPasswordCreateMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation< - ComAtprotoServerCreateAppPassword.OutputSchema, + com.atproto.server.createAppPassword.$OutputBody, Error, {name: string; privileged: boolean} >({ mutationFn: async ({name, privileged}) => { - return ( - await agent.com.atproto.server.createAppPassword({ - name, - privileged, - }) - ).data + return await pdsClient.call(com.atproto.server.createAppPassword, { + name, + privileged, + }) }, onSuccess() { queryClient.invalidateQueries({ @@ -45,10 +43,10 @@ export function useAppPasswordCreateMutation() { export function useAppPasswordDeleteMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async ({name}) => { - await agent.com.atproto.server.revokeAppPassword({ + await pdsClient.call(com.atproto.server.revokeAppPassword, { name, }) }, diff --git a/src/state/queries/bookmarks/useBookmarkMutation.ts b/src/state/queries/bookmarks/useBookmarkMutation.ts index c6e745aa04..4ba0d1d7f5 100644 --- a/src/state/queries/bookmarks/useBookmarkMutation.ts +++ b/src/state/queries/bookmarks/useBookmarkMutation.ts @@ -1,4 +1,4 @@ -import {type AppBskyFeedDefs} from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import {useMutation, useQueryClient} from '@tanstack/react-query' import {isNetworkError} from '#/lib/strings/errors' @@ -8,10 +8,11 @@ import { optimisticallyDeleteBookmark, optimisticallySaveBookmark, } from '#/state/queries/bookmarks/useBookmarksQuery' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' type MutationArgs = - | {action: 'create'; post: AppBskyFeedDefs.PostView} + | {action: 'create'; post: app.bsky.feed.defs.PostView} | { action: 'delete' /** @@ -23,20 +24,20 @@ type MutationArgs = export function useBookmarkMutation() { const qc = useQueryClient() - const agent = useAgent() + const client = useAppviewClient() return useMutation({ async mutationFn(args: MutationArgs) { if (args.action === 'create') { updatePostShadow(qc, args.post.uri, {bookmarked: true}) - await agent.app.bsky.bookmark.createBookmark({ + await client.call(app.bsky.bookmark.createBookmark, { uri: args.post.uri, cid: args.post.cid, }) } else if (args.action === 'delete') { updatePostShadow(qc, args.uri, {bookmarked: false}) - await agent.app.bsky.bookmark.deleteBookmark({ - uri: args.uri, + await client.call(app.bsky.bookmark.deleteBookmark, { + uri: args.uri as AtUriString, }) } }, diff --git a/src/state/queries/bookmarks/useBookmarksQuery.ts b/src/state/queries/bookmarks/useBookmarksQuery.ts index 3e8e87a132..ee2abb80a5 100644 --- a/src/state/queries/bookmarks/useBookmarksQuery.ts +++ b/src/state/queries/bookmarks/useBookmarksQuery.ts @@ -1,9 +1,5 @@ -import { - type $Typed, - type AppBskyBookmarkGetBookmarks, - AppBskyFeedDefs, - AtUri, -} from '@atproto/api' +import {type l} from '@atproto/lex' +import {AtUri} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -16,28 +12,28 @@ import { embedViewRecordToPostView, getEmbeddedPost, } from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' export const bookmarksQueryKeyRoot = 'bookmarks' export const createBookmarksQueryKey = () => [bookmarksQueryKeyRoot] export function useBookmarksQuery() { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyBookmarkGetBookmarks.OutputSchema, + app.bsky.bookmark.getBookmarks.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, string | undefined >({ queryKey: createBookmarksQueryKey(), async queryFn({pageParam}) { - const res = await agent.app.bsky.bookmark.getBookmarks({ + return await client.call(app.bsky.bookmark.getBookmarks, { cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -45,7 +41,7 @@ export function useBookmarksQuery() { } export async function truncateAndInvalidate(qc: QueryClient) { - qc.setQueriesData>( + qc.setQueriesData>( {queryKey: [bookmarksQueryKeyRoot]}, data => { if (data) { @@ -62,9 +58,9 @@ export async function truncateAndInvalidate(qc: QueryClient) { export async function optimisticallySaveBookmark( qc: QueryClient, - post: AppBskyFeedDefs.PostView, + post: app.bsky.feed.defs.PostView, ) { - qc.setQueriesData>( + qc.setQueriesData>( { queryKey: [bookmarksQueryKeyRoot], }, @@ -75,19 +71,17 @@ export async function optimisticallySaveBookmark( pages: data.pages.map((page, index) => { if (index === 0) { post.$type = 'app.bsky.feed.defs#postView' + const bookmark: app.bsky.bookmark.defs.BookmarkView = { + createdAt: new Date().toISOString() as l.DatetimeString, + subject: { + uri: post.uri, + cid: post.cid, + }, + item: post as l.$Typed, + } return { ...page, - bookmarks: [ - { - createdAt: new Date().toISOString(), - subject: { - uri: post.uri, - cid: post.cid, - }, - item: post as $Typed, - }, - ...page.bookmarks, - ], + bookmarks: [bookmark, ...page.bookmarks], } } return page @@ -101,7 +95,7 @@ export async function optimisticallyDeleteBookmark( qc: QueryClient, {uri}: {uri: string}, ) { - qc.setQueriesData>( + qc.setQueriesData>( { queryKey: [bookmarksQueryKeyRoot], }, @@ -123,9 +117,9 @@ export async function optimisticallyDeleteBookmark( export function* findAllPostsInQueryData( queryClient: QueryClient, uri: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [bookmarksQueryKeyRoot], }) @@ -137,13 +131,7 @@ export function* findAllPostsInQueryData( } for (const page of queryData?.pages) { for (const bookmark of page.bookmarks) { - if ( - !bsky.dangerousIsType( - bookmark.item, - AppBskyFeedDefs.isPostView, - ) - ) - continue + if (!bsky.isType(app.bsky.feed.defs.postView, bookmark.item)) continue if (didOrHandleUriMatches(atUri, bookmark.item)) { yield bookmark.item diff --git a/src/state/queries/explore-feed-previews.tsx b/src/state/queries/explore-feed-previews.tsx index acbc4ee7e1..cb604953ab 100644 --- a/src/state/queries/explore-feed-previews.tsx +++ b/src/state/queries/explore-feed-previews.tsx @@ -1,10 +1,6 @@ import {useMemo, useRef} from 'react' -import { - type AppBskyActorDefs, - AppBskyFeedDefs, - AtUri, - moderatePost, -} from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {moderatePost} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import { @@ -28,7 +24,9 @@ import { embedViewRecordToPostView, getEmbeddedPost, } from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' const RQKEY_ROOT = 'feed-previews' const RQKEY = (feeds: string[]) => [RQKEY_ROOT, feeds] @@ -85,7 +83,7 @@ export type FeedPreviewItem = | { type: 'preview:header' key: string - feed: AppBskyFeedDefs.GeneratorView + feed: app.bsky.feed.defs.GeneratorView } | { type: 'preview:footer' @@ -97,7 +95,7 @@ export type FeedPreviewItem = key: string slice: FeedPostSlice indexInSlice: number - feed: AppBskyFeedDefs.GeneratorView + feed: app.bsky.feed.defs.GeneratorView showReplyTo: boolean hideTopBorder: boolean } @@ -108,7 +106,7 @@ export type FeedPreviewItem = } export function useFeedPreviews( - feedsMaybeWithDuplicates: AppBskyFeedDefs.GeneratorView[], + feedsMaybeWithDuplicates: app.bsky.feed.defs.GeneratorView[], isEnabled: boolean = true, ) { const feeds = useMemo( @@ -121,7 +119,7 @@ export function useFeedPreviews( const uris = feeds.map(feed => feed.uri) const {_} = useLingui() - const agent = useAgent() + const client = useAppviewClient() const {data: preferences} = usePreferencesQuery() const userInterests = aggregateUserInterests(preferences) const moderationOpts = useModerationOpts() @@ -130,8 +128,8 @@ export function useFeedPreviews( const processedPageCache = useRef( new Map< { - feed: AppBskyFeedDefs.GeneratorView - posts: AppBskyFeedDefs.FeedViewPost[] + feed: app.bsky.feed.defs.GeneratorView + posts: app.bsky.feed.defs.FeedViewPost[] }, FeedPreviewItem[] >(), @@ -143,7 +141,7 @@ export function useFeedPreviews( queryFn: async ({pageParam}) => { const feed = feeds[pageParam] const api = new CustomFeedAPI({ - agent, + client, feedParams: {feed: feed.uri}, userInterests, }) @@ -207,7 +205,12 @@ export function useFeedPreviews( if (item.isFallbackMarker) continue const moderations = item.items.map(item => - moderatePost(item.post, moderationOpts!), + // TODO(phase4): drop toLex once feed-manip is migrated off + // @atproto/api and yields lex-typed slice items. + moderatePost( + bsky.toLex(item.post), + moderationOpts!, + ), ) // apply moderation filters @@ -233,10 +236,20 @@ export function useFeedPreviews( const feedPostSliceItem: FeedPostSliceItem = { _reactKey: `${item._reactKey}-${i}-${subItem.post.uri}`, uri: subItem.post.uri, - post: subItem.post, - record: subItem.record, + // TODO(phase4): drop toLex once feed-manip is migrated + // off @atproto/api and yields lex-typed slice items. + post: bsky.toLex( + subItem.post, + ), + record: bsky.toLex( + subItem.record, + ), moderation: moderations[i], - parentAuthor: subItem.parentAuthor, + parentAuthor: subItem.parentAuthor + ? bsky.toLex( + subItem.parentAuthor, + ) + : undefined, isParentBlocked: subItem.isParentBlocked, isParentNotFound: subItem.isParentNotFound, } @@ -349,13 +362,13 @@ export function useFeedPreviews( export function* findAllPostsInQueryData( queryClient: QueryClient, uri: string, -): Generator { +): Generator { const atUri = new AtUri(uri) const queryDatas = queryClient.getQueriesData< InfiniteData<{ - feed: AppBskyFeedDefs.GeneratorView - posts: AppBskyFeedDefs.FeedViewPost[] + feed: app.bsky.feed.defs.GeneratorView + posts: app.bsky.feed.defs.FeedViewPost[] }> >({ queryKey: [RQKEY_ROOT], @@ -375,7 +388,7 @@ export function* findAllPostsInQueryData( yield embedViewRecordToPostView(quotedPost) } - if (AppBskyFeedDefs.isPostView(item.reply?.parent)) { + if (bsky.isType(app.bsky.feed.defs.postView, item.reply?.parent)) { if (didOrHandleUriMatches(atUri, item.reply.parent)) { yield item.reply.parent } @@ -389,7 +402,7 @@ export function* findAllPostsInQueryData( } } - if (AppBskyFeedDefs.isPostView(item.reply?.root)) { + if (bsky.isType(app.bsky.feed.defs.postView, item.reply?.root)) { if (didOrHandleUriMatches(atUri, item.reply.root)) { yield item.reply.root } @@ -407,11 +420,11 @@ export function* findAllPostsInQueryData( export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< InfiniteData<{ - feed: AppBskyFeedDefs.GeneratorView - posts: AppBskyFeedDefs.FeedViewPost[] + feed: app.bsky.feed.defs.GeneratorView + posts: app.bsky.feed.defs.FeedViewPost[] }> >({ queryKey: [RQKEY_ROOT], @@ -430,13 +443,13 @@ export function* findAllProfilesInQueryData( yield quotedPost.author } if ( - AppBskyFeedDefs.isPostView(item.reply?.parent) && + bsky.isType(app.bsky.feed.defs.postView, item.reply?.parent) && item.reply?.parent?.author.did === did ) { yield item.reply.parent.author } if ( - AppBskyFeedDefs.isPostView(item.reply?.root) && + bsky.isType(app.bsky.feed.defs.postView, item.reply?.root) && item.reply?.root?.author.did === did ) { yield item.reply.root.author diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index c3ac1acd53..840807bf03 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -1,13 +1,7 @@ import {useCallback, useEffect, useMemo, useRef} from 'react' -import { - type AppBskyActorDefs, - type AppBskyFeedDefs, - type AppBskyGraphDefs, - type AppBskyUnspeccedGetPopularFeedGenerators, - AtUri, - moderateFeedGenerator, - RichText, -} from '@atproto/api' +import {AtUri, type AtUriString} from '@atproto/syntax' +import {moderateFeedGenerator} from '@bsky.app/sdk/moderation' +import {RichText} from '@bsky.app/sdk/richtext' import {t} from '@lingui/core/macro' import { type InfiniteData, @@ -26,7 +20,8 @@ import {GCTIME, STALE} from '#/state/queries' import {RQKEY as listQueryKey} from '#/state/queries/list' import {usePreferencesQuery} from '#/state/queries/preferences' import {createQueryKey} from '#/state/queries/util' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' +import {app} from '#/lexicons' import {router} from '#/routes' import {useModerationOpts} from '../preferences/moderation-opts' import {type FeedDescriptor} from './post-feed' @@ -34,7 +29,7 @@ import {precacheResolvedUri} from './resolve-uri' export type FeedSourceFeedInfo = { type: 'feed' - view?: AppBskyFeedDefs.GeneratorView + view?: app.bsky.feed.defs.GeneratorView uri: string feedDescriptor: FeedDescriptor route: { @@ -51,12 +46,12 @@ export type FeedSourceFeedInfo = { likeCount: number | undefined acceptsInteractions?: boolean likeUri: string | undefined - contentMode: AppBskyFeedDefs.GeneratorView['contentMode'] + contentMode: app.bsky.feed.defs.GeneratorView['contentMode'] } export type FeedSourceListInfo = { type: 'list' - view?: AppBskyGraphDefs.ListView + view?: app.bsky.graph.defs.ListView uri: string feedDescriptor: FeedDescriptor route: { @@ -93,7 +88,7 @@ const feedSourceNSIDs = { } export function hydrateFeedGenerator( - view: AppBskyFeedDefs.GeneratorView, + view: app.bsky.feed.defs.GeneratorView, ): FeedSourceInfo { const urip = new AtUri(view.uri) const collection = @@ -135,7 +130,9 @@ export function hydrateFeedGenerator( } } -export function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo { +export function hydrateList( + view: app.bsky.graph.defs.ListView, +): FeedSourceInfo { const urip = new AtUri(view.uri) const collection = urip.collection === 'app.bsky.feed.generator' ? 'feed' : 'lists' @@ -184,7 +181,7 @@ export function getAvatarTypeFromUri(uri: string) { export function useFeedSourceInfoQuery({uri}: {uri: string}) { const type = getFeedTypeFromUri(uri) - const agent = useAgent() + const client = useAppviewClient() return useQuery({ staleTime: STALE.INFINITY, @@ -193,14 +190,16 @@ export function useFeedSourceInfoQuery({uri}: {uri: string}) { let view: FeedSourceInfo if (type === 'feed') { - const res = await agent.app.bsky.feed.getFeedGenerator({feed: uri}) - view = hydrateFeedGenerator(res.data.view) + const res = await client.call(app.bsky.feed.getFeedGenerator, { + feed: uri as AtUriString, + }) + view = hydrateFeedGenerator(res.view) } else { - const res = await agent.app.bsky.graph.getList({ - list: uri, + const res = await client.call(app.bsky.graph.getList, { + list: uri as AtUriString, limit: 1, }) - view = hydrateList(res.data.list) + view = hydrateList(res.list) } return view @@ -234,7 +233,7 @@ export function createGetPopularFeedsQueryKey( export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { const {hasSession} = useSession() - const agent = useAgent() + const client = useAppviewClient() const limit = options?.limit || 10 const {data: preferences} = usePreferencesQuery() const queryClient = useQueryClient() @@ -255,24 +254,27 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { enabled: Boolean(moderationOpts) && options?.enabled !== false, queryKey: createGetPopularFeedsQueryKey(options), queryFn: async ({pageParam}) => { - const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ - limit, - cursor: pageParam, - }) + const res = await client.call( + app.bsky.unspecced.getPopularFeedGenerators, + { + limit, + cursor: pageParam, + }, + ) // precache feeds - for (const feed of res.data.feeds) { + for (const feed of res.feeds) { const hydratedFeed = hydrateFeedGenerator(feed) precacheFeed(queryClient, hydratedFeed) } - return res.data + return res }, initialPageParam: undefined as string | undefined, getNextPageParam: lastPage => lastPage.cursor, select: useCallback( ( - data: InfiniteData, + data: InfiniteData, ) => { const { savedFeeds, @@ -336,24 +338,27 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { } export function useSearchPopularFeedsMutation() { - const agent = useAgent() + const client = useAppviewClient() const moderationOpts = useModerationOpts() return useMutation({ mutationFn: async (query: string) => { - const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ - limit: 10, - query: query, - }) + const res = await client.call( + app.bsky.unspecced.getPopularFeedGenerators, + { + limit: 10, + query: query, + }, + ) if (moderationOpts) { - return res.data.feeds.filter(feed => { + return res.feeds.filter(feed => { const decision = moderateFeedGenerator(feed, moderationOpts) return !decision.ui('contentMedia').blur }) } - return res.data.feeds + return res.feeds }, }) } @@ -371,7 +376,7 @@ export function usePopularFeedsSearch({ query: string enabled?: boolean }) { - const agent = useAgent() + const client = useAppviewClient() const moderationOpts = useModerationOpts() const enabledInner = enabled ?? Boolean(moderationOpts) @@ -379,12 +384,15 @@ export function usePopularFeedsSearch({ enabled: enabledInner, queryKey: createPopularFeedsSearchQueryKey(query), queryFn: async () => { - const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ - limit: 15, - query: query, - }) + const res = await client.call( + app.bsky.unspecced.getPopularFeedGenerators, + { + limit: 15, + query: query, + }, + ) - return res.data.feeds + return res.feeds }, placeholderData: keepPreviousData, select(data) { @@ -397,7 +405,7 @@ export function usePopularFeedsSearch({ } export type SavedFeedSourceInfo = FeedSourceInfo & { - savedFeed: AppBskyActorDefs.SavedFeed + savedFeed: app.bsky.actor.defs.SavedFeed } const PWI_DISCOVER_FEED_STUB: SavedFeedSourceInfo = { @@ -444,7 +452,7 @@ const createPinnedFeedInfosQueryKey = ( export function usePinnedFeedsInfos() { const {hasSession} = useSession() - const agent = useAgent() + const client = useAppviewClient() const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery() const pinnedItems = preferences?.savedFeeds.filter(feed => feed.pinned) ?? [] @@ -467,13 +475,13 @@ export function usePinnedFeedsInfos() { const pinnedFeeds = pinnedItems.filter(feed => feed.type === 'feed') let feedsPromise = Promise.resolve() if (pinnedFeeds.length > 0) { - feedsPromise = agent.app.bsky.feed - .getFeedGenerators({ - feeds: pinnedFeeds.map(f => f.value), + feedsPromise = client + .call(app.bsky.feed.getFeedGenerators, { + feeds: pinnedFeeds.map(f => f.value as AtUriString), }) .then(res => { - for (let i = 0; i < res.data.feeds.length; i++) { - const feedView = res.data.feeds[i] + for (let i = 0; i < res.feeds.length; i++) { + const feedView = res.feeds[i] resolved.set(feedView.uri, hydrateFeedGenerator(feedView)) } }) @@ -482,13 +490,13 @@ export function usePinnedFeedsInfos() { // Get all lists. This currently has to be done individually. const pinnedLists = pinnedItems.filter(feed => feed.type === 'list') const listsPromises = pinnedLists.map(list => - agent.app.bsky.graph - .getList({ - list: list.value, + client + .call(app.bsky.graph.getList, { + list: list.value as AtUriString, limit: 1, }) .then(res => { - const listView = res.data.list + const listView = res.list resolved.set(listView.uri, hydrateList(listView)) }), ) @@ -536,22 +544,22 @@ export function usePinnedFeedsInfos() { export type SavedFeedItem = | { type: 'feed' - config: AppBskyActorDefs.SavedFeed - view: AppBskyFeedDefs.GeneratorView + config: app.bsky.actor.defs.SavedFeed + view: app.bsky.feed.defs.GeneratorView } | { type: 'list' - config: AppBskyActorDefs.SavedFeed - view: AppBskyGraphDefs.ListView + config: app.bsky.actor.defs.SavedFeed + view: app.bsky.graph.defs.ListView } | { type: 'timeline' - config: AppBskyActorDefs.SavedFeed + config: app.bsky.actor.defs.SavedFeed view: undefined } export function useSavedFeeds() { - const agent = useAgent() + const client = useAppviewClient() const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery() const savedItems = preferences?.savedFeeds ?? [] const queryClient = useQueryClient() @@ -574,33 +582,33 @@ export function useSavedFeeds() { ) }, queryFn: async () => { - const resolvedFeeds = new Map() - const resolvedLists = new Map() + const resolvedFeeds = new Map() + const resolvedLists = new Map() const savedFeeds = savedItems.filter(feed => feed.type === 'feed') const savedLists = savedItems.filter(feed => feed.type === 'list') let feedsPromise = Promise.resolve() if (savedFeeds.length > 0) { - feedsPromise = agent.app.bsky.feed - .getFeedGenerators({ - feeds: savedFeeds.map(f => f.value), + feedsPromise = client + .call(app.bsky.feed.getFeedGenerators, { + feeds: savedFeeds.map(f => f.value as AtUriString), }) .then(res => { - res.data.feeds.forEach(f => { + res.feeds.forEach(f => { resolvedFeeds.set(f.uri, f) }) }) } const listsPromises = savedLists.map(list => - agent.app.bsky.graph - .getList({ - list: list.value, + client + .call(app.bsky.graph.getList, { + list: list.value as AtUriString, limit: 1, }) .then(res => { - const listView = res.data.list + const listView = res.list resolvedLists.set(listView.uri, listView) }), ) @@ -656,7 +664,7 @@ export function useSavedFeeds() { const feedInfoQueryKeyRoot = 'feedInfo' export function useFeedInfo(feedUri: string | undefined) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ staleTime: STALE.INFINITY, @@ -666,11 +674,11 @@ export function useFeedInfo(feedUri: string | undefined) { return null } - const res = await agent.app.bsky.feed.getFeedGenerator({ - feed: feedUri, + const res = await client.call(app.bsky.feed.getFeedGenerator, { + feed: feedUri as AtUriString, }) - const feedSourceInfo = hydrateFeedGenerator(res.data.view) + const feedSourceInfo = hydrateFeedGenerator(res.view) return feedSourceInfo }, }) @@ -690,10 +698,10 @@ function precacheFeed(queryClient: QueryClient, hydratedFeed: FeedSourceInfo) { export function precacheList( queryClient: QueryClient, - list: AppBskyGraphDefs.ListView, + list: app.bsky.graph.defs.ListView, ) { precacheResolvedUri(queryClient, list.creator.handle, list.creator.did) - queryClient.setQueryData( + queryClient.setQueryData( listQueryKey(list.uri), list, ) @@ -701,7 +709,7 @@ export function precacheList( export function precacheFeedFromGeneratorView( queryClient: QueryClient, - view: AppBskyFeedDefs.GeneratorView, + view: app.bsky.feed.defs.GeneratorView, ) { const hydratedFeed = hydrateFeedGenerator(view) precacheFeed(queryClient, hydratedFeed) diff --git a/src/state/queries/find-contacts.ts b/src/state/queries/find-contacts.ts index b1eb6c9c5e..e462e6a6c1 100644 --- a/src/state/queries/find-contacts.ts +++ b/src/state/queries/find-contacts.ts @@ -1,4 +1,3 @@ -import {type AppBskyContactGetMatches} from '@atproto/api' import { type InfiniteData, type QueryClient, @@ -6,8 +5,9 @@ import { useQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {type Match} from '#/components/contacts/state' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' import {STALE} from '.' @@ -15,13 +15,12 @@ const RQ_KEY_ROOT = 'find-contacts' export const findContactsStatusQueryKey = [RQ_KEY_ROOT, 'sync-status'] export function useContactsSyncStatusQuery() { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ queryKey: findContactsStatusQueryKey, queryFn: async () => { - const status = await agent.app.bsky.contact.getSyncStatus() - return status.data + return await client.call(app.bsky.contact.getSyncStatus, {}) }, staleTime: STALE.SECONDS.THIRTY, }) @@ -30,15 +29,14 @@ export function useContactsSyncStatusQuery() { export const findContactsGetMatchesQueryKey = [RQ_KEY_ROOT, 'matches'] export function useContactsMatchesQuery() { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery({ queryKey: findContactsGetMatchesQueryKey, queryFn: async ({pageParam}) => { - const matches = await agent.app.bsky.contact.getMatches({ + return await client.call(app.bsky.contact.getMatches, { cursor: pageParam, }) - return matches.data }, initialPageParam: undefined as string | undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -47,20 +45,19 @@ export function useContactsMatchesQuery() { } export function optimisticRemoveMatch(queryClient: QueryClient, did: string) { - queryClient.setQueryData>( - findContactsGetMatchesQueryKey, - old => { - if (!old) return old + queryClient.setQueryData< + InfiniteData + >(findContactsGetMatchesQueryKey, old => { + if (!old) return old - return { - ...old, - pages: old.pages.map(page => ({ - ...page, - matches: page.matches.filter(match => match.did !== did), - })), - } - }, - ) + return { + ...old, + pages: old.pages.map(page => ({ + ...page, + matches: page.matches.filter(match => match.did !== did), + })), + } + }) } export const findContactsMatchesPassthroughQueryKey = (dids: string[]) => [ @@ -95,7 +92,7 @@ export function* findAllProfilesInQueryData( did: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: findContactsGetMatchesQueryKey, }) diff --git a/src/state/queries/handle-availability.ts b/src/state/queries/handle-availability.ts index fb25697c76..1bbbfceab9 100644 --- a/src/state/queries/handle-availability.ts +++ b/src/state/queries/handle-availability.ts @@ -1,4 +1,3 @@ -import {ComAtprotoTempCheckHandleAvailability} from '@atproto/api' import {useQuery} from '@tanstack/react-query' import { @@ -9,9 +8,21 @@ import { import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue' import {createFullHandle} from '#/lib/strings/handles' import {useAnalytics} from '#/analytics' -import * as bsky from '#/types/bsky' import {Agent} from '../session/agent' +/* + * `com.atproto.temp.checkHandleAvailability` is an entryway-only endpoint that + * isn't generated into `#/lexicons`, so we describe its result union locally. + * The response is discriminated by `$type`; we narrow against these shapes + * rather than a branded lexicon guard. + */ +type CheckHandleAvailabilityResult = + | {$type: 'com.atproto.temp.checkHandleAvailability#resultAvailable'} + | { + $type: 'com.atproto.temp.checkHandleAvailability#resultUnavailable' + suggestions: {handle: string; method: string}[] + } + export const RQKEY_handleAvailability = ( handle: string, domain: string, @@ -87,22 +98,20 @@ export async function checkHandleAvailability( email, }) + const result = data.result as CheckHandleAvailabilityResult + if ( - bsky.dangerousIsType( - data.result, - ComAtprotoTempCheckHandleAvailability.isResultAvailable, - ) + result.$type === + 'com.atproto.temp.checkHandleAvailability#resultAvailable' ) { return {available: true} as const } else if ( - bsky.dangerousIsType( - data.result, - ComAtprotoTempCheckHandleAvailability.isResultUnavailable, - ) + result.$type === + 'com.atproto.temp.checkHandleAvailability#resultUnavailable' ) { return { available: false, - suggestions: data.result.suggestions, + suggestions: result.suggestions, } as const } else { throw new Error( diff --git a/src/state/queries/known-followers.ts b/src/state/queries/known-followers.ts index 05bbd5e67c..4973512ec4 100644 --- a/src/state/queries/known-followers.ts +++ b/src/state/queries/known-followers.ts @@ -1,7 +1,4 @@ -import { - type AppBskyActorDefs, - type AppBskyGraphGetKnownFollowers, -} from '@atproto/api' +import {type AtIdentifierString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -9,7 +6,8 @@ import { useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const PAGE_SIZE = 50 type RQPageParam = string | undefined @@ -18,22 +16,21 @@ const RQKEY_ROOT = 'profile-known-followers' export const RQKEY = (did: string) => [RQKEY_ROOT, did] export function useProfileKnownFollowersQuery(did: string | undefined) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyGraphGetKnownFollowers.OutputSchema, + app.bsky.graph.getKnownFollowers.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(did || ''), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.app.bsky.graph.getKnownFollowers({ - actor: did!, + return await client.call(app.bsky.graph.getKnownFollowers, { + actor: did! as AtIdentifierString, limit: PAGE_SIZE, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -44,9 +41,9 @@ export function useProfileKnownFollowersQuery(did: string | undefined) { export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/labeler.ts b/src/state/queries/labeler.ts index e7251866fe..3fd14cc5fc 100644 --- a/src/state/queries/labeler.ts +++ b/src/state/queries/labeler.ts @@ -1,4 +1,5 @@ -import {type AppBskyLabelerDefs} from '@atproto/api' +import {type DidString} from '@atproto/syntax' +import {addLabeler, removeLabeler} from '@bsky.app/sdk' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {z} from 'zod' @@ -9,7 +10,8 @@ import { usePreferencesQuery, } from '#/state/queries/preferences' import {createQueryKey} from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useAppviewClient, usePdsClient} from '#/state/session' +import {app} from '#/lexicons' const labelerInfoQueryKeyRoot = 'labeler-info' export const labelerInfoQueryKey = (did: string) => [ @@ -33,56 +35,60 @@ export function useLabelerInfoQuery({ did?: string enabled?: boolean }) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ enabled: !!did && enabled !== false, queryKey: labelerInfoQueryKey(did as string), queryFn: async () => { - const res = await agent.app.bsky.labeler.getServices({ - dids: [did!], + const res = await client.call(app.bsky.labeler.getServices, { + dids: [did! as DidString], detailed: true, }) - return res.data.views[0] as AppBskyLabelerDefs.LabelerViewDetailed + return res.views[0] as app.bsky.labeler.defs.LabelerViewDetailed }, }) } export function useLabelersInfoQuery({dids}: {dids: string[]}) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ enabled: !!dids.length, queryKey: labelersInfoQueryKey(dids), queryFn: async () => { - const res = await agent.app.bsky.labeler.getServices({dids}) - return res.data.views as AppBskyLabelerDefs.LabelerView[] + const res = await client.call(app.bsky.labeler.getServices, { + dids: dids as DidString[], + }) + return res.views as app.bsky.labeler.defs.LabelerView[] }, }) } export function useLabelersDetailedInfoQuery({dids}: {dids: string[]}) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ enabled: !!dids.length, queryKey: createLabelersDetailedInfoQueryKey(dids), gcTime: GCTIME.INFINITY, staleTime: STALE.MINUTES.ONE, queryFn: async () => { - const res = await agent.app.bsky.labeler.getServices({ - dids, + const res = await client.call(app.bsky.labeler.getServices, { + dids: dids as DidString[], detailed: true, }) - return res.data.views as AppBskyLabelerDefs.LabelerViewDetailed[] + return res.views as app.bsky.labeler.defs.LabelerViewDetailed[] }, }) } export function useRemoveLabelersMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation({ async mutationFn({dids}: {dids: string[]}) { - await Promise.all(dids.map(did => agent.removeLabeler(did))) + await Promise.all( + dids.map(did => client.call(removeLabeler, did as DidString)), + ) }, async onSuccess() { await queryClient.invalidateQueries({ @@ -94,7 +100,8 @@ export function useRemoveLabelersMutation() { export function useLabelerSubscriptionMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const preferences = usePreferencesQuery() return useMutation({ @@ -117,26 +124,32 @@ export function useLabelerSubscriptionMutation() { const labelerDids = ( preferences.data?.moderationPrefs?.labelers ?? [] ).map(l => l.did) - const invalidLabelers: string[] = [] + const invalidLabelers: DidString[] = [] if (labelerDids.length) { - const profiles = await agent.getProfiles({actors: labelerDids}) - if (profiles.data) { - for (const did of labelerDids) { - const exists = profiles.data.profiles.find(p => p.did === did) + const profiles = await appviewClient.call(app.bsky.actor.getProfiles, { + actors: labelerDids, + }) + if (profiles) { + for (const labelerDid of labelerDids) { + const exists = profiles.profiles.find(p => p.did === labelerDid) if (exists) { // profile came back but it's not a valid labeler if (exists.associated && !exists.associated.labeler) { - invalidLabelers.push(did) + invalidLabelers.push(labelerDid) } } else { // no response came back, might be deactivated or takendown - invalidLabelers.push(did) + invalidLabelers.push(labelerDid) } } } } if (invalidLabelers.length) { - await Promise.all(invalidLabelers.map(did => agent.removeLabeler(did))) + await Promise.all( + invalidLabelers.map(labelerDid => + pdsClient.call(removeLabeler, labelerDid), + ), + ) } if (subscribe) { @@ -144,9 +157,9 @@ export function useLabelerSubscriptionMutation() { if (labelerCount >= MAX_LABELERS) { throw new Error('MAX_LABELERS') } - await agent.addLabeler(did) + await pdsClient.call(addLabeler, did as DidString) } else { - await agent.removeLabeler(did) + await pdsClient.call(removeLabeler, did as DidString) } }, async onSuccess() { diff --git a/src/state/queries/list-members.ts b/src/state/queries/list-members.ts index 96d732b22b..405f7dde07 100644 --- a/src/state/queries/list-members.ts +++ b/src/state/queries/list-members.ts @@ -1,8 +1,5 @@ -import { - type AppBskyActorDefs, - type AppBskyGraphDefs, - type AppBskyGraphGetList, -} from '@atproto/api' +import {type Client} from '@atproto/lex-client' +import {type AtUriString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -12,7 +9,8 @@ import { } from '@tanstack/react-query' import {STALE} from '#/state/queries' -import {type SessionAgent, useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const PAGE_SIZE = 30 type RQPageParam = string | undefined @@ -23,23 +21,22 @@ export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] export const RQKEY_ALL = (uri: string) => [RQKEY_ROOT_ALL, uri] export function useListMembersQuery(uri?: string, limit: number = PAGE_SIZE) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyGraphGetList.OutputSchema, + app.bsky.graph.getList.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ staleTime: STALE.MINUTES.ONE, queryKey: RQKEY(uri ?? ''), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.app.bsky.graph.getList({ - list: uri!, // the enabled flag will prevent this from running until uri is set + return await client.call(app.bsky.graph.getList, { + list: uri! as AtUriString, // the enabled flag will prevent this from running until uri is set limit, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -48,32 +45,32 @@ export function useListMembersQuery(uri?: string, limit: number = PAGE_SIZE) { } export function useAllListMembersQuery(uri?: string) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ staleTime: STALE.MINUTES.ONE, queryKey: RQKEY_ALL(uri ?? ''), queryFn: async () => { - return getAllListMembers(agent, uri!) + return getAllListMembers(client, uri!) }, enabled: Boolean(uri), }) } -export async function getAllListMembers(agent: SessionAgent, uri: string) { +export async function getAllListMembers(client: Client, uri: string) { let hasMore = true let cursor: string | undefined - const listItems: AppBskyGraphDefs.ListItemView[] = [] + const listItems: app.bsky.graph.defs.ListItemView[] = [] // We want to cap this at 6 pages, just for anything weird happening with the api let i = 0 while (hasMore && i < 6) { - const res = await agent.app.bsky.graph.getList({ - list: uri, + const res = await client.call(app.bsky.graph.getList, { + list: uri as AtUriString, limit: 50, cursor, }) - listItems.push(...res.data.items) - hasMore = Boolean(res.data.cursor) - cursor = res.data.cursor + listItems.push(...res.items) + hasMore = Boolean(res.cursor) + cursor = res.cursor i++ } return listItems @@ -92,9 +89,9 @@ export async function invalidateListMembersQuery({ export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) @@ -115,7 +112,7 @@ export function* findAllProfilesInQueryData( } const allQueryData = queryClient.getQueriesData< - AppBskyGraphDefs.ListItemView[] + app.bsky.graph.defs.ListItemView[] >({ queryKey: [RQKEY_ROOT_ALL], }) diff --git a/src/state/queries/list-memberships.ts b/src/state/queries/list-memberships.ts index 75adcd5bb8..2e6cc7b226 100644 --- a/src/state/queries/list-memberships.ts +++ b/src/state/queries/list-memberships.ts @@ -1,8 +1,9 @@ import { - type AppBskyActorDefs, - type AppBskyGraphGetStarterPacksWithMembership, - AtUri, -} from '@atproto/api' + type AtUriString, + type DatetimeString, + type DidString, +} from '@atproto/syntax' +import {AtUri} from '@atproto/syntax' import { type InfiniteData, useMutation, @@ -13,7 +14,8 @@ import { RQKEY as LIST_MEMBERS_RQKEY, RQKEY_ALL as LIST_MEMBERS_ALL_RQKEY, } from '#/state/queries/list-members' -import {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession} from '#/state/session' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' import {RQKEY_WITH_MEMBERSHIP as STARTER_PACKS_WITH_MEMBERSHIPS_RKEY} from './actor-starter-packs' @@ -30,7 +32,7 @@ export function useListMembershipAddMutation({ onError?: (error: Error) => void } = {}) { const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation< {uri: string; cid: string}, @@ -41,14 +43,11 @@ export function useListMembershipAddMutation({ if (!currentAccount) { throw new Error('Not signed in') } - const res = await agent.app.bsky.graph.listitem.create( - {repo: currentAccount.did}, - { - subject: actorDid, - list: listUri, - createdAt: new Date().toISOString(), - }, - ) + const res = await pdsClient.create(app.bsky.graph.listitem, { + subject: actorDid as DidString, + list: listUri as AtUriString, + createdAt: new Date().toISOString() as DatetimeString, + }) return res }, onSuccess: (data, variables) => { @@ -66,7 +65,7 @@ export function useListMembershipAddMutation({ // update WITH_MEMBERSHIPS query for starter packs if (subject) { queryClient.setQueryData< - InfiniteData + InfiniteData >(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), old => { if (!old) return old @@ -86,8 +85,8 @@ export function useListMembershipAddMutation({ ...spWithMembership.starterPack, listItemsSample: [ { - uri: data.uri, - subject: subject as AppBskyActorDefs.ProfileView, + uri: data.uri as AtUriString, + subject: subject as app.bsky.actor.defs.ProfileView, }, ...(spWithMembership.starterPack.listItemsSample?.filter( item => item.subject.did !== variables.actorDid, @@ -101,8 +100,8 @@ export function useListMembershipAddMutation({ }, }, listItem: { - uri: data.uri, - subject: subject as AppBskyActorDefs.ProfileView, + uri: data.uri as AtUriString, + subject: subject as app.bsky.actor.defs.ProfileView, }, } } @@ -129,7 +128,7 @@ export function useListMembershipRemoveMutation({ onError?: (error: Error) => void } = {}) { const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation< void, @@ -141,8 +140,8 @@ export function useListMembershipRemoveMutation({ throw new Error('Not signed in') } const membershipUrip = new AtUri(membershipUri) - await agent.app.bsky.graph.listitem.delete({ - repo: currentAccount.did, + await pdsClient.delete(app.bsky.graph.listitem, { + repo: currentAccount.did as DidString, rkey: membershipUrip.rkey, }) }, @@ -160,7 +159,7 @@ export function useListMembershipRemoveMutation({ // update WITH_MEMBERSHIPS query for starter packs queryClient.setQueryData< - InfiniteData + InfiniteData >(STARTER_PACKS_WITH_MEMBERSHIPS_RKEY(variables.actorDid), old => { if (!old) return old diff --git a/src/state/queries/list.ts b/src/state/queries/list.ts index 64beaef9b9..98001770c1 100644 --- a/src/state/queries/list.ts +++ b/src/state/queries/list.ts @@ -1,13 +1,17 @@ +import {type Client} from '@atproto/lex-client' import { - type $Typed, - type AppBskyGraphDefs, - type AppBskyGraphGetList, - type AppBskyGraphList, + type AtIdentifierString, AtUri, - type ComAtprotoRepoApplyWrites, - type Facet, - type Un$Typed, -} from '@atproto/api' + type AtUriString, + type DatetimeString, + type NsidString, +} from '@atproto/syntax' +import { + blockActorList, + muteActorList, + unblockActorList, + unmuteActorList, +} from '@bsky.app/sdk' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import chunk from 'lodash.chunk' @@ -15,7 +19,8 @@ import {uploadBlob} from '#/lib/api' import {until} from '#/lib/async/until' import {type ImageMeta} from '#/state/gallery' import {STALE} from '#/state/queries' -import {type SessionAgent, useAgent, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' +import {app, com} from '#/lexicons' import {FEED_INFO_RQKEY_ROOT} from './feed' import {invalidate as invalidateMyLists} from './my-lists' import {RQKEY as PROFILE_LISTS_RQKEY} from './profile-lists' @@ -24,19 +29,19 @@ export const RQKEY_ROOT = 'list' export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] export function useListQuery(uri?: string) { - const agent = useAgent() - return useQuery({ + const client = useAppviewClient() + return useQuery({ staleTime: STALE.MINUTES.ONE, queryKey: RQKEY(uri || ''), async queryFn() { if (!uri) { throw new Error('URI not provided') } - const res = await agent.app.bsky.graph.getList({ - list: uri, + const res = await client.call(app.bsky.graph.getList, { + list: uri as AtUriString, limit: 1, }) - return res.data.list + return res.list }, enabled: !!uri, }) @@ -46,13 +51,14 @@ export interface ListCreateMutateParams { purpose: string name: string description: string - descriptionFacets: Facet[] | undefined + descriptionFacets: app.bsky.richtext.facet.Main[] | undefined avatar: ImageMeta | null | undefined } export function useListCreateMutation() { const {currentAccount} = useSession() const queryClient = useQueryClient() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() return useMutation<{uri: string; cid: string}, Error, ListCreateMutateParams>( { async mutationFn({ @@ -71,33 +77,24 @@ export function useListCreateMutation() { ) { throw new Error('Invalid list purpose: must be curatelist or modlist') } - const record: Un$Typed = { + const record: Omit = { purpose, name, description, descriptionFacets, avatar: undefined, - createdAt: new Date().toISOString(), + createdAt: new Date().toISOString() as DatetimeString, } if (avatar) { - const blobRes = await uploadBlob(agent, avatar.path, avatar.mime) - record.avatar = blobRes.data.blob + const blobRes = await uploadBlob(pdsClient, avatar.path, avatar.mime) + record.avatar = blobRes.blob } - const res = await agent.app.bsky.graph.list.create( - { - repo: currentAccount.did, - }, - record, - ) + const res = await pdsClient.create(app.bsky.graph.list, record) // wait for the appview to update - await whenAppViewReady( - agent, - res.uri, - (v: AppBskyGraphGetList.Response) => { - return typeof v?.data?.list.uri === 'string' - }, - ) + await whenAppViewReady(appviewClient, res.uri, v => { + return typeof v?.list.uri === 'string' + }) return res }, onSuccess() { @@ -114,12 +111,13 @@ export interface ListMetadataMutateParams { uri: string name: string description: string - descriptionFacets: Facet[] | undefined + descriptionFacets: app.bsky.richtext.facet.Main[] | undefined avatar: ImageMeta | null | undefined } export function useListMetadataMutation() { const {currentAccount} = useSession() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation< {uri: string; cid: string}, @@ -136,7 +134,7 @@ export function useListMetadataMutation() { } // get the current record - const {value: record} = await agent.app.bsky.graph.list.get({ + const {value: record} = await pdsClient.get(app.bsky.graph.list, { repo: currentAccount.did, rkey, }) @@ -146,31 +144,25 @@ export function useListMetadataMutation() { record.description = description record.descriptionFacets = descriptionFacets if (avatar) { - const blobRes = await uploadBlob(agent, avatar.path, avatar.mime) - record.avatar = blobRes.data.blob + const blobRes = await uploadBlob(pdsClient, avatar.path, avatar.mime) + record.avatar = blobRes.blob } else if (avatar === null) { record.avatar = undefined } - const res = ( - await agent.com.atproto.repo.putRecord({ - repo: currentAccount.did, - collection: 'app.bsky.graph.list', - rkey, - record, - }) - ).data + const res = await pdsClient.call(com.atproto.repo.putRecord, { + repo: currentAccount.did, + collection: 'app.bsky.graph.list', + rkey, + record, + }) // wait for the appview to update - await whenAppViewReady( - agent, - res.uri, - (v: AppBskyGraphGetList.Response) => { - const list = v.data.list - return ( - list.name === record.name && list.description === record.description - ) - }, - ) + await whenAppViewReady(appviewClient, res.uri, v => { + const list = v.list + return ( + list.name === record.name && list.description === record.description + ) + }) return res }, onSuccess(data, variables) { @@ -190,7 +182,8 @@ export function useListMetadataMutation() { export function useListDeleteMutation() { const {currentAccount} = useSession() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation({ mutationFn: async ({uri}) => { @@ -198,11 +191,11 @@ export function useListDeleteMutation() { return } // fetch all the listitem records that belong to this list - let cursor + let cursor: string | undefined let listitemRecordUris: string[] = [] for (let i = 0; i < 100; i++) { - const res = await agent.app.bsky.graph.listitem.list({ - repo: currentAccount.did, + const res = await pdsClient.list(app.bsky.graph.listitem, { + repo: currentAccount.did as AtIdentifierString, cursor, limit: 100, }) @@ -220,11 +213,11 @@ export function useListDeleteMutation() { // batch delete the list and listitem records const createDel = ( uri: string, - ): $Typed => { + ): com.atproto.repo.applyWrites.$InputBody['writes'][number] => { const urip = new AtUri(uri) return { $type: 'com.atproto.repo.applyWrites#delete', - collection: urip.collection, + collection: urip.collection as NsidString, rkey: urip.rkey, } } @@ -234,15 +227,17 @@ export function useListDeleteMutation() { // apply in chunks for (const writesChunk of chunk(writes, 10)) { - await agent.com.atproto.repo.applyWrites({ - repo: currentAccount.did, + await pdsClient.call(com.atproto.repo.applyWrites, { + repo: currentAccount.did as AtIdentifierString, writes: writesChunk, }) } - // wait for the appview to update - await whenAppViewReady(agent, uri, (v: AppBskyGraphGetList.Response) => { - return !v?.success + // wait for the appview to update. once the list is deleted, getList + // throws (404), `until` catches it and passes `undefined` here, so an + // absent body signals a completed delete. + await whenAppViewReady(appviewClient, uri, v => { + return !v }) }, onSuccess() { @@ -257,17 +252,18 @@ export function useListDeleteMutation() { export function useListMuteMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async ({uri, mute}) => { if (mute) { - await agent.muteModList(uri) + await pdsClient.call(muteActorList, {list: uri as AtUriString}) } else { - await agent.unmuteModList(uri) + await pdsClient.call(unmuteActorList, {list: uri as AtUriString}) } - await whenAppViewReady(agent, uri, (v: AppBskyGraphGetList.Response) => { - return Boolean(v?.data.list.viewer?.muted) === mute + await whenAppViewReady(appviewClient, uri, v => { + return Boolean(v?.list.viewer?.muted) === mute }) }, onSuccess(data, variables) { @@ -280,19 +276,20 @@ export function useListMuteMutation() { export function useListBlockMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async ({uri, block}) => { if (block) { - await agent.blockModList(uri) + await pdsClient.call(blockActorList, {list: uri as AtUriString}) } else { - await agent.unblockModList(uri) + await pdsClient.call(unblockActorList, {list: uri as AtUriString}) } - await whenAppViewReady(agent, uri, (v: AppBskyGraphGetList.Response) => { + await whenAppViewReady(appviewClient, uri, v => { return block - ? typeof v?.data.list.viewer?.blocked === 'string' - : !v?.data.list.viewer?.blocked + ? typeof v?.list.viewer?.blocked === 'string' + : !v?.list.viewer?.blocked }) }, onSuccess(data, variables) { @@ -304,17 +301,17 @@ export function useListBlockMutation() { } async function whenAppViewReady( - agent: SessionAgent, + client: Client, uri: string, - fn: (res: AppBskyGraphGetList.Response) => boolean, + fn: (res: app.bsky.graph.getList.$OutputBody) => boolean, ) { await until( 5, // 5 tries 1e3, // 1s delay between tries fn, () => - agent.app.bsky.graph.getList({ - list: uri, + client.call(app.bsky.graph.getList, { + list: uri as AtUriString, limit: 1, }), ) diff --git a/src/state/queries/lists-with-membership.ts b/src/state/queries/lists-with-membership.ts index 3147b8845f..cf291ef47e 100644 --- a/src/state/queries/lists-with-membership.ts +++ b/src/state/queries/lists-with-membership.ts @@ -1,7 +1,4 @@ -import { - type AppBskyActorDefs, - type AppBskyGraphGetListsWithMembership, -} from '@atproto/api' +import {type AtIdentifierString, type AtUriString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -10,10 +7,11 @@ import { } from '@tanstack/react-query' import {createQueryKey} from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' export type ListWithMembership = - AppBskyGraphGetListsWithMembership.ListWithMembership + app.bsky.graph.getListsWithMembership.ListWithMembership const listsWithMembershipQueryKeyRoot = 'lists-with-membership' export const createListsWithMembershipQueryKey = (args: {actor: string}) => @@ -26,23 +24,22 @@ export function useListsWithMembershipQuery({ actor: string | undefined enabled?: boolean }) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyGraphGetListsWithMembership.OutputSchema, + app.bsky.graph.getListsWithMembership.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, string | undefined >({ queryKey: createListsWithMembershipQueryKey({actor: actor ?? ''}), queryFn: async ({pageParam}: {pageParam?: string}) => { - const res = await agent.app.bsky.graph.getListsWithMembership({ - actor: actor!, // the enabled flag prevents this from running until actor is set + return await client.call(app.bsky.graph.getListsWithMembership, { + actor: actor! as AtIdentifierString, // the enabled flag prevents this from running until actor is set limit: 50, cursor: pageParam, }) - return res.data }, enabled: Boolean(actor) && enabled, initialPageParam: undefined, @@ -61,10 +58,10 @@ export function updateListMembershipOptimistically({ actor: string listUri: string membershipUri: string - subject: AppBskyActorDefs.ProfileView + subject: app.bsky.actor.defs.ProfileView }) { queryClient.setQueryData< - InfiniteData + InfiniteData >(createListsWithMembershipQueryKey({actor}), old => { if (!old) return old @@ -77,7 +74,7 @@ export function updateListMembershipOptimistically({ return { ...lwm, listItem: { - uri: membershipUri, + uri: membershipUri as AtUriString, subject, }, } @@ -99,7 +96,7 @@ export function removeListMembershipOptimistically({ listUri: string }) { queryClient.setQueryData< - InfiniteData + InfiniteData >(createListsWithMembershipQueryKey({actor}), old => { if (!old) return old diff --git a/src/state/queries/messages/accept-conversation.ts b/src/state/queries/messages/accept-conversation.ts index 668c7decc3..bcc4f25a2d 100644 --- a/src/state/queries/messages/accept-conversation.ts +++ b/src/state/queries/messages/accept-conversation.ts @@ -1,12 +1,8 @@ -import { - type ChatBskyConvoAcceptConvo, - type ChatBskyConvoDefs, -} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { type ConvoRequestListQueryData, optimisticDelete as optimisticDeleteRequest, @@ -29,19 +25,16 @@ export function useAcceptConversation( onError, }: { onMutate?: () => void - onSuccess?: (data: ChatBskyConvoAcceptConvo.OutputSchema) => void + onSuccess?: (data: chat.bsky.convo.acceptConvo.$OutputBody) => void onError?: (error: Error) => void }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() return useMutation({ mutationFn: async () => { - const {data} = await agent.chat.bsky.convo.acceptConvo( - {convoId}, - {headers: DM_SERVICE_HEADERS}, - ) + const data = await chatClient.call(chat.bsky.convo.acceptConvo, {convoId}) return data }, @@ -52,7 +45,7 @@ export function useAcceptConversation( queryClient.getQueriesData({ queryKey: [CONVO_LIST_ROOT_KEY], }) - let convoBeingAccepted: ChatBskyConvoDefs.ConvoView | null = null + let convoBeingAccepted: chat.bsky.convo.defs.ConvoView | null = null for (const [_key, data] of queryClient.getQueriesData( {queryKey: CONVO_LIST_PARTIAL_KEY('request')}, )) { @@ -65,7 +58,7 @@ export function useAcceptConversation( (old?: ConvoListQueryData) => optimisticDelete(convoId, old), ) if (convoBeingAccepted) { - const acceptedConvo: ChatBskyConvoDefs.ConvoView = { + const acceptedConvo: chat.bsky.convo.defs.ConvoView = { ...convoBeingAccepted, status: 'accepted', } diff --git a/src/state/queries/messages/actor-declaration.ts b/src/state/queries/messages/actor-declaration.ts index 9badf7e6b5..af977b7e60 100644 --- a/src/state/queries/messages/actor-declaration.ts +++ b/src/state/queries/messages/actor-declaration.ts @@ -1,13 +1,12 @@ -import { - type AppBskyActorDefs, - type ChatBskyActorDeclaration, -} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import {useMutation, useQueryClient} from '@tanstack/react-query' import {logger} from '#/logger' -import {type SessionAgent} from '#/state/session' -import {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession} from '#/state/session' +import {agentToLexClient} from '#/state/session/clients' +import {type SessionAgent} from '#/state/session/session-core' import {resolveAllowGroupInvites} from '#/components/dms/util' +import {type app, chat, com} from '#/lexicons' import {RQKEY as PROFILE_RKEY} from '../profile' export function useUpdateActorDeclaration({ @@ -19,7 +18,7 @@ export function useUpdateActorDeclaration({ }) { const queryClient = useQueryClient() const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async (update: { @@ -28,7 +27,7 @@ export function useUpdateActorDeclaration({ }) => { if (!currentAccount) throw new Error('Not signed in') const current = - queryClient.getQueryData( + queryClient.getQueryData( PROFILE_RKEY(currentAccount.did), ) const allowIncoming = @@ -41,8 +40,8 @@ export function useUpdateActorDeclaration({ update.allowGroupInvites ?? current?.associated?.chat?.allowGroupInvites, }) - const result = await agent.com.atproto.repo.putRecord({ - repo: currentAccount.did, + const result = await pdsClient.call(com.atproto.repo.putRecord, { + repo: currentAccount.did as DidString, collection: 'chat.bsky.actor.declaration', rkey: 'self', record: { @@ -57,7 +56,7 @@ export function useUpdateActorDeclaration({ if (!currentAccount) return queryClient.setQueryData( PROFILE_RKEY(currentAccount?.did), - (old?: AppBskyActorDefs.ProfileViewDetailed) => { + (old?: app.bsky.actor.defs.ProfileViewDetailed) => { if (!old) return old const allowIncoming = update.allowIncoming ?? @@ -81,7 +80,7 @@ export function useUpdateActorDeclaration({ allowGroupInvites, }, }, - } satisfies AppBskyActorDefs.ProfileViewDetailed + } satisfies app.bsky.actor.defs.ProfileViewDetailed }, ) }, @@ -101,13 +100,13 @@ export function useUpdateActorDeclaration({ // for use in the settings screen for testing export function useDeleteActorDeclaration() { const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async () => { if (!currentAccount) throw new Error('Not signed in') - const result = await agent.api.com.atproto.repo.deleteRecord({ - repo: currentAccount.did, + const result = await pdsClient.call(com.atproto.repo.deleteRecord, { + repo: currentAccount.did as DidString, collection: 'chat.bsky.actor.declaration', rkey: 'self', }) @@ -124,12 +123,20 @@ export async function fetchActorDeclarationRecord({ did?: string }) { if (!did) return - const res = await agent.com.atproto.repo - .getRecord({ - repo: did, - collection: 'chat.bsky.actor.declaration', - rkey: 'self', - }) + /* + * This helper is called with a bridge `SessionAgent` threaded from + * `#/ageAssurance/data`. Wrap it as an account lex `Client` so the record + * read goes through the same path as the migrated hooks; the caller keeps + * passing the agent until the bridge is removed (Phase 4). The cast is safe: + * `agentToLexClient` only reads `did` and `fetchHandler`, both of which the + * base `Agent` (and thus `SessionAgent`) provides - its `AtpAgent` parameter + * type is just narrower than it needs. TODO(phase4): drop with the bridge. + */ + const client = agentToLexClient( + agent as unknown as Parameters[0], + ) + const res = await client + .get(chat.bsky.actor.declaration, {repo: did as DidString, rkey: 'self'}) .catch(_e => undefined) - return res?.data.value as ChatBskyActorDeclaration.Main + return res?.value } diff --git a/src/state/queries/messages/add-group-members.ts b/src/state/queries/messages/add-group-members.ts index b5ddacbbff..fbbffd5acb 100644 --- a/src/state/queries/messages/add-group-members.ts +++ b/src/state/queries/messages/add-group-members.ts @@ -1,20 +1,15 @@ -import { - type ChatBskyActorDefs, - ChatBskyConvoDefs, - type ChatBskyConvoListConvos, - type ChatBskyGroupAddMembers, -} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import { type InfiniteData, useMutation, useQueryClient, } from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' import {useProfileQuery} from '#/state/queries/profile' -import {useAgent, useSession} from '#/state/session' -import type * as bsky from '#/types/bsky' +import {useChatClient, useSession} from '#/state/session' +import {chat} from '#/lexicons' +import * as bsky from '#/types/bsky' import {RQKEY as CONVO_KEY} from './conversation' import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations' import {listConvoMembersQueryKey} from './list-convo-members' @@ -25,12 +20,12 @@ export function useAddGroupMembers( onSuccess, onError, }: { - onSuccess?: (data: ChatBskyGroupAddMembers.OutputSchema) => void + onSuccess?: (data: chat.bsky.group.addMembers.$OutputBody) => void onError?: (error: Error) => void }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() const {currentAccount} = useSession() const {data: myProfile} = useProfileQuery({did: currentAccount?.did}) @@ -42,48 +37,60 @@ export function useAddGroupMembers( profiles: bsky.profile.AnyProfileView[] }) => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.addMembers( - {convoId, members}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + const data = await chatClient.call(chat.bsky.group.addMembers, { + convoId, + members: members as DidString[], + }) return data }, onMutate: ({profiles}) => { if (!convoId) return - const prevConvo = queryClient.getQueryData( - CONVO_KEY(convoId), - ) + const prevConvo = + queryClient.getQueryData( + CONVO_KEY(convoId), + ) const prevListEntries = queryClient.getQueriesData< - InfiniteData + InfiniteData >({queryKey: [CONVO_LIST_KEY]}) const prevMemberList = queryClient.getQueryData< - ChatBskyActorDefs.ProfileViewBasic[] + chat.bsky.actor.defs.ProfileViewBasic[] >(listConvoMembersQueryKey(convoId)) - const addedBy: ChatBskyActorDefs.ProfileViewBasic | undefined = myProfile - ? { - ...myProfile, + /* + * The profile views come from producers that still emit the old + * `@atproto/api` shapes (useProfileQuery migrates in a later task), while + * the chat caches are now typed on the lexicon views. Structurally + * identical modulo branded strings. TODO(phase4): drop toLex once those + * producers migrate. + */ + const addedBy: chat.bsky.actor.defs.ProfileViewBasic | undefined = + myProfile + ? bsky.toLex({ + ...myProfile, + $type: 'chat.bsky.actor.defs#profileViewBasic', + }) + : undefined + + const optimisticMembers: chat.bsky.actor.defs.ProfileViewBasic[] = + profiles.map(profile => + bsky.toLex({ + ...profile, $type: 'chat.bsky.actor.defs#profileViewBasic', - } - : undefined + kind: { + $type: 'chat.bsky.actor.defs#groupConvoMember', + role: 'standard', + addedBy, + }, + }), + ) - const optimisticMembers: ChatBskyActorDefs.ProfileViewBasic[] = - profiles.map(profile => ({ - ...profile, - $type: 'chat.bsky.actor.defs#profileViewBasic', - kind: { - $type: 'chat.bsky.actor.defs#groupConvoMember', - role: 'standard', - addedBy, - }, - })) - - queryClient.setQueryData( + queryClient.setQueryData( CONVO_KEY(convoId), prev => { if (!prev) return - if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return prev + if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind)) + return prev return { ...prev, members: [...prev.members, ...optimisticMembers], @@ -96,7 +103,7 @@ export function useAddGroupMembers( ) queryClient.setQueriesData< - InfiniteData + InfiniteData >({queryKey: [CONVO_LIST_KEY]}, prev => { if (!prev?.pages) return return { @@ -105,7 +112,8 @@ export function useAddGroupMembers( ...page, convos: page.convos.map(convo => { if (convo.id !== convoId) return convo - if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) return convo + if (!bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) + return convo return { ...convo, members: [...convo.members, ...optimisticMembers], @@ -120,7 +128,7 @@ export function useAddGroupMembers( } }) - queryClient.setQueryData( + queryClient.setQueryData( listConvoMembersQueryKey(convoId), prev => { if (!prev) return @@ -132,13 +140,13 @@ export function useAddGroupMembers( }, onSuccess: data => { if (convoId) { - queryClient.setQueryData( + queryClient.setQueryData( CONVO_KEY(convoId), data.convo, ) queryClient.setQueriesData< - InfiniteData + InfiniteData >({queryKey: [CONVO_LIST_KEY]}, prev => { if (!prev?.pages) return return { diff --git a/src/state/queries/messages/conversation.ts b/src/state/queries/messages/conversation.ts index 1b7699c0ef..190cf858a0 100644 --- a/src/state/queries/messages/conversation.ts +++ b/src/state/queries/messages/conversation.ts @@ -1,9 +1,3 @@ -import { - type ChatBskyActorDefs, - type ChatBskyConvoDefs, - type ChatBskyConvoGetConvo, - type ChatBskyConvoGetUnreadCounts, -} from '@atproto/api' import { type QueryClient, useMutation, @@ -11,10 +5,10 @@ import { useQueryClient, } from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {STALE} from '#/state/queries' import {useOnMarkAsRead} from '#/state/queries/messages/list-conversations' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { RQKEY_PARTIAL as UNREAD_COUNTS_PARTIAL_KEY, UNREAD_ACCEPTED_CAP, @@ -30,15 +24,12 @@ export const RQKEY_ROOT = 'convo' export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId] export function useConvoQuery({convoId}: {convoId: string}) { - const agent = useAgent() + const chatClient = useChatClient() return useQuery({ queryKey: RQKEY(convoId), queryFn: async () => { - const {data} = await agent.chat.bsky.convo.getConvo( - {convoId}, - {headers: DM_SERVICE_HEADERS}, - ) + const data = await chatClient.call(chat.bsky.convo.getConvo, {convoId}) return data.convo }, staleTime: STALE.INFINITY, @@ -47,7 +38,7 @@ export function useConvoQuery({convoId}: {convoId: string}) { export function precacheConvoQuery( queryClient: QueryClient, - convo: ChatBskyConvoDefs.ConvoView, + convo: chat.bsky.convo.defs.ConvoView, ) { queryClient.setQueryData(RQKEY(convo.id), convo) } @@ -55,7 +46,7 @@ export function precacheConvoQuery( export function useMarkAsReadMutation() { const optimisticUpdate = useOnMarkAsRead() const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() return useMutation({ mutationFn: async ({ @@ -67,16 +58,10 @@ export function useMarkAsReadMutation() { }) => { if (!convoId) throw new Error('No convoId provided') - await agent.chat.bsky.convo.updateRead( - { - convoId, - messageId, - }, - { - encoding: 'application/json', - headers: DM_SERVICE_HEADERS, - }, - ) + await chatClient.call(chat.bsky.convo.updateRead, { + convoId, + messageId, + }) }, onMutate({convoId}) { if (!convoId) throw new Error('No convoId provided') @@ -90,7 +75,7 @@ export function useMarkAsReadMutation() { // find the convo so we know which badge counter (if any) to decrement. // keep scanning past a stale unreadCount === 0 cache so another cache // holding the true unread state still drives the decrement - let unreadStatus: ChatBskyConvoDefs.ConvoView['status'] | undefined + let unreadStatus: chat.bsky.convo.defs.ConvoView['status'] | undefined for (const [, data] of prevListQueries) { if (!data) continue const convo = getConvoFromQueryData(convoId, data) @@ -105,11 +90,13 @@ export function useMarkAsReadMutation() { // the badge count query is a separate server query that the list caches // don't feed, so decrement it here to keep the badge in sync const prevUnreadCountsQueries = - queryClient.getQueriesData({ - queryKey: UNREAD_COUNTS_PARTIAL_KEY, - }) + queryClient.getQueriesData( + { + queryKey: UNREAD_COUNTS_PARTIAL_KEY, + }, + ) if (unreadStatus) { - queryClient.setQueriesData( + queryClient.setQueriesData( {queryKey: UNREAD_COUNTS_PARTIAL_KEY}, old => { if (!old) return old @@ -193,9 +180,9 @@ export function useMarkAsReadMutation() { export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< - ChatBskyConvoGetConvo.OutputSchema['convo'] + chat.bsky.convo.getConvo.$OutputBody['convo'] >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/messages/create-group-chat.ts b/src/state/queries/messages/create-group-chat.ts index 9f8aadc7d0..30933ce525 100644 --- a/src/state/queries/messages/create-group-chat.ts +++ b/src/state/queries/messages/create-group-chat.ts @@ -1,27 +1,27 @@ -import {type ChatBskyGroupCreateGroup} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import {precacheConvoQuery} from './conversation' export function useCreateGroupChat({ onSuccess, onError, }: { - onSuccess?: (data: ChatBskyGroupCreateGroup.OutputSchema) => void + onSuccess?: (data: chat.bsky.group.createGroup.$OutputBody) => void onError?: (error: Error) => void }) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() return useMutation({ mutationFn: async ({name, members}: {name: string; members: string[]}) => { - const {data} = await agent.chat.bsky.group.createGroup( - {name, members}, - {headers: DM_SERVICE_HEADERS}, - ) + const data = await chatClient.call(chat.bsky.group.createGroup, { + name, + members: members as DidString[], + }) return data }, diff --git a/src/state/queries/messages/create-join-link.ts b/src/state/queries/messages/create-join-link.ts index fbac855466..04c1434808 100644 --- a/src/state/queries/messages/create-join-link.ts +++ b/src/state/queries/messages/create-join-link.ts @@ -1,13 +1,10 @@ -import { - ChatBskyConvoDefs, - type ChatBskyGroupCreateJoinLink, - type ChatBskyGroupDefs, -} from '@atproto/api' +import {type DatetimeString} from '@atproto/syntax' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' +import * as bsky from '#/types/bsky' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -19,32 +16,34 @@ export function useCreateJoinLink( onSuccess, onError, }: { - onSuccess?: (data: ChatBskyGroupCreateJoinLink.OutputSchema) => void + onSuccess?: (data: chat.bsky.group.createJoinLink.$OutputBody) => void onError?: (error: Error) => void }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() return useMutation({ mutationFn: async ({ joinRule, requireApproval, }: { - joinRule: ChatBskyGroupDefs.JoinRule + joinRule: chat.bsky.group.defs.JoinRule requireApproval: boolean }) => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.createJoinLink( - {convoId, joinRule, requireApproval}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + const data = await chatClient.call(chat.bsky.group.createJoinLink, { + convoId, + joinRule, + requireApproval, + }) return data }, onMutate: ({joinRule, requireApproval}) => { if (!convoId) return return updateConvoOptimistic(queryClient, convoId, prev => { - if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined + if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind)) + return undefined return { ...prev, kind: { @@ -55,7 +54,9 @@ export function useCreateJoinLink( enabledStatus: 'enabled', joinRule, requireApproval, - createdAt: new Date().toISOString(), + // ISO string is a valid datetime; assert the branded type the + // generated JoinLinkView expects for this optimistic-only value. + createdAt: new Date().toISOString() as DatetimeString, }, }, } @@ -64,7 +65,8 @@ export function useCreateJoinLink( onSuccess: data => { if (convoId) { updateConvoOptimistic(queryClient, convoId, prev => { - if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined + if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind)) + return undefined return { ...prev, kind: {...prev.kind, joinLink: data.joinLink}, diff --git a/src/state/queries/messages/disable-join-link.ts b/src/state/queries/messages/disable-join-link.ts index c143feb6cc..88fec177e6 100644 --- a/src/state/queries/messages/disable-join-link.ts +++ b/src/state/queries/messages/disable-join-link.ts @@ -1,13 +1,10 @@ -import { - ChatBskyConvoDefs, - type ChatBskyGroupDisableJoinLink, -} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' +import * as bsky from '#/types/bsky' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -19,26 +16,28 @@ export function useDisableJoinLink( onSuccess, onError, }: { - onSuccess?: (data: ChatBskyGroupDisableJoinLink.OutputSchema) => void + onSuccess?: (data: chat.bsky.group.disableJoinLink.$OutputBody) => void onError?: (error: Error) => void }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() return useMutation({ mutationFn: async () => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.disableJoinLink( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + const data = await chatClient.call(chat.bsky.group.disableJoinLink, { + convoId, + }) return data }, onMutate: () => { if (!convoId) return return updateConvoOptimistic(queryClient, convoId, prev => { - if (!ChatBskyConvoDefs.isGroupConvo(prev.kind) || !prev.kind.joinLink) { + if ( + !bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind) || + !prev.kind.joinLink + ) { return undefined } return { @@ -53,7 +52,8 @@ export function useDisableJoinLink( onSuccess: data => { if (convoId) { updateConvoOptimistic(queryClient, convoId, prev => { - if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined + if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind)) + return undefined return { ...prev, kind: {...prev.kind, joinLink: data.joinLink}, diff --git a/src/state/queries/messages/edit-group-chat-name.ts b/src/state/queries/messages/edit-group-chat-name.ts index ef2a72f4ec..fa2b4f935a 100644 --- a/src/state/queries/messages/edit-group-chat-name.ts +++ b/src/state/queries/messages/edit-group-chat-name.ts @@ -1,9 +1,9 @@ -import {ChatBskyConvoDefs, type ChatBskyGroupEditGroup} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' +import * as bsky from '#/types/bsky' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -15,26 +15,27 @@ export function useEditGroupChatName( onSuccess, onError, }: { - onSuccess?: (data: ChatBskyGroupEditGroup.OutputSchema) => void + onSuccess?: (data: chat.bsky.group.editGroup.$OutputBody) => void onError?: (error: Error) => void }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() return useMutation({ mutationFn: async ({name: groupName}: {name: string}) => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.editGroup( - {convoId, name: groupName}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + const data = await chatClient.call(chat.bsky.group.editGroup, { + convoId, + name: groupName, + }) return data }, onMutate: ({name: groupName}) => { if (!convoId) return return updateConvoOptimistic(queryClient, convoId, prev => { - if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined + if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind)) + return undefined return { ...prev, kind: {...prev.kind, name: groupName}, diff --git a/src/state/queries/messages/edit-join-link.ts b/src/state/queries/messages/edit-join-link.ts index 7e34ce88d9..b8d1dd5146 100644 --- a/src/state/queries/messages/edit-join-link.ts +++ b/src/state/queries/messages/edit-join-link.ts @@ -1,13 +1,9 @@ -import { - ChatBskyConvoDefs, - type ChatBskyGroupDefs, - type ChatBskyGroupEditJoinLink, -} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' +import * as bsky from '#/types/bsky' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -19,32 +15,36 @@ export function useEditJoinLink( onSuccess, onError, }: { - onSuccess?: (data: ChatBskyGroupEditJoinLink.OutputSchema) => void + onSuccess?: (data: chat.bsky.group.editJoinLink.$OutputBody) => void onError?: (error: Error) => void }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() return useMutation({ mutationFn: async ({ joinRule, requireApproval, }: { - joinRule: ChatBskyGroupDefs.JoinRule + joinRule: chat.bsky.group.defs.JoinRule requireApproval: boolean }) => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.editJoinLink( - {convoId, joinRule, requireApproval}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + const data = await chatClient.call(chat.bsky.group.editJoinLink, { + convoId, + joinRule, + requireApproval, + }) return data }, onMutate: ({joinRule, requireApproval}) => { if (!convoId) return return updateConvoOptimistic(queryClient, convoId, prev => { - if (!ChatBskyConvoDefs.isGroupConvo(prev.kind) || !prev.kind.joinLink) { + if ( + !bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind) || + !prev.kind.joinLink + ) { return undefined } return { @@ -59,7 +59,8 @@ export function useEditJoinLink( onSuccess: data => { if (convoId) { updateConvoOptimistic(queryClient, convoId, prev => { - if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined + if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind)) + return undefined return { ...prev, kind: {...prev.kind, joinLink: data.joinLink}, diff --git a/src/state/queries/messages/enable-join-link.ts b/src/state/queries/messages/enable-join-link.ts index 4febc85441..abf2b35198 100644 --- a/src/state/queries/messages/enable-join-link.ts +++ b/src/state/queries/messages/enable-join-link.ts @@ -1,10 +1,10 @@ -import {ChatBskyConvoDefs, type ChatBskyGroupEnableJoinLink} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' +import * as bsky from '#/types/bsky' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -16,26 +16,28 @@ export function useEnableJoinLink( onSuccess, onError, }: { - onSuccess?: (data: ChatBskyGroupEnableJoinLink.OutputSchema) => void + onSuccess?: (data: chat.bsky.group.enableJoinLink.$OutputBody) => void onError?: (error: Error) => void }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() return useMutation({ mutationFn: async () => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.enableJoinLink( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + const data = await chatClient.call(chat.bsky.group.enableJoinLink, { + convoId, + }) return data }, onMutate: () => { if (!convoId) return return updateConvoOptimistic(queryClient, convoId, prev => { - if (!ChatBskyConvoDefs.isGroupConvo(prev.kind) || !prev.kind.joinLink) { + if ( + !bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind) || + !prev.kind.joinLink + ) { return undefined } return { @@ -50,7 +52,8 @@ export function useEnableJoinLink( onSuccess: data => { if (convoId) { updateConvoOptimistic(queryClient, convoId, prev => { - if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined + if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind)) + return undefined return { ...prev, kind: {...prev.kind, joinLink: data.joinLink}, diff --git a/src/state/queries/messages/get-convo-availability.ts b/src/state/queries/messages/get-convo-availability.ts index b73efe7953..3150b367e9 100644 --- a/src/state/queries/messages/get-convo-availability.ts +++ b/src/state/queries/messages/get-convo-availability.ts @@ -1,7 +1,8 @@ +import {type DidString} from '@atproto/syntax' import {useQuery} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import {STALE} from '..' const RQKEY_ROOT = 'convo-availability' @@ -11,15 +12,14 @@ export function useGetConvoAvailabilityQuery( did: string, {enabled = true}: {enabled?: boolean} = {}, ) { - const agent = useAgent() + const chatClient = useChatClient() return useQuery({ queryKey: RQKEY(did), queryFn: async () => { - const {data} = await agent.chat.bsky.convo.getConvoAvailability( - {members: [did]}, - {headers: DM_SERVICE_HEADERS}, - ) + const data = await chatClient.call(chat.bsky.convo.getConvoAvailability, { + members: [did as DidString], + }) return data }, diff --git a/src/state/queries/messages/get-convo-for-members.ts b/src/state/queries/messages/get-convo-for-members.ts index 58c1ab524a..4d325a78b5 100644 --- a/src/state/queries/messages/get-convo-for-members.ts +++ b/src/state/queries/messages/get-convo-for-members.ts @@ -1,27 +1,26 @@ -import {type ChatBskyConvoGetConvoForMembers} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import {precacheConvoQuery} from './conversation' export function useGetConvoForMembers({ onSuccess, onError, }: { - onSuccess?: (data: ChatBskyConvoGetConvoForMembers.OutputSchema) => void + onSuccess?: (data: chat.bsky.convo.getConvoForMembers.$OutputBody) => void onError?: (error: Error) => void }) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() return useMutation({ mutationFn: async (members: string[]) => { - const {data} = await agent.chat.bsky.convo.getConvoForMembers( - {members: members}, - {headers: DM_SERVICE_HEADERS}, - ) + const data = await chatClient.call(chat.bsky.convo.getConvoForMembers, { + members: members as DidString[], + }) return data }, diff --git a/src/state/queries/messages/get-status.ts b/src/state/queries/messages/get-status.ts index 89625dca24..c4874294dc 100644 --- a/src/state/queries/messages/get-status.ts +++ b/src/state/queries/messages/get-status.ts @@ -1,7 +1,7 @@ import {useQuery} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import {STALE} from '..' import {createQueryKey} from '../util' @@ -9,17 +9,14 @@ const chatActorStatusQueryKey = () => createQueryKey('chat-actor-status', {}, {persistedVersion: 1}) export function useChatActorStatusQuery() { - const agent = useAgent() + const chatClient = useChatClient() return useQuery({ gcTime: STALE.INFINITY, staleTime: STALE.SECONDS.FIFTEEN, queryKey: chatActorStatusQueryKey(), queryFn: async () => { - const {data} = await agent.chat.bsky.actor.getStatus( - {}, - {headers: DM_SERVICE_HEADERS}, - ) + const data = await chatClient.call(chat.bsky.actor.getStatus, {}) return data }, diff --git a/src/state/queries/messages/get-unread-counts.ts b/src/state/queries/messages/get-unread-counts.ts index 8c276d67f7..28a50dc0be 100644 --- a/src/state/queries/messages/get-unread-counts.ts +++ b/src/state/queries/messages/get-unread-counts.ts @@ -1,8 +1,8 @@ import {useQuery} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' -import {useAgent, useSession} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' import {useAgeAssurance} from '#/ageAssurance' +import {chat} from '#/lexicons' import {STALE} from '..' const RQKEY_ROOT = 'convo-unread-counts' @@ -18,7 +18,7 @@ export const UNREAD_ACCEPTED_CAP = 100 export const UNREAD_REQUEST_CAP = 100 export function useUnreadCountsQuery() { - const agent = useAgent() + const chatClient = useChatClient() const {hasSession} = useSession() const aa = useAgeAssurance() const includeGroupChats = !aa.flags.groupChatDisabled @@ -26,10 +26,9 @@ export function useUnreadCountsQuery() { return useQuery({ queryKey: RQKEY(includeGroupChats), queryFn: async () => { - const {data} = await agent.chat.bsky.convo.getUnreadCounts( - {includeGroupChats}, - {headers: DM_SERVICE_HEADERS}, - ) + const data = await chatClient.call(chat.bsky.convo.getUnreadCounts, { + includeGroupChats, + }) return data }, staleTime: STALE.SECONDS.FIFTEEN, diff --git a/src/state/queries/messages/join-requests.ts b/src/state/queries/messages/join-requests.ts index 179dfafaaf..aa0c9a751f 100644 --- a/src/state/queries/messages/join-requests.ts +++ b/src/state/queries/messages/join-requests.ts @@ -1,26 +1,21 @@ -import { - type ChatBskyActorDefs, - type ChatBskyGroupApproveJoinRequest, - type ChatBskyGroupListJoinRequests, - type ChatBskyGroupRejectJoinRequest, -} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import { type InfiniteData, useMutation, useQueryClient, } from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import {listConvoMembersQueryKey} from './list-convo-members' import {createListJoinRequestsQueryKey} from './list-join-requests' type JoinRequestAction = 'approve' | 'reject' type JoinRequestOutput = A extends 'approve' - ? ChatBskyGroupApproveJoinRequest.OutputSchema - : ChatBskyGroupRejectJoinRequest.OutputSchema + ? chat.bsky.group.approveJoinRequest.$OutputBody + : chat.bsky.group.rejectJoinRequest.$OutputBody export function useJoinRequestMutation( action: A, @@ -34,21 +29,21 @@ export function useJoinRequestMutation( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() return useMutation({ mutationFn: async ({member}: {member: string}) => { if (!convoId) throw new Error('No convoId provided') - const {data} = + const data = action === 'approve' - ? await agent.chat.bsky.group.approveJoinRequest( - {convoId, member}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - : await agent.chat.bsky.group.rejectJoinRequest( - {convoId, member}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + ? await chatClient.call(chat.bsky.group.approveJoinRequest, { + convoId, + member: member as DidString, + }) + : await chatClient.call(chat.bsky.group.rejectJoinRequest, { + convoId, + member: member as DidString, + }) return data as JoinRequestOutput }, onMutate: ({member}) => { @@ -57,7 +52,7 @@ export function useJoinRequestMutation( const requestsKey = createListJoinRequestsQueryKey({convoId}) const prevRequests = queryClient.getQueryData< - InfiniteData + InfiniteData >(requestsKey) const requestedByProfile = prevRequests?.pages @@ -65,7 +60,7 @@ export function useJoinRequestMutation( .find(request => request.requestedBy.did === member)?.requestedBy queryClient.setQueryData< - InfiniteData + InfiniteData >(requestsKey, prev => { if (!prev?.pages) return prev return { @@ -79,14 +74,14 @@ export function useJoinRequestMutation( } }) - let prevMembers: ChatBskyActorDefs.ProfileViewBasic[] | undefined + let prevMembers: chat.bsky.actor.defs.ProfileViewBasic[] | undefined if (action === 'approve' && requestedByProfile) { const membersKey = listConvoMembersQueryKey(convoId) prevMembers = - queryClient.getQueryData( + queryClient.getQueryData( membersKey, ) - queryClient.setQueryData( + queryClient.setQueryData( membersKey, prev => { if (!prev) return prev diff --git a/src/state/queries/messages/leave-conversation.ts b/src/state/queries/messages/leave-conversation.ts index 51b51bde80..0c03d391f4 100644 --- a/src/state/queries/messages/leave-conversation.ts +++ b/src/state/queries/messages/leave-conversation.ts @@ -1,13 +1,9 @@ -import { - type ChatBskyConvoLeaveConvo, - type ChatBskyConvoListConvos, -} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' import {invalidateJoinLinkPreviewsForConvo} from '#/state/queries/join-links' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { type ConvoRequestListQueryData, optimisticDelete as optimisticDeleteRequest, @@ -22,7 +18,7 @@ export function RQKEY(convoId: string | undefined) { type ConvoListQueryData = { pageParams: Array - pages: Array + pages: Array } export function useLeaveConvo( @@ -33,22 +29,19 @@ export function useLeaveConvo( onError, }: { onMutate?: () => void - onSuccess?: (data: ChatBskyConvoLeaveConvo.OutputSchema) => void + onSuccess?: (data: chat.bsky.convo.leaveConvo.$OutputBody) => void onError?: (error: Error) => void }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() return useMutation({ mutationKey: RQKEY(convoId), mutationFn: async () => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.convo.leaveConvo( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + const data = await chatClient.call(chat.bsky.convo.leaveConvo, {convoId}) return data }, diff --git a/src/state/queries/messages/list-conversation-requests.tsx b/src/state/queries/messages/list-conversation-requests.tsx index 10023db4eb..92ba6a7bd0 100644 --- a/src/state/queries/messages/list-conversation-requests.tsx +++ b/src/state/queries/messages/list-conversation-requests.tsx @@ -1,16 +1,12 @@ -import { - ChatBskyConvoDefs, - type ChatBskyConvoListConvoRequests, - ChatBskyGroupDefs, -} from '@atproto/api' import { type InfiniteData, type QueryClient, useInfiniteQuery, } from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' +import * as bsky from '#/types/bsky' const DEFAULT_LIMIT = 10 @@ -26,17 +22,16 @@ export function useListConvoRequests({ enabled?: boolean limit?: number } = {}) { - const agent = useAgent() + const chatClient = useChatClient() return useInfiniteQuery({ enabled, queryKey: RQKEY(limit), queryFn: async ({pageParam}) => { - const {data} = await agent.chat.bsky.convo.listConvoRequests( - {limit, cursor: pageParam}, - {headers: DM_SERVICE_HEADERS}, - ) - return data + return await chatClient.call(chat.bsky.convo.listConvoRequests, { + limit, + cursor: pageParam, + }) }, initialPageParam: undefined as RQPageParam, getNextPageParam: lastPage => lastPage.cursor, @@ -45,16 +40,18 @@ export function useListConvoRequests({ export type ConvoRequestListQueryData = { pageParams: Array - pages: Array + pages: Array } export type ConvoRequestItem = - ChatBskyConvoListConvoRequests.OutputSchema['requests'][number] + chat.bsky.convo.listConvoRequests.$OutputBody['requests'][number] export function optimisticUpdate( chatId: string, old: ConvoRequestListQueryData | undefined, - updateFn: (convo: ChatBskyConvoDefs.ConvoView) => ChatBskyConvoDefs.ConvoView, + updateFn: ( + convo: chat.bsky.convo.defs.ConvoView, + ) => chat.bsky.convo.defs.ConvoView, ): ConvoRequestListQueryData | undefined { if (!old) return old @@ -63,7 +60,10 @@ export function optimisticUpdate( pages: old.pages.map(page => ({ ...page, requests: page.requests.map((item): ConvoRequestItem => { - if (ChatBskyConvoDefs.isConvoView(item) && item.id === chatId) { + if ( + bsky.isType(chat.bsky.convo.defs.convoView, item) && + item.id === chatId + ) { return { ...updateFn(item), $type: 'chat.bsky.convo.defs#convoView', @@ -86,7 +86,9 @@ export function optimisticDelete( pages: old.pages.map(page => ({ ...page, requests: page.requests.filter( - item => !ChatBskyConvoDefs.isConvoView(item) || item.id !== chatId, + item => + !bsky.isType(chat.bsky.convo.defs.convoView, item) || + item.id !== chatId, ), })), } @@ -102,7 +104,7 @@ export function markAllRead( pages: old.pages.map(page => ({ ...page, requests: page.requests.map((item): ConvoRequestItem => { - if (ChatBskyConvoDefs.isConvoView(item)) { + if (bsky.isType(chat.bsky.convo.defs.convoView, item)) { return { ...item, $type: 'chat.bsky.convo.defs#convoView', @@ -127,7 +129,7 @@ export function optimisticDeleteJoinRequest( ...page, requests: page.requests.filter( item => - !ChatBskyGroupDefs.isJoinRequestConvoView(item) || + !bsky.isType(chat.bsky.group.defs.joinRequestConvoView, item) || item.convoId !== convoId, ), })), @@ -139,7 +141,7 @@ export function* findAllProfilesInQueryData( did: string, ) { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) @@ -148,13 +150,15 @@ export function* findAllProfilesInQueryData( for (const page of queryData.pages) { for (const item of page.requests) { - if (ChatBskyConvoDefs.isConvoView(item)) { + if (bsky.isType(chat.bsky.convo.defs.convoView, item)) { for (const member of item.members) { if (member.did === did) { yield member } } - } else if (ChatBskyGroupDefs.isJoinRequestConvoView(item)) { + } else if ( + bsky.isType(chat.bsky.group.defs.joinRequestConvoView, item) + ) { if (item.owner.did === did) { yield item.owner } diff --git a/src/state/queries/messages/list-conversations.tsx b/src/state/queries/messages/list-conversations.tsx index 1f73c720bd..b8fb324b93 100644 --- a/src/state/queries/messages/list-conversations.tsx +++ b/src/state/queries/messages/list-conversations.tsx @@ -1,9 +1,4 @@ import {useCallback, useEffect, useMemo} from 'react' -import { - type ChatBskyActorDefs, - ChatBskyConvoDefs, - type ChatBskyConvoListConvos, -} from '@atproto/api' import { type InfiniteData, type Query, @@ -14,11 +9,11 @@ import { } from '@tanstack/react-query' import throttle from 'lodash.throttle' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {useCurrentConvoId} from '#/state/messages/current-convo-id' import {useMessagesEventBus} from '#/state/messages/events' import {invalidateJoinLinkPreviewsForConvo} from '#/state/queries/join-links' -import {useAgent, useSession} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' +import {chat} from '#/lexicons' import * as bsky from '#/types/bsky' import {RQKEY as CONVO_KEY} from './conversation' import { @@ -68,7 +63,7 @@ export const RQKEY_PARTIAL = ( * filters client-side or convos leak into lists that should exclude them. */ export function convoMatchesQueryKey( - convo: ChatBskyConvoDefs.ConvoView, + convo: chat.bsky.convo.defs.ConvoView, queryKey: QueryKey, ): boolean { const [, status, readState, kind, lockStatus] = queryKey as ReturnType< @@ -76,7 +71,7 @@ export function convoMatchesQueryKey( > if (status !== 'all' && status !== convo.status) return false if (readState === 'unread' && convo.unreadCount === 0) return false - if (ChatBskyConvoDefs.isGroupConvo(convo.kind)) { + if (bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) { if (kind === 'direct') return false if (lockStatus && convo.kind.lockStatus !== lockStatus) return false } else { @@ -94,7 +89,7 @@ export function convoMatchesQueryKey( * longer matches (e.g. unreadCount dropped to 0), mirroring how read/mute * log events update convos in place everywhere. */ -export function convoListQueryPredicate(convo: ChatBskyConvoDefs.ConvoView) { +export function convoListQueryPredicate(convo: chat.bsky.convo.defs.ConvoView) { return (query: Query): boolean => { const data = query.state.data as ConvoListQueryData | undefined if (data && getConvoFromQueryData(convo.id, data)) return true @@ -119,24 +114,20 @@ export function useListConvosQuery({ limit?: number lockStatus?: 'unlocked' | 'locked' | 'locked-permanently' } = {}) { - const agent = useAgent() + const chatClient = useChatClient() return useInfiniteQuery({ enabled, queryKey: RQKEY(status ?? 'all', readState, kind, lockStatus, limit), queryFn: async ({pageParam}) => { - const {data} = await agent.chat.bsky.convo.listConvos( - { - limit, - cursor: pageParam, - readState: readState === 'unread' ? 'unread' : undefined, - kind: kind === 'all' ? undefined : kind, - lockStatus, - status, - }, - {headers: DM_SERVICE_HEADERS}, - ) - return data + return await chatClient.call(chat.bsky.convo.listConvos, { + limit, + cursor: pageParam, + readState: readState === 'unread' ? 'unread' : undefined, + kind: kind === 'all' ? undefined : kind, + lockStatus, + status, + }) }, initialPageParam: undefined as RQPageParam, getNextPageParam: lastPage => lastPage.cursor, @@ -200,10 +191,10 @@ export function ListConvosProviderInner({ function mutateMembers( convoId: string, fn: ( - members: ChatBskyActorDefs.ProfileViewBasic[], - ) => ChatBskyActorDefs.ProfileViewBasic[], + members: chat.bsky.actor.defs.ProfileViewBasic[], + ) => chat.bsky.actor.defs.ProfileViewBasic[], ) { - queryClient.setQueryData( + queryClient.setQueryData( listConvoMembersQueryKey(convoId), old => { if (!old) return // query doesn't exist yet, skip @@ -215,8 +206,8 @@ export function ListConvosProviderInner({ function updateConvoInAllLists( convoId: string, fn: ( - convo: ChatBskyConvoDefs.ConvoView, - ) => ChatBskyConvoDefs.ConvoView, + convo: chat.bsky.convo.defs.ConvoView, + ) => chat.bsky.convo.defs.ConvoView, ) { queryClient.setQueriesData( {queryKey: [RQKEY_ROOT]}, @@ -231,10 +222,10 @@ export function ListConvosProviderInner({ function mutateConvoView( convoId: string, fn: ( - convo: ChatBskyConvoDefs.ConvoView, - ) => ChatBskyConvoDefs.ConvoView, + convo: chat.bsky.convo.defs.ConvoView, + ) => chat.bsky.convo.defs.ConvoView, ) { - queryClient.setQueryData( + queryClient.setQueryData( CONVO_KEY(convoId), old => (old ? fn(old) : old), ) @@ -255,7 +246,7 @@ export function ListConvosProviderInner({ function handleMemberAdded( convoId: string, did: string, - relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[], + relatedProfiles: chat.bsky.actor.defs.ProfileViewBasic[], rev: string, ) { const newMember = relatedProfiles.find(r => r.did === did) @@ -265,7 +256,7 @@ export function ListConvosProviderInner({ const alreadyKnownMember = queryClient .getQueryData< - ChatBskyActorDefs.ProfileViewBasic[] + chat.bsky.actor.defs.ProfileViewBasic[] >(listConvoMembersQueryKey(convoId)) ?.some(m => m.did === did) ?? false mutateMembers(convoId, list => @@ -289,7 +280,7 @@ export function ListConvosProviderInner({ const alreadyRemovedMember = queryClient .getQueryData< - ChatBskyActorDefs.ProfileViewBasic[] + chat.bsky.actor.defs.ProfileViewBasic[] >(listConvoMembersQueryKey(convoId)) ?.some(m => m.did === did) === false mutateMembers(convoId, list => list.filter(m => m.did !== did)) @@ -302,24 +293,36 @@ export function ListConvosProviderInner({ } for (const log of events.logs) { - if (ChatBskyConvoDefs.isLogBeginConvo(log)) { + if (bsky.isType(chat.bsky.convo.defs.logBeginConvo, log)) { debouncedRefetch() - } else if (ChatBskyConvoDefs.isLogLeaveConvo(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logLeaveConvo, log)) { deleteConvoFromAllLists(log.convoId) // The viewer is no longer in this convo (they left on another // device, or were removed - removed members receive a // logLeaveConvo, not a logRemoveMember). Refetch any cached join // link preview so its viewer state reflects the lost membership. void invalidateJoinLinkPreviewsForConvo(queryClient, log.convoId) - } else if (ChatBskyConvoDefs.isLogDeleteMessage(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logDeleteMessage, log)) { updateConvoInAllLists( log.convoId, withRevGuard(log.rev, convo => { if ( - (ChatBskyConvoDefs.isDeletedMessageView(log.message) || - ChatBskyConvoDefs.isMessageView(log.message)) && - (ChatBskyConvoDefs.isDeletedMessageView(convo.lastMessage) || - ChatBskyConvoDefs.isMessageView(convo.lastMessage)) + (bsky.isType( + chat.bsky.convo.defs.deletedMessageView, + log.message, + ) || + bsky.isType( + chat.bsky.convo.defs.messageView, + log.message, + )) && + (bsky.isType( + chat.bsky.convo.defs.deletedMessageView, + convo.lastMessage, + ) || + bsky.isType( + chat.bsky.convo.defs.messageView, + convo.lastMessage, + )) ) { return log.message.id === convo.lastMessage.id ? { @@ -333,9 +336,9 @@ export function ListConvosProviderInner({ } }), ) - } else if (ChatBskyConvoDefs.isLogCreateMessage(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logCreateMessage, log)) { // Store in a new var to avoid TS errors due to closures. - const logRef: ChatBskyConvoDefs.LogCreateMessage = log + const logRef: chat.bsky.convo.defs.LogCreateMessage = log // Get all matching queries const queries = queryClient.getQueriesData({ @@ -343,7 +346,7 @@ export function ListConvosProviderInner({ }) // Check if convo exists in any query - let foundConvo: ChatBskyConvoDefs.ConvoView | null = null + let foundConvo: chat.bsky.convo.defs.ConvoView | null = null for (const [_key, query] of queries) { if (!query) continue const convo = getConvoFromQueryData(logRef.convoId, query) @@ -386,15 +389,23 @@ export function ListConvosProviderInner({ lastMessage: logRef.message, unreadCount: foundConvo.id !== currentConvoId - ? (ChatBskyConvoDefs.isMessageView(logRef.message) || - ChatBskyConvoDefs.isDeletedMessageView(logRef.message)) && + ? (bsky.isType( + chat.bsky.convo.defs.messageView, + logRef.message, + ) || + bsky.isType( + chat.bsky.convo.defs.deletedMessageView, + logRef.message, + )) && logRef.message.sender.did !== currentAccount?.did ? foundConvo.unreadCount + 1 : foundConvo.unreadCount : 0, } - function filterConvoFromPage(convo: ChatBskyConvoDefs.ConvoView[]) { + function filterConvoFromPage( + convo: chat.bsky.convo.defs.ConvoView[], + ) { return convo.filter(c => c.id !== logRef.convoId) } @@ -457,7 +468,7 @@ export function ListConvosProviderInner({ old => moveConvoToTopInRequests(updatedConvo, old), ) } - } else if (ChatBskyConvoDefs.isLogReadMessage(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logReadMessage, log)) { updateConvoInAllLists( log.convoId, withRevGuard(log.rev, convo => ({ @@ -466,7 +477,7 @@ export function ListConvosProviderInner({ rev: log.rev, })), ) - } else if (ChatBskyConvoDefs.isLogReadConvo(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logReadConvo, log)) { updateConvoInAllLists( log.convoId, withRevGuard(log.rev, convo => ({ @@ -475,12 +486,12 @@ export function ListConvosProviderInner({ rev: log.rev, })), ) - } else if (ChatBskyConvoDefs.isLogAcceptConvo(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logAcceptConvo, log)) { const requestQueries = queryClient.getQueriesData({ queryKey: RQKEY_PARTIAL('request'), }) - let foundConvo: ChatBskyConvoDefs.ConvoView | null = null + let foundConvo: chat.bsky.convo.defs.ConvoView | null = null for (const [_key, data] of requestQueries) { if (!data) continue foundConvo = getConvoFromQueryData(log.convoId, data) @@ -496,7 +507,7 @@ export function ListConvosProviderInner({ if (log.rev <= foundConvo.rev) { continue } - const acceptedConvo: ChatBskyConvoDefs.ConvoView = { + const acceptedConvo: chat.bsky.convo.defs.ConvoView = { ...foundConvo, status: 'accepted', rev: log.rev, @@ -557,7 +568,7 @@ export function ListConvosProviderInner({ } }, ) - } else if (ChatBskyConvoDefs.isLogMuteConvo(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logMuteConvo, log)) { mutateConvoView( log.convoId, withRevGuard(log.rev, convo => ({ @@ -566,7 +577,7 @@ export function ListConvosProviderInner({ rev: log.rev, })), ) - } else if (ChatBskyConvoDefs.isLogUnmuteConvo(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logUnmuteConvo, log)) { mutateConvoView( log.convoId, withRevGuard(log.rev, convo => ({ @@ -575,11 +586,11 @@ export function ListConvosProviderInner({ rev: log.rev, })), ) - } else if (ChatBskyConvoDefs.isLogLockConvo(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logLockConvo, log)) { mutateConvoView( log.convoId, withRevGuard(log.rev, convo => { - if (ChatBskyConvoDefs.isGroupConvo(convo.kind)) { + if (bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) { return { ...convo, kind: {...convo.kind, lockStatus: 'locked'}, @@ -594,11 +605,11 @@ export function ListConvosProviderInner({ void queryClient.invalidateQueries({ queryKey: CONVO_KEY(log.convoId), }) - } else if (ChatBskyConvoDefs.isLogUnlockConvo(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logUnlockConvo, log)) { mutateConvoView( log.convoId, withRevGuard(log.rev, convo => { - if (ChatBskyConvoDefs.isGroupConvo(convo.kind)) { + if (bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) { return { ...convo, kind: { @@ -613,11 +624,13 @@ export function ListConvosProviderInner({ return {...convo, rev: log.rev} }), ) - } else if (ChatBskyConvoDefs.isLogLockConvoPermanently(log)) { + } else if ( + bsky.isType(chat.bsky.convo.defs.logLockConvoPermanently, log) + ) { mutateConvoView( log.convoId, withRevGuard(log.rev, convo => { - if (ChatBskyConvoDefs.isGroupConvo(convo.kind)) { + if (bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) { return { ...convo, kind: {...convo.kind, lockStatus: 'locked-permanently'}, @@ -628,20 +641,20 @@ export function ListConvosProviderInner({ }), ) } else if ( - ChatBskyConvoDefs.isLogCreateJoinLink(log) || - ChatBskyConvoDefs.isLogEditJoinLink(log) || - ChatBskyConvoDefs.isLogEnableJoinLink(log) || - ChatBskyConvoDefs.isLogDisableJoinLink(log) + bsky.isType(chat.bsky.convo.defs.logCreateJoinLink, log) || + bsky.isType(chat.bsky.convo.defs.logEditJoinLink, log) || + bsky.isType(chat.bsky.convo.defs.logEnableJoinLink, log) || + bsky.isType(chat.bsky.convo.defs.logDisableJoinLink, log) ) { // Join link data not included in the log event, trigger refetch to get it debouncedRefetch() - } else if (ChatBskyConvoDefs.isLogEditGroup(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logEditGroup, log)) { // Updated group details (name etc.) aren't included in the log // event, so refetch to pick them up. debouncedRefetch() } else if ( - ChatBskyConvoDefs.isLogApproveJoinRequest(log) || - ChatBskyConvoDefs.isLogRejectJoinRequest(log) + bsky.isType(chat.bsky.convo.defs.logApproveJoinRequest, log) || + bsky.isType(chat.bsky.convo.defs.logRejectJoinRequest, log) ) { // Route through mutateConvoView (not updateConvoInAllLists) so the // single-convo cache updates too, keeping the in-convo requests @@ -652,7 +665,9 @@ export function ListConvosProviderInner({ applyJoinRequestCountDelta(convo, log.rev, -1), ), ) - } else if (ChatBskyConvoDefs.isLogIncomingJoinRequest(log)) { + } else if ( + bsky.isType(chat.bsky.convo.defs.logIncomingJoinRequest, log) + ) { // Route through mutateConvoView (not updateConvoInAllLists) so the // single-convo cache updates too, letting the in-convo requests // banner appear live. @@ -662,14 +677,16 @@ export function ListConvosProviderInner({ applyJoinRequestCountDelta(convo, log.rev, 1), ), ) - } else if (ChatBskyConvoDefs.isLogReadJoinRequests(log)) { + } else if ( + bsky.isType(chat.bsky.convo.defs.logReadJoinRequests, log) + ) { // The owner marked join requests as read (possibly on another // device). Zero the unread count but keep the total, mirroring the // useMarkJoinRequestsRead mutation. mutateConvoView( log.convoId, withRevGuard(log.rev, convo => { - if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) { + if (!bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) { return {...convo, rev: log.rev} } return { @@ -679,11 +696,18 @@ export function ListConvosProviderInner({ } }), ) - } else if (ChatBskyConvoDefs.isLogOutgoingJoinRequest(log)) { + } else if ( + bsky.isType(chat.bsky.convo.defs.logOutgoingJoinRequest, log) + ) { // Viewer isn't in the chat yet, but the inbox surfaces outgoing // requests, so refetch to pick up the new entry. debouncedRefetch() - } else if (ChatBskyConvoDefs.isLogWithdrawIncomingJoinRequest(log)) { + } else if ( + bsky.isType( + chat.bsky.convo.defs.logWithdrawIncomingJoinRequest, + log, + ) + ) { // A requester rescinded their request to a group the viewer owns. // Mirror of isLogIncomingJoinRequest: decrement the counts. mutateConvoView( @@ -692,14 +716,19 @@ export function ListConvosProviderInner({ applyJoinRequestCountDelta(convo, log.rev, -1), ), ) - } else if (ChatBskyConvoDefs.isLogWithdrawOutgoingJoinRequest(log)) { + } else if ( + bsky.isType( + chat.bsky.convo.defs.logWithdrawOutgoingJoinRequest, + log, + ) + ) { // The viewer rescinded their own outgoing join request (possibly on // another device). Remove it from the requests inbox cache. queryClient.setQueriesData( {queryKey: [REQUESTS_RQKEY_ROOT]}, old => optimisticDeleteJoinRequest(log.convoId, old), ) - } else if (ChatBskyConvoDefs.isLogAddReaction(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logAddReaction, log)) { updateConvoInAllLists( log.convoId, withRevGuard(log.rev, convo => { @@ -713,22 +742,28 @@ export function ListConvosProviderInner({ return { ...convo, members: [...convo.members, ...relatedProfilesSansMembers], - lastReaction: { + /* + * `log.message` can also be a deleted-message view per the + * log union, which the strict MessageAndReactionView type + * rejects - the old types absorbed this via the open-union + * catch-all. Keep the pre-migration runtime behavior (always + * store the view we got) and assert the cache type. + */ + lastReaction: bsky.toLex< + NonNullable + >({ $type: 'chat.bsky.convo.defs#messageAndReactionView', reaction: log.reaction, message: log.message, - }, + }), rev: log.rev, } }), ) - } else if (ChatBskyConvoDefs.isLogAddMember(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logAddMember, log)) { const data = log.message.data if ( - bsky.dangerousIsType( - data, - ChatBskyConvoDefs.isSystemMessageDataAddMember, - ) + bsky.isType(chat.bsky.convo.defs.systemMessageDataAddMember, data) ) { handleMemberAdded( log.convoId, @@ -742,12 +777,12 @@ export function ListConvosProviderInner({ queryKey: CONVO_KEY(log.convoId), }) debouncedRefetch() - } else if (ChatBskyConvoDefs.isLogRemoveMember(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logRemoveMember, log)) { const data = log.message.data if ( - bsky.dangerousIsType( + bsky.isType( + chat.bsky.convo.defs.systemMessageDataRemoveMember, data, - ChatBskyConvoDefs.isSystemMessageDataRemoveMember, ) ) { handleMemberRemoved(log.convoId, data.member.did, log.rev) @@ -757,12 +792,12 @@ export function ListConvosProviderInner({ queryKey: CONVO_KEY(log.convoId), }) debouncedRefetch() - } else if (ChatBskyConvoDefs.isLogMemberJoin(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logMemberJoin, log)) { const data = log.message.data if ( - bsky.dangerousIsType( + bsky.isType( + chat.bsky.convo.defs.systemMessageDataMemberJoin, data, - ChatBskyConvoDefs.isSystemMessageDataMemberJoin, ) ) { handleMemberAdded( @@ -776,12 +811,12 @@ export function ListConvosProviderInner({ queryKey: CONVO_KEY(log.convoId), }) debouncedRefetch() - } else if (ChatBskyConvoDefs.isLogMemberLeave(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logMemberLeave, log)) { const data = log.message.data if ( - bsky.dangerousIsType( + bsky.isType( + chat.bsky.convo.defs.systemMessageDataMemberLeave, data, - ChatBskyConvoDefs.isSystemMessageDataMemberLeave, ) ) { handleMemberRemoved(log.convoId, data.member.did, log.rev) @@ -790,7 +825,7 @@ export function ListConvosProviderInner({ queryKey: CONVO_KEY(log.convoId), }) debouncedRefetch() - } else if (ChatBskyConvoDefs.isLogRemoveReaction(log)) { + } else if (bsky.isType(chat.bsky.convo.defs.logRemoveReaction, log)) { queryClient.setQueriesData( {queryKey: [RQKEY_ROOT]}, (old?: ConvoListQueryData) => @@ -801,10 +836,14 @@ export function ListConvosProviderInner({ if ( // if the convo is the same log.convoId === convo.id && - ChatBskyConvoDefs.isMessageAndReactionView( + bsky.isType( + chat.bsky.convo.defs.messageAndReactionView, convo.lastReaction, ) && - ChatBskyConvoDefs.isMessageView(log.message) && + bsky.isType( + chat.bsky.convo.defs.messageView, + log.message, + ) && // ...and the message is the same convo.lastReaction.message.id === log.message.id && // ...and the reaction is the same @@ -886,7 +925,7 @@ export function useUnreadMessageCount(): { export type ConvoListQueryData = { pageParams: Array - pages: Array + pages: Array } export function useOnMarkAsRead() { @@ -923,8 +962,8 @@ export function useOnMarkAsRead() { */ function withRevGuard( rev: string, - fn: (convo: ChatBskyConvoDefs.ConvoView) => ChatBskyConvoDefs.ConvoView, -): (convo: ChatBskyConvoDefs.ConvoView) => ChatBskyConvoDefs.ConvoView { + fn: (convo: chat.bsky.convo.defs.ConvoView) => chat.bsky.convo.defs.ConvoView, +): (convo: chat.bsky.convo.defs.ConvoView) => chat.bsky.convo.defs.ConvoView { return convo => (rev <= convo.rev ? convo : fn(convo)) } @@ -932,8 +971,8 @@ function optimisticUpdate( chatId: string, old?: ConvoListQueryData, updateFn?: ( - convo: ChatBskyConvoDefs.ConvoView, - ) => ChatBskyConvoDefs.ConvoView, + convo: chat.bsky.convo.defs.ConvoView, + ) => chat.bsky.convo.defs.ConvoView, ) { if (!old || !updateFn) return old @@ -949,12 +988,12 @@ function optimisticUpdate( } function applyJoinRequestCountDelta( - convo: ChatBskyConvoDefs.ConvoView, + convo: chat.bsky.convo.defs.ConvoView, rev: string, delta: 1 | -1, -): ChatBskyConvoDefs.ConvoView { +): chat.bsky.convo.defs.ConvoView { // Join requests are only meaningful for group convos. - if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) { + if (!bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) { return {...convo, rev} } // Bump the total and unread counts together. Both are clamped at 0 and @@ -975,7 +1014,7 @@ function applyJoinRequestCountDelta( } function moveConvoToTopInRequests( - updatedConvo: ChatBskyConvoDefs.ConvoView, + updatedConvo: chat.bsky.convo.defs.ConvoView, old: ConvoRequestListQueryData | undefined, ): ConvoRequestListQueryData | undefined { if (!old) return old @@ -989,7 +1028,8 @@ function moveConvoToTopInRequests( pages: old.pages.map((page, i) => { const filtered = page.requests.filter( item => - !ChatBskyConvoDefs.isConvoView(item) || item.id !== updatedConvo.id, + !bsky.isType(chat.bsky.convo.defs.convoView, item) || + item.id !== updatedConvo.id, ) if (i === 0) { return { @@ -1003,13 +1043,13 @@ function moveConvoToTopInRequests( } function removeMemberFromConvoView( - convo: ChatBskyConvoDefs.ConvoView, + convo: chat.bsky.convo.defs.ConvoView, did: string, rev: string, alreadyRemovedMember: boolean, -): ChatBskyConvoDefs.ConvoView { +): chat.bsky.convo.defs.ConvoView { // Member add/remove/join/leave events are only meaningful for group convos. - if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) return convo + if (!bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) return convo const nextMembers = convo.members.filter(m => m.did !== did) return { ...convo, @@ -1025,13 +1065,13 @@ function removeMemberFromConvoView( } function addMemberToConvoView( - convo: ChatBskyConvoDefs.ConvoView, - member: ChatBskyActorDefs.ProfileViewBasic, + convo: chat.bsky.convo.defs.ConvoView, + member: chat.bsky.actor.defs.ProfileViewBasic, rev: string, alreadyKnownMember: boolean, -): ChatBskyConvoDefs.ConvoView { +): chat.bsky.convo.defs.ConvoView { // Member add/remove/join/leave events are only meaningful for group convos. - if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) return convo + if (!bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) return convo const alreadyInCuratedList = convo.members.some(m => m.did === member.did) const nextMembers = alreadyInCuratedList ? convo.members @@ -1077,7 +1117,7 @@ export function* findAllProfilesInQueryData( did: string, ) { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/messages/list-convo-members.ts b/src/state/queries/messages/list-convo-members.ts index d2ff52dd5c..51997c18a2 100644 --- a/src/state/queries/messages/list-convo-members.ts +++ b/src/state/queries/messages/list-convo-members.ts @@ -1,10 +1,9 @@ -import {type ChatBskyActorDefs} from '@atproto/api' import {type QueryClient, useQuery} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {STALE} from '#/state/queries' import {createQueryKey} from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' const RQKEY_ROOT = 'listConvoMembers' export const listConvoMembersQueryKey = (convoId: string) => @@ -18,21 +17,22 @@ export function useListConvoMembersQuery({ placeholderData, }: { convoId: string - placeholderData?: ChatBskyActorDefs.ProfileViewBasic[] + placeholderData?: chat.bsky.actor.defs.ProfileViewBasic[] }) { - const agent = useAgent() + const chatClient = useChatClient() return useQuery({ queryKey: listConvoMembersQueryKey(convoId), queryFn: async () => { const members = [] - let cursor + let cursor: string | undefined do { - const {data} = await agent.chat.bsky.convo.getConvoMembers( - {convoId, cursor, limit: LIMIT}, - {headers: DM_SERVICE_HEADERS}, - ) + const data = await chatClient.call(chat.bsky.convo.getConvoMembers, { + convoId, + cursor, + limit: LIMIT, + }) members.push(...data.members) cursor = data.cursor } while (cursor) @@ -47,9 +47,9 @@ export function useListConvoMembersQuery({ export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< - ChatBskyActorDefs.ProfileViewBasic[] + chat.bsky.actor.defs.ProfileViewBasic[] >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/messages/list-join-requests.ts b/src/state/queries/messages/list-join-requests.ts index 64a999c118..c628370d0e 100644 --- a/src/state/queries/messages/list-join-requests.ts +++ b/src/state/queries/messages/list-join-requests.ts @@ -1,11 +1,11 @@ import {useEffect} from 'react' -import {ChatBskyConvoDefs} from '@atproto/api' import {useInfiniteQuery, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {useMessagesEventBus} from '#/state/messages/events' import {createQueryKey} from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' +import * as bsky from '#/types/bsky' import {STALE} from '..' export const JOIN_REQUESTS_THRESHOLD = 20 @@ -22,7 +22,7 @@ export function useListJoinRequestsQuery({ convoId: string | undefined enabled?: boolean }) { - const agent = useAgent() + const chatClient = useChatClient() const queryClient = useQueryClient() const messagesBus = useMessagesEventBus() const isEnabled = enabled !== false && !!convoId @@ -35,9 +35,9 @@ export function useListJoinRequestsQuery({ if (event.type !== 'logs') return for (const log of event.logs) { if ( - ChatBskyConvoDefs.isLogIncomingJoinRequest(log) || - ChatBskyConvoDefs.isLogApproveJoinRequest(log) || - ChatBskyConvoDefs.isLogRejectJoinRequest(log) + bsky.isType(chat.bsky.convo.defs.logIncomingJoinRequest, log) || + bsky.isType(chat.bsky.convo.defs.logApproveJoinRequest, log) || + bsky.isType(chat.bsky.convo.defs.logRejectJoinRequest, log) ) { void queryClient.invalidateQueries({ queryKey: createListJoinRequestsQueryKey({convoId}), @@ -54,11 +54,11 @@ export function useListJoinRequestsQuery({ enabled: isEnabled, queryKey: createListJoinRequestsQueryKey({convoId: convoId ?? ''}), queryFn: async ({pageParam}) => { - const {data} = await agent.chat.bsky.group.listJoinRequests( - {convoId: convoId!, cursor: pageParam, limit: JOIN_REQUESTS_THRESHOLD}, - {headers: DM_SERVICE_HEADERS}, - ) - return data + return await chatClient.call(chat.bsky.group.listJoinRequests, { + convoId: convoId!, + cursor: pageParam, + limit: JOIN_REQUESTS_THRESHOLD, + }) }, initialPageParam: undefined as string | undefined, getNextPageParam: page => page.cursor, diff --git a/src/state/queries/messages/list-mutual-groups.ts b/src/state/queries/messages/list-mutual-groups.ts index 9ff4a1da09..46039109a9 100644 --- a/src/state/queries/messages/list-mutual-groups.ts +++ b/src/state/queries/messages/list-mutual-groups.ts @@ -1,8 +1,9 @@ +import {type DidString} from '@atproto/syntax' import {useInfiniteQuery} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {createQueryKey} from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' const listMutualGroupsQueryKeyRoot = 'list-mutual-groups' @@ -18,7 +19,7 @@ export function useListMutualGroupsQuery({ enabled?: boolean limit?: number }) { - const agent = useAgent() + const chatClient = useChatClient() const isEnabled = enabled !== false && !!subject return useInfiniteQuery({ @@ -27,11 +28,11 @@ export function useListMutualGroupsQuery({ enabled: isEnabled, queryKey: createListMutualGroupsQueryKey({subject: subject ?? ''}), queryFn: async ({pageParam}) => { - const {data} = await agent.chat.bsky.group.listMutualGroups( - {subject: subject!, cursor: pageParam, limit}, - {headers: DM_SERVICE_HEADERS}, - ) - return data + return await chatClient.call(chat.bsky.group.listMutualGroups, { + subject: subject! as DidString, + cursor: pageParam, + limit, + }) }, initialPageParam: undefined as string | undefined, getNextPageParam: page => page.cursor, diff --git a/src/state/queries/messages/lock-conversation.ts b/src/state/queries/messages/lock-conversation.ts index 122c0633b4..64967be1e1 100644 --- a/src/state/queries/messages/lock-conversation.ts +++ b/src/state/queries/messages/lock-conversation.ts @@ -1,8 +1,8 @@ -import {ChatBskyConvoDefs, type ChatBskyConvoLockConvo} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' +import * as bsky from '#/types/bsky' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -15,7 +15,7 @@ export function useLockConvo( onError, }: { onSuccess?: ( - data: ChatBskyConvoLockConvo.OutputSchema, + data: chat.bsky.convo.lockConvo.$OutputBody, variables: {lock: boolean; silent?: boolean}, ) => void onError?: ( @@ -25,29 +25,26 @@ export function useLockConvo( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() return useMutation({ mutationFn: async ({lock}: {lock: boolean; silent?: boolean}) => { if (!convoId) throw new Error('No convoId provided') if (lock) { - const {data} = await agent.chat.bsky.convo.lockConvo( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + const data = await chatClient.call(chat.bsky.convo.lockConvo, {convoId}) return data } else { - const {data} = await agent.chat.bsky.convo.unlockConvo( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + const data = await chatClient.call(chat.bsky.convo.unlockConvo, { + convoId, + }) return data } }, onMutate: ({lock}) => { if (!convoId) return return updateConvoOptimistic(queryClient, convoId, prev => { - if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) return undefined + if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind)) + return undefined return { ...prev, kind: { diff --git a/src/state/queries/messages/mark-join-request-read.ts b/src/state/queries/messages/mark-join-request-read.ts index ae6d7e88a7..3b4930cf4e 100644 --- a/src/state/queries/messages/mark-join-request-read.ts +++ b/src/state/queries/messages/mark-join-request-read.ts @@ -1,9 +1,9 @@ -import {ChatBskyConvoDefs} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' +import * as bsky from '#/types/bsky' import {RQKEY as CONVO_KEY} from './conversation' import { type ConvoListQueryData, @@ -12,26 +12,25 @@ import { export function useMarkJoinRequestsRead(convoId: string | undefined) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() return useMutation({ mutationFn: async () => { if (!convoId) throw new Error('No convoId provided') - await agent.chat.bsky.group.updateJoinRequestsRead( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + await chatClient.call(chat.bsky.group.updateJoinRequestsRead, {convoId}) }, onMutate: () => { if (!convoId) return - const prevConvo = queryClient.getQueryData( - CONVO_KEY(convoId), - ) - queryClient.setQueryData( + const prevConvo = + queryClient.getQueryData( + CONVO_KEY(convoId), + ) + queryClient.setQueryData( CONVO_KEY(convoId), old => { - if (!old || !ChatBskyConvoDefs.isGroupConvo(old.kind)) return old + if (!old || !bsky.isType(chat.bsky.convo.defs.groupConvo, old.kind)) + return old return { ...old, kind: {...old.kind, unreadJoinRequestCount: 0}, @@ -53,7 +52,7 @@ export function useMarkJoinRequestsRead(convoId: string | undefined) { convos: page.convos.map(convo => { if ( convo.id !== convoId || - !ChatBskyConvoDefs.isGroupConvo(convo.kind) + !bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind) ) { return convo } diff --git a/src/state/queries/messages/mute-conversation.ts b/src/state/queries/messages/mute-conversation.ts index 03a9ab0b4a..e0a3ef28c5 100644 --- a/src/state/queries/messages/mute-conversation.ts +++ b/src/state/queries/messages/mute-conversation.ts @@ -1,8 +1,7 @@ -import {type ChatBskyConvoMuteConvo} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -14,27 +13,23 @@ export function useMuteConvo( onSuccess, onError, }: { - onSuccess?: (data: ChatBskyConvoMuteConvo.OutputSchema) => void + onSuccess?: (data: chat.bsky.convo.muteConvo.$OutputBody) => void onError?: (error: Error) => void }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() return useMutation({ mutationFn: async ({mute}: {mute: boolean}) => { if (!convoId) throw new Error('No convoId provided') if (mute) { - const {data} = await agent.chat.bsky.convo.muteConvo( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + const data = await chatClient.call(chat.bsky.convo.muteConvo, {convoId}) return data } else { - const {data} = await agent.chat.bsky.convo.unmuteConvo( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + const data = await chatClient.call(chat.bsky.convo.unmuteConvo, { + convoId, + }) return data } }, diff --git a/src/state/queries/messages/remove-from-group.ts b/src/state/queries/messages/remove-from-group.ts index 95f246be5a..dc4e7bd80f 100644 --- a/src/state/queries/messages/remove-from-group.ts +++ b/src/state/queries/messages/remove-from-group.ts @@ -1,18 +1,14 @@ -import { - type ChatBskyActorDefs, - ChatBskyConvoDefs, - type ChatBskyConvoListConvos, - type ChatBskyGroupRemoveMembers, -} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import { type InfiniteData, useMutation, useQueryClient, } from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' +import * as bsky from '#/types/bsky' import {RQKEY as CONVO_KEY} from './conversation' import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations' import {listConvoMembersQueryKey} from './list-convo-members' @@ -23,42 +19,43 @@ export function useRemoveFromGroupChat( onSuccess, onError, }: { - onSuccess?: (data: ChatBskyGroupRemoveMembers.OutputSchema) => void + onSuccess?: (data: chat.bsky.group.removeMembers.$OutputBody) => void onError?: (error: Error) => void }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() return useMutation({ mutationFn: async ({members}: {members: string[]}) => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.removeMembers( - {convoId, members}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + const data = await chatClient.call(chat.bsky.group.removeMembers, { + convoId, + members: members as DidString[], + }) return data }, onMutate: ({members}) => { if (!convoId) return - const prevConvo = queryClient.getQueryData( - CONVO_KEY(convoId), - ) + const prevConvo = + queryClient.getQueryData( + CONVO_KEY(convoId), + ) const prevListEntries = queryClient.getQueriesData< - InfiniteData + InfiniteData >({queryKey: [CONVO_LIST_KEY]}) const prevMemberList = queryClient.getQueryData< - ChatBskyActorDefs.ProfileViewBasic[] + chat.bsky.actor.defs.ProfileViewBasic[] >(listConvoMembersQueryKey(convoId)) - queryClient.setQueryData( + queryClient.setQueryData( CONVO_KEY(convoId), prev => { if (!prev) return const nextMembers = prev.members.filter(m => !members.includes(m.did)) const removed = prev.members.length - nextMembers.length - if (!ChatBskyConvoDefs.isGroupConvo(prev.kind)) { + if (!bsky.isType(chat.bsky.convo.defs.groupConvo, prev.kind)) { return {...prev, members: nextMembers} } return { @@ -73,7 +70,7 @@ export function useRemoveFromGroupChat( ) queryClient.setQueriesData< - InfiniteData + InfiniteData >({queryKey: [CONVO_LIST_KEY]}, prev => { if (!prev?.pages) return return { @@ -86,7 +83,7 @@ export function useRemoveFromGroupChat( m => !members.includes(m.did), ) const removed = convo.members.length - nextMembers.length - if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) { + if (!bsky.isType(chat.bsky.convo.defs.groupConvo, convo.kind)) { return {...convo, members: nextMembers} } return { @@ -102,7 +99,7 @@ export function useRemoveFromGroupChat( } }) - queryClient.setQueryData( + queryClient.setQueryData( listConvoMembersQueryKey(convoId), prev => { if (!prev) return diff --git a/src/state/queries/messages/request-join-group-chat.ts b/src/state/queries/messages/request-join-group-chat.ts index f4d5650346..4810fb76f9 100644 --- a/src/state/queries/messages/request-join-group-chat.ts +++ b/src/state/queries/messages/request-join-group-chat.ts @@ -1,19 +1,18 @@ -import {type ChatBskyGroupRequestJoin} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent, useSession} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' +import {chat} from '#/lexicons' import {RQKEY_ROOT as REQUESTS_RQKEY_ROOT} from './list-conversation-requests' export function useRequestJoinGroupChat({ onSuccess, onError, }: { - onSuccess?: (data: ChatBskyGroupRequestJoin.OutputSchema) => void + onSuccess?: (data: chat.bsky.group.requestJoin.$OutputBody) => void onError?: (error: Error) => void } = {}) { - const agent = useAgent() + const chatClient = useChatClient() const queryClient = useQueryClient() const {hasSession} = useSession() @@ -22,11 +21,8 @@ export function useRequestJoinGroupChat({ if (!hasSession) throw new Error('Must be logged in to join') if (!code) throw new Error('No invite code') - const res = await agent.chat.bsky.group.requestJoin( - {code}, - {headers: DM_SERVICE_HEADERS}, - ) - return res.data + const res = await chatClient.call(chat.bsky.group.requestJoin, {code}) + return res }, onSuccess: data => { void queryClient.invalidateQueries({queryKey: [REQUESTS_RQKEY_ROOT]}) diff --git a/src/state/queries/messages/restrictChatSettings.ts b/src/state/queries/messages/restrictChatSettings.ts index 9cb5ee5cf8..e6b2c48ee3 100644 --- a/src/state/queries/messages/restrictChatSettings.ts +++ b/src/state/queries/messages/restrictChatSettings.ts @@ -1,13 +1,15 @@ -import {type ChatBskyActorDeclaration} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import {networkRetry} from '#/lib/async/retry' import {logger} from '#/logger' import {type SessionAgent} from '#/state/session' +import {agentToLexClient} from '#/state/session/clients' import { getDidFromAgentSession, getOtherRequiredDataFromCache, setOtherRequiredDataActorDeclarationCache, } from '#/ageAssurance/data' +import {chat} from '#/lexicons' /** * Updates the chat actor declaration record to restrict who can contact the @@ -49,7 +51,7 @@ export async function restrictChatSettings({ ) } - const record: ChatBskyActorDeclaration.Main = { + const record: chat.bsky.actor.declaration.Main = { $type: 'chat.bsky.actor.declaration', allowIncoming: restrictIncoming ? 'none' @@ -67,13 +69,23 @@ export async function restrictChatSettings({ return } + /* + * Callers thread a bridge `SessionAgent` (session-core / birthdate); wrap it + * as an account lex `Client` so the record write goes through the lex path. + * The cast is safe: `agentToLexClient` only reads `did` and `fetchHandler`, + * both of which the base `Agent` provides - its `AtpAgent` parameter type is + * just narrower than it needs. TODO(phase4): take a Client directly once the + * bridge is removed. + */ + const client = agentToLexClient( + agent as unknown as Parameters[0], + ) + try { await networkRetry(3, () => - agent.com.atproto.repo.putRecord({ - repo: did, - collection: 'chat.bsky.actor.declaration', + client.put(chat.bsky.actor.declaration, record, { + repo: did as DidString, rkey: 'self', - record, }), ) // important, update local cache to avoid running this again diff --git a/src/state/queries/messages/update-all-read.ts b/src/state/queries/messages/update-all-read.ts index 6fbffc58cd..de741f3051 100644 --- a/src/state/queries/messages/update-all-read.ts +++ b/src/state/queries/messages/update-all-read.ts @@ -1,9 +1,8 @@ -import {type ChatBskyConvoGetUnreadCounts} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import {RQKEY_PARTIAL as UNREAD_COUNTS_PARTIAL_KEY} from './get-unread-counts' import { type ConvoRequestListQueryData, @@ -29,14 +28,13 @@ export function useUpdateAllRead( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const chatClient = useChatClient() return useMutation({ mutationFn: async () => { - const {data} = await agent.chat.bsky.convo.updateAllRead( - {status}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + const data = await chatClient.call(chat.bsky.convo.updateAllRead, { + status, + }) return data }, @@ -99,10 +97,12 @@ export function useUpdateAllRead( // zero out the badge count query that actually drives the unread badge, // since it's a separate server query that the list caches don't feed const prevUnreadCountsQueries = - queryClient.getQueriesData({ - queryKey: UNREAD_COUNTS_PARTIAL_KEY, - }) - queryClient.setQueriesData( + queryClient.getQueriesData( + { + queryKey: UNREAD_COUNTS_PARTIAL_KEY, + }, + ) + queryClient.setQueriesData( {queryKey: UNREAD_COUNTS_PARTIAL_KEY}, old => { if (!old) return old diff --git a/src/state/queries/messages/utils/convo-cache.ts b/src/state/queries/messages/utils/convo-cache.ts index 2c9e2b3f96..1157aee79c 100644 --- a/src/state/queries/messages/utils/convo-cache.ts +++ b/src/state/queries/messages/utils/convo-cache.ts @@ -1,24 +1,21 @@ -import { - type ChatBskyConvoDefs, - type ChatBskyConvoListConvos, -} from '@atproto/api' import { type InfiniteData, type QueryClient, type QueryKey, } from '@tanstack/react-query' +import {type chat} from '#/lexicons' import {RQKEY as CONVO_KEY} from '../conversation' import {RQKEY_ROOT as CONVO_LIST_KEY} from '../list-conversations' type ConvoUpdater = ( - prev: ChatBskyConvoDefs.ConvoView, -) => ChatBskyConvoDefs.ConvoView | undefined + prev: chat.bsky.convo.defs.ConvoView, +) => chat.bsky.convo.defs.ConvoView | undefined export type ConvoCacheSnapshot = { - prevConvo: ChatBskyConvoDefs.ConvoView | undefined + prevConvo: chat.bsky.convo.defs.ConvoView | undefined prevListEntries: Array< - [QueryKey, InfiniteData | undefined] + [QueryKey, InfiniteData | undefined] > } @@ -34,14 +31,14 @@ export function updateConvoOptimistic( convoId: string, updater: ConvoUpdater, ): ConvoCacheSnapshot { - const prevConvo = queryClient.getQueryData( + const prevConvo = queryClient.getQueryData( CONVO_KEY(convoId), ) const prevListEntries = queryClient.getQueriesData< - InfiniteData + InfiniteData >({queryKey: [CONVO_LIST_KEY]}) - queryClient.setQueryData( + queryClient.setQueryData( CONVO_KEY(convoId), prev => { if (!prev) return @@ -51,7 +48,7 @@ export function updateConvoOptimistic( ) queryClient.setQueriesData< - InfiniteData + InfiniteData >({queryKey: [CONVO_LIST_KEY]}, prev => { if (!prev?.pages) return return { diff --git a/src/state/queries/messages/withdraw-join-group-chat.ts b/src/state/queries/messages/withdraw-join-group-chat.ts index 2ede8c941d..eafbafaa8a 100644 --- a/src/state/queries/messages/withdraw-join-group-chat.ts +++ b/src/state/queries/messages/withdraw-join-group-chat.ts @@ -1,9 +1,8 @@ -import {type ChatBskyGroupWithdrawJoinRequest} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent, useSession} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' +import {chat} from '#/lexicons' import { type ConvoRequestListQueryData, optimisticDeleteJoinRequest, @@ -14,10 +13,10 @@ export function useWithdrawJoinGroupChatRequest({ onSuccess, onError, }: { - onSuccess?: (data: ChatBskyGroupWithdrawJoinRequest.OutputSchema) => void + onSuccess?: (data: chat.bsky.group.withdrawJoinRequest.$OutputBody) => void onError?: (error: Error) => void } = {}) { - const agent = useAgent() + const chatClient = useChatClient() const queryClient = useQueryClient() const {hasSession} = useSession() @@ -27,11 +26,10 @@ export function useWithdrawJoinGroupChatRequest({ throw new Error('Must be logged in to withdraw a join request') if (!convoId) throw new Error('No convoId provided') - const res = await agent.chat.bsky.group.withdrawJoinRequest( - {convoId}, - {headers: DM_SERVICE_HEADERS}, - ) - return res.data + const res = await chatClient.call(chat.bsky.group.withdrawJoinRequest, { + convoId, + }) + return res }, onSuccess: (data, {convoId}) => { queryClient.setQueriesData( diff --git a/src/state/queries/my-blocked-accounts.ts b/src/state/queries/my-blocked-accounts.ts index a2e29136f5..32f9d27833 100644 --- a/src/state/queries/my-blocked-accounts.ts +++ b/src/state/queries/my-blocked-accounts.ts @@ -1,4 +1,3 @@ -import {type AppBskyActorDefs, type AppBskyGraphGetBlocks} from '@atproto/api' import { type InfiniteData, type QueryClient, @@ -6,28 +5,28 @@ import { useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const RQKEY_ROOT = 'my-blocked-accounts' export const RQKEY = () => [RQKEY_ROOT] type RQPageParam = string | undefined export function useMyBlockedAccountsQuery() { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyGraphGetBlocks.OutputSchema, + app.bsky.graph.getBlocks.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.app.bsky.graph.getBlocks({ + return await client.call(app.bsky.graph.getBlocks, { limit: 30, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -37,9 +36,9 @@ export function useMyBlockedAccountsQuery() { export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/my-lists.ts b/src/state/queries/my-lists.ts index aeb9cf4568..e7a2a97587 100644 --- a/src/state/queries/my-lists.ts +++ b/src/state/queries/my-lists.ts @@ -1,9 +1,10 @@ -import {type AppBskyGraphDefs} from '@atproto/api' +import {type AtIdentifierString} from '@atproto/syntax' import {type QueryClient, useQuery} from '@tanstack/react-query' import {accumulate} from '#/lib/async/accumulate' import {STALE} from '#/state/queries' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' +import {app} from '#/lexicons' export type MyListsFilter = | 'all' @@ -16,50 +17,50 @@ export const RQKEY = (filter: MyListsFilter) => [RQKEY_ROOT, filter] export function useMyListsQuery(filter: MyListsFilter) { const {currentAccount} = useSession() - const agent = useAgent() - return useQuery({ + const client = useAppviewClient() + return useQuery({ staleTime: STALE.MINUTES.ONE, queryKey: RQKEY(filter), async queryFn() { - let lists: AppBskyGraphDefs.ListView[] = [] + let lists: app.bsky.graph.defs.ListView[] = [] const promises = [ accumulate(cursor => - agent.app.bsky.graph - .getLists({ - actor: currentAccount!.did, + client + .call(app.bsky.graph.getLists, { + actor: currentAccount!.did as AtIdentifierString, cursor, limit: 50, }) .then(res => ({ - cursor: res.data.cursor, - items: res.data.lists, + cursor: res.cursor, + items: res.lists, })), ), ] if (filter === 'all-including-subscribed' || filter === 'mod') { promises.push( accumulate(cursor => - agent.app.bsky.graph - .getListMutes({ + client + .call(app.bsky.graph.getListMutes, { cursor, limit: 50, }) .then(res => ({ - cursor: res.data.cursor, - items: res.data.lists, + cursor: res.cursor, + items: res.lists, })), ), ) promises.push( accumulate(cursor => - agent.app.bsky.graph - .getListBlocks({ + client + .call(app.bsky.graph.getListBlocks, { cursor, limit: 50, }) .then(res => ({ - cursor: res.data.cursor, - items: res.data.lists, + cursor: res.cursor, + items: res.lists, })), ), ) diff --git a/src/state/queries/my-muted-accounts.ts b/src/state/queries/my-muted-accounts.ts index bf36b90296..06f1ec1a85 100644 --- a/src/state/queries/my-muted-accounts.ts +++ b/src/state/queries/my-muted-accounts.ts @@ -1,4 +1,3 @@ -import {type AppBskyActorDefs, type AppBskyGraphGetMutes} from '@atproto/api' import { type InfiniteData, type QueryClient, @@ -6,28 +5,28 @@ import { useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const RQKEY_ROOT = 'my-muted-accounts' export const RQKEY = () => [RQKEY_ROOT] type RQPageParam = string | undefined export function useMyMutedAccountsQuery() { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyGraphGetMutes.OutputSchema, + app.bsky.graph.getMutes.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.app.bsky.graph.getMutes({ + return await client.call(app.bsky.graph.getMutes, { limit: 30, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -37,9 +36,9 @@ export function useMyMutedAccountsQuery() { export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index cc24d3d0fc..57b4d6c29b 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -17,12 +17,8 @@ */ import {useCallback, useEffect, useMemo, useRef} from 'react' -import { - AppBskyFeedDefs, - AppBskyFeedPost, - AtUri, - moderatePost, -} from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {moderatePost} from '@bsky.app/sdk/moderation' import { type InfiniteData, type QueryClient, @@ -33,9 +29,10 @@ import { import {useModerationOpts} from '#/state/preferences/moderation-opts' import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {useThreadgateHiddenReplyUris} from '#/state/threadgate-hidden-replies' -import type * as bsky from '#/types/bsky' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import { didOrHandleUriMatches, embedViewRecordToPostView, @@ -60,7 +57,7 @@ export function useNotificationFeedQuery(opts: { enabled?: boolean filter: 'all' | 'mentions' }) { - const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() const moderationOpts = useModerationOpts() const unreads = useUnreadNotificationsApi() @@ -106,7 +103,7 @@ export function useNotificationFeedQuery(opts: { ] } const {page: fetchedPage} = await fetchPage({ - agent, + client, limit: PAGE_SIZE, cursor: pageParam, queryClient, @@ -199,7 +196,9 @@ export function useNotificationFeedQuery(opts: { * a `$type` field on the `subject`. But if the nested * `record` is a post, we know it's a post view. */ - if (AppBskyFeedPost.isRecord(item.subject?.record)) { + if ( + bsky.isType(app.bsky.feed.post, item.subject?.record) + ) { const mod = moderatePost(item.subject, moderationOpts!) if (mod.ui('contentList').filter) { return false @@ -276,7 +275,7 @@ export function useNotificationFeedQuery(opts: { export function* findAllPostsInQueryData( queryClient: QueryClient, uri: string, -): Generator { +): Generator { const atUri = new AtUri(uri) const queryDatas = queryClient.getQueriesData>({ @@ -295,10 +294,14 @@ export function* findAllPostsInQueryData( } } - if (AppBskyFeedDefs.isPostView(item.subject)) { + if (bsky.isType(app.bsky.feed.defs.postView, item.subject)) { const quotedPost = getEmbeddedPost(item.subject?.embed) if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) { - yield embedViewRecordToPostView(quotedPost) + // TODO(phase4): drop toLex once ../util is migrated and + // embedViewRecordToPostView returns the lexicon PostView. + yield bsky.toLex( + embedViewRecordToPostView(quotedPost), + ) } } } @@ -333,7 +336,7 @@ export function* findAllProfilesInQueryData( ) { yield item.subject.author } - if (AppBskyFeedDefs.isPostView(item.subject)) { + if (bsky.isType(app.bsky.feed.defs.postView, item.subject)) { const quotedPost = getEmbeddedPost(item.subject?.embed) if (quotedPost?.author.did === did) { yield quotedPost.author diff --git a/src/state/queries/notifications/settings.ts b/src/state/queries/notifications/settings.ts index 86e8a6c7f1..002603411e 100644 --- a/src/state/queries/notifications/settings.ts +++ b/src/state/queries/notifications/settings.ts @@ -1,7 +1,3 @@ -import { - type AppBskyNotificationDefs, - type ChatBskyNotificationDefs, -} from '@atproto/api' import {t} from '@lingui/core/macro' import { type QueryClient, @@ -10,10 +6,10 @@ import { useQueryClient, } from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useAppviewClient, useChatClient} from '#/state/session' import * as Toast from '#/components/Toast' +import {app, chat} from '#/lexicons' const RQKEY_ROOT = 'notification-settings' const RQKEY_APP = [RQKEY_ROOT, 'app'] @@ -24,18 +20,18 @@ const RQKEY_CHAT = [RQKEY_ROOT, 'chat'] // fetched and cached separately. This combined type names every preference for // the generic settings dialog, but it is never the shape of a query response. export type NotificationSettingsPreferences = Omit< - AppBskyNotificationDefs.Preferences, + app.bsky.notification.defs.Preferences, 'chat' > & - Partial> + Partial> export type AppNotificationSettingsPreferences = Omit< - AppBskyNotificationDefs.Preferences, + app.bsky.notification.defs.Preferences, 'chat' > export type ChatNotificationSettingsPreferences = Pick< - ChatBskyNotificationDefs.Preferences, + chat.bsky.notification.defs.Preferences, 'chat' | 'chatRequest' > @@ -45,9 +41,9 @@ export type NotificationSettingsPreferenceName = Exclude< > export type NotificationSettingsPreference = - | AppBskyNotificationDefs.Preference - | AppBskyNotificationDefs.FilterablePreference - | ChatBskyNotificationDefs.ChatPreference + | app.bsky.notification.defs.Preference + | app.bsky.notification.defs.FilterablePreference + | chat.bsky.notification.defs.ChatPreference export function isChatPreferenceName( name: NotificationSettingsPreferenceName, @@ -58,7 +54,7 @@ export function isChatPreferenceName( type NotificationSettingsUpdate = Partial type AppNotificationSettingsUpdate = Partial< - Omit + Omit > type ChatNotificationSettingsUpdate = @@ -67,13 +63,13 @@ type ChatNotificationSettingsUpdate = export function useNotificationSettingsQuery({ enabled, }: {enabled?: boolean} = {}) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ queryKey: RQKEY_APP, queryFn: async (): Promise => { - const res = await agent.app.bsky.notification.getPreferences() - return appPreferencesWithoutChat(res.data.preferences) + const res = await client.call(app.bsky.notification.getPreferences) + return appPreferencesWithoutChat(res.preferences) }, enabled, }) @@ -82,21 +78,20 @@ export function useNotificationSettingsQuery({ export function useChatNotificationSettingsQuery({ enabled, }: {enabled?: boolean} = {}) { - const agent = useAgent() + const client = useChatClient() return useQuery({ queryKey: RQKEY_CHAT, queryFn: async (): Promise => { - const res = await agent.chat.bsky.notification.getPreferences(undefined, { - headers: DM_SERVICE_HEADERS, - }) - return chatPreferencesForSettings(res.data.preferences) + const res = await client.call(chat.bsky.notification.getPreferences) + return chatPreferencesForSettings(res.preferences) }, enabled, }) } export function useNotificationSettingsUpdateMutation() { - const agent = useAgent() + const appviewClient = useAppviewClient() + const chatClient = useChatClient() const queryClient = useQueryClient() return useMutation({ @@ -104,13 +99,13 @@ export function useNotificationSettingsUpdateMutation() { const {appUpdate, chatUpdate} = splitNotificationSettingsUpdate(update) await Promise.all([ hasUpdates(appUpdate) - ? agent.app.bsky.notification.putPreferencesV2(appUpdate) + ? appviewClient.call( + app.bsky.notification.putPreferencesV2, + appUpdate, + ) : undefined, hasUpdates(chatUpdate) - ? agent.chat.bsky.notification.putPreferences(chatUpdate, { - headers: DM_SERVICE_HEADERS, - encoding: 'application/json', - }) + ? chatClient.call(chat.bsky.notification.putPreferences, chatUpdate) : undefined, ]) }, @@ -156,15 +151,15 @@ function optimisticUpdateNotificationSettings( } function appPreferencesWithoutChat( - preferences: AppBskyNotificationDefs.Preferences, -): Omit { + preferences: app.bsky.notification.defs.Preferences, +): Omit { const {chat: _ignoredChat, ...appPreferences} = preferences return appPreferences } function chatPreferencesForSettings( - preferences: ChatBskyNotificationDefs.Preferences, -): Pick { + preferences: chat.bsky.notification.defs.Preferences, +): Pick { return { chat: preferences.chat, chatRequest: preferences.chatRequest, diff --git a/src/state/queries/notifications/types.ts b/src/state/queries/notifications/types.ts index 1b66dd1759..140db4e84b 100644 --- a/src/state/queries/notifications/types.ts +++ b/src/state/queries/notifications/types.ts @@ -1,8 +1,4 @@ -import { - type AppBskyFeedDefs, - type AppBskyGraphDefs, - type AppBskyNotificationListNotifications, -} from '@atproto/api' +import {type app} from '#/lexicons' export type NotificationType = | StarterPackNotificationType @@ -11,11 +7,11 @@ export type NotificationType = export type FeedNotification = | (FeedNotificationBase & { type: StarterPackNotificationType - subject?: AppBskyGraphDefs.StarterPackViewBasic + subject?: app.bsky.graph.defs.StarterPackViewBasic }) | (FeedNotificationBase & { type: OtherNotificationType - subject?: AppBskyFeedDefs.PostView + subject?: app.bsky.feed.defs.PostView }) export interface FeedPage { @@ -54,8 +50,10 @@ type OtherNotificationType = type FeedNotificationBase = { _reactKey: string - notification: AppBskyNotificationListNotifications.Notification - additional?: AppBskyNotificationListNotifications.Notification[] + notification: app.bsky.notification.listNotifications.Notification + additional?: app.bsky.notification.listNotifications.Notification[] subjectUri?: string - subject?: AppBskyFeedDefs.PostView | AppBskyGraphDefs.StarterPackViewBasic + subject?: + | app.bsky.feed.defs.PostView + | app.bsky.graph.defs.StarterPackViewBasic } diff --git a/src/state/queries/notifications/unread.tsx b/src/state/queries/notifications/unread.tsx index bf7505f91b..a21b7e4053 100644 --- a/src/state/queries/notifications/unread.tsx +++ b/src/state/queries/notifications/unread.tsx @@ -11,6 +11,8 @@ import { useState, } from 'react' import {AppState} from 'react-native' +import {type DatetimeString} from '@atproto/syntax' +import {updateSeenNotifications} from '@bsky.app/sdk' import {useQueryClient} from '@tanstack/react-query' import {EventEmitter} from 'eventemitter3' @@ -18,7 +20,7 @@ import BroadcastChannel from '#/lib/broadcast' import {resetBadgeCount} from '#/lib/notifications/notifications' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {truncateAndInvalidate} from '#/state/queries/util' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {RQKEY as RQKEY_NOTIFS} from './feed' import {type CachedFeedPage, type FeedPage} from './types' import {fetchPage} from './util' @@ -52,7 +54,8 @@ apiContext.displayName = 'NotificationsUnreadApiContext' export function Provider({children}: React.PropsWithChildren<{}>) { const {hasSession} = useSession() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const moderationOpts = useModerationOpts() @@ -120,8 +123,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) { return { async markAllRead() { // update server - await agent.updateSeenNotifications( - cacheRef.current.syncedAt.toISOString(), + await pdsClient.call( + updateSeenNotifications, + // toISOString() always yields a valid datetime string + cacheRef.current.syncedAt.toISOString() as DatetimeString, ) // update & broadcast @@ -135,7 +140,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { isPoll, }: {invalidate?: boolean; isPoll?: boolean} = {}) { try { - if (!agent.session) return + if (!hasSession) return if (AppState.currentState !== 'active') { return } @@ -156,7 +161,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { // count const {page, indexedAt: lastIndexed} = await fetchPage({ - agent, + client: appviewClient, cursor: undefined, limit: 40, queryClient, @@ -207,7 +212,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } }, } - }, [setNumUnread, queryClient, moderationOpts, agent]) + }, [ + setNumUnread, + queryClient, + moderationOpts, + appviewClient, + pdsClient, + hasSession, + ]) checkUnreadRef.current = api.checkUnread return ( diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts index 153d64eb68..e735583db9 100644 --- a/src/state/queries/notifications/util.ts +++ b/src/state/queries/notifications/util.ts @@ -1,20 +1,15 @@ +import {type Client} from '@atproto/lex-client' +import {type AtUriString} from '@atproto/syntax' import { - type AppBskyFeedDefs, - AppBskyFeedLike, - AppBskyFeedPost, - AppBskyFeedRepost, - type AppBskyGraphDefs, - AppBskyGraphStarterpack, - type AppBskyNotificationListNotifications, hasMutedWord, moderateNotification, type ModerationOpts, -} from '@atproto/api' +} from '@bsky.app/sdk/moderation' import {type QueryClient} from '@tanstack/react-query' import chunk from 'lodash.chunk' import {labelIsHideableOffense} from '#/lib/moderation' -import {type SessionAgent} from '#/state/session' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' import {precacheProfile} from '../profile' import { @@ -38,7 +33,7 @@ const MS_2DAY = MS_1HR * 48 // = export async function fetchPage({ - agent, + client, cursor, limit, queryClient, @@ -46,7 +41,7 @@ export async function fetchPage({ fetchAdditionalData, reasons, }: { - agent: SessionAgent + client: Client cursor: string | undefined limit: number queryClient: QueryClient @@ -57,16 +52,16 @@ export async function fetchPage({ page: FeedPage indexedAt: string | undefined }> { - const res = await agent.listNotifications({ + const res = await client.call(app.bsky.notification.listNotifications, { limit, cursor, reasons, }) - const indexedAt = res.data.notifications[0]?.indexedAt + const indexedAt = res.notifications[0]?.indexedAt // filter out notifs by mod rules - const notifs = res.data.notifications.filter( + const notifs = res.notifications.filter( notif => !shouldFilterNotif(notif, moderationOpts), ) @@ -76,7 +71,7 @@ export async function fetchPage({ // we fetch subjects of notifications (usually posts) now instead of lazily // in the UI to avoid relayouts if (fetchAdditionalData) { - const subjects = await fetchSubjects(agent, notifsGrouped) + const subjects = await fetchSubjects(client, notifsGrouped) for (const notif of notifsGrouped) { if (notif.subjectUri) { if ( @@ -96,17 +91,17 @@ export async function fetchPage({ } } - let seenAt = res.data.seenAt ? new Date(res.data.seenAt) : new Date() + let seenAt = res.seenAt ? new Date(res.seenAt) : new Date() if (Number.isNaN(seenAt.getTime())) { seenAt = new Date() } return { page: { - cursor: res.data.cursor, + cursor: res.cursor, seenAt, items: notifsGrouped, - priority: res.data.priority ?? false, + priority: res.priority ?? false, }, indexedAt, } @@ -116,7 +111,7 @@ export async function fetchPage({ // = export function shouldFilterNotif( - notif: AppBskyNotificationListNotifications.Notification, + notif: app.bsky.notification.listNotifications.Notification, moderationOpts: ModerationOpts | undefined, ): boolean { const containsImperative = !!notif.author.labels?.some(labelIsHideableOffense) @@ -128,10 +123,7 @@ export function shouldFilterNotif( } if ( notif.reason === 'subscribed-post' && - bsky.dangerousIsType( - notif.record, - AppBskyFeedPost.isRecord, - ) && + bsky.isType(app.bsky.feed.post, notif.record) && hasMutedWord({ mutedWords: moderationOpts.prefs.mutedWords, text: notif.record.text, @@ -150,7 +142,7 @@ export function shouldFilterNotif( } export function groupNotifications( - notifs: AppBskyNotificationListNotifications.Notification[], + notifs: app.bsky.notification.listNotifications.Notification[], ): FeedNotification[] { const groupedNotifs: FeedNotification[] = [] for (const notif of notifs) { @@ -207,17 +199,21 @@ export function groupNotifications( } async function fetchSubjects( - agent: SessionAgent, + client: Client, groupedNotifs: FeedNotification[], ): Promise<{ - posts: Map - starterPacks: Map + posts: Map + starterPacks: Map }> { - const postUris = new Set() - const packUris = new Set() + /* + * Subject/reason-subject URIs arrive as plain `string` on the notification, + * so brand them to the at-uri slot the getPosts/getStarterPacks params expect. + */ + const postUris = new Set() + const packUris = new Set() for (const notif of groupedNotifs) { if (notif.subjectUri?.includes('app.bsky.feed.post')) { - postUris.add(notif.subjectUri) + postUris.add(notif.subjectUri as AtUriString) } else if ( notif.notification.reasonSubject?.includes('app.bsky.graph.starterpack') ) { @@ -228,25 +224,25 @@ async function fetchSubjects( const packUriChunks = chunk(Array.from(packUris), 25) const postsChunks = await Promise.all( postUriChunks.map(uris => - agent.app.bsky.feed.getPosts({uris}).then(res => res.data.posts), + client.call(app.bsky.feed.getPosts, {uris}).then(res => res.posts), ), ) const packsChunks = await Promise.all( packUriChunks.map(uris => - agent.app.bsky.graph - .getStarterPacks({uris}) - .then(res => res.data.starterPacks), + client + .call(app.bsky.graph.getStarterPacks, {uris}) + .then(res => res.starterPacks), ), ) - const postsMap = new Map() - const packsMap = new Map() + const postsMap = new Map() + const packsMap = new Map() for (const post of postsChunks.flat()) { - if (AppBskyFeedPost.isRecord(post.record)) { + if (bsky.isType(app.bsky.feed.post, post.record)) { postsMap.set(post.uri, post) } } for (const pack of packsChunks.flat()) { - if (AppBskyGraphStarterpack.isRecord(pack.record)) { + if (bsky.isType(app.bsky.graph.starterpack, pack.record)) { packsMap.set(pack.uri, pack) } } @@ -257,7 +253,7 @@ async function fetchSubjects( } function toKnownType( - notif: AppBskyNotificationListNotifications.Notification, + notif: app.bsky.notification.listNotifications.Notification, ): NotificationType { if (notif.reason === 'like') { if (notif.reasonSubject?.includes('feed.generator')) { @@ -286,7 +282,7 @@ function toKnownType( function getSubjectUri( type: NotificationType, - notif: AppBskyNotificationListNotifications.Notification, + notif: app.bsky.notification.listNotifications.Notification, ): string | undefined { if ( type === 'reply' || @@ -302,14 +298,8 @@ function getSubjectUri( type === 'repost-via-repost' ) { if ( - bsky.dangerousIsType( - notif.record, - AppBskyFeedRepost.isRecord, - ) || - bsky.dangerousIsType( - notif.record, - AppBskyFeedLike.isRecord, - ) + bsky.isType(app.bsky.feed.repost, notif.record) || + bsky.isType(app.bsky.feed.like, notif.record) ) { return typeof notif.record.subject?.uri === 'string' ? notif.record.subject?.uri diff --git a/src/state/queries/nuxs/types.ts b/src/state/queries/nuxs/types.ts index 475bdd9429..db4bf5a7ae 100644 --- a/src/state/queries/nuxs/types.ts +++ b/src/state/queries/nuxs/types.ts @@ -1,7 +1,7 @@ -import {type AppBskyActorDefs} from '@atproto/api' +import {type app} from '#/lexicons' export type Data = Record | undefined export type BaseNux< - T extends Pick & {data: Data}, -> = Pick & T + T extends Pick & {data: Data}, +> = Pick & T diff --git a/src/state/queries/nuxs/util.ts b/src/state/queries/nuxs/util.ts index dea35a9166..973b46f0dc 100644 --- a/src/state/queries/nuxs/util.ts +++ b/src/state/queries/nuxs/util.ts @@ -1,4 +1,4 @@ -import {type AppBskyActorDefs, nuxSchema} from '@atproto/api' +import {nuxSchema} from '@bsky.app/sdk/utils' import { type AppNux, @@ -6,8 +6,9 @@ import { nuxNames, NuxSchemas, } from '#/state/queries/nuxs/definitions' +import {type app} from '#/lexicons' -export function parseAppNux(nux: AppBskyActorDefs.Nux): AppNux | undefined { +export function parseAppNux(nux: app.bsky.actor.defs.Nux): AppNux | undefined { if (!nuxNames.has(nux.id as Nux)) return if (!nuxSchema.safeParse(nux).success) return @@ -32,11 +33,11 @@ export function parseAppNux(nux: AppBskyActorDefs.Nux): AppNux | undefined { } as AppNux } -export function serializeAppNux(nux: AppNux): AppBskyActorDefs.Nux { +export function serializeAppNux(nux: AppNux): app.bsky.actor.defs.Nux { const {data, ...rest} = nux const schema = NuxSchemas[nux.id] - const result: AppBskyActorDefs.Nux = { + const result: app.bsky.actor.defs.Nux = { ...rest, data: undefined, } diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index a699310ab5..beccf10415 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -1,14 +1,12 @@ import {useCallback, useEffect, useMemo, useRef} from 'react' import {AppState} from 'react-native' +import {type AtIdentifierString, type Client} from '@atproto/lex-client' +import {AtUri, type AtUriString} from '@atproto/syntax' import { - type AppBskyActorDefs, - AppBskyFeedDefs, - type AppBskyFeedPost, - AtUri, moderatePost, type ModerationDecision, type ModerationPrefs, -} from '@atproto/api' +} from '@bsky.app/sdk/moderation' import { type InfiniteData, type QueryClient, @@ -32,9 +30,11 @@ import {DISCOVER_FEED_URI} from '#/lib/constants' import {logger} from '#/logger' import {STALE} from '#/state/queries' import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const' -import {type SessionAgent, useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import * as userActionHistory from '#/state/userActionHistory' import {KnownError} from '#/view/com/posts/PostFeedErrorMessage' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import {useFeedTuners} from '../preferences/feed-tuners' import {useModerationOpts} from '../preferences/moderation-opts' import {usePreferencesQuery} from './preferences' @@ -79,10 +79,10 @@ export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) { export interface FeedPostSliceItem { _reactKey: string uri: string - post: AppBskyFeedDefs.PostView - record: AppBskyFeedPost.Record + post: app.bsky.feed.defs.PostView + record: app.bsky.feed.post.Main moderation: ModerationDecision - parentAuthor?: AppBskyActorDefs.ProfileViewBasic + parentAuthor?: app.bsky.actor.defs.ProfileViewBasic isParentBlocked?: boolean isParentNotFound?: boolean } @@ -97,8 +97,8 @@ export interface FeedPostSlice { reqId: string | undefined feedPostUri: string reason?: - | AppBskyFeedDefs.ReasonRepost - | AppBskyFeedDefs.ReasonPin + | app.bsky.feed.defs.ReasonRepost + | app.bsky.feed.defs.ReasonPin | ReasonFeedSource | {[k: string]: unknown; $type: string} } @@ -106,7 +106,7 @@ export interface FeedPostSlice { export interface FeedPageUnselected { api: FeedAPI cursor: string | undefined - feed: AppBskyFeedDefs.FeedViewPost[] + feed: app.bsky.feed.defs.FeedViewPost[] fetchedAt: number } @@ -147,7 +147,7 @@ export function usePostFeedQuery( f => f.pinned && f.value === 'following', ) ?? -1 const enableFollowingToDiscoverFallback = followingPinnedIndex === 0 - const agent = useAgent() + const client = useAppviewClient() const lastRun = useRef<{ data: InfiniteData args: typeof selectArgs @@ -192,7 +192,7 @@ export function usePostFeedQuery( feedDesc, feedParams: params || {}, feedTuners, - agent, + client, // Not in the query key because they don't change: userInterests, // Not in the query key. Reacting to it switching isn't important: @@ -209,7 +209,7 @@ export function usePostFeedQuery( * moderations happen later, which results in some posts being shown and * some not. */ - if (!agent.session) { + if (!client.did) { assertSomePostsPassModeration( res.feed, preferences?.moderationPrefs || @@ -287,7 +287,12 @@ export function usePostFeedQuery( .tune(page.feed) .map(slice => { const moderations = slice.items.map(item => - moderatePost(item.post, moderationOpts!), + moderatePost( + // TODO(phase4): drop toLex once feed-manip is migrated + // off @atproto/api and yields lex-typed slice items. + bsky.toLex(item.post), + moderationOpts!, + ), ) // apply moderation filter @@ -337,10 +342,20 @@ export function usePostFeedQuery( const feedPostSliceItem: FeedPostSliceItem = { _reactKey: `${slice._reactKey}-${i}-${item.post.uri}`, uri: item.post.uri, - post: item.post, - record: item.record, + // TODO(phase4): drop toLex once feed-manip is migrated + // off @atproto/api and yields lex-typed slice items. + post: bsky.toLex( + item.post, + ), + record: bsky.toLex( + item.record, + ), moderation: moderations[i], - parentAuthor: item.parentAuthor, + parentAuthor: item.parentAuthor + ? bsky.toLex( + item.parentAuthor, + ) + : undefined, isParentBlocked: item.isParentBlocked, isParentNotFound: item.isParentNotFound, } @@ -442,62 +457,86 @@ function createApi({ feedParams, feedTuners, userInterests, - agent, + client, enableFollowingToDiscoverFallback, }: { feedDesc: FeedDescriptor feedParams: FeedParams feedTuners: FeedTunerFn[] userInterests?: string - agent: SessionAgent + client: Client enableFollowingToDiscoverFallback: boolean }) { if (feedDesc === 'following') { if (feedParams.mergeFeedEnabled) { return new MergeFeedAPI({ - agent, + client, feedParams, feedTuners, userInterests, }) } else { if (enableFollowingToDiscoverFallback) { - return new HomeFeedAPI({agent, userInterests}) + return new HomeFeedAPI({client, userInterests}) } else { - return new FollowingFeedAPI({agent}) + return new FollowingFeedAPI({client}) } } } else if (feedDesc.startsWith('author')) { const [__, actor, filter] = feedDesc.split('|') - return new AuthorFeedAPI({agent, feedParams: {actor, filter}}) + /* + * The FeedAPI $Params types treat limit/includePins as required (post-parse + * shape), but they are supplied at fetch() time, so the constructor receives + * a partial. Cast to satisfy the constructor param. + */ + return new AuthorFeedAPI({ + client, + feedParams: { + actor: actor as AtIdentifierString, + filter, + } as app.bsky.feed.getAuthorFeed.$Params, + }) } else if (feedDesc.startsWith('likes')) { const [__, actor] = feedDesc.split('|') - return new LikesFeedAPI({agent, feedParams: {actor}}) + return new LikesFeedAPI({ + client, + feedParams: { + actor: actor as AtIdentifierString, + } as app.bsky.feed.getActorLikes.$Params, + }) } else if (feedDesc.startsWith('feedgen')) { const [__, feed] = feedDesc.split('|') return new CustomFeedAPI({ - agent, - feedParams: {feed}, + client, + feedParams: {feed: feed as AtUriString}, userInterests, }) } else if (feedDesc.startsWith('list')) { const [__, list] = feedDesc.split('|') - return new ListFeedAPI({agent, feedParams: {list}}) + return new ListFeedAPI({ + client, + feedParams: { + list: list as AtUriString, + } as app.bsky.feed.getListFeed.$Params, + }) } else if (feedDesc.startsWith('posts')) { const [__, uriList] = feedDesc.split('|') - return new PostListFeedAPI({agent, feedParams: {uris: uriList.split(',')}}) + return new PostListFeedAPI({ + client, + feedParams: {uris: uriList.split(',') as AtUriString[]}, + }) } else if (feedDesc === 'demo') { - return new DemoFeedAPI({agent}) + return new DemoFeedAPI({client}) } else { // shouldnt happen - return new FollowingFeedAPI({agent}) + return new FollowingFeedAPI({client}) } } export function* findAllPostsInQueryData( queryClient: QueryClient, uri: string, -): Generator { +): Generator { const atUri = new AtUri(uri) const queryDatas = queryClient.getQueriesData< @@ -520,7 +559,7 @@ export function* findAllPostsInQueryData( yield embedViewRecordToPostView(quotedPost) } - if (AppBskyFeedDefs.isPostView(item.reply?.parent)) { + if (bsky.isType(app.bsky.feed.defs.postView, item.reply?.parent)) { if (didOrHandleUriMatches(atUri, item.reply.parent)) { yield item.reply.parent } @@ -534,7 +573,7 @@ export function* findAllPostsInQueryData( } } - if (AppBskyFeedDefs.isPostView(item.reply?.root)) { + if (bsky.isType(app.bsky.feed.defs.postView, item.reply?.root)) { if (didOrHandleUriMatches(atUri, item.reply.root)) { yield item.reply.root } @@ -552,7 +591,7 @@ export function* findAllPostsInQueryData( export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< InfiniteData >({ @@ -572,13 +611,13 @@ export function* findAllProfilesInQueryData( yield quotedPost.author } if ( - AppBskyFeedDefs.isPostView(item.reply?.parent) && + bsky.isType(app.bsky.feed.defs.postView, item.reply?.parent) && item.reply?.parent?.author.did === did ) { yield item.reply.parent.author } if ( - AppBskyFeedDefs.isPostView(item.reply?.root) && + bsky.isType(app.bsky.feed.defs.postView, item.reply?.root) && item.reply?.root?.author.did === did ) { yield item.reply.root.author @@ -589,7 +628,7 @@ export function* findAllProfilesInQueryData( } function assertSomePostsPassModeration( - feed: AppBskyFeedDefs.FeedViewPost[], + feed: app.bsky.feed.defs.FeedViewPost[], moderationPrefs: ModerationPrefs, ) { // no posts in this feed diff --git a/src/state/queries/post-interaction-settings.ts b/src/state/queries/post-interaction-settings.ts index af178d7f8b..2b660b3501 100644 --- a/src/state/queries/post-interaction-settings.ts +++ b/src/state/queries/post-interaction-settings.ts @@ -1,8 +1,9 @@ -import {type AppBskyActorDefs} from '@atproto/api' +import {setPostInteractionSettings} from '@bsky.app/sdk' import {useMutation, useQueryClient} from '@tanstack/react-query' import {preferencesQueryKey} from '#/state/queries/preferences' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' +import {app} from '#/lexicons' export function usePostInteractionSettingsMutation({ onError, @@ -12,10 +13,10 @@ export function usePostInteractionSettingsMutation({ onSettled?: () => void } = {}) { const qc = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation({ - async mutationFn(props: AppBskyActorDefs.PostInteractionSettingsPref) { - await agent.setPostInteractionSettings(props) + async mutationFn(props: app.bsky.actor.defs.PostInteractionSettingsPref) { + await client.call(setPostInteractionSettings, props) }, async onSuccess() { await qc.invalidateQueries({ diff --git a/src/state/queries/post-liked-by.ts b/src/state/queries/post-liked-by.ts index e4f37c14ca..fbf8c69d28 100644 --- a/src/state/queries/post-liked-by.ts +++ b/src/state/queries/post-liked-by.ts @@ -1,4 +1,4 @@ -import {type AppBskyActorDefs, type AppBskyFeedGetLikes} from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -9,7 +9,8 @@ import { import {STALE} from '#/state/queries' import {createQueryKey} from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const PAGE_SIZE = 30 type RQPageParam = string | undefined @@ -19,22 +20,21 @@ const RQKEY_ROOT = 'liked-by' export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri] export function useLikedByQuery(resolvedUri: string | undefined) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyFeedGetLikes.OutputSchema, + app.bsky.feed.getLikes.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(resolvedUri || ''), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.getLikes({ - uri: resolvedUri || '', + return await client.call(app.bsky.feed.getLikes, { + uri: (resolvedUri || '') as AtUriString, limit: PAGE_SIZE, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -59,12 +59,14 @@ export const createLikedBySampleQueryKey = (args: {uri: string}) => * perturb the liked-by screen's pagination. */ export function useLikedBySampleQuery({uri}: {uri: string | undefined}) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ queryKey: createLikedBySampleQueryKey({uri: uri ?? ''}), queryFn: async () => { - const res = await agent.getLikes({uri: uri ?? '', limit: SAMPLE_SIZE}) - return res.data + return await client.call(app.bsky.feed.getLikes, { + uri: (uri ?? '') as AtUriString, + limit: SAMPLE_SIZE, + }) }, staleTime: STALE.MINUTES.FIVE, enabled: !!uri, @@ -79,9 +81,9 @@ export function useLikedBySampleQuery({uri}: {uri: string | undefined}) { export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) @@ -98,7 +100,7 @@ export function* findAllProfilesInQueryData( } } const sampleQueryDatas = - queryClient.getQueriesData({ + queryClient.getQueriesData({ queryKey: [likedBySampleQueryKeyRoot], }) for (const [_queryKey, queryData] of sampleQueryDatas) { diff --git a/src/state/queries/post-quotes.ts b/src/state/queries/post-quotes.ts index 1d0fa07e8e..4eb455d830 100644 --- a/src/state/queries/post-quotes.ts +++ b/src/state/queries/post-quotes.ts @@ -1,10 +1,4 @@ -import { - type AppBskyActorDefs, - AppBskyEmbedRecord, - type AppBskyFeedDefs, - type AppBskyFeedGetQuotes, - AtUri, -} from '@atproto/api' +import {AtUri, type AtUriString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -12,7 +6,9 @@ import { useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import { didOrHandleUriMatches, embedViewRecordToPostView, @@ -26,22 +22,21 @@ const RQKEY_ROOT = 'post-quotes' export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri] export function usePostQuotesQuery(resolvedUri: string | undefined) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyFeedGetQuotes.OutputSchema, + app.bsky.feed.getQuotes.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(resolvedUri || ''), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.api.app.bsky.feed.getQuotes({ - uri: resolvedUri || '', + return await client.call(app.bsky.feed.getQuotes, { + uri: (resolvedUri || '') as AtUriString, limit: PAGE_SIZE, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -53,8 +48,16 @@ export function usePostQuotesQuery(resolvedUri: string | undefined) { return { ...page, posts: page.posts.filter(post => { - if (post.embed && AppBskyEmbedRecord.isView(post.embed)) { - if (AppBskyEmbedRecord.isViewDetached(post.embed.record)) { + if ( + post.embed && + bsky.isType(app.bsky.embed.record.view, post.embed) + ) { + if ( + bsky.isType( + app.bsky.embed.record.viewDetached, + post.embed.record, + ) + ) { return false } } @@ -70,9 +73,9 @@ export function usePostQuotesQuery(resolvedUri: string | undefined) { export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) @@ -97,9 +100,9 @@ export function* findAllProfilesInQueryData( export function* findAllPostsInQueryData( queryClient: QueryClient, uri: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/post-reposted-by.ts b/src/state/queries/post-reposted-by.ts index 814a815aae..e7aad8353a 100644 --- a/src/state/queries/post-reposted-by.ts +++ b/src/state/queries/post-reposted-by.ts @@ -1,7 +1,4 @@ -import { - type AppBskyActorDefs, - type AppBskyFeedGetRepostedBy, -} from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -9,7 +6,8 @@ import { useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const PAGE_SIZE = 30 type RQPageParam = string | undefined @@ -19,22 +17,21 @@ const RQKEY_ROOT = 'post-reposted-by' export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri] export function usePostRepostedByQuery(resolvedUri: string | undefined) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyFeedGetRepostedBy.OutputSchema, + app.bsky.feed.getRepostedBy.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(resolvedUri || ''), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.getRepostedBy({ - uri: resolvedUri || '', + return await client.call(app.bsky.feed.getRepostedBy, { + uri: (resolvedUri || '') as AtUriString, limit: PAGE_SIZE, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -45,9 +42,9 @@ export function usePostRepostedByQuery(resolvedUri: string | undefined) { export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index 79df51d1bf..fc23844069 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -1,5 +1,6 @@ import {useCallback} from 'react' -import {type AppBskyActorDefs, type AppBskyFeedDefs, AtUri} from '@atproto/api' +import {AtUri, type AtUriString, type HandleString} from '@atproto/syntax' +import {deleteLike, deletePost, deleteRepost, like, repost} from '@bsky.app/sdk' import { type QueryClient, useMutation, @@ -10,10 +11,11 @@ import { import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue' import {updatePostShadow} from '#/state/cache/post-shadow' import {type Shadow} from '#/state/cache/types' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import * as userActionHistory from '#/state/userActionHistory' import {useAnalytics} from '#/analytics' import {type Metrics, toClout} from '#/analytics/metrics' +import {app, com} from '#/lexicons' import {useIsThreadMuted, useSetThreadMute} from '../cache/thread-mutes' import {findProfileQueryData} from './profile' @@ -21,8 +23,8 @@ const RQKEY_ROOT = 'post' export const RQKEY = (postUri: string) => [RQKEY_ROOT, postUri] export function usePostQuery(uri: string | undefined) { - const agent = useAgent() - return useQuery({ + const client = useAppviewClient() + return useQuery({ queryKey: RQKEY(uri || ''), queryFn: async () => { if (!uri) throw new Error('[unreachable] No URI provided') @@ -30,16 +32,17 @@ export function usePostQuery(uri: string | undefined) { const urip = new AtUri(uri) if (!urip.host.startsWith('did:')) { - const res = await agent.resolveHandle({ - handle: urip.host, + const {did} = await client.call(com.atproto.identity.resolveHandle, { + handle: urip.host as HandleString, }) - // @ts-expect-error TODO new-sdk-migration - urip.host = res.data.did + urip.host = did } - const res = await agent.getPosts({uris: [urip.toString()]}) - if (res.success && res.data.posts[0]) { - return res.data.posts[0] + const data = await client.call(app.bsky.feed.getPosts, { + uris: [urip.toString()], + }) + if (data.posts[0]) { + return data.posts[0] } throw new Error('No data') @@ -51,14 +54,14 @@ export function usePostQuery(uri: string | undefined) { export function precachePost( queryClient: QueryClient, uri: string, - post: AppBskyFeedDefs.PostView, + post: app.bsky.feed.defs.PostView, ) { queryClient.setQueryData(RQKEY(uri), post) } export function useGetPost() { const queryClient = useQueryClient() - const agent = useAgent() + const client = useAppviewClient() return useCallback( async ({uri}: {uri: string}) => { return queryClient.fetchQuery({ @@ -67,55 +70,51 @@ export function useGetPost() { const urip = new AtUri(uri) if (!urip.host.startsWith('did:')) { - const res = await agent.resolveHandle({ - handle: urip.host, - }) - // @ts-expect-error TODO new-sdk-migration - urip.host = res.data.did + const {did} = await client.call( + com.atproto.identity.resolveHandle, + {handle: urip.host as HandleString}, + ) + urip.host = did } - const res = await agent.getPosts({ + const data = await client.call(app.bsky.feed.getPosts, { uris: [urip.toString()], }) - if (res.success && res.data.posts[0]) { - return res.data.posts[0] + if (data.posts[0]) { + return data.posts[0] } throw new Error('useGetPost: post not found') }, }) }, - [queryClient, agent], + [queryClient, client], ) } export function useGetPosts() { const queryClient = useQueryClient() - const agent = useAgent() + const client = useAppviewClient() return useCallback( async ({uris}: {uris: string[]}) => { return queryClient.fetchQuery({ queryKey: RQKEY(uris.join(',') || ''), async queryFn() { - const res = await agent.getPosts({ - uris, + const data = await client.call(app.bsky.feed.getPosts, { + uris: uris as AtUriString[], }) - if (res.success) { - return res.data.posts - } else { - throw new Error('useGetPosts failed') - } + return data.posts }, }) }, - [queryClient, agent], + [queryClient, client], ) } export function usePostLikeMutationQueue( - post: Shadow, + post: Shadow, viaRepost: {uri: string; cid: string} | undefined, feedDescriptor: string | undefined, logContext: Metrics['post:like']['logContext'], @@ -179,20 +178,20 @@ export function usePostLikeMutationQueue( function usePostLikeMutation( feedDescriptor: string | undefined, logContext: Metrics['post:like']['logContext'], - post: Shadow, + post: Shadow, ) { const {currentAccount} = useSession() const queryClient = useQueryClient() const postAuthor = post.author - const agent = useAgent() + 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 >({ mutationFn: ({uri, cid, via}) => { - let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined + let ownProfile: app.bsky.actor.defs.ProfileViewDetailed | undefined if (currentAccount) { ownProfile = findProfileQueryData(queryClient, currentAccount.did) } @@ -215,7 +214,11 @@ function usePostLikeMutation( : undefined, feedDescriptor: feedDescriptor, }) - return agent.like(uri, cid, via) + return pdsClient.call(like, { + uri: uri as AtUriString, + cid: cid, + via: via ? {uri: via.uri as AtUriString, cid: via.cid} : undefined, + }) }, }) } @@ -223,9 +226,9 @@ function usePostLikeMutation( function usePostUnlikeMutation( feedDescriptor: string | undefined, logContext: Metrics['post:unlike']['logContext'], - post: Shadow, + post: Shadow, ) { - const agent = useAgent() + const pdsClient = usePdsClient() const ax = useAnalytics() return useMutation({ mutationFn: ({postUri, likeUri}) => { @@ -235,13 +238,13 @@ function usePostUnlikeMutation( logContext, feedDescriptor, }) - return agent.deleteLike(likeUri) + return pdsClient.call(deleteLike, likeUri as AtUriString) }, }) } export function usePostRepostMutationQueue( - post: Shadow, + post: Shadow, viaRepost: {uri: string; cid: string} | undefined, feedDescriptor: string | undefined, logContext: Metrics['post:repost']['logContext'], @@ -307,12 +310,12 @@ export function usePostRepostMutationQueue( function usePostRepostMutation( feedDescriptor: string | undefined, logContext: Metrics['post:repost']['logContext'], - post: Shadow, + post: Shadow, ) { - const agent = useAgent() + 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 >({ @@ -323,7 +326,11 @@ function usePostRepostMutation( logContext, feedDescriptor, }) - return agent.repost(uri, cid, via) + return pdsClient.call(repost, { + uri: uri as AtUriString, + cid: cid, + via: via ? {uri: via.uri as AtUriString, cid: via.cid} : undefined, + }) }, }) } @@ -331,9 +338,9 @@ function usePostRepostMutation( function usePostUnrepostMutation( feedDescriptor: string | undefined, logContext: Metrics['post:unrepost']['logContext'], - post: Shadow, + post: Shadow, ) { - const agent = useAgent() + const pdsClient = usePdsClient() const ax = useAnalytics() return useMutation({ mutationFn: ({postUri, repostUri}) => { @@ -343,17 +350,17 @@ function usePostUnrepostMutation( logContext, feedDescriptor, }) - return agent.deleteRepost(repostUri) + return pdsClient.call(deleteRepost, repostUri as AtUriString) }, }) } export function usePostDeleteMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async ({uri}) => { - await agent.deletePost(uri) + await pdsClient.call(deletePost, uri as AtUriString) }, onSuccess(_, variables) { updatePostShadow(queryClient, variables.uri, {isDeleted: true}) @@ -362,7 +369,7 @@ export function usePostDeleteMutation() { } export function useThreadMuteMutationQueue( - post: Shadow, + post: Shadow, rootUri: string, ) { const threadMuteMutation = useThreadMuteMutation() @@ -407,23 +414,29 @@ export function useThreadMuteMutationQueue( } function useThreadMuteMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation< {}, Error, {uri: string} // the root post's uri >({ - mutationFn: ({uri}) => { - return agent.api.app.bsky.graph.muteThread({root: uri}) + mutationFn: async ({uri}) => { + await pdsClient.call(app.bsky.graph.muteThread, { + root: uri as AtUriString, + }) + return {} }, }) } function useThreadUnmuteMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation<{}, Error, {uri: string}>({ - mutationFn: ({uri}) => { - return agent.api.app.bsky.graph.unmuteThread({root: uri}) + mutationFn: async ({uri}) => { + await pdsClient.call(app.bsky.graph.unmuteThread, { + root: uri as AtUriString, + }) + return {} }, }) } diff --git a/src/state/queries/postgate/index.ts b/src/state/queries/postgate/index.ts index f8b1cf624a..780cfbfecd 100644 --- a/src/state/queries/postgate/index.ts +++ b/src/state/queries/postgate/index.ts @@ -1,11 +1,6 @@ import {useRef} from 'react' -import { - AppBskyEmbedRecord, - AppBskyEmbedRecordWithMedia, - type AppBskyFeedDefs, - AppBskyFeedPostgate, - AtUri, -} from '@atproto/api' +import {type Client} from '@atproto/lex-client' +import {AtUri} from '@atproto/syntax' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {networkRetry, retry} from '#/lib/async/retry' @@ -19,28 +14,28 @@ import { mergePostgateRecords, POSTGATE_COLLECTION, } from '#/state/queries/postgate/util' -import {type SessionAgent, useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' +import {app, com} from '#/lexicons' import * as bsky from '#/types/bsky' export async function getPostgateRecord({ - agent, + pdsClient, postUri, }: { - agent: SessionAgent + pdsClient: Client postUri: string -}): Promise { +}): Promise { const urip = new AtUri(postUri) if (!urip.host.startsWith('did:')) { - const res = await agent.resolveHandle({ - handle: urip.host, + const {did} = await pdsClient.call(com.atproto.identity.resolveHandle, { + handle: urip.host as `${string}.${string}`, }) - // @ts-expect-error TODO new-sdk-migration - urip.host = res.data.did + urip.host = did } try { - const {data} = await retry( + const data = await retry( 2, e => { /* @@ -54,17 +49,14 @@ export async function getPostgateRecord({ return true }, () => - agent.api.com.atproto.repo.getRecord({ + pdsClient.call(com.atproto.repo.getRecord, { repo: urip.host, collection: POSTGATE_COLLECTION, rkey: urip.rkey, }), ) - if ( - data.value && - bsky.validate(data.value, AppBskyFeedPostgate.validateRecord) - ) { + if (data.value && bsky.matches(app.bsky.feed.postgate, data.value)) { return data.value } else { return undefined @@ -84,19 +76,19 @@ export async function getPostgateRecord({ } export async function writePostgateRecord({ - agent, + pdsClient, postUri, postgate, }: { - agent: SessionAgent + pdsClient: Client postUri: string - postgate: AppBskyFeedPostgate.Record + postgate: app.bsky.feed.postgate.Main }) { const postUrip = new AtUri(postUri) await networkRetry(2, () => - agent.api.com.atproto.repo.putRecord({ - repo: agent.session!.did, + pdsClient.call(com.atproto.repo.putRecord, { + repo: pdsClient.assertDid, collection: POSTGATE_COLLECTION, rkey: postUrip.rkey, record: postgate, @@ -106,24 +98,24 @@ export async function writePostgateRecord({ export async function upsertPostgate( { - agent, + pdsClient, postUri, }: { - agent: SessionAgent + pdsClient: Client postUri: string }, callback: ( - postgate: AppBskyFeedPostgate.Record | undefined, - ) => Promise, + postgate: app.bsky.feed.postgate.Main | undefined, + ) => Promise, ) { const prev = await getPostgateRecord({ - agent, + pdsClient, postUri, }) const next = await callback(prev) if (!next) return await writePostgateRecord({ - agent, + pdsClient, postUri, postgate: next, }) @@ -134,18 +126,20 @@ export const createPostgateQueryKey = (postUri: string) => [ postUri, ] export function usePostgateQuery({postUri}: {postUri: string}) { - const agent = useAgent() + const pdsClient = usePdsClient() return useQuery({ staleTime: STALE.SECONDS.THIRTY, queryKey: createPostgateQueryKey(postUri), async queryFn() { - return await getPostgateRecord({agent, postUri}).then(res => res ?? null) + return await getPostgateRecord({pdsClient, postUri}).then( + res => res ?? null, + ) }, }) } export function useWritePostgateMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation({ mutationFn: async ({ @@ -153,10 +147,10 @@ export function useWritePostgateMutation() { postgate, }: { postUri: string - postgate: AppBskyFeedPostgate.Record + postgate: app.bsky.feed.postgate.Main }) => { return writePostgateRecord({ - agent, + pdsClient, postUri, postgate, }) @@ -170,10 +164,10 @@ export function useWritePostgateMutation() { } export function useToggleQuoteDetachmentMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const getPosts = useGetPosts() - const prevEmbed = useRef(undefined) + const prevEmbed = useRef(undefined) return useMutation({ mutationFn: async ({ @@ -181,7 +175,7 @@ export function useToggleQuoteDetachmentMutation() { quoteUri, action, }: { - post: AppBskyFeedDefs.PostView + post: app.bsky.feed.defs.PostView quoteUri: string action: 'detach' | 'reattach' }) => { @@ -199,7 +193,7 @@ export function useToggleQuoteDetachmentMutation() { }) } - await upsertPostgate({agent, postUri: quoteUri}, async prev => { + await upsertPostgate({pdsClient, postUri: quoteUri}, async prev => { if (prev) { if (action === 'detach') { return mergePostgateRecords(prev, { @@ -247,8 +241,8 @@ export function useToggleQuoteDetachmentMutation() { if (action === 'detach' && prevEmbed.current) { // detach failed, add the embed back if ( - AppBskyEmbedRecord.isView(prevEmbed.current) || - AppBskyEmbedRecordWithMedia.isView(prevEmbed.current) + bsky.isType(app.bsky.embed.record.view, prevEmbed.current) || + bsky.isType(app.bsky.embed.recordWithMedia.view, prevEmbed.current) ) { updatePostShadow(queryClient, post.uri, { embed: prevEmbed.current, @@ -263,7 +257,7 @@ export function useToggleQuoteDetachmentMutation() { } export function useToggleQuotepostEnabledMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async ({ @@ -273,7 +267,7 @@ export function useToggleQuotepostEnabledMutation() { postUri: string action: 'enable' | 'disable' }) => { - await upsertPostgate({agent, postUri: postUri}, async prev => { + await upsertPostgate({pdsClient, postUri: postUri}, async prev => { if (prev) { if (action === 'disable') { return mergePostgateRecords(prev, { diff --git a/src/state/queries/postgate/util.ts b/src/state/queries/postgate/util.ts index 0952a1ad09..9f79048b19 100644 --- a/src/state/queries/postgate/util.ts +++ b/src/state/queries/postgate/util.ts @@ -1,31 +1,28 @@ -import { - type $Typed, - AppBskyEmbedRecord, - AppBskyEmbedRecordWithMedia, - type AppBskyFeedDefs, - type AppBskyFeedPostgate, - AtUri, -} from '@atproto/api' +import {type $Typed} from '@atproto/lex' +import {AtUri, type AtUriString, toDatetimeString} from '@atproto/syntax' + +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' export const POSTGATE_COLLECTION = 'app.bsky.feed.postgate' export function createPostgateRecord( - postgate: Partial & { - post: AppBskyFeedPostgate.Record['post'] + postgate: Omit, 'post'> & { + post: string }, -): AppBskyFeedPostgate.Record { +): app.bsky.feed.postgate.Main { return { $type: POSTGATE_COLLECTION, - createdAt: new Date().toISOString(), - post: postgate.post, + createdAt: toDatetimeString(new Date()), + post: postgate.post as AtUriString, detachedEmbeddingUris: postgate.detachedEmbeddingUris || [], embeddingRules: postgate.embeddingRules || [], } } export function mergePostgateRecords( - prev: AppBskyFeedPostgate.Record, - next: Partial, + prev: app.bsky.feed.postgate.Main, + next: Partial, ) { const detachedEmbeddingUris = Array.from( new Set([ @@ -50,10 +47,10 @@ export function createEmbedViewDetachedRecord({ uri, }: { uri: string -}): $Typed { - const record: $Typed = { +}): $Typed { + const record: $Typed = { $type: 'app.bsky.embed.record#viewDetached', - uri, + uri: uri as AtUriString, detached: true, } return { @@ -69,24 +66,27 @@ export function createMaybeDetachedQuoteEmbed({ detached, }: | { - post: AppBskyFeedDefs.PostView - quote: AppBskyFeedDefs.PostView + post: app.bsky.feed.defs.PostView + quote: app.bsky.feed.defs.PostView quoteUri: undefined detached: false } | { - post: AppBskyFeedDefs.PostView + post: app.bsky.feed.defs.PostView quote: undefined quoteUri: string detached: true - }): AppBskyEmbedRecord.View | AppBskyEmbedRecordWithMedia.View | undefined { - if (AppBskyEmbedRecord.isView(post.embed)) { + }): + | app.bsky.embed.record.View + | app.bsky.embed.recordWithMedia.View + | undefined { + if (bsky.isType(app.bsky.embed.record.view, post.embed)) { if (detached) { return createEmbedViewDetachedRecord({uri: quoteUri}) } else { return createEmbedRecordView({post: quote}) } - } else if (AppBskyEmbedRecordWithMedia.isView(post.embed)) { + } else if (bsky.isType(app.bsky.embed.recordWithMedia.view, post.embed)) { if (detached) { return { ...post.embed, @@ -99,8 +99,8 @@ export function createMaybeDetachedQuoteEmbed({ } export function createEmbedViewRecordFromPost( - post: AppBskyFeedDefs.PostView, -): $Typed { + post: app.bsky.feed.defs.PostView, +): $Typed { return { $type: 'app.bsky.embed.record#viewRecord', uri: post.uri, @@ -120,8 +120,8 @@ export function createEmbedViewRecordFromPost( export function createEmbedRecordView({ post, }: { - post: AppBskyFeedDefs.PostView -}): AppBskyEmbedRecord.View { + post: app.bsky.feed.defs.PostView +}): app.bsky.embed.record.View { return { $type: 'app.bsky.embed.record#view', record: createEmbedViewRecordFromPost(post), @@ -132,10 +132,10 @@ export function createEmbedRecordWithMediaView({ post, quote, }: { - post: AppBskyFeedDefs.PostView - quote: AppBskyFeedDefs.PostView -}): AppBskyEmbedRecordWithMedia.View | undefined { - if (!AppBskyEmbedRecordWithMedia.isView(post.embed)) return + post: app.bsky.feed.defs.PostView + quote: app.bsky.feed.defs.PostView +}): app.bsky.embed.recordWithMedia.View | undefined { + if (!bsky.isType(app.bsky.embed.recordWithMedia.view, post.embed)) return return { ...(post.embed || {}), record: { @@ -149,11 +149,11 @@ export function getMaybeDetachedQuoteEmbed({ post, }: { viewerDid: string - post: AppBskyFeedDefs.PostView + post: app.bsky.feed.defs.PostView }) { - if (AppBskyEmbedRecord.isView(post.embed)) { + if (bsky.isType(app.bsky.embed.record.view, post.embed)) { // detached - if (AppBskyEmbedRecord.isViewDetached(post.embed.record)) { + if (bsky.isType(app.bsky.embed.record.viewDetached, post.embed.record)) { const urip = new AtUri(post.embed.record.uri) return { embed: post.embed, @@ -164,7 +164,7 @@ export function getMaybeDetachedQuoteEmbed({ } // post - if (AppBskyEmbedRecord.isViewRecord(post.embed.record)) { + if (bsky.isType(app.bsky.embed.record.viewRecord, post.embed.record)) { const urip = new AtUri(post.embed.record.uri) return { embed: post.embed, @@ -173,9 +173,11 @@ export function getMaybeDetachedQuoteEmbed({ isDetached: false, } } - } else if (AppBskyEmbedRecordWithMedia.isView(post.embed)) { + } else if (bsky.isType(app.bsky.embed.recordWithMedia.view, post.embed)) { // detached - if (AppBskyEmbedRecord.isViewDetached(post.embed.record.record)) { + if ( + bsky.isType(app.bsky.embed.record.viewDetached, post.embed.record.record) + ) { const urip = new AtUri(post.embed.record.record.uri) return { embed: post.embed, @@ -186,7 +188,9 @@ export function getMaybeDetachedQuoteEmbed({ } // post - if (AppBskyEmbedRecord.isViewRecord(post.embed.record.record)) { + if ( + bsky.isType(app.bsky.embed.record.viewRecord, post.embed.record.record) + ) { const urip = new AtUri(post.embed.record.record.uri) return { embed: post.embed, diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts index 74c86d1020..ced8e5bd75 100644 --- a/src/state/queries/preferences/const.ts +++ b/src/state/queries/preferences/const.ts @@ -1,4 +1,4 @@ -import {DEFAULT_LABEL_SETTINGS} from '@atproto/api' +import {DEFAULT_LABEL_SETTINGS} from '@bsky.app/sdk' import { type ThreadViewPreferences, diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index 41adfc9a04..c2576408e1 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -1,9 +1,27 @@ import {useCallback} from 'react' +import {type DidString} from '@atproto/syntax' import { - type AppBskyActorDefs, + addSavedFeeds, type BskyFeedViewPreference, - type LabelPreference, -} from '@atproto/api' + dismissNudges, + getPreferences, + overwriteSavedFeeds, + queueNudges, + removeMutedWord, + removeMutedWords, + removeSavedFeeds, + setActiveProgressGuide, + setAdultContentEnabled, + setContentLabelPref, + setFeedViewPrefs, + setIsBetaUser, + setThreadViewPrefs, + setVerificationPrefs, + updateMutedWord, + updateSavedFeeds, + upsertMutedWords, +} from '@bsky.app/sdk' +import {type LabelPreference} from '@bsky.app/sdk/moderation' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {PROD_DEFAULT_FEED} from '#/lib/constants' @@ -20,11 +38,13 @@ import { type UsePreferencesQueryResponse, } from '#/state/queries/preferences/types' import {createQueryKey} from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' import {saveLabelers} from '#/state/session/agent-config' import {useAgeAssurance} from '#/ageAssurance' import {makeAgeRestrictedModerationPrefs} from '#/ageAssurance/util' import {useAnalytics} from '#/analytics' +import {app} from '#/lexicons' +import {toLex} from '#/types/bsky' export * from '#/state/queries/preferences/const' export * from '#/state/queries/preferences/moderation' @@ -37,7 +57,7 @@ export const preferencesQueryKey = createQueryKey( ) export function usePreferencesQuery() { - const agent = useAgent() + const client = usePdsClient() const aa = useAgeAssurance() const query = useQuery({ @@ -47,14 +67,14 @@ export function usePreferencesQuery() { queryKey: preferencesQueryKey, gcTime: GCTIME.INFINITY, queryFn: async () => { - if (!agent.did) { + if (!client.did) { return DEFAULT_LOGGED_OUT_PREFERENCES } else { - const res = await agent.getPreferences() + const res = await client.call(getPreferences) // save to local storage to ensure there are labels on initial requests void saveLabelers( - agent.did, + client.did, res.moderationPrefs.labelers.map(l => l.did), ) @@ -90,8 +110,16 @@ export function usePreferencesQuery() { ) { data = { ...data, - moderationPrefs: makeAgeRestrictedModerationPrefs( - data.moderationPrefs, + /* + * TODO(phase4): drop the toLex bridges once + * `#/ageAssurance/util` (makeAgeRestrictedModerationPrefs) sources + * `ModerationPrefs` from `@bsky.app/sdk/moderation` instead of + * `@atproto/api`. The two shapes differ only in scalar branding + * (e.g. MutedWord.actorTarget's UnknownString), so the values are + * structurally interchangeable at this boundary. + */ + moderationPrefs: toLex( + makeAgeRestrictedModerationPrefs(toLex(data.moderationPrefs)), ), } } @@ -113,11 +141,11 @@ export function usePreferencesQuery() { export function useClearPreferencesMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation({ mutationFn: async () => { - await agent.app.bsky.actor.putPreferences({preferences: []}) + await client.call(app.bsky.actor.putPreferences, {preferences: []}) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -128,7 +156,7 @@ export function useClearPreferencesMutation() { export function usePreferencesSetContentLabelMutation() { const ax = useAnalytics() - const agent = useAgent() + const client = usePdsClient() const queryClient = useQueryClient() return useMutation< @@ -137,7 +165,11 @@ export function usePreferencesSetContentLabelMutation() { {label: string; visibility: LabelPreference; labelerDid: string | undefined} >({ mutationFn: async ({label, visibility, labelerDid}) => { - await agent.setContentLabelPref(label, visibility, labelerDid) + await client.call(setContentLabelPref, { + key: label, + value: visibility, + labelerDid: labelerDid as DidString | undefined, + }) ax.metric('moderation:changeLabelPreference', {preference: visibility}) // triggers a refetch await queryClient.invalidateQueries({ @@ -149,7 +181,7 @@ export function usePreferencesSetContentLabelMutation() { export function useSetContentLabelMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation({ mutationFn: async ({ @@ -161,7 +193,11 @@ export function useSetContentLabelMutation() { visibility: LabelPreference labelerDid?: string }) => { - await agent.setContentLabelPref(label, visibility, labelerDid) + await client.call(setContentLabelPref, { + key: label, + value: visibility, + labelerDid: labelerDid as DidString | undefined, + }) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -172,11 +208,11 @@ export function useSetContentLabelMutation() { export function usePreferencesSetAdultContentMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation({ mutationFn: async ({enabled}) => { - await agent.setAdultContentEnabled(enabled) + await client.call(setAdultContentEnabled, enabled) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -187,7 +223,7 @@ export function usePreferencesSetAdultContentMutation() { export function useSetFeedViewPreferencesMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation>({ mutationFn: async prefs => { @@ -195,7 +231,7 @@ export function useSetFeedViewPreferencesMutation() { * special handling here, merged into `feedViewPrefs` above, since * following was previously called `home` */ - await agent.setFeedViewPrefs('home', prefs) + await client.call(setFeedViewPrefs, {feed: 'home', ...prefs}) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -212,11 +248,11 @@ export function useSetThreadViewPreferencesMutation({ onError?: (error: unknown) => void }) { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation>({ mutationFn: async prefs => { - await agent.setThreadViewPrefs(prefs) + await client.call(setThreadViewPrefs, prefs) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -229,11 +265,11 @@ export function useSetThreadViewPreferencesMutation({ export function useOverwriteSavedFeedsMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() - return useMutation({ + return useMutation({ mutationFn: async savedFeeds => { - await agent.overwriteSavedFeeds(savedFeeds) + await client.call(overwriteSavedFeeds, savedFeeds) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -244,15 +280,15 @@ export function useOverwriteSavedFeedsMutation() { export function useAddSavedFeedsMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation< void, unknown, - Pick[] + Pick[] >({ mutationFn: async savedFeeds => { - await agent.addSavedFeeds(savedFeeds) + await client.call(addSavedFeeds, savedFeeds) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -263,11 +299,11 @@ export function useAddSavedFeedsMutation() { export function useRemoveFeedMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() - return useMutation>({ + return useMutation>({ mutationFn: async savedFeed => { - await agent.removeSavedFeeds([savedFeed.id]) + await client.call(removeSavedFeeds, [savedFeed.id]) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -278,21 +314,21 @@ export function useRemoveFeedMutation() { export function useReplaceForYouWithDiscoverFeedMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation({ mutationFn: async ({ forYouFeedConfig, discoverFeedConfig, }: { - forYouFeedConfig: AppBskyActorDefs.SavedFeed | undefined - discoverFeedConfig: AppBskyActorDefs.SavedFeed | undefined + forYouFeedConfig: app.bsky.actor.defs.SavedFeed | undefined + discoverFeedConfig: app.bsky.actor.defs.SavedFeed | undefined }) => { if (forYouFeedConfig) { - await agent.removeSavedFeeds([forYouFeedConfig.id]) + await client.call(removeSavedFeeds, [forYouFeedConfig.id]) } if (!discoverFeedConfig) { - await agent.addSavedFeeds([ + await client.call(addSavedFeeds, [ { type: 'feed', value: PROD_DEFAULT_FEED('whats-hot'), @@ -300,7 +336,7 @@ export function useReplaceForYouWithDiscoverFeedMutation() { }, ]) } else { - await agent.updateSavedFeeds([ + await client.call(updateSavedFeeds, [ { ...discoverFeedConfig, pinned: true, @@ -317,11 +353,11 @@ export function useReplaceForYouWithDiscoverFeedMutation() { export function useUpdateSavedFeedsMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() - return useMutation({ + return useMutation({ mutationFn: async feeds => { - await agent.updateSavedFeeds(feeds) + await client.call(updateSavedFeeds, feeds) // triggers a refetch await queryClient.invalidateQueries({ @@ -333,11 +369,11 @@ export function useUpdateSavedFeedsMutation() { export function useUpsertMutedWordsMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation({ - mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => { - await agent.upsertMutedWords(mutedWords) + mutationFn: async (mutedWords: app.bsky.actor.defs.MutedWord[]) => { + await client.call(upsertMutedWords, mutedWords) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -348,11 +384,11 @@ export function useUpsertMutedWordsMutation() { export function useUpdateMutedWordMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation({ - mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => { - await agent.updateMutedWord(mutedWord) + mutationFn: async (mutedWord: app.bsky.actor.defs.MutedWord) => { + await client.call(updateMutedWord, mutedWord) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -363,11 +399,11 @@ export function useUpdateMutedWordMutation() { export function useRemoveMutedWordMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation({ - mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => { - await agent.removeMutedWord(mutedWord) + mutationFn: async (mutedWord: app.bsky.actor.defs.MutedWord) => { + await client.call(removeMutedWord, mutedWord) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -378,11 +414,11 @@ export function useRemoveMutedWordMutation() { export function useRemoveMutedWordsMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation({ - mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => { - await agent.removeMutedWords(mutedWords) + mutationFn: async (mutedWords: app.bsky.actor.defs.MutedWord[]) => { + await client.call(removeMutedWords, mutedWords) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -393,11 +429,11 @@ export function useRemoveMutedWordsMutation() { export function useQueueNudgesMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation({ mutationFn: async (nudges: string | string[]) => { - await agent.bskyAppQueueNudges(nudges) + await client.call(queueNudges, Array.isArray(nudges) ? nudges : [nudges]) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -408,11 +444,14 @@ export function useQueueNudgesMutation() { export function useDismissNudgesMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation({ mutationFn: async (nudges: string | string[]) => { - await agent.bskyAppDismissNudges(nudges) + await client.call( + dismissNudges, + Array.isArray(nudges) ? nudges : [nudges], + ) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -423,13 +462,13 @@ export function useDismissNudgesMutation() { export function useSetActiveProgressGuideMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation({ mutationFn: async ( - guide: AppBskyActorDefs.BskyAppProgressGuide | undefined, + guide: app.bsky.actor.defs.BskyAppProgressGuide | undefined, ) => { - await agent.bskyAppSetActiveProgressGuide(guide) + await client.call(setActiveProgressGuide, guide) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -440,11 +479,11 @@ export function useSetActiveProgressGuideMutation() { export function useSetIsBetaUserMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation({ mutationFn: async (isBetaUser: boolean) => { - await agent.setIsBetaUser(isBetaUser) + await client.call(setIsBetaUser, isBetaUser) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, @@ -456,11 +495,11 @@ export function useSetIsBetaUserMutation() { export function useSetVerificationPrefsMutation() { const ax = useAnalytics() const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() - return useMutation({ + return useMutation({ mutationFn: async prefs => { - await agent.setVerificationPrefs(prefs) + await client.call(setVerificationPrefs, prefs) if (prefs.hideBadges) { ax.metric('verification:settings:hideBadges', {}) } else { diff --git a/src/state/queries/preferences/moderation.ts b/src/state/queries/preferences/moderation.ts index 55c155dc5b..fc0e18aca8 100644 --- a/src/state/queries/preferences/moderation.ts +++ b/src/state/queries/preferences/moderation.ts @@ -1,5 +1,6 @@ import {useMemo} from 'react' -import {AtpAgent, interpretLabelValueDefinitions} from '@atproto/api' +import {Client} from '@atproto/lex-client' +import {interpretLabelValueDefinitions} from '@bsky.app/sdk/moderation' import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities' import {useLabelersDetailedInfoQuery} from '../labeler' @@ -13,7 +14,7 @@ export function useMyLabelersQuery({ const prefs = usePreferencesQuery() let dids = Array.from( new Set( - AtpAgent.appLabelers.concat( + (Client.appLabelers as readonly string[]).concat( prefs.data?.moderationPrefs.labelers.map(l => l.did) || [], ), ), diff --git a/src/state/queries/preferences/types.ts b/src/state/queries/preferences/types.ts index ab0a957522..f93ca955a4 100644 --- a/src/state/queries/preferences/types.ts +++ b/src/state/queries/preferences/types.ts @@ -1,4 +1,4 @@ -import {type BskyFeedViewPreference, type BskyPreferences} from '@atproto/api' +import {type BskyFeedViewPreference, type BskyPreferences} from '@bsky.app/sdk' export type UsePreferencesQueryResponse = Omit< BskyPreferences, diff --git a/src/state/queries/profile-feedgens.ts b/src/state/queries/profile-feedgens.ts index 56e7c70182..7eb79053df 100644 --- a/src/state/queries/profile-feedgens.ts +++ b/src/state/queries/profile-feedgens.ts @@ -1,14 +1,13 @@ -import { - type AppBskyFeedGetActorFeeds, - moderateFeedGenerator, -} from '@atproto/api' +import {type AtIdentifierString} from '@atproto/syntax' +import {moderateFeedGenerator} from '@bsky.app/sdk/moderation' import { type InfiniteData, type QueryKey, useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' import {useModerationOpts} from '../preferences/moderation-opts' const PAGE_SIZE = 50 @@ -24,25 +23,25 @@ export function useProfileFeedgensQuery( ) { const moderationOpts = useModerationOpts() const enabled = opts?.enabled !== false && Boolean(moderationOpts) - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyFeedGetActorFeeds.OutputSchema, + app.bsky.feed.getActorFeeds.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(did), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.app.bsky.feed.getActorFeeds({ - actor: did, + const data = await client.call(app.bsky.feed.getActorFeeds, { + actor: did as AtIdentifierString, limit: PAGE_SIZE, cursor: pageParam, }) - res.data.feeds.sort((a, b) => { + data.feeds.sort((a, b) => { return (b.likeCount || 0) - (a.likeCount || 0) }) - return res.data + return data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, diff --git a/src/state/queries/profile-followers.ts b/src/state/queries/profile-followers.ts index 62968af2d8..4ab392bad4 100644 --- a/src/state/queries/profile-followers.ts +++ b/src/state/queries/profile-followers.ts @@ -1,7 +1,4 @@ -import { - type AppBskyActorDefs, - type AppBskyGraphGetFollowers, -} from '@atproto/api' +import {type AtIdentifierString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -9,8 +6,9 @@ import { useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' import {useAnalytics} from '#/analytics' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const DEFAULT_SORT = 'latest' const PAGE_SIZE = 30 @@ -33,26 +31,25 @@ export function useProfileFollowersQuery( ) { const ax = useAnalytics() const isSortEnabled = ax.features.enabled(ax.features.FollowSortEnable) - const agent = useAgent() + const client = useAppviewClient() const sortParam = isSortEnabled ? sort || DEFAULT_SORT : undefined return useInfiniteQuery< - AppBskyGraphGetFollowers.OutputSchema, + app.bsky.graph.getFollowers.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(did || '', sortParam), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.app.bsky.graph.getFollowers({ - actor: did || '', + return await client.call(app.bsky.graph.getFollowers, { + actor: (did || '') as AtIdentifierString, limit: PAGE_SIZE, cursor: pageParam, sort: sortParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -63,9 +60,9 @@ export function useProfileFollowersQuery( export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/profile-follows.ts b/src/state/queries/profile-follows.ts index 1c4d5dc69c..4a03abfc20 100644 --- a/src/state/queries/profile-follows.ts +++ b/src/state/queries/profile-follows.ts @@ -1,4 +1,4 @@ -import {type AppBskyActorDefs, type AppBskyGraphGetFollows} from '@atproto/api' +import {type AtIdentifierString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -7,8 +7,9 @@ import { } from '@tanstack/react-query' import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' import {useAnalytics} from '#/analytics' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const DEFAULT_SORT = 'latest' const PAGE_SIZE = 30 @@ -34,27 +35,26 @@ export function useProfileFollowsQuery( ) { const ax = useAnalytics() const isSortEnabled = ax.features.enabled(ax.features.FollowSortEnable) - const agent = useAgent() + const client = useAppviewClient() const sortParam = isSortEnabled ? sort || DEFAULT_SORT : undefined return useInfiniteQuery< - AppBskyGraphGetFollows.OutputSchema, + app.bsky.graph.getFollows.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ staleTime: STALE.MINUTES.ONE, queryKey: RQKEY(did || '', sortParam), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.app.bsky.graph.getFollows({ - actor: did || '', + return await client.call(app.bsky.graph.getFollows, { + actor: (did || '') as AtIdentifierString, limit: limit || PAGE_SIZE, cursor: pageParam, sort: sortParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -65,9 +65,9 @@ export function useProfileFollowsQuery( export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/profile-lists.ts b/src/state/queries/profile-lists.ts index 965da72e51..d1225f48dc 100644 --- a/src/state/queries/profile-lists.ts +++ b/src/state/queries/profile-lists.ts @@ -1,11 +1,13 @@ -import {type AppBskyGraphGetLists, moderateUserList} from '@atproto/api' +import {type AtIdentifierString} from '@atproto/syntax' +import {moderateUserList} from '@bsky.app/sdk/moderation' import { type InfiniteData, type QueryKey, useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' import {useModerationOpts} from '../preferences/moderation-opts' const PAGE_SIZE = 30 @@ -17,23 +19,21 @@ export const RQKEY = (did: string) => [RQKEY_ROOT, did] export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) { const moderationOpts = useModerationOpts() const enabled = opts?.enabled !== false && Boolean(moderationOpts) - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyGraphGetLists.OutputSchema, + app.bsky.graph.getLists.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(did), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.app.bsky.graph.getLists({ - actor: did, + return await client.call(app.bsky.graph.getLists, { + actor: did as AtIdentifierString, limit: PAGE_SIZE, cursor: pageParam, }) - - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index c7e793f958..53a0534dd9 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -1,14 +1,19 @@ import {useCallback} from 'react' +import {type Un$Typed} from '@atproto/lex' import { - type AppBskyActorDefs, - type AppBskyActorGetProfile, - type AppBskyActorGetProfiles, - type AppBskyActorProfile, - type AppBskyGraphGetFollows, - AtUri, - type ComAtprotoRepoUploadBlob, - type Un$Typed, -} from '@atproto/api' + type AtIdentifierString, + type Client, + type DidString, +} from '@atproto/lex-client' +import {type DatetimeString} from '@atproto/lex-schema' +import {AtUri, type AtUriString} from '@atproto/syntax' +import { + deleteFollow, + follow, + muteActor, + unmuteActor, + upsertProfile, +} from '@bsky.app/sdk' import { type InfiniteData, keepPreviousData, @@ -32,10 +37,11 @@ import { useUnstableProfileViewCache, } from '#/state/queries/unstable-profile-cache' import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache' -import {type SessionAgent, useAgent, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import * as userActionHistory from '#/state/userActionHistory' import {useAnalytics} from '#/analytics' import {type Metrics, toClout} from '#/analytics/metrics' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' import { ProgressGuideAction, @@ -67,9 +73,9 @@ export function useProfileQuery({ did: string | undefined staleTime?: number }) { - const agent = useAgent() + const client = useAppviewClient() const {getUnstableProfile} = useUnstableProfileViewCache() - return useQuery({ + return useQuery({ // WARNING // this staleTime is load-bearing // if you remove it, the UI infinite-loops @@ -78,12 +84,13 @@ export function useProfileQuery({ refetchOnWindowFocus: true, queryKey: RQKEY(did ?? ''), queryFn: async () => { - const res = await agent.getProfile({actor: did ?? ''}) - return res.data + return await client.call(app.bsky.actor.getProfile, { + actor: (did ?? '') as AtIdentifierString, + }) }, placeholderData: () => { if (!did) return - return getUnstableProfile(did) as AppBskyActorDefs.ProfileViewDetailed + return getUnstableProfile(did) as app.bsky.actor.defs.ProfileViewDetailed }, enabled: !!did, }) @@ -96,21 +103,22 @@ export function useProfilesQuery({ handles: string[] maintainData?: boolean }) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ enabled: handles.length > 0, staleTime: STALE.MINUTES.FIVE, queryKey: profilesQueryKey(handles), queryFn: async () => { - const res = await agent.getProfiles({actors: handles}) - return res.data + return await client.call(app.bsky.actor.getProfiles, { + actors: handles as AtIdentifierString[], + }) }, placeholderData: maintainData ? keepPreviousData : undefined, }) } export function usePrefetchProfileQuery() { - const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() const prefetchProfileQuery = useCallback( async (did: string) => { @@ -118,30 +126,32 @@ export function usePrefetchProfileQuery() { staleTime: STALE.SECONDS.THIRTY, queryKey: RQKEY(did), queryFn: async () => { - const res = await agent.getProfile({actor: did || ''}) - return res.data + return await client.call(app.bsky.actor.getProfile, { + actor: (did || '') as AtIdentifierString, + }) }, }) }, - [queryClient, agent], + [queryClient, client], ) return prefetchProfileQuery } interface ProfileUpdateParams { - profile: AppBskyActorDefs.ProfileViewDetailed + profile: app.bsky.actor.defs.ProfileViewDetailed updates: - | Un$Typed + | Un$Typed | (( - existing: Un$Typed, - ) => Un$Typed) + existing: Un$Typed, + ) => Un$Typed) newUserAvatar?: ImageMeta | undefined | null newUserBanner?: ImageMeta | undefined | null - checkCommitted?: (res: AppBskyActorGetProfile.Response) => boolean + checkCommitted?: (res: app.bsky.actor.getProfile.$OutputBody) => boolean } export function useProfileUpdateMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const pdsClient = usePdsClient() + const appviewClient = useAppviewClient() const updateProfileVerificationCache = useUpdateProfileVerificationCache() return useMutation({ mutationFn: async ({ @@ -151,28 +161,24 @@ export function useProfileUpdateMutation() { newUserBanner, checkCommitted, }) => { - let newUserAvatarPromise: - | Promise - | undefined + let newUserAvatarPromise: ReturnType | undefined if (newUserAvatar) { newUserAvatarPromise = uploadBlob( - agent, + pdsClient, newUserAvatar.path, newUserAvatar.mime, ) } - let newUserBannerPromise: - | Promise - | undefined + let newUserBannerPromise: ReturnType | undefined if (newUserBanner) { newUserBannerPromise = uploadBlob( - agent, + pdsClient, newUserBanner.path, newUserBanner.mime, ) } - await agent.upsertProfile(async existing => { - let next: Un$Typed = existing || {} + await pdsClient.call(upsertProfile, async existing => { + let next: Un$Typed = existing || {} if (typeof updates === 'function') { next = updates(next) } else { @@ -184,37 +190,37 @@ export function useProfileUpdateMutation() { } if (newUserAvatarPromise) { const res = await newUserAvatarPromise - next.avatar = res.data.blob + next.avatar = res.blob } else if (newUserAvatar === null) { next.avatar = undefined } if (newUserBannerPromise) { const res = await newUserBannerPromise - next.banner = res.data.blob + next.banner = res.blob } else if (newUserBanner === null) { next.banner = undefined } return next }) await whenAppViewReady( - agent, + appviewClient, profile.did, checkCommitted || (res => { if (typeof newUserAvatar !== 'undefined') { - if (newUserAvatar === null && res.data.avatar) { + if (newUserAvatar === null && res.avatar) { // url hasn't cleared yet return false - } else if (res.data.avatar === profile.avatar) { + } else if (res.avatar === profile.avatar) { // url hasn't changed yet return false } } if (typeof newUserBanner !== 'undefined') { - if (newUserBanner === null && res.data.banner) { + if (newUserBanner === null && res.banner) { // url hasn't cleared yet return false - } else if (res.data.banner === profile.banner) { + } else if (res.banner === profile.banner) { // url hasn't changed yet return false } @@ -223,8 +229,8 @@ export function useProfileUpdateMutation() { return true } return ( - res.data.displayName === updates.displayName && - res.data.description === updates.description + res.displayName === updates.displayName && + res.description === updates.description ) }), ) @@ -248,7 +254,7 @@ export function useProfileFollowMutationQueue( position?: number, contextProfileDid?: string, ) { - const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() const {currentAccount} = useSession() const did = profile.did @@ -290,7 +296,7 @@ export function useProfileFollowMutationQueue( // Optimistically update profile follows cache for avatar displays if (currentAccount?.did) { type FollowsQueryData = - InfiniteData + InfiniteData queryClient.setQueryData( PROFILE_FOLLOWS_RQKEY(currentAccount.did), old => { @@ -307,7 +313,7 @@ export function useProfileFollowMutationQueue( { ...old.pages[0], follows: [ - profile as AppBskyActorDefs.ProfileView, + profile as app.bsky.actor.defs.ProfileView, ...old.pages[0].follows, ], }, @@ -329,12 +335,12 @@ export function useProfileFollowMutationQueue( } if (finalFollowingUri) { - void agent.app.bsky.graph - .getSuggestedFollowsByActor({ - actor: did, + void client + .call(app.bsky.graph.getSuggestedFollowsByActor, { + actor: did as AtIdentifierString, }) .then(res => { - const dids = res.data.suggestions + const dids = res.suggestions .filter(a => !a.viewer?.following) .map(a => a.did) .slice(0, 8) @@ -371,13 +377,13 @@ function useProfileFollowMutation( ) { const ax = useAnalytics() const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const {captureAction} = useProgressGuideControls() return useMutation<{uri: string; cid: string}, Error, {did: string}>({ mutationFn: async ({did}) => { - let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined + let ownProfile: app.bsky.actor.defs.ProfileViewDetailed | undefined if (currentAccount) { ownProfile = findProfileQueryData(queryClient, currentAccount.did) } @@ -396,7 +402,7 @@ function useProfileFollowMutation( position, contextProfileDid, }) - return await agent.follow(did) + return await pdsClient.call(follow, {did: did as DidString}) }, }) } @@ -405,11 +411,11 @@ function useProfileUnfollowMutation( logContext: Metrics['profile:unfollow']['logContext'], ) { const ax = useAnalytics() - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async ({followUri}) => { ax.metric('profile:unfollow', {logContext}) - return await agent.deleteFollow(followUri) + return await pdsClient.call(deleteFollow, followUri as AtUriString) }, }) } @@ -468,10 +474,10 @@ export function useProfileMuteMutationQueue( function useProfileMuteMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async ({did}) => { - await agent.mute(did) + await pdsClient.call(muteActor, {actor: did as AtIdentifierString}) }, onSuccess() { void queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()}) @@ -481,10 +487,10 @@ function useProfileMuteMutation() { function useProfileUnmuteMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async ({did}) => { - await agent.unmute(did) + await pdsClient.call(unmuteActor, {actor: did as AtIdentifierString}) }, onSuccess() { void queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()}) @@ -556,17 +562,17 @@ export function useProfileBlockMutationQueue( function useProfileBlockMutation() { const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation<{uri: string; cid: string}, Error, {did: string}>({ mutationFn: async ({did}) => { if (!currentAccount) { throw new Error('Not signed in') } - return await agent.app.bsky.graph.block.create( - {repo: currentAccount.did}, - {subject: did, createdAt: new Date().toISOString()}, - ) + return await pdsClient.create(app.bsky.graph.block, { + subject: did as DidString, + createdAt: new Date().toISOString() as DatetimeString, + }) }, onSuccess(_, {did}) { void queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()}) @@ -577,7 +583,7 @@ function useProfileBlockMutation() { function useProfileUnblockMutation() { const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation({ mutationFn: async ({blockUri}) => { @@ -585,10 +591,7 @@ function useProfileUnblockMutation() { throw new Error('Not signed in') } const {rkey} = new AtUri(blockUri) - await agent.app.bsky.graph.block.delete({ - repo: currentAccount.did, - rkey, - }) + await pdsClient.delete(app.bsky.graph.block, {rkey}) }, onSuccess(_, {did}) { resetProfilePostsQueries(queryClient, did, 1000) @@ -597,24 +600,27 @@ function useProfileUnblockMutation() { } async function whenAppViewReady( - agent: SessionAgent, + client: Client, actor: string, - fn: (res: AppBskyActorGetProfile.Response) => boolean, + fn: (res: app.bsky.actor.getProfile.$OutputBody) => boolean, ) { await until( 5, // 5 tries 1e3, // 1s delay between tries fn, - () => agent.app.bsky.actor.getProfile({actor}), + () => + client.call(app.bsky.actor.getProfile, { + actor: actor as AtIdentifierString, + }), ) } export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const profileQueryDatas = - queryClient.getQueriesData({ + queryClient.getQueriesData({ queryKey: [RQKEY_ROOT], }) for (const [_queryKey, queryData] of profileQueryDatas) { @@ -626,7 +632,7 @@ export function* findAllProfilesInQueryData( } } const profilesQueryDatas = - queryClient.getQueriesData({ + queryClient.getQueriesData({ queryKey: [profilesQueryKeyRoot], }) for (const [_queryKey, queryData] of profilesQueryDatas) { @@ -644,8 +650,8 @@ export function* findAllProfilesInQueryData( export function findProfileQueryData( queryClient: QueryClient, did: string, -): AppBskyActorDefs.ProfileViewDetailed | undefined { - return queryClient.getQueryData( +): app.bsky.actor.defs.ProfileViewDetailed | undefined { + return queryClient.getQueryData( RQKEY(did), ) } diff --git a/src/state/queries/resolve-link.ts b/src/state/queries/resolve-link.ts index a239b9f2bf..be499d1f23 100644 --- a/src/state/queries/resolve-link.ts +++ b/src/state/queries/resolve-link.ts @@ -1,8 +1,13 @@ import {type QueryClient, queryOptions, useQuery} from '@tanstack/react-query' -import {type ResolvedLink, resolveGif, resolveLink} from '#/lib/api/resolve' +import { + type ResolveClients, + type ResolvedLink, + resolveGif, + resolveLink, +} from '#/lib/api/resolve' import {STALE} from '#/state/queries/index' -import {type SessionAgent, useAgent} from '#/state/session' +import {useAgent, useChatClient, useLexClient} from '#/state/session' import {type Gif} from '#/features/gifPicker/types' export const RQKEY_LINK_ROOT = 'resolve-link' @@ -11,24 +16,37 @@ export const RQKEY_LINK = (url: string) => [RQKEY_LINK_ROOT, url] export const RQKEY_GIF_ROOT = 'resolve-gif' export const RQKEY_GIF = (url: string) => [RQKEY_GIF_ROOT, url] -export function resolveLinkQueryOptions(agent: SessionAgent, url: string) { +export function resolveLinkQueryOptions(clients: ResolveClients, url: string) { return queryOptions({ staleTime: STALE.HOURS.ONE, queryKey: RQKEY_LINK(url), - queryFn: () => resolveLink(agent, url), + queryFn: () => resolveLink(clients, url), }) } -export function useResolveLinkQuery(url: string) { +/** + * Bundle the clients the link resolver needs from the session hooks. The + * appview client serves the `app.bsky.*` reads and handle resolution, the chat + * client serves group join-link previews, and the bridge agent is still passed + * through to the not-yet-migrated `getLinkMeta`. + */ +export function useResolveClients(): ResolveClients { + const appview = useLexClient() + const chat = useChatClient() const agent = useAgent() - return useQuery(resolveLinkQueryOptions(agent, url)) + return {appview, chat, agent} +} + +export function useResolveLinkQuery(url: string) { + const clients = useResolveClients() + return useQuery(resolveLinkQueryOptions(clients, url)) } export function fetchResolveLinkQuery( queryClient: QueryClient, - agent: SessionAgent, + clients: ResolveClients, url: string, ) { - return queryClient.fetchQuery(resolveLinkQueryOptions(agent, url)) + return queryClient.fetchQuery(resolveLinkQueryOptions(clients, url)) } export function precacheResolveLinkQuery( queryClient: QueryClient, @@ -39,25 +57,20 @@ export function precacheResolveLinkQuery( } export function useResolveGifQuery(gif: Gif) { - const agent = useAgent() return useQuery({ staleTime: STALE.HOURS.ONE, queryKey: RQKEY_GIF(gif.url), queryFn: async () => { - return await resolveGif(agent, gif) + return await resolveGif(gif) }, }) } -export function fetchResolveGifQuery( - queryClient: QueryClient, - agent: SessionAgent, - gif: Gif, -) { +export function fetchResolveGifQuery(queryClient: QueryClient, gif: Gif) { return queryClient.fetchQuery({ staleTime: STALE.HOURS.ONE, queryKey: RQKEY_GIF(gif.url), queryFn: async () => { - return await resolveGif(agent, gif) + return await resolveGif(gif) }, }) } diff --git a/src/state/queries/resolve-uri.ts b/src/state/queries/resolve-uri.ts index 007e27997e..c828b48135 100644 --- a/src/state/queries/resolve-uri.ts +++ b/src/state/queries/resolve-uri.ts @@ -1,15 +1,17 @@ -import {AtUri} from '@atproto/api' +import {type Client} from '@atproto/lex-client' +import {AtUri, type HandleString} from '@atproto/syntax' import {type QueryClient, queryOptions, useQuery} from '@tanstack/react-query' import {STALE} from '#/state/queries' -import {type SessionAgent, useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {com} from '#/lexicons' import {useUnstableProfileViewCache} from './profile' const RQKEY_ROOT = 'resolved-did' export const RQKEY = (didOrHandle: string) => [RQKEY_ROOT, didOrHandle] const resolvedDidQueryOptions = ( - agent: SessionAgent, + client: Client, getUnstableProfile: (did: string) => {did: string} | undefined, didOrHandle: string | undefined, ) => @@ -21,8 +23,10 @@ const resolvedDidQueryOptions = ( // Just return the did if it's already one if (didOrHandle.startsWith('did:')) return didOrHandle - const res = await agent.resolveHandle({handle: didOrHandle}) - return res.data.did + const {did} = await client.call(com.atproto.identity.resolveHandle, { + handle: didOrHandle as HandleString, + }) + return did }, initialData: () => { // Return undefined if no did or handle @@ -37,11 +41,11 @@ export function useResolveUriQuery(uri: string | undefined) { const urip = new AtUri(uri || '') const host = urip.host - const agent = useAgent() + const client = useAppviewClient() const {getUnstableProfile} = useUnstableProfileViewCache() return useQuery({ - ...resolvedDidQueryOptions(agent, getUnstableProfile, host), + ...resolvedDidQueryOptions(client, getUnstableProfile, host), select: did => ({ did, uri: AtUri.make(did, urip.collection, urip.rkey).toString(), @@ -50,11 +54,11 @@ export function useResolveUriQuery(uri: string | undefined) { } export function useResolveDidQuery(didOrHandle: string | undefined) { - const agent = useAgent() + const client = useAppviewClient() const {getUnstableProfile} = useUnstableProfileViewCache() return useQuery( - resolvedDidQueryOptions(agent, getUnstableProfile, didOrHandle), + resolvedDidQueryOptions(client, getUnstableProfile, didOrHandle), ) } diff --git a/src/state/queries/search-posts-params.ts b/src/state/queries/search-posts-params.ts index c2769b2295..33950edae1 100644 --- a/src/state/queries/search-posts-params.ts +++ b/src/state/queries/search-posts-params.ts @@ -4,12 +4,22 @@ * tested in isolation (the search-posts query hook re-exports these). */ -import {type AppBskyFeedSearchPostsV2} from '@atproto/api' +import {type l} from '@atproto/lex' import { filtersToApiParams, type SearchFilters, } from '#/screens/Search/searchParams' +import {type app} from '#/lexicons' + +/** + * Input params for `app.bsky.feed.searchPostsV2`. Uses the schema's input type + * (not `$Params`, which is the output type with defaults applied and `limit` + * required) since these are the params we build up and pass to `client.call`. + */ +type SearchPostsV2Params = l.InferInput< + typeof app.bsky.feed.searchPostsV2.$params +> const DATE_RE = /^\d{4}-\d{2}-\d{2}/ @@ -204,21 +214,27 @@ function mergeList(a?: string[], b?: string[]): string[] | undefined { export function buildSearchPostsV2Filters( embedded: Omit, filters?: SearchFilters, -): AppBskyFeedSearchPostsV2.QueryParams { +): SearchPostsV2Params { const apiFilters = filters ? filtersToApiParams(filters) : {} - const params: AppBskyFeedSearchPostsV2.QueryParams = {} + const params: SearchPostsV2Params = {} + /* + * The values below originate from free-text operators and the advanced-search + * dialog as plain strings; the lexicon input params brand them by format + * (at-identifier, uri, etc.). The backend validates these, so we assert the + * branded types at assignment rather than validating client-side. + */ const authors = mergeList( embedded.author ? [embedded.author] : undefined, apiFilters.authors, ) - if (authors) params.authors = authors + if (authors) params.authors = authors as SearchPostsV2Params['authors'] const mentions = mergeList( embedded.mentions ? [embedded.mentions] : undefined, apiFilters.mentions, ) - if (mentions) params.mentions = mentions + if (mentions) params.mentions = mentions as SearchPostsV2Params['mentions'] const domains = mergeList( embedded.domain ? [embedded.domain] : undefined, @@ -230,7 +246,7 @@ export function buildSearchPostsV2Filters( embedded.url ? [embedded.url] : undefined, apiFilters.urls, ) - if (urls) params.urls = urls + if (urls) params.urls = urls as SearchPostsV2Params['urls'] const hashtags = mergeList(embedded.tag, apiFilters.hashtags) if (hashtags) params.hashtags = hashtags @@ -250,12 +266,16 @@ export function buildSearchPostsV2Filters( * are always include), so they pass straight through from the dialog filters. */ if (apiFilters.excludeAuthors) - params.excludeAuthors = apiFilters.excludeAuthors + params.excludeAuthors = + apiFilters.excludeAuthors as SearchPostsV2Params['excludeAuthors'] if (apiFilters.excludeMentions) - params.excludeMentions = apiFilters.excludeMentions + params.excludeMentions = + apiFilters.excludeMentions as SearchPostsV2Params['excludeMentions'] if (apiFilters.excludeDomains) params.excludeDomains = apiFilters.excludeDomains - if (apiFilters.excludeUrls) params.excludeUrls = apiFilters.excludeUrls + if (apiFilters.excludeUrls) + params.excludeUrls = + apiFilters.excludeUrls as SearchPostsV2Params['excludeUrls'] if (apiFilters.excludeHashtags) params.excludeHashtags = apiFilters.excludeHashtags diff --git a/src/state/queries/search-posts-v2.ts b/src/state/queries/search-posts-v2.ts index e33328b5a0..faf4d45f26 100644 --- a/src/state/queries/search-posts-v2.ts +++ b/src/state/queries/search-posts-v2.ts @@ -1,10 +1,6 @@ import {useCallback, useMemo, useRef} from 'react' -import { - type AppBskyFeedDefs, - type AppBskyFeedSearchPostsV2, - AtUri, - moderatePost, -} from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {moderatePost} from '@bsky.app/sdk/moderation' import { type InfiniteData, type QueryClient, @@ -13,8 +9,9 @@ import { } from '@tanstack/react-query' import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {type SearchFilters} from '#/screens/Search/searchParams' +import {app} from '#/lexicons' import { appendFromMe, buildSearchPostsV2Filters, @@ -48,7 +45,7 @@ export function useSearchPostsV2Query({ enabled?: boolean filters?: SearchFilters }) { - const agent = useAgent() + const client = useAppviewClient() const moderationOpts = useModerationOpts() const selectArgs = useMemo( () => ({ @@ -59,15 +56,15 @@ export function useSearchPostsV2Query({ [query, filters?.author, filters?.from, moderationOpts], ) const lastRun = useRef<{ - data: InfiniteData + data: InfiniteData args: typeof selectArgs - result: InfiniteData + result: InfiniteData } | null>(null) return useInfiniteQuery< - AppBskyFeedSearchPostsV2.OutputSchema, + app.bsky.feed.searchPostsV2.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, string | undefined >({ @@ -81,7 +78,7 @@ export function useSearchPostsV2Query({ const {q, ...embedded} = extractSearchPostsParams(query) const builtFilters = buildSearchPostsV2Filters(embedded, filters) const finalQuery = appendFromMe(q, filters?.from === 'me') - const res = await agent.app.bsky.feed.searchPostsV2({ + return await client.call(app.bsky.feed.searchPostsV2, { ...builtFilters, query: finalQuery, limit: 25, @@ -93,13 +90,12 @@ export function useSearchPostsV2Query({ sort: sort === 'latest' ? 'recent' : sort, allTime: true, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, enabled: enabled ?? !!moderationOpts, select: useCallback( - (data: InfiniteData) => { + (data: InfiniteData) => { const {moderationOpts, isSearchingSpecificUser} = selectArgs /* @@ -175,9 +171,9 @@ export function useSearchPostsV2Query({ export function* findAllPostsInQueryData( queryClient: QueryClient, uri: string, -): Generator { +): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [searchPostsQueryKeyRoot], }) diff --git a/src/state/queries/starter-packs.ts b/src/state/queries/starter-packs.ts index f9bd11372f..ffc57ab7d0 100644 --- a/src/state/queries/starter-packs.ts +++ b/src/state/queries/starter-packs.ts @@ -1,12 +1,6 @@ -import { - AppBskyFeedDefs, - AppBskyGraphDefs, - type AppBskyGraphGetStarterPack, - AppBskyGraphStarterpack, - type AppBskyRichtextFacet, - AtUri, - RichText, -} from '@atproto/api' +import {type Client} from '@atproto/lex-client' +import {AtUri, type AtUriString, type DatetimeString} from '@atproto/syntax' +import {RichText} from '@bsky.app/sdk/richtext' import { type QueryClient, useMutation, @@ -25,7 +19,8 @@ import { import {invalidateActorStarterPacksQuery} from '#/state/queries/actor-starter-packs' import {STALE} from '#/state/queries/index' import {invalidateListMembersQuery} from '#/state/queries/list-members' -import {type SessionAgent, useAgent} from '#/state/session' +import {useAppviewClient, usePdsClient} from '#/state/session' +import {app, com} from '#/lexicons' import * as bsky from '#/types/bsky' const RQKEY_ROOT = 'starter-pack' @@ -55,9 +50,9 @@ export function useStarterPackQuery({ did?: string rkey?: string }) { - const agent = useAgent() + const client = useAppviewClient() - return useQuery({ + return useQuery({ queryKey: RQKEY(uri ? {uri} : {did, rkey}), queryFn: async () => { if (!uri) { @@ -66,10 +61,10 @@ export function useStarterPackQuery({ uri = httpStarterPackUriToAtUri(uri) as string } - const res = await agent.app.bsky.graph.getStarterPack({ - starterPack: uri, + const res = await client.call(app.bsky.graph.getStarterPack, { + starterPack: uri as AtUriString, }) - return res.data.starterPack + return res.starterPack }, enabled: Boolean(uri) || Boolean(did && rkey), staleTime: STALE.MINUTES.FIVE, @@ -92,7 +87,7 @@ interface UseCreateStarterPackMutationParams { name: string description?: string profiles: bsky.profile.AnyProfileView[] - feeds?: AppBskyFeedDefs.GeneratorView[] + feeds?: app.bsky.feed.defs.GeneratorView[] } export function useCreateStarterPackMutation({ @@ -103,7 +98,8 @@ export function useCreateStarterPackMutation({ onError: (e: Error) => void }) { const queryClient = useQueryClient() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() return useMutation< {uri: string; cid: string}, @@ -111,10 +107,10 @@ export function useCreateStarterPackMutation({ UseCreateStarterPackMutationParams >({ mutationFn: async ({name, description, feeds, profiles}) => { - let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined + let descriptionFacets: app.bsky.richtext.facet.Main[] | undefined if (description) { const rt = new RichText({text: description}) - await rt.detectFacets(agent) + await rt.detectFacets(pdsClient) descriptionFacets = rt.facets } @@ -124,30 +120,25 @@ export function useCreateStarterPackMutation({ description, profiles, descriptionFacets, - agent, + client: pdsClient, }) - return await agent.app.bsky.graph.starterpack.create( - { - repo: agent.assertDid, - }, - { - name, - description, - descriptionFacets, - list: listRes?.uri, - feeds: feeds?.map(f => ({uri: f.uri})), - createdAt: new Date().toISOString(), - }, - ) + return await pdsClient.create(app.bsky.graph.starterpack, { + name, + description, + descriptionFacets, + list: listRes?.uri as AtUriString, + feeds: feeds?.map(f => ({uri: f.uri})), + createdAt: new Date().toISOString() as DatetimeString, + }) }, onSuccess: async data => { - await whenAppViewReady(agent, data.uri, v => { - return typeof v?.data.starterPack.uri === 'string' + await whenAppViewReady(appviewClient, data.uri, v => { + return typeof v?.starterPack.uri === 'string' }) await invalidateActorStarterPacksQuery({ queryClient, - did: agent.session!.did, + did: pdsClient.assertDid, }) onSuccess(data) }, @@ -165,14 +156,15 @@ export function useEditStarterPackMutation({ onError: (error: Error) => void }) { const queryClient = useQueryClient() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() return useMutation< void, Error, UseCreateStarterPackMutationParams & { - currentStarterPack: AppBskyGraphDefs.StarterPackView - currentListItems: AppBskyGraphDefs.ListItemView[] + currentStarterPack: app.bsky.graph.defs.StarterPackView + currentListItems: app.bsky.graph.defs.ListItemView[] } >({ mutationFn: async ({ @@ -183,32 +175,36 @@ export function useEditStarterPackMutation({ currentStarterPack, currentListItems, }) => { - let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined + let descriptionFacets: app.bsky.richtext.facet.Main[] | undefined if (description) { const rt = new RichText({text: description}) - await rt.detectFacets(agent) + await rt.detectFacets(pdsClient) descriptionFacets = rt.facets } - if (!AppBskyGraphStarterpack.isRecord(currentStarterPack.record)) { + if (!bsky.isType(app.bsky.graph.starterpack, currentStarterPack.record)) { throw new Error('Invalid starter pack') } const removedItems = currentListItems.filter( i => - i.subject.did !== agent.session?.did && + i.subject.did !== pdsClient.did && !profiles.find(p => p.did === i.subject.did && p.did), ) if (removedItems.length !== 0) { const chunks = chunk(removedItems, 50) for (const chunk of chunks) { - await agent.com.atproto.repo.applyWrites({ - repo: agent.session!.did, - writes: chunk.map(i => ({ - $type: 'com.atproto.repo.applyWrites#delete', - collection: 'app.bsky.graph.listitem', - rkey: new AtUri(i.uri).rkey, - })), + await pdsClient.call(com.atproto.repo.applyWrites, { + repo: pdsClient.assertDid, + writes: chunk.map( + ( + i, + ): com.atproto.repo.applyWrites.$InputBody['writes'][number] => ({ + $type: 'com.atproto.repo.applyWrites#delete', + collection: 'app.bsky.graph.listitem', + rkey: new AtUri(i.uri).rkey, + }), + ), }) } } @@ -219,28 +215,33 @@ export function useEditStarterPackMutation({ if (addedProfiles.length > 0) { const chunks = chunk(addedProfiles, 50) for (const chunk of chunks) { - await agent.com.atproto.repo.applyWrites({ - repo: agent.session!.did, - writes: chunk.map(p => ({ - $type: 'com.atproto.repo.applyWrites#create', - collection: 'app.bsky.graph.listitem', - value: { - $type: 'app.bsky.graph.listitem', - subject: p.did, - list: currentStarterPack.list?.uri, - createdAt: new Date().toISOString(), - }, - })), + await pdsClient.call(com.atproto.repo.applyWrites, { + repo: pdsClient.assertDid, + writes: chunk.map( + ( + p, + ): com.atproto.repo.applyWrites.$InputBody['writes'][number] => ({ + $type: 'com.atproto.repo.applyWrites#create', + collection: 'app.bsky.graph.listitem', + value: { + $type: 'app.bsky.graph.listitem', + subject: p.did, + list: currentStarterPack.list?.uri, + createdAt: new Date().toISOString(), + }, + }), + ), }) } } const rkey = parseStarterPackUri(currentStarterPack.uri)!.rkey - await agent.com.atproto.repo.putRecord({ - repo: agent.session!.did, + await pdsClient.call(com.atproto.repo.putRecord, { + repo: pdsClient.assertDid, collection: 'app.bsky.graph.starterpack', rkey, record: { + $type: 'app.bsky.graph.starterpack', name, description, descriptionFacets, @@ -253,12 +254,12 @@ export function useEditStarterPackMutation({ }, onSuccess: async (_, {currentStarterPack}) => { const parsed = parseStarterPackUri(currentStarterPack.uri) - await whenAppViewReady(agent, currentStarterPack.uri, v => { - return currentStarterPack.cid !== v?.data.starterPack.cid + await whenAppViewReady(appviewClient, currentStarterPack.uri, v => { + return currentStarterPack.cid !== v?.starterPack.cid }) await invalidateActorStarterPacksQuery({ queryClient, - did: agent.session!.did, + did: pdsClient.assertDid, }) if (currentStarterPack.list) { await invalidateListMembersQuery({ @@ -268,7 +269,7 @@ export function useEditStarterPackMutation({ } await invalidateStarterPack({ queryClient, - did: agent.session!.did, + did: pdsClient.assertDid, rkey: parsed!.rkey, }) onSuccess() @@ -286,35 +287,34 @@ export function useDeleteStarterPackMutation({ onSuccess: () => void onError: (error: Error) => void }) { - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation({ mutationFn: async ({listUri, rkey}: {listUri?: string; rkey: string}) => { - if (!agent.session) { - throw new Error(`Requires signed in user`) - } + const did = pdsClient.assertDid if (listUri) { - await agent.app.bsky.graph.list.delete({ - repo: agent.session.did, + await pdsClient.delete(app.bsky.graph.list, { + repo: did, rkey: new AtUri(listUri).rkey, }) } - await agent.app.bsky.graph.starterpack.delete({ - repo: agent.session.did, + await pdsClient.delete(app.bsky.graph.starterpack, { + repo: did, rkey, }) }, onSuccess: async (_, {listUri, rkey}) => { const uri = createStarterPackUri({ - did: agent.session!.did, + did: pdsClient.assertDid, rkey, }) if (uri) { - await whenAppViewReady(agent, uri, v => { - return Boolean(v?.data?.starterPack) === false + await whenAppViewReady(appviewClient, uri, v => { + return Boolean(v?.starterPack) === false }) } @@ -323,11 +323,11 @@ export function useDeleteStarterPackMutation({ } await invalidateActorStarterPacksQuery({ queryClient, - did: agent.session!.did, + did: pdsClient.assertDid, }) await invalidateStarterPack({ queryClient, - did: agent.session!.did, + did: pdsClient.assertDid, rkey, }) onSuccess() @@ -339,48 +339,51 @@ export function useDeleteStarterPackMutation({ } async function whenAppViewReady( - agent: SessionAgent, + client: Client, uri: string, - fn: (res?: AppBskyGraphGetStarterPack.Response) => boolean, + fn: (res?: app.bsky.graph.getStarterPack.$OutputBody) => boolean, ) { await until( 5, // 5 tries 1e3, // 1s delay between tries fn, - () => agent.app.bsky.graph.getStarterPack({starterPack: uri}), + () => + client.call(app.bsky.graph.getStarterPack, { + starterPack: uri as AtUriString, + }), ) } export function precacheStarterPack( queryClient: QueryClient, starterPack: - | AppBskyGraphDefs.StarterPackViewBasic - | AppBskyGraphDefs.StarterPackView, + | app.bsky.graph.defs.StarterPackViewBasic + | app.bsky.graph.defs.StarterPackView, ) { - if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) { + if (!bsky.isType(app.bsky.graph.starterpack, starterPack.record)) { return } - let starterPackView: AppBskyGraphDefs.StarterPackView | undefined - if (AppBskyGraphDefs.isStarterPackView(starterPack)) { + let starterPackView: app.bsky.graph.defs.StarterPackView | undefined + if (bsky.isType(app.bsky.graph.defs.starterPackView, starterPack)) { starterPackView = starterPack } else if ( - AppBskyGraphDefs.isStarterPackViewBasic(starterPack) && - bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord) + bsky.isType(app.bsky.graph.defs.starterPackViewBasic, starterPack) && + bsky.matches(app.bsky.graph.starterpack, starterPack.record) ) { - let feeds: AppBskyFeedDefs.GeneratorView[] | undefined + let feeds: app.bsky.feed.defs.GeneratorView[] | undefined if (starterPack.record.feeds) { feeds = [] for (const feed of starterPack.record.feeds) { // note: types are wrong? claims to be `FeedItem`, but we actually // get un$typed `GeneratorView` objects here -sfn - if (bsky.validate(feed, AppBskyFeedDefs.validateGeneratorView)) { + if (bsky.matches(app.bsky.feed.defs.generatorView, feed)) { feeds.push(feed) } } } - const listView: AppBskyGraphDefs.ListViewBasic = { + const listView: app.bsky.graph.defs.ListViewBasic = { uri: starterPack.record.list, // This will be populated once the data from server is fetched cid: '', diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts index 7197eb2980..c44d1f6c13 100644 --- a/src/state/queries/suggested-follows.ts +++ b/src/state/queries/suggested-follows.ts @@ -1,9 +1,5 @@ import {useCallback, useMemo} from 'react' -import { - type AppBskyActorDefs, - type AppBskyActorGetSuggestions, - type AppBskyGraphGetSuggestedFollowsByActor, -} from '@atproto/api' +import {type AtIdentifierString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -12,8 +8,8 @@ import { } from '@tanstack/react-query' import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' -import type * as bsky from '#/types/bsky' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const suggestedFollowsQueryKeyRoot = 'suggested-follows' @@ -32,18 +28,21 @@ export function useSuggestedFollowsByActorQuery({ enabled?: boolean staleTime?: number }) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ staleTime, queryKey: suggestedFollowsByActorQueryKey(did), queryFn: async () => { - const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({ - actor: did, - }) - const suggestions = res.data.suggestions.filter( + const data = await client.call( + app.bsky.graph.getSuggestedFollowsByActor, + { + actor: did as AtIdentifierString, + }, + ) + const suggestions = data.suggestions.filter( profile => !profile.viewer?.following, ) - return {suggestions, recId: res.data.recIdStr} + return {suggestions, recId: data.recIdStr} }, enabled, }) @@ -102,7 +101,7 @@ export function useSuggestedFollowsByActorWithDismiss({ export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did) yield* findAllProfilesInSuggestedFollowsByActorQueryData(queryClient, did) } @@ -112,7 +111,7 @@ function* findAllProfilesInSuggestedFollowsQueryData( did: string, ) { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [suggestedFollowsQueryKeyRoot], }) @@ -135,7 +134,7 @@ function* findAllProfilesInSuggestedFollowsByActorQueryData( did: string, ) { const queryDatas = - queryClient.getQueriesData( + queryClient.getQueriesData( { queryKey: [suggestedFollowsByActorQueryKeyRoot], }, diff --git a/src/state/queries/threadgate/index.ts b/src/state/queries/threadgate/index.ts index 403950e0f0..41ba960103 100644 --- a/src/state/queries/threadgate/index.ts +++ b/src/state/queries/threadgate/index.ts @@ -1,4 +1,5 @@ -import {type AppBskyFeedDefs, AppBskyFeedThreadgate, AtUri} from '@atproto/api' +import {type Client} from '@atproto/lex-client' +import {AtUri} from '@atproto/syntax' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {networkRetry, retry} from '#/lib/async/retry' @@ -12,8 +13,9 @@ import { threadgateViewToAllowUISetting, } from '#/state/queries/threadgate/util' import {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread' -import {type SessionAgent, useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies' +import {app, com} from '#/lexicons' import * as bsky from '#/types/bsky' export * from '#/state/queries/threadgate/types' @@ -35,9 +37,9 @@ export function useThreadgateRecordQuery({ initialData, }: { postUri?: string - initialData?: AppBskyFeedThreadgate.Record + initialData?: app.bsky.feed.threadgate.Main } = {}) { - const agent = useAgent() + const pdsClient = usePdsClient() return useQuery({ enabled: !!postUri, @@ -46,7 +48,7 @@ export function useThreadgateRecordQuery({ staleTime: STALE.MINUTES.ONE, async queryFn() { return getThreadgateRecord({ - agent, + pdsClient, postUri: postUri!, }) }, @@ -63,7 +65,7 @@ export function useThreadgateViewQuery({ initialData, }: { postUri?: string - initialData?: AppBskyFeedDefs.ThreadgateView + initialData?: app.bsky.feed.defs.ThreadgateView } = {}) { const getPost = useGetPost() @@ -80,24 +82,23 @@ export function useThreadgateViewQuery({ } export async function getThreadgateRecord({ - agent, + pdsClient, postUri, }: { - agent: SessionAgent + pdsClient: Client postUri: string -}): Promise { +}): Promise { const urip = new AtUri(postUri) if (!urip.host.startsWith('did:')) { - const res = await agent.resolveHandle({ - handle: urip.host, + const {did} = await pdsClient.call(com.atproto.identity.resolveHandle, { + handle: urip.host as `${string}.${string}`, }) - // @ts-expect-error TODO new-sdk-migration - urip.host = res.data.did + urip.host = did } try { - const {data} = await retry( + const data = await retry( 2, e => { /* @@ -111,17 +112,14 @@ export async function getThreadgateRecord({ return true }, () => - agent.api.com.atproto.repo.getRecord({ + pdsClient.call(com.atproto.repo.getRecord, { repo: urip.host, collection: 'app.bsky.feed.threadgate', rkey: urip.rkey, }), ) - if ( - data.value && - bsky.validate(data.value, AppBskyFeedThreadgate.validateRecord) - ) { + if (data.value && bsky.matches(app.bsky.feed.threadgate, data.value)) { return data.value } else { return null @@ -141,13 +139,13 @@ export async function getThreadgateRecord({ } export async function writeThreadgateRecord({ - agent, + pdsClient, postUri, threadgate, }: { - agent: SessionAgent + pdsClient: Client postUri: string - threadgate: AppBskyFeedThreadgate.Record + threadgate: app.bsky.feed.threadgate.Main }) { const postUrip = new AtUri(postUri) const record = createThreadgateRecord({ @@ -157,8 +155,8 @@ export async function writeThreadgateRecord({ }) await networkRetry(2, () => - agent.api.com.atproto.repo.putRecord({ - repo: agent.session!.did, + pdsClient.call(com.atproto.repo.putRecord, { + repo: pdsClient.assertDid, collection: 'app.bsky.feed.threadgate', rkey: postUrip.rkey, record, @@ -168,25 +166,25 @@ export async function writeThreadgateRecord({ export async function upsertThreadgate( { - agent, + pdsClient, postUri, }: { - agent: SessionAgent + pdsClient: Client postUri: string }, callback: ( - threadgate: AppBskyFeedThreadgate.Record | null, - ) => Promise, + threadgate: app.bsky.feed.threadgate.Main | null, + ) => Promise, ) { const prev = await getThreadgateRecord({ - agent, + pdsClient, postUri, }) const next = await callback(prev) if (!next) return validateThreadgateRecordOrThrow(next) await writeThreadgateRecord({ - agent, + pdsClient, postUri, threadgate: next, }) @@ -196,15 +194,15 @@ export async function upsertThreadgate( * Update the allow list for a threadgate record. */ export async function updateThreadgateAllow({ - agent, + pdsClient, postUri, allow, }: { - agent: SessionAgent + pdsClient: Client postUri: string allow: ThreadgateAllowUISetting[] }) { - return upsertThreadgate({agent, postUri}, async prev => { + return upsertThreadgate({pdsClient, postUri}, async prev => { if (prev) { return { ...prev, @@ -220,7 +218,7 @@ export async function updateThreadgateAllow({ } export function useSetThreadgateAllowMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const getPost = useGetPost() const updatePostThreadThreadgate = useUpdatePostThreadThreadgateQueryCache() @@ -233,7 +231,7 @@ export function useSetThreadgateAllowMutation() { postUri: string allow: ThreadgateAllowUISetting[] }) => { - return upsertThreadgate({agent, postUri}, async prev => { + return upsertThreadgate({pdsClient, postUri}, async prev => { if (prev) { return { ...prev, @@ -248,7 +246,7 @@ export function useSetThreadgateAllowMutation() { }) }, async onSuccess(_, {postUri, allow}) { - const data = await retry( + const data = await retry( 5, // 5 tries _e => true, async () => { @@ -285,7 +283,7 @@ export function useSetThreadgateAllowMutation() { } export function useToggleReplyVisibilityMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const hiddenReplies = useThreadgateHiddenReplyUrisAPI() @@ -305,7 +303,7 @@ export function useToggleReplyVisibilityMutation() { hiddenReplies.removeHiddenReplyUri(replyUri) } - await upsertThreadgate({agent, postUri}, async prev => { + await upsertThreadgate({pdsClient, postUri}, async prev => { if (prev) { if (action === 'hide') { return mergeThreadgateRecords(prev, { @@ -358,9 +356,9 @@ export class InvalidInteractionSettingsError extends Error { } export function validateThreadgateRecordOrThrow( - record: AppBskyFeedThreadgate.Record, + record: app.bsky.feed.threadgate.Main, ) { - const result = AppBskyFeedThreadgate.validateRecord(record) + const result = bsky.safeParse(app.bsky.feed.threadgate, record) if (result.success) { if ((result.value.hiddenReplies?.length ?? 0) > MAX_HIDDEN_REPLIES) { diff --git a/src/state/queries/threadgate/util.ts b/src/state/queries/threadgate/util.ts index 807afef76f..7d822d7cc1 100644 --- a/src/state/queries/threadgate/util.ts +++ b/src/state/queries/threadgate/util.ts @@ -1,26 +1,27 @@ -import {type AppBskyFeedDefs, AppBskyFeedThreadgate} from '@atproto/api' +import {type AtUriString, toDatetimeString} from '@atproto/syntax' import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate/types' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' export function threadgateViewToAllowUISetting( - threadgateView: AppBskyFeedDefs.ThreadgateView | undefined, + threadgateView: app.bsky.feed.defs.ThreadgateView | undefined, ): ThreadgateAllowUISetting[] { // Validate the record for clarity, since backwards compat code is a little confusing const threadgate = threadgateView && - bsky.validate(threadgateView.record, AppBskyFeedThreadgate.validateRecord) + bsky.matches(app.bsky.feed.threadgate, threadgateView.record) ? threadgateView.record : undefined return threadgateRecordToAllowUISetting(threadgate) } /** - * Converts a full {@link AppBskyFeedThreadgate.Record} to a list of + * Converts a full {@link app.bsky.feed.threadgate.Main} to a list of * {@link ThreadgateAllowUISetting}, for use by app UI. */ export function threadgateRecordToAllowUISetting( - threadgate: AppBskyFeedThreadgate.Record | undefined, + threadgate: app.bsky.feed.threadgate.Main | undefined, ): ThreadgateAllowUISetting[] { /* * If `threadgate` doesn't exist (default), or if `threadgate.allow === undefined`, it means @@ -40,13 +41,13 @@ export function threadgateRecordToAllowUISetting( const settings: ThreadgateAllowUISetting[] = threadgate.allow .map(allow => { let setting: ThreadgateAllowUISetting | undefined - if (AppBskyFeedThreadgate.isMentionRule(allow)) { + if (bsky.isType(app.bsky.feed.threadgate.mentionRule, allow)) { setting = {type: 'mention'} - } else if (AppBskyFeedThreadgate.isFollowingRule(allow)) { + } else if (bsky.isType(app.bsky.feed.threadgate.followingRule, allow)) { setting = {type: 'following'} - } else if (AppBskyFeedThreadgate.isListRule(allow)) { + } else if (bsky.isType(app.bsky.feed.threadgate.listRule, allow)) { setting = {type: 'list', list: allow.list} - } else if (AppBskyFeedThreadgate.isFollowerRule(allow)) { + } else if (bsky.isType(app.bsky.feed.threadgate.followerRule, allow)) { setting = {type: 'followers'} } return setting @@ -57,7 +58,7 @@ export function threadgateRecordToAllowUISetting( /** * Converts an array of {@link ThreadgateAllowUISetting} to the `allow` prop on - * {@link AppBskyFeedThreadgate.Record}. + * {@link app.bsky.feed.threadgate.Main}. * * If the `allow` property on the record is undefined, we infer that to mean * that everyone can reply. If it's an empty array, we infer that to mean that @@ -65,12 +66,12 @@ export function threadgateRecordToAllowUISetting( */ export function threadgateAllowUISettingToAllowRecordValue( threadgate: ThreadgateAllowUISetting[], -): AppBskyFeedThreadgate.Record['allow'] { +): app.bsky.feed.threadgate.Main['allow'] { if (threadgate.find(v => v.type === 'everybody')) { return undefined } - let allow: Exclude = [] + let allow: Exclude = [] if (!threadgate.find(v => v.type === 'nobody')) { for (const rule of threadgate) { @@ -83,7 +84,7 @@ export function threadgateAllowUISettingToAllowRecordValue( } else if (rule.type === 'list') { allow.push({ $type: 'app.bsky.feed.threadgate#listRule', - list: rule.list, + list: rule.list as AtUriString, }) } } @@ -93,18 +94,20 @@ export function threadgateAllowUISettingToAllowRecordValue( } /** - * Merges two {@link AppBskyFeedThreadgate.Record} objects, combining their + * Merges two {@link app.bsky.feed.threadgate.Main} objects, combining their * `allow` and `hiddenReplies` arrays and de-deduplicating them. * * Note: `allow` can be undefined here, be sure you don't accidentally set it * to an empty array. See other comments in this file. */ export function mergeThreadgateRecords( - prev: AppBskyFeedThreadgate.Record, - next: Partial, -): AppBskyFeedThreadgate.Record { + prev: app.bsky.feed.threadgate.Main, + next: Omit, 'hiddenReplies'> & { + hiddenReplies?: string[] + }, +): app.bsky.feed.threadgate.Main { // can be undefined if everyone can reply! - const allow: AppBskyFeedThreadgate.Record['allow'] | undefined = + const allow: app.bsky.feed.threadgate.Main['allow'] | undefined = prev.allow || next.allow ? [...(prev.allow || []), ...(next.allow || [])].filter( (v, i, a) => a.findIndex(t => t.$type === v.$type) === i, @@ -112,7 +115,7 @@ export function mergeThreadgateRecords( : undefined const hiddenReplies = Array.from( new Set([...(prev.hiddenReplies || []), ...(next.hiddenReplies || [])]), - ) + ) as AtUriString[] return createThreadgateRecord({ post: prev.post, @@ -122,21 +125,28 @@ export function mergeThreadgateRecords( } /** - * Create a new {@link AppBskyFeedThreadgate.Record} object with the given - * properties. + * Create a new {@link app.bsky.feed.threadgate.Main} object with the given + * properties. `post` is accepted as a plain string (callers hold raw AT-URIs) + * and asserted to the branded `AtUriString` here. */ export function createThreadgateRecord( - threadgate: Partial, -): AppBskyFeedThreadgate.Record { + threadgate: Omit< + Partial, + 'post' | 'hiddenReplies' + > & { + post?: string + hiddenReplies?: string[] + }, +): app.bsky.feed.threadgate.Main { if (!threadgate.post) { throw new Error('Cannot create a threadgate record without a post URI') } return { $type: 'app.bsky.feed.threadgate', - post: threadgate.post, - createdAt: new Date().toISOString(), + post: threadgate.post as AtUriString, + createdAt: toDatetimeString(new Date()), allow: threadgate.allow, // can be undefined! - hiddenReplies: threadgate.hiddenReplies || [], + hiddenReplies: (threadgate.hiddenReplies || []) as AtUriString[], } } diff --git a/src/state/queries/trending/useGetSuggestedOnboardingUsersQuery.ts b/src/state/queries/trending/useGetSuggestedOnboardingUsersQuery.ts index 3697c51a7c..bbdb762cf7 100644 --- a/src/state/queries/trending/useGetSuggestedOnboardingUsersQuery.ts +++ b/src/state/queries/trending/useGetSuggestedOnboardingUsersQuery.ts @@ -1,7 +1,3 @@ -import { - type AppBskyActorDefs, - type AppBskyUnspeccedGetSuggestedOnboardingUsers, -} from '@atproto/api' import {type QueryClient, useQuery} from '@tanstack/react-query' import {createBskyTopicsHeader} from '#/lib/api/feed/utils' @@ -10,6 +6,7 @@ import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' import {usePreferencesQuery} from '#/state/queries/preferences' import {useAgent} from '#/state/session' +import {type app} from '#/lexicons' export type QueryProps = { category?: string | null @@ -66,13 +63,12 @@ export function useGetSuggestedOnboardingUsersQuery(props: QueryProps) { export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { - const responses = - queryClient.getQueriesData( - { - queryKey: [getSuggestedOnboardingUsersQueryKeyRoot], - }, - ) +): Generator { + const responses = queryClient.getQueriesData<{ + actors: app.bsky.actor.defs.ProfileView[] + }>({ + queryKey: [getSuggestedOnboardingUsersQueryKeyRoot], + }) for (const [_key, response] of responses) { if (!response) { continue diff --git a/src/state/queries/trending/useGetSuggestedUsersForDiscoverQuery.ts b/src/state/queries/trending/useGetSuggestedUsersForDiscoverQuery.ts index c18a4d05c4..62e28fc781 100644 --- a/src/state/queries/trending/useGetSuggestedUsersForDiscoverQuery.ts +++ b/src/state/queries/trending/useGetSuggestedUsersForDiscoverQuery.ts @@ -1,7 +1,3 @@ -import { - type AppBskyActorDefs, - type AppBskyUnspeccedGetSuggestedUsersForDiscover, -} from '@atproto/api' import {type QueryClient, useQuery} from '@tanstack/react-query' import { @@ -13,6 +9,7 @@ import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' import {usePreferencesQuery} from '#/state/queries/preferences' import {useAgent} from '#/state/session' +import {type app} from '#/lexicons' export type QueryProps = { limit?: number @@ -58,13 +55,12 @@ export function useGetSuggestedUsersForDiscoverQuery(props: QueryProps = {}) { export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { - const responses = - queryClient.getQueriesData( - { - queryKey: [getSuggestedUsersForDiscoverQueryKeyRoot], - }, - ) +): Generator { + const responses = queryClient.getQueriesData<{ + actors: app.bsky.actor.defs.ProfileView[] + }>({ + queryKey: [getSuggestedUsersForDiscoverQueryKeyRoot], + }) for (const [_key, response] of responses) { if (!response) { continue diff --git a/src/state/queries/trending/useGetSuggestedUsersForExploreQuery.ts b/src/state/queries/trending/useGetSuggestedUsersForExploreQuery.ts index a2e56573e5..eebe77790d 100644 --- a/src/state/queries/trending/useGetSuggestedUsersForExploreQuery.ts +++ b/src/state/queries/trending/useGetSuggestedUsersForExploreQuery.ts @@ -1,7 +1,3 @@ -import { - type AppBskyActorDefs, - type AppBskyUnspeccedGetSuggestedUsersForExplore, -} from '@atproto/api' import {type QueryClient, useQuery} from '@tanstack/react-query' import { @@ -13,6 +9,7 @@ import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' import {usePreferencesQuery} from '#/state/queries/preferences' import {useAgent} from '#/state/session' +import {type app} from '#/lexicons' export type QueryProps = { category?: string | null @@ -60,13 +57,12 @@ export function useGetSuggestedUsersForExploreQuery(props: QueryProps = {}) { export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { - const responses = - queryClient.getQueriesData( - { - queryKey: [getSuggestedUsersForExploreQueryKeyRoot], - }, - ) +): Generator { + const responses = queryClient.getQueriesData<{ + actors: app.bsky.actor.defs.ProfileView[] + }>({ + queryKey: [getSuggestedUsersForExploreQueryKeyRoot], + }) for (const [_key, response] of responses) { if (!response) { continue diff --git a/src/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery.ts b/src/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery.ts index 73fdf6b451..cd98552d7f 100644 --- a/src/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery.ts +++ b/src/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery.ts @@ -1,7 +1,3 @@ -import { - type AppBskyActorDefs, - type AppBskyUnspeccedGetSuggestedUsersForSeeMore, -} from '@atproto/api' import {type QueryClient, useQuery} from '@tanstack/react-query' import { @@ -13,6 +9,7 @@ import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' import {usePreferencesQuery} from '#/state/queries/preferences' import {useAgent} from '#/state/session' +import {type app} from '#/lexicons' export type QueryProps = { category?: string | null @@ -66,13 +63,12 @@ export function useGetSuggestedUsersForSeeMoreQuery(props: QueryProps = {}) { export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { - const responses = - queryClient.getQueriesData( - { - queryKey: [getSuggestedUsersForSeeMoreQueryKeyRoot], - }, - ) +): Generator { + const responses = queryClient.getQueriesData<{ + actors: app.bsky.actor.defs.ProfileView[] + }>({ + queryKey: [getSuggestedUsersForSeeMoreQueryKeyRoot], + }) for (const [_key, response] of responses) { if (!response) { continue diff --git a/src/state/queries/trending/useGetTrendsQuery.ts b/src/state/queries/trending/useGetTrendsQuery.ts index e7d6806b3e..d7f269410a 100644 --- a/src/state/queries/trending/useGetTrendsQuery.ts +++ b/src/state/queries/trending/useGetTrendsQuery.ts @@ -1,5 +1,5 @@ import {useCallback, useMemo} from 'react' -import {hasMutedWord} from '@atproto/api' +import {hasMutedWord} from '@bsky.app/sdk/moderation' import {useQuery} from '@tanstack/react-query' import { diff --git a/src/state/queries/usePostThread/const.ts b/src/state/queries/usePostThread/const.ts index 9b74361307..f693a13878 100644 --- a/src/state/queries/usePostThread/const.ts +++ b/src/state/queries/usePostThread/const.ts @@ -1,27 +1,24 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import {type AppBskyUnspeccedGetPostThreadV2} from '@atproto/api' - /** - * See the `below` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams} + * See the `below` param on `app.bsky.unspecced.getPostThreadV2.$Params` */ export const LINEAR_VIEW_BELOW = 10 /** - * See the `branchingFactor` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams} + * See the `branchingFactor` param on `app.bsky.unspecced.getPostThreadV2.$Params` */ export const LINEAR_VIEW_BF = 1 /** - * See the `below` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams} + * See the `below` param on `app.bsky.unspecced.getPostThreadV2.$Params` */ export const TREE_VIEW_BELOW = 4 /** - * See the `branchingFactor` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams} + * See the `branchingFactor` param on `app.bsky.unspecced.getPostThreadV2.$Params` */ export const TREE_VIEW_BF = undefined /** - * See the `below` param on {@link AppBskyUnspeccedGetPostThreadV2.QueryParams} + * See the `below` param on `app.bsky.unspecced.getPostThreadV2.$Params` */ export const TREE_VIEW_BELOW_DESKTOP = 6 diff --git a/src/state/queries/usePostThread/index.ts b/src/state/queries/usePostThread/index.ts index ceb87fe47b..9382060bc9 100644 --- a/src/state/queries/usePostThread/index.ts +++ b/src/state/queries/usePostThread/index.ts @@ -1,4 +1,5 @@ import {useCallback, useMemo, useState} from 'react' +import {type AtUriString} from '@atproto/syntax' import {useQuery, useQueryClient} from '@tanstack/react-query' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -27,10 +28,11 @@ import { } from '#/state/queries/usePostThread/types' import {getThreadgateRecord} from '#/state/queries/usePostThread/utils' import * as views from '#/state/queries/usePostThread/views' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {useBreakpoints} from '#/alf' import {IS_WEB} from '#/env' +import {app} from '#/lexicons' export * from '#/state/queries/usePostThread/context' export {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread/queryCache' @@ -38,7 +40,7 @@ export * from '#/state/queries/usePostThread/types' export function usePostThread({anchor}: {anchor?: string}) { const qc = useQueryClient() - const agent = useAgent() + const appviewClient = useAppviewClient() const {hasSession} = useSession() const {gtPhone} = useBreakpoints() const moderationOpts = useModerationOpts() @@ -71,12 +73,15 @@ export function usePostThread({anchor}: {anchor?: string}) { enabled: isThreadPreferencesLoaded && !!anchor && !!moderationOpts, queryKey: postThreadQueryKey, async queryFn(ctx) { - const {data} = await agent.app.bsky.unspecced.getPostThreadV2({ - anchor: anchor!, - branchingFactor: view === 'linear' ? LINEAR_VIEW_BF : TREE_VIEW_BF, - below, - sort: sort, - }) + const data = await appviewClient.call( + app.bsky.unspecced.getPostThreadV2, + { + anchor: anchor! as AtUriString, + branchingFactor: view === 'linear' ? LINEAR_VIEW_BF : TREE_VIEW_BF, + below, + sort: sort, + }, + ) /* * Initialize `ctx.meta` to track if we know we have additional replies @@ -161,10 +166,9 @@ export function usePostThread({anchor}: {anchor?: string}) { enabled: additionalQueryEnabled, queryKey: postThreadOtherQueryKey, async queryFn() { - const {data} = await agent.app.bsky.unspecced.getPostThreadOtherV2({ - anchor: anchor!, + return await appviewClient.call(app.bsky.unspecced.getPostThreadOtherV2, { + anchor: anchor! as AtUriString, }) - return data }, }) const serverOtherThreadItems: ThreadItem[] = useMemo(() => { diff --git a/src/state/queries/usePostThread/queryCache.ts b/src/state/queries/usePostThread/queryCache.ts index 73a8575c6e..5a84bf85a4 100644 --- a/src/state/queries/usePostThread/queryCache.ts +++ b/src/state/queries/usePostThread/queryCache.ts @@ -1,13 +1,6 @@ import {useCallback} from 'react' -import { - type $Typed, - type AppBskyActorDefs, - type AppBskyFeedDefs, - AppBskyUnspeccedDefs, - type AppBskyUnspeccedGetPostThreadOtherV2, - type AppBskyUnspeccedGetPostThreadV2, - AtUri, -} from '@atproto/api' +import {type $Typed} from '@atproto/lex' +import {AtUri} from '@atproto/syntax' import {type QueryClient, useQueryClient} from '@tanstack/react-query' import { @@ -36,6 +29,8 @@ import { embedViewRecordToPostView, getEmbeddedPost, } from '#/state/queries/util' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' export function createCacheMutator({ queryClient, @@ -51,18 +46,18 @@ export function createCacheMutator({ return { insertReplies( parentUri: string, - replies: AppBskyUnspeccedGetPostThreadV2.ThreadItem[], + replies: app.bsky.unspecced.getPostThreadV2.ThreadItem[], ) { /* * Main thread query mutator. */ - queryClient.setQueryData( + queryClient.setQueryData( postThreadQueryKey, data => { if (!data) return return { ...data, - thread: mutator([ + thread: mutator([ ...data.thread, ]), } @@ -72,15 +67,15 @@ export function createCacheMutator({ /* * Additional replies query mutator. */ - queryClient.setQueryData( + queryClient.setQueryData( postThreadOtherQueryKey, data => { if (!data) return return { ...data, - thread: mutator([ - ...data.thread, - ]), + thread: mutator( + [...data.thread], + ), } }, ) @@ -89,7 +84,10 @@ export function createCacheMutator({ for (let i = 0; i < thread.length; i++) { const parent = thread[i] - if (!AppBskyUnspeccedDefs.isThreadItemPost(parent.value)) continue + if ( + !bsky.isType(app.bsky.unspecced.defs.threadItemPost, parent.value) + ) + continue if (parent.uri !== parentUri) continue /* @@ -124,7 +122,8 @@ export function createCacheMutator({ const isParentRoot = parent.depth === 0 const isParentBelowRoot = parent.depth > 0 const optimisticReply = replies.at(0) - const opIsReplier = AppBskyUnspeccedDefs.isThreadItemPost( + const opIsReplier = bsky.isType( + app.bsky.unspecced.defs.threadItemPost, optimisticReply?.value, ) ? opDid === optimisticReply.value.post.author.did @@ -172,8 +171,8 @@ export function createCacheMutator({ * Unused atm, post shadow does the trick, but it would be nice to clean up * the whole sub-tree on deletes. */ - deletePost(post: AppBskyUnspeccedGetPostThreadV2.ThreadItem) { - queryClient.setQueryData( + deletePost(post: app.bsky.unspecced.getPostThreadV2.ThreadItem) { + queryClient.setQueryData( postThreadQueryKey, queryData => { if (!queryData) return @@ -182,7 +181,10 @@ export function createCacheMutator({ for (let i = 0; i < thread.length; i++) { const existingPost = thread[i] - if (!AppBskyUnspeccedDefs.isThreadItemPost(post.value)) continue + if ( + !bsky.isType(app.bsky.unspecced.defs.threadItemPost, post.value) + ) + continue if (existingPost.uri === post.uri) { const branch = getBranch(thread, i, existingPost.depth) @@ -204,7 +206,7 @@ export function createCacheMutator({ export function getThreadPlaceholder( queryClient: QueryClient, uri: string, -): $Typed | void { +): $Typed | void { let partial for (let item of getThreadPlaceholderCandidates(queryClient, uri)) { /* @@ -231,8 +233,8 @@ export function* getThreadPlaceholderCandidates( uri: string, ): Generator< $Typed< - Omit & { - value: $Typed + Omit & { + value: $Typed } >, void @@ -253,33 +255,37 @@ export function* getThreadPlaceholderCandidates( for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { yield postViewToThreadPlaceholder(post) } + /* + * TODO(phase4): drop toLex once the feed/quote/search/bookmarks/explore + * `findAllPostsInQueryData` generators are migrated to `#/lexicons` types. + */ for (let post of findAllPostsInFeedQueryData(queryClient, uri)) { - yield postViewToThreadPlaceholder(post) + yield postViewToThreadPlaceholder(bsky.toLex(post)) } for (let post of findAllPostsInQuoteQueryData(queryClient, uri)) { - yield postViewToThreadPlaceholder(post) + yield postViewToThreadPlaceholder(bsky.toLex(post)) } for (let post of findAllPostsInSearchQueryData(queryClient, uri)) { - yield postViewToThreadPlaceholder(post) + yield postViewToThreadPlaceholder(bsky.toLex(post)) } for (let post of findAllPostsInBookmarksQueryData(queryClient, uri)) { - yield postViewToThreadPlaceholder(post) + yield postViewToThreadPlaceholder(bsky.toLex(post)) } for (let post of findAllPostsInExploreFeedPreviewsQueryData( queryClient, uri, )) { - yield postViewToThreadPlaceholder(post) + yield postViewToThreadPlaceholder(bsky.toLex(post)) } } export function* findAllPostsInQueryData( queryClient: QueryClient, uri: string, -): Generator { +): Generator { const atUri = new AtUri(uri) const queryDatas = - queryClient.getQueriesData({ + queryClient.getQueriesData({ queryKey: [postThreadQueryKeyRoot], }) @@ -289,14 +295,18 @@ export function* findAllPostsInQueryData( const {thread} = queryData for (const item of thread) { - if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { + if (bsky.isType(app.bsky.unspecced.defs.threadItemPost, item.value)) { if (didOrHandleUriMatches(atUri, item.value.post)) { yield item.value.post } const qp = getEmbeddedPost(item.value.post.embed) if (qp && didOrHandleUriMatches(atUri, qp)) { - yield embedViewRecordToPostView(qp) + /* + * TODO(phase4): drop toLex once `embedViewRecordToPostView` in + * `#/state/queries/util` is migrated to `#/lexicons` types. + */ + yield bsky.toLex(embedViewRecordToPostView(qp)) } } } @@ -306,9 +316,9 @@ export function* findAllPostsInQueryData( export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const queryDatas = - queryClient.getQueriesData({ + queryClient.getQueriesData({ queryKey: [postThreadQueryKeyRoot], }) @@ -318,14 +328,18 @@ export function* findAllProfilesInQueryData( const {thread} = queryData for (const item of thread) { - if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { + if (bsky.isType(app.bsky.unspecced.defs.threadItemPost, item.value)) { if (item.value.post.author.did === did) { yield item.value.post.author } const qp = getEmbeddedPost(item.value.post.embed) if (qp && qp.author.did === did) { - yield qp.author + /* + * TODO(phase4): drop toLex once `getEmbeddedPost` in + * `#/state/queries/util` is migrated to `#/lexicons` types. + */ + yield bsky.toLex(qp.author) } } } @@ -337,14 +351,15 @@ export function useUpdatePostThreadThreadgateQueryCache() { const context = usePostThreadContext() return useCallback( - (threadgate: AppBskyFeedDefs.ThreadgateView) => { + (threadgate: app.bsky.feed.defs.ThreadgateView) => { if (!context) return function mutator(thread: ApiThreadItem[]): T[] { for (let i = 0; i < thread.length; i++) { const item = thread[i] - if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) continue + if (!bsky.isType(app.bsky.unspecced.defs.threadItemPost, item.value)) + continue if (item.depth === 0) { thread.splice(i, 1, { @@ -363,13 +378,13 @@ export function useUpdatePostThreadThreadgateQueryCache() { return thread as T[] } - qc.setQueryData( + qc.setQueryData( context.postThreadQueryKey, data => { if (!data) return return { ...data, - thread: mutator([ + thread: mutator([ ...data.thread, ]), } diff --git a/src/state/queries/usePostThread/traversal.ts b/src/state/queries/usePostThread/traversal.ts index 81abd958fb..107697f76f 100644 --- a/src/state/queries/usePostThread/traversal.ts +++ b/src/state/queries/usePostThread/traversal.ts @@ -1,4 +1,4 @@ -import {AppBskyUnspeccedDefs, type ModerationOpts} from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import { type ApiThreadItem, @@ -14,6 +14,8 @@ import { storeTraversalMetadata, } from '#/state/queries/usePostThread/utils' import * as views from '#/state/queries/usePostThread/views' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' export function sortAndAnnotateThreadItems( thread: ApiThreadItem[], @@ -45,7 +47,7 @@ export function sortAndAnnotateThreadItems( let parentMetadata: TraversalMetadata | undefined let metadata: TraversalMetadata | undefined - if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { + if (bsky.isType(app.bsky.unspecced.defs.threadItemPost, item.value)) { parentMetadata = metadatas.get( getPostRecord(item.value.post).reply?.parent?.uri || '', ) @@ -64,13 +66,24 @@ export function sortAndAnnotateThreadItems( * _up_ from there. */ } else if (item.depth === 0) { - if (AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item.value)) { + if ( + bsky.isType( + app.bsky.unspecced.defs.threadItemNoUnauthenticated, + item.value, + ) + ) { threadItems.push(views.threadPostNoUnauthenticated(item)) - } else if (AppBskyUnspeccedDefs.isThreadItemNotFound(item.value)) { + } else if ( + bsky.isType(app.bsky.unspecced.defs.threadItemNotFound, item.value) + ) { threadItems.push(views.threadPostNotFound(item)) - } else if (AppBskyUnspeccedDefs.isThreadItemBlocked(item.value)) { + } else if ( + bsky.isType(app.bsky.unspecced.defs.threadItemBlocked, item.value) + ) { threadItems.push(views.threadPostBlocked(item)) - } else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { + } else if ( + bsky.isType(app.bsky.unspecced.defs.threadItemPost, item.value) + ) { const post = views.threadPost({ uri: item.uri, depth: item.depth, @@ -84,7 +97,10 @@ export function sortAndAnnotateThreadItems( const parent = thread[pi] if ( - AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(parent.value) + bsky.isType( + app.bsky.unspecced.defs.threadItemNoUnauthenticated, + parent.value, + ) ) { const post = views.threadPostNoUnauthenticated(parent) post.ui = getThreadPostNoUnauthenticatedUI({ @@ -96,13 +112,22 @@ export function sortAndAnnotateThreadItems( threadItems.unshift(post) // for now, break parent traversal at first no-unauthed break parentTraversal - } else if (AppBskyUnspeccedDefs.isThreadItemNotFound(parent.value)) { + } else if ( + bsky.isType( + app.bsky.unspecced.defs.threadItemNotFound, + parent.value, + ) + ) { threadItems.unshift(views.threadPostNotFound(parent)) break parentTraversal - } else if (AppBskyUnspeccedDefs.isThreadItemBlocked(parent.value)) { + } else if ( + bsky.isType(app.bsky.unspecced.defs.threadItemBlocked, parent.value) + ) { threadItems.unshift(views.threadPostBlocked(parent)) break parentTraversal - } else if (AppBskyUnspeccedDefs.isThreadItemPost(parent.value)) { + } else if ( + bsky.isType(app.bsky.unspecced.defs.threadItemPost, parent.value) + ) { threadItems.unshift( views.threadPost({ uri: parent.uri, @@ -122,16 +147,21 @@ export function sortAndAnnotateThreadItems( * we could. */ const shouldBreak = - AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item.value) || - AppBskyUnspeccedDefs.isThreadItemNotFound(item.value) || - AppBskyUnspeccedDefs.isThreadItemBlocked(item.value) + bsky.isType( + app.bsky.unspecced.defs.threadItemNoUnauthenticated, + item.value, + ) || + bsky.isType(app.bsky.unspecced.defs.threadItemNotFound, item.value) || + bsky.isType(app.bsky.unspecced.defs.threadItemBlocked, item.value) if (shouldBreak) { const branch = getBranch(thread, i, item.depth) // could insert tombstone i = branch.end continue traversal - } else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { + } else if ( + bsky.isType(app.bsky.unspecced.defs.threadItemPost, item.value) + ) { if (parentMetadata) { /* * Set this value before incrementing the `repliesSeenCounter` later @@ -179,7 +209,9 @@ export function sortAndAnnotateThreadItems( for (let ci = startIndex; ci <= branch.end; ci++) { const child = thread[ci] - if (AppBskyUnspeccedDefs.isThreadItemPost(child.value)) { + if ( + bsky.isType(app.bsky.unspecced.defs.threadItemPost, child.value) + ) { const childParentMetadata = metadatas.get( getPostRecord(child.value.post).reply?.parent?.uri || '', ) diff --git a/src/state/queries/usePostThread/types.ts b/src/state/queries/usePostThread/types.ts index 295fd8bd3e..2c2548ff50 100644 --- a/src/state/queries/usePostThread/types.ts +++ b/src/state/queries/usePostThread/types.ts @@ -1,16 +1,10 @@ -import { - type AppBskyFeedDefs, - type AppBskyFeedPost, - type AppBskyFeedThreadgate, - type AppBskyUnspeccedDefs, - type AppBskyUnspeccedGetPostThreadOtherV2, - type AppBskyUnspeccedGetPostThreadV2, - type ModerationDecision, -} from '@atproto/api' +import {type ModerationDecision} from '@bsky.app/sdk/moderation' + +import {type app} from '#/lexicons' export type ApiThreadItem = - | AppBskyUnspeccedGetPostThreadV2.ThreadItem - | AppBskyUnspeccedGetPostThreadOtherV2.ThreadItem + | app.bsky.unspecced.getPostThreadV2.ThreadItem + | app.bsky.unspecced.getPostThreadOtherV2.ThreadItem export const postThreadQueryKeyRoot = 'post-thread-v2' as const @@ -18,13 +12,13 @@ export const createPostThreadQueryKey = (props: PostThreadParams) => [postThreadQueryKeyRoot, props] as const export const createPostThreadOtherQueryKey = ( - props: Omit & { + props: Omit & { anchor?: string }, ) => [postThreadQueryKeyRoot, 'other', props] as const export type PostThreadParams = Pick< - AppBskyUnspeccedGetPostThreadV2.QueryParams, + app.bsky.unspecced.getPostThreadV2.$Params, 'sort' > & { anchor?: string @@ -33,9 +27,9 @@ export type PostThreadParams = Pick< export type UsePostThreadQueryResult = { hasOtherReplies: boolean - thread: AppBskyUnspeccedGetPostThreadV2.ThreadItem[] - threadgate?: Omit & { - record: AppBskyFeedThreadgate.Record + thread: app.bsky.unspecced.getPostThreadV2.ThreadItem[] + threadgate?: Omit & { + record: app.bsky.feed.threadgate.Main } } @@ -45,9 +39,9 @@ export type ThreadItem = key: string uri: string depth: number - value: Omit & { - post: Omit & { - record: AppBskyFeedPost.Record + value: Omit & { + post: Omit & { + record: app.bsky.feed.post.Main } } isBlurred: boolean @@ -67,7 +61,7 @@ export type ThreadItem = key: string uri: string depth: number - value: AppBskyUnspeccedDefs.ThreadItemNoUnauthenticated + value: app.bsky.unspecced.defs.ThreadItemNoUnauthenticated ui: { showParentReplyLine: boolean showChildReplyLine: boolean @@ -78,14 +72,14 @@ export type ThreadItem = key: string uri: string depth: number - value: AppBskyUnspeccedDefs.ThreadItemNotFound + value: app.bsky.unspecced.defs.ThreadItemNotFound } | { type: 'threadPostBlocked' key: string uri: string depth: number - value: AppBskyUnspeccedDefs.ThreadItemBlocked + value: app.bsky.unspecced.defs.ThreadItemBlocked } | { type: 'replyComposer' diff --git a/src/state/queries/usePostThread/utils.ts b/src/state/queries/usePostThread/utils.ts index be3be36b85..dde80db181 100644 --- a/src/state/queries/usePostThread/utils.ts +++ b/src/state/queries/usePostThread/utils.ts @@ -1,38 +1,24 @@ -import { - type AppBskyFeedDefs, - AppBskyFeedPost, - AppBskyFeedThreadgate, - AppBskyUnspeccedDefs, - type AppBskyUnspeccedGetPostThreadV2, - AtUri, -} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import { type ApiThreadItem, type ThreadItem, type TraversalMetadata, } from '#/state/queries/usePostThread/types' +import {app} from '#/lexicons' import {isDevMode} from '#/storage/hooks/dev-mode' import * as bsky from '#/types/bsky' export function getThreadgateRecord( - view: AppBskyUnspeccedGetPostThreadV2.OutputSchema['threadgate'], + view: app.bsky.unspecced.getPostThreadV2.$OutputBody['threadgate'], ) { - return bsky.dangerousIsType( - view?.record, - AppBskyFeedThreadgate.isRecord, - ) + return bsky.isType(app.bsky.feed.threadgate, view?.record) ? view?.record : undefined } -export function getRootPostAtUri(post: AppBskyFeedDefs.PostView) { - if ( - bsky.dangerousIsType( - post.record, - AppBskyFeedPost.isRecord, - ) - ) { +export function getRootPostAtUri(post: app.bsky.feed.defs.PostView) { + if (bsky.isType(app.bsky.feed.post, post.record)) { /** * If the record has no `reply` field, it is a root post. */ @@ -45,8 +31,8 @@ export function getRootPostAtUri(post: AppBskyFeedDefs.PostView) { } } -export function getPostRecord(post: AppBskyFeedDefs.PostView) { - return post.record as AppBskyFeedPost.Record +export function getPostRecord(post: app.bsky.feed.defs.PostView) { + return post.record as app.bsky.feed.post.Main } export function getTraversalMetadata({ @@ -60,7 +46,7 @@ export function getTraversalMetadata({ nextItem?: ApiThreadItem parentMetadata?: TraversalMetadata }): TraversalMetadata { - if (!AppBskyUnspeccedDefs.isThreadItemPost(item.value)) { + if (!bsky.isType(app.bsky.unspecced.defs.threadItemPost, item.value)) { throw new Error(`Expected thread item to be a post`) } const repliesCount = item.value.post.replyCount || 0 diff --git a/src/state/queries/usePostThread/views.ts b/src/state/queries/usePostThread/views.ts index 4e7140eab0..d03f49219c 100644 --- a/src/state/queries/usePostThread/views.ts +++ b/src/state/queries/usePostThread/views.ts @@ -1,13 +1,6 @@ -import { - type $Typed, - type AppBskyFeedDefs, - type AppBskyFeedPost, - type AppBskyUnspeccedDefs, - type AppBskyUnspeccedGetPostThreadV2, - AtUri, - moderatePost, - type ModerationOpts, -} from '@atproto/api' +import {type $Typed} from '@atproto/lex' +import {AtUri} from '@atproto/syntax' +import {moderatePost, type ModerationOpts} from '@bsky.app/sdk/moderation' import {makeProfileLink} from '#/lib/routes/links' import { @@ -15,6 +8,7 @@ import { type ThreadItem, type TraversalMetadata, } from '#/state/queries/usePostThread/types' +import {type app} from '#/lexicons' export function threadPostNoUnauthenticated({ uri, @@ -26,7 +20,7 @@ export function threadPostNoUnauthenticated({ key: uri, uri, depth, - value: value as AppBskyUnspeccedDefs.ThreadItemNoUnauthenticated, + value: value as app.bsky.unspecced.defs.ThreadItemNoUnauthenticated, // @ts-ignore populated by the traversal ui: {}, } @@ -42,7 +36,7 @@ export function threadPostNotFound({ key: uri, uri, depth, - value: value as AppBskyUnspeccedDefs.ThreadItemNotFound, + value: value as app.bsky.unspecced.defs.ThreadItemNotFound, } } @@ -56,7 +50,7 @@ export function threadPostBlocked({ key: uri, uri, depth, - value: value as AppBskyUnspeccedDefs.ThreadItemBlocked, + value: value as app.bsky.unspecced.defs.ThreadItemBlocked, } } @@ -69,7 +63,7 @@ export function threadPost({ }: { uri: string depth: number - value: $Typed + value: $Typed moderationOpts: ModerationOpts threadgateHiddenReplies: Set }): Extract { @@ -91,8 +85,8 @@ export function threadPost({ * Do not spread anything here, load bearing for post shadow strict * equality reference checks. */ - post: value.post as Omit & { - record: AppBskyFeedPost.Record + post: value.post as Omit & { + record: app.bsky.feed.post.Main }, }, isBlurred, @@ -161,10 +155,10 @@ export function skeleton({ } export function postViewToThreadPlaceholder( - post: AppBskyFeedDefs.PostView, + post: app.bsky.feed.defs.PostView, ): $Typed< - Omit & { - value: $Typed + Omit & { + value: $Typed } > { return { diff --git a/src/state/queries/util.ts b/src/state/queries/util.ts index 7ea54745c0..a3fe12aec7 100644 --- a/src/state/queries/util.ts +++ b/src/state/queries/util.ts @@ -1,17 +1,11 @@ -import { - type AppBskyActorDefs, - AppBskyEmbedRecord, - AppBskyEmbedRecordWithMedia, - type AppBskyFeedDefs, - AppBskyFeedPost, - type AtUri, -} from '@atproto/api' +import {type AtUri} from '@atproto/syntax' import { type InfiniteData, type QueryClient, type QueryKey, } from '@tanstack/react-query' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' export type StructuredQueryKey> = readonly [ @@ -93,7 +87,7 @@ export async function truncateAndInvalidate( // of the currentUri that is being checked. export function didOrHandleUriMatches( atUri: AtUri, - record: {uri: string; author: AppBskyActorDefs.ProfileViewBasic}, + record: {uri: string; author: app.bsky.actor.defs.ProfileViewBasic}, ) { if (atUri.host.startsWith('did:')) { return atUri.href === record.uri @@ -104,26 +98,19 @@ export function didOrHandleUriMatches( export function getEmbeddedPost( v: unknown, -): AppBskyEmbedRecord.ViewRecord | undefined { - if ( - bsky.dangerousIsType(v, AppBskyEmbedRecord.isView) - ) { +): app.bsky.embed.record.ViewRecord | undefined { + if (bsky.isType(app.bsky.embed.record.view, v)) { if ( - AppBskyEmbedRecord.isViewRecord(v.record) && - AppBskyFeedPost.isRecord(v.record.value) + bsky.isType(app.bsky.embed.record.viewRecord, v.record) && + bsky.isType(app.bsky.feed.post, v.record.value) ) { return v.record } } - if ( - bsky.dangerousIsType( - v, - AppBskyEmbedRecordWithMedia.isView, - ) - ) { + if (bsky.isType(app.bsky.embed.recordWithMedia.view, v)) { if ( - AppBskyEmbedRecord.isViewRecord(v.record.record) && - AppBskyFeedPost.isRecord(v.record.record.value) + bsky.isType(app.bsky.embed.record.viewRecord, v.record.record) && + bsky.isType(app.bsky.feed.post, v.record.record.value) ) { return v.record.record } @@ -131,8 +118,8 @@ export function getEmbeddedPost( } export function embedViewRecordToPostView( - v: AppBskyEmbedRecord.ViewRecord, -): AppBskyFeedDefs.PostView { + v: app.bsky.embed.record.ViewRecord, +): app.bsky.feed.defs.PostView { return { uri: v.uri, cid: v.cid, diff --git a/src/state/queries/verification/useUpdateProfileVerificationCache.ts b/src/state/queries/verification/useUpdateProfileVerificationCache.ts index f5ccf1458b..cb3e3890dd 100644 --- a/src/state/queries/verification/useUpdateProfileVerificationCache.ts +++ b/src/state/queries/verification/useUpdateProfileVerificationCache.ts @@ -1,9 +1,11 @@ import {useCallback} from 'react' +import {type AtIdentifierString} from '@atproto/syntax' import {useQueryClient} from '@tanstack/react-query' import {logger} from '#/logger' import {updateProfileShadow} from '#/state/cache/profile-shadow' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' /** @@ -13,13 +15,13 @@ import type * as bsky from '#/types/bsky' */ export function useUpdateProfileVerificationCache() { const qc = useQueryClient() - const agent = useAgent() + const client = useAppviewClient() return useCallback( async ({profile}: {profile: bsky.profile.AnyProfileView}) => { try { - const {data: updated} = await agent.getProfile({ - actor: profile.did ?? '', + const updated = await client.call(app.bsky.actor.getProfile, { + actor: (profile.did ?? '') as AtIdentifierString, }) updateProfileShadow(qc, profile.did, { verification: updated.verification, @@ -30,6 +32,6 @@ export function useUpdateProfileVerificationCache() { }) } }, - [agent, qc], + [client, qc], ) } diff --git a/src/state/queries/verification/useVerificationCreateMutation.tsx b/src/state/queries/verification/useVerificationCreateMutation.tsx index 083a6f3b54..9ba4df9e46 100644 --- a/src/state/queries/verification/useVerificationCreateMutation.tsx +++ b/src/state/queries/verification/useVerificationCreateMutation.tsx @@ -1,15 +1,19 @@ -import {type AppBskyActorGetProfile} from '@atproto/api' +import {type AtIdentifierString, type DidString} from '@atproto/lex-client' +import {type DatetimeString} from '@atproto/lex-schema' +import {type HandleString} from '@atproto/syntax' import {useMutation} from '@tanstack/react-query' import {until} from '#/lib/async/until' import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {useAnalytics} from '#/analytics' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' export function useVerificationCreateMutation() { const ax = useAnalytics() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const {currentAccount} = useSession() const updateProfileVerificationCache = useUpdateProfileVerificationCache() @@ -19,20 +23,17 @@ export function useVerificationCreateMutation() { throw new Error('User not logged in') } - const {uri} = await agent.app.bsky.graph.verification.create( - {repo: currentAccount.did}, - { - subject: profile.did, - createdAt: new Date().toISOString(), - handle: profile.handle, - displayName: profile.displayName || '', - }, - ) + const {uri} = await pdsClient.create(app.bsky.graph.verification, { + subject: profile.did as DidString, + createdAt: new Date().toISOString() as DatetimeString, + handle: profile.handle as HandleString, + displayName: profile.displayName || '', + }) await until( 5, 1e3, - ({data: profile}: AppBskyActorGetProfile.Response) => { + (profile: app.bsky.actor.getProfile.$OutputBody) => { if ( profile.verification && profile.verification.verifications.find(v => v.uri === uri) @@ -42,7 +43,9 @@ export function useVerificationCreateMutation() { return false }, () => { - return agent.getProfile({actor: profile.did ?? ''}) + return appviewClient.call(app.bsky.actor.getProfile, { + actor: (profile.did ?? '') as AtIdentifierString, + }) }, ) }, diff --git a/src/state/queries/verification/useVerificationsRemoveMutation.tsx b/src/state/queries/verification/useVerificationsRemoveMutation.tsx index 3fa95497fd..e97b40fc89 100644 --- a/src/state/queries/verification/useVerificationsRemoveMutation.tsx +++ b/src/state/queries/verification/useVerificationsRemoveMutation.tsx @@ -1,19 +1,18 @@ -import { - type AppBskyActorDefs, - type AppBskyActorGetProfile, - AtUri, -} from '@atproto/api' +import {type AtIdentifierString} from '@atproto/lex-client' +import {AtUri} from '@atproto/syntax' import {useMutation} from '@tanstack/react-query' import {until} from '#/lib/async/until' import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {useAnalytics} from '#/analytics' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' export function useVerificationsRemoveMutation() { const ax = useAnalytics() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const {currentAccount} = useSession() const updateProfileVerificationCache = useUpdateProfileVerificationCache() @@ -23,7 +22,7 @@ export function useVerificationsRemoveMutation() { verifications, }: { profile: bsky.profile.AnyProfileView - verifications: AppBskyActorDefs.VerificationView[] + verifications: app.bsky.actor.defs.VerificationView[] }) { if (!currentAccount) { throw new Error('User not logged in') @@ -33,8 +32,7 @@ export function useVerificationsRemoveMutation() { await Promise.all( uris.map(uri => { - return agent.app.bsky.graph.verification.delete({ - repo: currentAccount.did, + return pdsClient.delete(app.bsky.graph.verification, { rkey: new AtUri(uri).rkey, }) }), @@ -43,7 +41,7 @@ export function useVerificationsRemoveMutation() { await until( 5, 1e3, - ({data: profile}: AppBskyActorGetProfile.Response) => { + (profile: app.bsky.actor.getProfile.$OutputBody) => { if ( !profile.verification?.verifications.some(v => uris.includes(v.uri)) ) { @@ -52,7 +50,9 @@ export function useVerificationsRemoveMutation() { return false }, () => { - return agent.getProfile({actor: profile.did ?? ''}) + return appviewClient.call(app.bsky.actor.getProfile, { + actor: (profile.did ?? '') as AtIdentifierString, + }) }, ) }, diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 50e9d613ab..0d46f5c4b1 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -50,10 +50,10 @@ import { AppBskyDraftCreateDraft, AppBskyUnspeccedDefs, type AppBskyUnspeccedGetPostThreadV2, - AtUri, ChatBskyGroupDefs, type RichText, } from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {plural} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' @@ -95,8 +95,17 @@ import { } from '#/state/preferences/languages' import {usePreferencesQuery} from '#/state/queries/preferences' import {useProfileQuery} from '#/state/queries/profile' -import {resolveLinkQueryOptions} from '#/state/queries/resolve-link' -import {type SessionAgent, useAgent, useSession} from '#/state/session' +import { + resolveLinkQueryOptions, + useResolveClients, +} from '#/state/queries/resolve-link' +import { + type SessionAgent, + useAgent, + useLexClient, + usePdsClient, + useSession, +} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' import {type ComposerOpts, type OnPostSuccessData} from '#/state/shell/composer' import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' @@ -267,6 +276,9 @@ export const ComposePost = ({ const t = useTheme() const ax = useAnalytics() const agent = useAgent() + const pdsClient = usePdsClient() + const appviewClient = useLexClient() + const resolveClients = useResolveClients() const queryClient = useQueryClient() const currentDid = currentAccount!.did const {closeComposer} = useComposerControls() @@ -955,7 +967,7 @@ export const ComposePost = ({ .map(post => post.embed.link!.uri) const linkQueries = useQueries({ queries: linkUris.map(uri => ({ - ...resolveLinkQueryOptions(agent, uri), + ...resolveLinkQueryOptions(resolveClients, uri), enabled: false, })), }) @@ -1046,12 +1058,16 @@ export const ComposePost = ({ try { logger.info(`composer: posting...`) postUri = ( - await apilib.post(agent, queryClient, { - thread: filteredThread, - replyTo: replyTo?.uri, - onStateChange: setPublishingStage, - langs: currentLanguages, - }) + await apilib.post( + {pdsClient, appviewClient, resolveClients}, + queryClient, + { + thread: filteredThread, + replyTo: replyTo?.uri, + onStateChange: setPublishingStage, + langs: currentLanguages, + }, + ) ).uris[0] // Fire published event for every video that made it into the post. diff --git a/src/view/com/composer/drafts/state/api.ts b/src/view/com/composer/drafts/state/api.ts index 12ed4acf5a..398c416e20 100644 --- a/src/view/com/composer/drafts/state/api.ts +++ b/src/view/com/composer/drafts/state/api.ts @@ -12,6 +12,7 @@ import {shortenLinks} from '#/lib/strings/rich-text-manip' import {type ComposerImage} from '#/state/gallery' import {threadgateAllowUISettingToAllowRecordValue} from '#/state/queries/threadgate/util' import {createPublicAgent} from '#/state/session/agent' +import {getPublicLexClient} from '#/state/session/clients' import { type ComposerState, type EmbedDraft, @@ -137,8 +138,10 @@ async function postDraftToServerPost( // Add quote record embed if (post.embed.quote) { + const agent = createPublicAgent() + const publicClient = getPublicLexClient() const resolved = await resolveLink( - createPublicAgent(), + {appview: publicClient, chat: publicClient, agent}, post.embed.quote.uri, ) if (resolved && resolved.type === 'record') { diff --git a/src/view/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx index 8371ad993a..267d4a4829 100644 --- a/src/view/com/composer/text-input/web/Autocomplete.tsx +++ b/src/view/com/composer/text-input/web/Autocomplete.tsx @@ -1,6 +1,7 @@ import {forwardRef, useEffect, useImperativeHandle, useState} from 'react' import {Pressable, View} from 'react-native' -import {type AppBskyActorDefs, type ModerationOpts} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type ModerationOpts} from '@bsky.app/sdk/moderation' import {Trans} from '@lingui/react/macro' import {ReactRenderer} from '@tiptap/react' import { diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index c307d03221..9f054e0ab0 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -1,10 +1,7 @@ import {type StyleProp, View, type ViewStyle} from 'react-native' -import { - type $Typed, - AppBskyFeedDefs, - type AppBskyGraphDefs, - AtUri, -} from '@atproto/api' +import {type AppBskyFeedDefs, type AppBskyGraphDefs} from '@atproto/api' +import {type $Typed} from '@atproto/lex' +import {AtUri} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' @@ -22,6 +19,8 @@ import {atoms as a, useTheme} from '#/alf' import {Link} from '#/components/Link' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import {MissingFeed} from './MissingFeed' type FeedSourceCardProps = { @@ -46,7 +45,7 @@ export function FeedSourceCard({ }: FeedSourceCardProps) { if (feedData) { let feed: FeedSourceInfo - if (AppBskyFeedDefs.isGeneratorView(feedData)) { + if (bsky.isType(app.bsky.feed.defs.generatorView, feedData)) { feed = hydrateFeedGenerator(feedData) } else { feed = hydrateList(feedData) diff --git a/src/view/com/feeds/MissingFeed.tsx b/src/view/com/feeds/MissingFeed.tsx index bc2af31077..bd0f4f5948 100644 --- a/src/view/com/feeds/MissingFeed.tsx +++ b/src/view/com/feeds/MissingFeed.tsx @@ -1,5 +1,5 @@ import {type StyleProp, View, type ViewStyle} from 'react-native' -import {AtUri} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx index 49abf2f3ff..e65c5d2bca 100644 --- a/src/view/com/notifications/NotificationFeedItem.tsx +++ b/src/view/com/notifications/NotificationFeedItem.tsx @@ -8,25 +8,20 @@ import { TouchableOpacity, View, } from 'react-native' +import {type AppBskyActorDefs, type AppBskyFeedDefs} from '@atproto/api' +import {TID} from '@atproto/common-web' +import {AtUri, type DidString} from '@atproto/syntax' import { - type AppBskyActorDefs, - type AppBskyFeedDefs, - AppBskyFeedPost, - type AppBskyGraphDefs, - AppBskyGraphFollow, - AppBskyGraphStarterpack, - AtUri, moderateProfile, type ModerationDecision, type ModerationOpts, -} from '@atproto/api' -import {TID} from '@atproto/common-web' +} from '@bsky.app/sdk/moderation' import {plural} from '@lingui/core/macro' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS, MAX_POST_LINES} from '#/lib/constants' +import {MAX_POST_LINES} from '#/lib/constants' import {useAnimatedValue} from '#/lib/hooks/useAnimatedValue' import {makeProfileLink} from '#/lib/routes/links' import {type NavigationProp} from '#/lib/routes/types' @@ -38,7 +33,7 @@ import {useProfileShadow} from '#/state/cache/profile-shadow' import {type FeedNotification} from '#/state/queries/notifications/feed' import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache' -import {useAgent, useSession} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard' import {Post} from '#/view/com/post/Post' import {formatCount} from '#/view/com/util/numeric/format' @@ -73,6 +68,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {app, chat} from '#/lexicons' import * as bsky from '#/types/bsky' const MAX_AUTHORS = 5 @@ -153,12 +149,15 @@ let NotificationFeedItem = ({ { profile: item.notification.author, href: makeProfileLink(item.notification.author), - moderation: moderateProfile(item.notification.author, moderationOpts), + moderation: moderateProfile( + bsky.toLex(item.notification.author), + moderationOpts, + ), }, ...(item.additional?.map(({author}) => ({ profile: author, href: makeProfileLink(author), - moderation: moderateProfile(author, moderationOpts), + moderation: moderateProfile(bsky.toLex(author), moderationOpts), })) || []), ].filter( (author, index, arr) => @@ -191,10 +190,7 @@ let NotificationFeedItem = ({ if (item.type !== 'follow') return false if ( item.notification.author.viewer?.following && - bsky.dangerousIsType( - item.notification.record, - AppBskyGraphFollow.isRecord, - ) + bsky.isType(app.bsky.graph.follow, item.notification.record) ) { let followingTimestamp try { @@ -923,21 +919,21 @@ function FollowBackButton({profile}: {profile: AppBskyActorDefs.ProfileView}) { function SayHelloBtn({profile}: {profile: AppBskyActorDefs.ProfileView}) { const {t: l} = useLingui() - const agent = useAgent() + const chatClient = useChatClient() const navigation = useNavigation() const [isLoading, setIsLoading] = useState(false) const onPress = async () => { try { setIsLoading(true) - const res = await agent.api.chat.bsky.convo.getConvoForMembers( + const res = await chatClient.call( + chat.bsky.convo.getConvoForMembers, { - members: [profile.did, agent.session!.did], + members: [profile.did as DidString, chatClient.assertDid], }, - {headers: DM_SERVICE_HEADERS}, ) navigation.navigate('MessagesConversation', { - conversation: res.data.convo.id, + conversation: res.convo.id, }) } catch (e) { logger.error('Failed to get conversation', {safeMessage: e}) @@ -1157,13 +1153,7 @@ function ExpandedAuthorProfileCard({ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) { const t = useTheme() - if ( - post && - bsky.dangerousIsType( - post?.record, - AppBskyFeedPost.isRecord, - ) - ) { + if (post && bsky.isType(app.bsky.feed.post, post?.record)) { const text = post.record.text return ( diff --git a/src/view/com/post-thread/PostQuotes.tsx b/src/view/com/post-thread/PostQuotes.tsx index b16281f9e0..8623b13452 100644 --- a/src/view/com/post-thread/PostQuotes.tsx +++ b/src/view/com/post-thread/PostQuotes.tsx @@ -1,10 +1,6 @@ import {useCallback, useState} from 'react' -import { - type AppBskyFeedDefs, - AppBskyFeedPost, - moderatePost, - type ModerationDecision, -} from '@atproto/api' +import {type AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api' +import {moderatePost, type ModerationDecision} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' @@ -80,7 +76,8 @@ export function PostQuotes({uri}: {uri: string}) { ) { return null } - const moderation = moderatePost(post, moderationOpts) + // TODO(phase4): drop toLex once usePostQuotesQuery emits #/lexicons views + const moderation = moderatePost(toLex(post), moderationOpts) return {post, record: post.record, moderation} }), ) diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index c160cab83f..dc0dc57189 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -1,13 +1,9 @@ import {useCallback, useMemo, useState} from 'react' import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native' -import { - type AppBskyFeedDefs, - AppBskyFeedPost, - AtUri, - moderatePost, - type ModerationDecision, - RichText as RichTextAPI, -} from '@atproto/api' +import {type AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {moderatePost, type ModerationDecision} from '@bsky.app/sdk/moderation' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {useQueryClient} from '@tanstack/react-query' import {MAX_POST_LINES} from '#/lib/constants' @@ -38,6 +34,7 @@ import {TranslatedPost} from '#/components/Post/Translated' import {PostControls} from '#/components/PostControls' import {RichText} from '#/components/RichText' import {SubtleHover} from '#/components/SubtleHover' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' export function Post({ @@ -56,9 +53,7 @@ export function Post({ const moderationOpts = useModerationOpts() const record = useMemo( () => - bsky.validate(post.record, AppBskyFeedPost.validateRecord) - ? post.record - : undefined, + bsky.matches(app.bsky.feed.post, post.record) ? post.record : undefined, [post], ) const postShadowed = usePostShadow(post) @@ -67,13 +62,17 @@ export function Post({ record ? new RichTextAPI({ text: record.text, - facets: record.facets, + // TODO(phase4): drop toLex once the post record producer emits #/lexicons facets + facets: bsky.toLex(record.facets), }) : undefined, [record], ) const moderation = useMemo( - () => (moderationOpts ? moderatePost(post, moderationOpts) : undefined), + () => + moderationOpts + ? moderatePost(bsky.toLex(post), moderationOpts) + : undefined, [moderationOpts, post], ) if (postShadowed === POST_TOMBSTONE) { @@ -137,7 +136,8 @@ function PostInner({ text: record.text, author: post.author, embed: post.embed, - moderation, + // TODO(phase4): drop toLex once the composer state accepts SDK ModerationDecision + moderation: bsky.toLex(moderation), langs: record.langs, }, logContext: 'PostReply', diff --git a/src/view/com/posts/PostFeedErrorMessage.tsx b/src/view/com/posts/PostFeedErrorMessage.tsx index 0298ce228b..9c84a77727 100644 --- a/src/view/com/posts/PostFeedErrorMessage.tsx +++ b/src/view/com/posts/PostFeedErrorMessage.tsx @@ -1,10 +1,7 @@ import {useCallback, useMemo} from 'react' import {View} from 'react-native' -import { - type AppBskyActorDefs, - AppBskyFeedGetAuthorFeed, - AtUri, -} from '@atproto/api' +import {type AppBskyActorDefs, AppBskyFeedGetAuthorFeed} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {msg as msgLingui} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx index 9129c4167f..60e3a5915a 100644 --- a/src/view/com/posts/PostFeedItem.tsx +++ b/src/view/com/posts/PostFeedItem.tsx @@ -4,11 +4,10 @@ import { type AppBskyActorDefs, AppBskyFeedDefs, AppBskyFeedPost, - AppBskyFeedThreadgate, - AtUri, - type ModerationDecision, - RichText as RichTextAPI, } from '@atproto/api' +import {AtUri} from '@atproto/syntax' +import {type ModerationDecision} from '@bsky.app/sdk/moderation' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {useQueryClient} from '@tanstack/react-query' import {type ReasonFeedSource} from '#/lib/api/feed/types' @@ -52,6 +51,7 @@ import {RichText} from '#/components/RichText' import {SubtleHover} from '#/components/SubtleHover' import {useAnalytics} from '#/analytics' import {useActorStatus} from '#/features/liveNow' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' import {PostFeedReason} from './PostFeedReason' @@ -103,7 +103,8 @@ export function PostFeedItem({ () => new RichTextAPI({ text: record.text, - facets: record.facets, + // TODO(phase4): drop toLex once the post record producer emits #/lexicons facets + facets: bsky.toLex(record.facets), }), [record], ) @@ -280,9 +281,9 @@ let FeedItemInner = ({ * If `post[0]` in this slice is the actual root post (not an orphan thread), * then we may have a threadgate record to reference */ - const threadgateRecord = bsky.dangerousIsType( + const threadgateRecord = bsky.isType( + app.bsky.feed.threadgate, rootPost.threadgate?.record, - AppBskyFeedThreadgate.isRecord, ) ? rootPost.threadgate.record : undefined @@ -303,10 +304,7 @@ let FeedItemInner = ({ }) const additionalPostAlerts: AppModerationCause[] = useMemo(() => { const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri) - const rootPostUri = bsky.dangerousIsType( - post.record, - AppBskyFeedPost.isRecord, - ) + const rootPostUri = bsky.isType(app.bsky.feed.post, post.record) ? post.record?.reply?.root?.uri || post.uri : undefined const isControlledByViewer = @@ -476,9 +474,7 @@ let PostContent = ({ const record = useMemo( () => - bsky.validate(post.record, AppBskyFeedPost.validateRecord) - ? post.record - : undefined, + bsky.matches(app.bsky.feed.post, post.record) ? post.record : undefined, [post], ) diff --git a/src/view/com/posts/PostFeedReason.tsx b/src/view/com/posts/PostFeedReason.tsx index fd3ec73470..9d2bd3892c 100644 --- a/src/view/com/posts/PostFeedReason.tsx +++ b/src/view/com/posts/PostFeedReason.tsx @@ -1,5 +1,6 @@ import {StyleSheet, View} from 'react-native' -import {AppBskyFeedDefs, type ModerationDecision} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type ModerationDecision} from '@bsky.app/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -14,6 +15,8 @@ import {Repost_Stroke2_Corner3_Rounded as RepostIcon} from '#/components/icons/R import {Link} from '#/components/Link' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {Text} from '#/components/Typography' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import {FeedNameText} from '../util/FeedInfoText' export function PostFeedReason({ @@ -63,7 +66,7 @@ export function PostFeedReason({ ) } - if (AppBskyFeedDefs.isReasonRepost(reason)) { + if (bsky.isType(app.bsky.feed.defs.reasonRepost, reason)) { const isOwner = reason.by.did === currentAccount?.did const reposter = createSanitizedDisplayName( reason.by, @@ -102,7 +105,7 @@ export function PostFeedReason({ ) } - if (AppBskyFeedDefs.isReasonPin(reason)) { + if (bsky.isType(app.bsky.feed.defs.reasonPin, reason)) { return (