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