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