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