Bluesky app fork with some witchin' additions 馃挮
at feat/tealfm 300 lines 7.5 kB view raw
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}