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