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

Configure Feed

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

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