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, }), )