mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

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

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