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