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

Configure Feed

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

at cope-settings-sync 1284 lines 44 kB view raw
1import {type JSX, useCallback, useRef} from 'react' 2import * as Linking from 'expo-linking' 3import * as Notifications from 'expo-notifications' 4import {i18n, type MessageDescriptor} from '@lingui/core' 5import {msg} from '@lingui/core/macro' 6import { 7 type BottomTabBarProps, 8 createBottomTabNavigator, 9} from '@react-navigation/bottom-tabs' 10import { 11 CommonActions, 12 createNavigationContainerRef, 13 DarkTheme, 14 DefaultTheme, 15 type LinkingOptions, 16 NavigationContainer, 17 StackActions, 18} from '@react-navigation/native' 19 20import {timeout} from '#/lib/async/timeout' 21import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 22import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' 23import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 24import { 25 getNotificationPayload, 26 type NotificationPayload, 27 notificationToURL, 28 storePayloadForAccountSwitch, 29} from '#/lib/hooks/useNotificationHandler' 30import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration' 31import {useCallOnce} from '#/lib/once' 32import {buildStateObject} from '#/lib/routes/helpers' 33import { 34 type AllNavigatorParams, 35 type BottomTabNavigatorParams, 36 type FlatNavigatorParams, 37 type HomeTabNavigatorParams, 38 type MessagesTabNavigatorParams, 39 type MyProfileTabNavigatorParams, 40 type NotificationsTabNavigatorParams, 41 type RouteParams, 42 type SearchTabNavigatorParams, 43 type State, 44} from '#/lib/routes/types' 45import {bskyTitle} from '#/lib/strings/headings' 46import {useDisableVerifyEmailReminder} from '#/state/preferences/disable-verify-email-reminder' 47import {useUnreadNotifications} from '#/state/queries/notifications/unread' 48import {useSession} from '#/state/session' 49import {useLoggedOutViewControls} from '#/state/shell/logged-out' 50import { 51 shouldRequestEmailConfirmation, 52 snoozeEmailConfirmationPrompt, 53} from '#/state/shell/reminders' 54import {useCloseAllActiveElements} from '#/state/util' 55import {CommunityGuidelinesScreen} from '#/view/screens/CommunityGuidelines' 56import {CopyrightPolicyScreen} from '#/view/screens/CopyrightPolicy' 57import {DebugModScreen} from '#/view/screens/DebugMod' 58import {FeedsScreen} from '#/view/screens/Feeds' 59import {HomeScreen} from '#/view/screens/Home' 60import {ListsScreen} from '#/view/screens/Lists' 61import {ModerationBlockedAccounts} from '#/view/screens/ModerationBlockedAccounts' 62import {ModerationModlistsScreen} from '#/view/screens/ModerationModlists' 63import {ModerationMutedAccounts} from '#/view/screens/ModerationMutedAccounts' 64import {NotFoundScreen} from '#/view/screens/NotFound' 65import {NotificationsScreen} from '#/view/screens/Notifications' 66import {PostThreadScreen} from '#/view/screens/PostThread' 67import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy' 68import {ProfileScreen} from '#/view/screens/Profile' 69import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy' 70import {StorybookScreen} from '#/view/screens/Storybook' 71import {SupportScreen} from '#/view/screens/Support' 72import {TermsOfServiceScreen} from '#/view/screens/TermsOfService' 73import {BottomBar} from '#/view/shell/bottom-bar/BottomBar' 74import {createNativeStackNavigatorWithAuth} from '#/view/shell/createNativeStackNavigatorWithAuth' 75import {BookmarksScreen} from '#/screens/Bookmarks' 76import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen' 77import {FindContactsFlowScreen} from '#/screens/FindContactsFlowScreen' 78import HashtagScreen from '#/screens/Hashtag' 79import {LogScreen} from '#/screens/Log' 80import {AuthCallback} from '#/screens/Login/AuthCallback' 81import {MessagesScreen} from '#/screens/Messages/ChatList' 82import {MessagesConversationScreen} from '#/screens/Messages/Conversation' 83import {MessagesConversationSettingsScreen} from '#/screens/Messages/ConversationSettings' 84import {MessagesInboxScreen} from '#/screens/Messages/Inbox' 85import {MessagesSettingsScreen} from '#/screens/Messages/Settings' 86import {ModerationScreen} from '#/screens/Moderation' 87import {Screen as ModerationVerificationSettings} from '#/screens/Moderation/VerificationSettings' 88import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings' 89import {NotificationsActivityListScreen} from '#/screens/Notifications/ActivityList' 90import {PostLikedByScreen} from '#/screens/Post/PostLikedBy' 91import {PostQuotesScreen} from '#/screens/Post/PostQuotes' 92import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy' 93import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers' 94import {ProfileFeedScreen} from '#/screens/Profile/ProfileFeed' 95import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers' 96import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows' 97import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy' 98import {ProfileSearchScreen} from '#/screens/Profile/ProfileSearch' 99import {ProfileListScreen} from '#/screens/ProfileList' 100import {SavedFeeds} from '#/screens/SavedFeeds' 101import {SearchScreen} from '#/screens/Search' 102import {AboutSettingsScreen} from '#/screens/Settings/AboutSettings' 103import {AccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings' 104import {AccountSettingsScreen} from '#/screens/Settings/AccountSettings' 105import {ActivityPrivacySettingsScreen} from '#/screens/Settings/ActivityPrivacySettings' 106import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings' 107import {AppearanceColorThemeSettingsScreen} from '#/screens/Settings/AppearanceSettings/ColorThemeSettings' 108import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings' 109import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords' 110import {AutomationLabelSettingsScreen} from '#/screens/Settings/AutomationLabelSettings' 111import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings' 112import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' 113import {FindContactsSettingsScreen} from '#/screens/Settings/FindContactsSettings' 114import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences' 115import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings' 116import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings' 117import {LegacyNotificationSettingsScreen} from '#/screens/Settings/LegacyNotificationSettings' 118import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings' 119import {ActivityNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ActivityNotificationSettings' 120import {LikeNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/LikeNotificationSettings' 121import {LikesOnRepostsNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings' 122import {MentionNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/MentionNotificationSettings' 123import {MiscellaneousNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings' 124import {NewFollowerNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/NewFollowerNotificationSettings' 125import {QuoteNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/QuoteNotificationSettings' 126import {ReplyNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ReplyNotificationSettings' 127import {RepostNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/RepostNotificationSettings' 128import {RepostsOnRepostsNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings' 129import {PetLabelSettingsScreen} from '#/screens/Settings/PetLabelSettings' 130import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' 131import {RunesSettingsScreen} from '#/screens/Settings/RunesSettings' 132import {RunesDisplayAlsoLikedSettingsScreen} from '#/screens/Settings/RunesSettings/AlsoLikedSettings' 133import {RunesBadgesSettingsScreen} from '#/screens/Settings/RunesSettings/BadgesSettings' 134import {RunesDisplaySettingsScreen} from '#/screens/Settings/RunesSettings/DisplaySettings' 135import {RunesSettingsSyncSettingsScreen} from '#/screens/Settings/RunesSettings/SettingsSyncSettings' 136import {RunesExtraSettingsScreen} from '#/screens/Settings/RunesSettings/ExtraSettings' 137import {RunesUsabilityFeedSettingsScreen} from '#/screens/Settings/RunesSettings/FeedSettings' 138import {RunesImpressionsSettingsScreen} from '#/screens/Settings/RunesSettings/ImpressionsSettings' 139import {RunesInfrastructureSettingsScreen} from '#/screens/Settings/RunesSettings/InfrastructureSettings' 140import {RunesMenusSettingsScreen} from '#/screens/Settings/RunesSettings/MenusSettings' 141import {RunesUsabilitySettingsScreen} from '#/screens/Settings/RunesSettings/UsabilitySettings' 142import {SettingsScreen} from '#/screens/Settings/Settings' 143import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' 144import { 145 StarterPackScreen, 146 StarterPackScreenShort, 147} from '#/screens/StarterPack/StarterPackScreen' 148import {Wizard} from '#/screens/StarterPack/Wizard' 149import TopicScreen from '#/screens/Topic' 150import {VideoFeed} from '#/screens/VideoFeed' 151import {type Theme, useTheme} from '#/alf' 152import { 153 EmailDialogScreenID, 154 useEmailDialogControl, 155} from '#/components/dialogs/EmailDialog' 156import {useAnalytics} from '#/analytics' 157import {setNavigationMetadata} from '#/analytics/metadata' 158import {IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env' 159import {router} from '#/routes' 160import {Referrer} from '../modules/expo-bluesky-swiss-army' 161 162const navigationRef = createNavigationContainerRef<AllNavigatorParams>() 163 164/** 165 * Stores the navigation state before account switch on native. 166 * This allows the user to stay on the same page when switching accounts. 167 */ 168let storedNavigationStateForAccountSwitch: State | undefined 169 170export function storeNavigationStateForAccountSwitch() { 171 if (IS_NATIVE && navigationRef.isReady()) { 172 storedNavigationStateForAccountSwitch = navigationRef.getRootState() 173 } 174} 175 176function consumeStoredNavigationState(): State | undefined { 177 const state = storedNavigationStateForAccountSwitch 178 storedNavigationStateForAccountSwitch = undefined 179 return state 180} 181 182const HomeTab = createNativeStackNavigatorWithAuth<HomeTabNavigatorParams>() 183const SearchTab = createNativeStackNavigatorWithAuth<SearchTabNavigatorParams>() 184const NotificationsTab = 185 createNativeStackNavigatorWithAuth<NotificationsTabNavigatorParams>() 186const MyProfileTab = 187 createNativeStackNavigatorWithAuth<MyProfileTabNavigatorParams>() 188const MessagesTab = 189 createNativeStackNavigatorWithAuth<MessagesTabNavigatorParams>() 190const Flat = createNativeStackNavigatorWithAuth<FlatNavigatorParams>() 191const Tab = createBottomTabNavigator<BottomTabNavigatorParams>() 192 193/** 194 * These "common screens" are reused across stacks. 195 */ 196function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { 197 const title = (page: MessageDescriptor) => 198 bskyTitle(i18n._(page), unreadCountLabel) 199 200 return ( 201 <> 202 <Stack.Screen 203 name="NotFound" 204 getComponent={() => NotFoundScreen} 205 options={{title: title(msg`Not Found`)}} 206 /> 207 <Stack.Screen 208 name="AuthCallback" 209 getComponent={() => AuthCallback} 210 options={{title: title(msg`Signing in...`)}} 211 /> 212 <Stack.Screen 213 name="Lists" 214 component={ListsScreen} 215 options={{title: title(msg`Lists`), requireAuth: true}} 216 /> 217 <Stack.Screen 218 name="Moderation" 219 getComponent={() => ModerationScreen} 220 options={{title: title(msg`Moderation`), requireAuth: true}} 221 /> 222 <Stack.Screen 223 name="ModerationModlists" 224 getComponent={() => ModerationModlistsScreen} 225 options={{title: title(msg`Moderation Lists`), requireAuth: true}} 226 /> 227 <Stack.Screen 228 name="ModerationMutedAccounts" 229 getComponent={() => ModerationMutedAccounts} 230 options={{title: title(msg`Muted Accounts`), requireAuth: true}} 231 /> 232 <Stack.Screen 233 name="ModerationBlockedAccounts" 234 getComponent={() => ModerationBlockedAccounts} 235 options={{title: title(msg`Blocked Accounts`), requireAuth: true}} 236 /> 237 <Stack.Screen 238 name="ModerationInteractionSettings" 239 getComponent={() => ModerationInteractionSettings} 240 options={{ 241 title: title(msg`Post Interaction Settings`), 242 requireAuth: true, 243 }} 244 /> 245 <Stack.Screen 246 name="ModerationVerificationSettings" 247 getComponent={() => ModerationVerificationSettings} 248 options={{ 249 title: title(msg`Verification Settings`), 250 requireAuth: true, 251 }} 252 /> 253 <Stack.Screen 254 name="Settings" 255 getComponent={() => SettingsScreen} 256 options={{title: title(msg`Settings`), requireAuth: true}} 257 /> 258 <Stack.Screen 259 name="LanguageSettings" 260 getComponent={() => LanguageSettingsScreen} 261 options={{title: title(msg`Language Settings`), requireAuth: true}} 262 /> 263 <Stack.Screen 264 name="Profile" 265 getComponent={() => ProfileScreen} 266 options={({route}) => ({ 267 title: bskyTitle(`@${route.params.name}`, unreadCountLabel), 268 })} 269 /> 270 <Stack.Screen 271 name="ProfileFollowers" 272 getComponent={() => ProfileFollowersScreen} 273 options={({route}) => ({ 274 title: title(msg`People following @${route.params.name}`), 275 })} 276 /> 277 <Stack.Screen 278 name="ProfileFollows" 279 getComponent={() => ProfileFollowsScreen} 280 options={({route}) => ({ 281 title: title(msg`People followed by @${route.params.name}`), 282 })} 283 /> 284 <Stack.Screen 285 name="ProfileKnownFollowers" 286 getComponent={() => ProfileKnownFollowersScreen} 287 options={({route}) => ({ 288 title: title(msg`Followers of @${route.params.name} that you know`), 289 })} 290 /> 291 <Stack.Screen 292 name="ProfileList" 293 getComponent={() => ProfileListScreen} 294 options={{title: title(msg`List`), requireAuth: true}} 295 /> 296 <Stack.Screen 297 name="ProfileSearch" 298 getComponent={() => ProfileSearchScreen} 299 options={({route}) => ({ 300 title: title(msg`Search @${route.params.name}'s posts`), 301 })} 302 /> 303 <Stack.Screen 304 name="PostThread" 305 getComponent={() => PostThreadScreen} 306 options={({route}) => ({ 307 title: title(msg`Post by @${route.params.name}`), 308 })} 309 /> 310 <Stack.Screen 311 name="PostLikedBy" 312 getComponent={() => PostLikedByScreen} 313 options={({route}) => ({ 314 title: title(msg`Post by @${route.params.name}`), 315 })} 316 /> 317 <Stack.Screen 318 name="PostRepostedBy" 319 getComponent={() => PostRepostedByScreen} 320 options={({route}) => ({ 321 title: title(msg`Post by @${route.params.name}`), 322 })} 323 /> 324 <Stack.Screen 325 name="PostQuotes" 326 getComponent={() => PostQuotesScreen} 327 options={({route}) => ({ 328 title: title(msg`Post by @${route.params.name}`), 329 })} 330 /> 331 <Stack.Screen 332 name="ProfileFeed" 333 getComponent={() => ProfileFeedScreen} 334 options={{title: title(msg`Feed`)}} 335 /> 336 <Stack.Screen 337 name="ProfileFeedLikedBy" 338 getComponent={() => ProfileFeedLikedByScreen} 339 options={{title: title(msg`Liked by`)}} 340 /> 341 <Stack.Screen 342 name="ProfileLabelerLikedBy" 343 getComponent={() => ProfileLabelerLikedByScreen} 344 options={{title: title(msg`Liked by`)}} 345 /> 346 <Stack.Screen 347 name="Debug" 348 getComponent={() => StorybookScreen} 349 options={{title: title(msg`Storybook`), requireAuth: true}} 350 /> 351 <Stack.Screen 352 name="DebugMod" 353 getComponent={() => DebugModScreen} 354 options={{title: title(msg`Moderation states`), requireAuth: true}} 355 /> 356 <Stack.Screen 357 name="SharedPreferencesTester" 358 getComponent={() => SharedPreferencesTesterScreen} 359 options={{title: title(msg`Shared Preferences Tester`)}} 360 /> 361 <Stack.Screen 362 name="Log" 363 getComponent={() => LogScreen} 364 options={{title: title(msg`Log`), requireAuth: true}} 365 /> 366 <Stack.Screen 367 name="Support" 368 getComponent={() => SupportScreen} 369 options={{title: title(msg`Support`)}} 370 /> 371 <Stack.Screen 372 name="PrivacyPolicy" 373 getComponent={() => PrivacyPolicyScreen} 374 options={{title: title(msg`Privacy Policy`)}} 375 /> 376 <Stack.Screen 377 name="TermsOfService" 378 getComponent={() => TermsOfServiceScreen} 379 options={{title: title(msg`Terms of Service`)}} 380 /> 381 <Stack.Screen 382 name="CommunityGuidelines" 383 getComponent={() => CommunityGuidelinesScreen} 384 options={{title: title(msg`Community Guidelines`)}} 385 /> 386 <Stack.Screen 387 name="CopyrightPolicy" 388 getComponent={() => CopyrightPolicyScreen} 389 options={{title: title(msg`Copyright Policy`)}} 390 /> 391 <Stack.Screen 392 name="AppPasswords" 393 getComponent={() => AppPasswordsScreen} 394 options={{title: title(msg`App Passwords`), requireAuth: true}} 395 /> 396 <Stack.Screen 397 name="SavedFeeds" 398 getComponent={() => SavedFeeds} 399 options={{title: title(msg`Edit My Feeds`), requireAuth: true}} 400 /> 401 <Stack.Screen 402 name="PreferencesFollowingFeed" 403 getComponent={() => FollowingFeedPreferencesScreen} 404 options={{ 405 title: title(msg`Following Feed Preferences`), 406 requireAuth: true, 407 }} 408 /> 409 <Stack.Screen 410 name="PreferencesThreads" 411 getComponent={() => ThreadPreferencesScreen} 412 options={{title: title(msg`Threads Preferences`), requireAuth: true}} 413 /> 414 <Stack.Screen 415 name="PreferencesExternalEmbeds" 416 getComponent={() => ExternalMediaPreferencesScreen} 417 options={{ 418 title: title(msg`External Media Preferences`), 419 requireAuth: true, 420 }} 421 /> 422 <Stack.Screen 423 name="AccessibilitySettings" 424 getComponent={() => AccessibilitySettingsScreen} 425 options={{ 426 title: title(msg`Accessibility Settings`), 427 requireAuth: true, 428 }} 429 /> 430 <Stack.Screen 431 name="RunesSettings" 432 getComponent={() => RunesSettingsScreen} 433 options={{ 434 title: title(msg`Runes`), 435 requireAuth: true, 436 }} 437 /> 438 <Stack.Screen 439 name="RunesMenusSettings" 440 getComponent={() => RunesMenusSettingsScreen} 441 options={{title: title(msg`Menus`), requireAuth: true}} 442 /> 443 <Stack.Screen 444 name="RunesBadgesSettings" 445 getComponent={() => RunesBadgesSettingsScreen} 446 options={{title: title(msg`Badges`), requireAuth: true}} 447 /> 448 <Stack.Screen 449 name="RunesImpressionsSettings" 450 getComponent={() => RunesImpressionsSettingsScreen} 451 options={{title: title(msg`Impressions`), requireAuth: true}} 452 /> 453 <Stack.Screen 454 name="RunesUsabilitySettings" 455 getComponent={() => RunesUsabilitySettingsScreen} 456 options={{title: title(msg`Usability`), requireAuth: true}} 457 /> 458 <Stack.Screen 459 name="RunesUsabilityFeedSettings" 460 getComponent={() => RunesUsabilityFeedSettingsScreen} 461 options={{title: title(msg`Feeds`), requireAuth: true}} 462 /> 463 <Stack.Screen 464 name="RunesDisplaySettings" 465 getComponent={() => RunesDisplaySettingsScreen} 466 options={{title: title(msg`Display`), requireAuth: true}} 467 /> 468 <Stack.Screen 469 name="RunesDisplayAlsoLikedSettings" 470 getComponent={() => RunesDisplayAlsoLikedSettingsScreen} 471 options={{title: title(msg`Also liked`), requireAuth: true}} 472 /> 473 <Stack.Screen 474 name="RunesInfrastructureSettings" 475 getComponent={() => RunesInfrastructureSettingsScreen} 476 options={{title: title(msg`Infrastructure`), requireAuth: true}} 477 /> 478 <Stack.Screen 479 name="RunesExtraSettings" 480 getComponent={() => RunesExtraSettingsScreen} 481 options={{title: title(msg`Extra settings`), requireAuth: true}} 482 /> 483 <Stack.Screen 484 name="RunesSettingsSyncSettings" 485 getComponent={() => RunesSettingsSyncSettingsScreen} 486 options={{title: title(msg`Settings Sync`), requireAuth: true}} 487 /> 488 <Stack.Screen 489 name="AppearanceSettings" 490 getComponent={() => AppearanceSettingsScreen} 491 options={{ 492 title: title(msg`Appearance`), 493 requireAuth: true, 494 }} 495 /> 496 <Stack.Screen 497 name="AppearanceColorThemeSettings" 498 getComponent={() => AppearanceColorThemeSettingsScreen} 499 options={{ 500 title: title(msg`Color Theme`), 501 requireAuth: true, 502 }} 503 /> 504 <Stack.Screen 505 name="AccountSettings" 506 getComponent={() => AccountSettingsScreen} 507 options={{ 508 title: title(msg`Account`), 509 requireAuth: true, 510 }} 511 /> 512 <Stack.Screen 513 name="AutomationLabelSettings" 514 getComponent={() => AutomationLabelSettingsScreen} 515 options={{ 516 title: title(msg`Automation Label`), 517 requireAuth: true, 518 }} 519 /> 520 <Stack.Screen 521 name="PetLabelSettings" 522 getComponent={() => PetLabelSettingsScreen} 523 options={{ 524 title: title(msg`Pet Label`), 525 requireAuth: true, 526 }} 527 /> 528 <Stack.Screen 529 name="PrivacyAndSecuritySettings" 530 getComponent={() => PrivacyAndSecuritySettingsScreen} 531 options={{ 532 title: title(msg`Privacy and Security`), 533 requireAuth: true, 534 }} 535 /> 536 <Stack.Screen 537 name="ActivityPrivacySettings" 538 getComponent={() => ActivityPrivacySettingsScreen} 539 options={{ 540 title: title(msg`Privacy and Security`), 541 requireAuth: true, 542 }} 543 /> 544 <Stack.Screen 545 name="FindContactsSettings" 546 getComponent={() => FindContactsSettingsScreen} 547 options={{ 548 title: title(msg`Find Contacts`), 549 requireAuth: true, 550 }} 551 /> 552 <Stack.Screen 553 name="NotificationSettings" 554 getComponent={() => NotificationSettingsScreen} 555 options={{title: title(msg`Notification settings`), requireAuth: true}} 556 /> 557 <Stack.Screen 558 name="ReplyNotificationSettings" 559 getComponent={() => ReplyNotificationSettingsScreen} 560 options={{ 561 title: title(msg`Reply notifications`), 562 requireAuth: true, 563 }} 564 /> 565 <Stack.Screen 566 name="MentionNotificationSettings" 567 getComponent={() => MentionNotificationSettingsScreen} 568 options={{ 569 title: title(msg`Mention notifications`), 570 requireAuth: true, 571 }} 572 /> 573 <Stack.Screen 574 name="QuoteNotificationSettings" 575 getComponent={() => QuoteNotificationSettingsScreen} 576 options={{ 577 title: title(msg`Quote notifications`), 578 requireAuth: true, 579 }} 580 /> 581 <Stack.Screen 582 name="LikeNotificationSettings" 583 getComponent={() => LikeNotificationSettingsScreen} 584 options={{ 585 title: title(msg`Like notifications`), 586 requireAuth: true, 587 }} 588 /> 589 <Stack.Screen 590 name="RepostNotificationSettings" 591 getComponent={() => RepostNotificationSettingsScreen} 592 options={{ 593 title: title(msg`Repost notifications`), 594 requireAuth: true, 595 }} 596 /> 597 <Stack.Screen 598 name="NewFollowerNotificationSettings" 599 getComponent={() => NewFollowerNotificationSettingsScreen} 600 options={{ 601 title: title(msg`New follower notifications`), 602 requireAuth: true, 603 }} 604 /> 605 <Stack.Screen 606 name="LikesOnRepostsNotificationSettings" 607 getComponent={() => LikesOnRepostsNotificationSettingsScreen} 608 options={{ 609 title: title(msg`Likes of your reposts notifications`), 610 requireAuth: true, 611 }} 612 /> 613 <Stack.Screen 614 name="RepostsOnRepostsNotificationSettings" 615 getComponent={() => RepostsOnRepostsNotificationSettingsScreen} 616 options={{ 617 title: title(msg`Reposts of your reposts notifications`), 618 requireAuth: true, 619 }} 620 /> 621 <Stack.Screen 622 name="ActivityNotificationSettings" 623 getComponent={() => ActivityNotificationSettingsScreen} 624 options={{ 625 title: title(msg`Activity notifications`), 626 requireAuth: true, 627 }} 628 /> 629 <Stack.Screen 630 name="MiscellaneousNotificationSettings" 631 getComponent={() => MiscellaneousNotificationSettingsScreen} 632 options={{ 633 title: title(msg`Miscellaneous notifications`), 634 requireAuth: true, 635 }} 636 /> 637 <Stack.Screen 638 name="ContentAndMediaSettings" 639 getComponent={() => ContentAndMediaSettingsScreen} 640 options={{ 641 title: title(msg`Content and Media`), 642 requireAuth: true, 643 }} 644 /> 645 <Stack.Screen 646 name="InterestsSettings" 647 getComponent={() => InterestsSettingsScreen} 648 options={{ 649 title: title(msg`Your interests`), 650 requireAuth: true, 651 }} 652 /> 653 <Stack.Screen 654 name="AboutSettings" 655 getComponent={() => AboutSettingsScreen} 656 options={{ 657 title: title(msg`About`), 658 requireAuth: true, 659 }} 660 /> 661 <Stack.Screen 662 name="AppIconSettings" 663 getComponent={() => AppIconSettingsScreen} 664 options={{ 665 title: title(msg`App Icon`), 666 requireAuth: true, 667 }} 668 /> 669 <Stack.Screen 670 name="Hashtag" 671 getComponent={() => HashtagScreen} 672 options={{title: title(msg`Hashtag`)}} 673 /> 674 <Stack.Screen 675 name="Topic" 676 getComponent={() => TopicScreen} 677 options={{title: title(msg`Topic`)}} 678 /> 679 <Stack.Screen 680 name="MessagesConversation" 681 getComponent={() => MessagesConversationScreen} 682 options={{title: title(msg`Chat`), requireAuth: true}} 683 /> 684 <Stack.Screen 685 name="MessagesConversationSettings" 686 getComponent={() => MessagesConversationSettingsScreen} 687 options={{title: title(msg`Group chat settings`), requireAuth: true}} 688 /> 689 <Stack.Screen 690 name="MessagesSettings" 691 getComponent={() => MessagesSettingsScreen} 692 options={{title: title(msg`Chat settings`), requireAuth: true}} 693 /> 694 <Stack.Screen 695 name="MessagesInbox" 696 getComponent={() => MessagesInboxScreen} 697 options={{title: title(msg`Chat request inbox`), requireAuth: true}} 698 /> 699 <Stack.Screen 700 name="NotificationsActivityList" 701 getComponent={() => NotificationsActivityListScreen} 702 options={{title: title(msg`Notifications`), requireAuth: true}} 703 /> 704 <Stack.Screen 705 name="LegacyNotificationSettings" 706 getComponent={() => LegacyNotificationSettingsScreen} 707 options={{title: title(msg`Notification settings`), requireAuth: true}} 708 /> 709 <Stack.Screen 710 name="Feeds" 711 getComponent={() => FeedsScreen} 712 options={{title: title(msg`Feeds`)}} 713 /> 714 <Stack.Screen 715 name="StarterPack" 716 getComponent={() => StarterPackScreen} 717 options={{title: title(msg`Starter Pack`)}} 718 /> 719 <Stack.Screen 720 name="StarterPackShort" 721 getComponent={() => StarterPackScreenShort} 722 options={{title: title(msg`Starter Pack`)}} 723 /> 724 <Stack.Screen 725 name="StarterPackWizard" 726 getComponent={() => Wizard} 727 options={{title: title(msg`Create a starter pack`), requireAuth: true}} 728 /> 729 <Stack.Screen 730 name="StarterPackEdit" 731 getComponent={() => Wizard} 732 options={{title: title(msg`Edit your starter pack`), requireAuth: true}} 733 /> 734 <Stack.Screen 735 name="VideoFeed" 736 getComponent={() => VideoFeed} 737 options={{ 738 title: title(msg`Video Feed`), 739 requireAuth: true, 740 }} 741 /> 742 <Stack.Screen 743 name="Bookmarks" 744 getComponent={() => BookmarksScreen} 745 options={{ 746 title: title(msg`Saved Posts`), 747 requireAuth: true, 748 }} 749 /> 750 <Stack.Screen 751 name="FindContactsFlow" 752 getComponent={() => FindContactsFlowScreen} 753 options={{ 754 title: title(msg`Find Contacts`), 755 requireAuth: true, 756 gestureEnabled: false, 757 }} 758 /> 759 </> 760 ) 761} 762 763/** 764 * The TabsNavigator is used by native mobile to represent the routes 765 * in 3 distinct tab-stacks with a different root screen on each. 766 */ 767function TabsNavigator({ 768 layout, 769}: { 770 layout: React.ComponentProps<typeof Tab.Navigator>['layout'] 771}) { 772 const tabBar = useCallback( 773 (props: JSX.IntrinsicAttributes & BottomTabBarProps) => ( 774 <BottomBar {...props} /> 775 ), 776 [], 777 ) 778 779 return ( 780 <Tab.Navigator 781 initialRouteName="HomeTab" 782 backBehavior="initialRoute" 783 screenOptions={{headerShown: false, lazy: true}} 784 tabBar={tabBar} 785 layout={layout}> 786 <Tab.Screen name="HomeTab" getComponent={() => HomeTabNavigator} /> 787 <Tab.Screen name="SearchTab" getComponent={() => SearchTabNavigator} /> 788 <Tab.Screen 789 name="MessagesTab" 790 getComponent={() => MessagesTabNavigator} 791 /> 792 <Tab.Screen 793 name="NotificationsTab" 794 getComponent={() => NotificationsTabNavigator} 795 /> 796 <Tab.Screen 797 name="MyProfileTab" 798 getComponent={() => MyProfileTabNavigator} 799 /> 800 </Tab.Navigator> 801 ) 802} 803 804function screenOptions(t: Theme) { 805 return { 806 fullScreenGestureEnabled: true, 807 headerShown: false, 808 contentStyle: t.atoms.bg, 809 } as const 810} 811 812function HomeTabNavigator() { 813 const t = useTheme() 814 815 const BLURRED_SCROLL_EDGE_EFFECT = IS_LIQUID_GLASS 816 ? ({ 817 headerShown: true, 818 headerTransparent: true, 819 headerTitle: '', 820 headerBackVisible: false, 821 scrollEdgeEffects: { 822 top: 'soft', 823 }, 824 } as const) 825 : {} 826 827 return ( 828 <HomeTab.Navigator screenOptions={screenOptions(t)} initialRouteName="Home"> 829 <HomeTab.Screen 830 name="Home" 831 getComponent={() => HomeScreen} 832 options={BLURRED_SCROLL_EDGE_EFFECT} 833 /> 834 <HomeTab.Screen 835 name="Start" 836 getComponent={() => HomeScreen} 837 options={BLURRED_SCROLL_EDGE_EFFECT} 838 /> 839 {commonScreens(HomeTab as typeof Flat)} 840 </HomeTab.Navigator> 841 ) 842} 843 844function SearchTabNavigator() { 845 const t = useTheme() 846 return ( 847 <SearchTab.Navigator 848 screenOptions={screenOptions(t)} 849 initialRouteName="Search"> 850 <SearchTab.Screen name="Search" getComponent={() => SearchScreen} /> 851 {commonScreens(SearchTab as typeof Flat)} 852 </SearchTab.Navigator> 853 ) 854} 855 856function NotificationsTabNavigator() { 857 const t = useTheme() 858 return ( 859 <NotificationsTab.Navigator 860 screenOptions={screenOptions(t)} 861 initialRouteName="Notifications"> 862 <NotificationsTab.Screen 863 name="Notifications" 864 getComponent={() => NotificationsScreen} 865 options={{requireAuth: true}} 866 /> 867 {commonScreens(NotificationsTab as typeof Flat)} 868 </NotificationsTab.Navigator> 869 ) 870} 871 872function MyProfileTabNavigator() { 873 const t = useTheme() 874 return ( 875 <MyProfileTab.Navigator 876 screenOptions={screenOptions(t)} 877 initialRouteName="MyProfile"> 878 <MyProfileTab.Screen 879 // MyProfile is not in AllNavigationParams - asserting as Profile at least 880 // gives us typechecking for initialParams -sfn 881 name={'MyProfile' as 'Profile'} 882 getComponent={() => ProfileScreen} 883 initialParams={{name: 'me', hideBackButton: true}} 884 /> 885 {commonScreens(MyProfileTab as unknown as typeof Flat)} 886 </MyProfileTab.Navigator> 887 ) 888} 889 890function MessagesTabNavigator() { 891 const t = useTheme() 892 return ( 893 <MessagesTab.Navigator 894 screenOptions={screenOptions(t)} 895 initialRouteName="Messages"> 896 <MessagesTab.Screen 897 name="Messages" 898 getComponent={() => MessagesScreen} 899 options={({route}) => ({ 900 requireAuth: true, 901 animationTypeForReplace: route.params?.animation ?? 'push', 902 })} 903 /> 904 {commonScreens(MessagesTab as typeof Flat)} 905 </MessagesTab.Navigator> 906 ) 907} 908 909/** 910 * The FlatNavigator is used by Web to represent the routes 911 * in a single ("flat") stack. 912 */ 913const FlatNavigator = ({ 914 layout, 915}: { 916 layout: React.ComponentProps<typeof Flat.Navigator>['layout'] 917}) => { 918 const t = useTheme() 919 const numUnread = useUnreadNotifications() 920 const screenListeners = useWebScrollRestoration() 921 const title = (page: MessageDescriptor) => bskyTitle(i18n._(page), numUnread) 922 923 return ( 924 <Flat.Navigator 925 layout={layout} 926 screenListeners={screenListeners} 927 screenOptions={screenOptions(t)}> 928 <Flat.Screen 929 name="Home" 930 getComponent={() => HomeScreen} 931 options={{title: title(msg`Home`)}} 932 /> 933 <Flat.Screen 934 name="Search" 935 getComponent={() => SearchScreen} 936 options={{title: title(msg`Explore`)}} 937 /> 938 <Flat.Screen 939 name="Notifications" 940 getComponent={() => NotificationsScreen} 941 options={{title: title(msg`Notifications`), requireAuth: true}} 942 /> 943 <Flat.Screen 944 name="Messages" 945 getComponent={() => MessagesScreen} 946 options={{title: title(msg`Messages`), requireAuth: true}} 947 /> 948 <Flat.Screen 949 name="Start" 950 getComponent={() => HomeScreen} 951 options={{title: title(msg`Home`)}} 952 /> 953 {commonScreens(Flat, numUnread)} 954 </Flat.Navigator> 955 ) 956} 957 958/** 959 * The RoutesContainer should wrap all components which need access 960 * to the navigation context. 961 */ 962 963const LINKING = { 964 // TODO figure out what we are going to use 965 // note: `bluesky://` and `witchsky://` is what is used in app.config.js 966 prefixes: [ 967 'bsky://', 968 'bluesky://', 969 'witchsky://', 970 'https://bsky.app', 971 'https://witchsky.app', 972 ], 973 974 getPathFromState(state: State) { 975 // find the current node in the navigation tree 976 let node = state.routes[state.index || 0] 977 while (node.state?.routes && typeof node.state?.index === 'number') { 978 node = node.state?.routes[node.state?.index] 979 } 980 981 // build the path 982 const route = router.matchName(node.name) 983 if (typeof route === 'undefined') { 984 return '/' // default to home 985 } 986 return route.build((node.params || {}) as RouteParams) 987 }, 988 989 getStateFromPath(path: string) { 990 const [name, params] = router.matchPath(path) 991 992 // Any time we receive a url that starts with `intent/` we want to ignore it here. It will be handled in the 993 // intent handler hook. We should check for the trailing slash, because if there isn't one then it isn't a valid 994 // intent 995 // On web, there is no route state that's created by default, so we should initialize it as the home route. On 996 // native, since the home tab and the home screen are defined as initial routes, we don't need to return a state 997 // since it will be created by react-navigation. 998 if (path.includes('intent/')) { 999 if (IS_NATIVE) return 1000 return buildStateObject('Flat', 'Home', params) 1001 } 1002 1003 if (IS_NATIVE) { 1004 if (name === 'Search') { 1005 return buildStateObject('SearchTab', 'Search', params) 1006 } 1007 if (name === 'Notifications') { 1008 return buildStateObject('NotificationsTab', 'Notifications', params) 1009 } 1010 if (name === 'Home') { 1011 return buildStateObject('HomeTab', 'Home', params) 1012 } 1013 if (name === 'Messages') { 1014 return buildStateObject('MessagesTab', 'Messages', params) 1015 } 1016 // if the path is something else, like a post, profile, or even settings, we need to initialize the home tab as pre-existing state otherwise the back button will not work 1017 return buildStateObject('HomeTab', name, params, [ 1018 { 1019 name: 'Home', 1020 params: {}, 1021 }, 1022 ]) 1023 } else { 1024 const res = buildStateObject('Flat', name, params) 1025 return res 1026 } 1027 }, 1028} satisfies LinkingOptions<AllNavigatorParams> 1029 1030function RoutesContainer({children}: React.PropsWithChildren<{}>) { 1031 const ax = useAnalytics() 1032 const notyLogger = ax.logger.useChild(ax.logger.Context.Notifications) 1033 const theme = useColorSchemeStyle(DefaultTheme, DarkTheme) 1034 const {currentAccount, accounts} = useSession() 1035 const {onPressSwitchAccount} = useAccountSwitcher() 1036 const {setShowLoggedOut} = useLoggedOutViewControls() 1037 const previousScreen = useRef<string | undefined>(undefined) 1038 const emailDialogControl = useEmailDialogControl() 1039 const closeAllActiveElements = useCloseAllActiveElements() 1040 const linkingUrl = Linking.useLinkingURL() 1041 1042 const disableVerifyEmailReminder = useDisableVerifyEmailReminder() 1043 1044 // Consume stored navigation state from account switch (native only) 1045 const initialState = IS_NATIVE ? consumeStoredNavigationState() : undefined 1046 1047 /** 1048 * Handle navigation to a conversation, or prepares for account switch. 1049 * 1050 * Non-reactive because we need the latest data from some hooks 1051 * after an async call - sfn 1052 */ 1053 const handleChatMessage = useNonReactiveCallback( 1054 (payload: Extract<NotificationPayload, {reason: 'chat-message'}>) => { 1055 notyLogger.debug(`handleChatMessage`, {payload}) 1056 1057 if (payload.recipientDid !== currentAccount?.did) { 1058 // handled in useNotificationHandler after account switch finishes 1059 storePayloadForAccountSwitch(payload) 1060 closeAllActiveElements() 1061 1062 const account = accounts.find(a => a.did === payload.recipientDid) 1063 if (account) { 1064 onPressSwitchAccount(account, 'Notification') 1065 } else { 1066 setShowLoggedOut(true) 1067 } 1068 } else { 1069 // @ts-expect-error nested navigators aren't typed -sfn 1070 navigate('MessagesTab', { 1071 screen: 'Messages', 1072 params: { 1073 pushToConversation: payload.convoId, 1074 }, 1075 }) 1076 } 1077 }, 1078 ) 1079 1080 function handlePushNotificationEntry() { 1081 if (!IS_NATIVE) return 1082 1083 // intent urls are handled by `useIntentHandler` 1084 if (linkingUrl) return 1085 1086 const notificationResponse = Notifications.getLastNotificationResponse() 1087 1088 if (notificationResponse) { 1089 notyLogger.debug(`handlePushNotificationEntry: response`, { 1090 response: notificationResponse, 1091 }) 1092 1093 // Clear the last notification response to ensure it's not used again 1094 try { 1095 Notifications.clearLastNotificationResponse() 1096 } catch (error) { 1097 notyLogger.error( 1098 `handlePushNotificationEntry: error clearing notification response`, 1099 {error}, 1100 ) 1101 } 1102 1103 const payload = getNotificationPayload(notificationResponse.notification) 1104 1105 if (payload) { 1106 ax.metric('notifications:openApp', { 1107 reason: payload.reason, 1108 causedBoot: true, 1109 }) 1110 1111 if (payload.reason === 'chat-message') { 1112 handleChatMessage(payload) 1113 } else { 1114 const path = notificationToURL(payload) 1115 1116 if (path === '/notifications') { 1117 resetToTab('NotificationsTab') 1118 notyLogger.debug(`handlePushNotificationEntry: default navigate`) 1119 } else if (path) { 1120 const [screen, params] = router.matchPath(path) 1121 // @ts-expect-error nested navigators aren't typed -sfn 1122 navigate('HomeTab', {screen, params}) 1123 notyLogger.debug(`handlePushNotificationEntry: navigate`, { 1124 screen, 1125 params, 1126 }) 1127 } 1128 } 1129 } 1130 } 1131 } 1132 1133 const onNavigationReady = useCallOnce(() => { 1134 const currentScreen = getCurrentRouteName() 1135 setNavigationMetadata({ 1136 previousScreen: currentScreen, 1137 currentScreen, 1138 }) 1139 previousScreen.current = currentScreen 1140 1141 handlePushNotificationEntry() 1142 1143 ax.metric('router:navigate', {}) 1144 1145 if ( 1146 currentAccount && 1147 shouldRequestEmailConfirmation(currentAccount) && 1148 !disableVerifyEmailReminder 1149 ) { 1150 emailDialogControl.open({ 1151 id: EmailDialogScreenID.VerificationReminder, 1152 }) 1153 snoozeEmailConfirmationPrompt() 1154 } 1155 1156 ax.metric('init', { 1157 initMs: Math.round( 1158 // @ts-ignore Emitted by Metro in the bundle prelude 1159 performance.now() - global.__BUNDLE_START_TIME__, 1160 ), 1161 }) 1162 1163 if (IS_WEB) { 1164 const referrerInfo = Referrer.getReferrerInfo() 1165 if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { 1166 ax.metric('deepLink:referrerReceived', { 1167 to: window.location.href, 1168 referrer: referrerInfo?.referrer, 1169 hostname: referrerInfo?.hostname, 1170 }) 1171 } 1172 } 1173 1174 // temp, just testing 1175 void ax.features.enabled(ax.features.AATest) 1176 }) 1177 1178 return ( 1179 <NavigationContainer 1180 ref={navigationRef} 1181 linking={LINKING} 1182 theme={theme} 1183 initialState={initialState} 1184 onStateChange={() => { 1185 const currentScreen = getCurrentRouteName() 1186 // do this before metric 1187 setNavigationMetadata({ 1188 previousScreen: previousScreen.current, 1189 currentScreen, 1190 }) 1191 ax.metric('router:navigate', {from: previousScreen.current}) 1192 previousScreen.current = currentScreen 1193 }} 1194 onReady={onNavigationReady} 1195 // WARNING: Implicit navigation to nested navigators is depreciated in React Navigation 7.x 1196 // However, there's a fair amount of places we do that, especially in when popping to the top of stacks. 1197 // See BottomBar.tsx for an example of how to handle nested navigators in the tabs correctly. 1198 // I'm scared of missing a spot (esp. with push notifications etc) so let's enable this legacy behaviour for now. 1199 // We will need to confirm we handle nested navigators correctly by the time we migrate to React Navigation 8.x 1200 // -sfn 1201 navigationInChildEnabled> 1202 {children} 1203 </NavigationContainer> 1204 ) 1205} 1206 1207function getCurrentRouteName() { 1208 if (navigationRef.isReady()) { 1209 return navigationRef.getCurrentRoute()?.name 1210 } else { 1211 return undefined 1212 } 1213} 1214 1215/** 1216 * These helpers can be used from outside of the RoutesContainer 1217 * (eg in the state models). 1218 */ 1219 1220function navigate<K extends keyof AllNavigatorParams>( 1221 name: K, 1222 params?: AllNavigatorParams[K], 1223) { 1224 if (navigationRef.isReady()) { 1225 return Promise.race([ 1226 new Promise<void>(resolve => { 1227 const handler = () => { 1228 resolve() 1229 navigationRef.removeListener('state', handler) 1230 } 1231 navigationRef.addListener('state', handler) 1232 1233 // @ts-ignore I dont know what would make typescript happy but I have a life -prf 1234 navigationRef.navigate(name, params) 1235 }), 1236 timeout(1e3), 1237 ]) 1238 } 1239 return Promise.resolve() 1240} 1241 1242function resetToTab( 1243 tabName: 'HomeTab' | 'SearchTab' | 'MessagesTab' | 'NotificationsTab', 1244) { 1245 if (navigationRef.isReady()) { 1246 navigate(tabName) 1247 if (navigationRef.canGoBack()) { 1248 navigationRef.dispatch(StackActions.popToTop()) //we need to check .canGoBack() before calling it 1249 } 1250 } 1251} 1252 1253// returns a promise that resolves after the state reset is complete 1254function reset(): Promise<void> { 1255 if (navigationRef.isReady()) { 1256 navigationRef.dispatch( 1257 CommonActions.reset({ 1258 index: 0, 1259 routes: [{name: IS_NATIVE ? 'HomeTab' : 'Home'}], 1260 }), 1261 ) 1262 return Promise.race([ 1263 timeout(1e3), 1264 new Promise<void>(resolve => { 1265 const handler = () => { 1266 resolve() 1267 navigationRef.removeListener('state', handler) 1268 } 1269 navigationRef.addListener('state', handler) 1270 }), 1271 ]) 1272 } else { 1273 return Promise.resolve() 1274 } 1275} 1276 1277export { 1278 FlatNavigator, 1279 navigate, 1280 reset, 1281 resetToTab, 1282 RoutesContainer, 1283 TabsNavigator, 1284}