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