Remove old onboarding (#4224)
* Hardcode onboarding_v2 to true, rm dead code * Rm initialState, use initialStateReduced * Rm dead code * Drop *reduced prefix in code * Prettierzio/stable
parent
9bd411c151
commit
adbbded003
|
@ -1,5 +1,4 @@
|
||||||
export type Gate =
|
export type Gate =
|
||||||
// Keep this alphabetic please.
|
// Keep this alphabetic please.
|
||||||
| 'reduced_onboarding_and_home_algo_v2'
|
|
||||||
| 'request_notifications_permission_after_onboarding'
|
| 'request_notifications_permission_after_onboarding'
|
||||||
| 'show_follow_back_label_v2'
|
| 'show_follow_back_label_v2'
|
||||||
|
|
|
@ -1,378 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {Image} from 'expo-image'
|
|
||||||
import {LinearGradient} from 'expo-linear-gradient'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
|
||||||
import {FeedConfig} from '#/screens/Onboarding/StepAlgoFeeds'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
|
||||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
|
||||||
import {RichText} from '#/components/RichText'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
|
|
||||||
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]}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_lg,
|
|
||||||
a.font_bold,
|
|
||||||
ctx.selected && styles.textSelected,
|
|
||||||
]}>
|
|
||||||
{feed.displayName}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
{opacity: 0.6},
|
|
||||||
a.text_md,
|
|
||||||
a.py_xs,
|
|
||||||
ctx.selected && styles.textSelected,
|
|
||||||
]}>
|
|
||||||
<Trans>by @{feed.creatorHandle}</Trans>
|
|
||||||
</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.atoms.border_contrast_low,
|
|
||||||
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_1, a.flex_grow]}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
a.font_bold,
|
|
||||||
ctx.selected && styles.textSelected,
|
|
||||||
]}
|
|
||||||
numberOfLines={1}>
|
|
||||||
{feed.displayName}
|
|
||||||
</Text>
|
|
||||||
<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.atoms.border_contrast_low,
|
|
||||||
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_contrast_low,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<View style={[a.pt_md, a.gap_xs]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
{width: '60%', height: 12},
|
|
||||||
a.rounded_sm,
|
|
||||||
t.atoms.bg_contrast_100,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,168 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {
|
|
||||||
DescriptionText,
|
|
||||||
OnboardingControls,
|
|
||||||
TitleText,
|
|
||||||
} from '#/screens/Onboarding/Layout'
|
|
||||||
import {Context} from '#/screens/Onboarding/state'
|
|
||||||
import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard'
|
|
||||||
import {atoms as a, tokens, useTheme} from '#/alf'
|
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
|
||||||
import {IconCircle} from '#/components/IconCircle'
|
|
||||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
|
||||||
import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
|
|
||||||
import {Loader} from '#/components/Loader'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import {IS_PROD} from '#/env'
|
|
||||||
|
|
||||||
export type FeedConfig = {
|
|
||||||
default: boolean
|
|
||||||
uri: string
|
|
||||||
gradient?: typeof tokens.gradients.midnight | typeof tokens.gradients.nordic
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PRIMARY_FEEDS: FeedConfig[] = [
|
|
||||||
{
|
|
||||||
default: IS_PROD, // these feeds are only available in prod
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
logEvent('onboarding:algoFeeds:nextPressed', {
|
|
||||||
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]} />
|
|
||||||
|
|
||||||
<TitleText>
|
|
||||||
<Trans>Choose your main feeds</Trans>
|
|
||||||
</TitleText>
|
|
||||||
<DescriptionText>
|
|
||||||
<Trans>
|
|
||||||
Custom feeds built by the community bring you new experiences and help
|
|
||||||
you find the content you love.
|
|
||||||
</Trans>
|
|
||||||
</DescriptionText>
|
|
||||||
|
|
||||||
<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_medium,
|
|
||||||
]}>
|
|
||||||
<Trans>We recommend our "Discover" feed:</Trans>
|
|
||||||
</Text>
|
|
||||||
<FeedCard config={PRIMARY_FEEDS[0]} />
|
|
||||||
</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_medium,
|
|
||||||
]}>
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,19 +1,14 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {TID} from '@atproto/common-web'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
import {BSKY_APP_ACCOUNT_DID, IS_PROD_SERVICE} from '#/lib/constants'
|
import {BSKY_APP_ACCOUNT_DID} from '#/lib/constants'
|
||||||
import {DISCOVER_SAVED_FEED, TIMELINE_SAVED_FEED} from '#/lib/constants'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {
|
import {preferencesQueryKey} from '#/state/queries/preferences'
|
||||||
preferencesQueryKey,
|
|
||||||
useOverwriteSavedFeedsMutation,
|
|
||||||
} from '#/state/queries/preferences'
|
|
||||||
import {RQKEY as profileRQKey} from '#/state/queries/profile'
|
import {RQKEY as profileRQKey} from '#/state/queries/profile'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {useOnboardingDispatch} from '#/state/shell'
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
|
@ -24,10 +19,7 @@ import {
|
||||||
TitleText,
|
TitleText,
|
||||||
} from '#/screens/Onboarding/Layout'
|
} from '#/screens/Onboarding/Layout'
|
||||||
import {Context} from '#/screens/Onboarding/state'
|
import {Context} from '#/screens/Onboarding/state'
|
||||||
import {
|
import {bulkWriteFollows} from '#/screens/Onboarding/util'
|
||||||
bulkWriteFollows,
|
|
||||||
sortPrimaryAlgorithmFeeds,
|
|
||||||
} from '#/screens/Onboarding/util'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import {IconCircle} from '#/components/IconCircle'
|
import {IconCircle} from '#/components/IconCircle'
|
||||||
|
@ -45,83 +37,21 @@ export function StepFinished() {
|
||||||
const {state, dispatch} = React.useContext(Context)
|
const {state, dispatch} = React.useContext(Context)
|
||||||
const onboardDispatch = useOnboardingDispatch()
|
const onboardDispatch = useOnboardingDispatch()
|
||||||
const [saving, setSaving] = React.useState(false)
|
const [saving, setSaving] = React.useState(false)
|
||||||
const {mutateAsync: overwriteSavedFeeds} = useOverwriteSavedFeedsMutation()
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const gate = useGate()
|
|
||||||
|
|
||||||
const finishOnboarding = React.useCallback(async () => {
|
const finishOnboarding = React.useCallback(async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
|
|
||||||
// TODO uncomment
|
const {interestsStepResults, profileStepResults} = state
|
||||||
const {
|
|
||||||
interestsStepResults,
|
|
||||||
suggestedAccountsStepResults,
|
|
||||||
algoFeedsStepResults,
|
|
||||||
topicalFeedsStepResults,
|
|
||||||
profileStepResults,
|
|
||||||
} = state
|
|
||||||
const {selectedInterests} = interestsStepResults
|
const {selectedInterests} = interestsStepResults
|
||||||
const selectedFeeds = [
|
|
||||||
...sortPrimaryAlgorithmFeeds(algoFeedsStepResults.feedUris),
|
|
||||||
...topicalFeedsStepResults.feedUris,
|
|
||||||
]
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
bulkWriteFollows(
|
bulkWriteFollows(agent, [BSKY_APP_ACCOUNT_DID]),
|
||||||
agent,
|
|
||||||
suggestedAccountsStepResults.accountDids.concat(BSKY_APP_ACCOUNT_DID),
|
|
||||||
),
|
|
||||||
// these must be serial
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await agent.setInterestsPref({tags: selectedInterests})
|
await agent.setInterestsPref({tags: selectedInterests})
|
||||||
|
|
||||||
/*
|
|
||||||
* In the reduced onboading experiment, we'll rely on the default
|
|
||||||
* feeds set in `createAgentAndCreateAccount`. No feeds will be
|
|
||||||
* selected in onboarding and therefore we don't need to run this
|
|
||||||
* code (which would overwrite the other feeds already set).
|
|
||||||
*/
|
|
||||||
if (!gate('reduced_onboarding_and_home_algo_v2')) {
|
|
||||||
const otherFeeds = selectedFeeds.length
|
|
||||||
? selectedFeeds.map(f => ({
|
|
||||||
type: 'feed',
|
|
||||||
value: f,
|
|
||||||
pinned: true,
|
|
||||||
id: TID.nextStr(),
|
|
||||||
}))
|
|
||||||
: []
|
|
||||||
|
|
||||||
/*
|
|
||||||
* If no selected feeds and we're in prod, add the discover feed
|
|
||||||
* (mimics old behavior)
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
IS_PROD_SERVICE(agent.service.toString()) &&
|
|
||||||
!otherFeeds.length
|
|
||||||
) {
|
|
||||||
otherFeeds.push({
|
|
||||||
...DISCOVER_SAVED_FEED,
|
|
||||||
pinned: true,
|
|
||||||
id: TID.nextStr(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await overwriteSavedFeeds([
|
|
||||||
{
|
|
||||||
...TIMELINE_SAVED_FEED,
|
|
||||||
pinned: true,
|
|
||||||
id: TID.nextStr(),
|
|
||||||
},
|
|
||||||
...otherFeeds,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
})(),
|
})(),
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
if (!gate('reduced_onboarding_and_home_algo_v2')) return
|
|
||||||
|
|
||||||
const {imageUri, imageMime} = profileStepResults
|
const {imageUri, imageMime} = profileStepResults
|
||||||
if (imageUri && imageMime) {
|
if (imageUri && imageMime) {
|
||||||
const blobPromise = uploadBlob(agent, imageUri, imageMime)
|
const blobPromise = uploadBlob(agent, imageUri, imageMime)
|
||||||
|
@ -134,7 +64,6 @@ export function StepFinished() {
|
||||||
return existing
|
return existing
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
logEvent('onboarding:finished:avatarResult', {
|
logEvent('onboarding:finished:avatarResult', {
|
||||||
avatarResult: profileStepResults.isCreatedAvatar
|
avatarResult: profileStepResults.isCreatedAvatar
|
||||||
? 'created'
|
? 'created'
|
||||||
|
@ -169,17 +98,7 @@ export function StepFinished() {
|
||||||
track('OnboardingV2:StepFinished:End')
|
track('OnboardingV2:StepFinished:End')
|
||||||
track('OnboardingV2:Complete')
|
track('OnboardingV2:Complete')
|
||||||
logEvent('onboarding:finished:nextPressed', {})
|
logEvent('onboarding:finished:nextPressed', {})
|
||||||
}, [
|
}, [state, dispatch, onboardDispatch, setSaving, track, agent, queryClient])
|
||||||
state,
|
|
||||||
dispatch,
|
|
||||||
onboardDispatch,
|
|
||||||
setSaving,
|
|
||||||
overwriteSavedFeeds,
|
|
||||||
track,
|
|
||||||
agent,
|
|
||||||
gate,
|
|
||||||
queryClient,
|
|
||||||
])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
track('OnboardingV2:StepFinished:Start')
|
track('OnboardingV2:StepFinished:Start')
|
||||||
|
|
|
@ -1,161 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {
|
|
||||||
usePreferencesQuery,
|
|
||||||
useSetFeedViewPreferencesMutation,
|
|
||||||
} from 'state/queries/preferences'
|
|
||||||
import {
|
|
||||||
DescriptionText,
|
|
||||||
OnboardingControls,
|
|
||||||
TitleText,
|
|
||||||
} from '#/screens/Onboarding/Layout'
|
|
||||||
import {Context} from '#/screens/Onboarding/state'
|
|
||||||
import {atoms as a} from '#/alf'
|
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
|
||||||
import {Divider} from '#/components/Divider'
|
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
|
||||||
import {IconCircle} from '#/components/IconCircle'
|
|
||||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
|
||||||
import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
|
|
||||||
export function StepFollowingFeed() {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {track} = useAnalytics()
|
|
||||||
const {dispatch} = React.useContext(Context)
|
|
||||||
|
|
||||||
const {data: preferences} = usePreferencesQuery()
|
|
||||||
const {mutate: setFeedViewPref, variables} =
|
|
||||||
useSetFeedViewPreferencesMutation()
|
|
||||||
|
|
||||||
const showReplies = !(
|
|
||||||
variables?.hideReplies ?? preferences?.feedViewPrefs.hideReplies
|
|
||||||
)
|
|
||||||
const showReposts = !(
|
|
||||||
variables?.hideReposts ?? preferences?.feedViewPrefs.hideReposts
|
|
||||||
)
|
|
||||||
const showQuotes = !(
|
|
||||||
variables?.hideQuotePosts ?? preferences?.feedViewPrefs.hideQuotePosts
|
|
||||||
)
|
|
||||||
|
|
||||||
const onContinue = React.useCallback(() => {
|
|
||||||
dispatch({type: 'next'})
|
|
||||||
track('OnboardingV2:StepFollowingFeed:End')
|
|
||||||
logEvent('onboarding:followingFeed:nextPressed', {})
|
|
||||||
}, [track, dispatch])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
track('OnboardingV2:StepFollowingFeed:Start')
|
|
||||||
}, [track])
|
|
||||||
|
|
||||||
return (
|
|
||||||
// Hack for now to move the image container up
|
|
||||||
<View style={[a.align_start]}>
|
|
||||||
<IconCircle icon={FilterTimeline} style={[a.mb_2xl]} />
|
|
||||||
|
|
||||||
<TitleText>
|
|
||||||
<Trans>Your default feed is "Following"</Trans>
|
|
||||||
</TitleText>
|
|
||||||
<DescriptionText style={[a.mb_md]}>
|
|
||||||
<Trans>It shows posts from the people you follow as they happen.</Trans>
|
|
||||||
</DescriptionText>
|
|
||||||
|
|
||||||
<View style={[a.w_full]}>
|
|
||||||
<Toggle.Item
|
|
||||||
name="Show Replies" // no need to translate
|
|
||||||
label={_(msg`Show replies in Following feed`)}
|
|
||||||
value={showReplies}
|
|
||||||
onChange={() => {
|
|
||||||
setFeedViewPref({
|
|
||||||
hideReplies: showReplies,
|
|
||||||
})
|
|
||||||
}}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.w_full,
|
|
||||||
a.py_lg,
|
|
||||||
a.justify_between,
|
|
||||||
a.align_center,
|
|
||||||
]}>
|
|
||||||
<Text style={[a.text_md, a.font_bold]}>
|
|
||||||
<Trans>Show replies in Following</Trans>
|
|
||||||
</Text>
|
|
||||||
<Toggle.Switch />
|
|
||||||
</View>
|
|
||||||
</Toggle.Item>
|
|
||||||
<Divider />
|
|
||||||
<Toggle.Item
|
|
||||||
name="Show Reposts" // no need to translate
|
|
||||||
label={_(msg`Show re-posts in Following feed`)}
|
|
||||||
value={showReposts}
|
|
||||||
onChange={() => {
|
|
||||||
setFeedViewPref({
|
|
||||||
hideReposts: showReposts,
|
|
||||||
})
|
|
||||||
}}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.w_full,
|
|
||||||
a.py_lg,
|
|
||||||
a.justify_between,
|
|
||||||
a.align_center,
|
|
||||||
]}>
|
|
||||||
<Text style={[a.text_md, a.font_bold]}>
|
|
||||||
<Trans>Show reposts in Following</Trans>
|
|
||||||
</Text>
|
|
||||||
<Toggle.Switch />
|
|
||||||
</View>
|
|
||||||
</Toggle.Item>
|
|
||||||
<Divider />
|
|
||||||
<Toggle.Item
|
|
||||||
name="Show Quotes" // no need to translate
|
|
||||||
label={_(msg`Show quote-posts in Following feed`)}
|
|
||||||
value={showQuotes}
|
|
||||||
onChange={() => {
|
|
||||||
setFeedViewPref({
|
|
||||||
hideQuotePosts: showQuotes,
|
|
||||||
})
|
|
||||||
}}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.w_full,
|
|
||||||
a.py_lg,
|
|
||||||
a.justify_between,
|
|
||||||
a.align_center,
|
|
||||||
]}>
|
|
||||||
<Text style={[a.text_md, a.font_bold]}>
|
|
||||||
<Trans>Show quotes in Following</Trans>
|
|
||||||
</Text>
|
|
||||||
<Toggle.Switch />
|
|
||||||
</View>
|
|
||||||
</Toggle.Item>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<DescriptionText style={[a.mt_lg]}>
|
|
||||||
<Trans>You can change these settings later.</Trans>
|
|
||||||
</DescriptionText>
|
|
||||||
|
|
||||||
<OnboardingControls.Portal>
|
|
||||||
<Button
|
|
||||||
variant="gradient"
|
|
||||||
color="gradient_sky"
|
|
||||||
size="large"
|
|
||||||
label={_(msg`Continue to next step`)}
|
|
||||||
onPress={onContinue}>
|
|
||||||
<ButtonText>
|
|
||||||
<Trans>Continue</Trans>
|
|
||||||
</ButtonText>
|
|
||||||
<ButtonIcon icon={ChevronRight} position="right" />
|
|
||||||
</Button>
|
|
||||||
</OnboardingControls.Portal>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -5,12 +5,11 @@ import {useLingui} from '@lingui/react'
|
||||||
import {useQuery} from '@tanstack/react-query'
|
import {useQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {capitalize} from '#/lib/strings/capitalize'
|
import {capitalize} from '#/lib/strings/capitalize'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {useOnboardingDispatch} from '#/state/shell'
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
|
|
||||||
import {
|
import {
|
||||||
DescriptionText,
|
DescriptionText,
|
||||||
OnboardingControls,
|
OnboardingControls,
|
||||||
|
@ -34,8 +33,6 @@ export function StepInterests() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const gate = useGate()
|
|
||||||
const requestNotificationsPermission = useRequestNotificationsPermission()
|
|
||||||
|
|
||||||
const {state, dispatch, interestsDisplayNames} = React.useContext(Context)
|
const {state, dispatch, interestsDisplayNames} = React.useContext(Context)
|
||||||
const [saving, setSaving] = React.useState(false)
|
const [saving, setSaving] = React.useState(false)
|
||||||
|
@ -132,12 +129,6 @@ export function StepInterests() {
|
||||||
track('OnboardingV2:StepInterests:Start')
|
track('OnboardingV2:StepInterests:Start')
|
||||||
}, [track])
|
}, [track])
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!gate('reduced_onboarding_and_home_algo_v2')) {
|
|
||||||
requestNotificationsPermission('StartOnboarding')
|
|
||||||
}
|
|
||||||
}, [gate, requestNotificationsPermission])
|
|
||||||
|
|
||||||
const title = isError ? (
|
const title = isError ? (
|
||||||
<Trans>Oh no! Something went wrong.</Trans>
|
<Trans>Oh no! Something went wrong.</Trans>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -1,131 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {UseMutateFunction} from '@tanstack/react-query'
|
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {isIOS} from '#/platform/detection'
|
|
||||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
|
||||||
import * as Prompt from '#/components/Prompt'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
|
|
||||||
function Card({children}: React.PropsWithChildren<{}>) {
|
|
||||||
const t = useTheme()
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.gap_sm,
|
|
||||||
a.px_lg,
|
|
||||||
a.py_md,
|
|
||||||
a.rounded_sm,
|
|
||||||
a.mb_md,
|
|
||||||
t.atoms.bg_contrast_50,
|
|
||||||
]}>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AdultContentEnabledPref({
|
|
||||||
mutate,
|
|
||||||
variables,
|
|
||||||
}: {
|
|
||||||
mutate: UseMutateFunction<void, unknown, {enabled: boolean}, unknown>
|
|
||||||
variables: {enabled: boolean} | undefined
|
|
||||||
}) {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const t = useTheme()
|
|
||||||
const prompt = Prompt.usePromptControl()
|
|
||||||
|
|
||||||
// Reuse logic here form ContentFilteringSettings.tsx
|
|
||||||
const {data: preferences} = usePreferencesQuery()
|
|
||||||
|
|
||||||
const onToggleAdultContent = React.useCallback(async () => {
|
|
||||||
if (isIOS) {
|
|
||||||
prompt.open()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
mutate({
|
|
||||||
enabled: !(
|
|
||||||
variables?.enabled ?? preferences?.moderationPrefs.adultContentEnabled
|
|
||||||
),
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
Toast.show(
|
|
||||||
_(msg`There was an issue syncing your preferences with the server`),
|
|
||||||
)
|
|
||||||
logger.error('Failed to update preferences with server', {error: e})
|
|
||||||
}
|
|
||||||
}, [variables, preferences, mutate, _, prompt])
|
|
||||||
|
|
||||||
if (!preferences) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{preferences.userAge && preferences.userAge >= 18 ? (
|
|
||||||
<View style={[a.w_full, a.px_xs]}>
|
|
||||||
<Toggle.Item
|
|
||||||
name={_(msg`Enable adult content in your feeds`)}
|
|
||||||
label={_(msg`Enable adult content in your feeds`)}
|
|
||||||
value={
|
|
||||||
variables?.enabled ??
|
|
||||||
preferences?.moderationPrefs.adultContentEnabled
|
|
||||||
}
|
|
||||||
onChange={onToggleAdultContent}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.w_full,
|
|
||||||
a.justify_between,
|
|
||||||
a.align_center,
|
|
||||||
a.py_md,
|
|
||||||
]}>
|
|
||||||
<Text style={[a.font_bold]}>
|
|
||||||
<Trans>Enable Adult Content</Trans>
|
|
||||||
</Text>
|
|
||||||
<Toggle.Switch />
|
|
||||||
</View>
|
|
||||||
</Toggle.Item>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<Card>
|
|
||||||
<CircleInfo size="sm" fill={t.palette.contrast_500} />
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.flex_1,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
a.leading_snug,
|
|
||||||
{paddingTop: 1},
|
|
||||||
]}>
|
|
||||||
<Trans>You must be 18 years or older to enable adult content</Trans>
|
|
||||||
</Text>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Prompt.Outer control={prompt}>
|
|
||||||
<Prompt.TitleText>
|
|
||||||
<Trans>Adult Content</Trans>
|
|
||||||
</Prompt.TitleText>
|
|
||||||
<Prompt.DescriptionText>
|
|
||||||
<Trans>
|
|
||||||
Due to Apple policies, adult content can only be enabled on the web
|
|
||||||
after completing sign up.
|
|
||||||
</Trans>
|
|
||||||
</Prompt.DescriptionText>
|
|
||||||
<Prompt.Actions>
|
|
||||||
<Prompt.Action onPress={() => prompt.close()} cta={_(msg`OK`)} />
|
|
||||||
</Prompt.Actions>
|
|
||||||
</Prompt.Outer>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
|
|
||||||
import {
|
|
||||||
usePreferencesQuery,
|
|
||||||
usePreferencesSetContentLabelMutation,
|
|
||||||
} from '#/state/queries/preferences'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
|
||||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
|
|
||||||
export function ModerationOption({
|
|
||||||
labelValueDefinition,
|
|
||||||
disabled,
|
|
||||||
}: {
|
|
||||||
labelValueDefinition: InterpretedLabelValueDefinition
|
|
||||||
disabled?: boolean
|
|
||||||
}) {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const t = useTheme()
|
|
||||||
const {data: preferences} = usePreferencesQuery()
|
|
||||||
const {mutate, variables} = usePreferencesSetContentLabelMutation()
|
|
||||||
const label = labelValueDefinition.identifier
|
|
||||||
const visibility =
|
|
||||||
variables?.visibility ?? preferences?.moderationPrefs.labels?.[label]
|
|
||||||
|
|
||||||
const allLabelStrings = useGlobalLabelStrings()
|
|
||||||
const labelStrings =
|
|
||||||
labelValueDefinition.identifier in allLabelStrings
|
|
||||||
? allLabelStrings[labelValueDefinition.identifier]
|
|
||||||
: {
|
|
||||||
name: labelValueDefinition.identifier,
|
|
||||||
description: `Labeled "${labelValueDefinition.identifier}"`,
|
|
||||||
}
|
|
||||||
|
|
||||||
const onChange = React.useCallback(
|
|
||||||
(vis: string[]) => {
|
|
||||||
mutate({
|
|
||||||
label,
|
|
||||||
visibility: vis[0] as LabelPreference,
|
|
||||||
labelerDid: undefined,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[mutate, label],
|
|
||||||
)
|
|
||||||
|
|
||||||
const labels = {
|
|
||||||
hide: _(msg`Hide`),
|
|
||||||
warn: _(msg`Warn`),
|
|
||||||
show: _(msg`Show`),
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.justify_between,
|
|
||||||
a.gap_sm,
|
|
||||||
a.py_xs,
|
|
||||||
a.px_xs,
|
|
||||||
a.align_center,
|
|
||||||
]}>
|
|
||||||
<View style={[a.gap_xs, a.flex_1]}>
|
|
||||||
<Text style={[a.font_bold]}>{labelStrings.name}</Text>
|
|
||||||
<Text style={[t.atoms.text_contrast_medium, a.leading_snug]}>
|
|
||||||
{labelStrings.description}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[a.justify_center, {minHeight: 40}]}>
|
|
||||||
{disabled ? (
|
|
||||||
<Text style={[a.font_bold]}>
|
|
||||||
<Trans>Hide</Trans>
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<ToggleButton.Group
|
|
||||||
label={_(
|
|
||||||
msg`Configure content filtering setting for category: ${labelStrings.name.toLowerCase()}`,
|
|
||||||
)}
|
|
||||||
values={[visibility ?? 'hide']}
|
|
||||||
onChange={onChange}>
|
|
||||||
<ToggleButton.Button name="ignore" label={labels.show}>
|
|
||||||
<ToggleButton.ButtonText>{labels.show}</ToggleButton.ButtonText>
|
|
||||||
</ToggleButton.Button>
|
|
||||||
<ToggleButton.Button name="warn" label={labels.warn}>
|
|
||||||
<ToggleButton.ButtonText>{labels.warn}</ToggleButton.ButtonText>
|
|
||||||
</ToggleButton.Button>
|
|
||||||
<ToggleButton.Button name="hide" label={labels.hide}>
|
|
||||||
<ToggleButton.ButtonText>{labels.hide}</ToggleButton.ButtonText>
|
|
||||||
</ToggleButton.Button>
|
|
||||||
</ToggleButton.Group>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {LABELS} from '@atproto/api'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
|
||||||
import {usePreferencesSetAdultContentMutation} from 'state/queries/preferences'
|
|
||||||
import {
|
|
||||||
DescriptionText,
|
|
||||||
OnboardingControls,
|
|
||||||
TitleText,
|
|
||||||
} from '#/screens/Onboarding/Layout'
|
|
||||||
import {Context} from '#/screens/Onboarding/state'
|
|
||||||
import {AdultContentEnabledPref} from '#/screens/Onboarding/StepModeration/AdultContentEnabledPref'
|
|
||||||
import {ModerationOption} from '#/screens/Onboarding/StepModeration/ModerationOption'
|
|
||||||
import {atoms as a} from '#/alf'
|
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
|
||||||
import {IconCircle} from '#/components/IconCircle'
|
|
||||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
|
||||||
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
|
|
||||||
import {Loader} from '#/components/Loader'
|
|
||||||
|
|
||||||
export function StepModeration() {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {track} = useAnalytics()
|
|
||||||
const {state, dispatch} = React.useContext(Context)
|
|
||||||
const {data: preferences} = usePreferencesQuery()
|
|
||||||
const {mutate, variables} = usePreferencesSetAdultContentMutation()
|
|
||||||
|
|
||||||
// We need to know if the screen is mounted so we know if we want to run entering animations
|
|
||||||
// https://github.com/software-mansion/react-native-reanimated/discussions/2513
|
|
||||||
const isMounted = React.useRef(false)
|
|
||||||
React.useLayoutEffect(() => {
|
|
||||||
isMounted.current = true
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const adultContentEnabled = !!(
|
|
||||||
(variables && variables.enabled) ||
|
|
||||||
(!variables && preferences?.moderationPrefs.adultContentEnabled)
|
|
||||||
)
|
|
||||||
|
|
||||||
const onContinue = React.useCallback(() => {
|
|
||||||
dispatch({type: 'next'})
|
|
||||||
track('OnboardingV2:StepModeration:End')
|
|
||||||
logEvent('onboarding:moderation:nextPressed', {})
|
|
||||||
}, [track, dispatch])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
track('OnboardingV2:StepModeration:Start')
|
|
||||||
}, [track])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[a.align_start]}>
|
|
||||||
<IconCircle icon={EyeSlash} style={[a.mb_2xl]} />
|
|
||||||
|
|
||||||
<TitleText>
|
|
||||||
<Trans>You're in control</Trans>
|
|
||||||
</TitleText>
|
|
||||||
<DescriptionText style={[a.mb_xl]}>
|
|
||||||
<Trans>
|
|
||||||
Select what you want to see (or not see), and we’ll handle the rest.
|
|
||||||
</Trans>
|
|
||||||
</DescriptionText>
|
|
||||||
|
|
||||||
{!preferences ? (
|
|
||||||
<View style={[a.pt_md]}>
|
|
||||||
<Loader size="xl" />
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<AdultContentEnabledPref mutate={mutate} variables={variables} />
|
|
||||||
|
|
||||||
<View style={[a.gap_sm, a.w_full]}>
|
|
||||||
<ModerationOption
|
|
||||||
labelValueDefinition={LABELS.porn}
|
|
||||||
disabled={!adultContentEnabled}
|
|
||||||
/>
|
|
||||||
<ModerationOption
|
|
||||||
labelValueDefinition={LABELS.sexual}
|
|
||||||
disabled={!adultContentEnabled}
|
|
||||||
/>
|
|
||||||
<ModerationOption
|
|
||||||
labelValueDefinition={LABELS['graphic-media']}
|
|
||||||
disabled={!adultContentEnabled}
|
|
||||||
/>
|
|
||||||
<ModerationOption labelValueDefinition={LABELS.nudity} />
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<OnboardingControls.Portal>
|
|
||||||
<Button
|
|
||||||
key={state.activeStep} // remove focus state on nav
|
|
||||||
variant="gradient"
|
|
||||||
color="gradient_sky"
|
|
||||||
size="large"
|
|
||||||
label={_(msg`Continue to next step`)}
|
|
||||||
onPress={onContinue}>
|
|
||||||
<ButtonText>
|
|
||||||
<Trans>Continue</Trans>
|
|
||||||
</ButtonText>
|
|
||||||
<ButtonIcon icon={ChevronRight} position="right" />
|
|
||||||
</Button>
|
|
||||||
</OnboardingControls.Portal>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -92,11 +92,7 @@ export function StepProfile() {
|
||||||
}, [track])
|
}, [track])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// We have an experiment running for redueced onboarding, where this screen shows up as the first in onboarding.
|
requestNotificationsPermission('StartOnboarding')
|
||||||
// We only want to request permissions when that gate is actually active to prevent pollution
|
|
||||||
if (gate('reduced_onboarding_and_home_algo_v2')) {
|
|
||||||
requestNotificationsPermission('StartOnboarding')
|
|
||||||
}
|
|
||||||
}, [gate, requestNotificationsPermission])
|
}, [gate, requestNotificationsPermission])
|
||||||
|
|
||||||
const openPicker = React.useCallback(
|
const openPicker = React.useCallback(
|
||||||
|
|
|
@ -1,188 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {View, ViewStyle} from 'react-native'
|
|
||||||
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
|
||||||
|
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
|
||||||
import {atoms as a, flatten, useTheme} from '#/alf'
|
|
||||||
import {useItemContext} from '#/components/forms/Toggle'
|
|
||||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
|
||||||
import {RichText} from '#/components/RichText'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
|
|
||||||
export function SuggestedAccountCard({
|
|
||||||
profile,
|
|
||||||
moderationOpts,
|
|
||||||
}: {
|
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
|
||||||
moderationOpts: ReturnType<typeof useModerationOpts>
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
|
||||||
const ctx = useItemContext()
|
|
||||||
const moderation = moderateProfile(profile, moderationOpts!)
|
|
||||||
|
|
||||||
const styles = React.useMemo(() => {
|
|
||||||
const light = t.name === 'light'
|
|
||||||
const base: ViewStyle[] = [t.atoms.bg_contrast_50]
|
|
||||||
const hover: ViewStyle[] = [t.atoms.bg_contrast_25]
|
|
||||||
const selected: ViewStyle[] = [
|
|
||||||
{
|
|
||||||
backgroundColor: light ? t.palette.primary_50 : t.palette.primary_950,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const selectedHover: ViewStyle[] = [
|
|
||||||
{
|
|
||||||
backgroundColor: light ? t.palette.primary_25 : t.palette.primary_975,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const checkboxBase: ViewStyle[] = [t.atoms.bg]
|
|
||||||
const checkboxSelected: ViewStyle[] = [
|
|
||||||
{
|
|
||||||
backgroundColor: t.palette.primary_500,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const avatarBase: ViewStyle[] = [t.atoms.bg_contrast_100]
|
|
||||||
const avatarSelected: ViewStyle[] = [
|
|
||||||
{
|
|
||||||
backgroundColor: light ? t.palette.primary_100 : t.palette.primary_900,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return {
|
|
||||||
base,
|
|
||||||
hover: flatten(hover),
|
|
||||||
selected: flatten(selected),
|
|
||||||
selectedHover: flatten(selectedHover),
|
|
||||||
checkboxBase: flatten(checkboxBase),
|
|
||||||
checkboxSelected: flatten(checkboxSelected),
|
|
||||||
avatarBase: flatten(avatarBase),
|
|
||||||
avatarSelected: flatten(avatarSelected),
|
|
||||||
}
|
|
||||||
}, [t])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.p_md,
|
|
||||||
a.pr_lg,
|
|
||||||
a.gap_md,
|
|
||||||
a.rounded_md,
|
|
||||||
styles.base,
|
|
||||||
(ctx.hovered || ctx.focused || ctx.pressed) && styles.hover,
|
|
||||||
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={[a.flex_row, a.flex_1, a.align_center, a.gap_md]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
{width: 48, height: 48},
|
|
||||||
a.relative,
|
|
||||||
a.rounded_full,
|
|
||||||
styles.avatarBase,
|
|
||||||
ctx.selected && styles.avatarSelected,
|
|
||||||
]}>
|
|
||||||
<UserAvatar
|
|
||||||
size={48}
|
|
||||||
avatar={profile.avatar}
|
|
||||||
moderation={moderation.ui('avatar')}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={[a.flex_1]}>
|
|
||||||
<Text style={[a.font_bold, a.text_md, a.pb_xs]} numberOfLines={1}>
|
|
||||||
{profile.displayName}
|
|
||||||
</Text>
|
|
||||||
<Text style={[t.atoms.text_contrast_medium]}>{profile.handle}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.justify_center,
|
|
||||||
a.align_center,
|
|
||||||
a.rounded_sm,
|
|
||||||
styles.checkboxBase,
|
|
||||||
ctx.selected && styles.checkboxSelected,
|
|
||||||
{
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
{ctx.selected && <Check size="sm" fill={t.palette.white} />}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{profile.description && (
|
|
||||||
<>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
{
|
|
||||||
opacity: ctx.selected ? 0.3 : 1,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
},
|
|
||||||
a.w_full,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
ctx.selected && {
|
|
||||||
borderTopColor: t.palette.primary_200,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RichText
|
|
||||||
value={profile.description}
|
|
||||||
disableLinks
|
|
||||||
numberOfLines={2}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SuggestedAccountCardPlaceholder() {
|
|
||||||
const t = useTheme()
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.flex_row,
|
|
||||||
a.justify_between,
|
|
||||||
a.align_center,
|
|
||||||
a.p_md,
|
|
||||||
a.pr_lg,
|
|
||||||
a.gap_xl,
|
|
||||||
a.rounded_md,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
]}>
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_md]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
{width: 48, height: 48},
|
|
||||||
a.relative,
|
|
||||||
a.rounded_full,
|
|
||||||
t.atoms.bg_contrast_100,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<View style={[a.gap_xs]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
{width: 100, height: 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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,210 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {AppBskyActorDefs} from '@atproto/api'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {capitalize} from '#/lib/strings/capitalize'
|
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
|
||||||
import {useProfilesQuery} from '#/state/queries/profile'
|
|
||||||
import {
|
|
||||||
DescriptionText,
|
|
||||||
OnboardingControls,
|
|
||||||
TitleText,
|
|
||||||
} from '#/screens/Onboarding/Layout'
|
|
||||||
import {Context} from '#/screens/Onboarding/state'
|
|
||||||
import {
|
|
||||||
SuggestedAccountCard,
|
|
||||||
SuggestedAccountCardPlaceholder,
|
|
||||||
} from '#/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard'
|
|
||||||
import {aggregateInterestItems} from '#/screens/Onboarding/util'
|
|
||||||
import {atoms as a, useBreakpoints} from '#/alf'
|
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
|
||||||
import {IconCircle} from '#/components/IconCircle'
|
|
||||||
import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
|
|
||||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
|
||||||
import {Loader} from '#/components/Loader'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
|
|
||||||
export function Inner({
|
|
||||||
profiles,
|
|
||||||
onSelect,
|
|
||||||
moderationOpts,
|
|
||||||
}: {
|
|
||||||
profiles: AppBskyActorDefs.ProfileViewDetailed[]
|
|
||||||
onSelect: (dids: string[]) => void
|
|
||||||
moderationOpts: ReturnType<typeof useModerationOpts>
|
|
||||||
}) {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const [dids, setDids] = React.useState<string[]>(profiles.map(p => p.did))
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
onSelect(dids)
|
|
||||||
}, [dids, onSelect])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Toggle.Group
|
|
||||||
values={dids}
|
|
||||||
onChange={setDids}
|
|
||||||
label={_(msg`Select some accounts below to follow`)}>
|
|
||||||
<View style={[a.gap_md]}>
|
|
||||||
{profiles.map(profile => (
|
|
||||||
<Toggle.Item
|
|
||||||
key={profile.did}
|
|
||||||
name={profile.did}
|
|
||||||
label={_(msg`Follow ${profile.handle}`)}>
|
|
||||||
<SuggestedAccountCard
|
|
||||||
profile={profile}
|
|
||||||
moderationOpts={moderationOpts}
|
|
||||||
/>
|
|
||||||
</Toggle.Item>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</Toggle.Group>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StepSuggestedAccounts() {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {gtMobile} = useBreakpoints()
|
|
||||||
const {track} = useAnalytics()
|
|
||||||
const {state, dispatch, interestsDisplayNames} = React.useContext(Context)
|
|
||||||
const suggestedDids = React.useMemo(() => {
|
|
||||||
return aggregateInterestItems(
|
|
||||||
state.interestsStepResults.selectedInterests,
|
|
||||||
state.interestsStepResults.apiResponse.suggestedAccountDids,
|
|
||||||
state.interestsStepResults.apiResponse.suggestedAccountDids.default || [],
|
|
||||||
)
|
|
||||||
}, [state.interestsStepResults])
|
|
||||||
const moderationOpts = useModerationOpts()
|
|
||||||
const {
|
|
||||||
isLoading: isProfilesLoading,
|
|
||||||
isError,
|
|
||||||
data,
|
|
||||||
error,
|
|
||||||
} = useProfilesQuery({
|
|
||||||
handles: suggestedDids,
|
|
||||||
})
|
|
||||||
const [dids, setDids] = React.useState<string[]>([])
|
|
||||||
const [saving, setSaving] = React.useState(false)
|
|
||||||
|
|
||||||
const interestsText = React.useMemo(() => {
|
|
||||||
const i = state.interestsStepResults.selectedInterests.map(
|
|
||||||
i => interestsDisplayNames[i] || capitalize(i),
|
|
||||||
)
|
|
||||||
return i.join(', ')
|
|
||||||
}, [state.interestsStepResults.selectedInterests, interestsDisplayNames])
|
|
||||||
|
|
||||||
const handleContinue = React.useCallback(async () => {
|
|
||||||
setSaving(true)
|
|
||||||
|
|
||||||
if (dids.length) {
|
|
||||||
dispatch({type: 'setSuggestedAccountsStepResults', accountDids: dids})
|
|
||||||
}
|
|
||||||
|
|
||||||
setSaving(false)
|
|
||||||
dispatch({type: 'next'})
|
|
||||||
track('OnboardingV2:StepSuggestedAccounts:End', {
|
|
||||||
selectedAccountsLength: dids.length,
|
|
||||||
})
|
|
||||||
logEvent('onboarding:suggestedAccounts:nextPressed', {
|
|
||||||
selectedAccountsLength: dids.length,
|
|
||||||
skipped: false,
|
|
||||||
})
|
|
||||||
}, [dids, setSaving, dispatch, track])
|
|
||||||
|
|
||||||
const handleSkip = React.useCallback(() => {
|
|
||||||
// if a user comes back and clicks skip, erase follows
|
|
||||||
dispatch({type: 'setSuggestedAccountsStepResults', accountDids: []})
|
|
||||||
dispatch({type: 'next'})
|
|
||||||
logEvent('onboarding:suggestedAccounts:nextPressed', {
|
|
||||||
selectedAccountsLength: 0,
|
|
||||||
skipped: true,
|
|
||||||
})
|
|
||||||
}, [dispatch])
|
|
||||||
|
|
||||||
const isLoading = isProfilesLoading && moderationOpts
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
track('OnboardingV2:StepSuggestedAccounts:Start')
|
|
||||||
}, [track])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[a.align_start]}>
|
|
||||||
<IconCircle icon={At} style={[a.mb_2xl]} />
|
|
||||||
|
|
||||||
<TitleText>
|
|
||||||
<Trans>Here are some accounts for you to follow</Trans>
|
|
||||||
</TitleText>
|
|
||||||
<DescriptionText>
|
|
||||||
{state.interestsStepResults.selectedInterests.length ? (
|
|
||||||
<Trans>Based on your interest in {interestsText}</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>These are popular accounts you might like:</Trans>
|
|
||||||
)}
|
|
||||||
</DescriptionText>
|
|
||||||
|
|
||||||
<View style={[a.w_full, a.pt_xl]}>
|
|
||||||
{isLoading ? (
|
|
||||||
<View style={[a.gap_md]}>
|
|
||||||
{Array(10)
|
|
||||||
.fill(0)
|
|
||||||
.map((_, i) => (
|
|
||||||
<SuggestedAccountCardPlaceholder key={i} />
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
) : isError || !data ? (
|
|
||||||
<Text>{error?.toString()}</Text>
|
|
||||||
) : (
|
|
||||||
<Inner
|
|
||||||
profiles={data.profiles}
|
|
||||||
onSelect={setDids}
|
|
||||||
moderationOpts={moderationOpts}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<OnboardingControls.Portal>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.gap_md,
|
|
||||||
gtMobile ? {flexDirection: 'row-reverse'} : a.flex_col,
|
|
||||||
]}>
|
|
||||||
<Button
|
|
||||||
disabled={dids.length === 0}
|
|
||||||
variant="gradient"
|
|
||||||
color="gradient_sky"
|
|
||||||
size="large"
|
|
||||||
label={_(
|
|
||||||
msg`Follow selected accounts and continue to the next step`,
|
|
||||||
)}
|
|
||||||
onPress={handleContinue}>
|
|
||||||
<ButtonText>
|
|
||||||
{dids.length === 20 ? (
|
|
||||||
<Trans>Follow All</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>Follow</Trans>
|
|
||||||
)}
|
|
||||||
</ButtonText>
|
|
||||||
<ButtonIcon icon={saving ? Loader : Plus} position="right" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="solid"
|
|
||||||
color="secondary"
|
|
||||||
size="large"
|
|
||||||
label={_(
|
|
||||||
msg`Continue to the next step without following any accounts`,
|
|
||||||
)}
|
|
||||||
onPress={handleSkip}>
|
|
||||||
<ButtonText>
|
|
||||||
<Trans>Skip</Trans>
|
|
||||||
</ButtonText>
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</OnboardingControls.Portal>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,125 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
|
||||||
import {capitalize} from '#/lib/strings/capitalize'
|
|
||||||
import {IS_TEST_USER} from 'lib/constants'
|
|
||||||
import {useSession} from 'state/session'
|
|
||||||
import {
|
|
||||||
DescriptionText,
|
|
||||||
OnboardingControls,
|
|
||||||
TitleText,
|
|
||||||
} from '#/screens/Onboarding/Layout'
|
|
||||||
import {Context} from '#/screens/Onboarding/state'
|
|
||||||
import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard'
|
|
||||||
import {aggregateInterestItems} from '#/screens/Onboarding/util'
|
|
||||||
import {atoms as a} from '#/alf'
|
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
|
||||||
import {IconCircle} from '#/components/IconCircle'
|
|
||||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
|
||||||
import {ListMagnifyingGlass_Stroke2_Corner0_Rounded as ListMagnifyingGlass} from '#/components/icons/ListMagnifyingGlass'
|
|
||||||
import {Loader} from '#/components/Loader'
|
|
||||||
|
|
||||||
export function StepTopicalFeeds() {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {track} = useAnalytics()
|
|
||||||
const {currentAccount} = useSession()
|
|
||||||
const {state, dispatch, interestsDisplayNames} = React.useContext(Context)
|
|
||||||
const [selectedFeedUris, setSelectedFeedUris] = React.useState<string[]>([])
|
|
||||||
const [saving, setSaving] = React.useState(false)
|
|
||||||
const suggestedFeedUris = React.useMemo(() => {
|
|
||||||
if (IS_TEST_USER(currentAccount?.handle)) return []
|
|
||||||
return aggregateInterestItems(
|
|
||||||
state.interestsStepResults.selectedInterests,
|
|
||||||
state.interestsStepResults.apiResponse.suggestedFeedUris,
|
|
||||||
state.interestsStepResults.apiResponse.suggestedFeedUris.default || [],
|
|
||||||
).slice(0, 10)
|
|
||||||
}, [
|
|
||||||
currentAccount?.handle,
|
|
||||||
state.interestsStepResults.apiResponse.suggestedFeedUris,
|
|
||||||
state.interestsStepResults.selectedInterests,
|
|
||||||
])
|
|
||||||
|
|
||||||
const interestsText = React.useMemo(() => {
|
|
||||||
const i = state.interestsStepResults.selectedInterests.map(
|
|
||||||
i => interestsDisplayNames[i] || capitalize(i),
|
|
||||||
)
|
|
||||||
return i.join(', ')
|
|
||||||
}, [state.interestsStepResults.selectedInterests, interestsDisplayNames])
|
|
||||||
|
|
||||||
const saveFeeds = React.useCallback(async () => {
|
|
||||||
setSaving(true)
|
|
||||||
|
|
||||||
dispatch({type: 'setTopicalFeedsStepResults', feedUris: selectedFeedUris})
|
|
||||||
|
|
||||||
setSaving(false)
|
|
||||||
dispatch({type: 'next'})
|
|
||||||
track('OnboardingV2:StepTopicalFeeds:End', {
|
|
||||||
selectedFeeds: selectedFeedUris,
|
|
||||||
selectedFeedsLength: selectedFeedUris.length,
|
|
||||||
})
|
|
||||||
logEvent('onboarding:topicalFeeds:nextPressed', {
|
|
||||||
selectedFeeds: selectedFeedUris,
|
|
||||||
selectedFeedsLength: selectedFeedUris.length,
|
|
||||||
})
|
|
||||||
}, [selectedFeedUris, dispatch, track])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
track('OnboardingV2:StepTopicalFeeds:Start')
|
|
||||||
}, [track])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[a.align_start]}>
|
|
||||||
<IconCircle icon={ListMagnifyingGlass} style={[a.mb_2xl]} />
|
|
||||||
|
|
||||||
<TitleText>
|
|
||||||
<Trans>Feeds can be topical as well!</Trans>
|
|
||||||
</TitleText>
|
|
||||||
<DescriptionText>
|
|
||||||
{state.interestsStepResults.selectedInterests.length ? (
|
|
||||||
<Trans>
|
|
||||||
Here are some topical feeds based on your interests: {interestsText}
|
|
||||||
. You can choose to follow as many as you like.
|
|
||||||
</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>
|
|
||||||
Here are some popular topical feeds. You can choose to follow as
|
|
||||||
many as you like.
|
|
||||||
</Trans>
|
|
||||||
)}
|
|
||||||
</DescriptionText>
|
|
||||||
|
|
||||||
<View style={[a.w_full, a.pb_2xl, a.pt_2xl]}>
|
|
||||||
<Toggle.Group
|
|
||||||
values={selectedFeedUris}
|
|
||||||
onChange={setSelectedFeedUris}
|
|
||||||
label={_(msg`Select topical feeds to follow from the list below`)}>
|
|
||||||
<View style={[a.gap_md]}>
|
|
||||||
{suggestedFeedUris.map(uri => (
|
|
||||||
<FeedCard key={uri} config={{default: false, uri}} />
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</Toggle.Group>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<OnboardingControls.Portal>
|
|
||||||
<Button
|
|
||||||
key={state.activeStep} // remove focus state on nav
|
|
||||||
variant="gradient"
|
|
||||||
color="gradient_sky"
|
|
||||||
size="large"
|
|
||||||
label={_(msg`Continue to next step`)}
|
|
||||||
onPress={saveFeeds}>
|
|
||||||
<ButtonText>
|
|
||||||
<Trans>Continue</Trans>
|
|
||||||
</ButtonText>
|
|
||||||
<ButtonIcon icon={saving ? Loader : ChevronRight} position="right" />
|
|
||||||
</Button>
|
|
||||||
</OnboardingControls.Portal>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -2,33 +2,18 @@ import React from 'react'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useGate} from '#/lib/statsig/statsig'
|
|
||||||
import {Layout, OnboardingControls} from '#/screens/Onboarding/Layout'
|
import {Layout, OnboardingControls} from '#/screens/Onboarding/Layout'
|
||||||
import {
|
import {Context, initialState, reducer} from '#/screens/Onboarding/state'
|
||||||
Context,
|
|
||||||
initialState,
|
|
||||||
initialStateReduced,
|
|
||||||
reducer,
|
|
||||||
reducerReduced,
|
|
||||||
} from '#/screens/Onboarding/state'
|
|
||||||
import {StepAlgoFeeds} from '#/screens/Onboarding/StepAlgoFeeds'
|
|
||||||
import {StepFinished} from '#/screens/Onboarding/StepFinished'
|
import {StepFinished} from '#/screens/Onboarding/StepFinished'
|
||||||
import {StepFollowingFeed} from '#/screens/Onboarding/StepFollowingFeed'
|
|
||||||
import {StepInterests} from '#/screens/Onboarding/StepInterests'
|
import {StepInterests} from '#/screens/Onboarding/StepInterests'
|
||||||
import {StepModeration} from '#/screens/Onboarding/StepModeration'
|
|
||||||
import {StepProfile} from '#/screens/Onboarding/StepProfile'
|
import {StepProfile} from '#/screens/Onboarding/StepProfile'
|
||||||
import {StepSuggestedAccounts} from '#/screens/Onboarding/StepSuggestedAccounts'
|
|
||||||
import {StepTopicalFeeds} from '#/screens/Onboarding/StepTopicalFeeds'
|
|
||||||
import {Portal} from '#/components/Portal'
|
import {Portal} from '#/components/Portal'
|
||||||
|
|
||||||
export function Onboarding() {
|
export function Onboarding() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const gate = useGate()
|
const [state, dispatch] = React.useReducer(reducer, {
|
||||||
const isReducedOnboardingEnabled = gate('reduced_onboarding_and_home_algo_v2')
|
...initialState,
|
||||||
const [state, dispatch] = React.useReducer(
|
})
|
||||||
isReducedOnboardingEnabled ? reducerReduced : reducer,
|
|
||||||
isReducedOnboardingEnabled ? {...initialStateReduced} : {...initialState},
|
|
||||||
)
|
|
||||||
|
|
||||||
const interestsDisplayNames = React.useMemo(() => {
|
const interestsDisplayNames = React.useMemo(() => {
|
||||||
return {
|
return {
|
||||||
|
@ -68,13 +53,6 @@ export function Onboarding() {
|
||||||
<Layout>
|
<Layout>
|
||||||
{state.activeStep === 'profile' && <StepProfile />}
|
{state.activeStep === 'profile' && <StepProfile />}
|
||||||
{state.activeStep === 'interests' && <StepInterests />}
|
{state.activeStep === 'interests' && <StepInterests />}
|
||||||
{state.activeStep === 'suggestedAccounts' && (
|
|
||||||
<StepSuggestedAccounts />
|
|
||||||
)}
|
|
||||||
{state.activeStep === 'followingFeed' && <StepFollowingFeed />}
|
|
||||||
{state.activeStep === 'algoFeeds' && <StepAlgoFeeds />}
|
|
||||||
{state.activeStep === 'topicalFeeds' && <StepTopicalFeeds />}
|
|
||||||
{state.activeStep === 'moderation' && <StepModeration />}
|
|
||||||
{state.activeStep === 'finished' && <StepFinished />}
|
{state.activeStep === 'finished' && <StepFinished />}
|
||||||
</Layout>
|
</Layout>
|
||||||
</Context.Provider>
|
</Context.Provider>
|
||||||
|
|
|
@ -6,31 +6,13 @@ import {AvatarColor, Emoji} from '#/screens/Onboarding/StepProfile/types'
|
||||||
export type OnboardingState = {
|
export type OnboardingState = {
|
||||||
hasPrev: boolean
|
hasPrev: boolean
|
||||||
totalSteps: number
|
totalSteps: number
|
||||||
activeStep:
|
activeStep: 'profile' | 'interests' | 'finished'
|
||||||
| 'profile'
|
|
||||||
| 'interests'
|
|
||||||
| 'suggestedAccounts'
|
|
||||||
| 'followingFeed'
|
|
||||||
| 'algoFeeds'
|
|
||||||
| 'topicalFeeds'
|
|
||||||
| 'moderation'
|
|
||||||
| 'profile'
|
|
||||||
| 'finished'
|
|
||||||
activeStepIndex: number
|
activeStepIndex: number
|
||||||
|
|
||||||
interestsStepResults: {
|
interestsStepResults: {
|
||||||
selectedInterests: string[]
|
selectedInterests: string[]
|
||||||
apiResponse: ApiResponseMap
|
apiResponse: ApiResponseMap
|
||||||
}
|
}
|
||||||
suggestedAccountsStepResults: {
|
|
||||||
accountDids: string[]
|
|
||||||
}
|
|
||||||
algoFeedsStepResults: {
|
|
||||||
feedUris: string[]
|
|
||||||
}
|
|
||||||
topicalFeedsStepResults: {
|
|
||||||
feedUris: string[]
|
|
||||||
}
|
|
||||||
profileStepResults: {
|
profileStepResults: {
|
||||||
isCreatedAvatar: boolean
|
isCreatedAvatar: boolean
|
||||||
image?: {
|
image?: {
|
||||||
|
@ -64,18 +46,6 @@ export type OnboardingAction =
|
||||||
selectedInterests: string[]
|
selectedInterests: string[]
|
||||||
apiResponse: ApiResponseMap
|
apiResponse: ApiResponseMap
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
type: 'setSuggestedAccountsStepResults'
|
|
||||||
accountDids: string[]
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'setAlgoFeedsStepResults'
|
|
||||||
feedUris: string[]
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'setTopicalFeedsStepResults'
|
|
||||||
feedUris: string[]
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
type: 'setProfileStepResults'
|
type: 'setProfileStepResults'
|
||||||
isCreatedAvatar: boolean
|
isCreatedAvatar: boolean
|
||||||
|
@ -98,37 +68,6 @@ export type ApiResponseMap = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialState: OnboardingState = {
|
|
||||||
hasPrev: false,
|
|
||||||
totalSteps: 7,
|
|
||||||
activeStep: 'interests',
|
|
||||||
activeStepIndex: 1,
|
|
||||||
|
|
||||||
interestsStepResults: {
|
|
||||||
selectedInterests: [],
|
|
||||||
apiResponse: {
|
|
||||||
interests: [],
|
|
||||||
suggestedAccountDids: {},
|
|
||||||
suggestedFeedUris: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
suggestedAccountsStepResults: {
|
|
||||||
accountDids: [],
|
|
||||||
},
|
|
||||||
algoFeedsStepResults: {
|
|
||||||
feedUris: [],
|
|
||||||
},
|
|
||||||
topicalFeedsStepResults: {
|
|
||||||
feedUris: [],
|
|
||||||
},
|
|
||||||
profileStepResults: {
|
|
||||||
isCreatedAvatar: false,
|
|
||||||
image: undefined,
|
|
||||||
imageUri: '',
|
|
||||||
imageMime: '',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const INTEREST_TO_DISPLAY_NAME_DEFAULTS: {
|
export const INTEREST_TO_DISPLAY_NAME_DEFAULTS: {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
} = {
|
} = {
|
||||||
|
@ -156,125 +95,7 @@ export const INTEREST_TO_DISPLAY_NAME_DEFAULTS: {
|
||||||
cooking: 'Cooking',
|
cooking: 'Cooking',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Context = React.createContext<{
|
export const initialState: OnboardingState = {
|
||||||
state: OnboardingState
|
|
||||||
dispatch: React.Dispatch<OnboardingAction>
|
|
||||||
interestsDisplayNames: {[key: string]: string}
|
|
||||||
}>({
|
|
||||||
state: {...initialState},
|
|
||||||
dispatch: () => {},
|
|
||||||
interestsDisplayNames: INTEREST_TO_DISPLAY_NAME_DEFAULTS,
|
|
||||||
})
|
|
||||||
|
|
||||||
export function reducer(
|
|
||||||
s: OnboardingState,
|
|
||||||
a: OnboardingAction,
|
|
||||||
): OnboardingState {
|
|
||||||
let next = {...s}
|
|
||||||
|
|
||||||
switch (a.type) {
|
|
||||||
case 'next': {
|
|
||||||
if (s.activeStep === 'interests') {
|
|
||||||
next.activeStep = 'suggestedAccounts'
|
|
||||||
next.activeStepIndex = 2
|
|
||||||
} else if (s.activeStep === 'suggestedAccounts') {
|
|
||||||
next.activeStep = 'followingFeed'
|
|
||||||
next.activeStepIndex = 3
|
|
||||||
} else if (s.activeStep === 'followingFeed') {
|
|
||||||
next.activeStep = 'algoFeeds'
|
|
||||||
next.activeStepIndex = 4
|
|
||||||
} else if (s.activeStep === 'algoFeeds') {
|
|
||||||
next.activeStep = 'topicalFeeds'
|
|
||||||
next.activeStepIndex = 5
|
|
||||||
} else if (s.activeStep === 'topicalFeeds') {
|
|
||||||
next.activeStep = 'moderation'
|
|
||||||
next.activeStepIndex = 6
|
|
||||||
} else if (s.activeStep === 'moderation') {
|
|
||||||
next.activeStep = 'finished'
|
|
||||||
next.activeStepIndex = 7
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'prev': {
|
|
||||||
if (s.activeStep === 'suggestedAccounts') {
|
|
||||||
next.activeStep = 'interests'
|
|
||||||
next.activeStepIndex = 1
|
|
||||||
} else if (s.activeStep === 'followingFeed') {
|
|
||||||
next.activeStep = 'suggestedAccounts'
|
|
||||||
next.activeStepIndex = 2
|
|
||||||
} else if (s.activeStep === 'algoFeeds') {
|
|
||||||
next.activeStep = 'followingFeed'
|
|
||||||
next.activeStepIndex = 3
|
|
||||||
} else if (s.activeStep === 'topicalFeeds') {
|
|
||||||
next.activeStep = 'algoFeeds'
|
|
||||||
next.activeStepIndex = 4
|
|
||||||
} else if (s.activeStep === 'moderation') {
|
|
||||||
next.activeStep = 'topicalFeeds'
|
|
||||||
next.activeStepIndex = 5
|
|
||||||
} else if (s.activeStep === 'finished') {
|
|
||||||
next.activeStep = 'moderation'
|
|
||||||
next.activeStepIndex = 6
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'finish': {
|
|
||||||
next = initialState
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'setInterestsStepResults': {
|
|
||||||
next.interestsStepResults = {
|
|
||||||
selectedInterests: a.selectedInterests,
|
|
||||||
apiResponse: a.apiResponse,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'setSuggestedAccountsStepResults': {
|
|
||||||
next.suggestedAccountsStepResults = {
|
|
||||||
accountDids: next.suggestedAccountsStepResults.accountDids.concat(
|
|
||||||
a.accountDids,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'setAlgoFeedsStepResults': {
|
|
||||||
next.algoFeedsStepResults = {
|
|
||||||
feedUris: a.feedUris,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'setTopicalFeedsStepResults': {
|
|
||||||
next.topicalFeedsStepResults = {
|
|
||||||
feedUris: next.topicalFeedsStepResults.feedUris.concat(a.feedUris),
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
...next,
|
|
||||||
hasPrev: next.activeStep !== 'interests',
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`onboarding`, {
|
|
||||||
hasPrev: state.hasPrev,
|
|
||||||
activeStep: state.activeStep,
|
|
||||||
activeStepIndex: state.activeStepIndex,
|
|
||||||
interestsStepResults: {
|
|
||||||
selectedInterests: state.interestsStepResults.selectedInterests,
|
|
||||||
},
|
|
||||||
suggestedAccountsStepResults: state.suggestedAccountsStepResults,
|
|
||||||
algoFeedsStepResults: state.algoFeedsStepResults,
|
|
||||||
topicalFeedsStepResults: state.topicalFeedsStepResults,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (s.activeStep !== state.activeStep) {
|
|
||||||
logger.debug(`onboarding: step changed`, {activeStep: state.activeStep})
|
|
||||||
}
|
|
||||||
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initialStateReduced: OnboardingState = {
|
|
||||||
hasPrev: false,
|
hasPrev: false,
|
||||||
totalSteps: 3,
|
totalSteps: 3,
|
||||||
activeStep: 'profile',
|
activeStep: 'profile',
|
||||||
|
@ -288,15 +109,6 @@ export const initialStateReduced: OnboardingState = {
|
||||||
suggestedFeedUris: {},
|
suggestedFeedUris: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
suggestedAccountsStepResults: {
|
|
||||||
accountDids: [],
|
|
||||||
},
|
|
||||||
algoFeedsStepResults: {
|
|
||||||
feedUris: [],
|
|
||||||
},
|
|
||||||
topicalFeedsStepResults: {
|
|
||||||
feedUris: [],
|
|
||||||
},
|
|
||||||
profileStepResults: {
|
profileStepResults: {
|
||||||
isCreatedAvatar: false,
|
isCreatedAvatar: false,
|
||||||
image: undefined,
|
image: undefined,
|
||||||
|
@ -305,7 +117,17 @@ export const initialStateReduced: OnboardingState = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reducerReduced(
|
export const Context = React.createContext<{
|
||||||
|
state: OnboardingState
|
||||||
|
dispatch: React.Dispatch<OnboardingAction>
|
||||||
|
interestsDisplayNames: {[key: string]: string}
|
||||||
|
}>({
|
||||||
|
state: {...initialState},
|
||||||
|
dispatch: () => {},
|
||||||
|
interestsDisplayNames: INTEREST_TO_DISPLAY_NAME_DEFAULTS,
|
||||||
|
})
|
||||||
|
|
||||||
|
export function reducer(
|
||||||
s: OnboardingState,
|
s: OnboardingState,
|
||||||
a: OnboardingAction,
|
a: OnboardingAction,
|
||||||
): OnboardingState {
|
): OnboardingState {
|
||||||
|
@ -333,7 +155,7 @@ export function reducerReduced(
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'finish': {
|
case 'finish': {
|
||||||
next = initialStateReduced
|
next = initialState
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'setInterestsStepResults': {
|
case 'setInterestsStepResults': {
|
||||||
|
@ -343,15 +165,6 @@ export function reducerReduced(
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'setSuggestedAccountsStepResults': {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'setAlgoFeedsStepResults': {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'setTopicalFeedsStepResults': {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'setProfileStepResults': {
|
case 'setProfileStepResults': {
|
||||||
next.profileStepResults = {
|
next.profileStepResults = {
|
||||||
isCreatedAvatar: a.isCreatedAvatar,
|
isCreatedAvatar: a.isCreatedAvatar,
|
||||||
|
@ -376,9 +189,6 @@ export function reducerReduced(
|
||||||
interestsStepResults: {
|
interestsStepResults: {
|
||||||
selectedInterests: state.interestsStepResults.selectedInterests,
|
selectedInterests: state.interestsStepResults.selectedInterests,
|
||||||
},
|
},
|
||||||
suggestedAccountsStepResults: state.suggestedAccountsStepResults,
|
|
||||||
algoFeedsStepResults: state.algoFeedsStepResults,
|
|
||||||
topicalFeedsStepResults: state.topicalFeedsStepResults,
|
|
||||||
profileStepResults: state.profileStepResults,
|
profileStepResults: state.profileStepResults,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -5,66 +5,6 @@ import {
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
|
||||||
import {until} from '#/lib/async/until'
|
import {until} from '#/lib/async/until'
|
||||||
import {PRIMARY_FEEDS} from './StepAlgoFeeds'
|
|
||||||
|
|
||||||
function shuffle(array: any) {
|
|
||||||
let currentIndex = array.length,
|
|
||||||
randomIndex
|
|
||||||
|
|
||||||
// While there remain elements to shuffle.
|
|
||||||
while (currentIndex > 0) {
|
|
||||||
// Pick a remaining element.
|
|
||||||
randomIndex = Math.floor(Math.random() * currentIndex)
|
|
||||||
currentIndex--
|
|
||||||
|
|
||||||
// And swap it with the current element.
|
|
||||||
;[array[currentIndex], array[randomIndex]] = [
|
|
||||||
array[randomIndex],
|
|
||||||
array[currentIndex],
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return array
|
|
||||||
}
|
|
||||||
|
|
||||||
export function aggregateInterestItems(
|
|
||||||
interests: string[],
|
|
||||||
map: {[key: string]: string[]},
|
|
||||||
fallbackItems: string[],
|
|
||||||
) {
|
|
||||||
const selected = interests.length
|
|
||||||
const all = interests
|
|
||||||
.map(i => {
|
|
||||||
// suggestions from server
|
|
||||||
const rawSuggestions = map[i]
|
|
||||||
|
|
||||||
// safeguard against a missing interest->suggestion mapping
|
|
||||||
if (!rawSuggestions || !rawSuggestions.length) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const suggestions = shuffle(rawSuggestions)
|
|
||||||
|
|
||||||
if (selected === 1) {
|
|
||||||
return suggestions // return all
|
|
||||||
} else if (selected === 2) {
|
|
||||||
return suggestions.slice(0, 5) // return 5
|
|
||||||
} else {
|
|
||||||
return suggestions.slice(0, 3) // return 3
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.flat()
|
|
||||||
// dedupe suggestions
|
|
||||||
const results = Array.from(new Set(all))
|
|
||||||
|
|
||||||
// backfill
|
|
||||||
if (results.length < 20) {
|
|
||||||
results.push(...shuffle(fallbackItems))
|
|
||||||
}
|
|
||||||
|
|
||||||
// dedupe and return 20
|
|
||||||
return Array.from(new Set(results)).slice(0, 20)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function bulkWriteFollows(agent: BskyAgent, dids: string[]) {
|
export async function bulkWriteFollows(agent: BskyAgent, dids: string[]) {
|
||||||
const session = agent.session
|
const session = agent.session
|
||||||
|
@ -109,19 +49,3 @@ async function whenFollowsIndexed(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Kinda hacky, but we want Discover to appear as the first pinned
|
|
||||||
* feed after Following
|
|
||||||
*/
|
|
||||||
export function sortPrimaryAlgorithmFeeds(uris: string[]) {
|
|
||||||
return uris.sort((a, b) => {
|
|
||||||
if (a === PRIMARY_FEEDS[0]?.uri) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if (b === PRIMARY_FEEDS[0]?.uri) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return a.localeCompare(b)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React from 'react'
|
||||||
import {LogBox, Pressable, View} from 'react-native'
|
import {LogBox, Pressable, View} from 'react-native'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useDangerousSetGate} from '#/lib/statsig/statsig'
|
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {useSessionApi} from '#/state/session'
|
import {useSessionApi} from '#/state/session'
|
||||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
|
@ -25,7 +24,6 @@ export function TestCtrls() {
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
const onboardingDispatch = useOnboardingDispatch()
|
const onboardingDispatch = useOnboardingDispatch()
|
||||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||||
const setGate = useDangerousSetGate()
|
|
||||||
const onPressSignInAlice = async () => {
|
const onPressSignInAlice = async () => {
|
||||||
await login(
|
await login(
|
||||||
{
|
{
|
||||||
|
@ -117,8 +115,6 @@ export function TestCtrls() {
|
||||||
<Pressable
|
<Pressable
|
||||||
testID="e2eStartOnboarding"
|
testID="e2eStartOnboarding"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
// TODO remove when experiment is over
|
|
||||||
setGate('reduced_onboarding_and_home_algo_v2', true)
|
|
||||||
onboardingDispatch({type: 'start'})
|
onboardingDispatch({type: 'start'})
|
||||||
}}
|
}}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
|
@ -128,8 +124,6 @@ export function TestCtrls() {
|
||||||
<Pressable
|
<Pressable
|
||||||
testID="e2eStartLongboarding"
|
testID="e2eStartLongboarding"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
// TODO remove when experiment is over
|
|
||||||
setGate('reduced_onboarding_and_home_algo_v2', false)
|
|
||||||
onboardingDispatch({type: 'start'})
|
onboardingDispatch({type: 'start'})
|
||||||
}}
|
}}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
|
|
Loading…
Reference in New Issue