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