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