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