New Onboarding (#2596)
* Add round and square buttons
* Allow some style for buttons, add icons
* Change text selection color
* Center button text, whoops
* Outer layout, some primitive updates
* WIP
* onboarding feed prefs (#2590)
* add `style` to toggle label to modify text style
* Revert "add `style` to toggle label to modify text style"
This reverts commit 8f4b517b85.
* following feed prefs
* remove unnecessary memo
* reusable divider component
* org imports
* add finished screen
* Theme SelectedAccountCard
* Require at least 3 interests
* Placeholder save logic
* WIP algo feeds
* Improve lineHeight handling, add RichText, improve Link by adding InlineLink
* Inherit lineHeight in heading comps
* Algo feeds mostly good
* Topical feeds ish
* Layout cleanup
* Improve button styles
* moderation prefs for onboarding (#2594)
* WIP algo feeds
* modify controlalbelgroup typing for easy .map()
* adjust padding on button
* add moderation screen
* add moderation screen
* add moderation screen
---------
Co-authored-by: Eric Bailey <git@esb.lol>
* Fix toggle button styles
* A11y props on outer portal
* Put it all on red
* New data shape
* Handle mock data
* Bulk write (not yet)
* Remove interests validation
* Clean up interests
* i18n layout and first step
* Clean up suggested follows screen
* Clean up following step
* Clean up algo feeds step
* Clean up topical feeds
* Add skeleton for feed card
* WIP moderation step
* cleanup moderation styles (#2605)
* cleanup moderation styles
* fix(?) toggle button group styles
* adjust toggle to fit any screen
* Some more cleanup
* Icons
* ToggleButton tweaks
* Reset
* Hook up data
* Better suggestions
* Bulk write
* Some logging
* Use new api
* Concat topical feeds
* Metrics
* Disable links in RichText, feedcards
* Tweak primary feed cards
* Update metrics
* Fix layout shift
* Fix ToggleButton again, whoops
* Error state
* Bump api package, ensure interests are saved
* Better fix for autofill
* i18n, button positions
* Remove unused export
* Add default prefs object
* Fix overflow in user cards
* Add 2 lines of bios to suggested accounts cards
* Nits
* Don't resolve facets by default
* Update storybook
* Disable flag for now
* Remove age dialog from moderations step
* Improvements and tweaks to new onboarding
---------
Co-authored-by: Hailey <153161762+haileyok@users.noreply.github.com>
Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {Image} from 'expo-image'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
import {useTheme, atoms as a} from '#/alf'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
|
||||
import {Text, H3} from '#/components/Typography'
|
||||
import {RichText} from '#/components/RichText'
|
||||
|
||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {FeedConfig} from '#/screens/Onboarding/StepAlgoFeeds'
|
||||
|
||||
function PrimaryFeedCardInner({
|
||||
feed,
|
||||
config,
|
||||
}: {
|
||||
feed: FeedSourceInfo
|
||||
config: FeedConfig
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const ctx = Toggle.useItemContext()
|
||||
|
||||
const styles = React.useMemo(
|
||||
() => ({
|
||||
active: [t.atoms.bg_contrast_25],
|
||||
selected: [
|
||||
a.shadow_md,
|
||||
{
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.primary_50 : t.palette.primary_950,
|
||||
},
|
||||
],
|
||||
selectedHover: [
|
||||
{
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.primary_25 : t.palette.primary_975,
|
||||
},
|
||||
],
|
||||
textSelected: [{color: t.palette.white}],
|
||||
checkboxSelected: [
|
||||
{
|
||||
borderColor: t.palette.white,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[t],
|
||||
)
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.w_full,
|
||||
a.p_lg,
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
t.atoms.bg_contrast_50,
|
||||
(ctx.hovered || ctx.focused || ctx.pressed) && styles.active,
|
||||
ctx.selected && styles.selected,
|
||||
ctx.selected &&
|
||||
(ctx.hovered || ctx.focused || ctx.pressed) &&
|
||||
styles.selectedHover,
|
||||
]}>
|
||||
{ctx.selected && config.gradient && (
|
||||
<LinearGradient
|
||||
colors={config.gradient.values.map(v => v[1])}
|
||||
locations={config.gradient.values.map(v => v[0])}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
width: 64,
|
||||
height: 64,
|
||||
},
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
t.atoms.bg,
|
||||
]}>
|
||||
<Image
|
||||
source={{uri: feed.avatar}}
|
||||
style={[a.w_full, a.h_full]}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={[a.pt_xs, a.flex_grow]}>
|
||||
<H3
|
||||
style={[
|
||||
a.text_lg,
|
||||
a.font_bold,
|
||||
ctx.selected && styles.textSelected,
|
||||
]}>
|
||||
{feed.displayName}
|
||||
</H3>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
{opacity: 0.6},
|
||||
a.text_md,
|
||||
a.py_xs,
|
||||
ctx.selected && styles.textSelected,
|
||||
]}>
|
||||
by @{feed.creatorHandle}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
width: 28,
|
||||
height: 28,
|
||||
},
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.rounded_sm,
|
||||
ctx.selected ? [a.border, styles.checkboxSelected] : t.atoms.bg,
|
||||
]}>
|
||||
{ctx.selected && <Check size="sm" fill={t.palette.white} />}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
opacity: ctx.selected ? 0.3 : 1,
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
a.mt_md,
|
||||
a.w_full,
|
||||
t.name === 'light' ? t.atoms.border : t.atoms.border_contrast,
|
||||
ctx.selected && {
|
||||
borderTopColor: t.palette.white,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<View style={[a.pt_md]}>
|
||||
<RichText
|
||||
value={feed.description}
|
||||
style={[
|
||||
a.text_md,
|
||||
ctx.selected &&
|
||||
(t.name === 'light'
|
||||
? t.atoms.text_inverted
|
||||
: {color: t.palette.white}),
|
||||
]}
|
||||
disableLinks
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function PrimaryFeedCard({config}: {config: FeedConfig}) {
|
||||
const {_} = useLingui()
|
||||
const {data: feed} = useFeedSourceInfoQuery({uri: config.uri})
|
||||
|
||||
return !feed ? (
|
||||
<FeedCardPlaceholder primary />
|
||||
) : (
|
||||
<Toggle.Item
|
||||
name={feed.uri}
|
||||
label={_(msg`Subscribe to the ${feed.displayName} feed`)}>
|
||||
<PrimaryFeedCardInner config={config} feed={feed} />
|
||||
</Toggle.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function FeedCardInner({feed}: {feed: FeedSourceInfo; config: FeedConfig}) {
|
||||
const t = useTheme()
|
||||
const ctx = Toggle.useItemContext()
|
||||
|
||||
const styles = React.useMemo(
|
||||
() => ({
|
||||
active: [t.atoms.bg_contrast_25],
|
||||
selected: [
|
||||
{
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.primary_50 : t.palette.primary_950,
|
||||
},
|
||||
],
|
||||
selectedHover: [
|
||||
{
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.primary_25 : t.palette.primary_975,
|
||||
},
|
||||
],
|
||||
textSelected: [],
|
||||
checkboxSelected: [
|
||||
{
|
||||
backgroundColor: t.palette.primary_500,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[t],
|
||||
)
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.w_full,
|
||||
a.p_md,
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
t.atoms.bg_contrast_50,
|
||||
(ctx.hovered || ctx.focused || ctx.pressed) && styles.active,
|
||||
ctx.selected && styles.selected,
|
||||
ctx.selected &&
|
||||
(ctx.hovered || ctx.focused || ctx.pressed) &&
|
||||
styles.selectedHover,
|
||||
]}>
|
||||
<View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
width: 44,
|
||||
height: 44,
|
||||
},
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
t.atoms.bg,
|
||||
]}>
|
||||
<Image
|
||||
source={{uri: feed.avatar}}
|
||||
style={[a.w_full, a.h_full]}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={[a.pt_2xs, a.flex_grow]}>
|
||||
<H3
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
ctx.selected && styles.textSelected,
|
||||
]}>
|
||||
{feed.displayName}
|
||||
</H3>
|
||||
<Text
|
||||
style={[
|
||||
{opacity: 0.8},
|
||||
a.pt_xs,
|
||||
ctx.selected && styles.textSelected,
|
||||
]}>
|
||||
@{feed.creatorHandle}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.rounded_sm,
|
||||
t.atoms.bg,
|
||||
ctx.selected && styles.checkboxSelected,
|
||||
{
|
||||
width: 28,
|
||||
height: 28,
|
||||
},
|
||||
]}>
|
||||
{ctx.selected && <Check size="sm" fill={t.palette.white} />}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
opacity: ctx.selected ? 0.3 : 1,
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
a.mt_md,
|
||||
a.w_full,
|
||||
t.name === 'light' ? t.atoms.border : t.atoms.border_contrast,
|
||||
ctx.selected && {
|
||||
borderTopColor: t.palette.primary_200,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<View style={[a.pt_md]}>
|
||||
<RichText value={feed.description} disableLinks />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function FeedCard({config}: {config: FeedConfig}) {
|
||||
const {_} = useLingui()
|
||||
const {data: feed} = useFeedSourceInfoQuery({uri: config.uri})
|
||||
|
||||
return !feed ? (
|
||||
<FeedCardPlaceholder />
|
||||
) : feed.avatar ? (
|
||||
<Toggle.Item
|
||||
name={feed.uri}
|
||||
label={_(msg`Subscribe to the ${feed.displayName} feed`)}>
|
||||
<FeedCardInner config={config} feed={feed} />
|
||||
</Toggle.Item>
|
||||
) : null
|
||||
}
|
||||
|
||||
export function FeedCardPlaceholder({primary}: {primary?: boolean}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.relative,
|
||||
a.w_full,
|
||||
a.p_md,
|
||||
a.rounded_md,
|
||||
a.overflow_hidden,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
width: primary ? 64 : 44,
|
||||
height: primary ? 64 : 44,
|
||||
},
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
t.atoms.bg_contrast_100,
|
||||
]}
|
||||
/>
|
||||
|
||||
<View style={[a.pt_2xs, a.flex_grow, a.gap_sm]}>
|
||||
<View
|
||||
style={[
|
||||
{width: 100, height: primary ? 20 : 16},
|
||||
a.rounded_sm,
|
||||
t.atoms.bg_contrast_100,
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
{width: 60, height: 12},
|
||||
a.rounded_sm,
|
||||
t.atoms.bg_contrast_100,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
a.mt_md,
|
||||
a.w_full,
|
||||
t.atoms.border,
|
||||
]}
|
||||
/>
|
||||
|
||||
<View style={[a.pt_md, a.gap_xs]}>
|
||||
<View
|
||||
style={[
|
||||
{width: '60%', height: 12},
|
||||
a.rounded_sm,
|
||||
t.atoms.bg_contrast_100,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
|
||||
import {atoms as a, tokens, useTheme} from '#/alf'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
|
||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||
|
||||
import {Context} from '#/screens/Onboarding/state'
|
||||
import {
|
||||
Title,
|
||||
Description,
|
||||
OnboardingControls,
|
||||
} from '#/screens/Onboarding/Layout'
|
||||
import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard'
|
||||
import {IconCircle} from '#/screens/Onboarding/IconCircle'
|
||||
|
||||
export type FeedConfig = {
|
||||
default: boolean
|
||||
uri: string
|
||||
gradient?: typeof tokens.gradients.midnight | typeof tokens.gradients.nordic
|
||||
}
|
||||
|
||||
const PRIMARY_FEEDS: FeedConfig[] = [
|
||||
{
|
||||
default: true,
|
||||
uri: 'at://did:plc:wqowuobffl66jv3kpsvo7ak4/app.bsky.feed.generator/the-algorithm',
|
||||
gradient: tokens.gradients.midnight,
|
||||
},
|
||||
{
|
||||
default: false,
|
||||
uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot',
|
||||
gradient: tokens.gradients.midnight,
|
||||
},
|
||||
]
|
||||
|
||||
const SECONDARY_FEEDS: FeedConfig[] = [
|
||||
{
|
||||
default: false,
|
||||
uri: 'at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/infreq',
|
||||
},
|
||||
{
|
||||
default: false,
|
||||
uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends',
|
||||
},
|
||||
{
|
||||
default: false,
|
||||
uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/best-of-follows',
|
||||
},
|
||||
{
|
||||
default: false,
|
||||
uri: 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/catch-up',
|
||||
},
|
||||
{
|
||||
default: false,
|
||||
uri: 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/at-bangers',
|
||||
},
|
||||
]
|
||||
|
||||
export function StepAlgoFeeds() {
|
||||
const {_} = useLingui()
|
||||
const {track} = useAnalytics()
|
||||
const t = useTheme()
|
||||
const {state, dispatch} = React.useContext(Context)
|
||||
const [primaryFeedUris, setPrimaryFeedUris] = React.useState<string[]>(
|
||||
PRIMARY_FEEDS.map(f => (f.default ? f.uri : '')).filter(Boolean),
|
||||
)
|
||||
const [secondaryFeedUris, setSeconaryFeedUris] = React.useState<string[]>([])
|
||||
const [saving, setSaving] = React.useState(false)
|
||||
|
||||
const saveFeeds = React.useCallback(async () => {
|
||||
setSaving(true)
|
||||
|
||||
const uris = primaryFeedUris.concat(secondaryFeedUris)
|
||||
dispatch({type: 'setAlgoFeedsStepResults', feedUris: uris})
|
||||
|
||||
setSaving(false)
|
||||
dispatch({type: 'next'})
|
||||
track('OnboardingV2:StepAlgoFeeds:End', {
|
||||
selectedPrimaryFeeds: primaryFeedUris,
|
||||
selectedPrimaryFeedsLength: primaryFeedUris.length,
|
||||
selectedSecondaryFeeds: secondaryFeedUris,
|
||||
selectedSecondaryFeedsLength: secondaryFeedUris.length,
|
||||
})
|
||||
}, [primaryFeedUris, secondaryFeedUris, dispatch, track])
|
||||
|
||||
React.useEffect(() => {
|
||||
track('OnboardingV2:StepAlgoFeeds:Start')
|
||||
}, [track])
|
||||
|
||||
return (
|
||||
<View style={[a.align_start]}>
|
||||
<IconCircle icon={ListSparkle} style={[a.mb_2xl]} />
|
||||
|
||||
<Title>
|
||||
<Trans>Choose your algorithmic feeds</Trans>
|
||||
</Title>
|
||||
<Description>
|
||||
<Trans>
|
||||
Feeds are created by users and can give you entirely new experiences.
|
||||
</Trans>
|
||||
</Description>
|
||||
|
||||
<View style={[a.w_full, a.pb_2xl]}>
|
||||
<Toggle.Group
|
||||
values={primaryFeedUris}
|
||||
onChange={setPrimaryFeedUris}
|
||||
label={_(msg`Select your primary algorithmic feeds`)}>
|
||||
<Text
|
||||
style={[a.text_md, a.pt_4xl, a.pb_md, t.atoms.text_contrast_700]}>
|
||||
<Trans>We recommend "For You" by Skygaze:</Trans>
|
||||
</Text>
|
||||
<FeedCard config={PRIMARY_FEEDS[0]} />
|
||||
<Text
|
||||
style={[a.text_md, a.pt_4xl, a.pb_lg, t.atoms.text_contrast_700]}>
|
||||
<Trans>Or you can try our "Discover" algorithm:</Trans>
|
||||
</Text>
|
||||
<FeedCard config={PRIMARY_FEEDS[1]} />
|
||||
</Toggle.Group>
|
||||
|
||||
<Toggle.Group
|
||||
values={secondaryFeedUris}
|
||||
onChange={setSeconaryFeedUris}
|
||||
label={_(msg`Select your secondary algorithmic feeds`)}>
|
||||
<Text
|
||||
style={[a.text_md, a.pt_4xl, a.pb_lg, t.atoms.text_contrast_700]}>
|
||||
<Trans>There are many feeds to try:</Trans>
|
||||
</Text>
|
||||
<View style={[a.gap_md]}>
|
||||
{SECONDARY_FEEDS.map(config => (
|
||||
<FeedCard key={config.uri} config={config} />
|
||||
))}
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
</View>
|
||||
|
||||
<OnboardingControls.Portal>
|
||||
<Button
|
||||
disabled={saving}
|
||||
key={state.activeStep} // remove focus state on nav
|
||||
variant="gradient"
|
||||
color="gradient_sky"
|
||||
size="large"
|
||||
label={_(msg`Continue to the next step`)}
|
||||
onPress={saveFeeds}>
|
||||
<ButtonText>
|
||||
<Trans>Continue</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={saving ? Loader : ChevronRight} position="right" />
|
||||
</Button>
|
||||
</OnboardingControls.Portal>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user