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