mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Remove old onboarding (#4224)

* Hardcode onboarding_v2 to true, rm dead code

* Rm initialState, use initialStateReduced

* Rm dead code

* Drop *reduced prefix in code

* Prettier

authored by danabra.mov and committed by

GitHub adbbded0 9bd411c1

+27 -1986
-1
src/lib/statsig/gates.ts
··· 1 1 export type Gate = 2 2 // Keep this alphabetic please. 3 - | 'reduced_onboarding_and_home_algo_v2' 4 3 | 'request_notifications_permission_after_onboarding' 5 4 | 'show_follow_back_label_v2'
-378
src/screens/Onboarding/StepAlgoFeeds/FeedCard.tsx
··· 1 - import React from 'react' 2 - import {View} from 'react-native' 3 - import {Image} from 'expo-image' 4 - import {LinearGradient} from 'expo-linear-gradient' 5 - import {msg, Trans} from '@lingui/macro' 6 - import {useLingui} from '@lingui/react' 7 - 8 - import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed' 9 - import {FeedConfig} from '#/screens/Onboarding/StepAlgoFeeds' 10 - import {atoms as a, useTheme} from '#/alf' 11 - import * as Toggle from '#/components/forms/Toggle' 12 - import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 13 - import {RichText} from '#/components/RichText' 14 - import {Text} from '#/components/Typography' 15 - 16 - function PrimaryFeedCardInner({ 17 - feed, 18 - config, 19 - }: { 20 - feed: FeedSourceInfo 21 - config: FeedConfig 22 - }) { 23 - const t = useTheme() 24 - const ctx = Toggle.useItemContext() 25 - 26 - const styles = React.useMemo( 27 - () => ({ 28 - active: [t.atoms.bg_contrast_25], 29 - selected: [ 30 - a.shadow_md, 31 - { 32 - backgroundColor: 33 - t.name === 'light' ? t.palette.primary_50 : t.palette.primary_950, 34 - }, 35 - ], 36 - selectedHover: [ 37 - { 38 - backgroundColor: 39 - t.name === 'light' ? t.palette.primary_25 : t.palette.primary_975, 40 - }, 41 - ], 42 - textSelected: [{color: t.palette.white}], 43 - checkboxSelected: [ 44 - { 45 - borderColor: t.palette.white, 46 - }, 47 - ], 48 - }), 49 - [t], 50 - ) 51 - 52 - return ( 53 - <View 54 - style={[ 55 - a.relative, 56 - a.w_full, 57 - a.p_lg, 58 - a.rounded_md, 59 - a.overflow_hidden, 60 - t.atoms.bg_contrast_50, 61 - (ctx.hovered || ctx.focused || ctx.pressed) && styles.active, 62 - ctx.selected && styles.selected, 63 - ctx.selected && 64 - (ctx.hovered || ctx.focused || ctx.pressed) && 65 - styles.selectedHover, 66 - ]}> 67 - {ctx.selected && config.gradient && ( 68 - <LinearGradient 69 - colors={config.gradient.values.map(v => v[1])} 70 - locations={config.gradient.values.map(v => v[0])} 71 - start={{x: 0, y: 0}} 72 - end={{x: 1, y: 1}} 73 - style={[a.absolute, a.inset_0]} 74 - /> 75 - )} 76 - 77 - <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}> 78 - <View 79 - style={[ 80 - { 81 - width: 64, 82 - height: 64, 83 - }, 84 - a.rounded_sm, 85 - a.overflow_hidden, 86 - t.atoms.bg, 87 - ]}> 88 - <Image 89 - source={{uri: feed.avatar}} 90 - style={[a.w_full, a.h_full]} 91 - accessibilityIgnoresInvertColors 92 - /> 93 - </View> 94 - 95 - <View style={[a.pt_xs, a.flex_grow]}> 96 - <Text 97 - style={[ 98 - a.text_lg, 99 - a.font_bold, 100 - ctx.selected && styles.textSelected, 101 - ]}> 102 - {feed.displayName} 103 - </Text> 104 - 105 - <Text 106 - style={[ 107 - {opacity: 0.6}, 108 - a.text_md, 109 - a.py_xs, 110 - ctx.selected && styles.textSelected, 111 - ]}> 112 - <Trans>by @{feed.creatorHandle}</Trans> 113 - </Text> 114 - </View> 115 - 116 - <View 117 - style={[ 118 - { 119 - width: 28, 120 - height: 28, 121 - }, 122 - a.justify_center, 123 - a.align_center, 124 - a.rounded_sm, 125 - ctx.selected ? [a.border, styles.checkboxSelected] : t.atoms.bg, 126 - ]}> 127 - {ctx.selected && <Check size="sm" fill={t.palette.white} />} 128 - </View> 129 - </View> 130 - 131 - <View 132 - style={[ 133 - { 134 - opacity: ctx.selected ? 0.3 : 1, 135 - borderTopWidth: 1, 136 - }, 137 - a.mt_md, 138 - a.w_full, 139 - t.atoms.border_contrast_low, 140 - ctx.selected && { 141 - borderTopColor: t.palette.white, 142 - }, 143 - ]} 144 - /> 145 - 146 - <View style={[a.pt_md]}> 147 - <RichText 148 - value={feed.description} 149 - style={[ 150 - a.text_md, 151 - ctx.selected && 152 - (t.name === 'light' 153 - ? t.atoms.text_inverted 154 - : {color: t.palette.white}), 155 - ]} 156 - disableLinks 157 - /> 158 - </View> 159 - </View> 160 - ) 161 - } 162 - 163 - export function PrimaryFeedCard({config}: {config: FeedConfig}) { 164 - const {_} = useLingui() 165 - const {data: feed} = useFeedSourceInfoQuery({uri: config.uri}) 166 - 167 - return !feed ? ( 168 - <FeedCardPlaceholder primary /> 169 - ) : ( 170 - <Toggle.Item 171 - name={feed.uri} 172 - label={_(msg`Subscribe to the ${feed.displayName} feed`)}> 173 - <PrimaryFeedCardInner config={config} feed={feed} /> 174 - </Toggle.Item> 175 - ) 176 - } 177 - 178 - function FeedCardInner({feed}: {feed: FeedSourceInfo; config: FeedConfig}) { 179 - const t = useTheme() 180 - const ctx = Toggle.useItemContext() 181 - 182 - const styles = React.useMemo( 183 - () => ({ 184 - active: [t.atoms.bg_contrast_25], 185 - selected: [ 186 - { 187 - backgroundColor: 188 - t.name === 'light' ? t.palette.primary_50 : t.palette.primary_950, 189 - }, 190 - ], 191 - selectedHover: [ 192 - { 193 - backgroundColor: 194 - t.name === 'light' ? t.palette.primary_25 : t.palette.primary_975, 195 - }, 196 - ], 197 - textSelected: [], 198 - checkboxSelected: [ 199 - { 200 - backgroundColor: t.palette.primary_500, 201 - }, 202 - ], 203 - }), 204 - [t], 205 - ) 206 - 207 - return ( 208 - <View 209 - style={[ 210 - a.relative, 211 - a.w_full, 212 - a.p_md, 213 - a.rounded_md, 214 - a.overflow_hidden, 215 - t.atoms.bg_contrast_50, 216 - (ctx.hovered || ctx.focused || ctx.pressed) && styles.active, 217 - ctx.selected && styles.selected, 218 - ctx.selected && 219 - (ctx.hovered || ctx.focused || ctx.pressed) && 220 - styles.selectedHover, 221 - ]}> 222 - <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}> 223 - <View 224 - style={[ 225 - { 226 - width: 44, 227 - height: 44, 228 - }, 229 - a.rounded_sm, 230 - a.overflow_hidden, 231 - t.atoms.bg, 232 - ]}> 233 - <Image 234 - source={{uri: feed.avatar}} 235 - style={[a.w_full, a.h_full]} 236 - accessibilityIgnoresInvertColors 237 - /> 238 - </View> 239 - 240 - <View style={[a.pt_2xs, a.flex_1, a.flex_grow]}> 241 - <Text 242 - style={[ 243 - a.text_md, 244 - a.font_bold, 245 - ctx.selected && styles.textSelected, 246 - ]} 247 - numberOfLines={1}> 248 - {feed.displayName} 249 - </Text> 250 - <Text 251 - style={[ 252 - {opacity: 0.8}, 253 - a.pt_xs, 254 - ctx.selected && styles.textSelected, 255 - ]}> 256 - @{feed.creatorHandle} 257 - </Text> 258 - </View> 259 - 260 - <View 261 - style={[ 262 - a.justify_center, 263 - a.align_center, 264 - a.rounded_sm, 265 - t.atoms.bg, 266 - ctx.selected && styles.checkboxSelected, 267 - { 268 - width: 28, 269 - height: 28, 270 - }, 271 - ]}> 272 - {ctx.selected && <Check size="sm" fill={t.palette.white} />} 273 - </View> 274 - </View> 275 - 276 - <View 277 - style={[ 278 - { 279 - opacity: ctx.selected ? 0.3 : 1, 280 - borderTopWidth: 1, 281 - }, 282 - a.mt_md, 283 - a.w_full, 284 - t.atoms.border_contrast_low, 285 - ctx.selected && { 286 - borderTopColor: t.palette.primary_200, 287 - }, 288 - ]} 289 - /> 290 - 291 - <View style={[a.pt_md]}> 292 - <RichText value={feed.description} disableLinks /> 293 - </View> 294 - </View> 295 - ) 296 - } 297 - 298 - export function FeedCard({config}: {config: FeedConfig}) { 299 - const {_} = useLingui() 300 - const {data: feed} = useFeedSourceInfoQuery({uri: config.uri}) 301 - 302 - return !feed ? ( 303 - <FeedCardPlaceholder /> 304 - ) : feed.avatar ? ( 305 - <Toggle.Item 306 - name={feed.uri} 307 - label={_(msg`Subscribe to the ${feed.displayName} feed`)}> 308 - <FeedCardInner config={config} feed={feed} /> 309 - </Toggle.Item> 310 - ) : null 311 - } 312 - 313 - export function FeedCardPlaceholder({primary}: {primary?: boolean}) { 314 - const t = useTheme() 315 - return ( 316 - <View 317 - style={[ 318 - a.relative, 319 - a.w_full, 320 - a.p_md, 321 - a.rounded_md, 322 - a.overflow_hidden, 323 - t.atoms.bg_contrast_25, 324 - ]}> 325 - <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}> 326 - <View 327 - style={[ 328 - { 329 - width: primary ? 64 : 44, 330 - height: primary ? 64 : 44, 331 - }, 332 - a.rounded_sm, 333 - a.overflow_hidden, 334 - t.atoms.bg_contrast_100, 335 - ]} 336 - /> 337 - 338 - <View style={[a.pt_2xs, a.flex_grow, a.gap_sm]}> 339 - <View 340 - style={[ 341 - {width: 100, height: primary ? 20 : 16}, 342 - a.rounded_sm, 343 - t.atoms.bg_contrast_100, 344 - ]} 345 - /> 346 - <View 347 - style={[ 348 - {width: 60, height: 12}, 349 - a.rounded_sm, 350 - t.atoms.bg_contrast_100, 351 - ]} 352 - /> 353 - </View> 354 - </View> 355 - 356 - <View 357 - style={[ 358 - { 359 - borderTopWidth: 1, 360 - }, 361 - a.mt_md, 362 - a.w_full, 363 - t.atoms.border_contrast_low, 364 - ]} 365 - /> 366 - 367 - <View style={[a.pt_md, a.gap_xs]}> 368 - <View 369 - style={[ 370 - {width: '60%', height: 12}, 371 - a.rounded_sm, 372 - t.atoms.bg_contrast_100, 373 - ]} 374 - /> 375 - </View> 376 - </View> 377 - ) 378 - }
-168
src/screens/Onboarding/StepAlgoFeeds/index.tsx
··· 1 - import React from 'react' 2 - import {View} from 'react-native' 3 - import {msg, Trans} from '@lingui/macro' 4 - import {useLingui} from '@lingui/react' 5 - 6 - import {useAnalytics} from '#/lib/analytics/analytics' 7 - import {logEvent} from '#/lib/statsig/statsig' 8 - import { 9 - DescriptionText, 10 - OnboardingControls, 11 - TitleText, 12 - } from '#/screens/Onboarding/Layout' 13 - import {Context} from '#/screens/Onboarding/state' 14 - import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard' 15 - import {atoms as a, tokens, useTheme} from '#/alf' 16 - import {Button, ButtonIcon, ButtonText} from '#/components/Button' 17 - import * as Toggle from '#/components/forms/Toggle' 18 - import {IconCircle} from '#/components/IconCircle' 19 - import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' 20 - import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle' 21 - import {Loader} from '#/components/Loader' 22 - import {Text} from '#/components/Typography' 23 - import {IS_PROD} from '#/env' 24 - 25 - export type FeedConfig = { 26 - default: boolean 27 - uri: string 28 - gradient?: typeof tokens.gradients.midnight | typeof tokens.gradients.nordic 29 - } 30 - 31 - export const PRIMARY_FEEDS: FeedConfig[] = [ 32 - { 33 - default: IS_PROD, // these feeds are only available in prod 34 - uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot', 35 - gradient: tokens.gradients.midnight, 36 - }, 37 - ] 38 - 39 - const SECONDARY_FEEDS: FeedConfig[] = [ 40 - { 41 - default: false, 42 - uri: 'at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/infreq', 43 - }, 44 - { 45 - default: false, 46 - uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends', 47 - }, 48 - { 49 - default: false, 50 - uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/best-of-follows', 51 - }, 52 - { 53 - default: false, 54 - uri: 'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/catch-up', 55 - }, 56 - { 57 - default: false, 58 - uri: 'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/at-bangers', 59 - }, 60 - ] 61 - 62 - export function StepAlgoFeeds() { 63 - const {_} = useLingui() 64 - const {track} = useAnalytics() 65 - const t = useTheme() 66 - const {state, dispatch} = React.useContext(Context) 67 - const [primaryFeedUris, setPrimaryFeedUris] = React.useState<string[]>( 68 - PRIMARY_FEEDS.map(f => (f.default ? f.uri : '')).filter(Boolean), 69 - ) 70 - const [secondaryFeedUris, setSeconaryFeedUris] = React.useState<string[]>([]) 71 - const [saving, setSaving] = React.useState(false) 72 - 73 - const saveFeeds = React.useCallback(async () => { 74 - setSaving(true) 75 - 76 - const uris = primaryFeedUris.concat(secondaryFeedUris) 77 - dispatch({type: 'setAlgoFeedsStepResults', feedUris: uris}) 78 - 79 - setSaving(false) 80 - dispatch({type: 'next'}) 81 - track('OnboardingV2:StepAlgoFeeds:End', { 82 - selectedPrimaryFeeds: primaryFeedUris, 83 - selectedPrimaryFeedsLength: primaryFeedUris.length, 84 - selectedSecondaryFeeds: secondaryFeedUris, 85 - selectedSecondaryFeedsLength: secondaryFeedUris.length, 86 - }) 87 - logEvent('onboarding:algoFeeds:nextPressed', { 88 - selectedPrimaryFeeds: primaryFeedUris, 89 - selectedPrimaryFeedsLength: primaryFeedUris.length, 90 - selectedSecondaryFeeds: secondaryFeedUris, 91 - selectedSecondaryFeedsLength: secondaryFeedUris.length, 92 - }) 93 - }, [primaryFeedUris, secondaryFeedUris, dispatch, track]) 94 - 95 - React.useEffect(() => { 96 - track('OnboardingV2:StepAlgoFeeds:Start') 97 - }, [track]) 98 - 99 - return ( 100 - <View style={[a.align_start]}> 101 - <IconCircle icon={ListSparkle} style={[a.mb_2xl]} /> 102 - 103 - <TitleText> 104 - <Trans>Choose your main feeds</Trans> 105 - </TitleText> 106 - <DescriptionText> 107 - <Trans> 108 - Custom feeds built by the community bring you new experiences and help 109 - you find the content you love. 110 - </Trans> 111 - </DescriptionText> 112 - 113 - <View style={[a.w_full, a.pb_2xl]}> 114 - <Toggle.Group 115 - values={primaryFeedUris} 116 - onChange={setPrimaryFeedUris} 117 - label={_(msg`Select your primary algorithmic feeds`)}> 118 - <Text 119 - style={[ 120 - a.text_md, 121 - a.pt_4xl, 122 - a.pb_md, 123 - t.atoms.text_contrast_medium, 124 - ]}> 125 - <Trans>We recommend our "Discover" feed:</Trans> 126 - </Text> 127 - <FeedCard config={PRIMARY_FEEDS[0]} /> 128 - </Toggle.Group> 129 - 130 - <Toggle.Group 131 - values={secondaryFeedUris} 132 - onChange={setSeconaryFeedUris} 133 - label={_(msg`Select your secondary algorithmic feeds`)}> 134 - <Text 135 - style={[ 136 - a.text_md, 137 - a.pt_4xl, 138 - a.pb_lg, 139 - t.atoms.text_contrast_medium, 140 - ]}> 141 - <Trans>There are many feeds to try:</Trans> 142 - </Text> 143 - <View style={[a.gap_md]}> 144 - {SECONDARY_FEEDS.map(config => ( 145 - <FeedCard key={config.uri} config={config} /> 146 - ))} 147 - </View> 148 - </Toggle.Group> 149 - </View> 150 - 151 - <OnboardingControls.Portal> 152 - <Button 153 - disabled={saving} 154 - key={state.activeStep} // remove focus state on nav 155 - variant="gradient" 156 - color="gradient_sky" 157 - size="large" 158 - label={_(msg`Continue to the next step`)} 159 - onPress={saveFeeds}> 160 - <ButtonText> 161 - <Trans>Continue</Trans> 162 - </ButtonText> 163 - <ButtonIcon icon={saving ? Loader : ChevronRight} position="right" /> 164 - </Button> 165 - </OnboardingControls.Portal> 166 - </View> 167 - ) 168 - }
+7 -88
src/screens/Onboarding/StepFinished.tsx
··· 1 1 import React from 'react' 2 2 import {View} from 'react-native' 3 - import {TID} from '@atproto/common-web' 4 3 import {msg, Trans} from '@lingui/macro' 5 4 import {useLingui} from '@lingui/react' 6 5 import {useQueryClient} from '@tanstack/react-query' 7 6 8 7 import {useAnalytics} from '#/lib/analytics/analytics' 9 - import {BSKY_APP_ACCOUNT_DID, IS_PROD_SERVICE} from '#/lib/constants' 10 - import {DISCOVER_SAVED_FEED, TIMELINE_SAVED_FEED} from '#/lib/constants' 11 - import {logEvent, useGate} from '#/lib/statsig/statsig' 8 + import {BSKY_APP_ACCOUNT_DID} from '#/lib/constants' 9 + import {logEvent} from '#/lib/statsig/statsig' 12 10 import {logger} from '#/logger' 13 - import { 14 - preferencesQueryKey, 15 - useOverwriteSavedFeedsMutation, 16 - } from '#/state/queries/preferences' 11 + import {preferencesQueryKey} from '#/state/queries/preferences' 17 12 import {RQKEY as profileRQKey} from '#/state/queries/profile' 18 13 import {useAgent} from '#/state/session' 19 14 import {useOnboardingDispatch} from '#/state/shell' ··· 24 19 TitleText, 25 20 } from '#/screens/Onboarding/Layout' 26 21 import {Context} from '#/screens/Onboarding/state' 27 - import { 28 - bulkWriteFollows, 29 - sortPrimaryAlgorithmFeeds, 30 - } from '#/screens/Onboarding/util' 22 + import {bulkWriteFollows} from '#/screens/Onboarding/util' 31 23 import {atoms as a, useTheme} from '#/alf' 32 24 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 33 25 import {IconCircle} from '#/components/IconCircle' ··· 45 37 const {state, dispatch} = React.useContext(Context) 46 38 const onboardDispatch = useOnboardingDispatch() 47 39 const [saving, setSaving] = React.useState(false) 48 - const {mutateAsync: overwriteSavedFeeds} = useOverwriteSavedFeedsMutation() 49 40 const queryClient = useQueryClient() 50 41 const agent = useAgent() 51 - const gate = useGate() 52 42 53 43 const finishOnboarding = React.useCallback(async () => { 54 44 setSaving(true) 55 45 56 - // TODO uncomment 57 - const { 58 - interestsStepResults, 59 - suggestedAccountsStepResults, 60 - algoFeedsStepResults, 61 - topicalFeedsStepResults, 62 - profileStepResults, 63 - } = state 46 + const {interestsStepResults, profileStepResults} = state 64 47 const {selectedInterests} = interestsStepResults 65 - const selectedFeeds = [ 66 - ...sortPrimaryAlgorithmFeeds(algoFeedsStepResults.feedUris), 67 - ...topicalFeedsStepResults.feedUris, 68 - ] 69 - 70 48 try { 71 49 await Promise.all([ 72 - bulkWriteFollows( 73 - agent, 74 - suggestedAccountsStepResults.accountDids.concat(BSKY_APP_ACCOUNT_DID), 75 - ), 76 - // these must be serial 50 + bulkWriteFollows(agent, [BSKY_APP_ACCOUNT_DID]), 77 51 (async () => { 78 52 await agent.setInterestsPref({tags: selectedInterests}) 79 - 80 - /* 81 - * In the reduced onboading experiment, we'll rely on the default 82 - * feeds set in `createAgentAndCreateAccount`. No feeds will be 83 - * selected in onboarding and therefore we don't need to run this 84 - * code (which would overwrite the other feeds already set). 85 - */ 86 - if (!gate('reduced_onboarding_and_home_algo_v2')) { 87 - const otherFeeds = selectedFeeds.length 88 - ? selectedFeeds.map(f => ({ 89 - type: 'feed', 90 - value: f, 91 - pinned: true, 92 - id: TID.nextStr(), 93 - })) 94 - : [] 95 - 96 - /* 97 - * If no selected feeds and we're in prod, add the discover feed 98 - * (mimics old behavior) 99 - */ 100 - if ( 101 - IS_PROD_SERVICE(agent.service.toString()) && 102 - !otherFeeds.length 103 - ) { 104 - otherFeeds.push({ 105 - ...DISCOVER_SAVED_FEED, 106 - pinned: true, 107 - id: TID.nextStr(), 108 - }) 109 - } 110 - 111 - await overwriteSavedFeeds([ 112 - { 113 - ...TIMELINE_SAVED_FEED, 114 - pinned: true, 115 - id: TID.nextStr(), 116 - }, 117 - ...otherFeeds, 118 - ]) 119 - } 120 53 })(), 121 - 122 54 (async () => { 123 - if (!gate('reduced_onboarding_and_home_algo_v2')) return 124 - 125 55 const {imageUri, imageMime} = profileStepResults 126 56 if (imageUri && imageMime) { 127 57 const blobPromise = uploadBlob(agent, imageUri, imageMime) ··· 134 64 return existing 135 65 }) 136 66 } 137 - 138 67 logEvent('onboarding:finished:avatarResult', { 139 68 avatarResult: profileStepResults.isCreatedAvatar 140 69 ? 'created' ··· 169 98 track('OnboardingV2:StepFinished:End') 170 99 track('OnboardingV2:Complete') 171 100 logEvent('onboarding:finished:nextPressed', {}) 172 - }, [ 173 - state, 174 - dispatch, 175 - onboardDispatch, 176 - setSaving, 177 - overwriteSavedFeeds, 178 - track, 179 - agent, 180 - gate, 181 - queryClient, 182 - ]) 101 + }, [state, dispatch, onboardDispatch, setSaving, track, agent, queryClient]) 183 102 184 103 React.useEffect(() => { 185 104 track('OnboardingV2:StepFinished:Start')
-161
src/screens/Onboarding/StepFollowingFeed.tsx
··· 1 - import React from 'react' 2 - import {View} from 'react-native' 3 - import {msg, Trans} from '@lingui/macro' 4 - import {useLingui} from '@lingui/react' 5 - 6 - import {useAnalytics} from '#/lib/analytics/analytics' 7 - import {logEvent} from '#/lib/statsig/statsig' 8 - import { 9 - usePreferencesQuery, 10 - useSetFeedViewPreferencesMutation, 11 - } from 'state/queries/preferences' 12 - import { 13 - DescriptionText, 14 - OnboardingControls, 15 - TitleText, 16 - } from '#/screens/Onboarding/Layout' 17 - import {Context} from '#/screens/Onboarding/state' 18 - import {atoms as a} from '#/alf' 19 - import {Button, ButtonIcon, ButtonText} from '#/components/Button' 20 - import {Divider} from '#/components/Divider' 21 - import * as Toggle from '#/components/forms/Toggle' 22 - import {IconCircle} from '#/components/IconCircle' 23 - import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' 24 - import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' 25 - import {Text} from '#/components/Typography' 26 - 27 - export function StepFollowingFeed() { 28 - const {_} = useLingui() 29 - const {track} = useAnalytics() 30 - const {dispatch} = React.useContext(Context) 31 - 32 - const {data: preferences} = usePreferencesQuery() 33 - const {mutate: setFeedViewPref, variables} = 34 - useSetFeedViewPreferencesMutation() 35 - 36 - const showReplies = !( 37 - variables?.hideReplies ?? preferences?.feedViewPrefs.hideReplies 38 - ) 39 - const showReposts = !( 40 - variables?.hideReposts ?? preferences?.feedViewPrefs.hideReposts 41 - ) 42 - const showQuotes = !( 43 - variables?.hideQuotePosts ?? preferences?.feedViewPrefs.hideQuotePosts 44 - ) 45 - 46 - const onContinue = React.useCallback(() => { 47 - dispatch({type: 'next'}) 48 - track('OnboardingV2:StepFollowingFeed:End') 49 - logEvent('onboarding:followingFeed:nextPressed', {}) 50 - }, [track, dispatch]) 51 - 52 - React.useEffect(() => { 53 - track('OnboardingV2:StepFollowingFeed:Start') 54 - }, [track]) 55 - 56 - return ( 57 - // Hack for now to move the image container up 58 - <View style={[a.align_start]}> 59 - <IconCircle icon={FilterTimeline} style={[a.mb_2xl]} /> 60 - 61 - <TitleText> 62 - <Trans>Your default feed is "Following"</Trans> 63 - </TitleText> 64 - <DescriptionText style={[a.mb_md]}> 65 - <Trans>It shows posts from the people you follow as they happen.</Trans> 66 - </DescriptionText> 67 - 68 - <View style={[a.w_full]}> 69 - <Toggle.Item 70 - name="Show Replies" // no need to translate 71 - label={_(msg`Show replies in Following feed`)} 72 - value={showReplies} 73 - onChange={() => { 74 - setFeedViewPref({ 75 - hideReplies: showReplies, 76 - }) 77 - }}> 78 - <View 79 - style={[ 80 - a.flex_row, 81 - a.w_full, 82 - a.py_lg, 83 - a.justify_between, 84 - a.align_center, 85 - ]}> 86 - <Text style={[a.text_md, a.font_bold]}> 87 - <Trans>Show replies in Following</Trans> 88 - </Text> 89 - <Toggle.Switch /> 90 - </View> 91 - </Toggle.Item> 92 - <Divider /> 93 - <Toggle.Item 94 - name="Show Reposts" // no need to translate 95 - label={_(msg`Show re-posts in Following feed`)} 96 - value={showReposts} 97 - onChange={() => { 98 - setFeedViewPref({ 99 - hideReposts: showReposts, 100 - }) 101 - }}> 102 - <View 103 - style={[ 104 - a.flex_row, 105 - a.w_full, 106 - a.py_lg, 107 - a.justify_between, 108 - a.align_center, 109 - ]}> 110 - <Text style={[a.text_md, a.font_bold]}> 111 - <Trans>Show reposts in Following</Trans> 112 - </Text> 113 - <Toggle.Switch /> 114 - </View> 115 - </Toggle.Item> 116 - <Divider /> 117 - <Toggle.Item 118 - name="Show Quotes" // no need to translate 119 - label={_(msg`Show quote-posts in Following feed`)} 120 - value={showQuotes} 121 - onChange={() => { 122 - setFeedViewPref({ 123 - hideQuotePosts: showQuotes, 124 - }) 125 - }}> 126 - <View 127 - style={[ 128 - a.flex_row, 129 - a.w_full, 130 - a.py_lg, 131 - a.justify_between, 132 - a.align_center, 133 - ]}> 134 - <Text style={[a.text_md, a.font_bold]}> 135 - <Trans>Show quotes in Following</Trans> 136 - </Text> 137 - <Toggle.Switch /> 138 - </View> 139 - </Toggle.Item> 140 - </View> 141 - 142 - <DescriptionText style={[a.mt_lg]}> 143 - <Trans>You can change these settings later.</Trans> 144 - </DescriptionText> 145 - 146 - <OnboardingControls.Portal> 147 - <Button 148 - variant="gradient" 149 - color="gradient_sky" 150 - size="large" 151 - label={_(msg`Continue to next step`)} 152 - onPress={onContinue}> 153 - <ButtonText> 154 - <Trans>Continue</Trans> 155 - </ButtonText> 156 - <ButtonIcon icon={ChevronRight} position="right" /> 157 - </Button> 158 - </OnboardingControls.Portal> 159 - </View> 160 - ) 161 - }
+1 -10
src/screens/Onboarding/StepInterests/index.tsx
··· 5 5 import {useQuery} from '@tanstack/react-query' 6 6 7 7 import {useAnalytics} from '#/lib/analytics/analytics' 8 - import {logEvent, useGate} from '#/lib/statsig/statsig' 8 + import {logEvent} from '#/lib/statsig/statsig' 9 9 import {capitalize} from '#/lib/strings/capitalize' 10 10 import {logger} from '#/logger' 11 11 import {useAgent} from '#/state/session' 12 12 import {useOnboardingDispatch} from '#/state/shell' 13 - import {useRequestNotificationsPermission} from 'lib/notifications/notifications' 14 13 import { 15 14 DescriptionText, 16 15 OnboardingControls, ··· 34 33 const t = useTheme() 35 34 const {gtMobile} = useBreakpoints() 36 35 const {track} = useAnalytics() 37 - const gate = useGate() 38 - const requestNotificationsPermission = useRequestNotificationsPermission() 39 36 40 37 const {state, dispatch, interestsDisplayNames} = React.useContext(Context) 41 38 const [saving, setSaving] = React.useState(false) ··· 131 128 track('OnboardingV2:Begin') 132 129 track('OnboardingV2:StepInterests:Start') 133 130 }, [track]) 134 - 135 - React.useEffect(() => { 136 - if (!gate('reduced_onboarding_and_home_algo_v2')) { 137 - requestNotificationsPermission('StartOnboarding') 138 - } 139 - }, [gate, requestNotificationsPermission]) 140 131 141 132 const title = isError ? ( 142 133 <Trans>Oh no! Something went wrong.</Trans>
-131
src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx
··· 1 - import React from 'react' 2 - import {View} from 'react-native' 3 - import {msg, Trans} from '@lingui/macro' 4 - import {useLingui} from '@lingui/react' 5 - import {UseMutateFunction} from '@tanstack/react-query' 6 - 7 - import {logger} from '#/logger' 8 - import {isIOS} from '#/platform/detection' 9 - import {usePreferencesQuery} from '#/state/queries/preferences' 10 - import * as Toast from '#/view/com/util/Toast' 11 - import {atoms as a, useTheme} from '#/alf' 12 - import * as Toggle from '#/components/forms/Toggle' 13 - import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 14 - import * as Prompt from '#/components/Prompt' 15 - import {Text} from '#/components/Typography' 16 - 17 - function Card({children}: React.PropsWithChildren<{}>) { 18 - const t = useTheme() 19 - return ( 20 - <View 21 - style={[ 22 - a.w_full, 23 - a.flex_row, 24 - a.align_center, 25 - a.gap_sm, 26 - a.px_lg, 27 - a.py_md, 28 - a.rounded_sm, 29 - a.mb_md, 30 - t.atoms.bg_contrast_50, 31 - ]}> 32 - {children} 33 - </View> 34 - ) 35 - } 36 - 37 - export function AdultContentEnabledPref({ 38 - mutate, 39 - variables, 40 - }: { 41 - mutate: UseMutateFunction<void, unknown, {enabled: boolean}, unknown> 42 - variables: {enabled: boolean} | undefined 43 - }) { 44 - const {_} = useLingui() 45 - const t = useTheme() 46 - const prompt = Prompt.usePromptControl() 47 - 48 - // Reuse logic here form ContentFilteringSettings.tsx 49 - const {data: preferences} = usePreferencesQuery() 50 - 51 - const onToggleAdultContent = React.useCallback(async () => { 52 - if (isIOS) { 53 - prompt.open() 54 - return 55 - } 56 - 57 - try { 58 - mutate({ 59 - enabled: !( 60 - variables?.enabled ?? preferences?.moderationPrefs.adultContentEnabled 61 - ), 62 - }) 63 - } catch (e) { 64 - Toast.show( 65 - _(msg`There was an issue syncing your preferences with the server`), 66 - ) 67 - logger.error('Failed to update preferences with server', {error: e}) 68 - } 69 - }, [variables, preferences, mutate, _, prompt]) 70 - 71 - if (!preferences) return null 72 - 73 - return ( 74 - <> 75 - {preferences.userAge && preferences.userAge >= 18 ? ( 76 - <View style={[a.w_full, a.px_xs]}> 77 - <Toggle.Item 78 - name={_(msg`Enable adult content in your feeds`)} 79 - label={_(msg`Enable adult content in your feeds`)} 80 - value={ 81 - variables?.enabled ?? 82 - preferences?.moderationPrefs.adultContentEnabled 83 - } 84 - onChange={onToggleAdultContent}> 85 - <View 86 - style={[ 87 - a.flex_row, 88 - a.w_full, 89 - a.justify_between, 90 - a.align_center, 91 - a.py_md, 92 - ]}> 93 - <Text style={[a.font_bold]}> 94 - <Trans>Enable Adult Content</Trans> 95 - </Text> 96 - <Toggle.Switch /> 97 - </View> 98 - </Toggle.Item> 99 - </View> 100 - ) : ( 101 - <Card> 102 - <CircleInfo size="sm" fill={t.palette.contrast_500} /> 103 - <Text 104 - style={[ 105 - a.flex_1, 106 - t.atoms.text_contrast_medium, 107 - a.leading_snug, 108 - {paddingTop: 1}, 109 - ]}> 110 - <Trans>You must be 18 years or older to enable adult content</Trans> 111 - </Text> 112 - </Card> 113 - )} 114 - 115 - <Prompt.Outer control={prompt}> 116 - <Prompt.TitleText> 117 - <Trans>Adult Content</Trans> 118 - </Prompt.TitleText> 119 - <Prompt.DescriptionText> 120 - <Trans> 121 - Due to Apple policies, adult content can only be enabled on the web 122 - after completing sign up. 123 - </Trans> 124 - </Prompt.DescriptionText> 125 - <Prompt.Actions> 126 - <Prompt.Action onPress={() => prompt.close()} cta={_(msg`OK`)} /> 127 - </Prompt.Actions> 128 - </Prompt.Outer> 129 - </> 130 - ) 131 - }
-99
src/screens/Onboarding/StepModeration/ModerationOption.tsx
··· 1 - import React from 'react' 2 - import {View} from 'react-native' 3 - import {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api' 4 - import {msg, Trans} from '@lingui/macro' 5 - import {useLingui} from '@lingui/react' 6 - 7 - import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings' 8 - import { 9 - usePreferencesQuery, 10 - usePreferencesSetContentLabelMutation, 11 - } from '#/state/queries/preferences' 12 - import {atoms as a, useTheme} from '#/alf' 13 - import * as ToggleButton from '#/components/forms/ToggleButton' 14 - import {Text} from '#/components/Typography' 15 - 16 - export function ModerationOption({ 17 - labelValueDefinition, 18 - disabled, 19 - }: { 20 - labelValueDefinition: InterpretedLabelValueDefinition 21 - disabled?: boolean 22 - }) { 23 - const {_} = useLingui() 24 - const t = useTheme() 25 - const {data: preferences} = usePreferencesQuery() 26 - const {mutate, variables} = usePreferencesSetContentLabelMutation() 27 - const label = labelValueDefinition.identifier 28 - const visibility = 29 - variables?.visibility ?? preferences?.moderationPrefs.labels?.[label] 30 - 31 - const allLabelStrings = useGlobalLabelStrings() 32 - const labelStrings = 33 - labelValueDefinition.identifier in allLabelStrings 34 - ? allLabelStrings[labelValueDefinition.identifier] 35 - : { 36 - name: labelValueDefinition.identifier, 37 - description: `Labeled "${labelValueDefinition.identifier}"`, 38 - } 39 - 40 - const onChange = React.useCallback( 41 - (vis: string[]) => { 42 - mutate({ 43 - label, 44 - visibility: vis[0] as LabelPreference, 45 - labelerDid: undefined, 46 - }) 47 - }, 48 - [mutate, label], 49 - ) 50 - 51 - const labels = { 52 - hide: _(msg`Hide`), 53 - warn: _(msg`Warn`), 54 - show: _(msg`Show`), 55 - } 56 - 57 - return ( 58 - <View 59 - style={[ 60 - a.flex_row, 61 - a.justify_between, 62 - a.gap_sm, 63 - a.py_xs, 64 - a.px_xs, 65 - a.align_center, 66 - ]}> 67 - <View style={[a.gap_xs, a.flex_1]}> 68 - <Text style={[a.font_bold]}>{labelStrings.name}</Text> 69 - <Text style={[t.atoms.text_contrast_medium, a.leading_snug]}> 70 - {labelStrings.description} 71 - </Text> 72 - </View> 73 - <View style={[a.justify_center, {minHeight: 40}]}> 74 - {disabled ? ( 75 - <Text style={[a.font_bold]}> 76 - <Trans>Hide</Trans> 77 - </Text> 78 - ) : ( 79 - <ToggleButton.Group 80 - label={_( 81 - msg`Configure content filtering setting for category: ${labelStrings.name.toLowerCase()}`, 82 - )} 83 - values={[visibility ?? 'hide']} 84 - onChange={onChange}> 85 - <ToggleButton.Button name="ignore" label={labels.show}> 86 - <ToggleButton.ButtonText>{labels.show}</ToggleButton.ButtonText> 87 - </ToggleButton.Button> 88 - <ToggleButton.Button name="warn" label={labels.warn}> 89 - <ToggleButton.ButtonText>{labels.warn}</ToggleButton.ButtonText> 90 - </ToggleButton.Button> 91 - <ToggleButton.Button name="hide" label={labels.hide}> 92 - <ToggleButton.ButtonText>{labels.hide}</ToggleButton.ButtonText> 93 - </ToggleButton.Button> 94 - </ToggleButton.Group> 95 - )} 96 - </View> 97 - </View> 98 - ) 99 - }
-110
src/screens/Onboarding/StepModeration/index.tsx
··· 1 - import React from 'react' 2 - import {View} from 'react-native' 3 - import {LABELS} from '@atproto/api' 4 - import {msg, Trans} from '@lingui/macro' 5 - import {useLingui} from '@lingui/react' 6 - 7 - import {useAnalytics} from '#/lib/analytics/analytics' 8 - import {logEvent} from '#/lib/statsig/statsig' 9 - import {usePreferencesQuery} from '#/state/queries/preferences' 10 - import {usePreferencesSetAdultContentMutation} from 'state/queries/preferences' 11 - import { 12 - DescriptionText, 13 - OnboardingControls, 14 - TitleText, 15 - } from '#/screens/Onboarding/Layout' 16 - import {Context} from '#/screens/Onboarding/state' 17 - import {AdultContentEnabledPref} from '#/screens/Onboarding/StepModeration/AdultContentEnabledPref' 18 - import {ModerationOption} from '#/screens/Onboarding/StepModeration/ModerationOption' 19 - import {atoms as a} from '#/alf' 20 - import {Button, ButtonIcon, ButtonText} from '#/components/Button' 21 - import {IconCircle} from '#/components/IconCircle' 22 - import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' 23 - import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash' 24 - import {Loader} from '#/components/Loader' 25 - 26 - export function StepModeration() { 27 - const {_} = useLingui() 28 - const {track} = useAnalytics() 29 - const {state, dispatch} = React.useContext(Context) 30 - const {data: preferences} = usePreferencesQuery() 31 - const {mutate, variables} = usePreferencesSetAdultContentMutation() 32 - 33 - // We need to know if the screen is mounted so we know if we want to run entering animations 34 - // https://github.com/software-mansion/react-native-reanimated/discussions/2513 35 - const isMounted = React.useRef(false) 36 - React.useLayoutEffect(() => { 37 - isMounted.current = true 38 - }, []) 39 - 40 - const adultContentEnabled = !!( 41 - (variables && variables.enabled) || 42 - (!variables && preferences?.moderationPrefs.adultContentEnabled) 43 - ) 44 - 45 - const onContinue = React.useCallback(() => { 46 - dispatch({type: 'next'}) 47 - track('OnboardingV2:StepModeration:End') 48 - logEvent('onboarding:moderation:nextPressed', {}) 49 - }, [track, dispatch]) 50 - 51 - React.useEffect(() => { 52 - track('OnboardingV2:StepModeration:Start') 53 - }, [track]) 54 - 55 - return ( 56 - <View style={[a.align_start]}> 57 - <IconCircle icon={EyeSlash} style={[a.mb_2xl]} /> 58 - 59 - <TitleText> 60 - <Trans>You're in control</Trans> 61 - </TitleText> 62 - <DescriptionText style={[a.mb_xl]}> 63 - <Trans> 64 - Select what you want to see (or not see), and we’ll handle the rest. 65 - </Trans> 66 - </DescriptionText> 67 - 68 - {!preferences ? ( 69 - <View style={[a.pt_md]}> 70 - <Loader size="xl" /> 71 - </View> 72 - ) : ( 73 - <> 74 - <AdultContentEnabledPref mutate={mutate} variables={variables} /> 75 - 76 - <View style={[a.gap_sm, a.w_full]}> 77 - <ModerationOption 78 - labelValueDefinition={LABELS.porn} 79 - disabled={!adultContentEnabled} 80 - /> 81 - <ModerationOption 82 - labelValueDefinition={LABELS.sexual} 83 - disabled={!adultContentEnabled} 84 - /> 85 - <ModerationOption 86 - labelValueDefinition={LABELS['graphic-media']} 87 - disabled={!adultContentEnabled} 88 - /> 89 - <ModerationOption labelValueDefinition={LABELS.nudity} /> 90 - </View> 91 - </> 92 - )} 93 - 94 - <OnboardingControls.Portal> 95 - <Button 96 - key={state.activeStep} // remove focus state on nav 97 - variant="gradient" 98 - color="gradient_sky" 99 - size="large" 100 - label={_(msg`Continue to next step`)} 101 - onPress={onContinue}> 102 - <ButtonText> 103 - <Trans>Continue</Trans> 104 - </ButtonText> 105 - <ButtonIcon icon={ChevronRight} position="right" /> 106 - </Button> 107 - </OnboardingControls.Portal> 108 - </View> 109 - ) 110 - }
+1 -5
src/screens/Onboarding/StepProfile/index.tsx
··· 92 92 }, [track]) 93 93 94 94 React.useEffect(() => { 95 - // We have an experiment running for redueced onboarding, where this screen shows up as the first in onboarding. 96 - // We only want to request permissions when that gate is actually active to prevent pollution 97 - if (gate('reduced_onboarding_and_home_algo_v2')) { 98 - requestNotificationsPermission('StartOnboarding') 99 - } 95 + requestNotificationsPermission('StartOnboarding') 100 96 }, [gate, requestNotificationsPermission]) 101 97 102 98 const openPicker = React.useCallback(
-188
src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx
··· 1 - import React from 'react' 2 - import {View, ViewStyle} from 'react-native' 3 - import {AppBskyActorDefs, moderateProfile} from '@atproto/api' 4 - 5 - import {useModerationOpts} from '#/state/preferences/moderation-opts' 6 - import {UserAvatar} from '#/view/com/util/UserAvatar' 7 - import {atoms as a, flatten, useTheme} from '#/alf' 8 - import {useItemContext} from '#/components/forms/Toggle' 9 - import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 10 - import {RichText} from '#/components/RichText' 11 - import {Text} from '#/components/Typography' 12 - 13 - export function SuggestedAccountCard({ 14 - profile, 15 - moderationOpts, 16 - }: { 17 - profile: AppBskyActorDefs.ProfileViewDetailed 18 - moderationOpts: ReturnType<typeof useModerationOpts> 19 - }) { 20 - const t = useTheme() 21 - const ctx = useItemContext() 22 - const moderation = moderateProfile(profile, moderationOpts!) 23 - 24 - const styles = React.useMemo(() => { 25 - const light = t.name === 'light' 26 - const base: ViewStyle[] = [t.atoms.bg_contrast_50] 27 - const hover: ViewStyle[] = [t.atoms.bg_contrast_25] 28 - const selected: ViewStyle[] = [ 29 - { 30 - backgroundColor: light ? t.palette.primary_50 : t.palette.primary_950, 31 - }, 32 - ] 33 - const selectedHover: ViewStyle[] = [ 34 - { 35 - backgroundColor: light ? t.palette.primary_25 : t.palette.primary_975, 36 - }, 37 - ] 38 - const checkboxBase: ViewStyle[] = [t.atoms.bg] 39 - const checkboxSelected: ViewStyle[] = [ 40 - { 41 - backgroundColor: t.palette.primary_500, 42 - }, 43 - ] 44 - const avatarBase: ViewStyle[] = [t.atoms.bg_contrast_100] 45 - const avatarSelected: ViewStyle[] = [ 46 - { 47 - backgroundColor: light ? t.palette.primary_100 : t.palette.primary_900, 48 - }, 49 - ] 50 - 51 - return { 52 - base, 53 - hover: flatten(hover), 54 - selected: flatten(selected), 55 - selectedHover: flatten(selectedHover), 56 - checkboxBase: flatten(checkboxBase), 57 - checkboxSelected: flatten(checkboxSelected), 58 - avatarBase: flatten(avatarBase), 59 - avatarSelected: flatten(avatarSelected), 60 - } 61 - }, [t]) 62 - 63 - return ( 64 - <View 65 - style={[ 66 - a.w_full, 67 - a.p_md, 68 - a.pr_lg, 69 - a.gap_md, 70 - a.rounded_md, 71 - styles.base, 72 - (ctx.hovered || ctx.focused || ctx.pressed) && styles.hover, 73 - ctx.selected && styles.selected, 74 - ctx.selected && 75 - (ctx.hovered || ctx.focused || ctx.pressed) && 76 - styles.selectedHover, 77 - ]}> 78 - <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}> 79 - <View style={[a.flex_row, a.flex_1, a.align_center, a.gap_md]}> 80 - <View 81 - style={[ 82 - {width: 48, height: 48}, 83 - a.relative, 84 - a.rounded_full, 85 - styles.avatarBase, 86 - ctx.selected && styles.avatarSelected, 87 - ]}> 88 - <UserAvatar 89 - size={48} 90 - avatar={profile.avatar} 91 - moderation={moderation.ui('avatar')} 92 - /> 93 - </View> 94 - <View style={[a.flex_1]}> 95 - <Text style={[a.font_bold, a.text_md, a.pb_xs]} numberOfLines={1}> 96 - {profile.displayName} 97 - </Text> 98 - <Text style={[t.atoms.text_contrast_medium]}>{profile.handle}</Text> 99 - </View> 100 - </View> 101 - 102 - <View 103 - style={[ 104 - a.justify_center, 105 - a.align_center, 106 - a.rounded_sm, 107 - styles.checkboxBase, 108 - ctx.selected && styles.checkboxSelected, 109 - { 110 - width: 28, 111 - height: 28, 112 - }, 113 - ]}> 114 - {ctx.selected && <Check size="sm" fill={t.palette.white} />} 115 - </View> 116 - </View> 117 - 118 - {profile.description && ( 119 - <> 120 - <View 121 - style={[ 122 - { 123 - opacity: ctx.selected ? 0.3 : 1, 124 - borderTopWidth: 1, 125 - }, 126 - a.w_full, 127 - t.atoms.border_contrast_low, 128 - ctx.selected && { 129 - borderTopColor: t.palette.primary_200, 130 - }, 131 - ]} 132 - /> 133 - 134 - <RichText 135 - value={profile.description} 136 - disableLinks 137 - numberOfLines={2} 138 - /> 139 - </> 140 - )} 141 - </View> 142 - ) 143 - } 144 - 145 - export function SuggestedAccountCardPlaceholder() { 146 - const t = useTheme() 147 - return ( 148 - <View 149 - style={[ 150 - a.w_full, 151 - a.flex_row, 152 - a.justify_between, 153 - a.align_center, 154 - a.p_md, 155 - a.pr_lg, 156 - a.gap_xl, 157 - a.rounded_md, 158 - t.atoms.bg_contrast_25, 159 - ]}> 160 - <View style={[a.flex_row, a.align_center, a.gap_md]}> 161 - <View 162 - style={[ 163 - {width: 48, height: 48}, 164 - a.relative, 165 - a.rounded_full, 166 - t.atoms.bg_contrast_100, 167 - ]} 168 - /> 169 - <View style={[a.gap_xs]}> 170 - <View 171 - style={[ 172 - {width: 100, height: 16}, 173 - a.rounded_sm, 174 - t.atoms.bg_contrast_100, 175 - ]} 176 - /> 177 - <View 178 - style={[ 179 - {width: 60, height: 12}, 180 - a.rounded_sm, 181 - t.atoms.bg_contrast_100, 182 - ]} 183 - /> 184 - </View> 185 - </View> 186 - </View> 187 - ) 188 - }
-210
src/screens/Onboarding/StepSuggestedAccounts/index.tsx
··· 1 - import React from 'react' 2 - import {View} from 'react-native' 3 - import {AppBskyActorDefs} from '@atproto/api' 4 - import {msg, Trans} from '@lingui/macro' 5 - import {useLingui} from '@lingui/react' 6 - 7 - import {useAnalytics} from '#/lib/analytics/analytics' 8 - import {logEvent} from '#/lib/statsig/statsig' 9 - import {capitalize} from '#/lib/strings/capitalize' 10 - import {useModerationOpts} from '#/state/preferences/moderation-opts' 11 - import {useProfilesQuery} from '#/state/queries/profile' 12 - import { 13 - DescriptionText, 14 - OnboardingControls, 15 - TitleText, 16 - } from '#/screens/Onboarding/Layout' 17 - import {Context} from '#/screens/Onboarding/state' 18 - import { 19 - SuggestedAccountCard, 20 - SuggestedAccountCardPlaceholder, 21 - } from '#/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard' 22 - import {aggregateInterestItems} from '#/screens/Onboarding/util' 23 - import {atoms as a, useBreakpoints} from '#/alf' 24 - import {Button, ButtonIcon, ButtonText} from '#/components/Button' 25 - import * as Toggle from '#/components/forms/Toggle' 26 - import {IconCircle} from '#/components/IconCircle' 27 - import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' 28 - import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 29 - import {Loader} from '#/components/Loader' 30 - import {Text} from '#/components/Typography' 31 - 32 - export function Inner({ 33 - profiles, 34 - onSelect, 35 - moderationOpts, 36 - }: { 37 - profiles: AppBskyActorDefs.ProfileViewDetailed[] 38 - onSelect: (dids: string[]) => void 39 - moderationOpts: ReturnType<typeof useModerationOpts> 40 - }) { 41 - const {_} = useLingui() 42 - const [dids, setDids] = React.useState<string[]>(profiles.map(p => p.did)) 43 - 44 - React.useEffect(() => { 45 - onSelect(dids) 46 - }, [dids, onSelect]) 47 - 48 - return ( 49 - <Toggle.Group 50 - values={dids} 51 - onChange={setDids} 52 - label={_(msg`Select some accounts below to follow`)}> 53 - <View style={[a.gap_md]}> 54 - {profiles.map(profile => ( 55 - <Toggle.Item 56 - key={profile.did} 57 - name={profile.did} 58 - label={_(msg`Follow ${profile.handle}`)}> 59 - <SuggestedAccountCard 60 - profile={profile} 61 - moderationOpts={moderationOpts} 62 - /> 63 - </Toggle.Item> 64 - ))} 65 - </View> 66 - </Toggle.Group> 67 - ) 68 - } 69 - 70 - export function StepSuggestedAccounts() { 71 - const {_} = useLingui() 72 - const {gtMobile} = useBreakpoints() 73 - const {track} = useAnalytics() 74 - const {state, dispatch, interestsDisplayNames} = React.useContext(Context) 75 - const suggestedDids = React.useMemo(() => { 76 - return aggregateInterestItems( 77 - state.interestsStepResults.selectedInterests, 78 - state.interestsStepResults.apiResponse.suggestedAccountDids, 79 - state.interestsStepResults.apiResponse.suggestedAccountDids.default || [], 80 - ) 81 - }, [state.interestsStepResults]) 82 - const moderationOpts = useModerationOpts() 83 - const { 84 - isLoading: isProfilesLoading, 85 - isError, 86 - data, 87 - error, 88 - } = useProfilesQuery({ 89 - handles: suggestedDids, 90 - }) 91 - const [dids, setDids] = React.useState<string[]>([]) 92 - const [saving, setSaving] = React.useState(false) 93 - 94 - const interestsText = React.useMemo(() => { 95 - const i = state.interestsStepResults.selectedInterests.map( 96 - i => interestsDisplayNames[i] || capitalize(i), 97 - ) 98 - return i.join(', ') 99 - }, [state.interestsStepResults.selectedInterests, interestsDisplayNames]) 100 - 101 - const handleContinue = React.useCallback(async () => { 102 - setSaving(true) 103 - 104 - if (dids.length) { 105 - dispatch({type: 'setSuggestedAccountsStepResults', accountDids: dids}) 106 - } 107 - 108 - setSaving(false) 109 - dispatch({type: 'next'}) 110 - track('OnboardingV2:StepSuggestedAccounts:End', { 111 - selectedAccountsLength: dids.length, 112 - }) 113 - logEvent('onboarding:suggestedAccounts:nextPressed', { 114 - selectedAccountsLength: dids.length, 115 - skipped: false, 116 - }) 117 - }, [dids, setSaving, dispatch, track]) 118 - 119 - const handleSkip = React.useCallback(() => { 120 - // if a user comes back and clicks skip, erase follows 121 - dispatch({type: 'setSuggestedAccountsStepResults', accountDids: []}) 122 - dispatch({type: 'next'}) 123 - logEvent('onboarding:suggestedAccounts:nextPressed', { 124 - selectedAccountsLength: 0, 125 - skipped: true, 126 - }) 127 - }, [dispatch]) 128 - 129 - const isLoading = isProfilesLoading && moderationOpts 130 - 131 - React.useEffect(() => { 132 - track('OnboardingV2:StepSuggestedAccounts:Start') 133 - }, [track]) 134 - 135 - return ( 136 - <View style={[a.align_start]}> 137 - <IconCircle icon={At} style={[a.mb_2xl]} /> 138 - 139 - <TitleText> 140 - <Trans>Here are some accounts for you to follow</Trans> 141 - </TitleText> 142 - <DescriptionText> 143 - {state.interestsStepResults.selectedInterests.length ? ( 144 - <Trans>Based on your interest in {interestsText}</Trans> 145 - ) : ( 146 - <Trans>These are popular accounts you might like:</Trans> 147 - )} 148 - </DescriptionText> 149 - 150 - <View style={[a.w_full, a.pt_xl]}> 151 - {isLoading ? ( 152 - <View style={[a.gap_md]}> 153 - {Array(10) 154 - .fill(0) 155 - .map((_, i) => ( 156 - <SuggestedAccountCardPlaceholder key={i} /> 157 - ))} 158 - </View> 159 - ) : isError || !data ? ( 160 - <Text>{error?.toString()}</Text> 161 - ) : ( 162 - <Inner 163 - profiles={data.profiles} 164 - onSelect={setDids} 165 - moderationOpts={moderationOpts} 166 - /> 167 - )} 168 - </View> 169 - 170 - <OnboardingControls.Portal> 171 - <View 172 - style={[ 173 - a.gap_md, 174 - gtMobile ? {flexDirection: 'row-reverse'} : a.flex_col, 175 - ]}> 176 - <Button 177 - disabled={dids.length === 0} 178 - variant="gradient" 179 - color="gradient_sky" 180 - size="large" 181 - label={_( 182 - msg`Follow selected accounts and continue to the next step`, 183 - )} 184 - onPress={handleContinue}> 185 - <ButtonText> 186 - {dids.length === 20 ? ( 187 - <Trans>Follow All</Trans> 188 - ) : ( 189 - <Trans>Follow</Trans> 190 - )} 191 - </ButtonText> 192 - <ButtonIcon icon={saving ? Loader : Plus} position="right" /> 193 - </Button> 194 - <Button 195 - variant="solid" 196 - color="secondary" 197 - size="large" 198 - label={_( 199 - msg`Continue to the next step without following any accounts`, 200 - )} 201 - onPress={handleSkip}> 202 - <ButtonText> 203 - <Trans>Skip</Trans> 204 - </ButtonText> 205 - </Button> 206 - </View> 207 - </OnboardingControls.Portal> 208 - </View> 209 - ) 210 - }
-125
src/screens/Onboarding/StepTopicalFeeds.tsx
··· 1 - import React from 'react' 2 - import {View} from 'react-native' 3 - import {msg, Trans} from '@lingui/macro' 4 - import {useLingui} from '@lingui/react' 5 - 6 - import {useAnalytics} from '#/lib/analytics/analytics' 7 - import {logEvent} from '#/lib/statsig/statsig' 8 - import {capitalize} from '#/lib/strings/capitalize' 9 - import {IS_TEST_USER} from 'lib/constants' 10 - import {useSession} from 'state/session' 11 - import { 12 - DescriptionText, 13 - OnboardingControls, 14 - TitleText, 15 - } from '#/screens/Onboarding/Layout' 16 - import {Context} from '#/screens/Onboarding/state' 17 - import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard' 18 - import {aggregateInterestItems} from '#/screens/Onboarding/util' 19 - import {atoms as a} from '#/alf' 20 - import {Button, ButtonIcon, ButtonText} from '#/components/Button' 21 - import * as Toggle from '#/components/forms/Toggle' 22 - import {IconCircle} from '#/components/IconCircle' 23 - import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' 24 - import {ListMagnifyingGlass_Stroke2_Corner0_Rounded as ListMagnifyingGlass} from '#/components/icons/ListMagnifyingGlass' 25 - import {Loader} from '#/components/Loader' 26 - 27 - export function StepTopicalFeeds() { 28 - const {_} = useLingui() 29 - const {track} = useAnalytics() 30 - const {currentAccount} = useSession() 31 - const {state, dispatch, interestsDisplayNames} = React.useContext(Context) 32 - const [selectedFeedUris, setSelectedFeedUris] = React.useState<string[]>([]) 33 - const [saving, setSaving] = React.useState(false) 34 - const suggestedFeedUris = React.useMemo(() => { 35 - if (IS_TEST_USER(currentAccount?.handle)) return [] 36 - return aggregateInterestItems( 37 - state.interestsStepResults.selectedInterests, 38 - state.interestsStepResults.apiResponse.suggestedFeedUris, 39 - state.interestsStepResults.apiResponse.suggestedFeedUris.default || [], 40 - ).slice(0, 10) 41 - }, [ 42 - currentAccount?.handle, 43 - state.interestsStepResults.apiResponse.suggestedFeedUris, 44 - state.interestsStepResults.selectedInterests, 45 - ]) 46 - 47 - const interestsText = React.useMemo(() => { 48 - const i = state.interestsStepResults.selectedInterests.map( 49 - i => interestsDisplayNames[i] || capitalize(i), 50 - ) 51 - return i.join(', ') 52 - }, [state.interestsStepResults.selectedInterests, interestsDisplayNames]) 53 - 54 - const saveFeeds = React.useCallback(async () => { 55 - setSaving(true) 56 - 57 - dispatch({type: 'setTopicalFeedsStepResults', feedUris: selectedFeedUris}) 58 - 59 - setSaving(false) 60 - dispatch({type: 'next'}) 61 - track('OnboardingV2:StepTopicalFeeds:End', { 62 - selectedFeeds: selectedFeedUris, 63 - selectedFeedsLength: selectedFeedUris.length, 64 - }) 65 - logEvent('onboarding:topicalFeeds:nextPressed', { 66 - selectedFeeds: selectedFeedUris, 67 - selectedFeedsLength: selectedFeedUris.length, 68 - }) 69 - }, [selectedFeedUris, dispatch, track]) 70 - 71 - React.useEffect(() => { 72 - track('OnboardingV2:StepTopicalFeeds:Start') 73 - }, [track]) 74 - 75 - return ( 76 - <View style={[a.align_start]}> 77 - <IconCircle icon={ListMagnifyingGlass} style={[a.mb_2xl]} /> 78 - 79 - <TitleText> 80 - <Trans>Feeds can be topical as well!</Trans> 81 - </TitleText> 82 - <DescriptionText> 83 - {state.interestsStepResults.selectedInterests.length ? ( 84 - <Trans> 85 - Here are some topical feeds based on your interests: {interestsText} 86 - . You can choose to follow as many as you like. 87 - </Trans> 88 - ) : ( 89 - <Trans> 90 - Here are some popular topical feeds. You can choose to follow as 91 - many as you like. 92 - </Trans> 93 - )} 94 - </DescriptionText> 95 - 96 - <View style={[a.w_full, a.pb_2xl, a.pt_2xl]}> 97 - <Toggle.Group 98 - values={selectedFeedUris} 99 - onChange={setSelectedFeedUris} 100 - label={_(msg`Select topical feeds to follow from the list below`)}> 101 - <View style={[a.gap_md]}> 102 - {suggestedFeedUris.map(uri => ( 103 - <FeedCard key={uri} config={{default: false, uri}} /> 104 - ))} 105 - </View> 106 - </Toggle.Group> 107 - </View> 108 - 109 - <OnboardingControls.Portal> 110 - <Button 111 - key={state.activeStep} // remove focus state on nav 112 - variant="gradient" 113 - color="gradient_sky" 114 - size="large" 115 - label={_(msg`Continue to next step`)} 116 - onPress={saveFeeds}> 117 - <ButtonText> 118 - <Trans>Continue</Trans> 119 - </ButtonText> 120 - <ButtonIcon icon={saving ? Loader : ChevronRight} position="right" /> 121 - </Button> 122 - </OnboardingControls.Portal> 123 - </View> 124 - ) 125 - }
+4 -26
src/screens/Onboarding/index.tsx
··· 2 2 import {msg} from '@lingui/macro' 3 3 import {useLingui} from '@lingui/react' 4 4 5 - import {useGate} from '#/lib/statsig/statsig' 6 5 import {Layout, OnboardingControls} from '#/screens/Onboarding/Layout' 7 - import { 8 - Context, 9 - initialState, 10 - initialStateReduced, 11 - reducer, 12 - reducerReduced, 13 - } from '#/screens/Onboarding/state' 14 - import {StepAlgoFeeds} from '#/screens/Onboarding/StepAlgoFeeds' 6 + import {Context, initialState, reducer} from '#/screens/Onboarding/state' 15 7 import {StepFinished} from '#/screens/Onboarding/StepFinished' 16 - import {StepFollowingFeed} from '#/screens/Onboarding/StepFollowingFeed' 17 8 import {StepInterests} from '#/screens/Onboarding/StepInterests' 18 - import {StepModeration} from '#/screens/Onboarding/StepModeration' 19 9 import {StepProfile} from '#/screens/Onboarding/StepProfile' 20 - import {StepSuggestedAccounts} from '#/screens/Onboarding/StepSuggestedAccounts' 21 - import {StepTopicalFeeds} from '#/screens/Onboarding/StepTopicalFeeds' 22 10 import {Portal} from '#/components/Portal' 23 11 24 12 export function Onboarding() { 25 13 const {_} = useLingui() 26 - const gate = useGate() 27 - const isReducedOnboardingEnabled = gate('reduced_onboarding_and_home_algo_v2') 28 - const [state, dispatch] = React.useReducer( 29 - isReducedOnboardingEnabled ? reducerReduced : reducer, 30 - isReducedOnboardingEnabled ? {...initialStateReduced} : {...initialState}, 31 - ) 14 + const [state, dispatch] = React.useReducer(reducer, { 15 + ...initialState, 16 + }) 32 17 33 18 const interestsDisplayNames = React.useMemo(() => { 34 19 return { ··· 68 53 <Layout> 69 54 {state.activeStep === 'profile' && <StepProfile />} 70 55 {state.activeStep === 'interests' && <StepInterests />} 71 - {state.activeStep === 'suggestedAccounts' && ( 72 - <StepSuggestedAccounts /> 73 - )} 74 - {state.activeStep === 'followingFeed' && <StepFollowingFeed />} 75 - {state.activeStep === 'algoFeeds' && <StepAlgoFeeds />} 76 - {state.activeStep === 'topicalFeeds' && <StepTopicalFeeds />} 77 - {state.activeStep === 'moderation' && <StepModeration />} 78 56 {state.activeStep === 'finished' && <StepFinished />} 79 57 </Layout> 80 58 </Context.Provider>
+14 -204
src/screens/Onboarding/state.ts
··· 6 6 export type OnboardingState = { 7 7 hasPrev: boolean 8 8 totalSteps: number 9 - activeStep: 10 - | 'profile' 11 - | 'interests' 12 - | 'suggestedAccounts' 13 - | 'followingFeed' 14 - | 'algoFeeds' 15 - | 'topicalFeeds' 16 - | 'moderation' 17 - | 'profile' 18 - | 'finished' 9 + activeStep: 'profile' | 'interests' | 'finished' 19 10 activeStepIndex: number 20 11 21 12 interestsStepResults: { 22 13 selectedInterests: string[] 23 14 apiResponse: ApiResponseMap 24 - } 25 - suggestedAccountsStepResults: { 26 - accountDids: string[] 27 - } 28 - algoFeedsStepResults: { 29 - feedUris: string[] 30 - } 31 - topicalFeedsStepResults: { 32 - feedUris: string[] 33 15 } 34 16 profileStepResults: { 35 17 isCreatedAvatar: boolean ··· 65 47 apiResponse: ApiResponseMap 66 48 } 67 49 | { 68 - type: 'setSuggestedAccountsStepResults' 69 - accountDids: string[] 70 - } 71 - | { 72 - type: 'setAlgoFeedsStepResults' 73 - feedUris: string[] 74 - } 75 - | { 76 - type: 'setTopicalFeedsStepResults' 77 - feedUris: string[] 78 - } 79 - | { 80 50 type: 'setProfileStepResults' 81 51 isCreatedAvatar: boolean 82 52 image?: OnboardingState['profileStepResults']['image'] ··· 98 68 } 99 69 } 100 70 101 - export const initialState: OnboardingState = { 102 - hasPrev: false, 103 - totalSteps: 7, 104 - activeStep: 'interests', 105 - activeStepIndex: 1, 106 - 107 - interestsStepResults: { 108 - selectedInterests: [], 109 - apiResponse: { 110 - interests: [], 111 - suggestedAccountDids: {}, 112 - suggestedFeedUris: {}, 113 - }, 114 - }, 115 - suggestedAccountsStepResults: { 116 - accountDids: [], 117 - }, 118 - algoFeedsStepResults: { 119 - feedUris: [], 120 - }, 121 - topicalFeedsStepResults: { 122 - feedUris: [], 123 - }, 124 - profileStepResults: { 125 - isCreatedAvatar: false, 126 - image: undefined, 127 - imageUri: '', 128 - imageMime: '', 129 - }, 130 - } 131 - 132 71 export const INTEREST_TO_DISPLAY_NAME_DEFAULTS: { 133 72 [key: string]: string 134 73 } = { ··· 156 95 cooking: 'Cooking', 157 96 } 158 97 159 - export const Context = React.createContext<{ 160 - state: OnboardingState 161 - dispatch: React.Dispatch<OnboardingAction> 162 - interestsDisplayNames: {[key: string]: string} 163 - }>({ 164 - state: {...initialState}, 165 - dispatch: () => {}, 166 - interestsDisplayNames: INTEREST_TO_DISPLAY_NAME_DEFAULTS, 167 - }) 168 - 169 - export function reducer( 170 - s: OnboardingState, 171 - a: OnboardingAction, 172 - ): OnboardingState { 173 - let next = {...s} 174 - 175 - switch (a.type) { 176 - case 'next': { 177 - if (s.activeStep === 'interests') { 178 - next.activeStep = 'suggestedAccounts' 179 - next.activeStepIndex = 2 180 - } else if (s.activeStep === 'suggestedAccounts') { 181 - next.activeStep = 'followingFeed' 182 - next.activeStepIndex = 3 183 - } else if (s.activeStep === 'followingFeed') { 184 - next.activeStep = 'algoFeeds' 185 - next.activeStepIndex = 4 186 - } else if (s.activeStep === 'algoFeeds') { 187 - next.activeStep = 'topicalFeeds' 188 - next.activeStepIndex = 5 189 - } else if (s.activeStep === 'topicalFeeds') { 190 - next.activeStep = 'moderation' 191 - next.activeStepIndex = 6 192 - } else if (s.activeStep === 'moderation') { 193 - next.activeStep = 'finished' 194 - next.activeStepIndex = 7 195 - } 196 - break 197 - } 198 - case 'prev': { 199 - if (s.activeStep === 'suggestedAccounts') { 200 - next.activeStep = 'interests' 201 - next.activeStepIndex = 1 202 - } else if (s.activeStep === 'followingFeed') { 203 - next.activeStep = 'suggestedAccounts' 204 - next.activeStepIndex = 2 205 - } else if (s.activeStep === 'algoFeeds') { 206 - next.activeStep = 'followingFeed' 207 - next.activeStepIndex = 3 208 - } else if (s.activeStep === 'topicalFeeds') { 209 - next.activeStep = 'algoFeeds' 210 - next.activeStepIndex = 4 211 - } else if (s.activeStep === 'moderation') { 212 - next.activeStep = 'topicalFeeds' 213 - next.activeStepIndex = 5 214 - } else if (s.activeStep === 'finished') { 215 - next.activeStep = 'moderation' 216 - next.activeStepIndex = 6 217 - } 218 - break 219 - } 220 - case 'finish': { 221 - next = initialState 222 - break 223 - } 224 - case 'setInterestsStepResults': { 225 - next.interestsStepResults = { 226 - selectedInterests: a.selectedInterests, 227 - apiResponse: a.apiResponse, 228 - } 229 - break 230 - } 231 - case 'setSuggestedAccountsStepResults': { 232 - next.suggestedAccountsStepResults = { 233 - accountDids: next.suggestedAccountsStepResults.accountDids.concat( 234 - a.accountDids, 235 - ), 236 - } 237 - break 238 - } 239 - case 'setAlgoFeedsStepResults': { 240 - next.algoFeedsStepResults = { 241 - feedUris: a.feedUris, 242 - } 243 - break 244 - } 245 - case 'setTopicalFeedsStepResults': { 246 - next.topicalFeedsStepResults = { 247 - feedUris: next.topicalFeedsStepResults.feedUris.concat(a.feedUris), 248 - } 249 - break 250 - } 251 - } 252 - 253 - const state = { 254 - ...next, 255 - hasPrev: next.activeStep !== 'interests', 256 - } 257 - 258 - logger.debug(`onboarding`, { 259 - hasPrev: state.hasPrev, 260 - activeStep: state.activeStep, 261 - activeStepIndex: state.activeStepIndex, 262 - interestsStepResults: { 263 - selectedInterests: state.interestsStepResults.selectedInterests, 264 - }, 265 - suggestedAccountsStepResults: state.suggestedAccountsStepResults, 266 - algoFeedsStepResults: state.algoFeedsStepResults, 267 - topicalFeedsStepResults: state.topicalFeedsStepResults, 268 - }) 269 - 270 - if (s.activeStep !== state.activeStep) { 271 - logger.debug(`onboarding: step changed`, {activeStep: state.activeStep}) 272 - } 273 - 274 - return state 275 - } 276 - 277 - export const initialStateReduced: OnboardingState = { 98 + export const initialState: OnboardingState = { 278 99 hasPrev: false, 279 100 totalSteps: 3, 280 101 activeStep: 'profile', ··· 288 109 suggestedFeedUris: {}, 289 110 }, 290 111 }, 291 - suggestedAccountsStepResults: { 292 - accountDids: [], 293 - }, 294 - algoFeedsStepResults: { 295 - feedUris: [], 296 - }, 297 - topicalFeedsStepResults: { 298 - feedUris: [], 299 - }, 300 112 profileStepResults: { 301 113 isCreatedAvatar: false, 302 114 image: undefined, ··· 305 117 }, 306 118 } 307 119 308 - export function reducerReduced( 120 + export const Context = React.createContext<{ 121 + state: OnboardingState 122 + dispatch: React.Dispatch<OnboardingAction> 123 + interestsDisplayNames: {[key: string]: string} 124 + }>({ 125 + state: {...initialState}, 126 + dispatch: () => {}, 127 + interestsDisplayNames: INTEREST_TO_DISPLAY_NAME_DEFAULTS, 128 + }) 129 + 130 + export function reducer( 309 131 s: OnboardingState, 310 132 a: OnboardingAction, 311 133 ): OnboardingState { ··· 333 155 break 334 156 } 335 157 case 'finish': { 336 - next = initialStateReduced 158 + next = initialState 337 159 break 338 160 } 339 161 case 'setInterestsStepResults': { ··· 341 163 selectedInterests: a.selectedInterests, 342 164 apiResponse: a.apiResponse, 343 165 } 344 - break 345 - } 346 - case 'setSuggestedAccountsStepResults': { 347 - break 348 - } 349 - case 'setAlgoFeedsStepResults': { 350 - break 351 - } 352 - case 'setTopicalFeedsStepResults': { 353 166 break 354 167 } 355 168 case 'setProfileStepResults': { ··· 376 189 interestsStepResults: { 377 190 selectedInterests: state.interestsStepResults.selectedInterests, 378 191 }, 379 - suggestedAccountsStepResults: state.suggestedAccountsStepResults, 380 - algoFeedsStepResults: state.algoFeedsStepResults, 381 - topicalFeedsStepResults: state.topicalFeedsStepResults, 382 192 profileStepResults: state.profileStepResults, 383 193 }) 384 194
-76
src/screens/Onboarding/util.ts
··· 5 5 } from '@atproto/api' 6 6 7 7 import {until} from '#/lib/async/until' 8 - import {PRIMARY_FEEDS} from './StepAlgoFeeds' 9 - 10 - function shuffle(array: any) { 11 - let currentIndex = array.length, 12 - randomIndex 13 - 14 - // While there remain elements to shuffle. 15 - while (currentIndex > 0) { 16 - // Pick a remaining element. 17 - randomIndex = Math.floor(Math.random() * currentIndex) 18 - currentIndex-- 19 - 20 - // And swap it with the current element. 21 - ;[array[currentIndex], array[randomIndex]] = [ 22 - array[randomIndex], 23 - array[currentIndex], 24 - ] 25 - } 26 - 27 - return array 28 - } 29 - 30 - export function aggregateInterestItems( 31 - interests: string[], 32 - map: {[key: string]: string[]}, 33 - fallbackItems: string[], 34 - ) { 35 - const selected = interests.length 36 - const all = interests 37 - .map(i => { 38 - // suggestions from server 39 - const rawSuggestions = map[i] 40 - 41 - // safeguard against a missing interest->suggestion mapping 42 - if (!rawSuggestions || !rawSuggestions.length) { 43 - return [] 44 - } 45 - 46 - const suggestions = shuffle(rawSuggestions) 47 - 48 - if (selected === 1) { 49 - return suggestions // return all 50 - } else if (selected === 2) { 51 - return suggestions.slice(0, 5) // return 5 52 - } else { 53 - return suggestions.slice(0, 3) // return 3 54 - } 55 - }) 56 - .flat() 57 - // dedupe suggestions 58 - const results = Array.from(new Set(all)) 59 - 60 - // backfill 61 - if (results.length < 20) { 62 - results.push(...shuffle(fallbackItems)) 63 - } 64 - 65 - // dedupe and return 20 66 - return Array.from(new Set(results)).slice(0, 20) 67 - } 68 8 69 9 export async function bulkWriteFollows(agent: BskyAgent, dids: string[]) { 70 10 const session = agent.session ··· 109 49 }), 110 50 ) 111 51 } 112 - 113 - /** 114 - * Kinda hacky, but we want Discover to appear as the first pinned 115 - * feed after Following 116 - */ 117 - export function sortPrimaryAlgorithmFeeds(uris: string[]) { 118 - return uris.sort((a, b) => { 119 - if (a === PRIMARY_FEEDS[0]?.uri) { 120 - return -1 121 - } 122 - if (b === PRIMARY_FEEDS[0]?.uri) { 123 - return 1 124 - } 125 - return a.localeCompare(b) 126 - }) 127 - }
-6
src/view/com/testing/TestCtrls.e2e.tsx
··· 2 2 import {LogBox, Pressable, View} from 'react-native' 3 3 import {useQueryClient} from '@tanstack/react-query' 4 4 5 - import {useDangerousSetGate} from '#/lib/statsig/statsig' 6 5 import {useModalControls} from '#/state/modals' 7 6 import {useSessionApi} from '#/state/session' 8 7 import {useLoggedOutViewControls} from '#/state/shell/logged-out' ··· 25 24 const {openModal} = useModalControls() 26 25 const onboardingDispatch = useOnboardingDispatch() 27 26 const {setShowLoggedOut} = useLoggedOutViewControls() 28 - const setGate = useDangerousSetGate() 29 27 const onPressSignInAlice = async () => { 30 28 await login( 31 29 { ··· 117 115 <Pressable 118 116 testID="e2eStartOnboarding" 119 117 onPress={() => { 120 - // TODO remove when experiment is over 121 - setGate('reduced_onboarding_and_home_algo_v2', true) 122 118 onboardingDispatch({type: 'start'}) 123 119 }} 124 120 accessibilityRole="button" ··· 128 124 <Pressable 129 125 testID="e2eStartLongboarding" 130 126 onPress={() => { 131 - // TODO remove when experiment is over 132 - setGate('reduced_onboarding_and_home_algo_v2', false) 133 127 onboardingDispatch({type: 'start'}) 134 128 }} 135 129 accessibilityRole="button"