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