forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React from 'react'
2import {
3 AppBskyEmbedRecord,
4 AppBskyEmbedRecordWithMedia,
5 type AppBskyFeedDefs,
6 AppBskyFeedPostgate,
7 AtUri,
8 type BskyAgent,
9} from '@atproto/api'
10import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
11
12import {networkRetry, retry} from '#/lib/async/retry'
13import {logger} from '#/logger'
14import {updatePostShadow} from '#/state/cache/post-shadow'
15import {STALE} from '#/state/queries'
16import {useGetPosts} from '#/state/queries/post'
17import {
18 createMaybeDetachedQuoteEmbed,
19 createPostgateRecord,
20 mergePostgateRecords,
21 POSTGATE_COLLECTION,
22} from '#/state/queries/postgate/util'
23import {useAgent} from '#/state/session'
24import * as bsky from '#/types/bsky'
25
26export async function getPostgateRecord({
27 agent,
28 postUri,
29}: {
30 agent: BskyAgent
31 postUri: string
32}): Promise<AppBskyFeedPostgate.Record | undefined> {
33 const urip = new AtUri(postUri)
34
35 if (!urip.host.startsWith('did:')) {
36 const res = await agent.resolveHandle({
37 handle: urip.host,
38 })
39 // @ts-expect-error TODO new-sdk-migration
40 urip.host = res.data.did
41 }
42
43 try {
44 const {data} = await retry(
45 2,
46 e => {
47 /*
48 * If the record doesn't exist, we want to return null instead of
49 * throwing an error. NB: This will also catch reference errors, such as
50 * a typo in the URI.
51 */
52 if (e.message.includes(`Could not locate record:`)) {
53 return false
54 }
55 return true
56 },
57 () =>
58 agent.api.com.atproto.repo.getRecord({
59 repo: urip.host,
60 collection: POSTGATE_COLLECTION,
61 rkey: urip.rkey,
62 }),
63 )
64
65 if (
66 data.value &&
67 bsky.validate(data.value, AppBskyFeedPostgate.validateRecord)
68 ) {
69 return data.value
70 } else {
71 return undefined
72 }
73 } catch (e: any) {
74 /*
75 * If the record doesn't exist, we want to return null instead of
76 * throwing an error. NB: This will also catch reference errors, such as
77 * a typo in the URI.
78 */
79 if (e.message.includes(`Could not locate record:`)) {
80 return undefined
81 } else {
82 throw e
83 }
84 }
85}
86
87export async function writePostgateRecord({
88 agent,
89 postUri,
90 postgate,
91}: {
92 agent: BskyAgent
93 postUri: string
94 postgate: AppBskyFeedPostgate.Record
95}) {
96 const postUrip = new AtUri(postUri)
97
98 await networkRetry(2, () =>
99 agent.api.com.atproto.repo.putRecord({
100 repo: agent.session!.did,
101 collection: POSTGATE_COLLECTION,
102 rkey: postUrip.rkey,
103 record: postgate,
104 }),
105 )
106}
107
108export async function upsertPostgate(
109 {
110 agent,
111 postUri,
112 }: {
113 agent: BskyAgent
114 postUri: string
115 },
116 callback: (
117 postgate: AppBskyFeedPostgate.Record | undefined,
118 ) => Promise<AppBskyFeedPostgate.Record | undefined>,
119) {
120 const prev = await getPostgateRecord({
121 agent,
122 postUri,
123 })
124 const next = await callback(prev)
125 if (!next) return
126 await writePostgateRecord({
127 agent,
128 postUri,
129 postgate: next,
130 })
131}
132
133export const createPostgateQueryKey = (postUri: string) => [
134 'postgate-record',
135 postUri,
136]
137export function usePostgateQuery({postUri}: {postUri: string}) {
138 const agent = useAgent()
139 return useQuery({
140 staleTime: STALE.SECONDS.THIRTY,
141 queryKey: createPostgateQueryKey(postUri),
142 async queryFn() {
143 return await getPostgateRecord({agent, postUri}).then(res => res ?? null)
144 },
145 })
146}
147
148export function useWritePostgateMutation() {
149 const agent = useAgent()
150 const queryClient = useQueryClient()
151 return useMutation({
152 mutationFn: async ({
153 postUri,
154 postgate,
155 }: {
156 postUri: string
157 postgate: AppBskyFeedPostgate.Record
158 }) => {
159 return writePostgateRecord({
160 agent,
161 postUri,
162 postgate,
163 })
164 },
165 onSuccess(_, {postUri}) {
166 queryClient.invalidateQueries({
167 queryKey: createPostgateQueryKey(postUri),
168 })
169 },
170 })
171}
172
173export function useToggleQuoteDetachmentMutation() {
174 const agent = useAgent()
175 const queryClient = useQueryClient()
176 const getPosts = useGetPosts()
177 const prevEmbed = React.useRef<AppBskyFeedDefs.PostView['embed']>(undefined)
178
179 return useMutation({
180 mutationFn: async ({
181 post,
182 quoteUri,
183 action,
184 }: {
185 post: AppBskyFeedDefs.PostView
186 quoteUri: string
187 action: 'detach' | 'reattach'
188 }) => {
189 // cache here since post shadow mutates original object
190 prevEmbed.current = post.embed
191
192 if (action === 'detach') {
193 updatePostShadow(queryClient, post.uri, {
194 embed: createMaybeDetachedQuoteEmbed({
195 post,
196 quote: undefined,
197 quoteUri,
198 detached: true,
199 }),
200 })
201 }
202
203 await upsertPostgate({agent, postUri: quoteUri}, async prev => {
204 if (prev) {
205 if (action === 'detach') {
206 return mergePostgateRecords(prev, {
207 detachedEmbeddingUris: [post.uri],
208 })
209 } else if (action === 'reattach') {
210 return {
211 ...prev,
212 detachedEmbeddingUris:
213 prev.detachedEmbeddingUris?.filter(uri => uri !== post.uri) ||
214 [],
215 }
216 }
217 } else {
218 if (action === 'detach') {
219 return createPostgateRecord({
220 post: quoteUri,
221 detachedEmbeddingUris: [post.uri],
222 })
223 }
224 }
225 })
226 },
227 async onSuccess(_data, {post, quoteUri, action}) {
228 if (action === 'reattach') {
229 try {
230 const [quote] = await getPosts({uris: [quoteUri]})
231 updatePostShadow(queryClient, post.uri, {
232 embed: createMaybeDetachedQuoteEmbed({
233 post,
234 quote,
235 quoteUri: undefined,
236 detached: false,
237 }),
238 })
239 } catch (e: any) {
240 // ok if this fails, it's just optimistic UI
241 logger.error(`Postgate: failed to get quote post for re-attachment`, {
242 safeMessage: e.message,
243 })
244 }
245 }
246 },
247 onError(_, {post, action}) {
248 if (action === 'detach' && prevEmbed.current) {
249 // detach failed, add the embed back
250 if (
251 AppBskyEmbedRecord.isView(prevEmbed.current) ||
252 AppBskyEmbedRecordWithMedia.isView(prevEmbed.current)
253 ) {
254 updatePostShadow(queryClient, post.uri, {
255 embed: prevEmbed.current,
256 })
257 }
258 }
259 },
260 onSettled() {
261 prevEmbed.current = undefined
262 },
263 })
264}
265
266export function useToggleQuotepostEnabledMutation() {
267 const agent = useAgent()
268
269 return useMutation({
270 mutationFn: async ({
271 postUri,
272 action,
273 }: {
274 postUri: string
275 action: 'enable' | 'disable'
276 }) => {
277 await upsertPostgate({agent, postUri: postUri}, async prev => {
278 if (prev) {
279 if (action === 'disable') {
280 return mergePostgateRecords(prev, {
281 embeddingRules: [{$type: 'app.bsky.feed.postgate#disableRule'}],
282 })
283 } else if (action === 'enable') {
284 return {
285 ...prev,
286 embeddingRules: [],
287 }
288 }
289 } else {
290 if (action === 'disable') {
291 return createPostgateRecord({
292 post: postUri,
293 embeddingRules: [{$type: 'app.bsky.feed.postgate#disableRule'}],
294 })
295 }
296 }
297 })
298 },
299 })
300}