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