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