mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
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}