Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at theme-changes 319 lines 8.8 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 {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}