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