[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
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import {useCallback, useContext} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useOnboardingSuggestedStarterPacksQuery} from '#/state/queries/useOnboardingSuggestedStarterPacksQuery'
|
||||
import {useOnboardingDispatch} from '#/state/shell'
|
||||
import {OnboardingControls} from '#/screens/Onboarding/Layout'
|
||||
import {Context} from '#/screens/Onboarding/state'
|
||||
import {atoms as a, useBreakpoints} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwiseIcon} from '#/components/icons/ArrowRotateCounterClockwise'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {StarterPackCard} from './StarterPackCard'
|
||||
|
||||
export function StepSuggestedStarterpacks() {
|
||||
const {_} = useLingui()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
const {state, dispatch} = useContext(Context)
|
||||
const onboardDispatch = useOnboardingDispatch()
|
||||
|
||||
const {
|
||||
data: suggestedStarterPacks,
|
||||
isLoading,
|
||||
error,
|
||||
isRefetching,
|
||||
refetch,
|
||||
} = useOnboardingSuggestedStarterPacksQuery({
|
||||
enabled: true,
|
||||
overrideInterests: state.interestsStepResults.selectedInterests,
|
||||
})
|
||||
|
||||
const isError = !!error
|
||||
|
||||
const skipOnboarding = useCallback(() => {
|
||||
onboardDispatch({type: 'finish'})
|
||||
dispatch({type: 'finish'})
|
||||
}, [onboardDispatch, dispatch])
|
||||
|
||||
return (
|
||||
<View style={[a.align_start]} testID="onboardingInterests">
|
||||
<Text style={[a.font_bold, a.text_3xl]}>
|
||||
<Trans comment="Accounts suggested to the user for them to follow">
|
||||
Suggested for you
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.overflow_hidden,
|
||||
a.mt_lg,
|
||||
a.flex_1,
|
||||
a.justify_start,
|
||||
a.w_full,
|
||||
]}>
|
||||
{isLoading || !moderationOpts ? (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.mt_md,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
{minHeight: 400},
|
||||
]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
) : isError ? (
|
||||
<View style={[a.flex_1, a.px_xl, a.pt_5xl]}>
|
||||
<Admonition type="error">
|
||||
<Trans>
|
||||
An error occurred while fetching suggested accounts.
|
||||
</Trans>
|
||||
</Admonition>
|
||||
</View>
|
||||
) : (
|
||||
<View style={[a.flex_1, a.mt_md]}>
|
||||
{suggestedStarterPacks?.starterPacks.map(starterPack => (
|
||||
<View style={[a.pb_lg]} key={starterPack.uri}>
|
||||
<StarterPackCard view={starterPack} />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<OnboardingControls.Portal>
|
||||
{isError ? (
|
||||
<View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
|
||||
<Button
|
||||
disabled={isRefetching}
|
||||
color="secondary"
|
||||
size="large"
|
||||
label={_(msg`Retry`)}
|
||||
onPress={() => refetch()}>
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={ArrowRotateCounterClockwiseIcon} />
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="large"
|
||||
label={_(msg`Skip this flow`)}
|
||||
onPress={skipOnboarding}>
|
||||
<ButtonText>
|
||||
<Trans>Skip</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
|
||||
<Button
|
||||
color="primary"
|
||||
size="large"
|
||||
label={_(msg`Continue to next step`)}
|
||||
onPress={() => dispatch({type: 'next'})}>
|
||||
<ButtonText>
|
||||
<Trans>Continue</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</OnboardingControls.Portal>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user