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