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}