migrate the bookmark and draft queries to the appview client
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import {type AppBskyFeedDefs} from '@atproto/api'
|
import {type AppBskyFeedDefs} from '@atproto/api'
|
||||||
|
import {type AtUriString} from '@atproto/syntax'
|
||||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {isNetworkError} from '#/lib/strings/errors'
|
import {isNetworkError} from '#/lib/strings/errors'
|
||||||
@@ -8,7 +9,8 @@ import {
|
|||||||
optimisticallyDeleteBookmark,
|
optimisticallyDeleteBookmark,
|
||||||
optimisticallySaveBookmark,
|
optimisticallySaveBookmark,
|
||||||
} from '#/state/queries/bookmarks/useBookmarksQuery'
|
} from '#/state/queries/bookmarks/useBookmarksQuery'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAppviewClient} from '#/state/session'
|
||||||
|
import {app} from '#/lexicons'
|
||||||
|
|
||||||
type MutationArgs =
|
type MutationArgs =
|
||||||
| {action: 'create'; post: AppBskyFeedDefs.PostView}
|
| {action: 'create'; post: AppBskyFeedDefs.PostView}
|
||||||
@@ -23,20 +25,20 @@ type MutationArgs =
|
|||||||
|
|
||||||
export function useBookmarkMutation() {
|
export function useBookmarkMutation() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const agent = useAgent()
|
const client = useAppviewClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
async mutationFn(args: MutationArgs) {
|
async mutationFn(args: MutationArgs) {
|
||||||
if (args.action === 'create') {
|
if (args.action === 'create') {
|
||||||
updatePostShadow(qc, args.post.uri, {bookmarked: true})
|
updatePostShadow(qc, args.post.uri, {bookmarked: true})
|
||||||
await agent.app.bsky.bookmark.createBookmark({
|
await client.call(app.bsky.bookmark.createBookmark, {
|
||||||
uri: args.post.uri,
|
uri: args.post.uri as AtUriString,
|
||||||
cid: args.post.cid,
|
cid: args.post.cid,
|
||||||
})
|
})
|
||||||
} else if (args.action === 'delete') {
|
} else if (args.action === 'delete') {
|
||||||
updatePostShadow(qc, args.uri, {bookmarked: false})
|
updatePostShadow(qc, args.uri, {bookmarked: false})
|
||||||
await agent.app.bsky.bookmark.deleteBookmark({
|
await client.call(app.bsky.bookmark.deleteBookmark, {
|
||||||
uri: args.uri,
|
uri: args.uri as AtUriString,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import {type $Typed, AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||||
type $Typed,
|
|
||||||
type AppBskyBookmarkGetBookmarks,
|
|
||||||
AppBskyFeedDefs,
|
|
||||||
AtUri,
|
|
||||||
} from '@atproto/api'
|
|
||||||
import {
|
import {
|
||||||
type InfiniteData,
|
type InfiniteData,
|
||||||
type QueryClient,
|
type QueryClient,
|
||||||
@@ -16,28 +11,28 @@ import {
|
|||||||
embedViewRecordToPostView,
|
embedViewRecordToPostView,
|
||||||
getEmbeddedPost,
|
getEmbeddedPost,
|
||||||
} from '#/state/queries/util'
|
} from '#/state/queries/util'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAppviewClient} from '#/state/session'
|
||||||
|
import {app} from '#/lexicons'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export const bookmarksQueryKeyRoot = 'bookmarks'
|
export const bookmarksQueryKeyRoot = 'bookmarks'
|
||||||
export const createBookmarksQueryKey = () => [bookmarksQueryKeyRoot]
|
export const createBookmarksQueryKey = () => [bookmarksQueryKeyRoot]
|
||||||
|
|
||||||
export function useBookmarksQuery() {
|
export function useBookmarksQuery() {
|
||||||
const agent = useAgent()
|
const client = useAppviewClient()
|
||||||
|
|
||||||
return useInfiniteQuery<
|
return useInfiniteQuery<
|
||||||
AppBskyBookmarkGetBookmarks.OutputSchema,
|
app.bsky.bookmark.getBookmarks.$OutputBody,
|
||||||
Error,
|
Error,
|
||||||
InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>,
|
InfiniteData<app.bsky.bookmark.getBookmarks.$OutputBody>,
|
||||||
QueryKey,
|
QueryKey,
|
||||||
string | undefined
|
string | undefined
|
||||||
>({
|
>({
|
||||||
queryKey: createBookmarksQueryKey(),
|
queryKey: createBookmarksQueryKey(),
|
||||||
async queryFn({pageParam}) {
|
async queryFn({pageParam}) {
|
||||||
const res = await agent.app.bsky.bookmark.getBookmarks({
|
return await client.call(app.bsky.bookmark.getBookmarks, {
|
||||||
cursor: pageParam,
|
cursor: pageParam,
|
||||||
})
|
})
|
||||||
return res.data
|
|
||||||
},
|
},
|
||||||
initialPageParam: undefined,
|
initialPageParam: undefined,
|
||||||
getNextPageParam: lastPage => lastPage.cursor,
|
getNextPageParam: lastPage => lastPage.cursor,
|
||||||
@@ -45,7 +40,7 @@ export function useBookmarksQuery() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function truncateAndInvalidate(qc: QueryClient) {
|
export async function truncateAndInvalidate(qc: QueryClient) {
|
||||||
qc.setQueriesData<InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>>(
|
qc.setQueriesData<InfiniteData<app.bsky.bookmark.getBookmarks.$OutputBody>>(
|
||||||
{queryKey: [bookmarksQueryKeyRoot]},
|
{queryKey: [bookmarksQueryKeyRoot]},
|
||||||
data => {
|
data => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -64,7 +59,7 @@ export async function optimisticallySaveBookmark(
|
|||||||
qc: QueryClient,
|
qc: QueryClient,
|
||||||
post: AppBskyFeedDefs.PostView,
|
post: AppBskyFeedDefs.PostView,
|
||||||
) {
|
) {
|
||||||
qc.setQueriesData<InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>>(
|
qc.setQueriesData<InfiniteData<app.bsky.bookmark.getBookmarks.$OutputBody>>(
|
||||||
{
|
{
|
||||||
queryKey: [bookmarksQueryKeyRoot],
|
queryKey: [bookmarksQueryKeyRoot],
|
||||||
},
|
},
|
||||||
@@ -75,19 +70,22 @@ export async function optimisticallySaveBookmark(
|
|||||||
pages: data.pages.map((page, index) => {
|
pages: data.pages.map((page, index) => {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
post.$type = 'app.bsky.feed.defs#postView'
|
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<AppBskyFeedDefs.PostView>,
|
||||||
|
} as unknown as app.bsky.bookmark.defs.BookmarkView
|
||||||
return {
|
return {
|
||||||
...page,
|
...page,
|
||||||
bookmarks: [
|
bookmarks: [bookmark, ...page.bookmarks],
|
||||||
{
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
subject: {
|
|
||||||
uri: post.uri,
|
|
||||||
cid: post.cid,
|
|
||||||
},
|
|
||||||
item: post as $Typed<AppBskyFeedDefs.PostView>,
|
|
||||||
},
|
|
||||||
...page.bookmarks,
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return page
|
return page
|
||||||
@@ -101,7 +99,7 @@ export async function optimisticallyDeleteBookmark(
|
|||||||
qc: QueryClient,
|
qc: QueryClient,
|
||||||
{uri}: {uri: string},
|
{uri}: {uri: string},
|
||||||
) {
|
) {
|
||||||
qc.setQueriesData<InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>>(
|
qc.setQueriesData<InfiniteData<app.bsky.bookmark.getBookmarks.$OutputBody>>(
|
||||||
{
|
{
|
||||||
queryKey: [bookmarksQueryKeyRoot],
|
queryKey: [bookmarksQueryKeyRoot],
|
||||||
},
|
},
|
||||||
@@ -125,7 +123,7 @@ export function* findAllPostsInQueryData(
|
|||||||
uri: string,
|
uri: string,
|
||||||
): Generator<AppBskyFeedDefs.PostView, undefined> {
|
): Generator<AppBskyFeedDefs.PostView, undefined> {
|
||||||
const queryDatas = queryClient.getQueriesData<
|
const queryDatas = queryClient.getQueriesData<
|
||||||
InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>
|
InfiniteData<app.bsky.bookmark.getBookmarks.$OutputBody>
|
||||||
>({
|
>({
|
||||||
queryKey: [bookmarksQueryKeyRoot],
|
queryKey: [bookmarksQueryKeyRoot],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {AppBskyDraftCreateDraft, AppBskyDraftDefs} from '@atproto/api'
|
import {AppBskyDraftDefs} from '@atproto/api'
|
||||||
import {
|
import {
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
useMutation,
|
useMutation,
|
||||||
@@ -6,10 +6,12 @@ import {
|
|||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {isNetworkError} from '#/lib/strings/errors'
|
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 {type ComposerState} from '#/view/com/composer/state/composer'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {getDeviceId} from '#/analytics/identifiers'
|
import {getDeviceId} from '#/analytics/identifiers'
|
||||||
|
import {app} from '#/lexicons'
|
||||||
import {composerStateToDraft, draftViewToSummary} from './api'
|
import {composerStateToDraft, draftViewToSummary} from './api'
|
||||||
import {logger} from './logger'
|
import {logger} from './logger'
|
||||||
import * as storage from './storage'
|
import * as storage from './storage'
|
||||||
@@ -20,7 +22,7 @@ const DRAFTS_QUERY_KEY = ['drafts']
|
|||||||
* Hook to list all drafts for the current account
|
* Hook to list all drafts for the current account
|
||||||
*/
|
*/
|
||||||
export function useDraftsQuery() {
|
export function useDraftsQuery() {
|
||||||
const agent = useAgent()
|
const client = useAppviewClient()
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
|
|
||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
@@ -28,10 +30,12 @@ export function useDraftsQuery() {
|
|||||||
queryFn: async ({pageParam}) => {
|
queryFn: async ({pageParam}) => {
|
||||||
// Ensure media cache is populated before checking which media exists
|
// Ensure media cache is populated before checking which media exists
|
||||||
await storage.ensureMediaCachePopulated()
|
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 {
|
return {
|
||||||
cursor: res.data.cursor,
|
cursor: data.cursor,
|
||||||
drafts: res.data.drafts.map(view =>
|
drafts: data.drafts.map(view =>
|
||||||
draftViewToSummary({
|
draftViewToSummary({
|
||||||
view,
|
view,
|
||||||
analytics: ax,
|
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.
|
* This ensures we don't lose data if the network request fails.
|
||||||
*/
|
*/
|
||||||
export function useSaveDraftMutation() {
|
export function useSaveDraftMutation() {
|
||||||
const agent = useAgent()
|
const client = useAppviewClient()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
@@ -132,7 +136,14 @@ export function useSaveDraftMutation() {
|
|||||||
originalLocalRefs: Set<string> | undefined
|
originalLocalRefs: Set<string> | undefined
|
||||||
}> => {
|
}> => {
|
||||||
// Convert composer state to server draft format
|
// 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', {
|
logger.debug('saving draft', {
|
||||||
existingDraftId,
|
existingDraftId,
|
||||||
@@ -147,7 +158,7 @@ export function useSaveDraftMutation() {
|
|||||||
logger.debug('updating existing draft on server', {
|
logger.debug('updating existing draft on server', {
|
||||||
draftId: existingDraftId,
|
draftId: existingDraftId,
|
||||||
})
|
})
|
||||||
await agent.app.bsky.draft.updateDraft({
|
await client.call(app.bsky.draft.updateDraft, {
|
||||||
draft: {
|
draft: {
|
||||||
id: existingDraftId,
|
id: existingDraftId,
|
||||||
draft,
|
draft,
|
||||||
@@ -157,8 +168,8 @@ export function useSaveDraftMutation() {
|
|||||||
} else {
|
} else {
|
||||||
// Create new draft
|
// Create new draft
|
||||||
logger.debug('creating new draft on server')
|
logger.debug('creating new draft on server')
|
||||||
const res = await agent.app.bsky.draft.createDraft({draft})
|
const data = await client.call(app.bsky.draft.createDraft, {draft})
|
||||||
draftId = res.data.id
|
draftId = data.id
|
||||||
logger.debug('created new draft', {draftId})
|
logger.debug('created new draft', {draftId})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +214,7 @@ export function useSaveDraftMutation() {
|
|||||||
},
|
},
|
||||||
onError: error => {
|
onError: error => {
|
||||||
// Check for draft limit 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})
|
logger.error('Draft limit reached', {safeMessage: error.message})
|
||||||
// Error will be handled by caller
|
// Error will be handled by caller
|
||||||
} else if (!isNetworkError(error)) {
|
} else if (!isNetworkError(error)) {
|
||||||
@@ -220,7 +231,7 @@ export function useSaveDraftMutation() {
|
|||||||
* Takes the full draft data to avoid re-fetching for media cleanup.
|
* Takes the full draft data to avoid re-fetching for media cleanup.
|
||||||
*/
|
*/
|
||||||
export function useDeleteDraftMutation() {
|
export function useDeleteDraftMutation() {
|
||||||
const agent = useAgent()
|
const client = useAppviewClient()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
@@ -231,7 +242,7 @@ export function useDeleteDraftMutation() {
|
|||||||
draft: AppBskyDraftDefs.Draft
|
draft: AppBskyDraftDefs.Draft
|
||||||
}) => {
|
}) => {
|
||||||
// Delete from server first - if this fails, we keep local media for retry
|
// 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}) => {
|
onSuccess: async (_, {draft}) => {
|
||||||
// Only delete local media after server deletion succeeds
|
// Only delete local media after server deletion succeeds
|
||||||
@@ -264,7 +275,7 @@ export function useDeleteDraftMutation() {
|
|||||||
* Takes draftId and originalLocalRefs from composer state.
|
* Takes draftId and originalLocalRefs from composer state.
|
||||||
*/
|
*/
|
||||||
export function useCleanupPublishedDraftMutation() {
|
export function useCleanupPublishedDraftMutation() {
|
||||||
const agent = useAgent()
|
const client = useAppviewClient()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
@@ -280,7 +291,9 @@ export function useCleanupPublishedDraftMutation() {
|
|||||||
mediaFileCount: originalLocalRefs.size,
|
mediaFileCount: originalLocalRefs.size,
|
||||||
})
|
})
|
||||||
// Delete from server first
|
// 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})
|
logger.debug('deleted draft from server', {draftId})
|
||||||
},
|
},
|
||||||
onSuccess: async (_, {originalLocalRefs}) => {
|
onSuccess: async (_, {originalLocalRefs}) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user