tangled mirror of catsky-🐱 Soothing soft social-app fork with all the niche toggles! (Unofficial); for issues and PRs please put them on github:NekoDrone/catsky-social

Value prop screen - polish, convert to pager (#9133)

* turn value prop screen into a pager on native

* rm ts-ingore

* fix pager swipe on android

---------

Co-authored-by: Eric Bailey <git@esb.lol>

authored by samuel.fm Eric Bailey and committed by GitHub 508fa2ac d528ae79

assets/images/onboarding/value_prop_1_dark.webp

This is a binary file and will not be displayed.

assets/images/onboarding/value_prop_1_dark_borderless.webp

This is a binary file and will not be displayed.

assets/images/onboarding/value_prop_1_dim.webp

This is a binary file and will not be displayed.

assets/images/onboarding/value_prop_1_dim_borderless.webp

This is a binary file and will not be displayed.

assets/images/onboarding/value_prop_1_light.webp

This is a binary file and will not be displayed.

assets/images/onboarding/value_prop_1_light_borderless.webp

This is a binary file and will not be displayed.

+7 -10
src/screens/Onboarding/Layout.tsx
··· 5 5 import {msg} from '@lingui/macro' 6 6 import {useLingui} from '@lingui/react' 7 7 8 - import {isWeb} from '#/platform/detection' 8 + import {isAndroid, isWeb} from '#/platform/detection' 9 9 import {useOnboardingDispatch} from '#/state/shell' 10 10 import {Context} from '#/screens/Onboarding/state' 11 11 import { ··· 59 59 aria-label={dialogLabel} 60 60 accessibilityLabel={dialogLabel} 61 61 accessibilityHint={_(msg`Customizes your Bluesky experience`)} 62 - style={[ 63 - // @ts-ignore web only -prf 64 - isWeb ? a.fixed : a.absolute, 65 - a.inset_0, 66 - a.flex_1, 67 - t.atoms.bg, 68 - ]}> 62 + style={[isWeb ? a.fixed : a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}> 69 63 {__DEV__ && ( 70 64 <Button 71 65 variant="ghost" ··· 135 129 <ScrollView 136 130 ref={scrollview} 137 131 style={[a.h_full, a.w_full, {paddingTop: insets.top}]} 138 - contentContainerStyle={{borderWidth: 0}} 132 + contentContainerStyle={{borderWidth: 0, minHeight: '100%'}} 133 + showsVerticalScrollIndicator={!isAndroid} 139 134 scrollIndicatorInsets={{bottom: footerHeight - insets.bottom}} 140 135 // @ts-expect-error web only --prf 141 136 dataSet={{'stable-gutters': 1}}> ··· 173 168 </View> 174 169 </View> 175 170 176 - <View style={[a.w_full, a.mb_5xl, a.pt_md]}>{children}</View> 171 + <View style={[a.w_full, a.h_full, a.mb_5xl, a.pt_md]}> 172 + {children} 173 + </View> 177 174 178 175 <View style={{height: 100 + footerHeight}} /> 179 176 </View>
+9 -158
src/screens/Onboarding/StepFinished.tsx src/screens/Onboarding/StepFinished/index.tsx
··· 1 1 import {useCallback, useContext, useState} from 'react' 2 2 import {View} from 'react-native' 3 - import Animated, { 4 - Easing, 5 - LayoutAnimationConfig, 6 - SlideInRight, 7 - SlideOutLeft, 8 - } from 'react-native-reanimated' 9 - import {Image} from 'expo-image' 10 3 import { 11 4 type AppBskyActorDefs, 12 5 type AppBskyActorProfile, ··· 29 22 import {useRequestNotificationsPermission} from '#/lib/notifications/notifications' 30 23 import {logEvent, useGate} from '#/lib/statsig/statsig' 31 24 import {logger} from '#/logger' 32 - import {isNative} from '#/platform/detection' 25 + import {isWeb} from '#/platform/detection' 33 26 import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs' 34 27 import {getAllListMembers} from '#/state/queries/list-members' 35 28 import {preferencesQueryKey} from '#/state/queries/preferences' ··· 49 42 } from '#/screens/Onboarding/Layout' 50 43 import {Context, type OnboardingState} from '#/screens/Onboarding/state' 51 44 import {bulkWriteFollows} from '#/screens/Onboarding/util' 52 - import { 53 - atoms as a, 54 - native, 55 - platform, 56 - tokens, 57 - useBreakpoints, 58 - useTheme, 59 - } from '#/alf' 45 + import {atoms as a, useBreakpoints, useTheme} from '#/alf' 60 46 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 61 47 import {IconCircle} from '#/components/IconCircle' 62 48 import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow' ··· 67 53 import {Loader} from '#/components/Loader' 68 54 import {Text} from '#/components/Typography' 69 55 import * as bsky from '#/types/bsky' 56 + import {ValuePropositionPager} from './ValuePropositionPager' 70 57 71 58 export function StepFinished() { 72 59 const {state, dispatch} = useContext(Context) ··· 275 262 ) 276 263 } 277 264 278 - const PROP_1 = { 279 - light: platform({ 280 - native: require('../../../assets/images/onboarding/value_prop_1_light.webp'), 281 - web: require('../../../assets/images/onboarding/value_prop_1_light_borderless.webp'), 282 - }), 283 - dim: platform({ 284 - native: require('../../../assets/images/onboarding/value_prop_1_dim.webp'), 285 - web: require('../../../assets/images/onboarding/value_prop_1_dim_borderless.webp'), 286 - }), 287 - dark: platform({ 288 - native: require('../../../assets/images/onboarding/value_prop_1_dark.webp'), 289 - web: require('../../../assets/images/onboarding/value_prop_1_dark_borderless.webp'), 290 - }), 291 - } as const 292 - 293 - const PROP_2 = { 294 - light: require('../../../assets/images/onboarding/value_prop_2_light.webp'), 295 - dim: require('../../../assets/images/onboarding/value_prop_2_dim.webp'), 296 - dark: require('../../../assets/images/onboarding/value_prop_2_dark.webp'), 297 - } as const 298 - 299 - const PROP_3 = { 300 - light: require('../../../assets/images/onboarding/value_prop_3_light.webp'), 301 - dim: require('../../../assets/images/onboarding/value_prop_3_dim.webp'), 302 - dark: require('../../../assets/images/onboarding/value_prop_3_dark.webp'), 303 - } as const 304 - 305 265 function ValueProposition({ 306 266 finishOnboarding, 307 267 saving, ··· 312 272 state: OnboardingState 313 273 }) { 314 274 const [subStep, setSubStep] = useState<0 | 1 | 2>(0) 315 - const t = useTheme() 316 275 const {_} = useLingui() 317 276 const {gtMobile} = useBreakpoints() 318 277 319 - const image = [PROP_1[t.name], PROP_2[t.name], PROP_3[t.name]][subStep] 320 - 321 278 const onPress = () => { 322 279 if (subStep === 2) { 323 280 finishOnboarding() // has its own metrics ··· 330 287 } 331 288 } 332 289 333 - const {title, description, alt} = [ 334 - { 335 - title: _(msg`Free your feed`), 336 - description: _( 337 - msg`No more doomscrolling junk-filled algorithms. Find feeds that work for you, not against you.`, 338 - ), 339 - alt: _( 340 - msg`A collection of popular feeds you can find on Bluesky, including News, Booksky, Game Dev, Blacksky, and Fountain Pens`, 341 - ), 342 - }, 343 - { 344 - title: _(msg`Find your people`), 345 - description: _( 346 - msg`Ditch the trolls and clickbait. Find real people and conversations that matter to you.`, 347 - ), 348 - alt: _( 349 - msg`Your profile picture surrounded by concentric circles of other users' profile pictures`, 350 - ), 351 - }, 352 - { 353 - title: _(msg`Forget the noise`), 354 - description: _( 355 - msg`No ads, no invasive tracking, no engagement traps. Bluesky respects your time and attention.`, 356 - ), 357 - alt: _( 358 - msg`An illustration of several Bluesky posts alongside repost, like, and comment icons`, 359 - ), 360 - }, 361 - ][subStep] 362 - 363 290 return ( 364 291 <> 365 292 {!gtMobile && ( ··· 382 309 </OnboardingHeaderSlot.Portal> 383 310 )} 384 311 385 - <LayoutAnimationConfig skipEntering skipExiting> 386 - <Animated.View 387 - key={subStep} 388 - entering={native( 389 - SlideInRight.easing(Easing.out(Easing.exp)).duration(500), 390 - )} 391 - exiting={native( 392 - SlideOutLeft.easing(Easing.out(Easing.exp)).duration(500), 393 - )}> 394 - <View 395 - style={[ 396 - a.relative, 397 - a.align_center, 398 - a.justify_center, 399 - isNative && {marginHorizontal: tokens.space.xl * -1}, 400 - a.pointer_events_none, 401 - ]}> 402 - <Image 403 - source={image} 404 - style={[a.w_full, a.aspect_square]} 405 - alt={alt} 406 - accessibilityIgnoresInvertColors={false} // I guess we do need it to blend into the background 407 - /> 408 - {subStep === 1 && ( 409 - <Image 410 - source={state.profileStepResults.imageUri} 411 - style={[ 412 - a.z_10, 413 - a.absolute, 414 - a.rounded_full, 415 - { 416 - width: `${(80 / 393) * 100}%`, 417 - height: `${(80 / 393) * 100}%`, 418 - }, 419 - ]} 420 - accessibilityIgnoresInvertColors 421 - alt={_(msg`Your profile picture`)} 422 - /> 423 - )} 424 - </View> 425 - 426 - <View style={[a.mt_4xl, a.gap_2xl, a.align_center]}> 427 - <View style={[a.flex_row, a.gap_sm]}> 428 - <Dot active={subStep === 0} /> 429 - <Dot active={subStep === 1} /> 430 - <Dot active={subStep === 2} /> 431 - </View> 432 - 433 - <View style={[a.gap_sm]}> 434 - <Text style={[a.font_bold, a.text_3xl, a.text_center]}> 435 - {title} 436 - </Text> 437 - <Text 438 - style={[ 439 - t.atoms.text_contrast_medium, 440 - a.text_md, 441 - a.leading_snug, 442 - a.text_center, 443 - ]}> 444 - {description} 445 - </Text> 446 - </View> 447 - </View> 448 - </Animated.View> 449 - </LayoutAnimationConfig> 312 + <ValuePropositionPager 313 + step={subStep} 314 + setStep={ss => setSubStep(ss)} 315 + avatarUri={state.profileStepResults.imageUri} 316 + /> 450 317 451 318 <OnboardingControls.Portal> 452 319 <View style={gtMobile && [a.gap_md, a.flex_row]}> 453 - {gtMobile && ( 320 + {gtMobile && (isWeb ? subStep !== 2 : true) && ( 454 321 <Button 455 322 disabled={saving} 456 323 color="secondary" ··· 489 356 </View> 490 357 </OnboardingControls.Portal> 491 358 </> 492 - ) 493 - } 494 - 495 - function Dot({active}: {active: boolean}) { 496 - const t = useTheme() 497 - 498 - return ( 499 - <View 500 - style={[ 501 - a.rounded_full, 502 - {width: 8, height: 8}, 503 - active 504 - ? {backgroundColor: t.palette.primary_500} 505 - : t.atoms.bg_contrast_50, 506 - ]} 507 - /> 508 359 ) 509 360 } 510 361
+55
src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx
··· 1 + import {View} from 'react-native' 2 + import {msg} from '@lingui/macro' 3 + import {useLingui} from '@lingui/react' 4 + 5 + import {atoms as a, useTheme} from '#/alf' 6 + 7 + export function useValuePropText(step: 0 | 1 | 2) { 8 + const {_} = useLingui() 9 + 10 + return [ 11 + { 12 + title: _(msg`Free your feed`), 13 + description: _( 14 + msg`No more doomscrolling junk-filled algorithms. Find feeds that work for you, not against you.`, 15 + ), 16 + alt: _( 17 + msg`A collection of popular feeds you can find on Bluesky, including News, Booksky, Game Dev, Blacksky, and Fountain Pens`, 18 + ), 19 + }, 20 + { 21 + title: _(msg`Find your people`), 22 + description: _( 23 + msg`Ditch the trolls and clickbait. Find real people and conversations that matter to you.`, 24 + ), 25 + alt: _( 26 + msg`Your profile picture surrounded by concentric circles of other users' profile pictures`, 27 + ), 28 + }, 29 + { 30 + title: _(msg`Forget the noise`), 31 + description: _( 32 + msg`No ads, no invasive tracking, no engagement traps. Bluesky respects your time and attention.`, 33 + ), 34 + alt: _( 35 + msg`An illustration of several Bluesky posts alongside repost, like, and comment icons`, 36 + ), 37 + }, 38 + ][step] 39 + } 40 + 41 + export function Dot({active}: {active: boolean}) { 42 + const t = useTheme() 43 + 44 + return ( 45 + <View 46 + style={[ 47 + a.rounded_full, 48 + {width: 8, height: 8}, 49 + active 50 + ? {backgroundColor: t.palette.primary_500} 51 + : t.atoms.bg_contrast_50, 52 + ]} 53 + /> 54 + ) 55 + }
+127
src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx
··· 1 + import {useRef, useState} from 'react' 2 + import {View} from 'react-native' 3 + import PagerView from 'react-native-pager-view' 4 + import {Image} from 'expo-image' 5 + import {msg} from '@lingui/macro' 6 + import {useLingui} from '@lingui/react' 7 + 8 + import {atoms as a, tokens, useTheme} from '#/alf' 9 + import {Text} from '#/components/Typography' 10 + import {PROP_1, PROP_2, PROP_3} from './images' 11 + import {Dot, useValuePropText} from './ValuePropositionPager.shared' 12 + 13 + export function ValuePropositionPager({ 14 + step, 15 + setStep, 16 + avatarUri, 17 + }: { 18 + step: 0 | 1 | 2 19 + setStep: (step: 0 | 1 | 2) => void 20 + avatarUri?: string 21 + }) { 22 + const t = useTheme() 23 + const [activePage, setActivePage] = useState(step) 24 + const ref = useRef<PagerView>(null) 25 + 26 + if (step !== activePage) { 27 + setActivePage(step) 28 + ref.current?.setPage(step) 29 + } 30 + 31 + const images = [PROP_1[t.name], PROP_2[t.name], PROP_3[t.name]] 32 + 33 + return ( 34 + <View style={[a.h_full, {marginHorizontal: tokens.space.xl * -1}]}> 35 + <PagerView 36 + ref={ref} 37 + style={[a.flex_1]} 38 + initialPage={step} 39 + onPageSelected={evt => { 40 + const page = evt.nativeEvent.position as 0 | 1 | 2 41 + if (step !== page) { 42 + setActivePage(page) 43 + setStep(page) 44 + } 45 + }}> 46 + {([0, 1, 2] as const).map(page => ( 47 + <Page 48 + key={page} 49 + page={page} 50 + image={images[page]} 51 + avatarUri={avatarUri} 52 + /> 53 + ))} 54 + </PagerView> 55 + </View> 56 + ) 57 + } 58 + 59 + function Page({ 60 + page, 61 + image, 62 + avatarUri, 63 + }: { 64 + page: 0 | 1 | 2 65 + image: string 66 + avatarUri?: string 67 + }) { 68 + const {_} = useLingui() 69 + const t = useTheme() 70 + const {title, description, alt} = useValuePropText(page) 71 + 72 + return ( 73 + <View key={page}> 74 + <View 75 + style={[ 76 + a.relative, 77 + a.align_center, 78 + a.justify_center, 79 + a.pointer_events_none, 80 + ]}> 81 + <Image 82 + source={image} 83 + style={[a.w_full, a.aspect_square]} 84 + alt={alt} 85 + accessibilityIgnoresInvertColors={false} // I guess we do need it to blend into the background 86 + /> 87 + {page === 1 && ( 88 + <Image 89 + source={avatarUri} 90 + style={[ 91 + a.z_10, 92 + a.absolute, 93 + a.rounded_full, 94 + { 95 + width: `${(80 / 393) * 100}%`, 96 + height: `${(80 / 393) * 100}%`, 97 + }, 98 + ]} 99 + accessibilityIgnoresInvertColors 100 + alt={_(msg`Your profile picture`)} 101 + /> 102 + )} 103 + </View> 104 + 105 + <View style={[a.mt_4xl, a.gap_2xl, a.px_xl, a.align_center]}> 106 + <View style={[a.flex_row, a.gap_sm]}> 107 + <Dot active={page === 0} /> 108 + <Dot active={page === 1} /> 109 + <Dot active={page === 2} /> 110 + </View> 111 + 112 + <View style={[a.gap_sm]}> 113 + <Text style={[a.font_bold, a.text_3xl, a.text_center]}>{title}</Text> 114 + <Text 115 + style={[ 116 + t.atoms.text_contrast_medium, 117 + a.text_md, 118 + a.leading_snug, 119 + a.text_center, 120 + ]}> 121 + {description} 122 + </Text> 123 + </View> 124 + </View> 125 + </View> 126 + ) 127 + }
+80
src/screens/Onboarding/StepFinished/ValuePropositionPager.web.tsx
··· 1 + import {View} from 'react-native' 2 + import {Image} from 'expo-image' 3 + import {msg} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + 6 + import {atoms as a, useTheme} from '#/alf' 7 + import {Text} from '#/components/Typography' 8 + import {PROP_1, PROP_2, PROP_3} from './images' 9 + import {Dot, useValuePropText} from './ValuePropositionPager.shared' 10 + 11 + export function ValuePropositionPager({ 12 + step, 13 + avatarUri, 14 + }: { 15 + step: 0 | 1 | 2 16 + avatarUri?: string 17 + }) { 18 + const t = useTheme() 19 + const {_} = useLingui() 20 + 21 + const image = [PROP_1[t.name], PROP_2[t.name], PROP_3[t.name]][step] 22 + 23 + const {title, description, alt} = useValuePropText(step) 24 + 25 + return ( 26 + <View> 27 + <View 28 + style={[ 29 + a.relative, 30 + a.align_center, 31 + a.justify_center, 32 + a.pointer_events_none, 33 + ]}> 34 + <Image 35 + source={image} 36 + style={[a.w_full, {aspectRatio: 1}]} 37 + alt={alt} 38 + accessibilityIgnoresInvertColors={false} // I guess we do need it to blend into the background 39 + /> 40 + {step === 1 && ( 41 + <Image 42 + source={avatarUri} 43 + style={[ 44 + a.z_10, 45 + a.absolute, 46 + a.rounded_full, 47 + { 48 + width: `${(80 / 393) * 100}%`, 49 + height: `${(80 / 393) * 100}%`, 50 + }, 51 + ]} 52 + accessibilityIgnoresInvertColors 53 + alt={_(msg`Your profile picture`)} 54 + /> 55 + )} 56 + </View> 57 + 58 + <View style={[a.mt_4xl, a.gap_2xl, a.align_center]}> 59 + <View style={[a.flex_row, a.gap_sm]}> 60 + <Dot active={step === 0} /> 61 + <Dot active={step === 1} /> 62 + <Dot active={step === 2} /> 63 + </View> 64 + 65 + <View style={[a.gap_sm]}> 66 + <Text style={[a.font_bold, a.text_3xl, a.text_center]}>{title}</Text> 67 + <Text 68 + style={[ 69 + t.atoms.text_contrast_medium, 70 + a.text_md, 71 + a.leading_snug, 72 + a.text_center, 73 + ]}> 74 + {description} 75 + </Text> 76 + </View> 77 + </View> 78 + </View> 79 + ) 80 + }
+28
src/screens/Onboarding/StepFinished/images.ts
··· 1 + import {platform} from '#/alf' 2 + 3 + export const PROP_1 = { 4 + light: platform({ 5 + native: require('../../../../assets/images/onboarding/value_prop_1_light.webp'), 6 + web: require('../../../../assets/images/onboarding/value_prop_1_light_borderless.webp'), 7 + }), 8 + dim: platform({ 9 + native: require('../../../../assets/images/onboarding/value_prop_1_dim.webp'), 10 + web: require('../../../../assets/images/onboarding/value_prop_1_dim_borderless.webp'), 11 + }), 12 + dark: platform({ 13 + native: require('../../../../assets/images/onboarding/value_prop_1_dark.webp'), 14 + web: require('../../../../assets/images/onboarding/value_prop_1_dark_borderless.webp'), 15 + }), 16 + } as const 17 + 18 + export const PROP_2 = { 19 + light: require('../../../../assets/images/onboarding/value_prop_2_light.webp'), 20 + dim: require('../../../../assets/images/onboarding/value_prop_2_dim.webp'), 21 + dark: require('../../../../assets/images/onboarding/value_prop_2_dark.webp'), 22 + } as const 23 + 24 + export const PROP_3 = { 25 + light: require('../../../../assets/images/onboarding/value_prop_3_light.webp'), 26 + dim: require('../../../../assets/images/onboarding/value_prop_3_dim.webp'), 27 + dark: require('../../../../assets/images/onboarding/value_prop_3_dark.webp'), 28 + } as const