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