Bluesky app fork with some witchin' additions 💫
1import {useCallback, useMemo, useState} from 'react'
2import {View} from 'react-native'
3import {Trans, useLingui} from '@lingui/react/macro'
4
5import {useCleanError} from '#/lib/hooks/useCleanError'
6import {isAppPassword} from '#/lib/jwt'
7import {getAge, getDateAgo} from '#/lib/strings/time'
8import {logger} from '#/logger'
9import {
10 useBirthdateMutation,
11 useIsBirthdateUpdateAllowed,
12} from '#/state/birthdate'
13import {
14 usePreferencesQuery,
15 type UsePreferencesQueryResponse,
16} from '#/state/queries/preferences'
17import {useSession} from '#/state/session'
18import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
19import {atoms as a, useTheme, web} from '#/alf'
20import {Admonition} from '#/components/Admonition'
21import {Button, ButtonIcon, ButtonText} from '#/components/Button'
22import * as Dialog from '#/components/Dialog'
23import {DateField} from '#/components/forms/DateField'
24import {SimpleInlineLinkText} from '#/components/Link'
25import {Loader} from '#/components/Loader'
26import {Span, Text} from '#/components/Typography'
27import {IS_IOS, IS_WEB} from '#/env'
28
29export function BirthDateSettingsDialog({
30 control,
31}: {
32 control: Dialog.DialogControlProps
33}) {
34 const t = useTheme()
35 const {t: l} = useLingui()
36 const {isLoading, error, data: preferences} = usePreferencesQuery()
37 const isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed()
38 const {currentAccount} = useSession()
39 const isUsingAppPassword = currentAccount?.accessJwt
40 ? isAppPassword(currentAccount?.accessJwt)
41 : false
42
43 return (
44 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
45 <Dialog.Handle />
46 {isBirthdateUpdateAllowed ? (
47 <Dialog.ScrollableInner
48 label={l`My birthdate`}
49 style={web({maxWidth: 400})}>
50 <View style={[a.gap_md]}>
51 <Text style={[a.text_xl, a.font_semi_bold]}>
52 <Trans>My birthdate</Trans>
53 </Text>
54 <Text
55 style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
56 <Trans>
57 This information is private and not shared with other users.
58 </Trans>
59 </Text>
60
61 {isLoading ? (
62 <Loader size="xl" />
63 ) : error || !preferences ? (
64 <ErrorMessage
65 message={
66 error?.toString() ||
67 l`We were unable to load your birthdate preferences. Please try again.`
68 }
69 style={[a.rounded_sm]}
70 />
71 ) : isUsingAppPassword ? (
72 <Admonition type="info">
73 <Trans>
74 Hmm, it looks like you're signed in with an{' '}
75 <Span style={[a.italic]}>App Password</Span>. To set your
76 birthdate, you'll need to sign in with your main account
77 password, or ask whomever controls this account to do so.
78 </Trans>
79 </Admonition>
80 ) : (
81 <BirthdayInner control={control} preferences={preferences} />
82 )}
83 </View>
84
85 <Dialog.Close />
86 </Dialog.ScrollableInner>
87 ) : (
88 <Dialog.ScrollableInner
89 label={l`You recently changed your birthdate`}
90 style={web({maxWidth: 400})}>
91 <View style={[a.gap_sm]}>
92 <Text
93 style={[
94 a.text_xl,
95 a.font_semi_bold,
96 a.leading_snug,
97 {paddingRight: 32},
98 ]}>
99 <Trans>You recently changed your birthdate</Trans>
100 </Text>
101 <Text
102 style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
103 <Trans>
104 There is a limit to how often you can change your birthdate. You
105 may need to wait a day or two before updating it again.
106 </Trans>
107 </Text>
108 </View>
109
110 <Dialog.Close />
111 </Dialog.ScrollableInner>
112 )}
113 </Dialog.Outer>
114 )
115}
116
117function BirthdayInner({
118 control,
119 preferences,
120}: {
121 control: Dialog.DialogControlProps
122 preferences: UsePreferencesQueryResponse
123}) {
124 const {t: l} = useLingui()
125 const cleanError = useCleanError()
126 const [date, setDate] = useState(preferences.birthDate || getDateAgo(18))
127 const {isPending, error, mutateAsync: setBirthDate} = useBirthdateMutation()
128 const hasChanged = date !== preferences.birthDate
129 const errorMessage = useMemo(() => {
130 if (error) {
131 const e = error as Error
132 const {raw, clean} = cleanError(e)
133 return clean || raw || e.toString()
134 }
135 }, [error, cleanError])
136
137 const age = getAge(new Date(date))
138 const isUnder13 = age < 13
139 const isUnder18 = age >= 13 && age < 18
140
141 const onSave = useCallback(async () => {
142 try {
143 // skip if date is the same
144 if (hasChanged) {
145 await setBirthDate({birthDate: date})
146 }
147 control.close()
148 } catch (error) {
149 const e = error as Error
150 logger.error(`setBirthDate failed`, {message: e.message})
151 }
152 }, [date, setBirthDate, control, hasChanged])
153
154 return (
155 <View style={a.gap_lg} testID="birthDateSettingsDialog">
156 <View style={IS_IOS && [a.w_full, a.align_center]}>
157 <DateField
158 testID="birthdayInput"
159 value={date}
160 onChangeDate={newDate => setDate(new Date(newDate))}
161 label={l`Birthdate`}
162 accessibilityHint={l`Enter your birthdate`}
163 />
164 </View>
165 {isUnder18 && hasChanged && (
166 <Admonition type="info">
167 <Trans>
168 The birthdate you've entered means you are under 18 years old.
169 Certain content and features may be unavailable to you.
170 </Trans>
171 </Admonition>
172 )}
173 {isUnder13 && (
174 <Admonition type="error">
175 <Trans>
176 You must be at least 13 years old to use Bluesky. Read our{' '}
177 <SimpleInlineLinkText
178 to="https://bsky.social/about/support/tos"
179 label={l`Terms of Service`}>
180 Terms of Service
181 </SimpleInlineLinkText>{' '}
182 for more information.
183 </Trans>
184 </Admonition>
185 )}
186 {errorMessage ? (
187 <ErrorMessage message={errorMessage} style={[a.rounded_sm]} />
188 ) : undefined}
189 <View style={IS_WEB && [a.flex_row, a.justify_end]}>
190 <Button
191 label={hasChanged ? l`Save birthdate` : l`Done`}
192 size="large"
193 onPress={() => void onSave()}
194 variant="solid"
195 color="primary"
196 disabled={isUnder13}>
197 <ButtonText>
198 {hasChanged ? <Trans>Save</Trans> : <Trans>Done</Trans>}
199 </ButtonText>
200 {isPending && <ButtonIcon icon={Loader} />}
201 </Button>
202 </View>
203 </View>
204 )
205}