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