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

Configure Feed

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

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