From 75e4180800757c6ef9ab295477ffbdfce1ad9973 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:20 +0300 Subject: [PATCH] [SDK] Migrate the repo writes cluster (#11379) Co-authored-by: Claude Fable 5 --- .../contacts/screens/ViewMatches.tsx | 9 +- .../lists/CreateListFromStarterPackDialog.tsx | 43 ++--- src/features/liveNow/index.tsx | 54 +++--- src/lib/generate-starterpack.ts | 94 +++++------ src/screens/Onboarding/StepFinished/index.tsx | 14 +- .../StepSuggestedAccounts/index.tsx | 10 +- .../StarterPackCard.tsx | 10 +- src/screens/Onboarding/util.ts | 51 +++--- src/screens/Profile/components/GermButton.tsx | 51 +++--- src/screens/Settings/FindContactsSettings.tsx | 9 +- src/screens/StarterPack/StarterPackScreen.tsx | 10 +- src/state/birthdate.ts | 5 +- src/state/queries/activity-subscriptions.ts | 36 ++-- src/state/queries/list-memberships.ts | 37 ++-- src/state/queries/list.ts | 153 +++++++++-------- .../queries/messages/actor-declaration.ts | 22 ++- .../queries/messages/restrictChatSettings.ts | 22 +-- src/state/queries/profile.ts | 27 +-- src/state/queries/starter-packs.ts | 158 ++++++++++-------- .../useVerificationCreateMutation.tsx | 35 ++-- .../useVerificationsRemoveMutation.tsx | 25 +-- src/state/session/create-account.ts | 12 +- 22 files changed, 488 insertions(+), 399 deletions(-) diff --git a/src/components/contacts/screens/ViewMatches.tsx b/src/components/contacts/screens/ViewMatches.tsx index dab3a79c6b..0ef4711919 100644 --- a/src/components/contacts/screens/ViewMatches.tsx +++ b/src/components/contacts/screens/ViewMatches.tsx @@ -21,7 +21,7 @@ import { optimisticRemoveMatch, useMatchesPassthroughQuery, } from '#/state/queries/find-contacts' -import {useAgent, useAppviewClient, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {List, type ListMethods} from '#/view/com/util/List' import {UserAvatar} from '#/view/com/util/UserAvatar' import {OnboardingPosition} from '#/screens/Onboarding/Layout' @@ -91,7 +91,7 @@ export function ViewMatches({ const gutter = useGutters([0, 'wide']) const moderationOpts = useModerationOpts() const queryClient = useQueryClient() - const agent = useAgent() + const pdsClient = usePdsClient() const client = useAppviewClient() const insets = useSafeAreaInsets() const listRef = useRef(null) @@ -127,7 +127,10 @@ export function ViewMatches({ }) } - const uris = await wait(500, bulkWriteFollows(agent, followableDids)) + const uris = await wait( + 500, + bulkWriteFollows(pdsClient, client, followableDids), + ) for (const did of followableDids) { const uri = uris.get(did) diff --git a/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx b/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx index 3aabbec9ff..ceb07fb0e0 100644 --- a/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx +++ b/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx @@ -1,13 +1,14 @@ import {View} from 'react-native' -import { - type $Typed, - type AppBskyGraphDefs, - type AppBskyGraphListitem, - type AppBskyGraphStarterpack, - AtUri, - type ComAtprotoRepoApplyWrites, -} from '@atproto/api' +import {type AppBskyGraphDefs, type AppBskyGraphStarterpack} from '@atproto/api' import {TID} from '@atproto/common-web' +import {type $Typed} from '@atproto/lex' +import { + type AtIdentifierString, + AtUri, + type AtUriString, + type DidString, + toDatetimeString, +} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -20,7 +21,7 @@ import {wait} from '#/lib/async/wait' import {type NavigationProp} from '#/lib/routes/types' import {logger} from '#/logger' import {getAllListMembers} from '#/state/queries/list-members' -import {useAgent, useAppviewClient, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {atoms as a, platform, useTheme, web} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonText} from '#/components/Button' @@ -29,6 +30,7 @@ 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 {CreateOrEditListDialog} from './CreateOrEditListDialog' export function CreateListFromStarterPackDialog({ @@ -40,8 +42,8 @@ export function CreateListFromStarterPackDialog({ }) { const {_} = useLingui() const t = useTheme() - const agent = useAgent() const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const ax = useAnalytics() const {currentAccount} = useSession() const navigation = useNavigation() @@ -80,13 +82,14 @@ export function CreateListFromStarterPackDialog({ ) if (items.length > 0) { - const listitemWrites: $Typed[] = + const listitemWrites: $Typed[] = items.map(item => { - const listitemRecord: $Typed = { + const listitemRecord: $Typed = { $type: 'app.bsky.graph.listitem', - subject: item.subject.did, - list: listUri, - createdAt: new Date().toISOString(), + // the list view is still legacy-typed, so its strings are unbranded + subject: item.subject.did as DidString, + list: listUri as AtUriString, + createdAt: toDatetimeString(new Date()), } return { $type: 'com.atproto.repo.applyWrites#create', @@ -98,8 +101,8 @@ export function CreateListFromStarterPackDialog({ const chunks = chunk(listitemWrites, 50) for (const c of chunks) { - await agent.com.atproto.repo.applyWrites({ - repo: currentAccount.did, + await pdsClient.call(com.atproto.repo.applyWrites, { + repo: currentAccount.did as AtIdentifierString, writes: c, }) } @@ -107,10 +110,10 @@ export function CreateListFromStarterPackDialog({ await until( 5, 1e3, - (res: {data: {items: unknown[]}}) => res.data.items.length > 0, + (res: {items: unknown[]}) => res.items.length > 0, () => - agent.app.bsky.graph.getList({ - list: listUri, + appviewClient.call(app.bsky.graph.getList, { + list: listUri as AtUriString, limit: 1, }), ) diff --git a/src/features/liveNow/index.tsx b/src/features/liveNow/index.tsx index 23f33a5ffc..3b60b688bb 100644 --- a/src/features/liveNow/index.tsx +++ b/src/features/liveNow/index.tsx @@ -2,13 +2,12 @@ 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 l} from '@atproto/lex' +import {type AtIdentifierString, AtUri, toDatetimeString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' @@ -17,18 +16,20 @@ 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 {matchXrpcError} 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 {app, com} from '#/lexicons' import type * as bsky from '#/types/bsky' export * from '#/features/liveNow/utils' @@ -224,6 +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() @@ -232,10 +234,10 @@ 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 + let thumb: l.BlobRef | undefined if (linkMeta.image) { try { @@ -246,7 +248,11 @@ export function useUpsertLiveStatusMutation( img.source.path, img.source.mime, ) - thumb = blob.data.blob + /* + * `uploadBlob` still returns the legacy `BlobRef` class + * instance; it moves to the client with the blob pipeline. + */ + thumb = blob.data.blob as unknown as l.BlobRef } } catch (e: any) { ax.logger.error(`Failed to upload thumbnail for live status`, { @@ -263,7 +269,8 @@ export function useUpsertLiveStatusMutation( $type: 'app.bsky.embed.external#external', title: linkMeta.title ?? '', description: linkMeta.description ?? '', - uri: linkMeta.url, + // `getLinkMeta` returns a plain url string + uri: linkMeta.url as l.UriString, thumb, }, } @@ -271,32 +278,41 @@ export function useUpsertLiveStatusMutation( const record = { $type: 'app.bsky.actor.status', - createdAt: createdAt ?? new Date().toISOString(), + createdAt: toDatetimeString( + createdAt ? new Date(createdAt) : new Date(), + ), 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 + // the session account is still legacy-typed, so its did is unbranded + 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({ + /* + * Stays on the raw `putRecord`, not `pdsClient.put`: the lexicon lets + * `swapRecord` be null (meaning "must not already exist"), while the + * record-helper option type is `string | undefined`. + */ + 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 => + matchXrpcError(e, com.atproto.repo.putRecord) === 'InvalidSwap', }) return { @@ -353,7 +369,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() @@ -362,8 +378,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/lib/generate-starterpack.ts b/src/lib/generate-starterpack.ts index a53e95a6a3..a786fb47ad 100644 --- a/src/lib/generate-starterpack.ts +++ b/src/lib/generate-starterpack.ts @@ -1,11 +1,5 @@ -import { - type $Typed, - type AppBskyActorDefs, - type AppBskyGraphGetStarterPack, - type AtpAgent, - type ComAtprotoRepoApplyWrites, - type Facet, -} from '@atproto/api' +import {type Client} from '@atproto/lex' +import {type AtUriString, toDatetimeString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {useMutation} from '@tanstack/react-query' @@ -14,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 {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 ({ @@ -22,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: AtpAgent + 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: toDatetimeString(new Date()), + 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})), }) @@ -60,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) })(), ]) @@ -106,23 +97,19 @@ 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, + // `create` returns a plain string uri + list: list.uri as AtUriString, + createdAt: toDatetimeString(new Date()), + }) }, 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) }, @@ -138,7 +125,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', @@ -152,14 +139,17 @@ function createListItem({ } async function whenAppViewReady( - agent: AtpAgent, + 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/screens/Onboarding/StepFinished/index.tsx b/src/screens/Onboarding/StepFinished/index.tsx index 05f4ef8626..6f1dea3a4f 100644 --- a/src/screens/Onboarding/StepFinished/index.tsx +++ b/src/screens/Onboarding/StepFinished/index.tsx @@ -8,6 +8,7 @@ import { type Un$Typed, } from '@atproto/api' import {TID} from '@atproto/common-web' +import {type AtUriString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -26,7 +27,7 @@ import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-p import {getAllListMembers} from '#/state/queries/list-members' import {preferencesQueryKey} from '#/state/queries/preferences' import {RQKEY as profileRQKey} from '#/state/queries/profile' -import {useAgent, useAppviewClient} from '#/state/session' +import {useAgent, useAppviewClient, usePdsClient} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import { useActiveStarterPack, @@ -59,6 +60,7 @@ export function StepFinished() { const queryClient = useQueryClient() const agent = useAgent() const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const requestNotificationsPermission = useRequestNotificationsPermission() const activeStarterPack = useActiveStarterPack() const setActiveStarterPack = useSetActiveStarterPack() @@ -102,10 +104,15 @@ export function StepFinished() { await Promise.all([ bulkWriteFollows( - agent, + pdsClient, + appviewClient, [BSKY_APP_ACCOUNT_DID, ...(listItems?.map(i => i.subject.did) ?? [])], starterPack - ? {uri: starterPack.uri, cid: starterPack.cid} + ? // the starter pack view is still legacy-typed + { + uri: starterPack.uri as AtUriString, + cid: starterPack.cid, + } : undefined, ), (async () => { @@ -236,6 +243,7 @@ export function StepFinished() { queryClient, agent, appviewClient, + pdsClient, dispatch, onboardDispatch, activeStarterPack, diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx index 514e892895..f43d06cdd4 100644 --- a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx +++ b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx @@ -14,7 +14,7 @@ import {logger} from '#/logger' import {updateProfileShadow} from '#/state/cache/profile-shadow' import {useLanguagePrefs} from '#/state/preferences' import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import { OnboardingControls, OnboardingPosition, @@ -42,7 +42,8 @@ export function StepSuggestedAccounts() { const t = useTheme() const {gtMobile} = useBreakpoints() const moderationOpts = useModerationOpts() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const {currentAccount} = useSession() const queryClient = useQueryClient() @@ -119,7 +120,10 @@ export function StepSuggestedAccounts() { followingUri: 'pending', }) } - const uris = await wait(1e3, bulkWriteFollows(agent, followableDids)) + const uris = await wait( + 1e3, + bulkWriteFollows(pdsClient, appviewClient, followableDids), + ) for (const did of followableDids) { const uri = uris.get(did) updateProfileShadow(queryClient, did, { diff --git a/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx b/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx index fa296dfbd2..dc208917f5 100644 --- a/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx +++ b/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx @@ -1,6 +1,7 @@ import {useState} from 'react' import {View} from 'react-native' import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -11,7 +12,7 @@ import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted' import {logger} from '#/logger' import {updateProfileShadow} from '#/state/cache/profile-shadow' import {getAllListMembers} from '#/state/queries/list-members' -import {useAgent, useAppviewClient, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {bulkWriteFollows} from '#/screens/Onboarding/util' import {AvatarStack} from '#/screens/Search/components/StarterPackCard' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' @@ -35,8 +36,8 @@ export function StarterPackCard({ const ax = useAnalytics() const {currentAccount} = useSession() const {gtPhone} = useBreakpoints() - const agent = useAgent() const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const record = view.record const [isProcessing, setIsProcessing] = useState(false) @@ -74,8 +75,9 @@ export function StarterPackCard({ let followUris: Map try { - followUris = await bulkWriteFollows(agent, dids, { - uri: view.uri, + followUris = await bulkWriteFollows(pdsClient, appviewClient, dids, { + // the starter pack view is still legacy-typed + uri: view.uri as AtUriString, cid: view.cid, }) } catch (e) { diff --git a/src/screens/Onboarding/util.ts b/src/screens/Onboarding/util.ts index f4ecde3c5d..af8a40f092 100644 --- a/src/screens/Onboarding/util.ts +++ b/src/screens/Onboarding/util.ts @@ -1,37 +1,34 @@ -import { - type $Typed, - type AppBskyGraphFollow, - type AppBskyGraphGetFollows, - type AtpAgent, - type ComAtprotoRepoApplyWrites, - type ComAtprotoRepoStrongRef, -} from '@atproto/api' import {TID} from '@atproto/common-web' +import {type $Typed, type Client} from '@atproto/lex' +import { + type AtIdentifierString, + type DidString, + toDatetimeString, +} from '@atproto/syntax' import chunk from 'lodash.chunk' import {until} from '#/lib/async/until' +import {app, com} from '#/lexicons' export async function bulkWriteFollows( - agent: AtpAgent, + pdsClient: Client, + appviewClient: Client, dids: string[], - via?: ComAtprotoRepoStrongRef.Main, + via?: com.atproto.repo.strongRef.Main, ) { - const session = agent.session + const did = pdsClient.assertDid - if (!session) { - throw new Error(`bulkWriteFollows failed: no session`) - } - - const followRecords: $Typed[] = dids.map(did => { + const followRecords: $Typed[] = dids.map(did => { return { $type: 'app.bsky.graph.follow', - subject: did, - createdAt: new Date().toISOString(), + // callers hold plain dids read off legacy-typed views + subject: did as DidString, + createdAt: toDatetimeString(new Date()), via, } }) - const followWrites: $Typed[] = + const followWrites: $Typed[] = followRecords.map(r => ({ $type: 'com.atproto.repo.applyWrites#create', collection: 'app.bsky.graph.follow', @@ -41,35 +38,35 @@ export async function bulkWriteFollows( const chunks = chunk(followWrites, 50) for (const chunk of chunks) { - await agent.com.atproto.repo.applyWrites({ - repo: session.did, + await pdsClient.call(com.atproto.repo.applyWrites, { + repo: did, writes: chunk, }) } - await whenFollowsIndexed(agent, session.did, res => !!res.data.follows.length) + await whenFollowsIndexed(appviewClient, did, res => !!res.follows.length) const followUris = new Map() for (const r of followWrites) { followUris.set( r.value.subject as string, - `at://${session.did}/app.bsky.graph.follow/${r.rkey}`, + `at://${did}/app.bsky.graph.follow/${r.rkey}`, ) } return followUris } async function whenFollowsIndexed( - agent: AtpAgent, + appviewClient: Client, actor: string, - fn: (res: AppBskyGraphGetFollows.Response) => boolean, + fn: (res: app.bsky.graph.getFollows.$OutputBody) => boolean, ) { await until( 5, // 5 tries 1e3, // 1s delay between tries fn, () => - agent.app.bsky.graph.getFollows({ - actor, + appviewClient.call(app.bsky.graph.getFollows, { + actor: actor as AtIdentifierString, limit: 1, }), ) diff --git a/src/screens/Profile/components/GermButton.tsx b/src/screens/Profile/components/GermButton.tsx index 7e32d60bcc..5a69ec42c9 100644 --- a/src/screens/Profile/components/GermButton.tsx +++ b/src/screens/Profile/components/GermButton.tsx @@ -1,10 +1,8 @@ import {Platform, View} from 'react-native' import {Image} from 'expo-image' -import { - type AppBskyActorDefs, - type AppBskyActorGetProfile, - type AtpAgent, -} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type Client} from '@atproto/lex' +import {type DidString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -13,7 +11,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 {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' @@ -24,6 +22,7 @@ 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({ @@ -119,25 +118,27 @@ function GermSelfButton({did}: {did: string}) { const ax = useAnalytics() const {_} = useLingui() const selfExplanationDialogControl = Dialog.useDialogControl() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const {mutate: deleteDeclaration, isPending} = useMutation({ mutationFn: async () => { - const previousRecord = await agent.com.germnetwork.declaration - .get({ - repo: did, + const previousRecord = await pdsClient + // the profile view is still legacy-typed, so its did is unbranded + .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 }, @@ -147,14 +148,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`)) @@ -323,14 +325,17 @@ function platform() { } async function whenAppViewReady( - agent: AtpAgent, + 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/Settings/FindContactsSettings.tsx b/src/screens/Settings/FindContactsSettings.tsx index 6104fd12b7..c8ecdf4102 100644 --- a/src/screens/Settings/FindContactsSettings.tsx +++ b/src/screens/Settings/FindContactsSettings.tsx @@ -29,7 +29,7 @@ import { useContactsMatchesQuery, useContactsSyncStatusQuery, } from '#/state/queries/find-contacts' -import {useAgent, useAppviewClient, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {List} from '#/view/com/util/List' import {atoms as a, tokens, useGutters, useTheme} from '#/alf' @@ -370,7 +370,7 @@ function StatusHeader({ }) { const {_} = useLingui() const ax = useAnalytics() - const agent = useAgent() + const pdsClient = usePdsClient() const client = useAppviewClient() const queryClient = useQueryClient() const {currentAccount} = useSession() @@ -406,7 +406,10 @@ function StatusHeader({ followCount: didsToFollow.length, }) - const uris = await wait(500, bulkWriteFollows(agent, didsToFollow)) + const uris = await wait( + 500, + bulkWriteFollows(pdsClient, client, didsToFollow), + ) for (const did of didsToFollow) { const uri = uris.get(did) diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx index e7674c00b4..c79e39c190 100644 --- a/src/screens/StarterPack/StarterPackScreen.tsx +++ b/src/screens/StarterPack/StarterPackScreen.tsx @@ -7,6 +7,7 @@ import { AtUri, type ModerationOpts, } from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' @@ -37,7 +38,7 @@ import { useDeleteStarterPackMutation, useStarterPackQuery, } from '#/state/queries/starter-packs' -import {useAgent, useAppviewClient, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {useSetActiveStarterPack} from '#/state/shell/landing' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import { @@ -311,8 +312,8 @@ function Header({ const {_} = useLingui() const t = useTheme() const {currentAccount, hasSession} = useSession() - const agent = useAgent() const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const setActiveStarterPack = useSetActiveStarterPack() const {requestSwitchToAccount} = useLoggedOutViewControls() @@ -379,8 +380,9 @@ function Header({ let followUris: Map try { - followUris = await bulkWriteFollows(agent, dids, { - uri: starterPack.uri, + followUris = await bulkWriteFollows(pdsClient, appviewClient, dids, { + // the starter pack view is still legacy-typed + uri: starterPack.uri as AtUriString, cid: starterPack.cid, }) } catch (e) { diff --git a/src/state/birthdate.ts b/src/state/birthdate.ts index 183e51aa45..c405febe4e 100644 --- a/src/state/birthdate.ts +++ b/src/state/birthdate.ts @@ -3,7 +3,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query' import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings' import {preferencesQueryKey} from '#/state/queries/preferences' -import {useAgent, useSession} from '#/state/session' +import {useAgent, usePdsClient, useSession} from '#/state/session' import {usePatchAgeAssuranceOtherRequiredData} from '#/ageAssurance' import {isUnderAge} from '#/ageAssurance/util' import {IS_DEV} from '#/env' @@ -55,6 +55,7 @@ export function useIsBirthdateUpdateAllowed() { export function useBirthdateMutation() { const queryClient = useQueryClient() const agent = useAgent() + const pdsClient = usePdsClient() const patchOtherRequiredData = usePatchAgeAssuranceOtherRequiredData() return useMutation({ @@ -68,7 +69,7 @@ export function useBirthdateMutation() { if (isUnderAge(birthDate.toISOString(), 18)) { await restrictChatSettings({ - agent, + client: pdsClient, restrictIncoming: true, restrictGroupInvites: true, }) diff --git a/src/state/queries/activity-subscriptions.ts b/src/state/queries/activity-subscriptions.ts index e6dcbd8829..f77d1001cb 100644 --- a/src/state/queries/activity-subscriptions.ts +++ b/src/state/queries/activity-subscriptions.ts @@ -1,7 +1,5 @@ -import { - type AppBskyActorDefs, - type AppBskyNotificationDeclaration, -} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type AtIdentifierString} from '@atproto/syntax' import {t} from '@lingui/core/macro' import { type InfiniteData, @@ -12,7 +10,8 @@ import { useQueryClient, } from '@tanstack/react-query' -import {useAgent, useAppviewClient, useSession} from '#/state/session' +import {isRecordNotFoundError} from '#/lib/xrpc-error' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import * as Toast from '#/components/Toast' import {app} from '#/lexicons' @@ -36,27 +35,25 @@ 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, { + // the session account is still legacy-typed, so its did is unbranded + repo: currentAccount!.did as AtIdentifierString, rkey: 'self', }) return response } catch (err) { - if ( - err instanceof Error && - err.message.startsWith('Could not locate record') - ) { + if (isRecordNotFoundError(err)) { return { value: { $type: 'app.bsky.notification.declaration', allowSubscriptions: 'followers', - } satisfies AppBskyNotificationDeclaration.Record, + } satisfies app.bsky.notification.declaration.Main, } } else { throw err @@ -67,17 +64,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 }, @@ -87,7 +85,7 @@ export function useNotificationDeclarationMutation() { (old?: { uri: string cid: string - value: AppBskyNotificationDeclaration.Record + value: app.bsky.notification.declaration.Main }) => { if (!old) return old return { diff --git a/src/state/queries/list-memberships.ts b/src/state/queries/list-memberships.ts index 75adcd5bb8..68a02fd051 100644 --- a/src/state/queries/list-memberships.ts +++ b/src/state/queries/list-memberships.ts @@ -1,8 +1,13 @@ import { type AppBskyActorDefs, type AppBskyGraphGetStarterPacksWithMembership, - AtUri, } from '@atproto/api' +import { + AtUri, + type AtUriString, + type DidString, + toDatetimeString, +} from '@atproto/syntax' import { type InfiniteData, useMutation, @@ -13,7 +18,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 +36,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 +47,15 @@ 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(), - }, - ) + /* + * The mutation's inputs are plain strings held by legacy-typed views, so + * they are asserted to their branded forms here. + */ + const res = await pdsClient.create(app.bsky.graph.listitem, { + subject: actorDid as DidString, + list: listUri as AtUriString, + createdAt: toDatetimeString(new Date()), + }) return res }, onSuccess: (data, variables) => { @@ -129,7 +136,7 @@ export function useListMembershipRemoveMutation({ onError?: (error: Error) => void } = {}) { const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation< void, @@ -141,9 +148,9 @@ export function useListMembershipRemoveMutation({ throw new Error('Not signed in') } const membershipUrip = new AtUri(membershipUri) - await agent.app.bsky.graph.listitem.delete({ - repo: currentAccount.did, - rkey: membershipUrip.rkey, + await pdsClient.delete(app.bsky.graph.listitem, { + repo: currentAccount.did as DidString, + rkey: membershipUrip.rkeySafe, }) }, onSuccess: (data, variables) => { diff --git a/src/state/queries/list.ts b/src/state/queries/list.ts index b5deb087c3..ec551bc8e5 100644 --- a/src/state/queries/list.ts +++ b/src/state/queries/list.ts @@ -1,14 +1,11 @@ +import {type AppBskyGraphDefs} from '@atproto/api' +import {type $Typed, type Client, type l} from '@atproto/lex' import { - type $Typed, - type AppBskyGraphDefs, - type AppBskyGraphGetList, - type AppBskyGraphList, - type AtpAgent, + type AtIdentifierString, AtUri, - type ComAtprotoRepoApplyWrites, - type Facet, - type Un$Typed, -} from '@atproto/api' + type AtUriString, + toDatetimeString, +} from '@atproto/syntax' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import chunk from 'lodash.chunk' @@ -16,7 +13,13 @@ import {uploadBlob} from '#/lib/api' import {until} from '#/lib/async/until' import {type ImageMeta} from '#/state/gallery' import {STALE} from '#/state/queries' -import {useAgent, useSession} from '#/state/session' +import { + useAgent, + 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' @@ -25,7 +28,7 @@ export const RQKEY_ROOT = 'list' export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] export function useListQuery(uri?: string) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ staleTime: STALE.MINUTES.ONE, queryKey: RQKEY(uri || ''), @@ -33,11 +36,11 @@ export function useListQuery(uri?: string) { 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, }) @@ -47,13 +50,15 @@ 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({ @@ -72,33 +77,28 @@ 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: toDatetimeString(new Date()), } if (avatar) { const blobRes = await uploadBlob(agent, avatar.path, avatar.mime) - record.avatar = blobRes.data.blob + /* + * `uploadBlob` still returns the legacy `BlobRef` class instance; + * it moves to the client with the rest of the blob pipeline. + */ + record.avatar = blobRes.data.blob as unknown as l.BlobRef } - 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() { @@ -115,12 +115,14 @@ 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}, @@ -137,7 +139,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, }) @@ -148,30 +150,24 @@ export function useListMetadataMutation() { record.descriptionFacets = descriptionFacets if (avatar) { const blobRes = await uploadBlob(agent, avatar.path, avatar.mime) - record.avatar = blobRes.data.blob + record.avatar = blobRes.data.blob as unknown as l.BlobRef } 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) { @@ -191,7 +187,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}) => { @@ -199,11 +196,12 @@ 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, { + // the session account is still legacy-typed, so its did is unbranded + repo: currentAccount.did as AtIdentifierString, cursor, limit: 100, }) @@ -221,12 +219,12 @@ export function useListDeleteMutation() { // batch delete the list and listitem records const createDel = ( uri: string, - ): $Typed => { + ): $Typed => { const urip = new AtUri(uri) return { $type: 'com.atproto.repo.applyWrites#delete', - collection: urip.collection, - rkey: urip.rkey, + collection: urip.collectionSafe, + rkey: urip.rkeySafe, } } const writes = listitemRecordUris @@ -235,15 +233,20 @@ 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, `until` catches it and passes `undefined` here, so an absent + * body signals a completed delete - the old check read `!v.success` on + * the legacy response envelope, which lex does not expose. + */ + await whenAppViewReady(appviewClient, uri, v => { + return !v }) }, onSuccess() { @@ -259,16 +262,18 @@ export function useListDeleteMutation() { export function useListMuteMutation() { const queryClient = useQueryClient() const agent = useAgent() + const appviewClient = useAppviewClient() return useMutation({ mutationFn: async ({uri, mute}) => { + // `muteModList`/`unmuteModList` are preference writes, migrated in wave B if (mute) { await agent.muteModList(uri) } else { await agent.unmuteModList(uri) } - 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) { @@ -282,18 +287,20 @@ export function useListMuteMutation() { export function useListBlockMutation() { const queryClient = useQueryClient() const agent = useAgent() + const appviewClient = useAppviewClient() return useMutation({ mutationFn: async ({uri, block}) => { + // `blockModList`/`unblockModList` write a block record, migrated in wave B if (block) { await agent.blockModList(uri) } else { await agent.unblockModList(uri) } - 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) { @@ -305,17 +312,17 @@ export function useListBlockMutation() { } async function whenAppViewReady( - agent: AtpAgent, + 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/messages/actor-declaration.ts b/src/state/queries/messages/actor-declaration.ts index f6cd2d51d5..17f36cbdd8 100644 --- a/src/state/queries/messages/actor-declaration.ts +++ b/src/state/queries/messages/actor-declaration.ts @@ -3,11 +3,13 @@ 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 {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession} from '#/state/session' import {resolveAllowGroupInvites} from '#/components/dms/util' +import {com} from '#/lexicons' import {RQKEY as PROFILE_RKEY} from '../profile' export function useUpdateActorDeclaration({ @@ -19,7 +21,7 @@ export function useUpdateActorDeclaration({ }) { const queryClient = useQueryClient() const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async (update: { @@ -41,8 +43,9 @@ 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, { + // the session account is still legacy-typed, so its did is unbranded + repo: currentAccount.did as DidString, collection: 'chat.bsky.actor.declaration', rkey: 'self', record: { @@ -101,13 +104,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', }) @@ -116,6 +119,11 @@ export function useDeleteActorDeclaration() { }) } +/* + * Still takes the legacy agent: its only caller is `getOtherRequiredData` in + * `#/ageAssurance/data`, which is blocked on `getPreferences` and so cannot + * hand over a lex client yet. + */ export async function fetchActorDeclarationRecord({ agent, did, diff --git a/src/state/queries/messages/restrictChatSettings.ts b/src/state/queries/messages/restrictChatSettings.ts index ad33385dc8..b47440bf6b 100644 --- a/src/state/queries/messages/restrictChatSettings.ts +++ b/src/state/queries/messages/restrictChatSettings.ts @@ -1,13 +1,12 @@ -import type AtpAgent from '@atproto/api' -import {type ChatBskyActorDeclaration} from '@atproto/api' +import {type Client} from '@atproto/lex' import {networkRetry} from '#/lib/async/retry' import {logger} from '#/logger' import { - getDidFromAgentSession, getOtherRequiredDataFromCache, setOtherRequiredDataActorDeclarationCache, } from '#/ageAssurance/data' +import {chat} from '#/lexicons' /** * Updates the chat actor declaration record to restrict who can contact the @@ -24,15 +23,15 @@ import { * back to the lexicon defaults when the cache is empty. */ export async function restrictChatSettings({ - agent, + client, restrictIncoming = false, restrictGroupInvites = false, }: { - agent: AtpAgent + client: Client restrictIncoming?: boolean restrictGroupInvites?: boolean }): Promise { - const did = getDidFromAgentSession(agent) + const did = client.did if (!did) return const cached = getOtherRequiredDataFromCache({did})?.actorDeclaration @@ -49,7 +48,7 @@ export async function restrictChatSettings({ ) } - const record: ChatBskyActorDeclaration.Main = { + const record: chat.bsky.actor.declaration.Main = { $type: 'chat.bsky.actor.declaration', allowIncoming: restrictIncoming ? 'none' @@ -69,11 +68,14 @@ export async function restrictChatSettings({ try { await networkRetry(3, () => - agent.com.atproto.repo.putRecord({ + /* + * A record helper, not a raw `com.atproto.repo.putRecord`: lex forces + * `service: null` on record helpers, so the write lands on the account's + * PDS even though the collection is `chat.bsky.*`. + */ + client.put(chat.bsky.actor.declaration, record, { repo: did, - collection: 'chat.bsky.actor.declaration', rkey: 'self', - record, }), ) // important, update local cache to avoid running this again diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index 23fd4340ac..dea8d35eea 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -10,6 +10,11 @@ import { type ComAtprotoRepoUploadBlob, type Un$Typed, } from '@atproto/api' +import { + type AtIdentifierString, + type DidString, + toDatetimeString, +} from '@atproto/syntax' import { type InfiniteData, keepPreviousData, @@ -33,10 +38,11 @@ import { useUnstableProfileViewCache, } from '#/state/queries/unstable-profile-cache' import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache' -import {useAgent, useSession} from '#/state/session' +import {useAgent, 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, @@ -634,17 +640,18 @@ 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, { + // the profile view is still legacy-typed, so its did is unbranded + subject: did as DidString, + createdAt: toDatetimeString(new Date()), + }) }, onSuccess(_, {did}) { void queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()}) @@ -655,16 +662,16 @@ function useProfileBlockMutation() { function useProfileUnblockMutation() { const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation({ mutationFn: async ({blockUri}) => { if (!currentAccount) { throw new Error('Not signed in') } - const {rkey} = new AtUri(blockUri) - await agent.app.bsky.graph.block.delete({ - repo: currentAccount.did, + const {rkeySafe: rkey} = new AtUri(blockUri) + await pdsClient.delete(app.bsky.graph.block, { + repo: currentAccount.did as AtIdentifierString, rkey, }) }, diff --git a/src/state/queries/starter-packs.ts b/src/state/queries/starter-packs.ts index 6a1aecef0e..0c781bcfea 100644 --- a/src/state/queries/starter-packs.ts +++ b/src/state/queries/starter-packs.ts @@ -1,11 +1,10 @@ import { AppBskyFeedDefs, AppBskyGraphDefs, - type AppBskyGraphGetStarterPack, AppBskyGraphStarterpack, - type AtpAgent, - AtUri, } from '@atproto/api' +import {type Client, type LexValue} from '@atproto/lex' +import {AtUri, type AtUriString, toDatetimeString} from '@atproto/syntax' import {RichText} from '@bsky.app/sdk/richtext' import { type QueryClient, @@ -25,8 +24,8 @@ import { import {invalidateActorStarterPacksQuery} from '#/state/queries/actor-starter-packs' import {STALE} from '#/state/queries/index' import {invalidateListMembersQuery} from '#/state/queries/list-members' -import {useAgent, useAppviewClient} from '#/state/session' -import {type app} from '#/lexicons' +import {useAppviewClient, usePdsClient} from '#/state/session' +import {app, com} from '#/lexicons' import * as bsky from '#/types/bsky' const RQKEY_ROOT = 'starter-pack' @@ -56,7 +55,7 @@ export function useStarterPackQuery({ did?: string rkey?: string }) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ queryKey: RQKEY(uri ? {uri} : {did, rkey}), @@ -67,10 +66,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, @@ -104,12 +103,12 @@ export function useCreateStarterPackMutation({ onError: (e: Error) => void }) { const queryClient = useQueryClient() - const agent = useAgent() /* * Facet/mention resolution is an appview job - it resolves handles through * the appview, and the public fallback keeps it working when logged out. */ const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() return useMutation< {uri: string; cid: string}, @@ -130,30 +129,26 @@ 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, + // `createStarterPackList` returns a plain string uri + list: listRes?.uri as AtUriString, + feeds: feeds?.map(f => ({uri: f.uri as AtUriString})), + createdAt: toDatetimeString(new Date()), + }) }, 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) }, @@ -171,8 +166,8 @@ export function useEditStarterPackMutation({ onError: (error: Error) => void }) { const queryClient = useQueryClient() - const agent = useAgent() const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() return useMutation< void, @@ -197,25 +192,29 @@ export function useEditStarterPackMutation({ 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).rkeySafe, + }), + ), }) } } @@ -226,33 +225,44 @@ 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, list: currentStarterPack.list?.uri, - feeds, + /* + * Pre-existing quirk preserved verbatim: the edit path writes whole + * `GeneratorView`s where the lexicon declares `feedItem` refs. lex + * types the raw `putRecord` body as a `LexValue`, which the legacy + * view interface does not structurally satisfy, hence the cast. + */ + feeds: feeds as unknown as LexValue, createdAt: currentStarterPack.record.createdAt, updatedAt: new Date().toISOString(), }, @@ -260,12 +270,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({ @@ -275,7 +285,7 @@ export function useEditStarterPackMutation({ } await invalidateStarterPack({ queryClient, - did: agent.session!.did, + did: pdsClient.assertDid, rkey: parsed!.rkey, }) onSuccess() @@ -293,35 +303,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, - rkey: new AtUri(listUri).rkey, + await pdsClient.delete(app.bsky.graph.list, { + repo: did, + rkey: new AtUri(listUri).rkeySafe, }) } - 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 }) } @@ -330,11 +339,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() @@ -346,15 +355,18 @@ export function useDeleteStarterPackMutation({ } async function whenAppViewReady( - agent: AtpAgent, + 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/state/queries/verification/useVerificationCreateMutation.tsx b/src/state/queries/verification/useVerificationCreateMutation.tsx index 083a6f3b54..4f1a318f48 100644 --- a/src/state/queries/verification/useVerificationCreateMutation.tsx +++ b/src/state/queries/verification/useVerificationCreateMutation.tsx @@ -1,15 +1,22 @@ -import {type AppBskyActorGetProfile} from '@atproto/api' +import { + type AtIdentifierString, + type DidString, + type HandleString, + toDatetimeString, +} 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 +26,18 @@ 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, { + // the profile view is still legacy-typed, so its strings are unbranded + subject: profile.did as DidString, + createdAt: toDatetimeString(new Date()), + 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 +47,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..657c50c909 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 AppBskyActorDefs} from '@atproto/api' +import {type AtIdentifierString, 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() @@ -33,9 +32,8 @@ export function useVerificationsRemoveMutation() { await Promise.all( uris.map(uri => { - return agent.app.bsky.graph.verification.delete({ - repo: currentAccount.did, - rkey: new AtUri(uri).rkey, + return pdsClient.delete(app.bsky.graph.verification, { + rkey: new AtUri(uri).rkeySafe, }) }), ) @@ -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,10 @@ export function useVerificationsRemoveMutation() { return false }, () => { - return agent.getProfile({actor: profile.did ?? ''}) + return appviewClient.call(app.bsky.actor.getProfile, { + // the profile view is still legacy-typed, so its did is unbranded + actor: (profile.did ?? '') as AtIdentifierString, + }) }, ) }, diff --git a/src/state/session/create-account.ts b/src/state/session/create-account.ts index fd67681482..b44bed4dbd 100644 --- a/src/state/session/create-account.ts +++ b/src/state/session/create-account.ts @@ -1,5 +1,6 @@ import {type AppBskyActorProfile, type Un$Typed} from '@atproto/api' import {TID} from '@atproto/common-web' +import {type Client} from '@atproto/lex' import {PasswordSession} from '@atproto/lex-password-session' import {networkRetry} from '#/lib/async/retry' @@ -21,6 +22,7 @@ import { import {unsafeGetAndComputeAgeAssurance} from '#/ageAssurance/state' import {features} from '#/analytics' import {type BskyAppAgent} from './bridge-agent' +import {agentToPdsClient} from './clients' import {configureModerationForAccount} from './moderation' import { buildBundle, @@ -109,7 +111,11 @@ export async function createSessionBundleAndCreateAccount( if (isProd) { postSignupTasks.push( initializeSavedFeeds(bundle.agent), - restrictChatAfterAgeAssurance(aa, bundle.agent, earlyAccount.did), + restrictChatAfterAgeAssurance( + aa, + agentToPdsClient(bundle.agent), + earlyAccount.did, + ), ) } // Post-signup writes are not required to enter onboarding. @@ -207,14 +213,14 @@ function initializeSavedFeeds(agent: BskyAppAgent) { function restrictChatAfterAgeAssurance( ageAssurance: Promise, - agent: BskyAppAgent, + client: Client, did: string, ) { return ageAssurance.then(() => { const {flags} = unsafeGetAndComputeAgeAssurance({did}) if (flags?.chatDisabled || flags?.groupChatDisabled) { void restrictChatSettings({ - agent, + client, restrictIncoming: flags.chatDisabled, restrictGroupInvites: flags.groupChatDisabled, })