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