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