forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useState} from 'react'
2import {Alert, LayoutAnimation, Pressable, View} from 'react-native'
3import {Linking} from 'react-native'
4import {useReducedMotion} from 'react-native-reanimated'
5import {type AppBskyActorDefs, moderateProfile} from '@atproto/api'
6import {msg, Trans} from '@lingui/macro'
7import {useLingui} from '@lingui/react'
8import {useNavigation} from '@react-navigation/native'
9import {type NativeStackScreenProps} from '@react-navigation/native-stack'
10
11import {useActorStatus} from '#/lib/actor-status'
12import {HELP_DESK_URL} from '#/lib/constants'
13import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
14import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates'
15import {
16 type CommonNavigatorParams,
17 type NavigationProp,
18} from '#/lib/routes/types'
19import {useGate} from '#/lib/statsig/statsig'
20import {sanitizeDisplayName} from '#/lib/strings/display-names'
21import {sanitizeHandle} from '#/lib/strings/handles'
22import {isIOS, isNative} from '#/platform/detection'
23import {useProfileShadow} from '#/state/cache/profile-shadow'
24import * as persisted from '#/state/persisted'
25import {clearStorage} from '#/state/persisted'
26import {useModerationOpts} from '#/state/preferences/moderation-opts'
27import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration'
28import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile'
29import {useAgent} from '#/state/session'
30import {type SessionAccount, useSession, useSessionApi} from '#/state/session'
31import {useOnboardingDispatch} from '#/state/shell'
32import {useLoggedOutViewControls} from '#/state/shell/logged-out'
33import {useCloseAllActiveElements} from '#/state/util'
34import * as Toast from '#/view/com/util/Toast'
35import {UserAvatar} from '#/view/com/util/UserAvatar'
36import * as SettingsList from '#/screens/Settings/components/SettingsList'
37import {atoms as a, platform, tokens, useBreakpoints, useTheme} from '#/alf'
38import {AgeAssuranceDismissibleNotice} from '#/components/ageAssurance/AgeAssuranceDismissibleNotice'
39import {AvatarStackWithFetch} from '#/components/AvatarStack'
40import {Button, ButtonText} from '#/components/Button'
41import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from '#/components/contacts/country-allowlist'
42import {useDialogControl} from '#/components/Dialog'
43import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount'
44import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
45import {Bell_Stroke2_Corner0_Rounded as NotificationIcon} from '#/components/icons/Bell'
46import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo'
47import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron'
48import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion'
49import {CodeBrackets_Stroke2_Corner2_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets'
50import {Contacts_Stroke2_Corner2_Rounded as ContactsIcon} from '#/components/icons/Contacts'
51import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
52import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe'
53import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock'
54import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller'
55import {
56 Person_Stroke2_Corner2_Rounded as PersonIcon,
57 PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon,
58 PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon,
59 PersonX_Stroke2_Corner0_Rounded as PersonXIcon,
60} from '#/components/icons/Person'
61import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand'
62import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window'
63import * as Layout from '#/components/Layout'
64import {Loader} from '#/components/Loader'
65import * as Menu from '#/components/Menu'
66import {ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config'
67import * as Prompt from '#/components/Prompt'
68import {Text} from '#/components/Typography'
69import {useFullVerificationState} from '#/components/verification'
70import {
71 shouldShowVerificationCheckButton,
72 VerificationCheckButton,
73} from '#/components/verification/VerificationCheckButton'
74import {IS_INTERNAL} from '#/env'
75import {device, useStorage} from '#/storage'
76import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
77
78type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
79export function SettingsScreen({}: Props) {
80 const {_} = useLingui()
81 const reducedMotion = useReducedMotion()
82 const {logoutEveryAccount} = useSessionApi()
83 const {accounts, currentAccount} = useSession()
84 const switchAccountControl = useDialogControl()
85 const signOutPromptControl = Prompt.usePromptControl()
86 const {data: profile} = useProfileQuery({did: currentAccount?.did})
87 const {data: otherProfiles} = useProfilesQuery({
88 handles: accounts
89 .filter(acc => acc.did !== currentAccount?.did)
90 .map(acc => acc.handle),
91 })
92 const {pendingDid, onPressSwitchAccount} = useAccountSwitcher()
93 const [showAccounts, setShowAccounts] = useState(false)
94 const [showDevOptions, setShowDevOptions] = useState(false)
95 const findContactsEnabled =
96 useIsFindContactsFeatureEnabledBasedOnGeolocation()
97 const gate = useGate()
98
99 return (
100 <Layout.Screen>
101 <Layout.Header.Outer>
102 <Layout.Header.BackButton />
103 <Layout.Header.Content>
104 <Layout.Header.TitleText>
105 <Trans>Settings</Trans>
106 </Layout.Header.TitleText>
107 </Layout.Header.Content>
108 <Layout.Header.Slot />
109 </Layout.Header.Outer>
110 <Layout.Content>
111 <SettingsList.Container>
112 <AgeAssuranceDismissibleNotice style={[a.px_lg, a.pt_xs, a.pb_xl]} />
113
114 <View
115 style={[
116 a.px_xl,
117 a.pt_md,
118 a.pb_md,
119 a.w_full,
120 a.gap_2xs,
121 a.align_center,
122 {minHeight: 160},
123 ]}>
124 {profile && <ProfilePreview profile={profile} />}
125 </View>
126 {accounts.length > 1 ? (
127 <>
128 <SettingsList.PressableItem
129 label={_(msg`Switch account`)}
130 accessibilityHint={_(
131 msg`Shows other accounts you can switch to`,
132 )}
133 onPress={() => {
134 if (!reducedMotion) {
135 LayoutAnimation.configureNext(
136 LayoutAnimation.Presets.easeInEaseOut,
137 )
138 }
139 setShowAccounts(s => !s)
140 }}>
141 <SettingsList.ItemIcon icon={PersonGroupIcon} />
142 <SettingsList.ItemText>
143 <Trans>Switch account</Trans>
144 </SettingsList.ItemText>
145 {showAccounts ? (
146 <SettingsList.ItemIcon icon={ChevronUpIcon} size="md" />
147 ) : (
148 <AvatarStackWithFetch
149 profiles={accounts
150 .map(acc => acc.did)
151 .filter(did => did !== currentAccount?.did)
152 .slice(0, 5)}
153 />
154 )}
155 </SettingsList.PressableItem>
156 {showAccounts && (
157 <>
158 <SettingsList.Divider />
159 {accounts
160 .filter(acc => acc.did !== currentAccount?.did)
161 .map(account => (
162 <AccountRow
163 key={account.did}
164 account={account}
165 profile={otherProfiles?.profiles?.find(
166 p => p.did === account.did,
167 )}
168 pendingDid={pendingDid}
169 onPressSwitchAccount={onPressSwitchAccount}
170 />
171 ))}
172 <AddAccountRow />
173 </>
174 )}
175 </>
176 ) : (
177 <AddAccountRow />
178 )}
179 <SettingsList.Divider />
180 <SettingsList.LinkItem to="/settings/account" label={_(msg`Account`)}>
181 <SettingsList.ItemIcon icon={PersonIcon} />
182 <SettingsList.ItemText>
183 <Trans>Account</Trans>
184 </SettingsList.ItemText>
185 </SettingsList.LinkItem>
186 <SettingsList.LinkItem
187 to="/settings/privacy-and-security"
188 label={_(msg`Privacy and security`)}>
189 <SettingsList.ItemIcon icon={LockIcon} />
190 <SettingsList.ItemText>
191 <Trans>Privacy and security</Trans>
192 </SettingsList.ItemText>
193 </SettingsList.LinkItem>
194 <SettingsList.LinkItem to="/moderation" label={_(msg`Moderation`)}>
195 <SettingsList.ItemIcon icon={HandIcon} />
196 <SettingsList.ItemText>
197 <Trans>Moderation</Trans>
198 </SettingsList.ItemText>
199 </SettingsList.LinkItem>
200 <SettingsList.LinkItem
201 to="/settings/notifications"
202 label={_(msg`Notifications`)}>
203 <SettingsList.ItemIcon icon={NotificationIcon} />
204 <SettingsList.ItemText>
205 <Trans>Notifications</Trans>
206 </SettingsList.ItemText>
207 </SettingsList.LinkItem>
208 <SettingsList.LinkItem
209 to="/settings/content-and-media"
210 label={_(msg`Content and media`)}>
211 <SettingsList.ItemIcon icon={WindowIcon} />
212 <SettingsList.ItemText>
213 <Trans>Content and media</Trans>
214 </SettingsList.ItemText>
215 </SettingsList.LinkItem>
216 {isNative &&
217 findContactsEnabled &&
218 !gate('disable_settings_find_contacts') && (
219 <SettingsList.LinkItem
220 to="/settings/find-contacts"
221 label={_(msg`Find friends from contacts`)}>
222 <SettingsList.ItemIcon icon={ContactsIcon} />
223 <SettingsList.ItemText>
224 <Trans>Find friends from contacts</Trans>
225 </SettingsList.ItemText>
226 </SettingsList.LinkItem>
227 )}
228 <SettingsList.LinkItem
229 to="/settings/appearance"
230 label={_(msg`Appearance`)}>
231 <SettingsList.ItemIcon icon={PaintRollerIcon} />
232 <SettingsList.ItemText>
233 <Trans>Appearance</Trans>
234 </SettingsList.ItemText>
235 </SettingsList.LinkItem>
236 <SettingsList.LinkItem
237 to="/settings/accessibility"
238 label={_(msg`Accessibility`)}>
239 <SettingsList.ItemIcon icon={AccessibilityIcon} />
240 <SettingsList.ItemText>
241 <Trans>Accessibility</Trans>
242 </SettingsList.ItemText>
243 </SettingsList.LinkItem>
244 <SettingsList.LinkItem
245 to="/settings/language"
246 label={_(msg`Languages`)}>
247 <SettingsList.ItemIcon icon={EarthIcon} />
248 <SettingsList.ItemText>
249 <Trans>Languages</Trans>
250 </SettingsList.ItemText>
251 </SettingsList.LinkItem>
252 <SettingsList.PressableItem
253 onPress={() => Linking.openURL(HELP_DESK_URL)}
254 label={_(msg`Help`)}
255 accessibilityHint={_(msg`Opens helpdesk in browser`)}>
256 <SettingsList.ItemIcon icon={CircleQuestionIcon} />
257 <SettingsList.ItemText>
258 <Trans>Help</Trans>
259 </SettingsList.ItemText>
260 <SettingsList.Chevron />
261 </SettingsList.PressableItem>
262 <SettingsList.LinkItem to="/settings/about" label={_(msg`About`)}>
263 <SettingsList.ItemIcon icon={BubbleInfoIcon} />
264 <SettingsList.ItemText>
265 <Trans>About</Trans>
266 </SettingsList.ItemText>
267 </SettingsList.LinkItem>
268 <SettingsList.Divider />
269 <SettingsList.PressableItem
270 destructive
271 onPress={() => signOutPromptControl.open()}
272 label={_(msg`Sign out`)}>
273 <SettingsList.ItemText>
274 <Trans>Sign out</Trans>
275 </SettingsList.ItemText>
276 </SettingsList.PressableItem>
277 {IS_INTERNAL && (
278 <>
279 <SettingsList.Divider />
280 <SettingsList.PressableItem
281 onPress={() => {
282 if (!reducedMotion) {
283 LayoutAnimation.configureNext(
284 LayoutAnimation.Presets.easeInEaseOut,
285 )
286 }
287 setShowDevOptions(d => !d)
288 }}
289 label={_(msg`Developer options`)}>
290 <SettingsList.ItemIcon icon={CodeBracketsIcon} />
291 <SettingsList.ItemText>
292 <Trans>Developer options</Trans>
293 </SettingsList.ItemText>
294 </SettingsList.PressableItem>
295 {showDevOptions && <DevOptions />}
296 </>
297 )}
298 </SettingsList.Container>
299 </Layout.Content>
300
301 <Prompt.Basic
302 control={signOutPromptControl}
303 title={_(msg`Sign out?`)}
304 description={_(msg`You will be signed out of all your accounts.`)}
305 onConfirm={() => logoutEveryAccount('Settings')}
306 confirmButtonCta={_(msg`Sign out`)}
307 cancelButtonCta={_(msg`Cancel`)}
308 confirmButtonColor="negative"
309 />
310
311 <SwitchAccountDialog control={switchAccountControl} />
312 </Layout.Screen>
313 )
314}
315
316function ProfilePreview({
317 profile,
318}: {
319 profile: AppBskyActorDefs.ProfileViewDetailed
320}) {
321 const t = useTheme()
322 const {gtMobile} = useBreakpoints()
323 const shadow = useProfileShadow(profile)
324 const moderationOpts = useModerationOpts()
325 const verificationState = useFullVerificationState({
326 profile: shadow,
327 })
328 const {isActive: live} = useActorStatus(profile)
329
330 if (!moderationOpts) return null
331
332 const moderation = moderateProfile(profile, moderationOpts)
333 const displayName = sanitizeDisplayName(
334 profile.displayName || sanitizeHandle(profile.handle),
335 moderation.ui('displayName'),
336 )
337
338 return (
339 <>
340 <UserAvatar
341 size={80}
342 avatar={shadow.avatar}
343 moderation={moderation.ui('avatar')}
344 type={shadow.associated?.labeler ? 'labeler' : 'user'}
345 live={live}
346 />
347
348 <View
349 style={[
350 a.flex_row,
351 a.gap_xs,
352 a.align_center,
353 a.justify_center,
354 a.w_full,
355 ]}>
356 <Text
357 emoji
358 testID="profileHeaderDisplayName"
359 numberOfLines={1}
360 style={[
361 a.pt_sm,
362 t.atoms.text,
363 gtMobile ? a.text_4xl : a.text_3xl,
364 a.font_bold,
365 ]}>
366 {displayName}
367 </Text>
368 {shouldShowVerificationCheckButton(verificationState) && (
369 <View
370 style={[
371 {
372 marginTop: platform({web: 8, ios: 8, android: 10}),
373 },
374 ]}>
375 <VerificationCheckButton profile={shadow} size="lg" />
376 </View>
377 )}
378 </View>
379 <Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
380 {sanitizeHandle(profile.handle, '@')}
381 </Text>
382 </>
383 )
384}
385
386function DevOptions() {
387 const {_} = useLingui()
388 const agent = useAgent()
389 const [override, setOverride] = useStorage(device, [
390 'policyUpdateDebugOverride',
391 ])
392 const onboardingDispatch = useOnboardingDispatch()
393 const navigation = useNavigation<NavigationProp>()
394 const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration()
395 const {
396 tryApplyUpdate,
397 revertToEmbedded,
398 isCurrentlyRunningPullRequestDeployment,
399 currentChannel,
400 } = useApplyPullRequestOTAUpdate()
401 const [actyNotifNudged, setActyNotifNudged] = useActivitySubscriptionsNudged()
402
403 const resetOnboarding = async () => {
404 navigation.navigate('Home')
405 onboardingDispatch({type: 'start'})
406 Toast.show(_(msg`Onboarding reset`))
407 }
408
409 const clearAllStorage = async () => {
410 await clearStorage()
411 Toast.show(_(msg`Storage cleared, you need to restart the app now.`))
412 }
413
414 const onPressUnsnoozeReminder = () => {
415 const lastEmailConfirm = new Date()
416 // wind back 3 days
417 lastEmailConfirm.setDate(lastEmailConfirm.getDate() - 3)
418 persisted.write('reminders', {
419 ...persisted.get('reminders'),
420 lastEmailConfirm: lastEmailConfirm.toISOString(),
421 })
422 Toast.show(_(msg`You probably want to restart the app now.`))
423 }
424
425 const onPressActySubsUnNudge = () => {
426 setActyNotifNudged(false)
427 }
428
429 const onPressApplyOta = () => {
430 Alert.prompt(
431 'Apply OTA',
432 'Enter the channel for the OTA you wish to apply.',
433 [
434 {
435 style: 'cancel',
436 text: 'Cancel',
437 },
438 {
439 style: 'default',
440 text: 'Apply',
441 onPress: (channel?: string) => {
442 tryApplyUpdate(channel ?? '')
443 },
444 },
445 ],
446 'plain-text',
447 isCurrentlyRunningPullRequestDeployment
448 ? currentChannel
449 : 'pull-request-',
450 )
451 }
452
453 return (
454 <>
455 <SettingsList.PressableItem
456 onPress={() => navigation.navigate('Log')}
457 label={_(msg`Open system log`)}>
458 <SettingsList.ItemText>
459 <Trans>System log</Trans>
460 </SettingsList.ItemText>
461 </SettingsList.PressableItem>
462 <SettingsList.PressableItem
463 onPress={() => navigation.navigate('Debug')}
464 label={_(msg`Open storybook page`)}>
465 <SettingsList.ItemText>
466 <Trans>Storybook</Trans>
467 </SettingsList.ItemText>
468 </SettingsList.PressableItem>
469 <SettingsList.PressableItem
470 onPress={() => navigation.navigate('DebugMod')}
471 label={_(msg`Open moderation debug page`)}>
472 <SettingsList.ItemText>
473 <Trans>Debug Moderation</Trans>
474 </SettingsList.ItemText>
475 </SettingsList.PressableItem>
476 <SettingsList.PressableItem
477 onPress={() => deleteChatDeclarationRecord()}
478 label={_(msg`Open storybook page`)}>
479 <SettingsList.ItemText>
480 <Trans>Delete chat declaration record</Trans>
481 </SettingsList.ItemText>
482 </SettingsList.PressableItem>
483 <SettingsList.PressableItem
484 onPress={() => resetOnboarding()}
485 label={_(msg`Reset onboarding state`)}>
486 <SettingsList.ItemText>
487 <Trans>Reset onboarding state</Trans>
488 </SettingsList.ItemText>
489 </SettingsList.PressableItem>
490 <SettingsList.PressableItem
491 onPress={onPressUnsnoozeReminder}
492 label={_(msg`Unsnooze email reminder`)}>
493 <SettingsList.ItemText>
494 <Trans>Unsnooze email reminder</Trans>
495 </SettingsList.ItemText>
496 </SettingsList.PressableItem>
497 {actyNotifNudged && (
498 <SettingsList.PressableItem
499 onPress={onPressActySubsUnNudge}
500 label={_(msg`Reset activity subscription nudge`)}>
501 <SettingsList.ItemText>
502 <Trans>Reset activity subscription nudge</Trans>
503 </SettingsList.ItemText>
504 </SettingsList.PressableItem>
505 )}
506 <SettingsList.PressableItem
507 onPress={() => clearAllStorage()}
508 label={_(msg`Clear all storage data`)}>
509 <SettingsList.ItemText>
510 <Trans>Clear all storage data (restart after this)</Trans>
511 </SettingsList.ItemText>
512 </SettingsList.PressableItem>
513 {isIOS ? (
514 <SettingsList.PressableItem
515 onPress={onPressApplyOta}
516 label={_(msg`Apply Pull Request`)}>
517 <SettingsList.ItemText>
518 <Trans>Apply Pull Request</Trans>
519 </SettingsList.ItemText>
520 </SettingsList.PressableItem>
521 ) : null}
522 {isNative && isCurrentlyRunningPullRequestDeployment ? (
523 <SettingsList.PressableItem
524 onPress={revertToEmbedded}
525 label={_(msg`Unapply Pull Request`)}>
526 <SettingsList.ItemText>
527 <Trans>Unapply Pull Request {currentChannel}</Trans>
528 </SettingsList.ItemText>
529 </SettingsList.PressableItem>
530 ) : null}
531
532 <SettingsList.Divider />
533 <View style={[a.p_xl, a.gap_md]}>
534 <Text style={[a.text_lg, a.font_semi_bold]}>
535 PolicyUpdate202508 Debug
536 </Text>
537
538 <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_md]}>
539 <Button
540 onPress={() => {
541 setOverride(!override)
542 }}
543 label="Toggle"
544 color={override ? 'primary' : 'secondary'}
545 size="small"
546 style={[a.flex_1]}>
547 <ButtonText>
548 {override ? 'Disable debug mode' : 'Enable debug mode'}
549 </ButtonText>
550 </Button>
551
552 <Button
553 onPress={() => {
554 device.set([PolicyUpdate202508], false)
555 agent.bskyAppRemoveNuxs([PolicyUpdate202508])
556 Toast.show(`Done`, 'info')
557 }}
558 label="Reset policy update nux"
559 color="secondary"
560 size="small"
561 disabled={!override}>
562 <ButtonText>Reset state</ButtonText>
563 </Button>
564 </View>
565 </View>
566 <SettingsList.Divider />
567 </>
568 )
569}
570
571function AddAccountRow() {
572 const {_} = useLingui()
573 const {setShowLoggedOut} = useLoggedOutViewControls()
574 const closeEverything = useCloseAllActiveElements()
575
576 const onAddAnotherAccount = () => {
577 setShowLoggedOut(true)
578 closeEverything()
579 }
580
581 return (
582 <SettingsList.PressableItem
583 onPress={onAddAnotherAccount}
584 label={_(msg`Add another account`)}>
585 <SettingsList.ItemIcon icon={PersonPlusIcon} />
586 <SettingsList.ItemText>
587 <Trans>Add another account</Trans>
588 </SettingsList.ItemText>
589 </SettingsList.PressableItem>
590 )
591}
592
593function AccountRow({
594 profile,
595 account,
596 pendingDid,
597 onPressSwitchAccount,
598}: {
599 profile?: AppBskyActorDefs.ProfileViewDetailed
600 account: SessionAccount
601 pendingDid: string | null
602 onPressSwitchAccount: (
603 account: SessionAccount,
604 logContext: 'Settings',
605 ) => void
606}) {
607 const {_} = useLingui()
608 const t = useTheme()
609
610 const moderationOpts = useModerationOpts()
611 const removePromptControl = Prompt.usePromptControl()
612 const {removeAccount} = useSessionApi()
613 const {isActive: live} = useActorStatus(profile)
614
615 const onSwitchAccount = () => {
616 if (pendingDid) return
617 onPressSwitchAccount(account, 'Settings')
618 }
619
620 return (
621 <View style={[a.relative]}>
622 <SettingsList.PressableItem
623 onPress={onSwitchAccount}
624 label={_(msg`Switch account`)}>
625 {moderationOpts && profile ? (
626 <UserAvatar
627 size={28}
628 avatar={profile.avatar}
629 moderation={moderateProfile(profile, moderationOpts).ui('avatar')}
630 type={profile.associated?.labeler ? 'labeler' : 'user'}
631 live={live}
632 hideLiveBadge
633 />
634 ) : (
635 <View style={[{width: 28}]} />
636 )}
637 <SettingsList.ItemText
638 numberOfLines={1}
639 style={[a.pr_2xl, a.leading_snug]}>
640 {sanitizeHandle(account.handle, '@')}
641 </SettingsList.ItemText>
642 {pendingDid === account.did && <SettingsList.ItemIcon icon={Loader} />}
643 </SettingsList.PressableItem>
644 {!pendingDid && (
645 <Menu.Root>
646 <Menu.Trigger label={_(msg`Account options`)}>
647 {({props, state}) => (
648 <Pressable
649 {...props}
650 style={[
651 a.absolute,
652 {top: 10, right: tokens.space.lg},
653 a.p_xs,
654 a.rounded_full,
655 (state.hovered || state.pressed) && t.atoms.bg_contrast_25,
656 ]}>
657 <DotsHorizontal size="md" style={t.atoms.text} />
658 </Pressable>
659 )}
660 </Menu.Trigger>
661 <Menu.Outer showCancel>
662 <Menu.Item
663 label={_(msg`Remove account`)}
664 onPress={() => removePromptControl.open()}>
665 <Menu.ItemText>
666 <Trans>Remove account</Trans>
667 </Menu.ItemText>
668 <Menu.ItemIcon icon={PersonXIcon} />
669 </Menu.Item>
670 </Menu.Outer>
671 </Menu.Root>
672 )}
673
674 <Prompt.Basic
675 control={removePromptControl}
676 title={_(msg`Remove from quick access?`)}
677 description={_(
678 msg`This will remove @${account.handle} from the quick access list.`,
679 )}
680 onConfirm={() => {
681 removeAccount(account)
682 Toast.show(_(msg`Account removed from quick access`))
683 }}
684 confirmButtonCta={_(msg`Remove`)}
685 confirmButtonColor="negative"
686 />
687 </View>
688 )
689}