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