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

Configure Feed

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

at c540dae4e7db67031ee5f67feb076927999e364d 689 lines 25 kB view raw
1import {useState} from 'react' 2import {Alert, LayoutAnimation, Pressable, View} from 'react-native' 3import {Linking} from 'react-native' 4import {useReducedMotion} from 'react-native-reanimated' 5import {type AppBskyActorDefs, moderateProfile} from '@atproto/api' 6import {msg, Trans} from '@lingui/macro' 7import {useLingui} from '@lingui/react' 8import {useNavigation} from '@react-navigation/native' 9import {type NativeStackScreenProps} from '@react-navigation/native-stack' 10 11import {useActorStatus} from '#/lib/actor-status' 12import {HELP_DESK_URL} from '#/lib/constants' 13import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 14import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates' 15import { 16 type CommonNavigatorParams, 17 type NavigationProp, 18} from '#/lib/routes/types' 19import {useGate} from '#/lib/statsig/statsig' 20import {sanitizeDisplayName} from '#/lib/strings/display-names' 21import {sanitizeHandle} from '#/lib/strings/handles' 22import {isIOS, isNative} from '#/platform/detection' 23import {useProfileShadow} from '#/state/cache/profile-shadow' 24import * as persisted from '#/state/persisted' 25import {clearStorage} from '#/state/persisted' 26import {useModerationOpts} from '#/state/preferences/moderation-opts' 27import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration' 28import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile' 29import {useAgent} from '#/state/session' 30import {type SessionAccount, useSession, useSessionApi} from '#/state/session' 31import {useOnboardingDispatch} from '#/state/shell' 32import {useLoggedOutViewControls} from '#/state/shell/logged-out' 33import {useCloseAllActiveElements} from '#/state/util' 34import * as Toast from '#/view/com/util/Toast' 35import {UserAvatar} from '#/view/com/util/UserAvatar' 36import * as SettingsList from '#/screens/Settings/components/SettingsList' 37import {atoms as a, platform, tokens, useBreakpoints, useTheme} from '#/alf' 38import {AgeAssuranceDismissibleNotice} from '#/components/ageAssurance/AgeAssuranceDismissibleNotice' 39import {AvatarStackWithFetch} from '#/components/AvatarStack' 40import {Button, ButtonText} from '#/components/Button' 41import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from '#/components/contacts/country-allowlist' 42import {useDialogControl} from '#/components/Dialog' 43import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' 44import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' 45import {Bell_Stroke2_Corner0_Rounded as NotificationIcon} from '#/components/icons/Bell' 46import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' 47import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron' 48import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion' 49import {CodeBrackets_Stroke2_Corner2_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets' 50import {Contacts_Stroke2_Corner2_Rounded as ContactsIcon} from '#/components/icons/Contacts' 51import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' 52import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' 53import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock' 54import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' 55import { 56 Person_Stroke2_Corner2_Rounded as PersonIcon, 57 PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon, 58 PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon, 59 PersonX_Stroke2_Corner0_Rounded as PersonXIcon, 60} from '#/components/icons/Person' 61import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand' 62import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' 63import * as Layout from '#/components/Layout' 64import {Loader} from '#/components/Loader' 65import * as Menu from '#/components/Menu' 66import {ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config' 67import * as Prompt from '#/components/Prompt' 68import {Text} from '#/components/Typography' 69import {useFullVerificationState} from '#/components/verification' 70import { 71 shouldShowVerificationCheckButton, 72 VerificationCheckButton, 73} from '#/components/verification/VerificationCheckButton' 74import {IS_INTERNAL} 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 {_} = useLingui() 81 const reducedMotion = useReducedMotion() 82 const {logoutEveryAccount} = useSessionApi() 83 const {accounts, currentAccount} = useSession() 84 const switchAccountControl = useDialogControl() 85 const signOutPromptControl = Prompt.usePromptControl() 86 const {data: profile} = useProfileQuery({did: currentAccount?.did}) 87 const {data: otherProfiles} = useProfilesQuery({ 88 handles: accounts 89 .filter(acc => acc.did !== currentAccount?.did) 90 .map(acc => acc.handle), 91 }) 92 const {pendingDid, onPressSwitchAccount} = useAccountSwitcher() 93 const [showAccounts, setShowAccounts] = useState(false) 94 const [showDevOptions, setShowDevOptions] = useState(false) 95 const findContactsEnabled = 96 useIsFindContactsFeatureEnabledBasedOnGeolocation() 97 const gate = useGate() 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 {isNative && 217 findContactsEnabled && 218 !gate('disable_settings_find_contacts') && ( 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 237 to="/settings/accessibility" 238 label={_(msg`Accessibility`)}> 239 <SettingsList.ItemIcon icon={AccessibilityIcon} /> 240 <SettingsList.ItemText> 241 <Trans>Accessibility</Trans> 242 </SettingsList.ItemText> 243 </SettingsList.LinkItem> 244 <SettingsList.LinkItem 245 to="/settings/language" 246 label={_(msg`Languages`)}> 247 <SettingsList.ItemIcon icon={EarthIcon} /> 248 <SettingsList.ItemText> 249 <Trans>Languages</Trans> 250 </SettingsList.ItemText> 251 </SettingsList.LinkItem> 252 <SettingsList.PressableItem 253 onPress={() => Linking.openURL(HELP_DESK_URL)} 254 label={_(msg`Help`)} 255 accessibilityHint={_(msg`Opens helpdesk in browser`)}> 256 <SettingsList.ItemIcon icon={CircleQuestionIcon} /> 257 <SettingsList.ItemText> 258 <Trans>Help</Trans> 259 </SettingsList.ItemText> 260 <SettingsList.Chevron /> 261 </SettingsList.PressableItem> 262 <SettingsList.LinkItem to="/settings/about" label={_(msg`About`)}> 263 <SettingsList.ItemIcon icon={BubbleInfoIcon} /> 264 <SettingsList.ItemText> 265 <Trans>About</Trans> 266 </SettingsList.ItemText> 267 </SettingsList.LinkItem> 268 <SettingsList.Divider /> 269 <SettingsList.PressableItem 270 destructive 271 onPress={() => signOutPromptControl.open()} 272 label={_(msg`Sign out`)}> 273 <SettingsList.ItemText> 274 <Trans>Sign out</Trans> 275 </SettingsList.ItemText> 276 </SettingsList.PressableItem> 277 {IS_INTERNAL && ( 278 <> 279 <SettingsList.Divider /> 280 <SettingsList.PressableItem 281 onPress={() => { 282 if (!reducedMotion) { 283 LayoutAnimation.configureNext( 284 LayoutAnimation.Presets.easeInEaseOut, 285 ) 286 } 287 setShowDevOptions(d => !d) 288 }} 289 label={_(msg`Developer options`)}> 290 <SettingsList.ItemIcon icon={CodeBracketsIcon} /> 291 <SettingsList.ItemText> 292 <Trans>Developer options</Trans> 293 </SettingsList.ItemText> 294 </SettingsList.PressableItem> 295 {showDevOptions && <DevOptions />} 296 </> 297 )} 298 </SettingsList.Container> 299 </Layout.Content> 300 301 <Prompt.Basic 302 control={signOutPromptControl} 303 title={_(msg`Sign out?`)} 304 description={_(msg`You will be signed out of all your accounts.`)} 305 onConfirm={() => logoutEveryAccount('Settings')} 306 confirmButtonCta={_(msg`Sign out`)} 307 cancelButtonCta={_(msg`Cancel`)} 308 confirmButtonColor="negative" 309 /> 310 311 <SwitchAccountDialog control={switchAccountControl} /> 312 </Layout.Screen> 313 ) 314} 315 316function ProfilePreview({ 317 profile, 318}: { 319 profile: AppBskyActorDefs.ProfileViewDetailed 320}) { 321 const t = useTheme() 322 const {gtMobile} = useBreakpoints() 323 const shadow = useProfileShadow(profile) 324 const moderationOpts = useModerationOpts() 325 const verificationState = useFullVerificationState({ 326 profile: shadow, 327 }) 328 const {isActive: live} = useActorStatus(profile) 329 330 if (!moderationOpts) return null 331 332 const moderation = moderateProfile(profile, moderationOpts) 333 const displayName = sanitizeDisplayName( 334 profile.displayName || sanitizeHandle(profile.handle), 335 moderation.ui('displayName'), 336 ) 337 338 return ( 339 <> 340 <UserAvatar 341 size={80} 342 avatar={shadow.avatar} 343 moderation={moderation.ui('avatar')} 344 type={shadow.associated?.labeler ? 'labeler' : 'user'} 345 live={live} 346 /> 347 348 <View 349 style={[ 350 a.flex_row, 351 a.gap_xs, 352 a.align_center, 353 a.justify_center, 354 a.w_full, 355 ]}> 356 <Text 357 emoji 358 testID="profileHeaderDisplayName" 359 numberOfLines={1} 360 style={[ 361 a.pt_sm, 362 t.atoms.text, 363 gtMobile ? a.text_4xl : a.text_3xl, 364 a.font_bold, 365 ]}> 366 {displayName} 367 </Text> 368 {shouldShowVerificationCheckButton(verificationState) && ( 369 <View 370 style={[ 371 { 372 marginTop: platform({web: 8, ios: 8, android: 10}), 373 }, 374 ]}> 375 <VerificationCheckButton profile={shadow} size="lg" /> 376 </View> 377 )} 378 </View> 379 <Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}> 380 {sanitizeHandle(profile.handle, '@')} 381 </Text> 382 </> 383 ) 384} 385 386function DevOptions() { 387 const {_} = useLingui() 388 const agent = useAgent() 389 const [override, setOverride] = useStorage(device, [ 390 'policyUpdateDebugOverride', 391 ]) 392 const onboardingDispatch = useOnboardingDispatch() 393 const navigation = useNavigation<NavigationProp>() 394 const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration() 395 const { 396 tryApplyUpdate, 397 revertToEmbedded, 398 isCurrentlyRunningPullRequestDeployment, 399 currentChannel, 400 } = useApplyPullRequestOTAUpdate() 401 const [actyNotifNudged, setActyNotifNudged] = useActivitySubscriptionsNudged() 402 403 const resetOnboarding = async () => { 404 navigation.navigate('Home') 405 onboardingDispatch({type: 'start'}) 406 Toast.show(_(msg`Onboarding reset`)) 407 } 408 409 const clearAllStorage = async () => { 410 await clearStorage() 411 Toast.show(_(msg`Storage cleared, you need to restart the app now.`)) 412 } 413 414 const onPressUnsnoozeReminder = () => { 415 const lastEmailConfirm = new Date() 416 // wind back 3 days 417 lastEmailConfirm.setDate(lastEmailConfirm.getDate() - 3) 418 persisted.write('reminders', { 419 ...persisted.get('reminders'), 420 lastEmailConfirm: lastEmailConfirm.toISOString(), 421 }) 422 Toast.show(_(msg`You probably want to restart the app now.`)) 423 } 424 425 const onPressActySubsUnNudge = () => { 426 setActyNotifNudged(false) 427 } 428 429 const onPressApplyOta = () => { 430 Alert.prompt( 431 'Apply OTA', 432 'Enter the channel for the OTA you wish to apply.', 433 [ 434 { 435 style: 'cancel', 436 text: 'Cancel', 437 }, 438 { 439 style: 'default', 440 text: 'Apply', 441 onPress: (channel?: string) => { 442 tryApplyUpdate(channel ?? '') 443 }, 444 }, 445 ], 446 'plain-text', 447 isCurrentlyRunningPullRequestDeployment 448 ? currentChannel 449 : 'pull-request-', 450 ) 451 } 452 453 return ( 454 <> 455 <SettingsList.PressableItem 456 onPress={() => navigation.navigate('Log')} 457 label={_(msg`Open system log`)}> 458 <SettingsList.ItemText> 459 <Trans>System log</Trans> 460 </SettingsList.ItemText> 461 </SettingsList.PressableItem> 462 <SettingsList.PressableItem 463 onPress={() => navigation.navigate('Debug')} 464 label={_(msg`Open storybook page`)}> 465 <SettingsList.ItemText> 466 <Trans>Storybook</Trans> 467 </SettingsList.ItemText> 468 </SettingsList.PressableItem> 469 <SettingsList.PressableItem 470 onPress={() => navigation.navigate('DebugMod')} 471 label={_(msg`Open moderation debug page`)}> 472 <SettingsList.ItemText> 473 <Trans>Debug Moderation</Trans> 474 </SettingsList.ItemText> 475 </SettingsList.PressableItem> 476 <SettingsList.PressableItem 477 onPress={() => deleteChatDeclarationRecord()} 478 label={_(msg`Open storybook page`)}> 479 <SettingsList.ItemText> 480 <Trans>Delete chat declaration record</Trans> 481 </SettingsList.ItemText> 482 </SettingsList.PressableItem> 483 <SettingsList.PressableItem 484 onPress={() => resetOnboarding()} 485 label={_(msg`Reset onboarding state`)}> 486 <SettingsList.ItemText> 487 <Trans>Reset onboarding state</Trans> 488 </SettingsList.ItemText> 489 </SettingsList.PressableItem> 490 <SettingsList.PressableItem 491 onPress={onPressUnsnoozeReminder} 492 label={_(msg`Unsnooze email reminder`)}> 493 <SettingsList.ItemText> 494 <Trans>Unsnooze email reminder</Trans> 495 </SettingsList.ItemText> 496 </SettingsList.PressableItem> 497 {actyNotifNudged && ( 498 <SettingsList.PressableItem 499 onPress={onPressActySubsUnNudge} 500 label={_(msg`Reset activity subscription nudge`)}> 501 <SettingsList.ItemText> 502 <Trans>Reset activity subscription nudge</Trans> 503 </SettingsList.ItemText> 504 </SettingsList.PressableItem> 505 )} 506 <SettingsList.PressableItem 507 onPress={() => clearAllStorage()} 508 label={_(msg`Clear all storage data`)}> 509 <SettingsList.ItemText> 510 <Trans>Clear all storage data (restart after this)</Trans> 511 </SettingsList.ItemText> 512 </SettingsList.PressableItem> 513 {isIOS ? ( 514 <SettingsList.PressableItem 515 onPress={onPressApplyOta} 516 label={_(msg`Apply Pull Request`)}> 517 <SettingsList.ItemText> 518 <Trans>Apply Pull Request</Trans> 519 </SettingsList.ItemText> 520 </SettingsList.PressableItem> 521 ) : null} 522 {isNative && isCurrentlyRunningPullRequestDeployment ? ( 523 <SettingsList.PressableItem 524 onPress={revertToEmbedded} 525 label={_(msg`Unapply Pull Request`)}> 526 <SettingsList.ItemText> 527 <Trans>Unapply Pull Request {currentChannel}</Trans> 528 </SettingsList.ItemText> 529 </SettingsList.PressableItem> 530 ) : null} 531 532 <SettingsList.Divider /> 533 <View style={[a.p_xl, a.gap_md]}> 534 <Text style={[a.text_lg, a.font_semi_bold]}> 535 PolicyUpdate202508 Debug 536 </Text> 537 538 <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_md]}> 539 <Button 540 onPress={() => { 541 setOverride(!override) 542 }} 543 label="Toggle" 544 color={override ? 'primary' : 'secondary'} 545 size="small" 546 style={[a.flex_1]}> 547 <ButtonText> 548 {override ? 'Disable debug mode' : 'Enable debug mode'} 549 </ButtonText> 550 </Button> 551 552 <Button 553 onPress={() => { 554 device.set([PolicyUpdate202508], false) 555 agent.bskyAppRemoveNuxs([PolicyUpdate202508]) 556 Toast.show(`Done`, 'info') 557 }} 558 label="Reset policy update nux" 559 color="secondary" 560 size="small" 561 disabled={!override}> 562 <ButtonText>Reset state</ButtonText> 563 </Button> 564 </View> 565 </View> 566 <SettingsList.Divider /> 567 </> 568 ) 569} 570 571function AddAccountRow() { 572 const {_} = useLingui() 573 const {setShowLoggedOut} = useLoggedOutViewControls() 574 const closeEverything = useCloseAllActiveElements() 575 576 const onAddAnotherAccount = () => { 577 setShowLoggedOut(true) 578 closeEverything() 579 } 580 581 return ( 582 <SettingsList.PressableItem 583 onPress={onAddAnotherAccount} 584 label={_(msg`Add another account`)}> 585 <SettingsList.ItemIcon icon={PersonPlusIcon} /> 586 <SettingsList.ItemText> 587 <Trans>Add another account</Trans> 588 </SettingsList.ItemText> 589 </SettingsList.PressableItem> 590 ) 591} 592 593function AccountRow({ 594 profile, 595 account, 596 pendingDid, 597 onPressSwitchAccount, 598}: { 599 profile?: AppBskyActorDefs.ProfileViewDetailed 600 account: SessionAccount 601 pendingDid: string | null 602 onPressSwitchAccount: ( 603 account: SessionAccount, 604 logContext: 'Settings', 605 ) => void 606}) { 607 const {_} = useLingui() 608 const t = useTheme() 609 610 const moderationOpts = useModerationOpts() 611 const removePromptControl = Prompt.usePromptControl() 612 const {removeAccount} = useSessionApi() 613 const {isActive: live} = useActorStatus(profile) 614 615 const onSwitchAccount = () => { 616 if (pendingDid) return 617 onPressSwitchAccount(account, 'Settings') 618 } 619 620 return ( 621 <View style={[a.relative]}> 622 <SettingsList.PressableItem 623 onPress={onSwitchAccount} 624 label={_(msg`Switch account`)}> 625 {moderationOpts && profile ? ( 626 <UserAvatar 627 size={28} 628 avatar={profile.avatar} 629 moderation={moderateProfile(profile, moderationOpts).ui('avatar')} 630 type={profile.associated?.labeler ? 'labeler' : 'user'} 631 live={live} 632 hideLiveBadge 633 /> 634 ) : ( 635 <View style={[{width: 28}]} /> 636 )} 637 <SettingsList.ItemText 638 numberOfLines={1} 639 style={[a.pr_2xl, a.leading_snug]}> 640 {sanitizeHandle(account.handle, '@')} 641 </SettingsList.ItemText> 642 {pendingDid === account.did && <SettingsList.ItemIcon icon={Loader} />} 643 </SettingsList.PressableItem> 644 {!pendingDid && ( 645 <Menu.Root> 646 <Menu.Trigger label={_(msg`Account options`)}> 647 {({props, state}) => ( 648 <Pressable 649 {...props} 650 style={[ 651 a.absolute, 652 {top: 10, right: tokens.space.lg}, 653 a.p_xs, 654 a.rounded_full, 655 (state.hovered || state.pressed) && t.atoms.bg_contrast_25, 656 ]}> 657 <DotsHorizontal size="md" style={t.atoms.text} /> 658 </Pressable> 659 )} 660 </Menu.Trigger> 661 <Menu.Outer showCancel> 662 <Menu.Item 663 label={_(msg`Remove account`)} 664 onPress={() => removePromptControl.open()}> 665 <Menu.ItemText> 666 <Trans>Remove account</Trans> 667 </Menu.ItemText> 668 <Menu.ItemIcon icon={PersonXIcon} /> 669 </Menu.Item> 670 </Menu.Outer> 671 </Menu.Root> 672 )} 673 674 <Prompt.Basic 675 control={removePromptControl} 676 title={_(msg`Remove from quick access?`)} 677 description={_( 678 msg`This will remove @${account.handle} from the quick access list.`, 679 )} 680 onConfirm={() => { 681 removeAccount(account) 682 Toast.show(_(msg`Account removed from quick access`)) 683 }} 684 confirmButtonCta={_(msg`Remove`)} 685 confirmButtonColor="negative" 686 /> 687 </View> 688 ) 689}