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