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

Configure Feed

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

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