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