Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 353 lines 12 kB view raw
1import {useState} from 'react' 2import {View} from 'react-native' 3import {XRPCError} from '@atproto/xrpc' 4import {msg, Trans} from '@lingui/macro' 5import {useLingui} from '@lingui/react' 6import {validate as validateEmail} from 'email-validator' 7 8import {useCleanError} from '#/lib/hooks/useCleanError' 9import { 10 SupportCode, 11 useCreateSupportLink, 12} from '#/lib/hooks/useCreateSupportLink' 13import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 14import {useTLDs} from '#/lib/hooks/useTLDs' 15import {isEmailMaybeInvalid} from '#/lib/strings/email' 16import {type AppLanguage} from '#/locale/languages' 17import {useLanguagePrefs} from '#/state/preferences' 18import {useSession} from '#/state/session' 19import {atoms as a, web} from '#/alf' 20import {Admonition} from '#/components/Admonition' 21import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' 22import {urls} from '#/components/ageAssurance/const' 23import {KWS_SUPPORTED_LANGS} from '#/components/ageAssurance/const' 24import {Button, ButtonIcon, ButtonText} from '#/components/Button' 25import * as Dialog from '#/components/Dialog' 26import {Divider} from '#/components/Divider' 27import * as TextField from '#/components/forms/TextField' 28import {ShieldCheck_Stroke2_Corner0_Rounded as Shield} from '#/components/icons/Shield' 29import {LanguageSelect} from '#/components/LanguageSelect' 30import {SimpleInlineLinkText} from '#/components/Link' 31import {Loader} from '#/components/Loader' 32import {Text} from '#/components/Typography' 33import {logger} from '#/ageAssurance' 34import {useAgeAssurance} from '#/ageAssurance' 35import {useBeginAgeAssurance} from '#/ageAssurance/useBeginAgeAssurance' 36 37export {useDialogControl} from '#/components/Dialog/context' 38 39export function AgeAssuranceInitDialog({ 40 control, 41}: { 42 control: Dialog.DialogControlProps 43}) { 44 const {_} = useLingui() 45 return ( 46 <Dialog.Outer control={control}> 47 <Dialog.Handle /> 48 49 <Dialog.ScrollableInner 50 label={_( 51 msg`Begin the age assurance process by completing the fields below.`, 52 )} 53 style={[ 54 web({ 55 maxWidth: 400, 56 }), 57 ]}> 58 <Inner /> 59 <Dialog.Close /> 60 </Dialog.ScrollableInner> 61 </Dialog.Outer> 62 ) 63} 64 65function Inner() { 66 const {_} = useLingui() 67 const {currentAccount} = useSession() 68 const langPrefs = useLanguagePrefs() 69 const cleanError = useCleanError() 70 const {close} = Dialog.useDialogContext() 71 const aa = useAgeAssurance() 72 const lastInitiatedAt = aa.state.lastInitiatedAt 73 const getTimeAgo = useGetTimeAgo() 74 const tlds = useTLDs() 75 const createSupportLink = useCreateSupportLink() 76 77 const wasRecentlyInitiated = 78 lastInitiatedAt && 79 new Date(lastInitiatedAt).getTime() > Date.now() - 5 * 60 * 1000 // 5 minutes 80 81 const [success, setSuccess] = useState(false) 82 const [email, setEmail] = useState(currentAccount?.email || '') 83 const [emailError, setEmailError] = useState<string>('') 84 const [languageError, setLanguageError] = useState(false) 85 const [disabled, setDisabled] = useState(false) 86 const [language, setLanguage] = useState<string | undefined>( 87 convertToKWSSupportedLanguage(langPrefs.appLanguage), 88 ) 89 const [error, setError] = useState<React.ReactNode>(null) 90 91 const {mutateAsync: begin, isPending} = useBeginAgeAssurance() 92 93 const runEmailValidation = () => { 94 if (validateEmail(email)) { 95 setEmailError('') 96 setDisabled(false) 97 98 if (tlds && isEmailMaybeInvalid(email, tlds)) { 99 setEmailError( 100 _( 101 msg`Please double-check that you have entered your email address correctly.`, 102 ), 103 ) 104 return {status: 'maybe'} 105 } 106 107 return {status: 'valid'} 108 } 109 110 setEmailError(_(msg`Please enter a valid email address.`)) 111 setDisabled(true) 112 113 return {status: 'invalid'} 114 } 115 116 const onSubmit = async () => { 117 setLanguageError(false) 118 119 logger.metric('ageAssurance:initDialogSubmit', {}) 120 121 try { 122 const {status} = runEmailValidation() 123 124 if (status === 'invalid') return 125 if (!language) { 126 setLanguageError(true) 127 return 128 } 129 130 await begin({ 131 email, 132 language, 133 }) 134 135 setSuccess(true) 136 } catch (e) { 137 let errorMessage: React.ReactNode = _( 138 msg`Something went wrong, please try again`, 139 ) 140 141 if (e instanceof XRPCError) { 142 if (e.error === 'InvalidEmail') { 143 errorMessage = _( 144 msg`Please enter a valid, non-temporary email address. You may need to access this email in the future.`, 145 ) 146 logger.metric('ageAssurance:initDialogError', {code: 'InvalidEmail'}) 147 } else if (e.error === 'DidTooLong') { 148 errorMessage = ( 149 <> 150 <Trans> 151 We're having issues initializing the age assurance process for 152 your account. Please{' '} 153 <SimpleInlineLinkText 154 to={createSupportLink({code: SupportCode.AA_DID, email})} 155 label={_(msg`Contact support`)}> 156 contact support 157 </SimpleInlineLinkText>{' '} 158 for assistance. 159 </Trans> 160 </> 161 ) 162 logger.metric('ageAssurance:initDialogError', {code: 'DidTooLong'}) 163 } else { 164 logger.metric('ageAssurance:initDialogError', {code: 'other'}) 165 } 166 } else { 167 const {clean, raw} = cleanError(e) 168 errorMessage = clean || raw || errorMessage 169 logger.metric('ageAssurance:initDialogError', {code: 'other'}) 170 } 171 172 setError(errorMessage) 173 } 174 } 175 176 return ( 177 <View> 178 <View style={[a.align_start]}> 179 <AgeAssuranceBadge /> 180 181 <Text style={[a.text_xl, a.font_bold, a.pt_xl, a.pb_md]}> 182 {success ? <Trans>Success!</Trans> : <Trans>Verify your age</Trans>} 183 </Text> 184 185 <View style={[a.pb_xl, a.gap_sm]}> 186 {success ? ( 187 <Text style={[a.text_sm, a.leading_snug]}> 188 <Trans> 189 Please check your email inbox for further instructions. It may 190 take a minute or two to arrive. 191 </Trans> 192 </Text> 193 ) : ( 194 <> 195 <Text style={[a.text_sm, a.leading_snug]}> 196 <Trans> 197 We have partnered with{' '} 198 <SimpleInlineLinkText 199 label={_(msg`KWS website`)} 200 to={urls.kwsHome} 201 style={[a.text_sm, a.leading_snug]}> 202 KWS 203 </SimpleInlineLinkText>{' '} 204 to handle age verification. When you click "Begin" below, KWS 205 will email you instructions to complete the verification 206 process. If your email address has already been used to verify 207 your age for another game or service that uses KWS, you won鈥檛 208 need to do it again. When you鈥檙e done, you'll be brought back 209 to continue using Bluesky. 210 </Trans> 211 </Text> 212 <Text style={[a.text_sm, a.leading_snug]}> 213 <Trans>This should only take a few minutes.</Trans> 214 </Text> 215 </> 216 )} 217 </View> 218 219 {success ? ( 220 <View style={[a.w_full]}> 221 <Button 222 label={_(msg`Close dialog`)} 223 size="large" 224 variant="solid" 225 color="secondary" 226 onPress={() => close()}> 227 <ButtonText> 228 <Trans>Close dialog</Trans> 229 </ButtonText> 230 </Button> 231 </View> 232 ) : ( 233 <> 234 <Divider /> 235 236 <View style={[a.w_full, a.pt_xl, a.gap_lg, a.pb_lg]}> 237 {wasRecentlyInitiated && ( 238 <Admonition type="warning"> 239 <Trans> 240 You initiated this flow already,{' '} 241 {getTimeAgo(lastInitiatedAt, new Date(), {format: 'long'})}{' '} 242 ago. It may take up to 5 minutes for emails to reach your 243 inbox. Please consider waiting a few minutes before trying 244 again. 245 </Trans> 246 </Admonition> 247 )} 248 249 <View> 250 <TextField.LabelText> 251 <Trans>Your email</Trans> 252 </TextField.LabelText> 253 <TextField.Root isInvalid={!!emailError}> 254 <TextField.Input 255 label={_(msg`Your email`)} 256 placeholder={_(msg`Your email`)} 257 value={email} 258 onChangeText={setEmail} 259 onFocus={() => setEmailError('')} 260 onBlur={() => { 261 runEmailValidation() 262 }} 263 returnKeyType="done" 264 autoCapitalize="none" 265 autoComplete="off" 266 autoCorrect={false} 267 onSubmitEditing={onSubmit} 268 /> 269 </TextField.Root> 270 271 {emailError ? ( 272 <Admonition type="error" style={[a.mt_sm]}> 273 {emailError} 274 </Admonition> 275 ) : ( 276 <Admonition type="tip" style={[a.mt_sm]}> 277 <Trans> 278 Use your account email address, or another real email 279 address you control, in case KWS or Bluesky needs to 280 contact you. 281 </Trans> 282 </Admonition> 283 )} 284 </View> 285 286 <View> 287 <TextField.LabelText> 288 <Trans>Your preferred language</Trans> 289 </TextField.LabelText> 290 <LanguageSelect 291 label={_(msg`Preferred language`)} 292 value={language} 293 onChange={value => { 294 setLanguage(value) 295 setLanguageError(false) 296 }} 297 items={KWS_SUPPORTED_LANGS} 298 /> 299 300 {languageError && ( 301 <Admonition type="error" style={[a.mt_sm]}> 302 <Trans>Please select a language</Trans> 303 </Admonition> 304 )} 305 </View> 306 307 {error && <Admonition type="error">{error}</Admonition>} 308 309 <Button 310 disabled={disabled} 311 label={_(msg`Begin age assurance process`)} 312 size="large" 313 variant="solid" 314 color="primary" 315 onPress={onSubmit}> 316 <ButtonText> 317 <Trans>Begin</Trans> 318 </ButtonText> 319 <ButtonIcon 320 icon={isPending ? Loader : Shield} 321 position="right" 322 /> 323 </Button> 324 </View> 325 </> 326 )} 327 </View> 328 </View> 329 ) 330} 331 332// best-effort mapping of our languages to KWS supported languages 333function convertToKWSSupportedLanguage( 334 appLanguage: string, 335): string | undefined { 336 // `${Enum}` is how you get a type of string union of the enum values (???) -sfn 337 switch (appLanguage as `${AppLanguage}`) { 338 // only en is supported 339 case 'en-GB': 340 return 'en' 341 // pt-PT is pt (pt-BR is supported independently) 342 case 'pt-PT': 343 return 'pt' 344 // only chinese (simplified) is supported, map all chinese variants 345 case 'zh-Hans-CN': 346 case 'zh-Hant-HK': 347 case 'zh-Hant-TW': 348 return 'zh-Hans' 349 default: 350 // try and map directly - if undefined, they will have to pick from the dropdown 351 return KWS_SUPPORTED_LANGS.find(v => v.value === appLanguage)?.value 352 } 353}