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