diff options
author | dan <dan.abramov@gmail.com> | 2024-01-25 21:25:12 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-25 21:25:12 +0000 |
commit | 8d3179f0825a8d0bb55566c14e70e02a555ad3bf (patch) | |
tree | 2c29cf992f9d893d0481e3ab592e4c6c9b06f141 /src/state/queries/post.ts | |
parent | 3b26b32f7f8b51b8349754df2b4d12717a9b932e (diff) | |
download | voidsky-8d3179f0825a8d0bb55566c14e70e02a555ad3bf.tar.zst |
Fix races for post like/repost toggle (#2617)
Diffstat (limited to 'src/state/queries/post.ts')
-rw-r--r-- | src/state/queries/post.ts | 187 |
1 files changed, 121 insertions, 66 deletions
diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index 449304ad0..eb59f7da4 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -1,10 +1,11 @@ -import React from 'react' +import {useCallback} from 'react' import {AppBskyFeedDefs, AtUri} from '@atproto/api' import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query' - +import {Shadow} from '#/state/cache/types' import {getAgent} from '#/state/session' import {updatePostShadow} from '#/state/cache/post-shadow' import {track} from '#/lib/analytics/analytics' +import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue' export const RQKEY = (postUri: string) => ['post', postUri] @@ -25,7 +26,7 @@ export function usePostQuery(uri: string | undefined) { export function useGetPost() { const queryClient = useQueryClient() - return React.useCallback( + return useCallback( async ({uri}: {uri: string}) => { return queryClient.fetchQuery({ queryKey: RQKEY(uri || ''), @@ -55,103 +56,157 @@ export function useGetPost() { ) } -export function usePostLikeMutation() { +export function usePostLikeMutationQueue( + post: Shadow<AppBskyFeedDefs.PostView>, +) { + const postUri = post.uri + const postCid = post.cid + const initialLikeUri = post.viewer?.like + const likeMutation = usePostLikeMutation() + const unlikeMutation = usePostUnlikeMutation() + + const queueToggle = useToggleMutationQueue({ + initialState: initialLikeUri, + runMutation: async (prevLikeUri, shouldLike) => { + if (shouldLike) { + const {uri: likeUri} = await likeMutation.mutateAsync({ + uri: postUri, + cid: postCid, + }) + return likeUri + } else { + if (prevLikeUri) { + await unlikeMutation.mutateAsync({ + postUri: postUri, + likeUri: prevLikeUri, + }) + } + return undefined + } + }, + onSuccess(finalLikeUri) { + // finalize + updatePostShadow(postUri, { + likeUri: finalLikeUri, + }) + }, + }) + + const queueLike = useCallback(() => { + // optimistically update + updatePostShadow(postUri, { + likeUri: 'pending', + }) + return queueToggle(true) + }, [postUri, queueToggle]) + + const queueUnlike = useCallback(() => { + // optimistically update + updatePostShadow(postUri, { + likeUri: undefined, + }) + return queueToggle(false) + }, [postUri, queueToggle]) + + return [queueLike, queueUnlike] +} + +function usePostLikeMutation() { return useMutation< {uri: string}, // responds with the uri of the like Error, {uri: string; cid: string} // the post's uri and cid >({ mutationFn: post => getAgent().like(post.uri, post.cid), - onMutate(variables) { - // optimistically update the post-shadow - updatePostShadow(variables.uri, { - likeUri: 'pending', - }) - }, - onSuccess(data, variables) { - // finalize the post-shadow with the like URI - updatePostShadow(variables.uri, { - likeUri: data.uri, - }) + onSuccess() { track('Post:Like') }, - onError(error, variables) { - // revert the optimistic update - updatePostShadow(variables.uri, { - likeUri: undefined, - }) - }, }) } -export function usePostUnlikeMutation() { +function usePostUnlikeMutation() { return useMutation<void, Error, {postUri: string; likeUri: string}>({ - mutationFn: async ({likeUri}) => { - await getAgent().deleteLike(likeUri) + mutationFn: ({likeUri}) => getAgent().deleteLike(likeUri), + onSuccess() { track('Post:Unlike') }, - onMutate(variables) { - // optimistically update the post-shadow - updatePostShadow(variables.postUri, { - likeUri: undefined, - }) + }) +} + +export function usePostRepostMutationQueue( + post: Shadow<AppBskyFeedDefs.PostView>, +) { + const postUri = post.uri + const postCid = post.cid + const initialRepostUri = post.viewer?.repost + const repostMutation = usePostRepostMutation() + const unrepostMutation = usePostUnrepostMutation() + + const queueToggle = useToggleMutationQueue({ + initialState: initialRepostUri, + runMutation: async (prevRepostUri, shouldRepost) => { + if (shouldRepost) { + const {uri: repostUri} = await repostMutation.mutateAsync({ + uri: postUri, + cid: postCid, + }) + return repostUri + } else { + if (prevRepostUri) { + await unrepostMutation.mutateAsync({ + postUri: postUri, + repostUri: prevRepostUri, + }) + } + return undefined + } }, - onError(error, variables) { - // revert the optimistic update - updatePostShadow(variables.postUri, { - likeUri: variables.likeUri, + onSuccess(finalRepostUri) { + // finalize + updatePostShadow(postUri, { + repostUri: finalRepostUri, }) }, }) + + const queueRepost = useCallback(() => { + // optimistically update + updatePostShadow(postUri, { + repostUri: 'pending', + }) + return queueToggle(true) + }, [postUri, queueToggle]) + + const queueUnrepost = useCallback(() => { + // optimistically update + updatePostShadow(postUri, { + repostUri: undefined, + }) + return queueToggle(false) + }, [postUri, queueToggle]) + + return [queueRepost, queueUnrepost] } -export function usePostRepostMutation() { +function usePostRepostMutation() { return useMutation< {uri: string}, // responds with the uri of the repost Error, {uri: string; cid: string} // the post's uri and cid >({ mutationFn: post => getAgent().repost(post.uri, post.cid), - onMutate(variables) { - // optimistically update the post-shadow - updatePostShadow(variables.uri, { - repostUri: 'pending', - }) - }, - onSuccess(data, variables) { - // finalize the post-shadow with the repost URI - updatePostShadow(variables.uri, { - repostUri: data.uri, - }) + onSuccess() { track('Post:Repost') }, - onError(error, variables) { - // revert the optimistic update - updatePostShadow(variables.uri, { - repostUri: undefined, - }) - }, }) } -export function usePostUnrepostMutation() { +function usePostUnrepostMutation() { return useMutation<void, Error, {postUri: string; repostUri: string}>({ - mutationFn: async ({repostUri}) => { - await getAgent().deleteRepost(repostUri) + mutationFn: ({repostUri}) => getAgent().deleteRepost(repostUri), + onSuccess() { track('Post:Unrepost') }, - onMutate(variables) { - // optimistically update the post-shadow - updatePostShadow(variables.postUri, { - repostUri: undefined, - }) - }, - onError(error, variables) { - // revert the optimistic update - updatePostShadow(variables.postUri, { - repostUri: variables.repostUri, - }) - }, }) } |