import * as React from 'react' import {StyleSheet} from 'react-native' import * as SplashScreen from 'expo-splash-screen' import { NavigationContainer, createNavigationContainerRef, CommonActions, StackActions, DefaultTheme, DarkTheme, } from '@react-navigation/native' import { BottomTabBarProps, createBottomTabNavigator, } from '@react-navigation/bottom-tabs' import { HomeTabNavigatorParams, SearchTabNavigatorParams, FeedsTabNavigatorParams, NotificationsTabNavigatorParams, FlatNavigatorParams, AllNavigatorParams, MyProfileTabNavigatorParams, BottomTabNavigatorParams, } from 'lib/routes/types' import {BottomBar} from './view/shell/bottom-bar/BottomBar' import {buildStateObject} from 'lib/routes/helpers' import {State, RouteParams} from 'lib/routes/types' import {colors} from 'lib/styles' import {isNative} from 'platform/detection' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {router} from './routes' import {usePalette} from 'lib/hooks/usePalette' import {bskyTitle} from 'lib/strings/headings' import {JSX} from 'react/jsx-runtime' import {timeout} from 'lib/async/timeout' import {useUnreadNotifications} from './state/queries/notifications/unread' import {HomeScreen} from './view/screens/Home' import {SearchScreen} from './view/screens/Search' import {FeedsScreen} from './view/screens/Feeds' import {NotificationsScreen} from './view/screens/Notifications' import {ListsScreen} from './view/screens/Lists' import {ModerationScreen} from './view/screens/Moderation' import {ModerationModlistsScreen} from './view/screens/ModerationModlists' import {NotFoundScreen} from './view/screens/NotFound' import {SettingsScreen} from './view/screens/Settings' import {LanguageSettingsScreen} from './view/screens/LanguageSettings' import {ProfileScreen} from './view/screens/Profile' import {ProfileFollowersScreen} from './view/screens/ProfileFollowers' import {ProfileFollowsScreen} from './view/screens/ProfileFollows' import {ProfileFeedScreen} from './view/screens/ProfileFeed' import {ProfileFeedLikedByScreen} from './view/screens/ProfileFeedLikedBy' import {ProfileListScreen} from './view/screens/ProfileList' import {PostThreadScreen} from './view/screens/PostThread' import {PostLikedByScreen} from './view/screens/PostLikedBy' import {PostRepostedByScreen} from './view/screens/PostRepostedBy' import {DebugScreen} from './view/screens/Debug' import {LogScreen} from './view/screens/Log' import {SupportScreen} from './view/screens/Support' import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy' import {TermsOfServiceScreen} from './view/screens/TermsOfService' import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines' import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy' import {AppPasswords} from 'view/screens/AppPasswords' import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts' import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts' import {SavedFeeds} from 'view/screens/SavedFeeds' import {PreferencesHomeFeed} from 'view/screens/PreferencesHomeFeed' import {PreferencesThreads} from 'view/screens/PreferencesThreads' import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStackNavigatorWithAuth' const navigationRef = createNavigationContainerRef() const HomeTab = createNativeStackNavigatorWithAuth() const SearchTab = createNativeStackNavigatorWithAuth() const FeedsTab = createNativeStackNavigatorWithAuth() const NotificationsTab = createNativeStackNavigatorWithAuth() const MyProfileTab = createNativeStackNavigatorWithAuth() const Flat = createNativeStackNavigatorWithAuth() const Tab = createBottomTabNavigator() /** * These "common screens" are reused across stacks. */ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { const title = (page: string) => bskyTitle(page, unreadCountLabel) return ( <> NotFoundScreen} options={{title: title('Not Found')}} /> ModerationScreen} options={{title: title('Moderation'), requireAuth: true}} /> ModerationModlistsScreen} options={{title: title('Moderation Lists'), requireAuth: true}} /> ModerationMutedAccounts} options={{title: title('Muted Accounts'), requireAuth: true}} /> ModerationBlockedAccounts} options={{title: title('Blocked Accounts'), requireAuth: true}} /> SettingsScreen} options={{title: title('Settings'), requireAuth: true}} /> LanguageSettingsScreen} options={{title: title('Language Settings'), requireAuth: true}} /> ProfileScreen} options={({route}) => ({ title: title(`@${route.params.name}`), animation: 'none', })} /> ProfileFollowersScreen} options={({route}) => ({ title: title(`People following @${route.params.name}`), })} /> ProfileFollowsScreen} options={({route}) => ({ title: title(`People followed by @${route.params.name}`), })} /> ProfileListScreen} options={{title: title('List'), requireAuth: true}} /> PostThreadScreen} options={({route}) => ({title: title(`Post by @${route.params.name}`)})} /> PostLikedByScreen} options={({route}) => ({title: title(`Post by @${route.params.name}`)})} /> PostRepostedByScreen} options={({route}) => ({title: title(`Post by @${route.params.name}`)})} /> ProfileFeedScreen} options={{title: title('Feed')}} /> ProfileFeedLikedByScreen} options={{title: title('Liked by')}} /> DebugScreen} options={{title: title('Debug'), requireAuth: true}} /> LogScreen} options={{title: title('Log'), requireAuth: true}} /> SupportScreen} options={{title: title('Support')}} /> PrivacyPolicyScreen} options={{title: title('Privacy Policy')}} /> TermsOfServiceScreen} options={{title: title('Terms of Service')}} /> CommunityGuidelinesScreen} options={{title: title('Community Guidelines')}} /> CopyrightPolicyScreen} options={{title: title('Copyright Policy')}} /> AppPasswords} options={{title: title('App Passwords'), requireAuth: true}} /> SavedFeeds} options={{title: title('Edit My Feeds'), requireAuth: true}} /> PreferencesHomeFeed} options={{title: title('Home Feed Preferences'), requireAuth: true}} /> PreferencesThreads} options={{title: title('Threads Preferences'), requireAuth: true}} /> ) } /** * The TabsNavigator is used by native mobile to represent the routes * in 3 distinct tab-stacks with a different root screen on each. */ function TabsNavigator() { const tabBar = React.useCallback( (props: JSX.IntrinsicAttributes & BottomTabBarProps) => ( ), [], ) return ( HomeTabNavigator} /> SearchTabNavigator} /> FeedsTabNavigator} /> NotificationsTabNavigator} /> MyProfileTabNavigator} /> ) } function HomeTabNavigator() { const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark) return ( HomeScreen} /> {commonScreens(HomeTab)} ) } function SearchTabNavigator() { const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark) return ( SearchScreen} /> {commonScreens(SearchTab as typeof HomeTab)} ) } function FeedsTabNavigator() { const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark) return ( FeedsScreen} /> {commonScreens(FeedsTab as typeof HomeTab)} ) } function NotificationsTabNavigator() { const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark) return ( NotificationsScreen} options={{requireAuth: true}} /> {commonScreens(NotificationsTab as typeof HomeTab)} ) } function MyProfileTabNavigator() { const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark) return ( ProfileScreen} initialParams={{ name: 'me', }} /> {commonScreens(MyProfileTab as typeof HomeTab)} ) } /** * The FlatNavigator is used by Web to represent the routes * in a single ("flat") stack. */ const FlatNavigator = () => { const pal = usePalette('default') const numUnread = useUnreadNotifications() const title = (page: string) => bskyTitle(page, numUnread) return ( HomeScreen} options={{title: title('Home')}} /> SearchScreen} options={{title: title('Search')}} /> FeedsScreen} options={{title: title('Feeds')}} /> NotificationsScreen} options={{title: title('Notifications'), requireAuth: true}} /> {commonScreens(Flat as typeof HomeTab, numUnread)} ) } /** * The RoutesContainer should wrap all components which need access * to the navigation context. */ const LINKING = { prefixes: ['bsky://', 'https://bsky.app'], getPathFromState(state: State) { // find the current node in the navigation tree let node = state.routes[state.index || 0] while (node.state?.routes && typeof node.state?.index === 'number') { node = node.state?.routes[node.state?.index] } // build the path const route = router.matchName(node.name) if (typeof route === 'undefined') { return '/' // default to home } return route.build((node.params || {}) as RouteParams) }, getStateFromPath(path: string) { const [name, params] = router.matchPath(path) if (isNative) { if (name === 'Search') { return buildStateObject('SearchTab', 'Search', params) } if (name === 'Notifications') { return buildStateObject('NotificationsTab', 'Notifications', params) } if (name === 'Home') { return buildStateObject('HomeTab', 'Home', params) } // 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 return buildStateObject('HomeTab', name, params, [ { name: 'Home', params: {}, }, ]) } else { return buildStateObject('Flat', name, params) } }, } function RoutesContainer({children}: React.PropsWithChildren<{}>) { const theme = useColorSchemeStyle(DefaultTheme, DarkTheme) return ( { SplashScreen.hideAsync() const initMs = Math.round( // @ts-ignore Emitted by Metro in the bundle prelude performance.now() - global.__BUNDLE_START_TIME__, ) console.log(`Time to first paint: ${initMs} ms`) logModuleInitTrace() }}> {children} ) } /** * These helpers can be used from outside of the RoutesContainer * (eg in the state models). */ function navigate( name: K, params?: AllNavigatorParams[K], ) { if (navigationRef.isReady()) { return Promise.race([ new Promise(resolve => { const handler = () => { resolve() navigationRef.removeListener('state', handler) } navigationRef.addListener('state', handler) // @ts-ignore I dont know what would make typescript happy but I have a life -prf navigationRef.navigate(name, params) }), timeout(1e3), ]) } return Promise.resolve() } function resetToTab(tabName: 'HomeTab' | 'SearchTab' | 'NotificationsTab') { if (navigationRef.isReady()) { navigate(tabName) if (navigationRef.canGoBack()) { navigationRef.dispatch(StackActions.popToTop()) //we need to check .canGoBack() before calling it } } } // returns a promise that resolves after the state reset is complete function reset(): Promise { if (navigationRef.isReady()) { navigationRef.dispatch( CommonActions.reset({ index: 0, routes: [{name: isNative ? 'HomeTab' : 'Home'}], }), ) return Promise.race([ timeout(1e3), new Promise(resolve => { const handler = () => { resolve() navigationRef.removeListener('state', handler) } navigationRef.addListener('state', handler) }), ]) } else { return Promise.resolve() } } function handleLink(url: string) { let path if (url.startsWith('/')) { path = url } else if (url.startsWith('http')) { try { path = new URL(url).pathname } catch (e) { console.error('Invalid url', url, e) return } } else { console.error('Invalid url', url) return } const [name, params] = router.matchPath(path) if (isNative) { if (name === 'Search') { resetToTab('SearchTab') } else if (name === 'Notifications') { resetToTab('NotificationsTab') } else { resetToTab('HomeTab') // @ts-ignore matchPath doesnt give us type-checked output -prf navigate(name, params) } } else { // @ts-ignore matchPath doesnt give us type-checked output -prf navigate(name, params) } } const styles = StyleSheet.create({ bgDark: { backgroundColor: colors.black, }, bgLight: { backgroundColor: colors.white, }, }) function logModuleInitTrace() { if (__DEV__) { // This log is noisy, so keep false committed const shouldLog = false // Relies on our patch to polyfill.js in metro-runtime const initLogs = (global as any).__INIT_LOGS__ if (shouldLog && Array.isArray(initLogs)) { console.log(initLogs.join('\n')) } } } export { navigate, resetToTab, reset, handleLink, TabsNavigator, FlatNavigator, RoutesContainer, }