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