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 365 lines 12 kB view raw
1import {useCallback, useRef, useState} from 'react' 2import {type TextInput, View} from 'react-native' 3import {msg} from '@lingui/core/macro' 4import {useLingui} from '@lingui/react' 5import {Trans} from '@lingui/react/macro' 6 7import {DM_SERVICE_HEADERS} from '#/lib/constants' 8import {useCleanError} from '#/lib/hooks/useCleanError' 9import {sanitizeHandle} from '#/lib/strings/handles' 10import {logger} from '#/logger' 11import {useAgent, useSession, useSessionApi} from '#/state/session' 12import {atoms as a, useTheme, web} from '#/alf' 13import {Admonition} from '#/components/Admonition' 14import {type DialogOuterProps} from '#/components/Dialog' 15import { 16 isValidCode, 17 TokenField, 18} from '#/components/dialogs/EmailDialog/components/TokenField' 19import * as TextField from '#/components/forms/TextField' 20import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope' 21import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' 22import {createStaticClick, InlineLinkText} from '#/components/Link' 23import {Loader} from '#/components/Loader' 24import * as Prompt from '#/components/Prompt' 25import * as toast from '#/components/Toast' 26import {Span, Text} from '#/components/Typography' 27import {resetToTab} from '#/Navigation' 28 29const WHITESPACE_RE = /\s/gu 30const PASSWORD_MIN_LENGTH = 8 31 32enum Step { 33 SEND_CODE, 34 VERIFY_CODE, 35 CONFIRM_DELETION, 36} 37 38enum EmailState { 39 DEFAULT, 40 PENDING, 41} 42 43function isPasswordValid(password: string) { 44 return password.length >= PASSWORD_MIN_LENGTH 45} 46 47export function DeleteAccountDialog({ 48 control, 49 deactivateDialogControl, 50}: { 51 control: DialogOuterProps['control'] 52 deactivateDialogControl: DialogOuterProps['control'] 53}) { 54 return ( 55 <Prompt.Outer control={control}> 56 <DeleteAccountDialogInner 57 control={control} 58 deactivateDialogControl={deactivateDialogControl} 59 /> 60 </Prompt.Outer> 61 ) 62} 63 64function DeleteAccountDialogInner({ 65 control, 66 deactivateDialogControl, 67}: { 68 control: DialogOuterProps['control'] 69 deactivateDialogControl: DialogOuterProps['control'] 70}) { 71 const passwordRef = useRef<TextInput | null>(null) 72 const t = useTheme() 73 const {_} = useLingui() 74 const cleanError = useCleanError() 75 const agent = useAgent() 76 const {currentAccount} = useSession() 77 const {removeAccount} = useSessionApi() 78 79 const [emailState, setEmailState] = useState(EmailState.DEFAULT) 80 const [emailSentCount, setEmailSentCount] = useState(0) 81 const [step, setStep] = useState(Step.SEND_CODE) 82 const [confirmCode, setConfirmCode] = useState('') 83 const [password, setPassword] = useState('') 84 const [error, setError] = useState('') 85 86 const sendEmail = useCallback(async () => { 87 if (emailState === EmailState.PENDING) { 88 return 89 } 90 try { 91 setEmailState(EmailState.PENDING) 92 await agent.com.atproto.server.requestAccountDelete() 93 setError('') 94 setEmailSentCount(prevCount => prevCount + 1) 95 setStep(Step.VERIFY_CODE) 96 } catch (e: any) { 97 const {clean, raw} = cleanError(e) 98 const error = clean || raw || e 99 setError(error) 100 logger.error(raw || e, { 101 message: 'Failed to send account deletion verification email', 102 }) 103 } finally { 104 setEmailState(EmailState.DEFAULT) 105 } 106 }, [agent, cleanError, emailState, setEmailState]) 107 108 const confirmDeletion = useCallback(async () => { 109 try { 110 setError('') 111 if (!currentAccount?.did) { 112 throw new Error('Invalid did') 113 } 114 const token = confirmCode.replace(WHITESPACE_RE, '') 115 // Inform chat service of intent to delete account. 116 const {success} = await agent.api.chat.bsky.actor.deleteAccount( 117 undefined, 118 { 119 headers: DM_SERVICE_HEADERS, 120 }, 121 ) 122 if (!success) { 123 throw new Error('Failed to inform chat service of account deletion') 124 } 125 await agent.com.atproto.server.deleteAccount({ 126 did: currentAccount.did, 127 password, 128 token, 129 }) 130 control.close(() => { 131 toast.show(_(msg`Your account has been deleted, see ya! ✌️`)) 132 resetToTab('HomeTab') 133 removeAccount(currentAccount) 134 }) 135 } catch (e: any) { 136 const {clean, raw} = cleanError(e) 137 const error = clean || raw || e 138 setError(error) 139 logger.error(raw || e, { 140 message: 'Failed to delete account', 141 }) 142 setConfirmCode('') 143 setPassword('') 144 setStep(Step.VERIFY_CODE) 145 } 146 }, [ 147 _, 148 agent, 149 cleanError, 150 confirmCode, 151 control, 152 currentAccount, 153 password, 154 removeAccount, 155 ]) 156 157 const handleDeactivate = useCallback(() => { 158 control.close(() => deactivateDialogControl.open()) 159 }, [control, deactivateDialogControl]) 160 161 const handleSendEmail = useCallback(() => { 162 void sendEmail() 163 }, [sendEmail]) 164 165 const handleSubmitConfirmCode = useCallback(() => { 166 passwordRef.current?.focus() 167 }, []) 168 169 const handleDeleteAccount = useCallback(() => { 170 setStep(Step.CONFIRM_DELETION) 171 }, [setStep]) 172 173 const handleConfirmDeletion = useCallback(() => { 174 void confirmDeletion() 175 }, [confirmDeletion]) 176 177 const currentHandle = sanitizeHandle(currentAccount?.handle ?? '', '@') 178 const currentEmail = currentAccount?.email ?? '(no email)' 179 180 switch (step) { 181 case Step.SEND_CODE: 182 return ( 183 <> 184 <Prompt.Content> 185 <Prompt.TitleText> 186 {_(msg`Delete account “${currentHandle}`)} 187 </Prompt.TitleText> 188 <Prompt.DescriptionText> 189 <Trans> 190 For security reasons, well need to send a confirmation code to 191 your email address{' '} 192 <Span style={[a.font_semi_bold, t.atoms.text]}> 193 {currentEmail} 194 </Span> 195 . 196 </Trans> 197 </Prompt.DescriptionText> 198 </Prompt.Content> 199 <Prompt.Actions> 200 <Prompt.Action 201 icon={emailState === EmailState.PENDING ? Loader : Envelope} 202 cta={_(msg`Send email`)} 203 shouldCloseOnPress={false} 204 onPress={handleSendEmail} 205 /> 206 <Prompt.Cancel /> 207 </Prompt.Actions> 208 {error && ( 209 <Admonition style={[a.mt_lg]} type="error"> 210 <Text style={[a.flex_1, a.leading_snug]}>{error}</Text> 211 </Admonition> 212 )} 213 <Admonition style={[a.mt_lg]} type="tip"> 214 <Trans> 215 You can also{' '} 216 <Span 217 style={[{color: t.palette.primary_500}, web(a.underline)]} 218 onPress={handleDeactivate}> 219 temporarily deactivate 220 </Span>{' '} 221 your account instead. Your profile, posts, feeds, and lists will 222 no longer be visible to other Bluesky users. You can reactivate 223 your account at any time by logging in. 224 </Trans> 225 </Admonition> 226 </> 227 ) 228 case Step.VERIFY_CODE: 229 return ( 230 <> 231 <Prompt.Content> 232 <Prompt.TitleText> 233 {_(msg`Delete account “${currentHandle}`)} 234 </Prompt.TitleText> 235 <Prompt.DescriptionText> 236 <Trans> 237 Check{' '} 238 <Span style={[a.font_semi_bold, t.atoms.text]}> 239 {currentEmail} 240 </Span>{' '} 241 for an email with the confirmation code to enter below: 242 </Trans> 243 </Prompt.DescriptionText> 244 </Prompt.Content> 245 <View style={[a.mb_xs]}> 246 <TextField.LabelText> 247 <Trans>Confirmation code</Trans> 248 </TextField.LabelText> 249 <TokenField 250 value={confirmCode} 251 onChangeText={setConfirmCode} 252 onSubmitEditing={handleSubmitConfirmCode} 253 /> 254 </View> 255 <Text 256 style={[ 257 a.text_sm, 258 a.leading_snug, 259 a.mb_lg, 260 t.atoms.text_contrast_medium, 261 ]}> 262 {emailSentCount > 1 ? ( 263 <Trans> 264 Email sent!{' '} 265 <InlineLinkText 266 label={_(msg`Resend`)} 267 {...createStaticClick(() => { 268 void handleSendEmail() 269 })}> 270 Click here to resend. 271 </InlineLinkText> 272 </Trans> 273 ) : ( 274 <Trans> 275 Dont see a code?{' '} 276 <InlineLinkText 277 label={_(msg`Resend`)} 278 {...createStaticClick(() => { 279 void handleSendEmail() 280 })}> 281 Click here to resend. 282 </InlineLinkText> 283 </Trans> 284 )}{' '} 285 <Span style={{top: 1}}> 286 {emailState === EmailState.PENDING ? <Loader size="xs" /> : null} 287 </Span> 288 </Text> 289 <View style={[a.mb_xl]}> 290 <TextField.LabelText> 291 <Trans>Password</Trans> 292 </TextField.LabelText> 293 <TextField.Root> 294 <TextField.Icon icon={Lock} /> 295 <TextField.Input 296 inputRef={passwordRef} 297 testID="newPasswordInput" 298 label={_(msg`Enter your password`)} 299 autoCapitalize="none" 300 autoCorrect={false} 301 returnKeyType="done" 302 secureTextEntry={true} 303 autoComplete="off" 304 clearButtonMode="while-editing" 305 passwordRules={`minlength: ${PASSWORD_MIN_LENGTH}};`} 306 value={password} 307 onChangeText={setPassword} 308 onSubmitEditing={handleDeleteAccount} 309 /> 310 </TextField.Root> 311 </View> 312 <Prompt.Actions> 313 <Prompt.Action 314 color="negative" 315 disabled={!isValidCode(confirmCode) || !isPasswordValid(password)} 316 cta={_(msg`Delete my account`)} 317 shouldCloseOnPress={false} 318 onPress={handleDeleteAccount} 319 /> 320 <Prompt.Cancel /> 321 </Prompt.Actions> 322 {error && ( 323 <Admonition style={[a.mt_lg]} type="error"> 324 <Text style={[a.flex_1, a.leading_snug]}>{error}</Text> 325 </Admonition> 326 )} 327 </> 328 ) 329 case Step.CONFIRM_DELETION: 330 return ( 331 <> 332 <Prompt.Content> 333 <Prompt.TitleText> 334 {_(msg`Are you really, really sure?`)} 335 </Prompt.TitleText> 336 <Prompt.DescriptionText> 337 <Trans> 338 This will irreversibly delete your Bluesky account{' '} 339 <Span style={[a.font_semi_bold, t.atoms.text]}> 340 {currentHandle} 341 </Span>{' '} 342 and all associated data. Note that this will affect any other{' '} 343 <InlineLinkText 344 label={_(msg`Learn more about the AT Protocol.`)} 345 style={[a.text_md]} 346 to="https://bsky.social/about/faq"> 347 AT Protocol 348 </InlineLinkText>{' '} 349 services you use with this account. 350 </Trans> 351 </Prompt.DescriptionText> 352 </Prompt.Content> 353 <Prompt.Actions> 354 <Prompt.Action 355 color="negative" 356 cta={_(msg`Yes, delete my account`)} 357 shouldCloseOnPress={false} 358 onPress={handleConfirmDeletion} 359 /> 360 <Prompt.Cancel /> 361 </Prompt.Actions> 362 </> 363 ) 364 } 365}