From d106ba45d3ecc0278faef46b3cb18ce06a43250a Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 12 Jun 2024 15:31:46 -0700 Subject: [PATCH] automatically generate a starter pack --- .../StarterPack/ProfileStarterPacks.tsx | 126 ++++++++++++++++-- src/lib/generate-starterpack.ts | 102 ++++++++++++++ src/screens/StarterPack/Wizard/index.tsx | 51 +++---- 3 files changed, 231 insertions(+), 48 deletions(-) create mode 100644 src/lib/generate-starterpack.ts diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx index ccad672f9d..f9dcd9e5bf 100644 --- a/src/components/StarterPack/ProfileStarterPacks.tsx +++ b/src/components/StarterPack/ProfileStarterPacks.tsx @@ -6,7 +6,11 @@ import { View, ViewStyle, } from 'react-native' -import {AppBskyGraphDefs, AppBskyGraphGetActorStarterPacks} from '@atproto/api' +import { + AppBskyGraphDefs, + AppBskyGraphGetActorStarterPacks, + AtUri, +} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' @@ -14,12 +18,17 @@ import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query' import {logger} from '#/logger' import {isNative, isWeb} from '#/platform/detection' +import {generateStarterpack} from 'lib/generate-starterpack' import {NavigationProp} from 'lib/routes/types' +import {useAgent} from 'state/session' import {List, ListRef} from 'view/com/util/List' import {Text} from 'view/com/util/text/Text' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' import {LinearGradientBackground} from '#/components/LinearGradientBackground' +import {Loader} from '#/components/Loader' +import * as Prompt from '#/components/Prompt' import {StarterPackCard} from '#/components/StarterPack/StarterPackCard' interface SectionRef { @@ -134,6 +143,35 @@ function EmptyComponent() { 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(() => { + navigation.push('StarterPack', { + name: atUri.hostname, + rkey: atUri.rkey, + }) + setIsGenerating(false) + }, 1000) + } + } return ( - + + + + + + + + Generate a starter pack? + + + + You can customize your starter pack with feeds and labelers if you + create one on your own. + + + + { + navigation.navigate('StarterPackWizard', {}) + }} + /> + + + + {}} + showCancel={false} + /> + ) } diff --git a/src/lib/generate-starterpack.ts b/src/lib/generate-starterpack.ts new file mode 100644 index 0000000000..e8787dc54f --- /dev/null +++ b/src/lib/generate-starterpack.ts @@ -0,0 +1,102 @@ +import {AppBskyActorDefs, BskyAgent, Facet} from '@atproto/api' +import {msg} from '@lingui/macro' + +import {logger} from '#/logger' +import {sanitizeHandle} from 'lib/strings/handles' + +export const createStarterPackList = async ({ + name, + description, + descriptionFacets, + profiles, + agent, +}: { + name: string + description?: string + descriptionFacets?: Facet[] + profiles: AppBskyActorDefs.ProfileViewBasic[] + agent: BskyAgent +}): Promise<{uri: string; cid: string} | undefined> => { + if (profiles.length === 0) return + + const list = await agent.app.bsky.graph.list.create( + {repo: agent.session!.did}, + { + name, + description, + descriptionFacets, + avatar: undefined, + createdAt: new Date().toISOString(), + purpose: 'app.bsky.graph.defs#referencelist', + }, + ) + if (!list) throw new Error('List creation failed') + await agent.com.atproto.repo.applyWrites({ + repo: agent.session!.did, + writes: profiles.map(p => ({ + $type: 'com.atproto.repo.applyWrites#create', + collection: 'app.bsky.graph.listitem', + value: { + $type: 'app.bsky.graph.listitem', + subject: p.did, + list: list?.uri, + createdAt: new Date().toISOString(), + }, + })), + }) + + return list +} + +export async function generateStarterpack({ + agent, +}: { + agent: BskyAgent +}): Promise { + try { + const profileRes = await agent.app.bsky.actor.getProfile({ + actor: agent.session!.did, + }) + const profile = profileRes.data + + const defaultName = `${ + profile.displayName || `@${sanitizeHandle(profile.handle)}` + }${msg`'s Starter Pack`.message!}` + + const profilesRes = await agent.app.bsky.actor.searchActors({ + q: encodeURIComponent('*'), + limit: 49, + }) + const profiles = [ + profile, + ...profilesRes.data.actors.filter(p => p.viewer?.following), + ] + + if (profiles.length < 8) { + return 'NOT_ENOUGH_FOLLOWERS' + } + + const list = await createStarterPackList({ + name: defaultName ?? '', + profiles, + agent, + }) + + return ( + await agent.app.bsky.graph.starterpack.create( + { + repo: agent.session!.did, + validate: false, + }, + { + name: defaultName ?? '', + list: list?.uri, + createdAt: new Date().toISOString(), + }, + ) + ).uri + } catch (e: unknown) { + logger.error('Failed to generate starter pack', {error: e}) + return 'ERROR' + } +} diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx index d3c880e218..f4833373b7 100644 --- a/src/screens/StarterPack/Wizard/index.tsx +++ b/src/screens/StarterPack/Wizard/index.tsx @@ -20,6 +20,7 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack' import {useQueryClient} from '@tanstack/react-query' import {HITSLOP_10} from 'lib/constants' +import {createStarterPackList} from 'lib/generate-starterpack' import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types' import {enforceLen} from 'lib/strings/helpers' import {isAndroid, isNative, isWeb} from 'platform/detection' @@ -206,40 +207,6 @@ function WizardInner({ const uiStrings = wizardUiStrings[state.currentStep] - const createList = async (): Promise< - {uri: string; cid: string} | undefined - > => { - if (state.profiles.length === 0) return - - const list = await agent.app.bsky.graph.list.create( - {repo: currentAccount?.did}, - { - name: state.name ?? defaultName, - description: state.description, - descriptionFacets: [], - avatar: undefined, - createdAt: new Date().toISOString(), - purpose: 'app.bsky.graph.defs#referencelist', - }, - ) - if (!list) throw new Error('List creation failed') - await agent.com.atproto.repo.applyWrites({ - repo: currentAccount!.did, - writes: state.profiles.map(p => ({ - $type: 'com.atproto.repo.applyWrites#create', - collection: 'app.bsky.graph.listitem', - value: { - $type: 'app.bsky.graph.listitem', - subject: p.did, - list: list?.uri, - createdAt: new Date().toISOString(), - }, - })), - }) - - return list - } - const invalidateQueries = async () => { if (!did || !rkey) return @@ -301,7 +268,13 @@ function WizardInner({ }) } } else { - list = await createList() + list = await createStarterPackList({ + name: state.name ?? defaultName, + description: state.description, + descriptionFacets: [], + profiles: state.profiles, + agent, + }) } await agent.com.atproto.repo.putRecord({ @@ -336,7 +309,13 @@ function WizardInner({ }, 1000) } else { // Creating a new starter pack - const list = await createList() + const list = await createStarterPackList({ + name: state.name ?? defaultName, + description: state.description, + descriptionFacets: [], + profiles: state.profiles, + agent, + }) const res = await agent.app.bsky.graph.starterpack.create( { repo: currentAccount!.did,