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 962 lines 36 kB view raw
1import {useState} from 'react' 2import {Alert, LayoutAnimation, Pressable, View} from 'react-native' 3import type Animated from 'react-native-reanimated' 4import { 5 useAnimatedRef, 6 useReducedMotion, 7 useScrollViewOffset, 8} from 'react-native-reanimated' 9import {setStringAsync} from 'expo-clipboard' 10import {type AppBskyActorDefs, moderateProfile} from '@atproto/api' 11import {Trans, useLingui} from '@lingui/react/macro' 12import {useNavigation} from '@react-navigation/native' 13import {type NativeStackScreenProps} from '@react-navigation/native-stack' 14 15import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 16import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates' 17import { 18 type CommonNavigatorParams, 19 type NavigationProp, 20} from '#/lib/routes/types' 21import {sanitizeDisplayName} from '#/lib/strings/display-names' 22import {sanitizeHandle} from '#/lib/strings/handles' 23import {useProfileShadow} from '#/state/cache/profile-shadow' 24import * as persisted from '#/state/persisted' 25import {clearStorage} from '#/state/persisted' 26import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 27import {useModerationOpts} from '#/state/preferences/moderation-opts' 28import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration' 29import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile' 30import {useAgent} from '#/state/session' 31import {type SessionAccount, useSession, useSessionApi} from '#/state/session' 32import {pdsAgent} from '#/state/session/agent' 33import { 34 type AccountSortOption, 35 sortAccountItems, 36 useAccountSwitcherSortSettings, 37} from '#/state/session/sorting' 38import {useOnboardingDispatch} from '#/state/shell' 39import {useLoggedOutViewControls} from '#/state/shell/logged-out' 40import {useCloseAllActiveElements} from '#/state/util' 41import {UserAvatar} from '#/view/com/util/UserAvatar' 42import * as SettingsList from '#/screens/Settings/components/SettingsList' 43import {atoms as a, platform, tokens, useBreakpoints, useTheme} from '#/alf' 44import {AgeAssuranceDismissibleNotice} from '#/components/ageAssurance/AgeAssuranceDismissibleNotice' 45import {AvatarStackWithFetch} from '#/components/AvatarStack' 46import {Button, ButtonIcon, ButtonText} from '#/components/Button' 47import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from '#/components/contacts/country-allowlist' 48import {useDialogControl} from '#/components/Dialog' 49import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' 50import {SortableList} from '#/components/DraggableList' 51import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' 52import {ArrowRotateClockwise_Stroke2_Corner0_Rounded as ReverseIcon} from '#/components/icons/ArrowRotate' 53import {Bell_Stroke2_Corner0_Rounded as NotificationIcon} from '#/components/icons/Bell' 54import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' 55import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron' 56import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard' 57import {CodeBrackets_Stroke2_Corner2_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets' 58import {Contacts_Stroke2_Corner2_Rounded as ContactsIcon} from '#/components/icons/Contacts' 59import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' 60import {Eclipse_Stroke2_Corner0_Rounded as EclipseIcon} from '#/components/icons/Eclipse' 61import {Filter_Stroke2_Corner0_Rounded as SortIcon} from '#/components/icons/Filter' 62import {FloppyDisk_Stroke2_Corner0_Rounded as SaveIcon} from '#/components/icons/FloppyDisk' 63import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' 64import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock' 65import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' 66import { 67 Person_Stroke2_Corner2_Rounded as PersonIcon, 68 PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon, 69 PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon, 70 PersonX_Stroke2_Corner0_Rounded as PersonXIcon, 71} from '#/components/icons/Person' 72import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand' 73import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' 74import * as Layout from '#/components/Layout' 75import {Loader} from '#/components/Loader' 76import * as Menu from '#/components/Menu' 77import {ID as PolicyUpdate202508} from '#/components/PolicyUpdateOverlay/updates/202508/config' 78import {ProfileBadges} from '#/components/ProfileBadges' 79import * as Prompt from '#/components/Prompt' 80import * as Toast from '#/components/Toast' 81import {Text} from '#/components/Typography' 82import {useAnalytics} from '#/analytics' 83import {IS_INTERNAL, IS_IOS, IS_NATIVE} from '#/env' 84import {useActorStatus} from '#/features/liveNow' 85import {device, useStorage} from '#/storage' 86import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged' 87import {useDevMode} from '#/storage/hooks/dev-mode' 88import {useHiddenAccountsElsewhere} from '#/storage/hooks/hidden-accounts-elsewhere' 89 90type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> 91type AccountListItem = { 92 account: SessionAccount 93 profile?: AppBskyActorDefs.ProfileViewDetailed 94} 95 96export function SettingsScreen({}: Props) { 97 const ax = useAnalytics() 98 const {t: l} = useLingui() 99 const t = useTheme() 100 const reducedMotion = useReducedMotion() 101 const scrollRef = useAnimatedRef<Animated.ScrollView>() 102 const scrollOffset = useScrollViewOffset(scrollRef) 103 const {logoutEveryAccount, reorderAccounts} = useSessionApi() 104 const {accounts, currentAccount} = useSession() 105 const switchAccountControl = useDialogControl() 106 const signOutPromptControl = Prompt.usePromptControl() 107 const {data: profile} = useProfileQuery({did: currentAccount?.did}) 108 const {data: otherProfiles} = useProfilesQuery({ 109 handles: accounts 110 .filter(acc => acc.did !== currentAccount?.did) 111 .map(acc => acc.did), 112 }) 113 const {pendingDid, onPressSwitchAccount} = useAccountSwitcher() 114 const enableSquareButtons = useEnableSquareButtons() 115 const [showAccounts, setShowAccounts] = useState(false) 116 const [isDraggingAccounts, setIsDraggingAccounts] = useState(false) 117 const [isCustomSortEditing, setIsCustomSortEditing] = useState(false) 118 const [customAccountsDraft, setCustomAccountsDraft] = useState< 119 AccountListItem[] 120 >([]) 121 const {sortBy, setSortBy, reverse, setReverse} = 122 useAccountSwitcherSortSettings() 123 const [, , hiddenDidsSet] = useHiddenAccountsElsewhere() 124 const [showDevOptions, setShowDevOptions] = useState(false) 125 const findContactsEnabled = 126 useIsFindContactsFeatureEnabledBasedOnGeolocation() 127 const allAccounts = accounts.map(account => ({ 128 account, 129 profile: 130 account.did === currentAccount?.did 131 ? profile 132 : otherProfiles?.profiles?.find(p => p.did === account.did), 133 })) 134 const otherAccounts = allAccounts.filter( 135 item => item.account.did !== currentAccount?.did, 136 ) 137 const displayedAccounts = isCustomSortEditing 138 ? customAccountsDraft 139 : sortAccountItems(otherAccounts, sortBy, reverse) 140 141 const onSelectAccountsSort = (nextSortBy: AccountSortOption) => { 142 if (nextSortBy === 'custom') { 143 setCustomAccountsDraft(sortAccountItems(allAccounts, sortBy, reverse)) 144 setIsCustomSortEditing(true) 145 return 146 } 147 setSortBy(nextSortBy) 148 setIsCustomSortEditing(false) 149 setCustomAccountsDraft([]) 150 } 151 152 const onToggleReverseAccounts = () => { 153 setReverse(!reverse) 154 if (isCustomSortEditing) { 155 setCustomAccountsDraft(prev => [...prev].reverse()) 156 } 157 } 158 159 const onCancelCustomSort = () => { 160 setIsCustomSortEditing(false) 161 setCustomAccountsDraft([]) 162 } 163 164 const onSaveCustomSort = () => { 165 const orderedAccounts = reverse 166 ? [...customAccountsDraft].reverse() 167 : customAccountsDraft 168 reorderAccounts(orderedAccounts.map(item => item.account)) 169 setSortBy('custom') 170 setIsCustomSortEditing(false) 171 setCustomAccountsDraft([]) 172 } 173 174 return ( 175 <Layout.Screen> 176 <Layout.Header.Outer> 177 <Layout.Header.BackButton /> 178 <Layout.Header.Content> 179 <Layout.Header.TitleText> 180 <Trans>Settings</Trans> 181 </Layout.Header.TitleText> 182 </Layout.Header.Content> 183 <Layout.Header.Slot /> 184 </Layout.Header.Outer> 185 <Layout.Content ref={scrollRef} scrollEnabled={!isDraggingAccounts}> 186 <SettingsList.Container> 187 <AgeAssuranceDismissibleNotice style={[a.px_lg, a.pt_xs, a.pb_xl]} /> 188 189 <View 190 style={[ 191 a.px_xl, 192 a.pt_md, 193 a.pb_md, 194 a.w_full, 195 a.gap_2xs, 196 a.align_center, 197 {minHeight: 160}, 198 ]}> 199 {profile && <ProfilePreview profile={profile} />} 200 </View> 201 {accounts.length > 1 ? ( 202 <> 203 <View style={[a.relative]}> 204 <SettingsList.PressableItem 205 label={l`Switch account`} 206 accessibilityHint={l`Shows other accounts you can switch to`} 207 onPress={() => { 208 if (!reducedMotion) { 209 LayoutAnimation.configureNext( 210 LayoutAnimation.Presets.easeInEaseOut, 211 ) 212 } 213 if (showAccounts) { 214 setIsCustomSortEditing(false) 215 setCustomAccountsDraft([]) 216 } 217 setShowAccounts(s => !s) 218 }}> 219 <SettingsList.ItemIcon icon={PersonGroupIcon} /> 220 <SettingsList.ItemText 221 style={[showAccounts && {paddingRight: 64}]}> 222 <Trans>Switch account</Trans> 223 </SettingsList.ItemText> 224 {showAccounts ? ( 225 <SettingsList.ItemIcon icon={ChevronUpIcon} size="md" /> 226 ) : ( 227 <AvatarStackWithFetch 228 profiles={sortAccountItems(otherAccounts, sortBy, reverse) 229 .filter(item => !hiddenDidsSet.has(item.account.did)) 230 .map(item => item.account.did) 231 .slice(0, 5)} 232 /> 233 )} 234 </SettingsList.PressableItem> 235 {showAccounts && ( 236 <Menu.Root> 237 <Menu.Trigger label={l`Sort accounts`}> 238 {({props, state}) => ( 239 <Pressable 240 {...props} 241 style={[ 242 a.absolute, 243 {top: 10, right: 48}, 244 a.p_xs, 245 enableSquareButtons ? a.rounded_sm : a.rounded_full, 246 (state.hovered || state.pressed) && 247 t.atoms.bg_contrast_25, 248 ]}> 249 <SortIcon size="md" style={t.atoms.text} /> 250 </Pressable> 251 )} 252 </Menu.Trigger> 253 <Menu.Outer showCancel> 254 <Menu.Group> 255 <Menu.LabelText> 256 <Trans>Sort accounts</Trans> 257 </Menu.LabelText> 258 <Menu.Item 259 label={l`Alphabetical`} 260 onPress={() => onSelectAccountsSort('alphabetical')}> 261 <Menu.ItemRadio 262 selected={ 263 (isCustomSortEditing ? 'custom' : sortBy) === 264 'alphabetical' 265 } 266 /> 267 <Menu.ItemText> 268 <Trans>Alphabetical</Trans> 269 </Menu.ItemText> 270 </Menu.Item> 271 <Menu.Item 272 label={l`By date modified`} 273 onPress={() => onSelectAccountsSort('dateModified')}> 274 <Menu.ItemRadio 275 selected={ 276 (isCustomSortEditing ? 'custom' : sortBy) === 277 'dateModified' 278 } 279 /> 280 <Menu.ItemText> 281 <Trans>By date modified</Trans> 282 </Menu.ItemText> 283 </Menu.Item> 284 <Menu.Item 285 label={l`By date added`} 286 onPress={() => onSelectAccountsSort('dateAdded')}> 287 <Menu.ItemRadio 288 selected={ 289 (isCustomSortEditing ? 'custom' : sortBy) === 290 'dateAdded' 291 } 292 /> 293 <Menu.ItemText> 294 <Trans>By date added</Trans> 295 </Menu.ItemText> 296 </Menu.Item> 297 <Menu.Item 298 label={l`Custom`} 299 onPress={() => onSelectAccountsSort('custom')}> 300 <Menu.ItemRadio 301 selected={ 302 (isCustomSortEditing ? 'custom' : sortBy) === 303 'custom' 304 } 305 /> 306 <Menu.ItemText> 307 <Trans>Custom</Trans> 308 </Menu.ItemText> 309 </Menu.Item> 310 </Menu.Group> 311 <Menu.Divider /> 312 <Menu.Item 313 label={l`Reverse order`} 314 onPress={onToggleReverseAccounts}> 315 <Menu.ItemRadio selected={reverse} /> 316 <Menu.ItemText> 317 <Trans>Reverse order</Trans> 318 </Menu.ItemText> 319 <Menu.ItemIcon icon={ReverseIcon} position="right" /> 320 </Menu.Item> 321 </Menu.Outer> 322 </Menu.Root> 323 )} 324 </View> 325 {showAccounts && ( 326 <> 327 <SettingsList.Divider /> 328 {isCustomSortEditing ? ( 329 <SortableList 330 data={customAccountsDraft} 331 keyExtractor={item => item.account.did} 332 itemHeight={48} 333 scrollRef={scrollRef} 334 scrollOffset={scrollOffset} 335 onDragStart={() => setIsDraggingAccounts(true)} 336 onDragEnd={() => setIsDraggingAccounts(false)} 337 onReorder={setCustomAccountsDraft} 338 renderItem={(item, dragHandle) => ( 339 <AccountRow 340 key={item.account.did} 341 account={item.account} 342 profile={item.profile} 343 pendingDid={pendingDid} 344 disableSwitching 345 dragHandle={dragHandle} 346 onPressSwitchAccount={(account, logContext) => 347 void onPressSwitchAccount(account, logContext) 348 } 349 /> 350 )} 351 /> 352 ) : ( 353 displayedAccounts.map(item => ( 354 <AccountRow 355 key={item.account.did} 356 account={item.account} 357 profile={item.profile} 358 pendingDid={pendingDid} 359 onPressSwitchAccount={(account, logContext) => 360 void onPressSwitchAccount(account, logContext) 361 } 362 /> 363 )) 364 )} 365 {isCustomSortEditing && ( 366 <View 367 style={[a.flex_row, a.gap_sm, a.px_xl, a.pt_md, a.pb_sm]}> 368 <Button 369 label={l`Cancel`} 370 onPress={onCancelCustomSort} 371 color="secondary" 372 size="small" 373 style={[a.flex_1]}> 374 <ButtonText> 375 <Trans>Cancel</Trans> 376 </ButtonText> 377 </Button> 378 <Button 379 label={l`Save changes`} 380 onPress={onSaveCustomSort} 381 color="primary" 382 size="small" 383 style={[a.flex_1]}> 384 <ButtonIcon icon={SaveIcon} /> 385 <ButtonText> 386 <Trans>Save changes</Trans> 387 </ButtonText> 388 </Button> 389 </View> 390 )} 391 <AddAccountRow /> 392 </> 393 )} 394 </> 395 ) : ( 396 <AddAccountRow /> 397 )} 398 <SettingsList.Divider /> 399 <SettingsList.LinkItem to="/settings/account" label={l`Account`}> 400 <SettingsList.ItemIcon icon={PersonIcon} /> 401 <SettingsList.ItemText> 402 <Trans>Account</Trans> 403 </SettingsList.ItemText> 404 </SettingsList.LinkItem> 405 <SettingsList.LinkItem 406 to="/settings/privacy-and-security" 407 label={l`Privacy and security`}> 408 <SettingsList.ItemIcon icon={LockIcon} /> 409 <SettingsList.ItemText> 410 <Trans>Privacy and security</Trans> 411 </SettingsList.ItemText> 412 </SettingsList.LinkItem> 413 <SettingsList.LinkItem to="/moderation" label={l`Moderation`}> 414 <SettingsList.ItemIcon icon={HandIcon} /> 415 <SettingsList.ItemText> 416 <Trans>Moderation and content filters</Trans> 417 </SettingsList.ItemText> 418 </SettingsList.LinkItem> 419 <SettingsList.LinkItem 420 to="/settings/notifications" 421 label={l`Notifications`}> 422 <SettingsList.ItemIcon icon={NotificationIcon} /> 423 <SettingsList.ItemText> 424 <Trans>Notifications</Trans> 425 </SettingsList.ItemText> 426 </SettingsList.LinkItem> 427 <SettingsList.LinkItem 428 to="/settings/content-and-media" 429 label={l`Content and media`}> 430 <SettingsList.ItemIcon icon={WindowIcon} /> 431 <SettingsList.ItemText> 432 <Trans>Content and media</Trans> 433 </SettingsList.ItemText> 434 </SettingsList.LinkItem> 435 {IS_NATIVE && 436 findContactsEnabled && 437 !ax.features.enabled(ax.features.ImportContactsSettingsDisable) && ( 438 <SettingsList.LinkItem 439 to="/settings/find-contacts" 440 label={l`Find friends from contacts`}> 441 <SettingsList.ItemIcon icon={ContactsIcon} /> 442 <SettingsList.ItemText> 443 <Trans>Find friends from contacts</Trans> 444 </SettingsList.ItemText> 445 </SettingsList.LinkItem> 446 )} 447 <SettingsList.LinkItem 448 to="/settings/appearance" 449 label={l`Appearance`}> 450 <SettingsList.ItemIcon icon={PaintRollerIcon} /> 451 <SettingsList.ItemText> 452 <Trans>Appearance</Trans> 453 </SettingsList.ItemText> 454 </SettingsList.LinkItem> 455 <SettingsList.LinkItem 456 to="/settings/accessibility" 457 label={l`Accessibility`}> 458 <SettingsList.ItemIcon icon={AccessibilityIcon} /> 459 <SettingsList.ItemText> 460 <Trans>Accessibility</Trans> 461 </SettingsList.ItemText> 462 </SettingsList.LinkItem> 463 <SettingsList.LinkItem to="/settings/language" label={l`Languages`}> 464 <SettingsList.ItemIcon icon={EarthIcon} /> 465 <SettingsList.ItemText> 466 <Trans>Languages</Trans> 467 </SettingsList.ItemText> 468 </SettingsList.LinkItem> 469 <SettingsList.LinkItem to="/settings/runes" label={l`Runes`}> 470 <SettingsList.ItemIcon icon={EclipseIcon} /> 471 <SettingsList.ItemText> 472 <Trans>Runes</Trans> 473 </SettingsList.ItemText> 474 </SettingsList.LinkItem> 475 <SettingsList.LinkItem to="/settings/about" label={l`About`}> 476 <SettingsList.ItemIcon icon={BubbleInfoIcon} /> 477 <SettingsList.ItemText> 478 <Trans>About</Trans> 479 </SettingsList.ItemText> 480 </SettingsList.LinkItem> 481 <SettingsList.Divider /> 482 <SettingsList.PressableItem 483 destructive 484 onPress={() => signOutPromptControl.open()} 485 label={l`Sign out`}> 486 <SettingsList.ItemText> 487 <Trans>Sign out</Trans> 488 </SettingsList.ItemText> 489 </SettingsList.PressableItem> 490 {IS_INTERNAL && ( 491 <> 492 <SettingsList.Divider /> 493 <SettingsList.PressableItem 494 onPress={() => { 495 if (!reducedMotion) { 496 LayoutAnimation.configureNext( 497 LayoutAnimation.Presets.easeInEaseOut, 498 ) 499 } 500 setShowDevOptions(d => !d) 501 }} 502 label={l`Developer options`}> 503 <SettingsList.ItemIcon icon={CodeBracketsIcon} /> 504 <SettingsList.ItemText> 505 <Trans>Developer options</Trans> 506 </SettingsList.ItemText> 507 </SettingsList.PressableItem> 508 {showDevOptions && <DevOptions />} 509 </> 510 )} 511 </SettingsList.Container> 512 </Layout.Content> 513 514 <Prompt.Basic 515 control={signOutPromptControl} 516 title={l`Sign out?`} 517 description={l`You will be signed out of all your accounts.`} 518 onConfirm={() => logoutEveryAccount('Settings')} 519 confirmButtonCta={l`Sign out`} 520 cancelButtonCta={l`Cancel`} 521 confirmButtonColor="negative" 522 /> 523 524 <SwitchAccountDialog control={switchAccountControl} /> 525 </Layout.Screen> 526 ) 527} 528 529function ProfilePreview({ 530 profile, 531}: { 532 profile: AppBskyActorDefs.ProfileViewDetailed 533}) { 534 const t = useTheme() 535 const {gtMobile} = useBreakpoints() 536 const shadow = useProfileShadow(profile) 537 const moderationOpts = useModerationOpts() 538 const {isActive: live} = useActorStatus(profile) 539 540 if (!moderationOpts) return null 541 542 const moderation = moderateProfile(profile, moderationOpts) 543 const displayName = sanitizeDisplayName( 544 profile.displayName || sanitizeHandle(profile.handle), 545 moderation.ui('displayName'), 546 ) 547 548 return ( 549 <> 550 <UserAvatar 551 size={80} 552 avatar={shadow.avatar} 553 moderation={moderation.ui('avatar')} 554 type={shadow.associated?.labeler ? 'labeler' : 'user'} 555 live={live} 556 /> 557 558 <View 559 style={[ 560 a.flex_row, 561 a.gap_xs, 562 a.align_center, 563 a.justify_center, 564 a.w_full, 565 ]}> 566 <Text 567 emoji 568 testID="profileHeaderDisplayName" 569 numberOfLines={1} 570 style={[ 571 a.pt_sm, 572 t.atoms.text, 573 gtMobile ? a.text_4xl : a.text_3xl, 574 a.font_bold, 575 ]}> 576 {displayName} 577 </Text> 578 <ProfileBadges 579 profile={shadow} 580 size="xl" 581 interactive 582 style={[ 583 { 584 marginTop: platform({web: 8, ios: 8, android: 10}), 585 }, 586 ]} 587 /> 588 </View> 589 <Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}> 590 {sanitizeHandle(profile.handle, '@')} 591 </Text> 592 </> 593 ) 594} 595 596function DevOptions() { 597 const {t: l} = useLingui() 598 const agent = useAgent() 599 const [override, setOverride] = useStorage(device, [ 600 'policyUpdateDebugOverride', 601 ]) 602 const onboardingDispatch = useOnboardingDispatch() 603 const navigation = useNavigation<NavigationProp>() 604 const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration() 605 const { 606 tryApplyUpdate, 607 revertToEmbedded, 608 isCurrentlyRunningPullRequestDeployment, 609 currentChannel, 610 } = useApplyPullRequestOTAUpdate() 611 const [actyNotifNudged, setActyNotifNudged] = useActivitySubscriptionsNudged() 612 613 const resetOnboarding = () => { 614 navigation.navigate('Home') 615 onboardingDispatch({type: 'start'}) 616 Toast.show(l`Onboarding reset`) 617 } 618 619 const clearAllStorage = async () => { 620 await clearStorage() 621 Toast.show(l`Storage cleared, you need to restart the app now.`) 622 } 623 624 const onPressUnsnoozeReminder = () => { 625 const lastEmailConfirm = new Date() 626 // wind back 3 days 627 lastEmailConfirm.setDate(lastEmailConfirm.getDate() - 3) 628 void persisted.write('reminders', { 629 ...persisted.get('reminders'), 630 lastEmailConfirm: lastEmailConfirm.toISOString(), 631 }) 632 Toast.show(l`You probably want to restart the app now.`) 633 } 634 635 const onPressActySubsUnNudge = () => { 636 setActyNotifNudged(false) 637 } 638 639 const onPressApplyOta = () => { 640 Alert.prompt( 641 'Apply OTA', 642 'Enter the channel for the OTA you wish to apply.', 643 [ 644 { 645 style: 'cancel', 646 text: 'Cancel', 647 }, 648 { 649 style: 'default', 650 text: 'Apply', 651 onPress: (channel?: string) => { 652 void tryApplyUpdate(channel ?? '') 653 }, 654 }, 655 ], 656 'plain-text', 657 isCurrentlyRunningPullRequestDeployment 658 ? currentChannel 659 : 'pull-request-', 660 ) 661 } 662 663 return ( 664 <> 665 <SettingsList.PressableItem 666 onPress={() => navigation.navigate('Log')} 667 label={l`Open system log`}> 668 <SettingsList.ItemText> 669 <Trans>System log</Trans> 670 </SettingsList.ItemText> 671 </SettingsList.PressableItem> 672 <SettingsList.PressableItem 673 onPress={() => navigation.navigate('Debug')} 674 label={l`Open storybook page`}> 675 <SettingsList.ItemText> 676 <Trans>Storybook</Trans> 677 </SettingsList.ItemText> 678 </SettingsList.PressableItem> 679 <SettingsList.PressableItem 680 onPress={() => navigation.navigate('DebugMod')} 681 label={l`Open moderation debug page`}> 682 <SettingsList.ItemText> 683 <Trans>Debug Moderation</Trans> 684 </SettingsList.ItemText> 685 </SettingsList.PressableItem> 686 <SettingsList.PressableItem 687 onPress={() => deleteChatDeclarationRecord()} 688 label={l`Open storybook page`}> 689 <SettingsList.ItemText> 690 <Trans>Delete chat declaration record</Trans> 691 </SettingsList.ItemText> 692 </SettingsList.PressableItem> 693 <SettingsList.PressableItem 694 onPress={() => void resetOnboarding()} 695 label={l`Reset onboarding state`}> 696 <SettingsList.ItemText> 697 <Trans>Reset onboarding state</Trans> 698 </SettingsList.ItemText> 699 </SettingsList.PressableItem> 700 <SettingsList.PressableItem 701 onPress={onPressUnsnoozeReminder} 702 label={l`Unsnooze email reminder`}> 703 <SettingsList.ItemText> 704 <Trans>Unsnooze email reminder</Trans> 705 </SettingsList.ItemText> 706 </SettingsList.PressableItem> 707 {actyNotifNudged && ( 708 <SettingsList.PressableItem 709 onPress={onPressActySubsUnNudge} 710 label={l`Reset activity subscription nudge`}> 711 <SettingsList.ItemText> 712 <Trans>Reset activity subscription nudge</Trans> 713 </SettingsList.ItemText> 714 </SettingsList.PressableItem> 715 )} 716 <SettingsList.PressableItem 717 onPress={() => void clearAllStorage()} 718 label={l`Clear all storage data`}> 719 <SettingsList.ItemText> 720 <Trans>Clear all storage data (restart after this)</Trans> 721 </SettingsList.ItemText> 722 </SettingsList.PressableItem> 723 {IS_IOS ? ( 724 <SettingsList.PressableItem 725 onPress={onPressApplyOta} 726 label={l`Apply Pull Request`}> 727 <SettingsList.ItemText> 728 <Trans>Apply Pull Request</Trans> 729 </SettingsList.ItemText> 730 </SettingsList.PressableItem> 731 ) : null} 732 {IS_NATIVE && isCurrentlyRunningPullRequestDeployment ? ( 733 <SettingsList.PressableItem 734 onPress={() => void revertToEmbedded()} 735 label={l`Unapply Pull Request`}> 736 <SettingsList.ItemText> 737 <Trans>Unapply Pull Request {currentChannel}</Trans> 738 </SettingsList.ItemText> 739 </SettingsList.PressableItem> 740 ) : null} 741 <SettingsList.Divider /> 742 <View style={[a.p_xl, a.gap_md]}> 743 <Text style={[a.text_lg, a.font_semi_bold]}> 744 PolicyUpdate202508 Debug 745 </Text> 746 747 <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_md]}> 748 <Button 749 onPress={() => { 750 setOverride(!override) 751 }} 752 label="Toggle" 753 color={override ? 'primary' : 'secondary'} 754 size="small" 755 style={[a.flex_1]}> 756 <ButtonText> 757 {override ? 'Disable debug mode' : 'Enable debug mode'} 758 </ButtonText> 759 </Button> 760 761 <Button 762 onPress={() => { 763 device.set([PolicyUpdate202508], false) 764 void pdsAgent(agent).bskyAppRemoveNuxs([PolicyUpdate202508]) 765 Toast.show(`Done`, { 766 type: 'info', 767 }) 768 }} 769 label="Reset policy update nux" 770 color="secondary" 771 size="small" 772 disabled={!override}> 773 <ButtonText>Reset state</ButtonText> 774 </Button> 775 </View> 776 </View> 777 <SettingsList.Divider /> 778 </> 779 ) 780} 781 782function AddAccountRow() { 783 const {t: l} = useLingui() 784 const {setShowLoggedOut} = useLoggedOutViewControls() 785 const closeEverything = useCloseAllActiveElements() 786 787 const onAddAnotherAccount = () => { 788 setShowLoggedOut(true) 789 closeEverything() 790 } 791 792 return ( 793 <SettingsList.PressableItem 794 onPress={onAddAnotherAccount} 795 label={l`Add another account`}> 796 <SettingsList.ItemIcon icon={PersonPlusIcon} /> 797 <SettingsList.ItemText> 798 <Trans>Add another account</Trans> 799 </SettingsList.ItemText> 800 </SettingsList.PressableItem> 801 ) 802} 803 804function AccountRow({ 805 profile, 806 account, 807 pendingDid, 808 disableSwitching, 809 dragHandle, 810 onPressSwitchAccount, 811}: { 812 profile?: AppBskyActorDefs.ProfileViewDetailed 813 account: SessionAccount 814 pendingDid: string | null 815 disableSwitching?: boolean 816 dragHandle?: React.ReactNode 817 onPressSwitchAccount: ( 818 account: SessionAccount, 819 logContext: 'Settings', 820 ) => void 821}) { 822 const {t: l} = useLingui() 823 const t = useTheme() 824 825 const moderationOpts = useModerationOpts() 826 const removePromptControl = Prompt.usePromptControl() 827 const {removeAccount} = useSessionApi() 828 const {isActive: live} = useActorStatus(profile) 829 const [devModeEnabled] = useDevMode() 830 const [hiddenAccountsElsewhere, setHiddenAccountsElsewhere, hiddenDidsSet] = 831 useHiddenAccountsElsewhere() 832 833 const enableSquareButtons = useEnableSquareButtons() 834 const isHiddenElsewhere = hiddenDidsSet.has(account.did) 835 836 const onSwitchAccount = () => { 837 if (pendingDid || disableSwitching) return 838 onPressSwitchAccount(account, 'Settings') 839 } 840 841 const onToggleHideElsewhere = () => { 842 setHiddenAccountsElsewhere( 843 isHiddenElsewhere 844 ? hiddenAccountsElsewhere.filter(did => did !== account.did) 845 : [...hiddenAccountsElsewhere, account.did], 846 ) 847 Toast.show( 848 isHiddenElsewhere 849 ? l`Account will show in other switchers again` 850 : l`Account hidden from other switchers`, 851 ) 852 } 853 854 const onCopyDid = () => { 855 void setStringAsync(account.did) 856 Toast.show(l`DID copied to clipboard`) 857 } 858 859 return ( 860 <View style={[a.relative, t.atoms.bg]}> 861 <SettingsList.PressableItem 862 onPress={onSwitchAccount} 863 label={l`Switch account`} 864 disabled={Boolean(disableSwitching)} 865 contentContainerStyle={[ 866 { 867 minHeight: 48, 868 }, 869 ]}> 870 {moderationOpts && profile ? ( 871 <UserAvatar 872 size={28} 873 avatar={profile.avatar} 874 moderation={moderateProfile(profile, moderationOpts).ui('avatar')} 875 type={profile.associated?.labeler ? 'labeler' : 'user'} 876 live={live} 877 hideLiveBadge 878 /> 879 ) : ( 880 <View style={[{width: 28}]} /> 881 )} 882 <SettingsList.ItemText 883 numberOfLines={1} 884 style={[ 885 a.leading_snug, 886 a.self_center, 887 !disableSwitching && a.pr_2xl, 888 ]}> 889 {sanitizeHandle(account.handle, '@')} 890 </SettingsList.ItemText> 891 {pendingDid === account.did && <SettingsList.ItemIcon icon={Loader} />} 892 {disableSwitching ? ( 893 <View style={[a.self_center]}>{dragHandle}</View> 894 ) : null} 895 </SettingsList.PressableItem> 896 {!pendingDid && !disableSwitching && ( 897 <Menu.Root> 898 <Menu.Trigger label={l`Account options`}> 899 {({props, state}) => ( 900 <Pressable 901 {...props} 902 style={[ 903 a.absolute, 904 {top: 12, right: tokens.space.lg}, 905 a.p_xs, 906 enableSquareButtons ? a.rounded_sm : a.rounded_full, 907 (state.hovered || state.pressed) && t.atoms.bg_contrast_25, 908 ]}> 909 <DotsHorizontal size="md" style={t.atoms.text} /> 910 </Pressable> 911 )} 912 </Menu.Trigger> 913 <Menu.Outer showCancel> 914 <Menu.Item 915 label={ 916 isHiddenElsewhere ? l`Hidden elsewhere` : l`Hide elsewhere` 917 } 918 onPress={onToggleHideElsewhere}> 919 <Menu.ItemText> 920 {isHiddenElsewhere ? ( 921 <Trans>Hidden elsewhere</Trans> 922 ) : ( 923 <Trans>Hide elsewhere</Trans> 924 )} 925 </Menu.ItemText> 926 <Menu.ItemRadio selected={isHiddenElsewhere} /> 927 </Menu.Item> 928 {devModeEnabled ? ( 929 <Menu.Item label={l`Copy DID`} onPress={onCopyDid}> 930 <Menu.ItemText> 931 <Trans>Copy DID</Trans> 932 </Menu.ItemText> 933 <Menu.ItemIcon icon={ClipboardIcon} /> 934 </Menu.Item> 935 ) : null} 936 <Menu.Divider /> 937 <Menu.Item 938 label={l`Remove account`} 939 onPress={() => removePromptControl.open()}> 940 <Menu.ItemText> 941 <Trans>Remove account</Trans> 942 </Menu.ItemText> 943 <Menu.ItemIcon icon={PersonXIcon} /> 944 </Menu.Item> 945 </Menu.Outer> 946 </Menu.Root> 947 )} 948 949 <Prompt.Basic 950 control={removePromptControl} 951 title={l`Remove from quick access?`} 952 description={l`This will remove @${account.handle} from the quick access list.`} 953 onConfirm={() => { 954 removeAccount(account) 955 Toast.show(l`Account removed from quick access`) 956 }} 957 confirmButtonCta={l`Remove`} 958 confirmButtonColor="negative" 959 /> 960 </View> 961 ) 962}