import { AppBskyActorDefs, AppBskyFeedDefs, AppBskyGraphDefs, AppBskyGraphGetStarterPack, AppBskyGraphStarterpack, AppBskyRichtextFacet, AtUri, BskyAgent, RichText, } from '@atproto/api' import {StarterPackView} from '@atproto/api/dist/client/types/app/bsky/graph/defs' import { QueryClient, useMutation, useQuery, useQueryClient, } from '@tanstack/react-query' import {until} from 'lib/async/until' import {createStarterPackList} from 'lib/generate-starterpack' import { createStarterPackUri, httpStarterPackUriToAtUri, parseStarterPackUri, } from 'lib/strings/starter-pack' import {invalidateActorStarterPacksQuery} from 'state/queries/actor-starter-packs' import {invalidateListMembersQuery} from 'state/queries/list-members' import {useAgent} from 'state/session' const RQKEY_ROOT = 'starter-pack' const RQKEY = (did?: string, rkey?: string) => { if (did?.startsWith('https://') || did?.startsWith('at://')) { const parsed = parseStarterPackUri(did) return [RQKEY_ROOT, parsed?.name, parsed?.rkey] } else { return [RQKEY_ROOT, did, rkey] } } export function useStarterPackQuery({ uri, did, rkey, }: { uri?: string did?: string rkey?: string }) { const agent = useAgent() return useQuery({ queryKey: RQKEY(did, rkey), queryFn: async () => { if (!uri) { uri = `at://${did}/app.bsky.graph.starterpack/${rkey}` } else if (uri && !uri.startsWith('at://')) { uri = httpStarterPackUriToAtUri(uri) as string } const res = await agent.app.bsky.graph.getStarterPack({ starterPack: uri, }) return res.data.starterPack }, enabled: Boolean(uri) || Boolean(did && rkey), }) } export async function invalidateStarterPack({ queryClient, did, rkey, }: { queryClient: QueryClient did: string rkey: string }) { await queryClient.invalidateQueries({queryKey: RQKEY(did, rkey)}) } interface UseCreateStarterPackMutationParams { name: string description?: string profiles: AppBskyActorDefs.ProfileViewBasic[] feeds?: AppBskyFeedDefs.GeneratorView[] } export function useCreateStarterPackMutation({ onSuccess, onError, }: { onSuccess: (data: {uri: string; cid: string}) => void onError: (e: Error) => void }) { const queryClient = useQueryClient() const agent = useAgent() return useMutation< {uri: string; cid: string}, Error, UseCreateStarterPackMutationParams >({ mutationFn: async ({name, description, feeds, profiles}) => { let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined if (description) { const rt = new RichText({text: description}) await rt.detectFacets(agent) descriptionFacets = rt.facets } let listRes listRes = await createStarterPackList({ name, description, profiles, descriptionFacets, agent, }) return await agent.app.bsky.graph.starterpack.create( { repo: agent.session?.did, }, { name, description, descriptionFacets, list: listRes?.uri, feeds: feeds?.map(f => ({uri: f.uri})), createdAt: new Date().toISOString(), }, ) }, onSuccess: async data => { await whenAppViewReady(agent, data.uri, v => { return typeof v?.data.starterPack.uri === 'string' }) await invalidateActorStarterPacksQuery({ queryClient, did: agent.session!.did, }) onSuccess(data) }, onError: async error => { onError(error) }, }) } export function useEditStarterPackMutation({ onSuccess, onError, }: { onSuccess: () => void onError: (error: Error) => void }) { const queryClient = useQueryClient() const agent = useAgent() return useMutation< void, Error, UseCreateStarterPackMutationParams & { currentStarterPack: AppBskyGraphDefs.StarterPackView currentListItems: AppBskyGraphDefs.ListItemView[] } >({ mutationFn: async ({ name, description, feeds, profiles, currentStarterPack, currentListItems, }) => { let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined if (description) { const rt = new RichText({text: description}) await rt.detectFacets(agent) descriptionFacets = rt.facets } if (!AppBskyGraphStarterpack.isRecord(currentStarterPack.record)) { throw new Error('Invalid starter pack') } const removedItems = currentListItems.filter( i => i.subject.did !== agent.session?.did && !profiles.find(p => p.did === i.subject.did && p.did), ) if (removedItems.length !== 0) { await agent.com.atproto.repo.applyWrites({ repo: agent.session!.did, writes: removedItems.map(i => ({ $type: 'com.atproto.repo.applyWrites#delete', collection: 'app.bsky.graph.listitem', rkey: new AtUri(i.uri).rkey, })), }) } const addedProfiles = profiles.filter( p => !currentListItems.find(i => i.subject.did === p.did), ) if (addedProfiles.length > 0) { await agent.com.atproto.repo.applyWrites({ repo: agent.session!.did, writes: addedProfiles.map(p => ({ $type: 'com.atproto.repo.applyWrites#create', collection: 'app.bsky.graph.listitem', value: { $type: 'app.bsky.graph.listitem', subject: p.did, list: currentStarterPack.list?.uri, createdAt: new Date().toISOString(), }, })), }) } const rkey = parseStarterPackUri(currentStarterPack.uri)!.rkey await agent.com.atproto.repo.putRecord({ repo: agent.session!.did, collection: 'app.bsky.graph.starterpack', rkey, record: { name, description, descriptionFacets, list: currentStarterPack.list?.uri, feeds, createdAt: currentStarterPack.record.createdAt, updatedAt: new Date().toISOString(), }, }) }, onSuccess: async (_, {currentStarterPack}) => { const parsed = parseStarterPackUri(currentStarterPack.uri) await whenAppViewReady(agent, currentStarterPack.uri, v => { return currentStarterPack.cid !== v?.data.starterPack.cid }) await invalidateActorStarterPacksQuery({ queryClient, did: agent.session!.did, }) if (currentStarterPack.list) { await invalidateListMembersQuery({ queryClient, uri: currentStarterPack.list.uri, }) } await invalidateStarterPack({ queryClient, did: agent.session!.did, rkey: parsed!.rkey, }) onSuccess() }, onError: error => { onError(error) }, }) } export function useDeleteStarterPackMutation({ onSuccess, onError, }: { onSuccess: () => void onError: (error: Error) => void }) { const agent = useAgent() const queryClient = useQueryClient() return useMutation({ mutationFn: async ({listUri, rkey}: {listUri?: string; rkey: string}) => { if (!agent.session) { throw new Error(`Requires logged in user`) } if (listUri) { await agent.app.bsky.graph.list.delete({ repo: agent.session.did, rkey: new AtUri(listUri).rkey, }) } await agent.app.bsky.graph.starterpack.delete({ repo: agent.session.did, rkey, }) }, onSuccess: async (_, {listUri, rkey}) => { const uri = createStarterPackUri({ did: agent.session!.did, rkey, }) if (uri) { await whenAppViewReady(agent, uri, v => { return Boolean(v?.data?.starterPack) === false }) } if (listUri) { await invalidateListMembersQuery({queryClient, uri: listUri}) } await invalidateActorStarterPacksQuery({ queryClient, did: agent.session!.did, }) await invalidateStarterPack({ queryClient, did: agent.session!.did, rkey, }) onSuccess() }, onError: error => { onError(error) }, }) } async function whenAppViewReady( agent: BskyAgent, uri: string, fn: (res?: AppBskyGraphGetStarterPack.Response) => boolean, ) { await until( 5, // 5 tries 1e3, // 1s delay between tries fn, () => agent.app.bsky.graph.getStarterPack({starterPack: uri}), ) }