Bluesky app fork with some witchin' additions 馃挮
at feat/tealfm 318 lines 8.7 kB view raw
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}