diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index 449304ad..eb59f7da 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, +) { + 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({ - 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, - }) - }, - onError(error, variables) { - // revert the optimistic update - updatePostShadow(variables.postUri, { - likeUri: variables.likeUri, - }) - }, }) } -export function usePostRepostMutation() { +export function usePostRepostMutationQueue( + post: Shadow, +) { + 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 + } + }, + 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] +} + +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({ - 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, - }) - }, }) } diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 41f5d80d..65582ba8 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -22,10 +22,8 @@ import {Haptics} from 'lib/haptics' import {HITSLOP_10, HITSLOP_20} from 'lib/constants' import {useModalControls} from '#/state/modals' import { - usePostLikeMutation, - usePostUnlikeMutation, - usePostRepostMutation, - usePostUnrepostMutation, + usePostLikeMutationQueue, + usePostRepostMutationQueue, } from '#/state/queries/post' import {useComposerControls} from '#/state/shell/composer' import {Shadow} from '#/state/cache/types' @@ -54,10 +52,8 @@ let PostCtrls = ({ const {_} = useLingui() const {openComposer} = useComposerControls() const {closeModal} = useModalControls() - const postLikeMutation = usePostLikeMutation() - const postUnlikeMutation = usePostUnlikeMutation() - const postRepostMutation = usePostRepostMutation() - const postUnrepostMutation = usePostUnrepostMutation() + const [queueLike, queueUnlike] = usePostLikeMutationQueue(post) + const [queueRepost, queueUnrepost] = usePostRepostMutationQueue(post) const requireAuth = useRequireAuth() const defaultCtrlColor = React.useMemo( @@ -68,48 +64,35 @@ let PostCtrls = ({ ) as StyleProp const onPressToggleLike = React.useCallback(async () => { - if (!post.viewer?.like) { - Haptics.default() - postLikeMutation.mutate({ - uri: post.uri, - cid: post.cid, - }) - } else { - postUnlikeMutation.mutate({ - postUri: post.uri, - likeUri: post.viewer.like, - }) + try { + if (!post.viewer?.like) { + Haptics.default() + await queueLike() + } else { + await queueUnlike() + } + } catch (e: any) { + if (e?.name !== 'AbortError') { + throw e + } } - }, [ - post.viewer?.like, - post.uri, - post.cid, - postLikeMutation, - postUnlikeMutation, - ]) + }, [post.viewer?.like, queueLike, queueUnlike]) - const onRepost = useCallback(() => { + const onRepost = useCallback(async () => { closeModal() - if (!post.viewer?.repost) { - Haptics.default() - postRepostMutation.mutate({ - uri: post.uri, - cid: post.cid, - }) - } else { - postUnrepostMutation.mutate({ - postUri: post.uri, - repostUri: post.viewer.repost, - }) + try { + if (!post.viewer?.repost) { + Haptics.default() + await queueRepost() + } else { + await queueUnrepost() + } + } catch (e: any) { + if (e?.name !== 'AbortError') { + throw e + } } - }, [ - post.uri, - post.cid, - post.viewer?.repost, - closeModal, - postRepostMutation, - postUnrepostMutation, - ]) + }, [post.viewer?.repost, queueRepost, queueUnrepost, closeModal]) const onQuote = useCallback(() => { closeModal()