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