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