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