Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
1import React from 'react'
2import {Modal, ScrollView, View} from 'react-native'
3import {SystemBars} from 'react-native-edge-to-edge'
4import {useSafeAreaInsets} from 'react-native-safe-area-context'
5import {msg, plural} from '@lingui/core/macro'
6import {useLingui} from '@lingui/react'
7import {Trans} from '@lingui/react/macro'
8
9import {logger} from '#/logger'
10import {isSignupQueued, useAgent, useSessionApi} from '#/state/session'
11import {pdsAgent} from '#/state/session/agent'
12import {useOnboardingDispatch} from '#/state/shell'
13import {Logo} from '#/view/icons/Logo'
14import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf'
15import {Button, ButtonIcon, ButtonText} from '#/components/Button'
16import {Loader} from '#/components/Loader'
17import {P, Text} from '#/components/Typography'
18import {IS_IOS, IS_LIQUID_GLASS, IS_WEB} from '#/env'
19
20const COL_WIDTH = 400
21
22export function SignupQueued() {
23 const {_} = useLingui()
24 const t = useTheme()
25 const insets = useSafeAreaInsets()
26 const {gtMobile} = useBreakpoints()
27 const onboardingDispatch = useOnboardingDispatch()
28 const {logoutCurrentAccount} = useSessionApi()
29 const agent = useAgent()
30
31 const [isProcessing, setProcessing] = React.useState(false)
32 const [estimatedTime, setEstimatedTime] = React.useState<string | undefined>(
33 undefined,
34 )
35 const [placeInQueue, setPlaceInQueue] = React.useState<number | undefined>(
36 undefined,
37 )
38
39 const checkStatus = React.useCallback(async () => {
40 setProcessing(true)
41 try {
42 const res = await pdsAgent(agent).com.atproto.temp.checkSignupQueue()
43 if (res.data.activated) {
44 // ready to go, exchange the access token for a usable one and kick off onboarding
45 await agent.sessionManager.refreshSession()
46 if (!isSignupQueued(agent.session?.accessJwt)) {
47 onboardingDispatch({type: 'start'})
48 }
49 } else {
50 // not ready, update UI
51 setEstimatedTime(msToString(res.data.estimatedTimeMs))
52 if (typeof res.data.placeInQueue !== 'undefined') {
53 setPlaceInQueue(Math.max(res.data.placeInQueue, 1))
54 }
55 }
56 } catch (e: any) {
57 logger.error('Failed to check signup queue', {err: e.toString()})
58 } finally {
59 setProcessing(false)
60 }
61 }, [
62 setProcessing,
63 setEstimatedTime,
64 setPlaceInQueue,
65 onboardingDispatch,
66 agent,
67 ])
68
69 React.useEffect(() => {
70 checkStatus()
71 const interval = setInterval(checkStatus, 60e3)
72 return () => clearInterval(interval)
73 }, [checkStatus])
74
75 const checkBtn = (
76 <Button
77 variant="solid"
78 color="primary"
79 size="large"
80 label={_(msg`Check my status`)}
81 onPress={checkStatus}
82 disabled={isProcessing}>
83 <ButtonText>
84 <Trans>Check my status</Trans>
85 </ButtonText>
86 {isProcessing && <ButtonIcon icon={Loader} />}
87 </Button>
88 )
89
90 const logoutBtn = (
91 <Button
92 variant="ghost"
93 size="large"
94 color="primary"
95 label={_(msg`Sign out`)}
96 onPress={() => logoutCurrentAccount('SignupQueued')}>
97 <ButtonText>
98 <Trans>Sign out</Trans>
99 </ButtonText>
100 </Button>
101 )
102
103 const webLayout = IS_WEB && gtMobile
104
105 return (
106 <Modal
107 visible
108 animationType={native('slide')}
109 presentationStyle="formSheet"
110 style={[web(a.util_screen_outer)]}>
111 {IS_IOS && !IS_LIQUID_GLASS && (
112 <SystemBars style={{statusBar: 'light'}} />
113 )}
114 <ScrollView
115 style={[a.flex_1, t.atoms.bg]}
116 contentContainerStyle={{borderWidth: 0}}
117 bounces={false}>
118 <View
119 style={[
120 a.flex_row,
121 a.justify_center,
122 gtMobile ? a.pt_4xl : [a.px_xl, a.pt_xl],
123 ]}>
124 <View style={[a.flex_1, {maxWidth: COL_WIDTH}]}>
125 <View
126 style={[a.w_full, a.justify_center, a.align_center, a.my_4xl]}>
127 <Logo width={120} />
128 </View>
129
130 <Text style={[a.text_4xl, a.font_bold, a.pb_sm]}>
131 <Trans>You're in line</Trans>
132 </Text>
133 <P style={[t.atoms.text_contrast_medium]}>
134 <Trans>
135 There's been a rush of new users to Bluesky! We'll activate your
136 account as soon as we can.
137 </Trans>
138 </P>
139
140 <View
141 style={[
142 a.rounded_sm,
143 a.px_2xl,
144 a.py_4xl,
145 a.mt_2xl,
146 a.mb_md,
147 a.border,
148 t.atoms.bg_contrast_25,
149 t.atoms.border_contrast_medium,
150 ]}>
151 {typeof placeInQueue === 'number' && (
152 <Text
153 style={[a.text_5xl, a.text_center, a.font_bold, a.mb_2xl]}>
154 {placeInQueue}
155 </Text>
156 )}
157 <P style={[a.text_center]}>
158 {typeof placeInQueue === 'number' ? (
159 <Trans>left to go.</Trans>
160 ) : (
161 <Trans>You are in line.</Trans>
162 )}{' '}
163 {estimatedTime ? (
164 <Trans>
165 We estimate {estimatedTime} until your account is ready.
166 </Trans>
167 ) : (
168 <Trans>
169 We will let you know when your account is ready.
170 </Trans>
171 )}
172 </P>
173 </View>
174
175 {webLayout && (
176 <View
177 style={[
178 a.w_full,
179 a.flex_row,
180 a.justify_between,
181 a.pt_5xl,
182 {paddingBottom: 200},
183 ]}>
184 {logoutBtn}
185 {checkBtn}
186 </View>
187 )}
188 </View>
189 </View>
190 </ScrollView>
191
192 {!webLayout && (
193 <View
194 style={[
195 a.align_center,
196 t.atoms.bg,
197 gtMobile ? a.px_5xl : a.px_xl,
198 {paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom)},
199 ]}>
200 <View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}>
201 {checkBtn}
202 {logoutBtn}
203 </View>
204 </View>
205 )}
206 </Modal>
207 )
208}
209
210function msToString(ms: number | undefined): string | undefined {
211 if (ms && ms > 0) {
212 const estimatedTimeMins = Math.ceil(ms / 60e3)
213 if (estimatedTimeMins > 59) {
214 const estimatedTimeHrs = Math.round(estimatedTimeMins / 60)
215 if (estimatedTimeHrs > 6) {
216 // dont even bother
217 return undefined
218 }
219 // hours
220 return `${estimatedTimeHrs} ${plural(estimatedTimeHrs, {
221 one: 'hour',
222 other: 'hours',
223 })}`
224 }
225 // minutes
226 return `${estimatedTimeMins} ${plural(estimatedTimeMins, {
227 one: 'minute',
228 other: 'minutes',
229 })}`
230 }
231 return undefined
232}