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