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