forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {
2 AppBskyFeedDefs,
3 AppBskyGraphDefs,
4 type AppBskyGraphGetStarterPack,
5 AppBskyGraphStarterpack,
6 type AppBskyRichtextFacet,
7 AtUri,
8 type BskyAgent,
9 RichText,
10} from '@atproto/api'
11import {
12 type QueryClient,
13 useMutation,
14 useQuery,
15 useQueryClient,
16} from '@tanstack/react-query'
17import chunk from 'lodash.chunk'
18
19import {until} from '#/lib/async/until'
20import {createStarterPackList} from '#/lib/generate-starterpack'
21import {
22 createStarterPackUri,
23 httpStarterPackUriToAtUri,
24 parseStarterPackUri,
25} from '#/lib/strings/starter-pack'
26import {invalidateActorStarterPacksQuery} from '#/state/queries/actor-starter-packs'
27import {STALE} from '#/state/queries/index'
28import {invalidateListMembersQuery} from '#/state/queries/list-members'
29import {useAgent} from '#/state/session'
30import * as bsky from '#/types/bsky'
31
32const RQKEY_ROOT = 'starter-pack'
33const RQKEY = ({
34 uri,
35 did,
36 rkey,
37}: {
38 uri?: string
39 did?: string
40 rkey?: string
41}) => {
42 if (uri?.startsWith('https://') || uri?.startsWith('at://')) {
43 const parsed = parseStarterPackUri(uri)
44 return [RQKEY_ROOT, parsed?.name, parsed?.rkey]
45 } else {
46 return [RQKEY_ROOT, did, rkey]
47 }
48}
49
50export function useStarterPackQuery({
51 uri,
52 did,
53 rkey,
54}: {
55 uri?: string
56 did?: string
57 rkey?: string
58}) {
59 const agent = useAgent()
60
61 return useQuery<AppBskyGraphDefs.StarterPackView>({
62 queryKey: RQKEY(uri ? {uri} : {did, rkey}),
63 queryFn: async () => {
64 if (!uri) {
65 uri = `at://${did}/app.bsky.graph.starterpack/${rkey}`
66 } else if (uri && !uri.startsWith('at://')) {
67 uri = httpStarterPackUriToAtUri(uri) as string
68 }
69
70 const res = await agent.app.bsky.graph.getStarterPack({
71 starterPack: uri,
72 })
73 return res.data.starterPack
74 },
75 enabled: Boolean(uri) || Boolean(did && rkey),
76 staleTime: STALE.MINUTES.FIVE,
77 })
78}
79
80export async function invalidateStarterPack({
81 queryClient,
82 did,
83 rkey,
84}: {
85 queryClient: QueryClient
86 did: string
87 rkey: string
88}) {
89 await queryClient.invalidateQueries({queryKey: RQKEY({did, rkey})})
90}
91
92interface UseCreateStarterPackMutationParams {
93 name: string
94 description?: string
95 profiles: bsky.profile.AnyProfileView[]
96 feeds?: AppBskyFeedDefs.GeneratorView[]
97}
98
99export function useCreateStarterPackMutation({
100 onSuccess,
101 onError,
102}: {
103 onSuccess: (data: {uri: string; cid: string}) => void
104 onError: (e: Error) => void
105}) {
106 const queryClient = useQueryClient()
107 const agent = useAgent()
108
109 return useMutation<
110 {uri: string; cid: string},
111 Error,
112 UseCreateStarterPackMutationParams
113 >({
114 mutationFn: async ({name, description, feeds, profiles}) => {
115 let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined
116 if (description) {
117 const rt = new RichText({text: description})
118 await rt.detectFacets(agent)
119 descriptionFacets = rt.facets
120 }
121
122 let listRes
123 listRes = await createStarterPackList({
124 name,
125 description,
126 profiles,
127 descriptionFacets,
128 agent,
129 })
130
131 return await agent.app.bsky.graph.starterpack.create(
132 {
133 repo: agent.assertDid,
134 },
135 {
136 name,
137 description,
138 descriptionFacets,
139 list: listRes?.uri,
140 feeds: feeds?.map(f => ({uri: f.uri})),
141 createdAt: new Date().toISOString(),
142 },
143 )
144 },
145 onSuccess: async data => {
146 await whenAppViewReady(agent, data.uri, v => {
147 return typeof v?.data.starterPack.uri === 'string'
148 })
149 await invalidateActorStarterPacksQuery({
150 queryClient,
151 did: agent.session!.did,
152 })
153 onSuccess(data)
154 },
155 onError: async error => {
156 onError(error)
157 },
158 })
159}
160
161export function useEditStarterPackMutation({
162 onSuccess,
163 onError,
164}: {
165 onSuccess: () => void
166 onError: (error: Error) => void
167}) {
168 const queryClient = useQueryClient()
169 const agent = useAgent()
170
171 return useMutation<
172 void,
173 Error,
174 UseCreateStarterPackMutationParams & {
175 currentStarterPack: AppBskyGraphDefs.StarterPackView
176 currentListItems: AppBskyGraphDefs.ListItemView[]
177 }
178 >({
179 mutationFn: async ({
180 name,
181 description,
182 feeds,
183 profiles,
184 currentStarterPack,
185 currentListItems,
186 }) => {
187 let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined
188 if (description) {
189 const rt = new RichText({text: description})
190 await rt.detectFacets(agent)
191 descriptionFacets = rt.facets
192 }
193
194 if (!AppBskyGraphStarterpack.isRecord(currentStarterPack.record)) {
195 throw new Error('Invalid starter pack')
196 }
197
198 const removedItems = currentListItems.filter(
199 i =>
200 i.subject.did !== agent.session?.did &&
201 !profiles.find(p => p.did === i.subject.did && p.did),
202 )
203 if (removedItems.length !== 0) {
204 const chunks = chunk(removedItems, 50)
205 for (const chunk of chunks) {
206 await agent.com.atproto.repo.applyWrites({
207 repo: agent.session!.did,
208 writes: chunk.map(i => ({
209 $type: 'com.atproto.repo.applyWrites#delete',
210 collection: 'app.bsky.graph.listitem',
211 rkey: new AtUri(i.uri).rkey,
212 })),
213 })
214 }
215 }
216
217 const addedProfiles = profiles.filter(
218 p => !currentListItems.find(i => i.subject.did === p.did),
219 )
220 if (addedProfiles.length > 0) {
221 const chunks = chunk(addedProfiles, 50)
222 for (const chunk of chunks) {
223 await agent.com.atproto.repo.applyWrites({
224 repo: agent.session!.did,
225 writes: chunk.map(p => ({
226 $type: 'com.atproto.repo.applyWrites#create',
227 collection: 'app.bsky.graph.listitem',
228 value: {
229 $type: 'app.bsky.graph.listitem',
230 subject: p.did,
231 list: currentStarterPack.list?.uri,
232 createdAt: new Date().toISOString(),
233 },
234 })),
235 })
236 }
237 }
238
239 const rkey = parseStarterPackUri(currentStarterPack.uri)!.rkey
240 await agent.com.atproto.repo.putRecord({
241 repo: agent.session!.did,
242 collection: 'app.bsky.graph.starterpack',
243 rkey,
244 record: {
245 name,
246 description,
247 descriptionFacets,
248 list: currentStarterPack.list?.uri,
249 feeds,
250 createdAt: currentStarterPack.record.createdAt,
251 updatedAt: new Date().toISOString(),
252 },
253 })
254 },
255 onSuccess: async (_, {currentStarterPack}) => {
256 const parsed = parseStarterPackUri(currentStarterPack.uri)
257 await whenAppViewReady(agent, currentStarterPack.uri, v => {
258 return currentStarterPack.cid !== v?.data.starterPack.cid
259 })
260 await invalidateActorStarterPacksQuery({
261 queryClient,
262 did: agent.session!.did,
263 })
264 if (currentStarterPack.list) {
265 await invalidateListMembersQuery({
266 queryClient,
267 uri: currentStarterPack.list.uri,
268 })
269 }
270 await invalidateStarterPack({
271 queryClient,
272 did: agent.session!.did,
273 rkey: parsed!.rkey,
274 })
275 onSuccess()
276 },
277 onError: error => {
278 onError(error)
279 },
280 })
281}
282
283export function useDeleteStarterPackMutation({
284 onSuccess,
285 onError,
286}: {
287 onSuccess: () => void
288 onError: (error: Error) => void
289}) {
290 const agent = useAgent()
291 const queryClient = useQueryClient()
292
293 return useMutation({
294 mutationFn: async ({listUri, rkey}: {listUri?: string; rkey: string}) => {
295 if (!agent.session) {
296 throw new Error(`Requires signed in user`)
297 }
298
299 if (listUri) {
300 await agent.app.bsky.graph.list.delete({
301 repo: agent.session.did,
302 rkey: new AtUri(listUri).rkey,
303 })
304 }
305 await agent.app.bsky.graph.starterpack.delete({
306 repo: agent.session.did,
307 rkey,
308 })
309 },
310 onSuccess: async (_, {listUri, rkey}) => {
311 const uri = createStarterPackUri({
312 did: agent.session!.did,
313 rkey,
314 })
315
316 if (uri) {
317 await whenAppViewReady(agent, uri, v => {
318 return Boolean(v?.data?.starterPack) === false
319 })
320 }
321
322 if (listUri) {
323 await invalidateListMembersQuery({queryClient, uri: listUri})
324 }
325 await invalidateActorStarterPacksQuery({
326 queryClient,
327 did: agent.session!.did,
328 })
329 await invalidateStarterPack({
330 queryClient,
331 did: agent.session!.did,
332 rkey,
333 })
334 onSuccess()
335 },
336 onError: error => {
337 onError(error)
338 },
339 })
340}
341
342async function whenAppViewReady(
343 agent: BskyAgent,
344 uri: string,
345 fn: (res?: AppBskyGraphGetStarterPack.Response) => boolean,
346) {
347 await until(
348 5, // 5 tries
349 1e3, // 1s delay between tries
350 fn,
351 () => agent.app.bsky.graph.getStarterPack({starterPack: uri}),
352 )
353}
354
355export async function precacheStarterPack(
356 queryClient: QueryClient,
357 starterPack:
358 | AppBskyGraphDefs.StarterPackViewBasic
359 | AppBskyGraphDefs.StarterPackView,
360) {
361 if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) {
362 return
363 }
364
365 let starterPackView: AppBskyGraphDefs.StarterPackView | undefined
366 if (AppBskyGraphDefs.isStarterPackView(starterPack)) {
367 starterPackView = starterPack
368 } else if (
369 AppBskyGraphDefs.isStarterPackViewBasic(starterPack) &&
370 bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord)
371 ) {
372 let feeds: AppBskyFeedDefs.GeneratorView[] | undefined
373 if (starterPack.record.feeds) {
374 feeds = []
375 for (const feed of starterPack.record.feeds) {
376 // note: types are wrong? claims to be `FeedItem`, but we actually
377 // get un$typed `GeneratorView` objects here -sfn
378 if (bsky.validate(feed, AppBskyFeedDefs.validateGeneratorView)) {
379 feeds.push(feed)
380 }
381 }
382 }
383
384 const listView: AppBskyGraphDefs.ListViewBasic = {
385 uri: starterPack.record.list,
386 // This will be populated once the data from server is fetched
387 cid: '',
388 name: starterPack.record.name,
389 purpose: 'app.bsky.graph.defs#referencelist',
390 }
391 starterPackView = {
392 ...starterPack,
393 $type: 'app.bsky.graph.defs#starterPackView',
394 list: listView,
395 feeds,
396 }
397 }
398
399 if (starterPackView) {
400 queryClient.setQueryData(RQKEY({uri: starterPack.uri}), starterPackView)
401 }
402}