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

Configure Feed

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

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