Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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