Files
bsky-social-app/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx
T
Samuel Newman 1127e50248 [Onboarding] Add starterpacks step (#9007)
* add onboarding starterpacks step

* tweak starter pack cards

* fix web styling

* use new API

* add screen transitions to onboarding

* skip host account

* bump limit to 6

* tweak font weight

* rm commented out code

* fix yarn.lock

* limit starter packs experiment to english speakers
2025-10-15 07:32:38 -07:00

175 lines
4.9 KiB
TypeScript

import {useState} from 'react'
import {View} from 'react-native'
import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {batchedUpdates} from '#/lib/batchedUpdates'
import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
import {logger} from '#/logger'
import {updateProfileShadow} from '#/state/cache/profile-shadow'
import {getAllListMembers} from '#/state/queries/list-members'
import {useAgent, useSession} from '#/state/session'
import {bulkWriteFollows} from '#/screens/Onboarding/util'
import {AvatarStack} from '#/screens/Search/components/StarterPackCard'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
const IGNORED_ACCOUNT = 'did:plc:pifkcjimdcfwaxkanzhwxufp'
export function StarterPackCard({
view,
}: {
view: AppBskyGraphDefs.StarterPackView
}) {
const t = useTheme()
const {_} = useLingui()
const {currentAccount} = useSession()
const {gtPhone} = useBreakpoints()
const agent = useAgent()
const queryClient = useQueryClient()
const record = view.record
const [isProcessing, setIsProcessing] = useState(false)
const [isFollowingAll, setIsFollowingAll] = useState(false)
const onFollowAll = async () => {
if (!view.list) return
setIsProcessing(true)
let listItems: AppBskyGraphDefs.ListItemView[] = []
try {
listItems = await getAllListMembers(agent, view.list.uri)
} catch (e) {
setIsProcessing(false)
Toast.show(_(msg`An error occurred while trying to follow all`), {
type: 'error',
})
logger.error('Failed to get list members for starter pack', {
safeMessage: e,
})
return
}
const dids = listItems
.filter(
li =>
li.subject.did !== IGNORED_ACCOUNT &&
li.subject.did !== currentAccount?.did &&
!isBlockedOrBlocking(li.subject) &&
!isMuted(li.subject) &&
!li.subject.viewer?.following,
)
.map(li => li.subject.did)
let followUris: Map<string, string>
try {
followUris = await bulkWriteFollows(agent, dids)
} catch (e) {
setIsProcessing(false)
Toast.show(_(msg`An error occurred while trying to follow all`), {
type: 'error',
})
logger.error('Failed to follow all accounts', {safeMessage: e})
}
setIsFollowingAll(true)
setIsProcessing(false)
batchedUpdates(() => {
for (let did of dids) {
updateProfileShadow(queryClient, did, {
followingUri: followUris.get(did),
})
}
})
Toast.show(_(msg`All accounts have been followed!`), {type: 'success'})
logger.metric('starterPack:followAll', {
logContext: 'Onboarding',
starterPack: view.uri,
count: dids.length,
})
}
if (
!bsky.dangerousIsType<AppBskyGraphStarterpack.Record>(
record,
AppBskyGraphStarterpack.isRecord,
)
) {
return null
}
const profileCount = gtPhone ? 11 : 8
const profiles = view.listItemsSample
?.slice(0, profileCount)
.map(item => item.subject)
return (
<View
style={[
a.w_full,
a.p_lg,
a.gap_md,
a.border,
a.rounded_lg,
a.overflow_hidden,
t.atoms.border_contrast_medium,
]}>
<AvatarStack
profiles={profiles ?? []}
numPending={profileCount}
total={view.list?.listItemCount}
/>
<View
style={[
a.w_full,
a.flex_row,
a.align_end,
a.gap_lg,
web({
position: 'static',
zIndex: 'unset',
}),
]}>
<View style={[a.flex_1, a.gap_2xs]}>
<Text
emoji
style={[a.text_md, a.font_semi_bold, a.leading_snug]}
numberOfLines={1}>
{record.name}
</Text>
<Text
emoji
style={[a.text_xs, t.atoms.text_contrast_medium, a.leading_snug]}
numberOfLines={2}>
{record.description}
</Text>
</View>
<Button
label={_(msg`Follow all`)}
disabled={isProcessing || isFollowingAll}
onPress={onFollowAll}
color="secondary"
size="small"
style={[a.z_50]}>
<ButtonText>
<Trans>Follow all</Trans>
</ButtonText>
{isFollowingAll ? (
<ButtonIcon icon={CheckIcon} />
) : (
isProcessing && <ButtonIcon icon={Loader} />
)}
</Button>
</View>
</View>
)
}