diff options
-rw-r--r-- | src/lib/statsig/gates.ts | 1 | ||||
-rw-r--r-- | src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx | 378 | ||||
-rw-r--r-- | src/screens/Onboarding/StepAlgoFeeds/index.tsx | 168 | ||||
-rw-r--r-- | src/screens/Onboarding/StepFinished.tsx | 95 | ||||
-rw-r--r-- | src/screens/Onboarding/StepFollowingFeed.tsx | 161 | ||||
-rw-r--r-- | src/screens/Onboarding/StepInterests/index.tsx | 11 | ||||
-rw-r--r-- | src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx | 131 | ||||
-rw-r--r-- | src/screens/Onboarding/StepModeration/ModerationOption.tsx | 99 | ||||
-rw-r--r-- | src/screens/Onboarding/StepModeration/index.tsx | 110 | ||||
-rw-r--r-- | src/screens/Onboarding/StepProfile/index.tsx | 6 | ||||
-rw-r--r-- | src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx | 188 | ||||
-rw-r--r-- | src/screens/Onboarding/StepSuggestedAccounts/index.tsx | 210 | ||||
-rw-r--r-- | src/screens/Onboarding/StepTopicalFeeds.tsx | 125 | ||||
-rw-r--r-- | src/screens/Onboarding/index.tsx | 30 | ||||
-rw-r--r-- | src/screens/Onboarding/state.ts | 218 | ||||
-rw-r--r-- | src/screens/Onboarding/util.ts | 76 | ||||
-rw-r--r-- | src/view/com/testing/TestCtrls.e2e.tsx | 6 |
17 files changed, 27 insertions, 1986 deletions
diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 81e49e151..c572c0721 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -1,5 +1,4 @@ export type Gate = // Keep this alphabetic please. - | 'reduced_onboarding_and_home_algo_v2' | 'request_notifications_permission_after_onboarding' | 'show_follow_back_label_v2' diff --git a/src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx b/src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx deleted file mode 100644 index 0aa063faa..000000000 --- a/src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx +++ /dev/null @@ -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> - ) -} diff --git a/src/screens/Onboarding/StepAlgoFeeds/index.tsx b/src/screens/Onboarding/StepAlgoFeeds/index.tsx deleted file mode 100644 index 19bb40104..000000000 --- a/src/screens/Onboarding/StepAlgoFeeds/index.tsx +++ /dev/null @@ -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> - ) -} diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx index 9658cfe15..b8a21680b 100644 --- a/src/screens/Onboarding/StepFinished.tsx +++ b/src/screens/Onboarding/StepFinished.tsx @@ -1,19 +1,14 @@ import React from 'react' import {View} from 'react-native' -import {TID} from '@atproto/common-web' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' import {useAnalytics} from '#/lib/analytics/analytics' -import {BSKY_APP_ACCOUNT_DID, IS_PROD_SERVICE} from '#/lib/constants' -import {DISCOVER_SAVED_FEED, TIMELINE_SAVED_FEED} from '#/lib/constants' -import {logEvent, useGate} from '#/lib/statsig/statsig' +import {BSKY_APP_ACCOUNT_DID} from '#/lib/constants' +import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' -import { - preferencesQueryKey, - useOverwriteSavedFeedsMutation, -} from '#/state/queries/preferences' +import {preferencesQueryKey} from '#/state/queries/preferences' import {RQKEY as profileRQKey} from '#/state/queries/profile' import {useAgent} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' @@ -24,10 +19,7 @@ import { TitleText, } from '#/screens/Onboarding/Layout' import {Context} from '#/screens/Onboarding/state' -import { - bulkWriteFollows, - sortPrimaryAlgorithmFeeds, -} from '#/screens/Onboarding/util' +import {bulkWriteFollows} from '#/screens/Onboarding/util' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {IconCircle} from '#/components/IconCircle' @@ -45,83 +37,21 @@ export function StepFinished() { const {state, dispatch} = React.useContext(Context) const onboardDispatch = useOnboardingDispatch() const [saving, setSaving] = React.useState(false) - const {mutateAsync: overwriteSavedFeeds} = useOverwriteSavedFeedsMutation() const queryClient = useQueryClient() const agent = useAgent() - const gate = useGate() const finishOnboarding = React.useCallback(async () => { setSaving(true) - // TODO uncomment - const { - interestsStepResults, - suggestedAccountsStepResults, - algoFeedsStepResults, - topicalFeedsStepResults, - profileStepResults, - } = state + const {interestsStepResults, profileStepResults} = state const {selectedInterests} = interestsStepResults - const selectedFeeds = [ - ...sortPrimaryAlgorithmFeeds(algoFeedsStepResults.feedUris), - ...topicalFeedsStepResults.feedUris, - ] - try { await Promise.all([ - bulkWriteFollows( - agent, - suggestedAccountsStepResults.accountDids.concat(BSKY_APP_ACCOUNT_DID), - ), - // these must be serial + bulkWriteFollows(agent, [BSKY_APP_ACCOUNT_DID]), (async () => { 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 () => { - if (!gate('reduced_onboarding_and_home_algo_v2')) return - const {imageUri, imageMime} = profileStepResults if (imageUri && imageMime) { const blobPromise = uploadBlob(agent, imageUri, imageMime) @@ -134,7 +64,6 @@ export function StepFinished() { return existing }) } - logEvent('onboarding:finished:avatarResult', { avatarResult: profileStepResults.isCreatedAvatar ? 'created' @@ -169,17 +98,7 @@ export function StepFinished() { track('OnboardingV2:StepFinished:End') track('OnboardingV2:Complete') logEvent('onboarding:finished:nextPressed', {}) - }, [ - state, - dispatch, - onboardDispatch, - setSaving, - overwriteSavedFeeds, - track, - agent, - gate, - queryClient, - ]) + }, [state, dispatch, onboardDispatch, setSaving, track, agent, queryClient]) React.useEffect(() => { track('OnboardingV2:StepFinished:Start') diff --git a/src/screens/Onboarding/StepFollowingFeed.tsx b/src/screens/Onboarding/StepFollowingFeed.tsx deleted file mode 100644 index a1c7299f0..000000000 --- a/src/screens/Onboarding/StepFollowingFeed.tsx +++ /dev/null @@ -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> - ) -} diff --git a/src/screens/Onboarding/StepInterests/index.tsx b/src/screens/Onboarding/StepInterests/index.tsx index 2589e66c2..866ea5c2f 100644 --- a/src/screens/Onboarding/StepInterests/index.tsx +++ b/src/screens/Onboarding/StepInterests/index.tsx @@ -5,12 +5,11 @@ import {useLingui} from '@lingui/react' import {useQuery} from '@tanstack/react-query' 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 {logger} from '#/logger' import {useAgent} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' -import {useRequestNotificationsPermission} from 'lib/notifications/notifications' import { DescriptionText, OnboardingControls, @@ -34,8 +33,6 @@ export function StepInterests() { const t = useTheme() const {gtMobile} = useBreakpoints() const {track} = useAnalytics() - const gate = useGate() - const requestNotificationsPermission = useRequestNotificationsPermission() const {state, dispatch, interestsDisplayNames} = React.useContext(Context) const [saving, setSaving] = React.useState(false) @@ -132,12 +129,6 @@ export function StepInterests() { track('OnboardingV2:StepInterests:Start') }, [track]) - React.useEffect(() => { - if (!gate('reduced_onboarding_and_home_algo_v2')) { - requestNotificationsPermission('StartOnboarding') - } - }, [gate, requestNotificationsPermission]) - const title = isError ? ( <Trans>Oh no! Something went wrong.</Trans> ) : ( diff --git a/src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx b/src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx deleted file mode 100644 index 7563bece1..000000000 --- a/src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx +++ /dev/null @@ -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> - </> - ) -} diff --git a/src/screens/Onboarding/StepModeration/ModerationOption.tsx b/src/screens/Onboarding/StepModeration/ModerationOption.tsx deleted file mode 100644 index d6334e6bd..000000000 --- a/src/screens/Onboarding/StepModeration/ModerationOption.tsx +++ /dev/null @@ -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> - ) -} diff --git a/src/screens/Onboarding/StepModeration/index.tsx b/src/screens/Onboarding/StepModeration/index.tsx deleted file mode 100644 index d494f48dd..000000000 --- a/src/screens/Onboarding/StepModeration/index.tsx +++ /dev/null @@ -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> - ) -} diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx index fc19d5bb3..e0a10419d 100644 --- a/src/screens/Onboarding/StepProfile/index.tsx +++ b/src/screens/Onboarding/StepProfile/index.tsx @@ -92,11 +92,7 @@ export function StepProfile() { }, [track]) React.useEffect(() => { - // We have an experiment running for redueced onboarding, where this screen shows up as the first in onboarding. - // 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') - } + requestNotificationsPermission('StartOnboarding') }, [gate, requestNotificationsPermission]) const openPicker = React.useCallback( diff --git a/src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx b/src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx deleted file mode 100644 index f0ba36e39..000000000 --- a/src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx +++ /dev/null @@ -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> - ) -} diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx deleted file mode 100644 index 774f2d3b0..000000000 --- a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx +++ /dev/null @@ -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> - ) -} diff --git a/src/screens/Onboarding/StepTopicalFeeds.tsx b/src/screens/Onboarding/StepTopicalFeeds.tsx deleted file mode 100644 index bfc9e91d1..000000000 --- a/src/screens/Onboarding/StepTopicalFeeds.tsx +++ /dev/null @@ -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> - ) -} diff --git a/src/screens/Onboarding/index.tsx b/src/screens/Onboarding/index.tsx index 20271a465..a5c423ca1 100644 --- a/src/screens/Onboarding/index.tsx +++ b/src/screens/Onboarding/index.tsx @@ -2,33 +2,18 @@ import React from 'react' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useGate} from '#/lib/statsig/statsig' import {Layout, OnboardingControls} from '#/screens/Onboarding/Layout' -import { - Context, - initialState, - initialStateReduced, - reducer, - reducerReduced, -} from '#/screens/Onboarding/state' -import {StepAlgoFeeds} from '#/screens/Onboarding/StepAlgoFeeds' +import {Context, initialState, reducer} from '#/screens/Onboarding/state' import {StepFinished} from '#/screens/Onboarding/StepFinished' -import {StepFollowingFeed} from '#/screens/Onboarding/StepFollowingFeed' import {StepInterests} from '#/screens/Onboarding/StepInterests' -import {StepModeration} from '#/screens/Onboarding/StepModeration' import {StepProfile} from '#/screens/Onboarding/StepProfile' -import {StepSuggestedAccounts} from '#/screens/Onboarding/StepSuggestedAccounts' -import {StepTopicalFeeds} from '#/screens/Onboarding/StepTopicalFeeds' import {Portal} from '#/components/Portal' export function Onboarding() { const {_} = useLingui() - const gate = useGate() - const isReducedOnboardingEnabled = gate('reduced_onboarding_and_home_algo_v2') - const [state, dispatch] = React.useReducer( - isReducedOnboardingEnabled ? reducerReduced : reducer, - isReducedOnboardingEnabled ? {...initialStateReduced} : {...initialState}, - ) + const [state, dispatch] = React.useReducer(reducer, { + ...initialState, + }) const interestsDisplayNames = React.useMemo(() => { return { @@ -68,13 +53,6 @@ export function Onboarding() { <Layout> {state.activeStep === 'profile' && <StepProfile />} {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 />} </Layout> </Context.Provider> diff --git a/src/screens/Onboarding/state.ts b/src/screens/Onboarding/state.ts index 50d815674..8f61cb22e 100644 --- a/src/screens/Onboarding/state.ts +++ b/src/screens/Onboarding/state.ts @@ -6,31 +6,13 @@ import {AvatarColor, Emoji} from '#/screens/Onboarding/StepProfile/types' export type OnboardingState = { hasPrev: boolean totalSteps: number - activeStep: - | 'profile' - | 'interests' - | 'suggestedAccounts' - | 'followingFeed' - | 'algoFeeds' - | 'topicalFeeds' - | 'moderation' - | 'profile' - | 'finished' + activeStep: 'profile' | 'interests' | 'finished' activeStepIndex: number interestsStepResults: { selectedInterests: string[] apiResponse: ApiResponseMap } - suggestedAccountsStepResults: { - accountDids: string[] - } - algoFeedsStepResults: { - feedUris: string[] - } - topicalFeedsStepResults: { - feedUris: string[] - } profileStepResults: { isCreatedAvatar: boolean image?: { @@ -65,18 +47,6 @@ export type OnboardingAction = apiResponse: ApiResponseMap } | { - type: 'setSuggestedAccountsStepResults' - accountDids: string[] - } - | { - type: 'setAlgoFeedsStepResults' - feedUris: string[] - } - | { - type: 'setTopicalFeedsStepResults' - feedUris: string[] - } - | { type: 'setProfileStepResults' isCreatedAvatar: boolean image?: OnboardingState['profileStepResults']['image'] @@ -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: { [key: string]: string } = { @@ -156,125 +95,7 @@ export const INTEREST_TO_DISPLAY_NAME_DEFAULTS: { cooking: 'Cooking', } -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, - 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 = { +export const initialState: OnboardingState = { hasPrev: false, totalSteps: 3, activeStep: 'profile', @@ -288,15 +109,6 @@ export const initialStateReduced: OnboardingState = { suggestedFeedUris: {}, }, }, - suggestedAccountsStepResults: { - accountDids: [], - }, - algoFeedsStepResults: { - feedUris: [], - }, - topicalFeedsStepResults: { - feedUris: [], - }, profileStepResults: { isCreatedAvatar: false, 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, a: OnboardingAction, ): OnboardingState { @@ -333,7 +155,7 @@ export function reducerReduced( break } case 'finish': { - next = initialStateReduced + next = initialState break } case 'setInterestsStepResults': { @@ -343,15 +165,6 @@ export function reducerReduced( } break } - case 'setSuggestedAccountsStepResults': { - break - } - case 'setAlgoFeedsStepResults': { - break - } - case 'setTopicalFeedsStepResults': { - break - } case 'setProfileStepResults': { next.profileStepResults = { isCreatedAvatar: a.isCreatedAvatar, @@ -376,9 +189,6 @@ export function reducerReduced( interestsStepResults: { selectedInterests: state.interestsStepResults.selectedInterests, }, - suggestedAccountsStepResults: state.suggestedAccountsStepResults, - algoFeedsStepResults: state.algoFeedsStepResults, - topicalFeedsStepResults: state.topicalFeedsStepResults, profileStepResults: state.profileStepResults, }) diff --git a/src/screens/Onboarding/util.ts b/src/screens/Onboarding/util.ts index 417417707..f3c800d05 100644 --- a/src/screens/Onboarding/util.ts +++ b/src/screens/Onboarding/util.ts @@ -5,66 +5,6 @@ import { } from '@atproto/api' 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[]) { 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) - }) -} diff --git a/src/view/com/testing/TestCtrls.e2e.tsx b/src/view/com/testing/TestCtrls.e2e.tsx index 31122f838..1291165b3 100644 --- a/src/view/com/testing/TestCtrls.e2e.tsx +++ b/src/view/com/testing/TestCtrls.e2e.tsx @@ -2,7 +2,6 @@ import React from 'react' import {LogBox, Pressable, View} from 'react-native' import {useQueryClient} from '@tanstack/react-query' -import {useDangerousSetGate} from '#/lib/statsig/statsig' import {useModalControls} from '#/state/modals' import {useSessionApi} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' @@ -25,7 +24,6 @@ export function TestCtrls() { const {openModal} = useModalControls() const onboardingDispatch = useOnboardingDispatch() const {setShowLoggedOut} = useLoggedOutViewControls() - const setGate = useDangerousSetGate() const onPressSignInAlice = async () => { await login( { @@ -117,8 +115,6 @@ export function TestCtrls() { <Pressable testID="e2eStartOnboarding" onPress={() => { - // TODO remove when experiment is over - setGate('reduced_onboarding_and_home_algo_v2', true) onboardingDispatch({type: 'start'}) }} accessibilityRole="button" @@ -128,8 +124,6 @@ export function TestCtrls() { <Pressable testID="e2eStartLongboarding" onPress={() => { - // TODO remove when experiment is over - setGate('reduced_onboarding_and_home_algo_v2', false) onboardingDispatch({type: 'start'}) }} accessibilityRole="button" |