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