Bluesky app fork with some witchin' additions 馃挮
at linkat-integration 254 lines 7.7 kB view raw
1import {useEffect, useRef, useState} from 'react' 2import {ScrollView, View} from 'react-native' 3import {useSafeAreaInsets} from 'react-native-safe-area-context' 4import {msg, Trans} from '@lingui/macro' 5import {useLingui} from '@lingui/react' 6 7import {useOnboardingDispatch} from '#/state/shell' 8import {useOnboardingInternalState} from '#/screens/Onboarding/state' 9import { 10 atoms as a, 11 native, 12 type TextStyleProp, 13 tokens, 14 useBreakpoints, 15 useTheme, 16 web, 17} from '#/alf' 18import {Button, ButtonIcon, ButtonText} from '#/components/Button' 19import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow' 20import {HEADER_SLOT_SIZE} from '#/components/Layout' 21import {createPortalGroup} from '#/components/Portal' 22import {P, Text} from '#/components/Typography' 23import {IS_ANDROID, IS_WEB} from '#/env' 24import {IS_INTERNAL} from '#/env' 25 26const ONBOARDING_COL_WIDTH = 420 27 28export const OnboardingControls = createPortalGroup() 29export const OnboardingHeaderSlot = createPortalGroup() 30 31export function Layout({children}: React.PropsWithChildren<{}>) { 32 const {_} = useLingui() 33 const t = useTheme() 34 const insets = useSafeAreaInsets() 35 const {gtMobile} = useBreakpoints() 36 const onboardDispatch = useOnboardingDispatch() 37 const {state, dispatch} = useOnboardingInternalState() 38 const scrollview = useRef<ScrollView>(null) 39 const prevActiveStep = useRef<string>(state.activeStep) 40 41 useEffect(() => { 42 if (state.activeStep !== prevActiveStep.current) { 43 prevActiveStep.current = state.activeStep 44 scrollview.current?.scrollTo({y: 0, animated: false}) 45 } 46 }, [state]) 47 48 const dialogLabel = _(msg`Set up your account`) 49 50 const [headerHeight, setHeaderHeight] = useState(0) 51 const [footerHeight, setFooterHeight] = useState(0) 52 53 return ( 54 <View 55 aria-modal 56 role="dialog" 57 aria-role="dialog" 58 aria-label={dialogLabel} 59 accessibilityLabel={dialogLabel} 60 accessibilityHint={_(msg`Customizes your Bluesky experience`)} 61 style={[IS_WEB ? a.fixed : a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}> 62 {!gtMobile ? ( 63 <View 64 style={[ 65 web(a.fixed), 66 native(a.absolute), 67 a.top_0, 68 a.left_0, 69 a.right_0, 70 a.flex_row, 71 a.w_full, 72 a.justify_center, 73 a.z_20, 74 a.px_xl, 75 {paddingTop: (web(tokens.space.lg) ?? 0) + insets.top}, 76 native([t.atoms.bg, a.pb_xs, {minHeight: 48}]), 77 web(a.pointer_events_box_none), 78 ]} 79 onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}> 80 <View 81 style={[ 82 a.w_full, 83 a.align_center, 84 a.flex_row, 85 a.justify_between, 86 web({maxWidth: ONBOARDING_COL_WIDTH}), 87 web(a.pointer_events_box_none), 88 ]}> 89 <HeaderSlot> 90 {state.canGoBack && ( 91 <Button 92 key={state.activeStep} // remove focus state on nav 93 color="secondary" 94 variant="ghost" 95 shape="round" 96 size="small" 97 label={_(msg`Go back to previous step`)} 98 onPress={() => dispatch({type: 'prev'})}> 99 <ButtonIcon icon={ArrowLeft} size="lg" /> 100 </Button> 101 )} 102 </HeaderSlot> 103 104 {IS_INTERNAL && ( 105 <Button 106 variant="ghost" 107 color="negative" 108 size="tiny" 109 onPress={() => onboardDispatch({type: 'skip'})} 110 // DEV ONLY 111 label="Clear onboarding state"> 112 <ButtonText>[DEV] Clear</ButtonText> 113 </Button> 114 )} 115 116 <HeaderSlot> 117 <OnboardingHeaderSlot.Outlet /> 118 </HeaderSlot> 119 </View> 120 </View> 121 ) : ( 122 <> 123 {IS_INTERNAL && ( 124 <View 125 style={[ 126 a.absolute, 127 a.align_center, 128 a.z_10, 129 {top: 0, left: 0, right: 0}, 130 ]}> 131 <Button 132 variant="ghost" 133 color="negative" 134 size="tiny" 135 onPress={() => onboardDispatch({type: 'skip'})} 136 // DEV ONLY 137 label="Clear onboarding state"> 138 <ButtonText>[DEV] Clear</ButtonText> 139 </Button> 140 </View> 141 )} 142 </> 143 )} 144 145 <ScrollView 146 ref={scrollview} 147 style={[a.h_full, a.w_full]} 148 contentContainerStyle={{ 149 borderWidth: 0, 150 minHeight: '100%', 151 paddingTop: gtMobile ? 40 : headerHeight, 152 paddingBottom: footerHeight, 153 }} 154 showsVerticalScrollIndicator={!IS_ANDROID} 155 scrollIndicatorInsets={{bottom: footerHeight - insets.bottom}} 156 // @ts-expect-error web only --prf 157 dataSet={{'stable-gutters': 1}} 158 centerContent={gtMobile}> 159 <View 160 style={[a.flex_row, a.justify_center, gtMobile ? a.px_5xl : a.px_xl]}> 161 <View style={[a.flex_1, web({maxWidth: ONBOARDING_COL_WIDTH})]}> 162 <View style={[a.w_full, a.py_md]}>{children}</View> 163 </View> 164 </View> 165 </ScrollView> 166 167 <View 168 onLayout={evt => setFooterHeight(evt.nativeEvent.layout.height)} 169 style={[ 170 IS_WEB ? a.fixed : a.absolute, 171 {bottom: 0, left: 0, right: 0}, 172 t.atoms.bg, 173 t.atoms.border_contrast_low, 174 a.border_t, 175 a.align_center, 176 gtMobile ? a.px_5xl : a.px_xl, 177 IS_WEB 178 ? a.py_2xl 179 : { 180 paddingTop: tokens.space.md, 181 paddingBottom: insets.bottom + tokens.space.md, 182 }, 183 ]}> 184 <View 185 style={[ 186 a.w_full, 187 {maxWidth: ONBOARDING_COL_WIDTH}, 188 gtMobile && [a.flex_row, a.justify_between, a.align_center], 189 ]}> 190 {gtMobile && 191 (state.canGoBack ? ( 192 <Button 193 key={state.activeStep} // remove focus state on nav 194 color="secondary" 195 variant="ghost" 196 shape="square" 197 size="small" 198 label={_(msg`Go back to previous step`)} 199 onPress={() => dispatch({type: 'prev'})}> 200 <ButtonIcon icon={ArrowLeft} size="lg" /> 201 </Button> 202 ) : ( 203 <View style={{height: 33}} /> 204 ))} 205 <OnboardingControls.Outlet /> 206 </View> 207 </View> 208 </View> 209 ) 210} 211 212function HeaderSlot({children}: {children?: React.ReactNode}) { 213 return ( 214 <View style={[{minHeight: HEADER_SLOT_SIZE, minWidth: HEADER_SLOT_SIZE}]}> 215 {children} 216 </View> 217 ) 218} 219 220export function OnboardingPosition() { 221 const {state} = useOnboardingInternalState() 222 const t = useTheme() 223 224 return ( 225 <Text style={[a.text_sm, a.font_medium, t.atoms.text_contrast_medium]}> 226 <Trans> 227 Step {state.activeStepIndex + 1} of {state.totalSteps} 228 </Trans> 229 </Text> 230 ) 231} 232 233export function OnboardingTitleText({ 234 children, 235 style, 236}: React.PropsWithChildren<TextStyleProp>) { 237 return ( 238 <Text style={[a.text_3xl, a.font_bold, a.leading_snug, style]}> 239 {children} 240 </Text> 241 ) 242} 243 244export function OnboardingDescriptionText({ 245 children, 246 style, 247}: React.PropsWithChildren<TextStyleProp>) { 248 const t = useTheme() 249 return ( 250 <P style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium, style]}> 251 {children} 252 </P> 253 ) 254}