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