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} from '@lingui/core/macro'
6import {useLingui} from '@lingui/react'
7import {Trans} from '@lingui/react/macro'
8import {useNavigation} from '@react-navigation/native'
9import {type NativeStackScreenProps} from '@react-navigation/native-stack'
10
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 {sanitizeDisplayName} from '#/lib/strings/display-names'
19import {sanitizeHandle} from '#/lib/strings/handles'
20import {useProfileShadow} from '#/state/cache/profile-shadow'
21import * as persisted from '#/state/persisted'
22import {clearStorage} from '#/state/persisted'
23import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
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 {pdsAgent} from '#/state/session/agent'
30import {useOnboardingDispatch} from '#/state/shell'
31import {useLoggedOutViewControls} from '#/state/shell/logged-out'
32import {useCloseAllActiveElements} from '#/state/util'
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 {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom'
44import {Bell_Stroke2_Corner0_Rounded as NotificationIcon} from '#/components/icons/Bell'
45import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo'
46import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron'
47import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion'
48import {CodeBrackets_Stroke2_Corner2_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets'
49import {Contacts_Stroke2_Corner2_Rounded as ContactsIcon} from '#/components/icons/Contacts'
50import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
51import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe'
52import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock'
53import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller'
54import {
55 Person_Stroke2_Corner2_Rounded as PersonIcon,
56 PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon,
57 PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon,
58 PersonX_Stroke2_Corner0_Rounded as PersonXIcon,
59} from '#/components/icons/Person'
60import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand'
61import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window'
62import * as Layout from '#/components/Layout'
63import {Loader} from '#/components/Loader'
64import * as Menu from '#/components/Menu'
65import {ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config'
66import {ProfileBadges} from '#/components/ProfileBadges'
67import * as Prompt from '#/components/Prompt'
68import * as Toast from '#/components/Toast'
69import {Text} from '#/components/Typography'
70import {useAnalytics} from '#/analytics'
71import {IS_INTERNAL, IS_IOS, IS_NATIVE} from '#/env'
72import {useActorStatus} from '#/features/liveNow'
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 ax = useAnalytics()
79 const {_} = useLingui()
80 const reducedMotion = useReducedMotion()
81 const {logoutEveryAccount} = useSessionApi()
82 const {accounts, currentAccount} = useSession()
83 const switchAccountControl = useDialogControl()
84 const signOutPromptControl = Prompt.usePromptControl()
85 const {data: profile} = useProfileQuery({did: currentAccount?.did})
86 const {data: otherProfiles} = useProfilesQuery({
87 handles: accounts
88 .filter(acc => acc.did !== currentAccount?.did)
89 .map(acc => acc.handle),
90 })
91 const {pendingDid, onPressSwitchAccount} = useAccountSwitcher()
92 const [showAccounts, setShowAccounts] = useState(false)
93 const [showDevOptions, setShowDevOptions] = useState(false)
94 const findContactsEnabled =
95 useIsFindContactsFeatureEnabledBasedOnGeolocation()
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={(account, logContext) =>
168 void onPressSwitchAccount(account, logContext)
169 }
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 {IS_NATIVE &&
217 findContactsEnabled &&
218 !ax.features.enabled(ax.features.ImportContactsSettingsDisable) && (
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 to="/settings/runes" label={_(msg`Runes`)}>
237 <SettingsList.ItemIcon icon={AtomIcon} />
238 <SettingsList.ItemText>
239 <Trans>Runes</Trans>
240 </SettingsList.ItemText>
241 </SettingsList.LinkItem>
242 <SettingsList.LinkItem
243 to="/settings/accessibility"
244 label={_(msg`Accessibility`)}>
245 <SettingsList.ItemIcon icon={AccessibilityIcon} />
246 <SettingsList.ItemText>
247 <Trans>Accessibility</Trans>
248 </SettingsList.ItemText>
249 </SettingsList.LinkItem>
250 <SettingsList.LinkItem
251 to="/settings/language"
252 label={_(msg`Languages`)}>
253 <SettingsList.ItemIcon icon={EarthIcon} />
254 <SettingsList.ItemText>
255 <Trans>Languages</Trans>
256 </SettingsList.ItemText>
257 </SettingsList.LinkItem>
258 <SettingsList.PressableItem
259 onPress={() => void Linking.openURL(HELP_DESK_URL)}
260 label={_(msg`Code`)}
261 accessibilityHint={_(msg`Opens code repository in browser`)}>
262 <SettingsList.ItemIcon icon={CircleQuestionIcon} />
263 <SettingsList.ItemText>
264 <Trans>Source code</Trans>
265 </SettingsList.ItemText>
266 <SettingsList.Chevron />
267 </SettingsList.PressableItem>
268 <SettingsList.LinkItem to="/settings/about" label={_(msg`About`)}>
269 <SettingsList.ItemIcon icon={BubbleInfoIcon} />
270 <SettingsList.ItemText>
271 <Trans>About</Trans>
272 </SettingsList.ItemText>
273 </SettingsList.LinkItem>
274 <SettingsList.Divider />
275 <SettingsList.PressableItem
276 destructive
277 onPress={() => signOutPromptControl.open()}
278 label={_(msg`Sign out`)}>
279 <SettingsList.ItemText>
280 <Trans>Sign out</Trans>
281 </SettingsList.ItemText>
282 </SettingsList.PressableItem>
283 {IS_INTERNAL && (
284 <>
285 <SettingsList.Divider />
286 <SettingsList.PressableItem
287 onPress={() => {
288 if (!reducedMotion) {
289 LayoutAnimation.configureNext(
290 LayoutAnimation.Presets.easeInEaseOut,
291 )
292 }
293 setShowDevOptions(d => !d)
294 }}
295 label={_(msg`Developer options`)}>
296 <SettingsList.ItemIcon icon={CodeBracketsIcon} />
297 <SettingsList.ItemText>
298 <Trans>Developer options</Trans>
299 </SettingsList.ItemText>
300 </SettingsList.PressableItem>
301 {showDevOptions && <DevOptions />}
302 </>
303 )}
304 </SettingsList.Container>
305 </Layout.Content>
306
307 <Prompt.Basic
308 control={signOutPromptControl}
309 title={_(msg`Sign out?`)}
310 description={_(msg`You will be signed out of all your accounts.`)}
311 onConfirm={() => logoutEveryAccount('Settings')}
312 confirmButtonCta={_(msg`Sign out`)}
313 cancelButtonCta={_(msg`Cancel`)}
314 confirmButtonColor="negative"
315 />
316
317 <SwitchAccountDialog control={switchAccountControl} />
318 </Layout.Screen>
319 )
320}
321
322function ProfilePreview({
323 profile,
324}: {
325 profile: AppBskyActorDefs.ProfileViewDetailed
326}) {
327 const t = useTheme()
328 const {gtMobile} = useBreakpoints()
329 const shadow = useProfileShadow(profile)
330 const moderationOpts = useModerationOpts()
331 const {isActive: live} = useActorStatus(profile)
332
333 if (!moderationOpts) return null
334
335 const moderation = moderateProfile(profile, moderationOpts)
336 const displayName = sanitizeDisplayName(
337 profile.displayName || sanitizeHandle(profile.handle),
338 moderation.ui('displayName'),
339 )
340
341 return (
342 <>
343 <UserAvatar
344 size={80}
345 avatar={shadow.avatar}
346 moderation={moderation.ui('avatar')}
347 type={shadow.associated?.labeler ? 'labeler' : 'user'}
348 live={live}
349 />
350
351 <View
352 style={[
353 a.flex_row,
354 a.gap_xs,
355 a.align_center,
356 a.justify_center,
357 a.w_full,
358 ]}>
359 <Text
360 emoji
361 testID="profileHeaderDisplayName"
362 numberOfLines={1}
363 style={[
364 a.pt_sm,
365 t.atoms.text,
366 gtMobile ? a.text_4xl : a.text_3xl,
367 a.font_bold,
368 ]}>
369 {displayName}
370 </Text>
371 <ProfileBadges
372 profile={shadow}
373 size="xl"
374 interactive
375 style={[
376 {
377 marginTop: platform({web: 8, ios: 8, android: 10}),
378 },
379 ]}
380 />
381 </View>
382 <Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
383 {sanitizeHandle(profile.handle, '@')}
384 </Text>
385 </>
386 )
387}
388
389function DevOptions() {
390 const {_} = useLingui()
391 const agent = useAgent()
392 const [override, setOverride] = useStorage(device, [
393 'policyUpdateDebugOverride',
394 ])
395 const onboardingDispatch = useOnboardingDispatch()
396 const navigation = useNavigation<NavigationProp>()
397 const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration()
398 const {
399 tryApplyUpdate,
400 revertToEmbedded,
401 isCurrentlyRunningPullRequestDeployment,
402 currentChannel,
403 } = useApplyPullRequestOTAUpdate()
404 const [actyNotifNudged, setActyNotifNudged] = useActivitySubscriptionsNudged()
405
406 const resetOnboarding = () => {
407 navigation.navigate('Home')
408 onboardingDispatch({type: 'start'})
409 Toast.show(_(msg`Onboarding reset`))
410 }
411
412 const clearAllStorage = async () => {
413 await clearStorage()
414 Toast.show(_(msg`Storage cleared, you need to restart the app now.`))
415 }
416
417 const onPressUnsnoozeReminder = () => {
418 const lastEmailConfirm = new Date()
419 // wind back 3 days
420 lastEmailConfirm.setDate(lastEmailConfirm.getDate() - 3)
421 void persisted.write('reminders', {
422 ...persisted.get('reminders'),
423 lastEmailConfirm: lastEmailConfirm.toISOString(),
424 })
425 Toast.show(_(msg`You probably want to restart the app now.`))
426 }
427
428 const onPressActySubsUnNudge = () => {
429 setActyNotifNudged(false)
430 }
431
432 const onPressApplyOta = () => {
433 Alert.prompt(
434 'Apply OTA',
435 'Enter the channel for the OTA you wish to apply.',
436 [
437 {
438 style: 'cancel',
439 text: 'Cancel',
440 },
441 {
442 style: 'default',
443 text: 'Apply',
444 onPress: (channel?: string) => {
445 void tryApplyUpdate(channel ?? '')
446 },
447 },
448 ],
449 'plain-text',
450 isCurrentlyRunningPullRequestDeployment
451 ? currentChannel
452 : 'pull-request-',
453 )
454 }
455
456 return (
457 <>
458 <SettingsList.PressableItem
459 onPress={() => navigation.navigate('Log')}
460 label={_(msg`Open system log`)}>
461 <SettingsList.ItemText>
462 <Trans>System log</Trans>
463 </SettingsList.ItemText>
464 </SettingsList.PressableItem>
465 <SettingsList.PressableItem
466 onPress={() => navigation.navigate('Debug')}
467 label={_(msg`Open storybook page`)}>
468 <SettingsList.ItemText>
469 <Trans>Storybook</Trans>
470 </SettingsList.ItemText>
471 </SettingsList.PressableItem>
472 <SettingsList.PressableItem
473 onPress={() => navigation.navigate('DebugMod')}
474 label={_(msg`Open moderation debug page`)}>
475 <SettingsList.ItemText>
476 <Trans>Debug Moderation</Trans>
477 </SettingsList.ItemText>
478 </SettingsList.PressableItem>
479 <SettingsList.PressableItem
480 onPress={() => deleteChatDeclarationRecord()}
481 label={_(msg`Open storybook page`)}>
482 <SettingsList.ItemText>
483 <Trans>Delete chat declaration record</Trans>
484 </SettingsList.ItemText>
485 </SettingsList.PressableItem>
486 <SettingsList.PressableItem
487 onPress={() => void resetOnboarding()}
488 label={_(msg`Reset onboarding state`)}>
489 <SettingsList.ItemText>
490 <Trans>Reset onboarding state</Trans>
491 </SettingsList.ItemText>
492 </SettingsList.PressableItem>
493 <SettingsList.PressableItem
494 onPress={onPressUnsnoozeReminder}
495 label={_(msg`Unsnooze email reminder`)}>
496 <SettingsList.ItemText>
497 <Trans>Unsnooze email reminder</Trans>
498 </SettingsList.ItemText>
499 </SettingsList.PressableItem>
500 {actyNotifNudged && (
501 <SettingsList.PressableItem
502 onPress={onPressActySubsUnNudge}
503 label={_(msg`Reset activity subscription nudge`)}>
504 <SettingsList.ItemText>
505 <Trans>Reset activity subscription nudge</Trans>
506 </SettingsList.ItemText>
507 </SettingsList.PressableItem>
508 )}
509 <SettingsList.PressableItem
510 onPress={() => void clearAllStorage()}
511 label={_(msg`Clear all storage data`)}>
512 <SettingsList.ItemText>
513 <Trans>Clear all storage data (restart after this)</Trans>
514 </SettingsList.ItemText>
515 </SettingsList.PressableItem>
516 {IS_IOS ? (
517 <SettingsList.PressableItem
518 onPress={onPressApplyOta}
519 label={_(msg`Apply Pull Request`)}>
520 <SettingsList.ItemText>
521 <Trans>Apply Pull Request</Trans>
522 </SettingsList.ItemText>
523 </SettingsList.PressableItem>
524 ) : null}
525 {IS_NATIVE && isCurrentlyRunningPullRequestDeployment ? (
526 <SettingsList.PressableItem
527 onPress={() => void revertToEmbedded()}
528 label={_(msg`Unapply Pull Request`)}>
529 <SettingsList.ItemText>
530 <Trans>Unapply Pull Request {currentChannel}</Trans>
531 </SettingsList.ItemText>
532 </SettingsList.PressableItem>
533 ) : null}
534 <SettingsList.Divider />
535 <View style={[a.p_xl, a.gap_md]}>
536 <Text style={[a.text_lg, a.font_semi_bold]}>
537 PolicyUpdate202508 Debug
538 </Text>
539
540 <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_md]}>
541 <Button
542 onPress={() => {
543 setOverride(!override)
544 }}
545 label="Toggle"
546 color={override ? 'primary' : 'secondary'}
547 size="small"
548 style={[a.flex_1]}>
549 <ButtonText>
550 {override ? 'Disable debug mode' : 'Enable debug mode'}
551 </ButtonText>
552 </Button>
553
554 <Button
555 onPress={() => {
556 device.set([PolicyUpdate202508], false)
557 void pdsAgent(agent).bskyAppRemoveNuxs([PolicyUpdate202508])
558 Toast.show(`Done`, {
559 type: 'info',
560 })
561 }}
562 label="Reset policy update nux"
563 color="secondary"
564 size="small"
565 disabled={!override}>
566 <ButtonText>Reset state</ButtonText>
567 </Button>
568 </View>
569 </View>
570 <SettingsList.Divider />
571 </>
572 )
573}
574
575function AddAccountRow() {
576 const {_} = useLingui()
577 const {setShowLoggedOut} = useLoggedOutViewControls()
578 const closeEverything = useCloseAllActiveElements()
579
580 const onAddAnotherAccount = () => {
581 setShowLoggedOut(true)
582 closeEverything()
583 }
584
585 return (
586 <SettingsList.PressableItem
587 onPress={onAddAnotherAccount}
588 label={_(msg`Add another account`)}>
589 <SettingsList.ItemIcon icon={PersonPlusIcon} />
590 <SettingsList.ItemText>
591 <Trans>Add another account</Trans>
592 </SettingsList.ItemText>
593 </SettingsList.PressableItem>
594 )
595}
596
597function AccountRow({
598 profile,
599 account,
600 pendingDid,
601 onPressSwitchAccount,
602}: {
603 profile?: AppBskyActorDefs.ProfileViewDetailed
604 account: SessionAccount
605 pendingDid: string | null
606 onPressSwitchAccount: (
607 account: SessionAccount,
608 logContext: 'Settings',
609 ) => void
610}) {
611 const {_} = useLingui()
612 const t = useTheme()
613
614 const moderationOpts = useModerationOpts()
615 const removePromptControl = Prompt.usePromptControl()
616 const {removeAccount} = useSessionApi()
617 const {isActive: live} = useActorStatus(profile)
618
619 const enableSquareButtons = useEnableSquareButtons()
620
621 const onSwitchAccount = () => {
622 if (pendingDid) return
623 onPressSwitchAccount(account, 'Settings')
624 }
625
626 return (
627 <View style={[a.relative]}>
628 <SettingsList.PressableItem
629 onPress={onSwitchAccount}
630 label={_(msg`Switch account`)}>
631 {moderationOpts && profile ? (
632 <UserAvatar
633 size={28}
634 avatar={profile.avatar}
635 moderation={moderateProfile(profile, moderationOpts).ui('avatar')}
636 type={profile.associated?.labeler ? 'labeler' : 'user'}
637 live={live}
638 hideLiveBadge
639 />
640 ) : (
641 <View style={[{width: 28}]} />
642 )}
643 <SettingsList.ItemText
644 numberOfLines={1}
645 style={[a.pr_2xl, a.leading_snug]}>
646 {sanitizeHandle(account.handle, '@')}
647 </SettingsList.ItemText>
648 {pendingDid === account.did && <SettingsList.ItemIcon icon={Loader} />}
649 </SettingsList.PressableItem>
650 {!pendingDid && (
651 <Menu.Root>
652 <Menu.Trigger label={_(msg`Account options`)}>
653 {({props, state}) => (
654 <Pressable
655 {...props}
656 style={[
657 a.absolute,
658 {top: 10, right: tokens.space.lg},
659 a.p_xs,
660 enableSquareButtons ? a.rounded_sm : a.rounded_full,
661 (state.hovered || state.pressed) && t.atoms.bg_contrast_25,
662 ]}>
663 <DotsHorizontal size="md" style={t.atoms.text} />
664 </Pressable>
665 )}
666 </Menu.Trigger>
667 <Menu.Outer showCancel>
668 <Menu.Item
669 label={_(msg`Remove account`)}
670 onPress={() => removePromptControl.open()}>
671 <Menu.ItemText>
672 <Trans>Remove account</Trans>
673 </Menu.ItemText>
674 <Menu.ItemIcon icon={PersonXIcon} />
675 </Menu.Item>
676 </Menu.Outer>
677 </Menu.Root>
678 )}
679
680 <Prompt.Basic
681 control={removePromptControl}
682 title={_(msg`Remove from quick access?`)}
683 description={_(
684 msg`This will remove @${account.handle} from the quick access list.`,
685 )}
686 onConfirm={() => {
687 removeAccount(account)
688 Toast.show(_(msg`Account removed from quick access`))
689 }}
690 confirmButtonCta={_(msg`Remove`)}
691 confirmButtonColor="negative"
692 />
693 </View>
694 )
695}