mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at ruby-v 862 lines 28 kB view raw
1import * as React from 'react' 2import {JSX} from 'react/jsx-runtime' 3import {i18n, MessageDescriptor} from '@lingui/core' 4import {msg} from '@lingui/macro' 5import { 6 BottomTabBarProps, 7 createBottomTabNavigator, 8} from '@react-navigation/bottom-tabs' 9import { 10 CommonActions, 11 createNavigationContainerRef, 12 DarkTheme, 13 DefaultTheme, 14 NavigationContainer, 15 StackActions, 16} from '@react-navigation/native' 17 18import {timeout} from '#/lib/async/timeout' 19import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' 20import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration' 21import {buildStateObject} from '#/lib/routes/helpers' 22import { 23 AllNavigatorParams, 24 BottomTabNavigatorParams, 25 FlatNavigatorParams, 26 HomeTabNavigatorParams, 27 MessagesTabNavigatorParams, 28 MyProfileTabNavigatorParams, 29 NotificationsTabNavigatorParams, 30 SearchTabNavigatorParams, 31} from '#/lib/routes/types' 32import {RouteParams, State} from '#/lib/routes/types' 33import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig' 34import {bskyTitle} from '#/lib/strings/headings' 35import {isNative, isWeb} from '#/platform/detection' 36import {useModalControls} from '#/state/modals' 37import {useUnreadNotifications} from '#/state/queries/notifications/unread' 38import {useSession} from '#/state/session' 39import { 40 shouldRequestEmailConfirmation, 41 snoozeEmailConfirmationPrompt, 42} from '#/state/shell/reminders' 43import {CommunityGuidelinesScreen} from '#/view/screens/CommunityGuidelines' 44import {CopyrightPolicyScreen} from '#/view/screens/CopyrightPolicy' 45import {DebugModScreen} from '#/view/screens/DebugMod' 46import {FeedsScreen} from '#/view/screens/Feeds' 47import {HomeScreen} from '#/view/screens/Home' 48import {ListsScreen} from '#/view/screens/Lists' 49import {LogScreen} from '#/view/screens/Log' 50import {ModerationBlockedAccounts} from '#/view/screens/ModerationBlockedAccounts' 51import {ModerationModlistsScreen} from '#/view/screens/ModerationModlists' 52import {ModerationMutedAccounts} from '#/view/screens/ModerationMutedAccounts' 53import {NotFoundScreen} from '#/view/screens/NotFound' 54import {NotificationsScreen} from '#/view/screens/Notifications' 55import {PostThreadScreen} from '#/view/screens/PostThread' 56import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy' 57import {ProfileScreen} from '#/view/screens/Profile' 58import {ProfileFeedScreen} from '#/view/screens/ProfileFeed' 59import {ProfileFeedLikedByScreen} from '#/view/screens/ProfileFeedLikedBy' 60import {ProfileFollowersScreen} from '#/view/screens/ProfileFollowers' 61import {ProfileFollowsScreen} from '#/view/screens/ProfileFollows' 62import {ProfileListScreen} from '#/view/screens/ProfileList' 63import {SavedFeeds} from '#/view/screens/SavedFeeds' 64import {SearchScreen} from '#/view/screens/Search' 65import {Storybook} from '#/view/screens/Storybook' 66import {SupportScreen} from '#/view/screens/Support' 67import {TermsOfServiceScreen} from '#/view/screens/TermsOfService' 68import {BottomBar} from '#/view/shell/bottom-bar/BottomBar' 69import {createNativeStackNavigatorWithAuth} from '#/view/shell/createNativeStackNavigatorWithAuth' 70import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen' 71import HashtagScreen from '#/screens/Hashtag' 72import {MessagesScreen} from '#/screens/Messages/ChatList' 73import {MessagesConversationScreen} from '#/screens/Messages/Conversation' 74import {MessagesSettingsScreen} from '#/screens/Messages/Settings' 75import {ModerationScreen} from '#/screens/Moderation' 76import {PostLikedByScreen} from '#/screens/Post/PostLikedBy' 77import {PostQuotesScreen} from '#/screens/Post/PostQuotes' 78import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy' 79import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers' 80import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy' 81import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings' 82import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings' 83import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings' 84import { 85 StarterPackScreen, 86 StarterPackScreenShort, 87} from '#/screens/StarterPack/StarterPackScreen' 88import {Wizard} from '#/screens/StarterPack/Wizard' 89import {useTheme} from '#/alf' 90import {router} from '#/routes' 91import {Referrer} from '../modules/expo-bluesky-swiss-army' 92import {AboutSettingsScreen} from './screens/Settings/AboutSettings' 93import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings' 94import {AccountSettingsScreen} from './screens/Settings/AccountSettings' 95import {AppPasswordsScreen} from './screens/Settings/AppPasswords' 96import {ContentAndMediaSettingsScreen} from './screens/Settings/ContentAndMediaSettings' 97import {ExternalMediaPreferencesScreen} from './screens/Settings/ExternalMediaPreferences' 98import {FollowingFeedPreferencesScreen} from './screens/Settings/FollowingFeedPreferences' 99import {LanguageSettingsScreen} from './screens/Settings/LanguageSettings' 100import {PrivacyAndSecuritySettingsScreen} from './screens/Settings/PrivacyAndSecuritySettings' 101import {SettingsScreen} from './screens/Settings/Settings' 102import {ThreadPreferencesScreen} from './screens/Settings/ThreadPreferences' 103 104const navigationRef = createNavigationContainerRef<AllNavigatorParams>() 105 106const HomeTab = createNativeStackNavigatorWithAuth<HomeTabNavigatorParams>() 107const SearchTab = createNativeStackNavigatorWithAuth<SearchTabNavigatorParams>() 108const NotificationsTab = 109 createNativeStackNavigatorWithAuth<NotificationsTabNavigatorParams>() 110const MyProfileTab = 111 createNativeStackNavigatorWithAuth<MyProfileTabNavigatorParams>() 112const MessagesTab = 113 createNativeStackNavigatorWithAuth<MessagesTabNavigatorParams>() 114const Flat = createNativeStackNavigatorWithAuth<FlatNavigatorParams>() 115const Tab = createBottomTabNavigator<BottomTabNavigatorParams>() 116 117/** 118 * These "common screens" are reused across stacks. 119 */ 120function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { 121 const title = (page: MessageDescriptor) => 122 bskyTitle(i18n._(page), unreadCountLabel) 123 124 return ( 125 <> 126 <Stack.Screen 127 name="NotFound" 128 getComponent={() => NotFoundScreen} 129 options={{title: title(msg`Not Found`)}} 130 /> 131 <Stack.Screen 132 name="Lists" 133 component={ListsScreen} 134 options={{title: title(msg`Lists`), requireAuth: true}} 135 /> 136 <Stack.Screen 137 name="Moderation" 138 getComponent={() => ModerationScreen} 139 options={{title: title(msg`Moderation`), requireAuth: true}} 140 /> 141 <Stack.Screen 142 name="ModerationModlists" 143 getComponent={() => ModerationModlistsScreen} 144 options={{title: title(msg`Moderation Lists`), requireAuth: true}} 145 /> 146 <Stack.Screen 147 name="ModerationMutedAccounts" 148 getComponent={() => ModerationMutedAccounts} 149 options={{title: title(msg`Muted Accounts`), requireAuth: true}} 150 /> 151 <Stack.Screen 152 name="ModerationBlockedAccounts" 153 getComponent={() => ModerationBlockedAccounts} 154 options={{title: title(msg`Blocked Accounts`), requireAuth: true}} 155 /> 156 <Stack.Screen 157 name="Settings" 158 getComponent={() => SettingsScreen} 159 options={{title: title(msg`Settings`), requireAuth: true}} 160 /> 161 <Stack.Screen 162 name="LanguageSettings" 163 getComponent={() => LanguageSettingsScreen} 164 options={{title: title(msg`Language Settings`), requireAuth: true}} 165 /> 166 <Stack.Screen 167 name="Profile" 168 getComponent={() => ProfileScreen} 169 options={({route}) => ({ 170 title: bskyTitle(`@${route.params.name}`, unreadCountLabel), 171 })} 172 /> 173 <Stack.Screen 174 name="ProfileFollowers" 175 getComponent={() => ProfileFollowersScreen} 176 options={({route}) => ({ 177 title: title(msg`People following @${route.params.name}`), 178 })} 179 /> 180 <Stack.Screen 181 name="ProfileFollows" 182 getComponent={() => ProfileFollowsScreen} 183 options={({route}) => ({ 184 title: title(msg`People followed by @${route.params.name}`), 185 })} 186 /> 187 <Stack.Screen 188 name="ProfileKnownFollowers" 189 getComponent={() => ProfileKnownFollowersScreen} 190 options={({route}) => ({ 191 title: title(msg`Followers of @${route.params.name} that you know`), 192 })} 193 /> 194 <Stack.Screen 195 name="ProfileList" 196 getComponent={() => ProfileListScreen} 197 options={{title: title(msg`List`), requireAuth: true}} 198 /> 199 <Stack.Screen 200 name="PostThread" 201 getComponent={() => PostThreadScreen} 202 options={({route}) => ({ 203 title: title(msg`Post by @${route.params.name}`), 204 })} 205 /> 206 <Stack.Screen 207 name="PostLikedBy" 208 getComponent={() => PostLikedByScreen} 209 options={({route}) => ({ 210 title: title(msg`Post by @${route.params.name}`), 211 })} 212 /> 213 <Stack.Screen 214 name="PostRepostedBy" 215 getComponent={() => PostRepostedByScreen} 216 options={({route}) => ({ 217 title: title(msg`Post by @${route.params.name}`), 218 })} 219 /> 220 <Stack.Screen 221 name="PostQuotes" 222 getComponent={() => PostQuotesScreen} 223 options={({route}) => ({ 224 title: title(msg`Post by @${route.params.name}`), 225 })} 226 /> 227 <Stack.Screen 228 name="ProfileFeed" 229 getComponent={() => ProfileFeedScreen} 230 options={{title: title(msg`Feed`)}} 231 /> 232 <Stack.Screen 233 name="ProfileFeedLikedBy" 234 getComponent={() => ProfileFeedLikedByScreen} 235 options={{title: title(msg`Liked by`)}} 236 /> 237 <Stack.Screen 238 name="ProfileLabelerLikedBy" 239 getComponent={() => ProfileLabelerLikedByScreen} 240 options={{title: title(msg`Liked by`)}} 241 /> 242 <Stack.Screen 243 name="Debug" 244 getComponent={() => Storybook} 245 options={{title: title(msg`Storybook`), requireAuth: true}} 246 /> 247 <Stack.Screen 248 name="DebugMod" 249 getComponent={() => DebugModScreen} 250 options={{title: title(msg`Moderation states`), requireAuth: true}} 251 /> 252 <Stack.Screen 253 name="SharedPreferencesTester" 254 getComponent={() => SharedPreferencesTesterScreen} 255 options={{title: title(msg`Shared Preferences Tester`)}} 256 /> 257 <Stack.Screen 258 name="Log" 259 getComponent={() => LogScreen} 260 options={{title: title(msg`Log`), requireAuth: true}} 261 /> 262 <Stack.Screen 263 name="Support" 264 getComponent={() => SupportScreen} 265 options={{title: title(msg`Support`)}} 266 /> 267 <Stack.Screen 268 name="PrivacyPolicy" 269 getComponent={() => PrivacyPolicyScreen} 270 options={{title: title(msg`Privacy Policy`)}} 271 /> 272 <Stack.Screen 273 name="TermsOfService" 274 getComponent={() => TermsOfServiceScreen} 275 options={{title: title(msg`Terms of Service`)}} 276 /> 277 <Stack.Screen 278 name="CommunityGuidelines" 279 getComponent={() => CommunityGuidelinesScreen} 280 options={{title: title(msg`Community Guidelines`)}} 281 /> 282 <Stack.Screen 283 name="CopyrightPolicy" 284 getComponent={() => CopyrightPolicyScreen} 285 options={{title: title(msg`Copyright Policy`)}} 286 /> 287 <Stack.Screen 288 name="AppPasswords" 289 getComponent={() => AppPasswordsScreen} 290 options={{title: title(msg`App Passwords`), requireAuth: true}} 291 /> 292 <Stack.Screen 293 name="SavedFeeds" 294 getComponent={() => SavedFeeds} 295 options={{title: title(msg`Edit My Feeds`), requireAuth: true}} 296 /> 297 <Stack.Screen 298 name="PreferencesFollowingFeed" 299 getComponent={() => FollowingFeedPreferencesScreen} 300 options={{ 301 title: title(msg`Following Feed Preferences`), 302 requireAuth: true, 303 }} 304 /> 305 <Stack.Screen 306 name="PreferencesThreads" 307 getComponent={() => ThreadPreferencesScreen} 308 options={{title: title(msg`Threads Preferences`), requireAuth: true}} 309 /> 310 <Stack.Screen 311 name="PreferencesExternalEmbeds" 312 getComponent={() => ExternalMediaPreferencesScreen} 313 options={{ 314 title: title(msg`External Media Preferences`), 315 requireAuth: true, 316 }} 317 /> 318 <Stack.Screen 319 name="AccessibilitySettings" 320 getComponent={() => AccessibilitySettingsScreen} 321 options={{ 322 title: title(msg`Accessibility Settings`), 323 requireAuth: true, 324 }} 325 /> 326 <Stack.Screen 327 name="AppearanceSettings" 328 getComponent={() => AppearanceSettingsScreen} 329 options={{ 330 title: title(msg`Appearance`), 331 requireAuth: true, 332 }} 333 /> 334 <Stack.Screen 335 name="AccountSettings" 336 getComponent={() => AccountSettingsScreen} 337 options={{ 338 title: title(msg`Account`), 339 requireAuth: true, 340 }} 341 /> 342 <Stack.Screen 343 name="PrivacyAndSecuritySettings" 344 getComponent={() => PrivacyAndSecuritySettingsScreen} 345 options={{ 346 title: title(msg`Privacy and Security`), 347 requireAuth: true, 348 }} 349 /> 350 <Stack.Screen 351 name="ContentAndMediaSettings" 352 getComponent={() => ContentAndMediaSettingsScreen} 353 options={{ 354 title: title(msg`Content and Media`), 355 requireAuth: true, 356 }} 357 /> 358 <Stack.Screen 359 name="AboutSettings" 360 getComponent={() => AboutSettingsScreen} 361 options={{ 362 title: title(msg`About`), 363 requireAuth: true, 364 }} 365 /> 366 <Stack.Screen 367 name="AppIconSettings" 368 getComponent={() => AppIconSettingsScreen} 369 options={{ 370 title: title(msg`App Icon`), 371 requireAuth: true, 372 }} 373 /> 374 <Stack.Screen 375 name="Hashtag" 376 getComponent={() => HashtagScreen} 377 options={{title: title(msg`Hashtag`)}} 378 /> 379 <Stack.Screen 380 name="MessagesConversation" 381 getComponent={() => MessagesConversationScreen} 382 options={{title: title(msg`Chat`), requireAuth: true}} 383 /> 384 <Stack.Screen 385 name="MessagesSettings" 386 getComponent={() => MessagesSettingsScreen} 387 options={{title: title(msg`Chat settings`), requireAuth: true}} 388 /> 389 <Stack.Screen 390 name="NotificationSettings" 391 getComponent={() => NotificationSettingsScreen} 392 options={{title: title(msg`Notification settings`), requireAuth: true}} 393 /> 394 <Stack.Screen 395 name="Feeds" 396 getComponent={() => FeedsScreen} 397 options={{title: title(msg`Feeds`)}} 398 /> 399 <Stack.Screen 400 name="StarterPack" 401 getComponent={() => StarterPackScreen} 402 options={{title: title(msg`Starter Pack`)}} 403 /> 404 <Stack.Screen 405 name="StarterPackShort" 406 getComponent={() => StarterPackScreenShort} 407 options={{title: title(msg`Starter Pack`)}} 408 /> 409 <Stack.Screen 410 name="StarterPackWizard" 411 getComponent={() => Wizard} 412 options={{title: title(msg`Create a starter pack`), requireAuth: true}} 413 /> 414 <Stack.Screen 415 name="StarterPackEdit" 416 getComponent={() => Wizard} 417 options={{title: title(msg`Edit your starter pack`), requireAuth: true}} 418 /> 419 </> 420 ) 421} 422 423/** 424 * The TabsNavigator is used by native mobile to represent the routes 425 * in 3 distinct tab-stacks with a different root screen on each. 426 */ 427function TabsNavigator() { 428 const tabBar = React.useCallback( 429 (props: JSX.IntrinsicAttributes & BottomTabBarProps) => ( 430 <BottomBar {...props} /> 431 ), 432 [], 433 ) 434 435 return ( 436 <Tab.Navigator 437 initialRouteName="HomeTab" 438 backBehavior="initialRoute" 439 screenOptions={{headerShown: false, lazy: true}} 440 tabBar={tabBar}> 441 <Tab.Screen name="HomeTab" getComponent={() => HomeTabNavigator} /> 442 <Tab.Screen name="SearchTab" getComponent={() => SearchTabNavigator} /> 443 <Tab.Screen 444 name="NotificationsTab" 445 getComponent={() => NotificationsTabNavigator} 446 /> 447 <Tab.Screen 448 name="MyProfileTab" 449 getComponent={() => MyProfileTabNavigator} 450 /> 451 <Tab.Screen 452 name="MessagesTab" 453 getComponent={() => MessagesTabNavigator} 454 /> 455 </Tab.Navigator> 456 ) 457} 458 459function HomeTabNavigator() { 460 const t = useTheme() 461 462 return ( 463 <HomeTab.Navigator 464 screenOptions={{ 465 animationDuration: 285, 466 gestureEnabled: true, 467 fullScreenGestureEnabled: true, 468 headerShown: false, 469 contentStyle: t.atoms.bg, 470 }}> 471 <HomeTab.Screen name="Home" getComponent={() => HomeScreen} /> 472 <HomeTab.Screen name="Start" getComponent={() => HomeScreen} /> 473 {commonScreens(HomeTab)} 474 </HomeTab.Navigator> 475 ) 476} 477 478function SearchTabNavigator() { 479 const t = useTheme() 480 return ( 481 <SearchTab.Navigator 482 screenOptions={{ 483 animationDuration: 285, 484 gestureEnabled: true, 485 fullScreenGestureEnabled: true, 486 headerShown: false, 487 contentStyle: t.atoms.bg, 488 }}> 489 <SearchTab.Screen name="Search" getComponent={() => SearchScreen} /> 490 {commonScreens(SearchTab as typeof HomeTab)} 491 </SearchTab.Navigator> 492 ) 493} 494 495function NotificationsTabNavigator() { 496 const t = useTheme() 497 return ( 498 <NotificationsTab.Navigator 499 screenOptions={{ 500 animationDuration: 285, 501 gestureEnabled: true, 502 fullScreenGestureEnabled: true, 503 headerShown: false, 504 contentStyle: t.atoms.bg, 505 }}> 506 <NotificationsTab.Screen 507 name="Notifications" 508 getComponent={() => NotificationsScreen} 509 options={{requireAuth: true}} 510 /> 511 {commonScreens(NotificationsTab as typeof HomeTab)} 512 </NotificationsTab.Navigator> 513 ) 514} 515 516function MyProfileTabNavigator() { 517 const t = useTheme() 518 return ( 519 <MyProfileTab.Navigator 520 screenOptions={{ 521 animationDuration: 285, 522 gestureEnabled: true, 523 fullScreenGestureEnabled: true, 524 headerShown: false, 525 contentStyle: t.atoms.bg, 526 }}> 527 <MyProfileTab.Screen 528 // @ts-ignore // TODO: fix this broken type in ProfileScreen 529 name="MyProfile" 530 getComponent={() => ProfileScreen} 531 initialParams={{ 532 name: 'me', 533 hideBackButton: true, 534 }} 535 /> 536 {commonScreens(MyProfileTab as typeof HomeTab)} 537 </MyProfileTab.Navigator> 538 ) 539} 540 541function MessagesTabNavigator() { 542 const t = useTheme() 543 return ( 544 <MessagesTab.Navigator 545 screenOptions={{ 546 animationDuration: 285, 547 gestureEnabled: true, 548 fullScreenGestureEnabled: true, 549 headerShown: false, 550 contentStyle: t.atoms.bg, 551 }}> 552 <MessagesTab.Screen 553 name="Messages" 554 getComponent={() => MessagesScreen} 555 options={({route}) => ({ 556 requireAuth: true, 557 animationTypeForReplace: route.params?.animation ?? 'push', 558 })} 559 /> 560 {commonScreens(MessagesTab as typeof HomeTab)} 561 </MessagesTab.Navigator> 562 ) 563} 564 565/** 566 * The FlatNavigator is used by Web to represent the routes 567 * in a single ("flat") stack. 568 */ 569const FlatNavigator = () => { 570 const t = useTheme() 571 const numUnread = useUnreadNotifications() 572 const screenListeners = useWebScrollRestoration() 573 const title = (page: MessageDescriptor) => bskyTitle(i18n._(page), numUnread) 574 575 return ( 576 <Flat.Navigator 577 screenListeners={screenListeners} 578 screenOptions={{ 579 animationDuration: 285, 580 gestureEnabled: true, 581 fullScreenGestureEnabled: true, 582 headerShown: false, 583 contentStyle: t.atoms.bg, 584 }}> 585 <Flat.Screen 586 name="Home" 587 getComponent={() => HomeScreen} 588 options={{title: title(msg`Home`)}} 589 /> 590 <Flat.Screen 591 name="Search" 592 getComponent={() => SearchScreen} 593 options={{title: title(msg`Search`)}} 594 /> 595 <Flat.Screen 596 name="Notifications" 597 getComponent={() => NotificationsScreen} 598 options={{title: title(msg`Notifications`), requireAuth: true}} 599 /> 600 <Flat.Screen 601 name="Messages" 602 getComponent={() => MessagesScreen} 603 options={{title: title(msg`Messages`), requireAuth: true}} 604 /> 605 <Flat.Screen 606 name="Start" 607 getComponent={() => HomeScreen} 608 options={{title: title(msg`Home`)}} 609 /> 610 {commonScreens(Flat as typeof HomeTab, numUnread)} 611 </Flat.Navigator> 612 ) 613} 614 615/** 616 * The RoutesContainer should wrap all components which need access 617 * to the navigation context. 618 */ 619 620const LINKING = { 621 // TODO figure out what we are going to use 622 prefixes: ['bsky://', 'bluesky://', 'https://bsky.app'], 623 624 getPathFromState(state: State) { 625 // find the current node in the navigation tree 626 let node = state.routes[state.index || 0] 627 while (node.state?.routes && typeof node.state?.index === 'number') { 628 node = node.state?.routes[node.state?.index] 629 } 630 631 // build the path 632 const route = router.matchName(node.name) 633 if (typeof route === 'undefined') { 634 return '/' // default to home 635 } 636 return route.build((node.params || {}) as RouteParams) 637 }, 638 639 getStateFromPath(path: string) { 640 const [name, params] = router.matchPath(path) 641 642 // Any time we receive a url that starts with `intent/` we want to ignore it here. It will be handled in the 643 // intent handler hook. We should check for the trailing slash, because if there isn't one then it isn't a valid 644 // intent 645 // On web, there is no route state that's created by default, so we should initialize it as the home route. On 646 // native, since the home tab and the home screen are defined as initial routes, we don't need to return a state 647 // since it will be created by react-navigation. 648 if (path.includes('intent/')) { 649 if (isNative) return 650 return buildStateObject('Flat', 'Home', params) 651 } 652 653 if (isNative) { 654 if (name === 'Search') { 655 return buildStateObject('SearchTab', 'Search', params) 656 } 657 if (name === 'Notifications') { 658 return buildStateObject('NotificationsTab', 'Notifications', params) 659 } 660 if (name === 'Home') { 661 return buildStateObject('HomeTab', 'Home', params) 662 } 663 if (name === 'Messages') { 664 return buildStateObject('MessagesTab', 'Messages', params) 665 } 666 // 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 667 return buildStateObject('HomeTab', name, params, [ 668 { 669 name: 'Home', 670 params: {}, 671 }, 672 ]) 673 } else { 674 const res = buildStateObject('Flat', name, params) 675 return res 676 } 677 }, 678} 679 680function RoutesContainer({children}: React.PropsWithChildren<{}>) { 681 const theme = useColorSchemeStyle(DefaultTheme, DarkTheme) 682 const {currentAccount} = useSession() 683 const {openModal} = useModalControls() 684 const prevLoggedRouteName = React.useRef<string | undefined>(undefined) 685 686 function onReady() { 687 prevLoggedRouteName.current = getCurrentRouteName() 688 if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) { 689 openModal({name: 'verify-email', showReminder: true}) 690 snoozeEmailConfirmationPrompt() 691 } 692 } 693 694 return ( 695 <NavigationContainer 696 ref={navigationRef} 697 linking={LINKING} 698 theme={theme} 699 onStateChange={() => { 700 const routeName = getCurrentRouteName() 701 if (routeName === 'Notifications') { 702 logEvent('router:navigate:notifications', {}) 703 } 704 }} 705 onReady={() => { 706 attachRouteToLogEvents(getCurrentRouteName) 707 logModuleInitTime() 708 onReady() 709 }}> 710 {children} 711 </NavigationContainer> 712 ) 713} 714 715function getCurrentRouteName() { 716 if (navigationRef.isReady()) { 717 return navigationRef.getCurrentRoute()?.name 718 } else { 719 return undefined 720 } 721} 722 723/** 724 * These helpers can be used from outside of the RoutesContainer 725 * (eg in the state models). 726 */ 727 728function navigate<K extends keyof AllNavigatorParams>( 729 name: K, 730 params?: AllNavigatorParams[K], 731) { 732 if (navigationRef.isReady()) { 733 return Promise.race([ 734 new Promise<void>(resolve => { 735 const handler = () => { 736 resolve() 737 navigationRef.removeListener('state', handler) 738 } 739 navigationRef.addListener('state', handler) 740 741 // @ts-ignore I dont know what would make typescript happy but I have a life -prf 742 navigationRef.navigate(name, params) 743 }), 744 timeout(1e3), 745 ]) 746 } 747 return Promise.resolve() 748} 749 750function resetToTab(tabName: 'HomeTab' | 'SearchTab' | 'NotificationsTab') { 751 if (navigationRef.isReady()) { 752 navigate(tabName) 753 if (navigationRef.canGoBack()) { 754 navigationRef.dispatch(StackActions.popToTop()) //we need to check .canGoBack() before calling it 755 } 756 } 757} 758 759// returns a promise that resolves after the state reset is complete 760function reset(): Promise<void> { 761 if (navigationRef.isReady()) { 762 navigationRef.dispatch( 763 CommonActions.reset({ 764 index: 0, 765 routes: [{name: isNative ? 'HomeTab' : 'Home'}], 766 }), 767 ) 768 return Promise.race([ 769 timeout(1e3), 770 new Promise<void>(resolve => { 771 const handler = () => { 772 resolve() 773 navigationRef.removeListener('state', handler) 774 } 775 navigationRef.addListener('state', handler) 776 }), 777 ]) 778 } else { 779 return Promise.resolve() 780 } 781} 782 783function handleLink(url: string) { 784 let path 785 if (url.startsWith('/')) { 786 path = url 787 } else if (url.startsWith('http')) { 788 try { 789 path = new URL(url).pathname 790 } catch (e) { 791 console.error('Invalid url', url, e) 792 return 793 } 794 } else { 795 console.error('Invalid url', url) 796 return 797 } 798 799 const [name, params] = router.matchPath(path) 800 if (isNative) { 801 if (name === 'Search') { 802 resetToTab('SearchTab') 803 } else if (name === 'Notifications') { 804 resetToTab('NotificationsTab') 805 } else { 806 resetToTab('HomeTab') 807 // @ts-ignore matchPath doesnt give us type-checked output -prf 808 navigate(name, params) 809 } 810 } else { 811 // @ts-ignore matchPath doesnt give us type-checked output -prf 812 navigate(name, params) 813 } 814} 815 816let didInit = false 817function logModuleInitTime() { 818 if (didInit) { 819 return 820 } 821 didInit = true 822 823 const initMs = Math.round( 824 // @ts-ignore Emitted by Metro in the bundle prelude 825 performance.now() - global.__BUNDLE_START_TIME__, 826 ) 827 console.log(`Time to first paint: ${initMs} ms`) 828 logEvent('init', { 829 initMs, 830 }) 831 832 if (isWeb) { 833 const referrerInfo = Referrer.getReferrerInfo() 834 if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { 835 logEvent('deepLink:referrerReceived', { 836 to: window.location.href, 837 referrer: referrerInfo?.referrer, 838 hostname: referrerInfo?.hostname, 839 }) 840 } 841 } 842 843 if (__DEV__) { 844 // This log is noisy, so keep false committed 845 const shouldLog = false 846 // Relies on our patch to polyfill.js in metro-runtime 847 const initLogs = (global as any).__INIT_LOGS__ 848 if (shouldLog && Array.isArray(initLogs)) { 849 console.log(initLogs.join('\n')) 850 } 851 } 852} 853 854export { 855 FlatNavigator, 856 handleLink, 857 navigate, 858 reset, 859 resetToTab, 860 RoutesContainer, 861 TabsNavigator, 862}