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