Bluesky app fork with some witchin' additions 馃挮
at linkat-integration 697 lines 26 kB view raw
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}