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