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