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