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