diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx index c2c2fa7839..f627535e39 100644 --- a/src/components/StarterPack/ProfileStarterPacks.tsx +++ b/src/components/StarterPack/ProfileStarterPacks.tsx @@ -6,22 +6,18 @@ import { View, ViewStyle, } from 'react-native' -import { - AppBskyGraphDefs, - AppBskyGraphGetActorStarterPacks, - AtUri, -} from '@atproto/api' +import {AppBskyGraphDefs, AppBskyGraphGetActorStarterPacks} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query' import {logger} from '#/logger' -import {generateStarterpack} from 'lib/generate-starterpack' +import {useGenerateStarterPackMutation} from 'lib/generate-starterpack' import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {NavigationProp} from 'lib/routes/types' -import {useAgent} from 'state/session' +import {parseStarterPackUri} from 'lib/strings/starter-pack' import {List, ListRef} from 'view/com/util/List' import {Text} from 'view/com/util/text/Text' import {atoms as a, useTheme} from '#/alf' @@ -185,35 +181,33 @@ function Empty() { const {_} = useLingui() const t = useTheme() const navigation = useNavigation() - const agent = useAgent() const confirmDialogControl = useDialogControl() const followersDialogControl = useDialogControl() const errorDialogControl = useDialogControl() const [isGenerating, setIsGenerating] = React.useState(false) - const generate = async () => { - setIsGenerating(true) - - const res = await generateStarterpack({agent}) - - if (res === 'NOT_ENOUGH_FOLLOWERS') { - followersDialogControl.open() - setIsGenerating(false) - } else if (res === 'ERROR') { - errorDialogControl.open() - setIsGenerating(false) - } else { - const atUri = new AtUri(res) - setTimeout(() => { + const {mutate: generateStarterPack} = useGenerateStarterPackMutation({ + onSuccess: ({uri}) => { + const parsed = parseStarterPackUri(uri) + if (parsed) { navigation.push('StarterPack', { - name: atUri.hostname, - rkey: atUri.rkey, + name: parsed.name, + rkey: parsed.rkey, }) - setIsGenerating(false) - }, 1000) - } - } + } + setIsGenerating(false) + }, + onError: e => { + logger.error('Failed to generate starter pack', {safeMessage: e}) + setIsGenerating(false) + if (e.name === 'NOT_ENOUGH_FOLLOWERS') { + followersDialogControl.open() + } else { + errorDialogControl.open() + } + }, + }) return ( diff --git a/src/lib/generate-starterpack.ts b/src/lib/generate-starterpack.ts index 3d415b690e..ed8b75e7c9 100644 --- a/src/lib/generate-starterpack.ts +++ b/src/lib/generate-starterpack.ts @@ -1,9 +1,19 @@ -import {AppBskyActorDefs, BskyAgent, Facet} from '@atproto/api' +import { + AppBskyActorDefs, + AppBskyGraphGetStarterPack, + BskyAgent, + Facet, +} from '@atproto/api' import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useMutation} from '@tanstack/react-query' import {logger} from '#/logger' +import {until} from 'lib/async/until' +import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {enforceLen} from 'lib/strings/helpers' +import {useAgent} from 'state/session' export const createStarterPackList = async ({ name, @@ -47,6 +57,88 @@ export const createStarterPackList = async ({ return list } +export function useGenerateStarterPackMutation({ + onSuccess, + onError, +}: { + onSuccess: ({uri, cid}: {uri: string; cid: string}) => void + onError: (e: Error) => void +}) { + const {_} = useLingui() + const agent = useAgent() + const starterPackString = _(msg`Starter Pack`) + + return useMutation<{uri: string; cid: string}, Error, {}>({ + mutationFn: async () => { + let profile: AppBskyActorDefs.ProfileViewBasic | undefined + let profiles: AppBskyActorDefs.ProfileViewBasic[] | undefined + + await Promise.all([ + (async () => { + profile = ( + await agent.app.bsky.actor.getProfile({ + actor: agent.session!.did, + }) + ).data + })(), + (async () => { + profiles = ( + await agent.app.bsky.actor.searchActors({ + q: encodeURIComponent('*'), + limit: 49, + }) + ).data.actors.filter(p => p.viewer?.following) + })(), + ]) + + if (!profile || !profiles) { + throw new Error('ERROR_DATA') + } + + // We include ourselves when we make the list + if (profiles.length < 7) { + throw new Error('NOT_ENOUGH_FOLLOWERS') + } + + const displayName = enforceLen( + profile.displayName + ? sanitizeDisplayName(profile.displayName) + : `@${sanitizeHandle(profile.handle)}`, + 25, + true, + ) + const starterPackName = `${displayName}'s ${starterPackString}` + + const list = await createStarterPackList({ + name: starterPackName, + profiles, + agent, + }) + + return await agent.app.bsky.graph.starterpack.create( + { + repo: agent.session!.did, + validate: false, + }, + { + name: starterPackName, + list: list.uri, + createdAt: new Date().toISOString(), + }, + ) + }, + onSuccess: async data => { + await whenAppViewReady(agent, data.uri, v => { + return typeof v?.data.starterPack.uri === 'string' + }) + onSuccess(data) + }, + onError: error => { + onError(error) + }, + }) +} + export async function generateStarterpack({ agent, }: { @@ -126,3 +218,16 @@ function createListItem({did, listUri}: {did: string; listUri: string}) { }, } } + +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}), + ) +} diff --git a/src/state/queries/starter-packs.ts b/src/state/queries/starter-packs.ts index bc7bde8894..fe03af99e5 100644 --- a/src/state/queries/starter-packs.ts +++ b/src/state/queries/starter-packs.ts @@ -240,7 +240,7 @@ export function useEditStarterPackMutation({ }) onSuccess() }, - onError: async error => { + onError: error => { onError(error) }, }) @@ -285,7 +285,7 @@ export function useDeleteStarterPackMutation({ }) onSuccess() }, - onError: async error => { + onError: error => { onError(error) }, })