automatically generate a starter pack

This commit is contained in:
Hailey
2024-06-12 15:31:46 -07:00
parent 86b1afe483
commit d106ba45d3
3 changed files with 231 additions and 48 deletions
@@ -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<NavigationProp>()
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 (
<LinearGradientBackground
@@ -160,17 +198,81 @@ function EmptyComponent() {
your friends.
</Text>
</View>
<Button
label={_(msg`Create a starter pack`)}
variant="outline"
color="primary"
size="small"
onPress={() => navigation.navigate('StarterPackWizard', {})}
style={[{width: 100, marginLeft: 'auto'}]}>
<ButtonText>
<Trans>Create</Trans>
</ButtonText>
</Button>
<View style={[a.flex_row, a.gap_md, {marginLeft: 'auto'}]}>
<Button
label={_(msg`Create a starter pack for me`)}
variant="ghost"
color="primary"
size="small"
disabled={isGenerating}
onPress={confirmDialogControl.open}
style={{backgroundColor: 'transparent'}}>
<ButtonText style={{color: 'white'}}>
<Trans>Make one for me</Trans>
</ButtonText>
{isGenerating && <Loader size="md" />}
</Button>
<Button
label={_(msg`Create a starter pack`)}
variant="ghost"
color="primary"
size="small"
disabled={isGenerating}
onPress={() => navigation.navigate('StarterPackWizard', {})}
style={{
backgroundColor: 'white',
borderColor: 'white',
width: 100,
}}>
<ButtonText>
<Trans>Create</Trans>
</ButtonText>
</Button>
</View>
<Prompt.Outer control={confirmDialogControl}>
<Prompt.TitleText>
<Trans>Generate a starter pack?</Trans>
</Prompt.TitleText>
<Prompt.DescriptionText>
<Trans>
You can customize your starter pack with feeds and labelers if you
create one on your own.
</Trans>
</Prompt.DescriptionText>
<Prompt.Actions>
<Prompt.Action
color="primary"
cta={_(msg`I'll create one`)}
onPress={() => {
navigation.navigate('StarterPackWizard', {})
}}
/>
<Prompt.Action
color="secondary"
cta={_(msg`Generate anyway`)}
onPress={generate}
/>
</Prompt.Actions>
</Prompt.Outer>
<Prompt.Basic
control={followersDialogControl}
title={_(msg`Oops!`)}
description={_(
msg`You must be following at least seven other people to generate a starter pack.`,
)}
onConfirm={() => {}}
showCancel={false}
/>
<Prompt.Basic
control={errorDialogControl}
title={_(msg`Oops!`)}
description={_(
msg`An error occurred while generating your starter pack. Want to try again?`,
)}
onConfirm={generate}
confirmButtonCta={_(msg`Retry`)}
/>
</LinearGradientBackground>
)
}
+102
View File
@@ -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<string | 'NOT_ENOUGH_FOLLOWERS' | 'ERROR'> {
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'
}
}
+15 -36
View File
@@ -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,