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 {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}