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