From 8a87a5fdeab3c75ec3fa2f28059a02bc6ad95323 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 3 Aug 2026 17:49:19 +0300 Subject: [PATCH] migrate the bookmark and draft queries to the appview client Co-Authored-By: Claude Fable 5 --- .../queries/bookmarks/useBookmarkMutation.ts | 14 ++--- .../queries/bookmarks/useBookmarksQuery.ts | 52 +++++++++---------- src/view/com/composer/drafts/state/queries.ts | 45 ++++++++++------ 3 files changed, 62 insertions(+), 49 deletions(-) diff --git a/src/state/queries/bookmarks/useBookmarkMutation.ts b/src/state/queries/bookmarks/useBookmarkMutation.ts index c6e745aa04..af5a4a2d8d 100644 --- a/src/state/queries/bookmarks/useBookmarkMutation.ts +++ b/src/state/queries/bookmarks/useBookmarkMutation.ts @@ -1,4 +1,5 @@ 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,7 +9,8 @@ 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} @@ -23,20 +25,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({ - uri: args.post.uri, + await client.call(app.bsky.bookmark.createBookmark, { + uri: args.post.uri as AtUriString, 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..14323625f9 100644 --- a/src/state/queries/bookmarks/useBookmarksQuery.ts +++ b/src/state/queries/bookmarks/useBookmarksQuery.ts @@ -1,9 +1,4 @@ -import { - type $Typed, - type AppBskyBookmarkGetBookmarks, - AppBskyFeedDefs, - AtUri, -} from '@atproto/api' +import {type $Typed, AppBskyFeedDefs, AtUri} from '@atproto/api' import { type InfiniteData, type QueryClient, @@ -16,28 +11,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 +40,7 @@ export function useBookmarksQuery() { } export async function truncateAndInvalidate(qc: QueryClient) { - qc.setQueriesData>( + qc.setQueriesData>( {queryKey: [bookmarksQueryKeyRoot]}, data => { if (data) { @@ -64,7 +59,7 @@ export async function optimisticallySaveBookmark( qc: QueryClient, post: AppBskyFeedDefs.PostView, ) { - qc.setQueriesData>( + qc.setQueriesData>( { queryKey: [bookmarksQueryKeyRoot], }, @@ -75,19 +70,22 @@ export async function optimisticallySaveBookmark( pages: data.pages.map((page, index) => { if (index === 0) { post.$type = 'app.bsky.feed.defs#postView' + /* + * The optimistic entry is synthesized from an `@atproto/api` + * `PostView`, whose string fields are unbranded, so it is asserted + * to the vendored view type the query data is now keyed on. + */ + const bookmark = { + createdAt: new Date().toISOString(), + subject: { + uri: post.uri, + cid: post.cid, + }, + item: post as $Typed, + } as unknown as app.bsky.bookmark.defs.BookmarkView 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 +99,7 @@ export async function optimisticallyDeleteBookmark( qc: QueryClient, {uri}: {uri: string}, ) { - qc.setQueriesData>( + qc.setQueriesData>( { queryKey: [bookmarksQueryKeyRoot], }, @@ -125,7 +123,7 @@ export function* findAllPostsInQueryData( uri: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [bookmarksQueryKeyRoot], }) diff --git a/src/view/com/composer/drafts/state/queries.ts b/src/view/com/composer/drafts/state/queries.ts index 07dd67dd88..0108dbbbff 100644 --- a/src/view/com/composer/drafts/state/queries.ts +++ b/src/view/com/composer/drafts/state/queries.ts @@ -1,4 +1,4 @@ -import {AppBskyDraftCreateDraft, AppBskyDraftDefs} from '@atproto/api' +import {AppBskyDraftDefs} from '@atproto/api' import { useInfiniteQuery, useMutation, @@ -6,10 +6,12 @@ import { } from '@tanstack/react-query' import {isNetworkError} from '#/lib/strings/errors' -import {useAgent} from '#/state/session' +import {matchXrpcError} from '#/lib/xrpc-error' +import {useAppviewClient} from '#/state/session' import {type ComposerState} from '#/view/com/composer/state/composer' import {useAnalytics} from '#/analytics' import {getDeviceId} from '#/analytics/identifiers' +import {app} from '#/lexicons' import {composerStateToDraft, draftViewToSummary} from './api' import {logger} from './logger' import * as storage from './storage' @@ -20,7 +22,7 @@ const DRAFTS_QUERY_KEY = ['drafts'] * Hook to list all drafts for the current account */ export function useDraftsQuery() { - const agent = useAgent() + const client = useAppviewClient() const ax = useAnalytics() return useInfiniteQuery({ @@ -28,10 +30,12 @@ export function useDraftsQuery() { queryFn: async ({pageParam}) => { // Ensure media cache is populated before checking which media exists await storage.ensureMediaCachePopulated() - const res = await agent.app.bsky.draft.getDrafts({cursor: pageParam}) + const data = await client.call(app.bsky.draft.getDrafts, { + cursor: pageParam, + }) return { - cursor: res.data.cursor, - drafts: res.data.drafts.map(view => + cursor: data.cursor, + drafts: data.drafts.map(view => draftViewToSummary({ view, analytics: ax, @@ -116,7 +120,7 @@ export async function loadDraftMedia(draft: AppBskyDraftDefs.Draft): Promise<{ * This ensures we don't lose data if the network request fails. */ export function useSaveDraftMutation() { - const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() return useMutation({ @@ -132,7 +136,14 @@ export function useSaveDraftMutation() { originalLocalRefs: Set | undefined }> => { // Convert composer state to server draft format - const {draft, localRefPaths} = await composerStateToDraft(composerState) + const {draft: apiDraft, localRefPaths} = + await composerStateToDraft(composerState) + /* + * `composerStateToDraft` builds the draft against the `@atproto/api` + * types, whose string fields are unbranded, so it is asserted once here + * to the vendored input type. + */ + const draft = apiDraft as unknown as app.bsky.draft.defs.Draft logger.debug('saving draft', { existingDraftId, @@ -147,7 +158,7 @@ export function useSaveDraftMutation() { logger.debug('updating existing draft on server', { draftId: existingDraftId, }) - await agent.app.bsky.draft.updateDraft({ + await client.call(app.bsky.draft.updateDraft, { draft: { id: existingDraftId, draft, @@ -157,8 +168,8 @@ export function useSaveDraftMutation() { } else { // Create new draft logger.debug('creating new draft on server') - const res = await agent.app.bsky.draft.createDraft({draft}) - draftId = res.data.id + const data = await client.call(app.bsky.draft.createDraft, {draft}) + draftId = data.id logger.debug('created new draft', {draftId}) } @@ -203,7 +214,7 @@ export function useSaveDraftMutation() { }, onError: error => { // Check for draft limit error - if (error instanceof AppBskyDraftCreateDraft.DraftLimitReachedError) { + if (matchXrpcError(error, app.bsky.draft.createDraft)) { logger.error('Draft limit reached', {safeMessage: error.message}) // Error will be handled by caller } else if (!isNetworkError(error)) { @@ -220,7 +231,7 @@ export function useSaveDraftMutation() { * Takes the full draft data to avoid re-fetching for media cleanup. */ export function useDeleteDraftMutation() { - const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() return useMutation({ @@ -231,7 +242,7 @@ export function useDeleteDraftMutation() { draft: AppBskyDraftDefs.Draft }) => { // Delete from server first - if this fails, we keep local media for retry - await agent.app.bsky.draft.deleteDraft({id: draftId}) + await client.call(app.bsky.draft.deleteDraft, {id: draftId}) }, onSuccess: async (_, {draft}) => { // Only delete local media after server deletion succeeds @@ -264,7 +275,7 @@ export function useDeleteDraftMutation() { * Takes draftId and originalLocalRefs from composer state. */ export function useCleanupPublishedDraftMutation() { - const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() return useMutation({ @@ -280,7 +291,9 @@ export function useCleanupPublishedDraftMutation() { mediaFileCount: originalLocalRefs.size, }) // Delete from server first - await agent.app.bsky.draft.deleteDraft({id: draftId}) + await client.call(app.bsky.draft.deleteDraft, { + id: draftId, + }) logger.debug('deleted draft from server', {draftId}) }, onSuccess: async (_, {originalLocalRefs}) => {