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

Configure Feed

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

at main 403 lines 11 kB view raw
1import { 2 AppBskyFeedDefs, 3 AppBskyGraphDefs, 4 type AppBskyGraphGetStarterPack, 5 AppBskyGraphStarterpack, 6 type AppBskyRichtextFacet, 7 AtUri, 8 type BskyAgent, 9 RichText, 10} from '@atproto/api' 11import { 12 type QueryClient, 13 useMutation, 14 useQuery, 15 useQueryClient, 16} from '@tanstack/react-query' 17import chunk from 'lodash.chunk' 18 19import {until} from '#/lib/async/until' 20import {createStarterPackList} from '#/lib/generate-starterpack' 21import { 22 createStarterPackUri, 23 httpStarterPackUriToAtUri, 24 parseStarterPackUri, 25} from '#/lib/strings/starter-pack' 26import {invalidateActorStarterPacksQuery} from '#/state/queries/actor-starter-packs' 27import {STALE} from '#/state/queries/index' 28import {invalidateListMembersQuery} from '#/state/queries/list-members' 29import {useAgent} from '#/state/session' 30import {pdsAgent} from '#/state/session/agent' 31import * as bsky from '#/types/bsky' 32 33const RQKEY_ROOT = 'starter-pack' 34const RQKEY = ({ 35 uri, 36 did, 37 rkey, 38}: { 39 uri?: string 40 did?: string 41 rkey?: string 42}) => { 43 if (uri?.startsWith('https://') || uri?.startsWith('at://')) { 44 const parsed = parseStarterPackUri(uri) 45 return [RQKEY_ROOT, parsed?.name, parsed?.rkey] 46 } else { 47 return [RQKEY_ROOT, did, rkey] 48 } 49} 50 51export function useStarterPackQuery({ 52 uri, 53 did, 54 rkey, 55}: { 56 uri?: string 57 did?: string 58 rkey?: string 59}) { 60 const agent = useAgent() 61 62 return useQuery<AppBskyGraphDefs.StarterPackView>({ 63 queryKey: RQKEY(uri ? {uri} : {did, rkey}), 64 queryFn: async () => { 65 if (!uri) { 66 uri = `at://${did}/app.bsky.graph.starterpack/${rkey}` 67 } else if (uri && !uri.startsWith('at://')) { 68 uri = httpStarterPackUriToAtUri(uri) as string 69 } 70 71 const res = await agent.app.bsky.graph.getStarterPack({ 72 starterPack: uri, 73 }) 74 return res.data.starterPack 75 }, 76 enabled: Boolean(uri) || Boolean(did && rkey), 77 staleTime: STALE.MINUTES.FIVE, 78 }) 79} 80 81export async function invalidateStarterPack({ 82 queryClient, 83 did, 84 rkey, 85}: { 86 queryClient: QueryClient 87 did: string 88 rkey: string 89}) { 90 await queryClient.invalidateQueries({queryKey: RQKEY({did, rkey})}) 91} 92 93interface UseCreateStarterPackMutationParams { 94 name: string 95 description?: string 96 profiles: bsky.profile.AnyProfileView[] 97 feeds?: AppBskyFeedDefs.GeneratorView[] 98} 99 100export function useCreateStarterPackMutation({ 101 onSuccess, 102 onError, 103}: { 104 onSuccess: (data: {uri: string; cid: string}) => void 105 onError: (e: Error) => void 106}) { 107 const queryClient = useQueryClient() 108 const agent = useAgent() 109 110 return useMutation< 111 {uri: string; cid: string}, 112 Error, 113 UseCreateStarterPackMutationParams 114 >({ 115 mutationFn: async ({name, description, feeds, profiles}) => { 116 let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined 117 if (description) { 118 const rt = new RichText({text: description}) 119 await rt.detectFacets(agent) 120 descriptionFacets = rt.facets 121 } 122 123 let listRes 124 listRes = await createStarterPackList({ 125 name, 126 description, 127 profiles, 128 descriptionFacets, 129 agent, 130 }) 131 132 return await agent.app.bsky.graph.starterpack.create( 133 { 134 repo: agent.assertDid, 135 }, 136 { 137 name, 138 description, 139 descriptionFacets, 140 list: listRes?.uri, 141 feeds: feeds?.map(f => ({uri: f.uri})), 142 createdAt: new Date().toISOString(), 143 }, 144 ) 145 }, 146 onSuccess: async data => { 147 await whenAppViewReady(agent, data.uri, v => { 148 return typeof v?.data.starterPack.uri === 'string' 149 }) 150 await invalidateActorStarterPacksQuery({ 151 queryClient, 152 did: agent.session!.did, 153 }) 154 onSuccess(data) 155 }, 156 onError: async error => { 157 onError(error) 158 }, 159 }) 160} 161 162export function useEditStarterPackMutation({ 163 onSuccess, 164 onError, 165}: { 166 onSuccess: () => void 167 onError: (error: Error) => void 168}) { 169 const queryClient = useQueryClient() 170 const agent = useAgent() 171 172 return useMutation< 173 void, 174 Error, 175 UseCreateStarterPackMutationParams & { 176 currentStarterPack: AppBskyGraphDefs.StarterPackView 177 currentListItems: AppBskyGraphDefs.ListItemView[] 178 } 179 >({ 180 mutationFn: async ({ 181 name, 182 description, 183 feeds, 184 profiles, 185 currentStarterPack, 186 currentListItems, 187 }) => { 188 let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined 189 if (description) { 190 const rt = new RichText({text: description}) 191 await rt.detectFacets(agent) 192 descriptionFacets = rt.facets 193 } 194 195 if (!AppBskyGraphStarterpack.isRecord(currentStarterPack.record)) { 196 throw new Error('Invalid starter pack') 197 } 198 199 const removedItems = currentListItems.filter( 200 i => 201 i.subject.did !== agent.session?.did && 202 !profiles.find(p => p.did === i.subject.did && p.did), 203 ) 204 if (removedItems.length !== 0) { 205 const chunks = chunk(removedItems, 50) 206 for (const chunk of chunks) { 207 await pdsAgent(agent).com.atproto.repo.applyWrites({ 208 repo: agent.session!.did, 209 writes: chunk.map(i => ({ 210 $type: 'com.atproto.repo.applyWrites#delete', 211 collection: 'app.bsky.graph.listitem', 212 rkey: new AtUri(i.uri).rkey, 213 })), 214 }) 215 } 216 } 217 218 const addedProfiles = profiles.filter( 219 p => !currentListItems.find(i => i.subject.did === p.did), 220 ) 221 if (addedProfiles.length > 0) { 222 const chunks = chunk(addedProfiles, 50) 223 for (const chunk of chunks) { 224 await pdsAgent(agent).com.atproto.repo.applyWrites({ 225 repo: agent.session!.did, 226 writes: chunk.map(p => ({ 227 $type: 'com.atproto.repo.applyWrites#create', 228 collection: 'app.bsky.graph.listitem', 229 value: { 230 $type: 'app.bsky.graph.listitem', 231 subject: p.did, 232 list: currentStarterPack.list?.uri, 233 createdAt: new Date().toISOString(), 234 }, 235 })), 236 }) 237 } 238 } 239 240 const rkey = parseStarterPackUri(currentStarterPack.uri)!.rkey 241 await pdsAgent(agent).com.atproto.repo.putRecord({ 242 repo: agent.session!.did, 243 collection: 'app.bsky.graph.starterpack', 244 rkey, 245 record: { 246 name, 247 description, 248 descriptionFacets, 249 list: currentStarterPack.list?.uri, 250 feeds, 251 createdAt: currentStarterPack.record.createdAt, 252 updatedAt: new Date().toISOString(), 253 }, 254 }) 255 }, 256 onSuccess: async (_, {currentStarterPack}) => { 257 const parsed = parseStarterPackUri(currentStarterPack.uri) 258 await whenAppViewReady(agent, currentStarterPack.uri, v => { 259 return currentStarterPack.cid !== v?.data.starterPack.cid 260 }) 261 await invalidateActorStarterPacksQuery({ 262 queryClient, 263 did: agent.session!.did, 264 }) 265 if (currentStarterPack.list) { 266 await invalidateListMembersQuery({ 267 queryClient, 268 uri: currentStarterPack.list.uri, 269 }) 270 } 271 await invalidateStarterPack({ 272 queryClient, 273 did: agent.session!.did, 274 rkey: parsed!.rkey, 275 }) 276 onSuccess() 277 }, 278 onError: error => { 279 onError(error) 280 }, 281 }) 282} 283 284export function useDeleteStarterPackMutation({ 285 onSuccess, 286 onError, 287}: { 288 onSuccess: () => void 289 onError: (error: Error) => void 290}) { 291 const agent = useAgent() 292 const queryClient = useQueryClient() 293 294 return useMutation({ 295 mutationFn: async ({listUri, rkey}: {listUri?: string; rkey: string}) => { 296 if (!agent.session) { 297 throw new Error(`Requires signed in user`) 298 } 299 300 if (listUri) { 301 await agent.app.bsky.graph.list.delete({ 302 repo: agent.session.did, 303 rkey: new AtUri(listUri).rkey, 304 }) 305 } 306 await agent.app.bsky.graph.starterpack.delete({ 307 repo: agent.session.did, 308 rkey, 309 }) 310 }, 311 onSuccess: async (_, {listUri, rkey}) => { 312 const uri = createStarterPackUri({ 313 did: agent.session!.did, 314 rkey, 315 }) 316 317 if (uri) { 318 await whenAppViewReady(agent, uri, v => { 319 return Boolean(v?.data?.starterPack) === false 320 }) 321 } 322 323 if (listUri) { 324 await invalidateListMembersQuery({queryClient, uri: listUri}) 325 } 326 await invalidateActorStarterPacksQuery({ 327 queryClient, 328 did: agent.session!.did, 329 }) 330 await invalidateStarterPack({ 331 queryClient, 332 did: agent.session!.did, 333 rkey, 334 }) 335 onSuccess() 336 }, 337 onError: error => { 338 onError(error) 339 }, 340 }) 341} 342 343async function whenAppViewReady( 344 agent: BskyAgent, 345 uri: string, 346 fn: (res?: AppBskyGraphGetStarterPack.Response) => boolean, 347) { 348 await until( 349 5, // 5 tries 350 1e3, // 1s delay between tries 351 fn, 352 () => agent.app.bsky.graph.getStarterPack({starterPack: uri}), 353 ) 354} 355 356export function precacheStarterPack( 357 queryClient: QueryClient, 358 starterPack: 359 | AppBskyGraphDefs.StarterPackViewBasic 360 | AppBskyGraphDefs.StarterPackView, 361) { 362 if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) { 363 return 364 } 365 366 let starterPackView: AppBskyGraphDefs.StarterPackView | undefined 367 if (AppBskyGraphDefs.isStarterPackView(starterPack)) { 368 starterPackView = starterPack 369 } else if ( 370 AppBskyGraphDefs.isStarterPackViewBasic(starterPack) && 371 bsky.validate(starterPack.record, AppBskyGraphStarterpack.validateRecord) 372 ) { 373 let feeds: AppBskyFeedDefs.GeneratorView[] | undefined 374 if (starterPack.record.feeds) { 375 feeds = [] 376 for (const feed of starterPack.record.feeds) { 377 // note: types are wrong? claims to be `FeedItem`, but we actually 378 // get un$typed `GeneratorView` objects here -sfn 379 if (bsky.validate(feed, AppBskyFeedDefs.validateGeneratorView)) { 380 feeds.push(feed) 381 } 382 } 383 } 384 385 const listView: AppBskyGraphDefs.ListViewBasic = { 386 uri: starterPack.record.list, 387 // This will be populated once the data from server is fetched 388 cid: '', 389 name: starterPack.record.name, 390 purpose: 'app.bsky.graph.defs#referencelist', 391 } 392 starterPackView = { 393 ...starterPack, 394 $type: 'app.bsky.graph.defs#starterPackView', 395 list: listView, 396 feeds, 397 } 398 } 399 400 if (starterPackView) { 401 queryClient.setQueryData(RQKEY({uri: starterPack.uri}), starterPackView) 402 } 403}