forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}