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