about summary refs log tree commit diff
path: root/src/state/queries/post.ts
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2024-01-25 21:25:12 +0000
committerGitHub <noreply@github.com>2024-01-25 21:25:12 +0000
commit8d3179f0825a8d0bb55566c14e70e02a555ad3bf (patch)
tree2c29cf992f9d893d0481e3ab592e4c6c9b06f141 /src/state/queries/post.ts
parent3b26b32f7f8b51b8349754df2b4d12717a9b932e (diff)
downloadvoidsky-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.ts187
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,
-      })
-    },
   })
 }