Fix races for post like/repost toggle (#2617)

zio/stable
dan 2024-01-25 21:25:12 +00:00 committed by GitHub
parent 3b26b32f7f
commit 8d3179f082
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 153 additions and 115 deletions

View File

@ -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,
})
},
onError(error, variables) {
// revert the optimistic update
updatePostShadow(variables.postUri, {
likeUri: variables.likeUri,
})
},
})
}
export function usePostRepostMutation() {
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
}
},
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<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,
})
},
})
}

View File

@ -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<ViewStyle>
const onPressToggleLike = React.useCallback(async () => {
try {
if (!post.viewer?.like) {
Haptics.default()
postLikeMutation.mutate({
uri: post.uri,
cid: post.cid,
})
await queueLike()
} else {
postUnlikeMutation.mutate({
postUri: post.uri,
likeUri: post.viewer.like,
})
await queueUnlike()
}
}, [
post.viewer?.like,
post.uri,
post.cid,
postLikeMutation,
postUnlikeMutation,
])
} catch (e: any) {
if (e?.name !== 'AbortError') {
throw e
}
}
}, [post.viewer?.like, queueLike, queueUnlike])
const onRepost = useCallback(() => {
const onRepost = useCallback(async () => {
closeModal()
try {
if (!post.viewer?.repost) {
Haptics.default()
postRepostMutation.mutate({
uri: post.uri,
cid: post.cid,
})
await queueRepost()
} else {
postUnrepostMutation.mutate({
postUri: post.uri,
repostUri: post.viewer.repost,
})
await queueUnrepost()
}
}, [
post.uri,
post.cid,
post.viewer?.repost,
closeModal,
postRepostMutation,
postUnrepostMutation,
])
} catch (e: any) {
if (e?.name !== 'AbortError') {
throw e
}
}
}, [post.viewer?.repost, queueRepost, queueUnrepost, closeModal])
const onQuote = useCallback(() => {
closeModal()