my fork of the bluesky client
at main 376 lines 10 kB view raw
1import {useCallback} from 'react' 2import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api' 3import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' 4 5import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue' 6import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig' 7import {updatePostShadow} from '#/state/cache/post-shadow' 8import {Shadow} from '#/state/cache/types' 9import {useAgent, useSession} from '#/state/session' 10import * as userActionHistory from '#/state/userActionHistory' 11import {useIsThreadMuted, useSetThreadMute} from '../cache/thread-mutes' 12import {findProfileQueryData} from './profile' 13 14const RQKEY_ROOT = 'post' 15export const RQKEY = (postUri: string) => [RQKEY_ROOT, postUri] 16 17export function usePostQuery(uri: string | undefined) { 18 const agent = useAgent() 19 return useQuery<AppBskyFeedDefs.PostView>({ 20 queryKey: RQKEY(uri || ''), 21 async queryFn() { 22 const urip = new AtUri(uri!) 23 24 if (!urip.host.startsWith('did:')) { 25 const res = await agent.resolveHandle({ 26 handle: urip.host, 27 }) 28 urip.host = res.data.did 29 } 30 31 const res = await agent.getPosts({uris: [urip.toString()]}) 32 if (res.success && res.data.posts[0]) { 33 return res.data.posts[0] 34 } 35 36 throw new Error('No data') 37 }, 38 enabled: !!uri, 39 }) 40} 41 42export function useGetPost() { 43 const queryClient = useQueryClient() 44 const agent = useAgent() 45 return useCallback( 46 async ({uri}: {uri: string}) => { 47 return queryClient.fetchQuery({ 48 queryKey: RQKEY(uri || ''), 49 async queryFn() { 50 const urip = new AtUri(uri) 51 52 if (!urip.host.startsWith('did:')) { 53 const res = await agent.resolveHandle({ 54 handle: urip.host, 55 }) 56 urip.host = res.data.did 57 } 58 59 const res = await agent.getPosts({ 60 uris: [urip.toString()], 61 }) 62 63 if (res.success && res.data.posts[0]) { 64 return res.data.posts[0] 65 } 66 67 throw new Error('useGetPost: post not found') 68 }, 69 }) 70 }, 71 [queryClient, agent], 72 ) 73} 74 75export function useGetPosts() { 76 const queryClient = useQueryClient() 77 const agent = useAgent() 78 return useCallback( 79 async ({uris}: {uris: string[]}) => { 80 return queryClient.fetchQuery({ 81 queryKey: RQKEY(uris.join(',') || ''), 82 async queryFn() { 83 const res = await agent.getPosts({ 84 uris, 85 }) 86 87 if (res.success) { 88 return res.data.posts 89 } else { 90 throw new Error('useGetPosts failed') 91 } 92 }, 93 }) 94 }, 95 [queryClient, agent], 96 ) 97} 98 99export function usePostLikeMutationQueue( 100 post: Shadow<AppBskyFeedDefs.PostView>, 101 logContext: LogEvents['post:like']['logContext'] & 102 LogEvents['post:unlike']['logContext'], 103) { 104 const queryClient = useQueryClient() 105 const postUri = post.uri 106 const postCid = post.cid 107 const initialLikeUri = post.viewer?.like 108 const likeMutation = usePostLikeMutation(logContext, post) 109 const unlikeMutation = usePostUnlikeMutation(logContext) 110 111 const queueToggle = useToggleMutationQueue({ 112 initialState: initialLikeUri, 113 runMutation: async (prevLikeUri, shouldLike) => { 114 if (shouldLike) { 115 const {uri: likeUri} = await likeMutation.mutateAsync({ 116 uri: postUri, 117 cid: postCid, 118 }) 119 userActionHistory.like([postUri]) 120 return likeUri 121 } else { 122 if (prevLikeUri) { 123 await unlikeMutation.mutateAsync({ 124 postUri: postUri, 125 likeUri: prevLikeUri, 126 }) 127 userActionHistory.unlike([postUri]) 128 } 129 return undefined 130 } 131 }, 132 onSuccess(finalLikeUri) { 133 // finalize 134 updatePostShadow(queryClient, postUri, { 135 likeUri: finalLikeUri, 136 }) 137 }, 138 }) 139 140 const queueLike = useCallback(() => { 141 // optimistically update 142 updatePostShadow(queryClient, postUri, { 143 likeUri: 'pending', 144 }) 145 return queueToggle(true) 146 }, [queryClient, postUri, queueToggle]) 147 148 const queueUnlike = useCallback(() => { 149 // optimistically update 150 updatePostShadow(queryClient, postUri, { 151 likeUri: undefined, 152 }) 153 return queueToggle(false) 154 }, [queryClient, postUri, queueToggle]) 155 156 return [queueLike, queueUnlike] 157} 158 159function usePostLikeMutation( 160 logContext: LogEvents['post:like']['logContext'], 161 post: Shadow<AppBskyFeedDefs.PostView>, 162) { 163 const {currentAccount} = useSession() 164 const queryClient = useQueryClient() 165 const postAuthor = post.author 166 const agent = useAgent() 167 return useMutation< 168 {uri: string}, // responds with the uri of the like 169 Error, 170 {uri: string; cid: string} // the post's uri and cid 171 >({ 172 mutationFn: ({uri, cid}) => { 173 let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined 174 if (currentAccount) { 175 ownProfile = findProfileQueryData(queryClient, currentAccount.did) 176 } 177 logEvent('post:like', { 178 logContext, 179 doesPosterFollowLiker: postAuthor.viewer 180 ? Boolean(postAuthor.viewer.followedBy) 181 : undefined, 182 doesLikerFollowPoster: postAuthor.viewer 183 ? Boolean(postAuthor.viewer.following) 184 : undefined, 185 likerClout: toClout(ownProfile?.followersCount), 186 postClout: 187 post.likeCount != null && 188 post.repostCount != null && 189 post.replyCount != null 190 ? toClout(post.likeCount + post.repostCount + post.replyCount) 191 : undefined, 192 }) 193 return agent.like(uri, cid) 194 }, 195 }) 196} 197 198function usePostUnlikeMutation( 199 logContext: LogEvents['post:unlike']['logContext'], 200) { 201 const agent = useAgent() 202 return useMutation<void, Error, {postUri: string; likeUri: string}>({ 203 mutationFn: ({likeUri}) => { 204 logEvent('post:unlike', {logContext}) 205 return agent.deleteLike(likeUri) 206 }, 207 }) 208} 209 210export function usePostRepostMutationQueue( 211 post: Shadow<AppBskyFeedDefs.PostView>, 212 logContext: LogEvents['post:repost']['logContext'] & 213 LogEvents['post:unrepost']['logContext'], 214) { 215 const queryClient = useQueryClient() 216 const postUri = post.uri 217 const postCid = post.cid 218 const initialRepostUri = post.viewer?.repost 219 const repostMutation = usePostRepostMutation(logContext) 220 const unrepostMutation = usePostUnrepostMutation(logContext) 221 222 const queueToggle = useToggleMutationQueue({ 223 initialState: initialRepostUri, 224 runMutation: async (prevRepostUri, shouldRepost) => { 225 if (shouldRepost) { 226 const {uri: repostUri} = await repostMutation.mutateAsync({ 227 uri: postUri, 228 cid: postCid, 229 }) 230 return repostUri 231 } else { 232 if (prevRepostUri) { 233 await unrepostMutation.mutateAsync({ 234 postUri: postUri, 235 repostUri: prevRepostUri, 236 }) 237 } 238 return undefined 239 } 240 }, 241 onSuccess(finalRepostUri) { 242 // finalize 243 updatePostShadow(queryClient, postUri, { 244 repostUri: finalRepostUri, 245 }) 246 }, 247 }) 248 249 const queueRepost = useCallback(() => { 250 // optimistically update 251 updatePostShadow(queryClient, postUri, { 252 repostUri: 'pending', 253 }) 254 return queueToggle(true) 255 }, [queryClient, postUri, queueToggle]) 256 257 const queueUnrepost = useCallback(() => { 258 // optimistically update 259 updatePostShadow(queryClient, postUri, { 260 repostUri: undefined, 261 }) 262 return queueToggle(false) 263 }, [queryClient, postUri, queueToggle]) 264 265 return [queueRepost, queueUnrepost] 266} 267 268function usePostRepostMutation( 269 logContext: LogEvents['post:repost']['logContext'], 270) { 271 const agent = useAgent() 272 return useMutation< 273 {uri: string}, // responds with the uri of the repost 274 Error, 275 {uri: string; cid: string} // the post's uri and cid 276 >({ 277 mutationFn: post => { 278 logEvent('post:repost', {logContext}) 279 return agent.repost(post.uri, post.cid) 280 }, 281 }) 282} 283 284function usePostUnrepostMutation( 285 logContext: LogEvents['post:unrepost']['logContext'], 286) { 287 const agent = useAgent() 288 return useMutation<void, Error, {postUri: string; repostUri: string}>({ 289 mutationFn: ({repostUri}) => { 290 logEvent('post:unrepost', {logContext}) 291 return agent.deleteRepost(repostUri) 292 }, 293 }) 294} 295 296export function usePostDeleteMutation() { 297 const queryClient = useQueryClient() 298 const agent = useAgent() 299 return useMutation<void, Error, {uri: string}>({ 300 mutationFn: async ({uri}) => { 301 await agent.deletePost(uri) 302 }, 303 onSuccess(_, variables) { 304 updatePostShadow(queryClient, variables.uri, {isDeleted: true}) 305 }, 306 }) 307} 308 309export function useThreadMuteMutationQueue( 310 post: Shadow<AppBskyFeedDefs.PostView>, 311 rootUri: string, 312) { 313 const threadMuteMutation = useThreadMuteMutation() 314 const threadUnmuteMutation = useThreadUnmuteMutation() 315 const isThreadMuted = useIsThreadMuted(rootUri, post.viewer?.threadMuted) 316 const setThreadMute = useSetThreadMute() 317 318 const queueToggle = useToggleMutationQueue<boolean>({ 319 initialState: isThreadMuted, 320 runMutation: async (_prev, shouldMute) => { 321 if (shouldMute) { 322 await threadMuteMutation.mutateAsync({ 323 uri: rootUri, 324 }) 325 return true 326 } else { 327 await threadUnmuteMutation.mutateAsync({ 328 uri: rootUri, 329 }) 330 return false 331 } 332 }, 333 onSuccess(finalIsMuted) { 334 // finalize 335 setThreadMute(rootUri, finalIsMuted) 336 }, 337 }) 338 339 const queueMuteThread = useCallback(() => { 340 // optimistically update 341 setThreadMute(rootUri, true) 342 return queueToggle(true) 343 }, [setThreadMute, rootUri, queueToggle]) 344 345 const queueUnmuteThread = useCallback(() => { 346 // optimistically update 347 setThreadMute(rootUri, false) 348 return queueToggle(false) 349 }, [rootUri, setThreadMute, queueToggle]) 350 351 return [isThreadMuted, queueMuteThread, queueUnmuteThread] as const 352} 353 354function useThreadMuteMutation() { 355 const agent = useAgent() 356 return useMutation< 357 {}, 358 Error, 359 {uri: string} // the root post's uri 360 >({ 361 mutationFn: ({uri}) => { 362 logEvent('post:mute', {}) 363 return agent.api.app.bsky.graph.muteThread({root: uri}) 364 }, 365 }) 366} 367 368function useThreadUnmuteMutation() { 369 const agent = useAgent() 370 return useMutation<{}, Error, {uri: string}>({ 371 mutationFn: ({uri}) => { 372 logEvent('post:unmute', {}) 373 return agent.api.app.bsky.graph.unmuteThread({root: uri}) 374 }, 375 }) 376}