forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {
2 type $Typed,
3 type AppBskyGraphDefs,
4 type AppBskyGraphGetList,
5 type AppBskyGraphList,
6 AtUri,
7 type BskyAgent,
8 type ComAtprotoRepoApplyWrites,
9 type Facet,
10 type Un$Typed,
11} from '@atproto/api'
12import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
13import chunk from 'lodash.chunk'
14
15import {uploadBlob} from '#/lib/api'
16import {until} from '#/lib/async/until'
17import {type ImageMeta} from '#/state/gallery'
18import {STALE} from '#/state/queries'
19import {useAgent, useSession} from '#/state/session'
20import {pdsAgent} from '#/state/session/agent'
21import {invalidate as invalidateMyLists} from './my-lists'
22import {RQKEY as PROFILE_LISTS_RQKEY} from './profile-lists'
23
24export const RQKEY_ROOT = 'list'
25export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
26
27export function useListQuery(uri?: string) {
28 const agent = useAgent()
29 return useQuery<AppBskyGraphDefs.ListView, Error>({
30 staleTime: STALE.MINUTES.ONE,
31 queryKey: RQKEY(uri || ''),
32 async queryFn() {
33 if (!uri) {
34 throw new Error('URI not provided')
35 }
36 const res = await agent.app.bsky.graph.getList({
37 list: uri,
38 limit: 1,
39 })
40 return res.data.list
41 },
42 enabled: !!uri,
43 })
44}
45
46export interface ListCreateMutateParams {
47 purpose: string
48 name: string
49 description: string
50 descriptionFacets: Facet[] | undefined
51 avatar: ImageMeta | null | undefined
52}
53export function useListCreateMutation() {
54 const {currentAccount} = useSession()
55 const queryClient = useQueryClient()
56 const agent = useAgent()
57 return useMutation<{uri: string; cid: string}, Error, ListCreateMutateParams>(
58 {
59 async mutationFn({
60 purpose,
61 name,
62 description,
63 descriptionFacets,
64 avatar,
65 }) {
66 if (!currentAccount) {
67 throw new Error('Not signed in')
68 }
69 if (
70 purpose !== 'app.bsky.graph.defs#curatelist' &&
71 purpose !== 'app.bsky.graph.defs#modlist'
72 ) {
73 throw new Error('Invalid list purpose: must be curatelist or modlist')
74 }
75 const record: Un$Typed<AppBskyGraphList.Record> = {
76 purpose,
77 name,
78 description,
79 descriptionFacets,
80 avatar: undefined,
81 createdAt: new Date().toISOString(),
82 }
83 if (avatar) {
84 const blobRes = await uploadBlob(agent, avatar.path, avatar.mime)
85 record.avatar = blobRes.data.blob
86 }
87 const res = await agent.app.bsky.graph.list.create(
88 {
89 repo: currentAccount.did,
90 },
91 record,
92 )
93
94 // wait for the appview to update
95 await whenAppViewReady(
96 agent,
97 res.uri,
98 (v: AppBskyGraphGetList.Response) => {
99 return typeof v?.data?.list.uri === 'string'
100 },
101 )
102 return res
103 },
104 onSuccess() {
105 invalidateMyLists(queryClient)
106 queryClient.invalidateQueries({
107 queryKey: PROFILE_LISTS_RQKEY(currentAccount!.did),
108 })
109 },
110 },
111 )
112}
113
114export interface ListMetadataMutateParams {
115 uri: string
116 name: string
117 description: string
118 descriptionFacets: Facet[] | undefined
119 avatar: ImageMeta | null | undefined
120}
121export function useListMetadataMutation() {
122 const {currentAccount} = useSession()
123 const agent = useAgent()
124 const queryClient = useQueryClient()
125 return useMutation<
126 {uri: string; cid: string},
127 Error,
128 ListMetadataMutateParams
129 >({
130 async mutationFn({uri, name, description, descriptionFacets, avatar}) {
131 const {hostname, rkey} = new AtUri(uri)
132 if (!currentAccount) {
133 throw new Error('Not signed in')
134 }
135 if (currentAccount.did !== hostname) {
136 throw new Error('You do not own this list')
137 }
138
139 // get the current record
140 const {value: record} = await agent.app.bsky.graph.list.get({
141 repo: currentAccount.did,
142 rkey,
143 })
144
145 // update the fields
146 record.name = name
147 record.description = description
148 record.descriptionFacets = descriptionFacets
149 if (avatar) {
150 const blobRes = await uploadBlob(agent, avatar.path, avatar.mime)
151 record.avatar = blobRes.data.blob
152 } else if (avatar === null) {
153 record.avatar = undefined
154 }
155 const res = (
156 await pdsAgent(agent).com.atproto.repo.putRecord({
157 repo: currentAccount.did,
158 collection: 'app.bsky.graph.list',
159 rkey,
160 record,
161 })
162 ).data
163
164 // wait for the appview to update
165 await whenAppViewReady(
166 agent,
167 res.uri,
168 (v: AppBskyGraphGetList.Response) => {
169 const list = v.data.list
170 return (
171 list.name === record.name && list.description === record.description
172 )
173 },
174 )
175 return res
176 },
177 onSuccess(data, variables) {
178 invalidateMyLists(queryClient)
179 queryClient.invalidateQueries({
180 queryKey: PROFILE_LISTS_RQKEY(currentAccount!.did),
181 })
182 queryClient.invalidateQueries({
183 queryKey: RQKEY(variables.uri),
184 })
185 },
186 })
187}
188
189export function useListDeleteMutation() {
190 const {currentAccount} = useSession()
191 const agent = useAgent()
192 const queryClient = useQueryClient()
193 return useMutation<void, Error, {uri: string}>({
194 mutationFn: async ({uri}) => {
195 if (!currentAccount) {
196 return
197 }
198 // fetch all the listitem records that belong to this list
199 let cursor
200 let listitemRecordUris: string[] = []
201 for (let i = 0; i < 100; i++) {
202 const res = await agent.app.bsky.graph.listitem.list({
203 repo: currentAccount.did,
204 cursor,
205 limit: 100,
206 })
207 listitemRecordUris = listitemRecordUris.concat(
208 res.records
209 .filter(record => record.value.list === uri)
210 .map(record => record.uri),
211 )
212 cursor = res.cursor
213 if (!cursor) {
214 break
215 }
216 }
217
218 // batch delete the list and listitem records
219 const createDel = (
220 uri: string,
221 ): $Typed<ComAtprotoRepoApplyWrites.Delete> => {
222 const urip = new AtUri(uri)
223 return {
224 $type: 'com.atproto.repo.applyWrites#delete',
225 collection: urip.collection,
226 rkey: urip.rkey,
227 }
228 }
229 const writes = listitemRecordUris
230 .map(uri => createDel(uri))
231 .concat([createDel(uri)])
232
233 // apply in chunks
234 for (const writesChunk of chunk(writes, 10)) {
235 await pdsAgent(agent).com.atproto.repo.applyWrites({
236 repo: currentAccount.did,
237 writes: writesChunk,
238 })
239 }
240
241 // wait for the appview to update
242 await whenAppViewReady(agent, uri, (v: AppBskyGraphGetList.Response) => {
243 return !v?.success
244 })
245 },
246 onSuccess() {
247 invalidateMyLists(queryClient)
248 queryClient.invalidateQueries({
249 queryKey: PROFILE_LISTS_RQKEY(currentAccount!.did),
250 })
251 // TODO!! /* dont await */ this.rootStore.preferences.removeSavedFeed(this.uri)
252 },
253 })
254}
255
256export function useListMuteMutation() {
257 const queryClient = useQueryClient()
258 const agent = useAgent()
259 return useMutation<void, Error, {uri: string; mute: boolean}>({
260 mutationFn: async ({uri, mute}) => {
261 if (mute) {
262 await agent.muteModList(uri)
263 } else {
264 await agent.unmuteModList(uri)
265 }
266
267 await whenAppViewReady(agent, uri, (v: AppBskyGraphGetList.Response) => {
268 return Boolean(v?.data.list.viewer?.muted) === mute
269 })
270 },
271 onSuccess(data, variables) {
272 queryClient.invalidateQueries({
273 queryKey: RQKEY(variables.uri),
274 })
275 },
276 })
277}
278
279export function useListBlockMutation() {
280 const queryClient = useQueryClient()
281 const agent = useAgent()
282 return useMutation<void, Error, {uri: string; block: boolean}>({
283 mutationFn: async ({uri, block}) => {
284 if (block) {
285 await agent.blockModList(uri)
286 } else {
287 await agent.unblockModList(uri)
288 }
289
290 await whenAppViewReady(agent, uri, (v: AppBskyGraphGetList.Response) => {
291 return block
292 ? typeof v?.data.list.viewer?.blocked === 'string'
293 : !v?.data.list.viewer?.blocked
294 })
295 },
296 onSuccess(data, variables) {
297 queryClient.invalidateQueries({
298 queryKey: RQKEY(variables.uri),
299 })
300 },
301 })
302}
303
304async function whenAppViewReady(
305 agent: BskyAgent,
306 uri: string,
307 fn: (res: AppBskyGraphGetList.Response) => boolean,
308) {
309 await until(
310 5, // 5 tries
311 1e3, // 1s delay between tries
312 fn,
313 () =>
314 agent.app.bsky.graph.getList({
315 list: uri,
316 limit: 1,
317 }),
318 )
319}