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 {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 DeerIcon} 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 {DotGrid_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 {device, useStorage} from '#/storage'
76import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
77
78type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
79export function SettingsScreen({}: Props) {
80 const ax = useAnalytics()
81 const {_} = useLingui()
82 const reducedMotion = useReducedMotion()
83 const {logoutEveryAccount} = useSessionApi()
84 const {accounts, currentAccount} = useSession()
85 const switchAccountControl = useDialogControl()
86 const signOutPromptControl = Prompt.usePromptControl()
87 const {data: profile} = useProfileQuery({did: currentAccount?.did})
88 const {data: otherProfiles} = useProfilesQuery({
89 handles: accounts
90 .filter(acc => acc.did !== currentAccount?.did)
91 .map(acc => acc.handle),
92 })
93 const {pendingDid, onPressSwitchAccount} = useAccountSwitcher()
94 const [showAccounts, setShowAccounts] = useState(false)
95 const [showDevOptions, setShowDevOptions] = useState(false)
96 const findContactsEnabled =
97 useIsFindContactsFeatureEnabledBasedOnGeolocation()
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 {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/deer" label={_(msg`Deer`)}>
237 <SettingsList.ItemIcon icon={DeerIcon} />
238 <SettingsList.ItemText>
239 <Trans>Experiments</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={() => 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 verificationState = useFullVerificationState({
332 profile: shadow,
333 })
334 const {isActive: live} = useActorStatus(profile)
335
336 if (!moderationOpts) return null
337
338 const moderation = moderateProfile(profile, moderationOpts)
339 const displayName = sanitizeDisplayName(
340 profile.displayName || sanitizeHandle(profile.handle),
341 moderation.ui('displayName'),
342 )
343
344 return (
345 <>
346 <UserAvatar
347 size={80}
348 avatar={shadow.avatar}
349 moderation={moderation.ui('avatar')}
350 type={shadow.associated?.labeler ? 'labeler' : 'user'}
351 live={live}
352 />
353
354 <View
355 style={[
356 a.flex_row,
357 a.gap_xs,
358 a.align_center,
359 a.justify_center,
360 a.w_full,
361 ]}>
362 <Text
363 emoji
364 testID="profileHeaderDisplayName"
365 numberOfLines={1}
366 style={[
367 a.pt_sm,
368 t.atoms.text,
369 gtMobile ? a.text_4xl : a.text_3xl,
370 a.font_bold,
371 ]}>
372 {displayName}
373 </Text>
374 {shouldShowVerificationCheckButton(verificationState) && (
375 <View
376 style={[
377 {
378 marginTop: platform({web: 8, ios: 8, android: 10}),
379 },
380 ]}>
381 <VerificationCheckButton profile={shadow} size="lg" />
382 </View>
383 )}
384 </View>
385 <Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
386 {sanitizeHandle(profile.handle, '@')}
387 </Text>
388 </>
389 )
390}
391
392function DevOptions() {
393 const {_} = useLingui()
394 const agent = useAgent()
395 const [override, setOverride] = useStorage(device, [
396 'policyUpdateDebugOverride',
397 ])
398 const onboardingDispatch = useOnboardingDispatch()
399 const navigation = useNavigation<NavigationProp>()
400 const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration()
401 const {
402 tryApplyUpdate,
403 revertToEmbedded,
404 isCurrentlyRunningPullRequestDeployment,
405 currentChannel,
406 } = useApplyPullRequestOTAUpdate()
407 const [actyNotifNudged, setActyNotifNudged] = useActivitySubscriptionsNudged()
408
409 const resetOnboarding = async () => {
410 navigation.navigate('Home')
411 onboardingDispatch({type: 'start'})
412 Toast.show(_(msg`Onboarding reset`))
413 }
414
415 const clearAllStorage = async () => {
416 await clearStorage()
417 Toast.show(_(msg`Storage cleared, you need to restart the app now.`))
418 }
419
420 const onPressUnsnoozeReminder = () => {
421 const lastEmailConfirm = new Date()
422 // wind back 3 days
423 lastEmailConfirm.setDate(lastEmailConfirm.getDate() - 3)
424 persisted.write('reminders', {
425 ...persisted.get('reminders'),
426 lastEmailConfirm: lastEmailConfirm.toISOString(),
427 })
428 Toast.show(_(msg`You probably want to restart the app now.`))
429 }
430
431 const onPressActySubsUnNudge = () => {
432 setActyNotifNudged(false)
433 }
434
435 const onPressApplyOta = () => {
436 Alert.prompt(
437 'Apply OTA',
438 'Enter the channel for the OTA you wish to apply.',
439 [
440 {
441 style: 'cancel',
442 text: 'Cancel',
443 },
444 {
445 style: 'default',
446 text: 'Apply',
447 onPress: (channel?: string) => {
448 tryApplyUpdate(channel ?? '')
449 },
450 },
451 ],
452 'plain-text',
453 isCurrentlyRunningPullRequestDeployment
454 ? currentChannel
455 : 'pull-request-',
456 )
457 }
458
459 return (
460 <>
461 <SettingsList.PressableItem
462 onPress={() => navigation.navigate('Log')}
463 label={_(msg`Open system log`)}>
464 <SettingsList.ItemText>
465 <Trans>System log</Trans>
466 </SettingsList.ItemText>
467 </SettingsList.PressableItem>
468 <SettingsList.PressableItem
469 onPress={() => navigation.navigate('Debug')}
470 label={_(msg`Open storybook page`)}>
471 <SettingsList.ItemText>
472 <Trans>Storybook</Trans>
473 </SettingsList.ItemText>
474 </SettingsList.PressableItem>
475 <SettingsList.PressableItem
476 onPress={() => navigation.navigate('DebugMod')}
477 label={_(msg`Open moderation debug page`)}>
478 <SettingsList.ItemText>
479 <Trans>Debug Moderation</Trans>
480 </SettingsList.ItemText>
481 </SettingsList.PressableItem>
482 <SettingsList.PressableItem
483 onPress={() => deleteChatDeclarationRecord()}
484 label={_(msg`Open storybook page`)}>
485 <SettingsList.ItemText>
486 <Trans>Delete chat declaration record</Trans>
487 </SettingsList.ItemText>
488 </SettingsList.PressableItem>
489 <SettingsList.PressableItem
490 onPress={() => resetOnboarding()}
491 label={_(msg`Reset onboarding state`)}>
492 <SettingsList.ItemText>
493 <Trans>Reset onboarding state</Trans>
494 </SettingsList.ItemText>
495 </SettingsList.PressableItem>
496 <SettingsList.PressableItem
497 onPress={onPressUnsnoozeReminder}
498 label={_(msg`Unsnooze email reminder`)}>
499 <SettingsList.ItemText>
500 <Trans>Unsnooze email reminder</Trans>
501 </SettingsList.ItemText>
502 </SettingsList.PressableItem>
503 {actyNotifNudged && (
504 <SettingsList.PressableItem
505 onPress={onPressActySubsUnNudge}
506 label={_(msg`Reset activity subscription nudge`)}>
507 <SettingsList.ItemText>
508 <Trans>Reset activity subscription nudge</Trans>
509 </SettingsList.ItemText>
510 </SettingsList.PressableItem>
511 )}
512 <SettingsList.PressableItem
513 onPress={() => clearAllStorage()}
514 label={_(msg`Clear all storage data`)}>
515 <SettingsList.ItemText>
516 <Trans>Clear all storage data (restart after this)</Trans>
517 </SettingsList.ItemText>
518 </SettingsList.PressableItem>
519 {IS_IOS ? (
520 <SettingsList.PressableItem
521 onPress={onPressApplyOta}
522 label={_(msg`Apply Pull Request`)}>
523 <SettingsList.ItemText>
524 <Trans>Apply Pull Request</Trans>
525 </SettingsList.ItemText>
526 </SettingsList.PressableItem>
527 ) : null}
528 {IS_NATIVE && isCurrentlyRunningPullRequestDeployment ? (
529 <SettingsList.PressableItem
530 onPress={revertToEmbedded}
531 label={_(msg`Unapply Pull Request`)}>
532 <SettingsList.ItemText>
533 <Trans>Unapply Pull Request {currentChannel}</Trans>
534 </SettingsList.ItemText>
535 </SettingsList.PressableItem>
536 ) : null}
537
538 <SettingsList.Divider />
539 <View style={[a.p_xl, a.gap_md]}>
540 <Text style={[a.text_lg, a.font_semi_bold]}>
541 PolicyUpdate202508 Debug
542 </Text>
543
544 <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_md]}>
545 <Button
546 onPress={() => {
547 setOverride(!override)
548 }}
549 label="Toggle"
550 color={override ? 'primary' : 'secondary'}
551 size="small"
552 style={[a.flex_1]}>
553 <ButtonText>
554 {override ? 'Disable debug mode' : 'Enable debug mode'}
555 </ButtonText>
556 </Button>
557
558 <Button
559 onPress={() => {
560 device.set([PolicyUpdate202508], false)
561 agent.bskyAppRemoveNuxs([PolicyUpdate202508])
562 Toast.show(`Done`, 'info')
563 }}
564 label="Reset policy update nux"
565 color="secondary"
566 size="small"
567 disabled={!override}>
568 <ButtonText>Reset state</ButtonText>
569 </Button>
570 </View>
571 </View>
572 <SettingsList.Divider />
573 </>
574 )
575}
576
577function AddAccountRow() {
578 const {_} = useLingui()
579 const {setShowLoggedOut} = useLoggedOutViewControls()
580 const closeEverything = useCloseAllActiveElements()
581
582 const onAddAnotherAccount = () => {
583 setShowLoggedOut(true)
584 closeEverything()
585 }
586
587 return (
588 <SettingsList.PressableItem
589 onPress={onAddAnotherAccount}
590 label={_(msg`Add another account`)}>
591 <SettingsList.ItemIcon icon={PersonPlusIcon} />
592 <SettingsList.ItemText>
593 <Trans>Add another account</Trans>
594 </SettingsList.ItemText>
595 </SettingsList.PressableItem>
596 )
597}
598
599function AccountRow({
600 profile,
601 account,
602 pendingDid,
603 onPressSwitchAccount,
604}: {
605 profile?: AppBskyActorDefs.ProfileViewDetailed
606 account: SessionAccount
607 pendingDid: string | null
608 onPressSwitchAccount: (
609 account: SessionAccount,
610 logContext: 'Settings',
611 ) => void
612}) {
613 const {_} = useLingui()
614 const t = useTheme()
615
616 const moderationOpts = useModerationOpts()
617 const removePromptControl = Prompt.usePromptControl()
618 const {removeAccount} = useSessionApi()
619 const {isActive: live} = useActorStatus(profile)
620
621 const enableSquareButtons = useEnableSquareButtons()
622
623 const onSwitchAccount = () => {
624 if (pendingDid) return
625 onPressSwitchAccount(account, 'Settings')
626 }
627
628 return (
629 <View style={[a.relative]}>
630 <SettingsList.PressableItem
631 onPress={onSwitchAccount}
632 label={_(msg`Switch account`)}>
633 {moderationOpts && profile ? (
634 <UserAvatar
635 size={28}
636 avatar={profile.avatar}
637 moderation={moderateProfile(profile, moderationOpts).ui('avatar')}
638 type={profile.associated?.labeler ? 'labeler' : 'user'}
639 live={live}
640 hideLiveBadge
641 />
642 ) : (
643 <View style={[{width: 28}]} />
644 )}
645 <SettingsList.ItemText
646 numberOfLines={1}
647 style={[a.pr_2xl, a.leading_snug]}>
648 {sanitizeHandle(account.handle, '@')}
649 </SettingsList.ItemText>
650 {pendingDid === account.did && <SettingsList.ItemIcon icon={Loader} />}
651 </SettingsList.PressableItem>
652 {!pendingDid && (
653 <Menu.Root>
654 <Menu.Trigger label={_(msg`Account options`)}>
655 {({props, state}) => (
656 <Pressable
657 {...props}
658 style={[
659 a.absolute,
660 {top: 10, right: tokens.space.lg},
661 a.p_xs,
662 enableSquareButtons ? a.rounded_sm : a.rounded_full,
663 (state.hovered || state.pressed) && t.atoms.bg_contrast_25,
664 ]}>
665 <DotsHorizontal size="md" style={t.atoms.text} />
666 </Pressable>
667 )}
668 </Menu.Trigger>
669 <Menu.Outer showCancel>
670 <Menu.Item
671 label={_(msg`Remove account`)}
672 onPress={() => removePromptControl.open()}>
673 <Menu.ItemText>
674 <Trans>Remove account</Trans>
675 </Menu.ItemText>
676 <Menu.ItemIcon icon={PersonXIcon} />
677 </Menu.Item>
678 </Menu.Outer>
679 </Menu.Root>
680 )}
681
682 <Prompt.Basic
683 control={removePromptControl}
684 title={_(msg`Remove from quick access?`)}
685 description={_(
686 msg`This will remove @${account.handle} from the quick access list.`,
687 )}
688 onConfirm={() => {
689 removeAccount(account)
690 Toast.show(_(msg`Account removed from quick access`))
691 }}
692 confirmButtonCta={_(msg`Remove`)}
693 confirmButtonColor="negative"
694 />
695 </View>
696 )
697}