Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 695 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} 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}