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

Configure Feed

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

at post-text-option 393 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 {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 {useDisableViaRepostNotification} from '#/state/preferences/disable-via-repost-notification' 11import {useAgent, useSession} from '#/state/session' 12import * as userActionHistory from '#/state/userActionHistory' 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: LogEvents['post:like']['logContext'] & 108 LogEvents['post:unlike']['logContext'], 109) { 110 const queryClient = useQueryClient() 111 const postUri = post.uri 112 const postCid = post.cid 113 const initialLikeUri = post.viewer?.like 114 const likeMutation = usePostLikeMutation(feedDescriptor, logContext, post) 115 const unlikeMutation = usePostUnlikeMutation(feedDescriptor, logContext) 116 const disableViaRepostNotification = useDisableViaRepostNotification() 117 118 const queueToggle = useToggleMutationQueue({ 119 initialState: initialLikeUri, 120 runMutation: async (prevLikeUri, shouldLike) => { 121 if (shouldLike) { 122 const {uri: likeUri} = await likeMutation.mutateAsync({ 123 uri: postUri, 124 cid: postCid, 125 via: disableViaRepostNotification ? undefined : viaRepost, 126 }) 127 userActionHistory.like([postUri]) 128 return likeUri 129 } else { 130 if (prevLikeUri) { 131 await unlikeMutation.mutateAsync({ 132 postUri: postUri, 133 likeUri: prevLikeUri, 134 }) 135 userActionHistory.unlike([postUri]) 136 } 137 return undefined 138 } 139 }, 140 onSuccess(finalLikeUri) { 141 // finalize 142 updatePostShadow(queryClient, postUri, { 143 likeUri: finalLikeUri, 144 }) 145 }, 146 }) 147 148 const queueLike = useCallback(() => { 149 // optimistically update 150 updatePostShadow(queryClient, postUri, { 151 likeUri: 'pending', 152 }) 153 return queueToggle(true) 154 }, [queryClient, postUri, queueToggle]) 155 156 const queueUnlike = useCallback(() => { 157 // optimistically update 158 updatePostShadow(queryClient, postUri, { 159 likeUri: undefined, 160 }) 161 return queueToggle(false) 162 }, [queryClient, postUri, queueToggle]) 163 164 return [queueLike, queueUnlike] 165} 166 167function usePostLikeMutation( 168 feedDescriptor: string | undefined, 169 logContext: LogEvents['post:like']['logContext'], 170 post: Shadow<AppBskyFeedDefs.PostView>, 171) { 172 const {currentAccount} = useSession() 173 const queryClient = useQueryClient() 174 const postAuthor = post.author 175 const agent = useAgent() 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 logger.metric('post:like', { 187 logContext, 188 doesPosterFollowLiker: postAuthor.viewer 189 ? Boolean(postAuthor.viewer.followedBy) 190 : undefined, 191 doesLikerFollowPoster: postAuthor.viewer 192 ? Boolean(postAuthor.viewer.following) 193 : undefined, 194 likerClout: toClout(ownProfile?.followersCount), 195 postClout: 196 post.likeCount != null && 197 post.repostCount != null && 198 post.replyCount != null 199 ? toClout(post.likeCount + post.repostCount + post.replyCount) 200 : undefined, 201 feedDescriptor: feedDescriptor, 202 }) 203 return agent.like(uri, cid, via) 204 }, 205 }) 206} 207 208function usePostUnlikeMutation( 209 feedDescriptor: string | undefined, 210 logContext: LogEvents['post:unlike']['logContext'], 211) { 212 const agent = useAgent() 213 return useMutation<void, Error, {postUri: string; likeUri: string}>({ 214 mutationFn: ({likeUri}) => { 215 logger.metric('post:unlike', {logContext, feedDescriptor}) 216 return agent.deleteLike(likeUri) 217 }, 218 }) 219} 220 221export function usePostRepostMutationQueue( 222 post: Shadow<AppBskyFeedDefs.PostView>, 223 viaRepost: {uri: string; cid: string} | undefined, 224 feedDescriptor: string | undefined, 225 logContext: LogEvents['post:repost']['logContext'] & 226 LogEvents['post:unrepost']['logContext'], 227) { 228 const queryClient = useQueryClient() 229 const postUri = post.uri 230 const postCid = post.cid 231 const initialRepostUri = post.viewer?.repost 232 const repostMutation = usePostRepostMutation(feedDescriptor, logContext) 233 const unrepostMutation = usePostUnrepostMutation(feedDescriptor, logContext) 234 const disableViaRepostNotification = useDisableViaRepostNotification() 235 236 const queueToggle = useToggleMutationQueue({ 237 initialState: initialRepostUri, 238 runMutation: async (prevRepostUri, shouldRepost) => { 239 if (shouldRepost) { 240 const {uri: repostUri} = await repostMutation.mutateAsync({ 241 uri: postUri, 242 cid: postCid, 243 via: disableViaRepostNotification ? undefined : viaRepost, 244 }) 245 return repostUri 246 } else { 247 if (prevRepostUri) { 248 await unrepostMutation.mutateAsync({ 249 postUri: postUri, 250 repostUri: prevRepostUri, 251 }) 252 } 253 return undefined 254 } 255 }, 256 onSuccess(finalRepostUri) { 257 // finalize 258 updatePostShadow(queryClient, postUri, { 259 repostUri: finalRepostUri, 260 }) 261 }, 262 }) 263 264 const queueRepost = useCallback(() => { 265 // optimistically update 266 updatePostShadow(queryClient, postUri, { 267 repostUri: 'pending', 268 }) 269 return queueToggle(true) 270 }, [queryClient, postUri, queueToggle]) 271 272 const queueUnrepost = useCallback(() => { 273 // optimistically update 274 updatePostShadow(queryClient, postUri, { 275 repostUri: undefined, 276 }) 277 return queueToggle(false) 278 }, [queryClient, postUri, queueToggle]) 279 280 return [queueRepost, queueUnrepost] 281} 282 283function usePostRepostMutation( 284 feedDescriptor: string | undefined, 285 logContext: LogEvents['post:repost']['logContext'], 286) { 287 const agent = useAgent() 288 return useMutation< 289 {uri: string}, // responds with the uri of the repost 290 Error, 291 {uri: string; cid: string; via?: {uri: string; cid: string}} // the post's uri and cid, and the repost uri/cid if present 292 >({ 293 mutationFn: ({uri, cid, via}) => { 294 logger.metric('post:repost', {logContext, feedDescriptor}) 295 return agent.repost(uri, cid, via) 296 }, 297 }) 298} 299 300function usePostUnrepostMutation( 301 feedDescriptor: string | undefined, 302 logContext: LogEvents['post:unrepost']['logContext'], 303) { 304 const agent = useAgent() 305 return useMutation<void, Error, {postUri: string; repostUri: string}>({ 306 mutationFn: ({repostUri}) => { 307 logger.metric('post:unrepost', {logContext, feedDescriptor}) 308 return agent.deleteRepost(repostUri) 309 }, 310 }) 311} 312 313export function usePostDeleteMutation() { 314 const queryClient = useQueryClient() 315 const agent = useAgent() 316 return useMutation<void, Error, {uri: string}>({ 317 mutationFn: async ({uri}) => { 318 await agent.deletePost(uri) 319 }, 320 onSuccess(_, variables) { 321 updatePostShadow(queryClient, variables.uri, {isDeleted: true}) 322 }, 323 }) 324} 325 326export function useThreadMuteMutationQueue( 327 post: Shadow<AppBskyFeedDefs.PostView>, 328 rootUri: string, 329) { 330 const threadMuteMutation = useThreadMuteMutation() 331 const threadUnmuteMutation = useThreadUnmuteMutation() 332 const isThreadMuted = useIsThreadMuted(rootUri, post.viewer?.threadMuted) 333 const setThreadMute = useSetThreadMute() 334 335 const queueToggle = useToggleMutationQueue<boolean>({ 336 initialState: isThreadMuted, 337 runMutation: async (_prev, shouldMute) => { 338 if (shouldMute) { 339 await threadMuteMutation.mutateAsync({ 340 uri: rootUri, 341 }) 342 return true 343 } else { 344 await threadUnmuteMutation.mutateAsync({ 345 uri: rootUri, 346 }) 347 return false 348 } 349 }, 350 onSuccess(finalIsMuted) { 351 // finalize 352 setThreadMute(rootUri, finalIsMuted) 353 }, 354 }) 355 356 const queueMuteThread = useCallback(() => { 357 // optimistically update 358 setThreadMute(rootUri, true) 359 return queueToggle(true) 360 }, [setThreadMute, rootUri, queueToggle]) 361 362 const queueUnmuteThread = useCallback(() => { 363 // optimistically update 364 setThreadMute(rootUri, false) 365 return queueToggle(false) 366 }, [rootUri, setThreadMute, queueToggle]) 367 368 return [isThreadMuted, queueMuteThread, queueUnmuteThread] as const 369} 370 371function useThreadMuteMutation() { 372 const agent = useAgent() 373 return useMutation< 374 {}, 375 Error, 376 {uri: string} // the root post's uri 377 >({ 378 mutationFn: ({uri}) => { 379 logger.metric('post:mute', {}) 380 return agent.api.app.bsky.graph.muteThread({root: uri}) 381 }, 382 }) 383} 384 385function useThreadUnmuteMutation() { 386 const agent = useAgent() 387 return useMutation<{}, Error, {uri: string}>({ 388 mutationFn: ({uri}) => { 389 logger.metric('post:unmute', {}) 390 return agent.api.app.bsky.graph.unmuteThread({root: uri}) 391 }, 392 }) 393}