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