forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}