diff --git a/index.js b/index.js index 14d524cca2..4faf36dc48 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,5 @@ +import 'react-native-gesture-handler' // must be first + import {LogBox} from 'react-native' LogBox.ignoreLogs(['Require cycle:']) // suppress require-cycle warnings, it's fine diff --git a/package.json b/package.json index 6163dd66ee..804c50a4ca 100644 --- a/package.json +++ b/package.json @@ -70,14 +70,16 @@ "react-native": "0.71.3", "react-native-appstate-hook": "^1.0.6", "react-native-background-fetch": "^4.1.8", + "react-native-drawer-layout": "^3.2.0", "react-native-fast-image": "^8.6.3", "react-native-fs": "^2.20.0", + "react-native-gesture-handler": "~2.9.0", "react-native-haptic-feedback": "^1.14.0", "react-native-image-crop-picker": "^0.38.1", "react-native-inappbrowser-reborn": "^3.6.3", "react-native-linear-gradient": "^2.6.2", "react-native-progress": "^5.0.0", - "react-native-reanimated": "^2.9.1", + "react-native-reanimated": "~2.14.4", "react-native-root-siblings": "^4.1.1", "react-native-safe-area-context": "^4.4.1", "react-native-screens": "^3.13.1", diff --git a/src/lib/routes/helpers.ts b/src/lib/routes/helpers.ts index c8df8d79a7..708f27c92c 100644 --- a/src/lib/routes/helpers.ts +++ b/src/lib/routes/helpers.ts @@ -8,6 +8,22 @@ export function getCurrentRoute(state: State) { return node } +export function isStateAtTabRoot(state: State | undefined) { + if (!state) { + // NOTE + // if state is not defined it's because init is occuring + // and therefore we can safely assume we're at root + // -prf + return true + } + const currentRoute = getCurrentRoute(state) + return ( + isTab(currentRoute.name, 'Home') || + isTab(currentRoute.name, 'Search') || + isTab(currentRoute.name, 'Notifications') + ) +} + export function isTab(current: string, route: string) { // NOTE // our tab routes can be variously referenced by 3 different names @@ -19,3 +35,21 @@ export function isTab(current: string, route: string) { current === `${route}Inner` ) } + +export enum TabState { + InsideAtRoot, + Inside, + Outside, +} +export function getTabState(state: State | undefined, tab: string): TabState { + if (!state) { + return TabState.Outside + } + const currentRoute = getCurrentRoute(state) + if (isTab(currentRoute.name, tab)) { + return TabState.InsideAtRoot + } else if (isTab(state.routes[state.index || 0].name, tab)) { + return TabState.Inside + } + return TabState.Outside +} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 6867bff262..e7dc44ef17 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -20,26 +20,14 @@ export type HomeTabNavigatorParams = CommonNavigatorParams & { Home: undefined } -export type HomeDrawerNavigatorParams = { - HomeInner: undefined -} - export type SearchTabNavigatorParams = CommonNavigatorParams & { Search: undefined } -export type SearchDrawerNavigatorParams = { - SearchInner: undefined -} - export type NotificationsTabNavigatorParams = CommonNavigatorParams & { Notifications: undefined } -export type NotificationsDrawerNavigatorParams = { - NotificationsInner: undefined -} - export type AllNavigatorParams = CommonNavigatorParams & { HomeTab: undefined Home: undefined @@ -53,13 +41,7 @@ export type AllNavigatorParams = CommonNavigatorParams & { // this isn't strictly correct but it should be close enough // a TS wizard might be able to get this 100% // -prf -export type NavigationProp = NativeStackNavigationProp< - CommonNavigatorParams & { - HomeTab: undefined - NotificationsTab: undefined - SearchTab: undefined - } -> +export type NavigationProp = NativeStackNavigationProp export type State = | NavigationState diff --git a/src/routes.tsx b/src/routes.tsx index 8970c9cfe2..0d018e07ca 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -5,16 +5,12 @@ import { StackActions, } from '@react-navigation/native' import {createNativeStackNavigator} from '@react-navigation/native-stack' -import {createDrawerNavigator} from '@react-navigation/drawer' import {createBottomTabNavigator} from '@react-navigation/bottom-tabs' import { HomeTabNavigatorParams, - HomeDrawerNavigatorParams, SearchTabNavigatorParams, - SearchDrawerNavigatorParams, NotificationsTabNavigatorParams, - NotificationsDrawerNavigatorParams, AllNavigatorParams, State, } from 'lib/routes/types' @@ -38,12 +34,8 @@ import {LogScreen} from './view/screens/Log' const navigationRef = createNavigationContainerRef() -const HomeDrawer = createDrawerNavigator() const HomeTab = createNativeStackNavigator() -const SearchDrawer = createDrawerNavigator() const SearchTab = createNativeStackNavigator() -const NotificationsDrawer = - createDrawerNavigator() const NotificationsTab = createNativeStackNavigator() const Tab = createBottomTabNavigator() @@ -78,11 +70,8 @@ function r(pattern: string): Route { } const ROUTES: Record = { Home: r('/'), - HomeInner: r('/'), Search: r('/search'), - SearchInner: r('/search'), Notifications: r('/notifications'), - NotificationsInner: r('/notifications'), Settings: r('/settings'), Profile: r('/profile/:name'), ProfileFollowers: r('/profile/:name/followers'), @@ -171,17 +160,6 @@ function commonScreens(Stack: typeof HomeTab) { ) } -function HomeDrawerNavigator() { - const drawerContent = React.useCallback(props => , []) - return ( - - - - ) -} - function HomeTabNavigator() { return ( - + {commonScreens(HomeTab)} ) } -function SearchDrawerNavigator() { - const drawerContent = React.useCallback(props => , []) - return ( - - - - ) -} - function SearchTabNavigator() { return ( - + {commonScreens(SearchTab as typeof HomeTab)} ) } -function NotificationsDrawerNavigator() { - const drawerContent = React.useCallback(props => , []) - return ( - - - - ) -} - function NotificationsTabNavigator() { return ( {commonScreens(NotificationsTab as typeof HomeTab)} diff --git a/src/state/models/shell-ui.ts b/src/state/models/shell-ui.ts index 68d9cd3d09..7cb7645868 100644 --- a/src/state/models/shell-ui.ts +++ b/src/state/models/shell-ui.ts @@ -117,7 +117,7 @@ export interface ComposerOpts { export class ShellUiModel { darkMode = false minimalShellMode = false - isMainMenuOpen = false + isDrawerOpen = false isModalActive = false activeModals: Modal[] = [] isLightboxActive = false @@ -156,8 +156,12 @@ export class ShellUiModel { this.minimalShellMode = v } - setMainMenuOpen(v: boolean) { - this.isMainMenuOpen = v + openDrawer() { + this.isDrawerOpen = true + } + + closeDrawer() { + this.isDrawerOpen = false } openModal(modal: Modal) { diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 0f4fce02ca..322f040cc5 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -51,7 +51,7 @@ export const Link = observer(function Link({ if (noFeedback) { return ( - + {children ? children : {title || 'link'}} @@ -59,7 +59,7 @@ export const Link = observer(function Link({ ) } return ( - + {children ? children : {title || 'link'}} ) diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index e82f34218f..96f097e61f 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -34,8 +34,8 @@ export const ViewHeader = observer(function ViewHeader({ const onPressMenu = React.useCallback(() => { track('ViewHeader:MenuButtonClicked') - navigation.dispatch(DrawerActions.openDrawer()) - }, [track, navigation]) + store.shell.openDrawer() + }, [track, store]) if (typeof canGoBack === 'undefined') { canGoBack = navigation.canGoBack() diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 1dc5be1052..505b1fcfe8 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -3,10 +3,7 @@ import {FlatList, View} from 'react-native' import {useFocusEffect, useIsFocused} from '@react-navigation/native' import {observer} from 'mobx-react-lite' import useAppState from 'react-native-appstate-hook' -import { - NativeStackScreenProps, - HomeDrawerNavigatorParams, -} from 'lib/routes/types' +import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types' import {ViewHeader} from '../com/util/ViewHeader' import {Feed} from '../com/posts/Feed' import {LoadLatestBtn} from '../com/util/LoadLatestBtn' @@ -20,7 +17,7 @@ import {ComposeIcon2} from 'lib/icons' const HEADER_HEIGHT = 42 -type Props = NativeStackScreenProps +type Props = NativeStackScreenProps export const HomeScreen = observer(function Home(_opts: Props) { const store = useStores() const onMainScroll = useOnMainScroll(store) diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index bb7225f71d..492177d1f6 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -4,7 +4,7 @@ import {useFocusEffect} from '@react-navigation/native' import useAppState from 'react-native-appstate-hook' import { NativeStackScreenProps, - NotificationsDrawerNavigatorParams, + NotificationsTabNavigatorParams, } from 'lib/routes/types' import {ViewHeader} from '../com/util/ViewHeader' import {Feed} from '../com/notifications/Feed' @@ -16,8 +16,8 @@ import {useAnalytics} from 'lib/analytics' const NOTIFICATIONS_POLL_INTERVAL = 15e3 type Props = NativeStackScreenProps< - NotificationsDrawerNavigatorParams, - 'NotificationsInner' + NotificationsTabNavigatorParams, + 'Notifications' > export const NotificationsScreen = ({}: Props) => { const store = useStores() diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx index 2bcbd722ce..e715d2a2e7 100644 --- a/src/view/screens/Search.tsx +++ b/src/view/screens/Search.tsx @@ -15,7 +15,7 @@ import { import {ScrollView} from '../com/util/Views' import { NativeStackScreenProps, - SearchDrawerNavigatorParams, + SearchTabNavigatorParams, } from 'lib/routes/types' import {observer} from 'mobx-react-lite' import {UserAvatar} from '../com/util/UserAvatar' @@ -34,7 +34,7 @@ import {useAnalytics} from 'lib/analytics' const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10} const FIVE_MIN = 5 * 60 * 1e3 -type Props = NativeStackScreenProps +type Props = NativeStackScreenProps export const SearchScreen = observer(({}: Props) => { const pal = usePalette('default') const store = useStores() diff --git a/src/view/shell/BottomBar.tsx b/src/view/shell/BottomBar.tsx index 51dda62c93..7839c164d3 100644 --- a/src/view/shell/BottomBar.tsx +++ b/src/view/shell/BottomBar.tsx @@ -26,7 +26,7 @@ import { } from 'lib/icons' import {colors} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' -import {getCurrentRoute, isTab} from 'lib/routes/helpers' +import {getCurrentRoute, isTab, getTabState, TabState} from 'lib/routes/helpers' export const BottomBar = observer(({navigation}: BottomTabBarProps) => { const store = useStores() @@ -60,10 +60,10 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => { (tab: string) => { track(`MobileShell:${tab}ButtonPressed`) const state = navigation.getState() - const currentRoute = getCurrentRoute(state) - if (isTab(currentRoute.name, tab)) { + const tabState = getTabState(state, tab) + if (tabState === TabState.InsideAtRoot) { store.emitScreenSoftReset() - } else if (isTab(state.routes[state.index].name, tab)) { + } else if (tabState === TabState.Inside) { navigation.dispatch(StackActions.popToTop()) } else { navigation.navigate(`${tab}Tab`) diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 63994c72a9..6e2c466c62 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -9,10 +9,10 @@ import { ViewStyle, } from 'react-native' import { - DrawerContentComponentProps, - useDrawerStatus, -} from '@react-navigation/drawer' -import {StackActions} from '@react-navigation/native' + useNavigation, + useNavigationState, + StackActions, +} from '@react-navigation/native' import {observer} from 'mobx-react-lite' import { FontAwesomeIcon, @@ -38,38 +38,44 @@ import {useTheme} from 'lib/ThemeContext' import {usePalette} from 'lib/hooks/usePalette' import {useAnalytics} from 'lib/analytics' import {pluralize} from 'lib/strings/helpers' -import {getCurrentRoute, isTab} from 'lib/routes/helpers' +import {getCurrentRoute, isTab, getTabState, TabState} from 'lib/routes/helpers' +import {NavigationProp} from 'lib/routes/types' -export const Drawer = observer(({navigation}: DrawerContentComponentProps) => { +export const DrawerContent = observer(() => { const theme = useTheme() const pal = usePalette('default') const store = useStores() + const navigation = useNavigation() const {track} = useAnalytics() - const isDrawerOpen = useDrawerStatus() === 'open' + const {isAtHome, isAtSearch, isAtNotifications} = useNavigationState( + state => { + const currentRoute = state ? getCurrentRoute(state) : false + return { + isAtHome: currentRoute ? isTab(currentRoute.name, 'Home') : true, + isAtSearch: currentRoute ? isTab(currentRoute.name, 'Search') : false, + isAtNotifications: currentRoute + ? isTab(currentRoute.name, 'Notifications') + : false, + } + }, + ) // events // = - React.useEffect(() => { - console.log('Drawer', isDrawerOpen ? 'minimizing' : 'unminimizing', 'shell') - store.shell.setMinimalShellMode(isDrawerOpen) - }, [isDrawerOpen, store]) - const onPressTab = React.useCallback( (tab: string) => { track('Menu:ItemClicked', {url: tab}) const state = navigation.getState() - navigation.closeDrawer() - const currentRoute = getCurrentRoute(state) - if (isTab(currentRoute.name, tab)) { + store.shell.closeDrawer() + const tabState = getTabState(state, tab) + if (tabState === TabState.InsideAtRoot) { store.emitScreenSoftReset() - } else if (isTab(state.routes[state.index].name, tab)) { + } else if (tabState === TabState.Inside) { navigation.dispatch(StackActions.popToTop()) } else { - // wait for drawer anim to finish - setTimeout(() => { - navigation.navigate(`${tab}Tab`) - }, 250) + // @ts-ignore must be Home, Search, or Notifications + navigation.navigate(`${tab}Tab`) } }, [store, track, navigation], @@ -90,14 +96,14 @@ export const Drawer = observer(({navigation}: DrawerContentComponentProps) => { const onPressProfile = React.useCallback(() => { track('Menu:ItemClicked', {url: 'Profile'}) navigation.navigate('Profile', {name: store.me.handle}) - navigation.closeDrawer() - }, [navigation, track, store.me.handle]) + store.shell.closeDrawer() + }, [navigation, track, store.me.handle, store.shell]) const onPressSettings = React.useCallback(() => { track('Menu:ItemClicked', {url: 'Settings'}) navigation.navigate('Settings') - navigation.closeDrawer() - }, [navigation, track]) + store.shell.closeDrawer() + }, [navigation, track, store.shell]) const onPressFeedback = () => { track('Menu:FeedbackClicked') @@ -146,11 +152,6 @@ export const Drawer = observer(({navigation}: DrawerContentComponentProps) => { store.shell.setDarkMode(!store.shell.darkMode) } - const currentRoute = getCurrentRoute(navigation.getState()) - const isAtHome = isTab(currentRoute.name, 'Home') - const isAtSearch = isTab(currentRoute.name, 'Search') - const isAtNotifications = isTab(currentRoute.name, 'Notifications') - return ( { - const theme = useTheme() - const pal = usePalette('default') +const ShellInner = observer(() => { const store = useStores() const winDim = useWindowDimensions() const safeAreaInsets = useSafeAreaInsets() @@ -24,6 +26,51 @@ export const Shell: React.FC = observer(() => { () => ({height: '100%', paddingTop: safeAreaInsets.top}), [safeAreaInsets], ) + const renderDrawerContent = React.useCallback(() => , []) + const onOpenDrawer = React.useCallback( + () => store.shell.openDrawer(), + [store], + ) + const onCloseDrawer = React.useCallback( + () => store.shell.closeDrawer(), + [store], + ) + const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) + + return ( + <> + + + + + + + + + + store.shell.closeComposer()} + winHeight={winDim.height} + replyTo={store.shell.composerOpts?.replyTo} + imagesOpen={store.shell.composerOpts?.imagesOpen} + onPost={store.shell.composerOpts?.onPost} + quote={store.shell.composerOpts?.quote} + /> + + ) +}) + +export const Shell: React.FC = observer(() => { + const theme = useTheme() + const pal = usePalette('default') + const store = useStores() if (store.hackUpgradeNeeded) { return ( @@ -80,22 +127,7 @@ export const Shell: React.FC = observer(() => { } /> - - - - - - - - store.shell.closeComposer()} - winHeight={winDim.height} - replyTo={store.shell.composerOpts?.replyTo} - imagesOpen={store.shell.composerOpts?.imagesOpen} - onPost={store.shell.composerOpts?.onPost} - quote={store.shell.composerOpts?.quote} - /> + ) diff --git a/yarn.lock b/yarn.lock index c81643ecc8..cb674147b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13184,6 +13184,13 @@ react-native-dotenv@^3.3.1: dependencies: dotenv "^16.0.3" +react-native-drawer-layout@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/react-native-drawer-layout/-/react-native-drawer-layout-3.2.0.tgz#1ab05d0bed6bb684353c17c96e1d3e6c1a4e225d" + integrity sha512-d/kvzeBhXjqcRGlfkTSB96ZRKH6g6YxJK+gPtUOCOCH5piHpsupgX+tLAHdM8r8NUzN6Tl9656xfJTuVb8Zrgw== + dependencies: + use-latest-callback "^0.1.5" + react-native-fast-image@^8.6.3: version "8.6.3" resolved "https://registry.yarnpkg.com/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz#6edc3f9190092a909d636d93eecbcc54a8822255" @@ -13197,7 +13204,7 @@ react-native-fs@^2.20.0: base-64 "^0.1.0" utf8 "^3.0.0" -react-native-gesture-handler@^2.5.0: +react-native-gesture-handler@~2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.9.0.tgz#2f63812e523c646f25b9ad660fc6f75948e51241" integrity sha512-a0BcH3Qb1tgVqUutc6d3VuWQkI1AM3+fJx8dkxzZs9t06qA27QgURYFoklpabuWpsUTzuKRpxleykp25E8m7tg== @@ -13243,7 +13250,7 @@ react-native-progress@^5.0.0: dependencies: prop-types "^15.7.2" -react-native-reanimated@^2.9.1: +react-native-reanimated@~2.14.4: version "2.14.4" resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.14.4.tgz#3fa3da4e7b99f5dfb28f86bcf24d9d1024d38836" integrity sha512-DquSbl7P8j4SAmc+kRdd75Ianm8G+IYQ9T4AQ6lrpLVeDkhZmjWI0wkutKWnp6L7c5XNVUrFDUf69dwETLCItQ==