diff options
Diffstat (limited to 'src/state/queries')
-rw-r--r-- | src/state/queries/bookmarks/useBookmarkMutation.ts | 65 | ||||
-rw-r--r-- | src/state/queries/bookmarks/useBookmarksQuery.ts | 114 | ||||
-rw-r--r-- | src/state/queries/nuxs/definitions.ts | 6 |
3 files changed, 185 insertions, 0 deletions
diff --git a/src/state/queries/bookmarks/useBookmarkMutation.ts b/src/state/queries/bookmarks/useBookmarkMutation.ts new file mode 100644 index 000000000..c6e745aa0 --- /dev/null +++ b/src/state/queries/bookmarks/useBookmarkMutation.ts @@ -0,0 +1,65 @@ +import {type AppBskyFeedDefs} from '@atproto/api' +import {useMutation, useQueryClient} from '@tanstack/react-query' + +import {isNetworkError} from '#/lib/strings/errors' +import {logger} from '#/logger' +import {updatePostShadow} from '#/state/cache/post-shadow' +import { + optimisticallyDeleteBookmark, + optimisticallySaveBookmark, +} from '#/state/queries/bookmarks/useBookmarksQuery' +import {useAgent} from '#/state/session' + +type MutationArgs = + | {action: 'create'; post: AppBskyFeedDefs.PostView} + | { + action: 'delete' + /** + * For deletions, we only need to URI. Plus, in some cases we only know the + * URI, such as when a post was deleted by the author. + */ + uri: string + } + +export function useBookmarkMutation() { + const qc = useQueryClient() + const agent = useAgent() + + 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, + cid: args.post.cid, + }) + } else if (args.action === 'delete') { + updatePostShadow(qc, args.uri, {bookmarked: false}) + await agent.app.bsky.bookmark.deleteBookmark({ + uri: args.uri, + }) + } + }, + onSuccess(_, args) { + if (args.action === 'create') { + optimisticallySaveBookmark(qc, args.post) + } else if (args.action === 'delete') { + optimisticallyDeleteBookmark(qc, {uri: args.uri}) + } + }, + onError(e, args) { + if (args.action === 'create') { + updatePostShadow(qc, args.post.uri, {bookmarked: false}) + } else if (args.action === 'delete') { + updatePostShadow(qc, args.uri, {bookmarked: true}) + } + + if (!isNetworkError(e)) { + logger.error('bookmark mutation failed', { + bookmarkAction: args.action, + safeMessage: e, + }) + } + }, + }) +} diff --git a/src/state/queries/bookmarks/useBookmarksQuery.ts b/src/state/queries/bookmarks/useBookmarksQuery.ts new file mode 100644 index 000000000..46838facb --- /dev/null +++ b/src/state/queries/bookmarks/useBookmarksQuery.ts @@ -0,0 +1,114 @@ +import { + type $Typed, + type AppBskyBookmarkGetBookmarks, + type AppBskyFeedDefs, +} from '@atproto/api' +import { + type InfiniteData, + type QueryClient, + type QueryKey, + useInfiniteQuery, +} from '@tanstack/react-query' + +import {useAgent} from '#/state/session' + +export const bookmarksQueryKeyRoot = 'bookmarks' +export const createBookmarksQueryKey = () => [bookmarksQueryKeyRoot] + +export function useBookmarksQuery() { + const agent = useAgent() + + return useInfiniteQuery< + AppBskyBookmarkGetBookmarks.OutputSchema, + Error, + InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>, + QueryKey, + string | undefined + >({ + queryKey: createBookmarksQueryKey(), + async queryFn({pageParam}) { + const res = await agent.app.bsky.bookmark.getBookmarks({ + cursor: pageParam, + }) + return res.data + }, + initialPageParam: undefined, + getNextPageParam: lastPage => lastPage.cursor, + }) +} + +export async function truncateAndInvalidate(qc: QueryClient) { + qc.setQueriesData<InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>>( + {queryKey: [bookmarksQueryKeyRoot]}, + data => { + if (data) { + return { + pageParams: data.pageParams.slice(0, 1), + pages: data.pages.slice(0, 1), + } + } + return data + }, + ) + return qc.invalidateQueries({queryKey: [bookmarksQueryKeyRoot]}) +} + +export async function optimisticallySaveBookmark( + qc: QueryClient, + post: AppBskyFeedDefs.PostView, +) { + qc.setQueriesData<InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>>( + { + queryKey: [bookmarksQueryKeyRoot], + }, + data => { + if (!data) return data + return { + ...data, + pages: data.pages.map((page, index) => { + if (index === 0) { + post.$type = 'app.bsky.feed.defs#postView' + return { + ...page, + bookmarks: [ + { + createdAt: new Date().toISOString(), + subject: { + uri: post.uri, + cid: post.cid, + }, + item: post as $Typed<AppBskyFeedDefs.PostView>, + }, + ...page.bookmarks, + ], + } + } + return page + }), + } + }, + ) +} + +export async function optimisticallyDeleteBookmark( + qc: QueryClient, + {uri}: {uri: string}, +) { + qc.setQueriesData<InfiniteData<AppBskyBookmarkGetBookmarks.OutputSchema>>( + { + queryKey: [bookmarksQueryKeyRoot], + }, + data => { + if (!data) return data + return { + ...data, + pages: data.pages.map(page => { + return { + ...page, + bookmarks: page.bookmarks.filter(b => b.subject.uri !== uri), + } + }), + } + }, + ) +} diff --git a/src/state/queries/nuxs/definitions.ts b/src/state/queries/nuxs/definitions.ts index 7577d6b20..165649447 100644 --- a/src/state/queries/nuxs/definitions.ts +++ b/src/state/queries/nuxs/definitions.ts @@ -9,6 +9,7 @@ export enum Nux { ActivitySubscriptions = 'ActivitySubscriptions', AgeAssuranceDismissibleNotice = 'AgeAssuranceDismissibleNotice', AgeAssuranceDismissibleFeedBanner = 'AgeAssuranceDismissibleFeedBanner', + BookmarksAnnouncement = 'BookmarksAnnouncement', /* * Blocking announcements. New IDs are required for each new announcement. @@ -47,6 +48,10 @@ export type AppNux = BaseNux< id: Nux.PolicyUpdate202508 data: undefined } + | { + id: Nux.BookmarksAnnouncement + data: undefined + } > export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = { @@ -57,4 +62,5 @@ export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = { [Nux.AgeAssuranceDismissibleNotice]: undefined, [Nux.AgeAssuranceDismissibleFeedBanner]: undefined, [Nux.PolicyUpdate202508]: undefined, + [Nux.BookmarksAnnouncement]: undefined, } |