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