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