forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {type JSX, useCallback, useRef} from 'react'
2import * as Linking from 'expo-linking'
3import * as Notifications from 'expo-notifications'
4import {i18n, type MessageDescriptor} from '@lingui/core'
5import {msg} from '@lingui/core/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 {useCallOnce} from '#/lib/once'
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 RouteParams,
42 type SearchTabNavigatorParams,
43 type State,
44} from '#/lib/routes/types'
45import {bskyTitle} from '#/lib/strings/headings'
46import {useUnreadNotifications} from '#/state/queries/notifications/unread'
47import {useSession} from '#/state/session'
48import {useLoggedOutViewControls} from '#/state/shell/logged-out'
49import {
50 shouldRequestEmailConfirmation,
51 snoozeEmailConfirmationPrompt,
52} from '#/state/shell/reminders'
53import {useCloseAllActiveElements} from '#/state/util'
54import {CommunityGuidelinesScreen} from '#/view/screens/CommunityGuidelines'
55import {CopyrightPolicyScreen} from '#/view/screens/CopyrightPolicy'
56import {DebugModScreen} from '#/view/screens/DebugMod'
57import {FeedsScreen} from '#/view/screens/Feeds'
58import {HomeScreen} from '#/view/screens/Home'
59import {ListsScreen} from '#/view/screens/Lists'
60import {ModerationBlockedAccounts} from '#/view/screens/ModerationBlockedAccounts'
61import {ModerationModlistsScreen} from '#/view/screens/ModerationModlists'
62import {ModerationMutedAccounts} from '#/view/screens/ModerationMutedAccounts'
63import {NotFoundScreen} from '#/view/screens/NotFound'
64import {NotificationsScreen} from '#/view/screens/Notifications'
65import {PostThreadScreen} from '#/view/screens/PostThread'
66import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
67import {ProfileScreen} from '#/view/screens/Profile'
68import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy'
69import {StorybookScreen} from '#/view/screens/Storybook'
70import {SupportScreen} from '#/view/screens/Support'
71import {TermsOfServiceScreen} from '#/view/screens/TermsOfService'
72import {BottomBar} from '#/view/shell/bottom-bar/BottomBar'
73import {createNativeStackNavigatorWithAuth} from '#/view/shell/createNativeStackNavigatorWithAuth'
74import {BookmarksScreen} from '#/screens/Bookmarks'
75import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen'
76import {FindContactsFlowScreen} from '#/screens/FindContactsFlowScreen'
77import HashtagScreen from '#/screens/Hashtag'
78import {LogScreen} from '#/screens/Log'
79import {MessagesScreen} from '#/screens/Messages/ChatList'
80import {MessagesConversationScreen} from '#/screens/Messages/Conversation'
81import {MessagesInboxScreen} from '#/screens/Messages/Inbox'
82import {MessagesSettingsScreen} from '#/screens/Messages/Settings'
83import {ModerationScreen} from '#/screens/Moderation'
84import {Screen as ModerationVerificationSettings} from '#/screens/Moderation/VerificationSettings'
85import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings'
86import {NotificationsActivityListScreen} from '#/screens/Notifications/ActivityList'
87import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
88import {PostQuotesScreen} from '#/screens/Post/PostQuotes'
89import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
90import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
91import {ProfileFeedScreen} from '#/screens/Profile/ProfileFeed'
92import {ProfileFollowersScreen} from '#/screens/Profile/ProfileFollowers'
93import {ProfileFollowsScreen} from '#/screens/Profile/ProfileFollows'
94import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
95import {ProfileSearchScreen} from '#/screens/Profile/ProfileSearch'
96import {ProfileListScreen} from '#/screens/ProfileList'
97import {SavedFeeds} from '#/screens/SavedFeeds'
98import {SearchScreen} from '#/screens/Search'
99import {AboutSettingsScreen} from '#/screens/Settings/AboutSettings'
100import {AccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings'
101import {AccountSettingsScreen} from '#/screens/Settings/AccountSettings'
102import {ActivityPrivacySettingsScreen} from '#/screens/Settings/ActivityPrivacySettings'
103import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
104import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
105import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords'
106import {ContentAndMediaSettingsScreen} from '#/screens/Settings/ContentAndMediaSettings'
107import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences'
108import {FindContactsSettingsScreen} from '#/screens/Settings/FindContactsSettings'
109import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences'
110import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings'
111import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings'
112import {LegacyNotificationSettingsScreen} from '#/screens/Settings/LegacyNotificationSettings'
113import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
114import {ActivityNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ActivityNotificationSettings'
115import {LikeNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/LikeNotificationSettings'
116import {LikesOnRepostsNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings'
117import {MentionNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/MentionNotificationSettings'
118import {MiscellaneousNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings'
119import {NewFollowerNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/NewFollowerNotificationSettings'
120import {QuoteNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/QuoteNotificationSettings'
121import {ReplyNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ReplyNotificationSettings'
122import {RepostNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/RepostNotificationSettings'
123import {RepostsOnRepostsNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings'
124import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings'
125import {SettingsScreen} from '#/screens/Settings/Settings'
126import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences'
127import {
128 StarterPackScreen,
129 StarterPackScreenShort,
130} from '#/screens/StarterPack/StarterPackScreen'
131import {Wizard} from '#/screens/StarterPack/Wizard'
132import TopicScreen from '#/screens/Topic'
133import {VideoFeed} from '#/screens/VideoFeed'
134import {type Theme, useTheme} from '#/alf'
135import {
136 EmailDialogScreenID,
137 useEmailDialogControl,
138} from '#/components/dialogs/EmailDialog'
139import {useAnalytics} from '#/analytics'
140import {setNavigationMetadata} from '#/analytics/metadata'
141import {IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
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={() => StorybookScreen}
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({
641 layout,
642}: {
643 layout: React.ComponentProps<typeof Tab.Navigator>['layout']
644}) {
645 const tabBar = useCallback(
646 (props: JSX.IntrinsicAttributes & BottomTabBarProps) => (
647 <BottomBar {...props} />
648 ),
649 [],
650 )
651
652 return (
653 <Tab.Navigator
654 initialRouteName="HomeTab"
655 backBehavior="initialRoute"
656 screenOptions={{headerShown: false, lazy: true}}
657 tabBar={tabBar}
658 layout={layout}>
659 <Tab.Screen name="HomeTab" getComponent={() => HomeTabNavigator} />
660 <Tab.Screen name="SearchTab" getComponent={() => SearchTabNavigator} />
661 <Tab.Screen
662 name="MessagesTab"
663 getComponent={() => MessagesTabNavigator}
664 />
665 <Tab.Screen
666 name="NotificationsTab"
667 getComponent={() => NotificationsTabNavigator}
668 />
669 <Tab.Screen
670 name="MyProfileTab"
671 getComponent={() => MyProfileTabNavigator}
672 />
673 </Tab.Navigator>
674 )
675}
676
677function screenOptions(t: Theme) {
678 return {
679 fullScreenGestureEnabled: true,
680 headerShown: false,
681 contentStyle: t.atoms.bg,
682 } as const
683}
684
685function HomeTabNavigator() {
686 const t = useTheme()
687
688 const BLURRED_SCROLL_EDGE_EFFECT = IS_LIQUID_GLASS
689 ? ({
690 headerShown: true,
691 headerTransparent: true,
692 headerTitle: '',
693 scrollEdgeEffects: {
694 top: 'soft',
695 },
696 } as const)
697 : {}
698
699 return (
700 <HomeTab.Navigator screenOptions={screenOptions(t)} initialRouteName="Home">
701 <HomeTab.Screen
702 name="Home"
703 getComponent={() => HomeScreen}
704 options={BLURRED_SCROLL_EDGE_EFFECT}
705 />
706 <HomeTab.Screen
707 name="Start"
708 getComponent={() => HomeScreen}
709 options={BLURRED_SCROLL_EDGE_EFFECT}
710 />
711 {commonScreens(HomeTab as typeof Flat)}
712 </HomeTab.Navigator>
713 )
714}
715
716function SearchTabNavigator() {
717 const t = useTheme()
718 return (
719 <SearchTab.Navigator
720 screenOptions={screenOptions(t)}
721 initialRouteName="Search">
722 <SearchTab.Screen name="Search" getComponent={() => SearchScreen} />
723 {commonScreens(SearchTab as typeof Flat)}
724 </SearchTab.Navigator>
725 )
726}
727
728function NotificationsTabNavigator() {
729 const t = useTheme()
730 return (
731 <NotificationsTab.Navigator
732 screenOptions={screenOptions(t)}
733 initialRouteName="Notifications">
734 <NotificationsTab.Screen
735 name="Notifications"
736 getComponent={() => NotificationsScreen}
737 options={{requireAuth: true}}
738 />
739 {commonScreens(NotificationsTab as typeof Flat)}
740 </NotificationsTab.Navigator>
741 )
742}
743
744function MyProfileTabNavigator() {
745 const t = useTheme()
746 return (
747 <MyProfileTab.Navigator
748 screenOptions={screenOptions(t)}
749 initialRouteName="MyProfile">
750 <MyProfileTab.Screen
751 // MyProfile is not in AllNavigationParams - asserting as Profile at least
752 // gives us typechecking for initialParams -sfn
753 name={'MyProfile' as 'Profile'}
754 getComponent={() => ProfileScreen}
755 initialParams={{name: 'me', hideBackButton: true}}
756 />
757 {commonScreens(MyProfileTab as unknown as typeof Flat)}
758 </MyProfileTab.Navigator>
759 )
760}
761
762function MessagesTabNavigator() {
763 const t = useTheme()
764 return (
765 <MessagesTab.Navigator
766 screenOptions={screenOptions(t)}
767 initialRouteName="Messages">
768 <MessagesTab.Screen
769 name="Messages"
770 getComponent={() => MessagesScreen}
771 options={({route}) => ({
772 requireAuth: true,
773 animationTypeForReplace: route.params?.animation ?? 'push',
774 })}
775 />
776 {commonScreens(MessagesTab as typeof Flat)}
777 </MessagesTab.Navigator>
778 )
779}
780
781/**
782 * The FlatNavigator is used by Web to represent the routes
783 * in a single ("flat") stack.
784 */
785const FlatNavigator = ({
786 layout,
787}: {
788 layout: React.ComponentProps<typeof Flat.Navigator>['layout']
789}) => {
790 const t = useTheme()
791 const numUnread = useUnreadNotifications()
792 const screenListeners = useWebScrollRestoration()
793 const title = (page: MessageDescriptor) => bskyTitle(i18n._(page), numUnread)
794
795 return (
796 <Flat.Navigator
797 layout={layout}
798 screenListeners={screenListeners}
799 screenOptions={screenOptions(t)}>
800 <Flat.Screen
801 name="Home"
802 getComponent={() => HomeScreen}
803 options={{title: title(msg`Home`)}}
804 />
805 <Flat.Screen
806 name="Search"
807 getComponent={() => SearchScreen}
808 options={{title: title(msg`Explore`)}}
809 />
810 <Flat.Screen
811 name="Notifications"
812 getComponent={() => NotificationsScreen}
813 options={{title: title(msg`Notifications`), requireAuth: true}}
814 />
815 <Flat.Screen
816 name="Messages"
817 getComponent={() => MessagesScreen}
818 options={{title: title(msg`Messages`), requireAuth: true}}
819 />
820 <Flat.Screen
821 name="Start"
822 getComponent={() => HomeScreen}
823 options={{title: title(msg`Home`)}}
824 />
825 {commonScreens(Flat, numUnread)}
826 </Flat.Navigator>
827 )
828}
829
830/**
831 * The RoutesContainer should wrap all components which need access
832 * to the navigation context.
833 */
834
835const LINKING = {
836 // TODO figure out what we are going to use
837 // note: `bluesky://` is what is used in app.config.js
838 prefixes: ['bsky://', 'bluesky://', 'https://bsky.app'],
839
840 getPathFromState(state: State) {
841 // find the current node in the navigation tree
842 let node = state.routes[state.index || 0]
843 while (node.state?.routes && typeof node.state?.index === 'number') {
844 node = node.state?.routes[node.state?.index]
845 }
846
847 // build the path
848 const route = router.matchName(node.name)
849 if (typeof route === 'undefined') {
850 return '/' // default to home
851 }
852 return route.build((node.params || {}) as RouteParams)
853 },
854
855 getStateFromPath(path: string) {
856 const [name, params] = router.matchPath(path)
857
858 // Any time we receive a url that starts with `intent/` we want to ignore it here. It will be handled in the
859 // intent handler hook. We should check for the trailing slash, because if there isn't one then it isn't a valid
860 // intent
861 // On web, there is no route state that's created by default, so we should initialize it as the home route. On
862 // native, since the home tab and the home screen are defined as initial routes, we don't need to return a state
863 // since it will be created by react-navigation.
864 if (path.includes('intent/')) {
865 if (IS_NATIVE) return
866 return buildStateObject('Flat', 'Home', params)
867 }
868
869 if (IS_NATIVE) {
870 if (name === 'Search') {
871 return buildStateObject('SearchTab', 'Search', params)
872 }
873 if (name === 'Notifications') {
874 return buildStateObject('NotificationsTab', 'Notifications', params)
875 }
876 if (name === 'Home') {
877 return buildStateObject('HomeTab', 'Home', params)
878 }
879 if (name === 'Messages') {
880 return buildStateObject('MessagesTab', 'Messages', params)
881 }
882 // 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
883 return buildStateObject('HomeTab', name, params, [
884 {
885 name: 'Home',
886 params: {},
887 },
888 ])
889 } else {
890 const res = buildStateObject('Flat', name, params)
891 return res
892 }
893 },
894} satisfies LinkingOptions<AllNavigatorParams>
895
896function RoutesContainer({children}: React.PropsWithChildren<{}>) {
897 const ax = useAnalytics()
898 const notyLogger = ax.logger.useChild(ax.logger.Context.Notifications)
899 const theme = useColorSchemeStyle(DefaultTheme, DarkTheme)
900 const {currentAccount, accounts} = useSession()
901 const {onPressSwitchAccount} = useAccountSwitcher()
902 const {setShowLoggedOut} = useLoggedOutViewControls()
903 const previousScreen = useRef<string | undefined>(undefined)
904 const emailDialogControl = useEmailDialogControl()
905 const closeAllActiveElements = useCloseAllActiveElements()
906 const linkingUrl = Linking.useLinkingURL()
907
908 /**
909 * Handle navigation to a conversation, or prepares for account switch.
910 *
911 * Non-reactive because we need the latest data from some hooks
912 * after an async call - sfn
913 */
914 const handleChatMessage = useNonReactiveCallback(
915 (payload: Extract<NotificationPayload, {reason: 'chat-message'}>) => {
916 notyLogger.debug(`handleChatMessage`, {payload})
917
918 if (payload.recipientDid !== currentAccount?.did) {
919 // handled in useNotificationHandler after account switch finishes
920 storePayloadForAccountSwitch(payload)
921 closeAllActiveElements()
922
923 const account = accounts.find(a => a.did === payload.recipientDid)
924 if (account) {
925 onPressSwitchAccount(account, 'Notification')
926 } else {
927 setShowLoggedOut(true)
928 }
929 } else {
930 // @ts-expect-error nested navigators aren't typed -sfn
931 navigate('MessagesTab', {
932 screen: 'Messages',
933 params: {
934 pushToConversation: payload.convoId,
935 },
936 })
937 }
938 },
939 )
940
941 function handlePushNotificationEntry() {
942 if (!IS_NATIVE) return
943
944 // intent urls are handled by `useIntentHandler`
945 if (linkingUrl) return
946
947 const notificationResponse = Notifications.getLastNotificationResponse()
948
949 if (notificationResponse) {
950 notyLogger.debug(`handlePushNotificationEntry: response`, {
951 response: notificationResponse,
952 })
953
954 // Clear the last notification response to ensure it's not used again
955 try {
956 Notifications.clearLastNotificationResponse()
957 } catch (error) {
958 notyLogger.error(
959 `handlePushNotificationEntry: error clearing notification response`,
960 {error},
961 )
962 }
963
964 const payload = getNotificationPayload(notificationResponse.notification)
965
966 if (payload) {
967 ax.metric('notifications:openApp', {
968 reason: payload.reason,
969 causedBoot: true,
970 })
971
972 if (payload.reason === 'chat-message') {
973 handleChatMessage(payload)
974 } else {
975 const path = notificationToURL(payload)
976
977 if (path === '/notifications') {
978 resetToTab('NotificationsTab')
979 notyLogger.debug(`handlePushNotificationEntry: default navigate`)
980 } else if (path) {
981 const [screen, params] = router.matchPath(path)
982 // @ts-expect-error nested navigators aren't typed -sfn
983 navigate('HomeTab', {screen, params})
984 notyLogger.debug(`handlePushNotificationEntry: navigate`, {
985 screen,
986 params,
987 })
988 }
989 }
990 }
991 }
992 }
993
994 const onNavigationReady = useCallOnce(() => {
995 const currentScreen = getCurrentRouteName()
996 setNavigationMetadata({
997 previousScreen: currentScreen,
998 currentScreen,
999 })
1000 previousScreen.current = currentScreen
1001
1002 handlePushNotificationEntry()
1003
1004 ax.metric('router:navigate', {})
1005
1006 if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) {
1007 emailDialogControl.open({
1008 id: EmailDialogScreenID.VerificationReminder,
1009 })
1010 snoozeEmailConfirmationPrompt()
1011 }
1012
1013 ax.metric('init', {
1014 initMs: Math.round(
1015 // @ts-ignore Emitted by Metro in the bundle prelude
1016 performance.now() - global.__BUNDLE_START_TIME__,
1017 ),
1018 })
1019
1020 if (IS_WEB) {
1021 const referrerInfo = Referrer.getReferrerInfo()
1022 if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
1023 ax.metric('deepLink:referrerReceived', {
1024 to: window.location.href,
1025 referrer: referrerInfo?.referrer,
1026 hostname: referrerInfo?.hostname,
1027 })
1028 }
1029 }
1030
1031 // temp, just testing
1032 void ax.features.enabled(ax.features.AATest)
1033 })
1034
1035 return (
1036 <NavigationContainer
1037 ref={navigationRef}
1038 linking={LINKING}
1039 theme={theme}
1040 onStateChange={() => {
1041 const currentScreen = getCurrentRouteName()
1042 // do this before metric
1043 setNavigationMetadata({
1044 previousScreen: previousScreen.current,
1045 currentScreen,
1046 })
1047 ax.metric('router:navigate', {from: previousScreen.current})
1048 previousScreen.current = currentScreen
1049 }}
1050 onReady={onNavigationReady}
1051 // WARNING: Implicit navigation to nested navigators is depreciated in React Navigation 7.x
1052 // However, there's a fair amount of places we do that, especially in when popping to the top of stacks.
1053 // See BottomBar.tsx for an example of how to handle nested navigators in the tabs correctly.
1054 // I'm scared of missing a spot (esp. with push notifications etc) so let's enable this legacy behaviour for now.
1055 // We will need to confirm we handle nested navigators correctly by the time we migrate to React Navigation 8.x
1056 // -sfn
1057 navigationInChildEnabled>
1058 {children}
1059 </NavigationContainer>
1060 )
1061}
1062
1063function getCurrentRouteName() {
1064 if (navigationRef.isReady()) {
1065 return navigationRef.getCurrentRoute()?.name
1066 } else {
1067 return undefined
1068 }
1069}
1070
1071/**
1072 * These helpers can be used from outside of the RoutesContainer
1073 * (eg in the state models).
1074 */
1075
1076function navigate<K extends keyof AllNavigatorParams>(
1077 name: K,
1078 params?: AllNavigatorParams[K],
1079) {
1080 if (navigationRef.isReady()) {
1081 return Promise.race([
1082 new Promise<void>(resolve => {
1083 const handler = () => {
1084 resolve()
1085 navigationRef.removeListener('state', handler)
1086 }
1087 navigationRef.addListener('state', handler)
1088
1089 // @ts-ignore I dont know what would make typescript happy but I have a life -prf
1090 navigationRef.navigate(name, params)
1091 }),
1092 timeout(1e3),
1093 ])
1094 }
1095 return Promise.resolve()
1096}
1097
1098function resetToTab(
1099 tabName: 'HomeTab' | 'SearchTab' | 'MessagesTab' | 'NotificationsTab',
1100) {
1101 if (navigationRef.isReady()) {
1102 navigate(tabName)
1103 if (navigationRef.canGoBack()) {
1104 navigationRef.dispatch(StackActions.popToTop()) //we need to check .canGoBack() before calling it
1105 }
1106 }
1107}
1108
1109// returns a promise that resolves after the state reset is complete
1110function reset(): Promise<void> {
1111 if (navigationRef.isReady()) {
1112 navigationRef.dispatch(
1113 CommonActions.reset({
1114 index: 0,
1115 routes: [{name: IS_NATIVE ? 'HomeTab' : 'Home'}],
1116 }),
1117 )
1118 return Promise.race([
1119 timeout(1e3),
1120 new Promise<void>(resolve => {
1121 const handler = () => {
1122 resolve()
1123 navigationRef.removeListener('state', handler)
1124 }
1125 navigationRef.addListener('state', handler)
1126 }),
1127 ])
1128 } else {
1129 return Promise.resolve()
1130 }
1131}
1132
1133export {
1134 FlatNavigator,
1135 navigate,
1136 reset,
1137 resetToTab,
1138 RoutesContainer,
1139 TabsNavigator,
1140}