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