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 699 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 {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}