Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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