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

Configure Feed

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

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