From e73c7cee3982b95ea0d323d2ddcdfa6145b8c3f6 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 8 Dec 2022 15:34:22 -0600 Subject: Turn the main menu into a 'drawer' instead of a screen in the history --- src/state/models/navigation.ts | 30 ++--- src/view/routes.ts | 2 - src/view/screens/Menu.tsx | 264 --------------------------------------- src/view/shell/mobile/Menu.tsx | 269 ++++++++++++++++++++++++++++++++++++++++ src/view/shell/mobile/index.tsx | 89 +++++++++++-- 5 files changed, 352 insertions(+), 302 deletions(-) delete mode 100644 src/view/screens/Menu.tsx create mode 100644 src/view/shell/mobile/Menu.tsx (limited to 'src') diff --git a/src/state/models/navigation.ts b/src/state/models/navigation.ts index 8ca44dc7f..8d69e5c04 100644 --- a/src/state/models/navigation.ts +++ b/src/state/models/navigation.ts @@ -12,7 +12,7 @@ function genId() { // until we're fully sure what that is, the tabs are being repurposed into a fixed topology // - Tab 0: The "Default" tab // - Tab 1: The "Notifications" tab -// These tabs always retain the first 2 items in their history. +// These tabs always retain the first item in their history. // The default tab is used for basically everything except notifications. // -prf export enum TabPurpose { @@ -32,20 +32,14 @@ export type HistoryPtr = [number, number] export class NavigationTabModel { id = genId() history: HistoryItem[] - index = 1 + index = 0 isNewTab = false constructor(public fixedTabPurpose: TabPurpose) { if (fixedTabPurpose === TabPurpose.Notifs) { - this.history = [ - {url: '/menu', ts: Date.now(), id: genId()}, - {url: '/notifications', ts: Date.now(), id: genId()}, - ] + this.history = [{url: '/notifications', ts: Date.now(), id: genId()}] } else { - this.history = [ - {url: '/menu', ts: Date.now(), id: genId()}, - {url: '/', ts: Date.now(), id: genId()}, - ] + this.history = [{url: '/', ts: Date.now(), id: genId()}] } makeAutoObservable(this, { serialize: false, @@ -85,7 +79,7 @@ export class NavigationTabModel { getForwardList(n: number) { const start = Math.min(this.index + 1, this.history.length) - const end = Math.min(this.index + n, this.history.length) + const end = Math.min(this.index + n + 1, this.history.length) return this.history.slice(start, end).map((item, i) => ({ url: item.url, title: item.title, @@ -109,7 +103,7 @@ export class NavigationTabModel { this.history.length = this.index + 1 } // TEMP ensure the tab has its purpose's main view -prf - if (this.history.length < 2) { + if (this.history.length < 1) { const fixedUrl = this.fixedTabPurpose === TabPurpose.Notifs ? '/notifications' : '/' this.history.push({url: fixedUrl, ts: Date.now(), id: genId()}) @@ -142,17 +136,7 @@ export class NavigationTabModel { // a helper to bring the tab back to its base state // -prf fixedTabReset() { - if (this.index >= 1) { - // fall back in history to "main" view - if (this.index > 1) { - this.index = 1 - } - } else { - const url = - this.fixedTabPurpose === TabPurpose.Notifs ? '/notifications' : '/' - this.history = [this.history[0], {url, ts: Date.now(), id: genId()}] - this.index = 1 - } + this.index = 0 } goForward() { diff --git a/src/view/routes.ts b/src/view/routes.ts index e662e2cca..272a1b096 100644 --- a/src/view/routes.ts +++ b/src/view/routes.ts @@ -1,7 +1,6 @@ import React, {MutableRefObject} from 'react' import {FlatList} from 'react-native' import {IconProp} from '@fortawesome/fontawesome-svg-core' -import {Menu} from './screens/Menu' import {Home} from './screens/Home' import {Contacts} from './screens/Contacts' import {Search} from './screens/Search' @@ -34,7 +33,6 @@ export type MatchResult = { const r = (pattern: string) => new RegExp('^' + pattern + '([?]|$)', 'i') export const routes: Route[] = [ - [Menu, 'Menu', 'bars', r('/menu')], [Home, 'Home', 'house', r('/')], [Contacts, 'Contacts', ['far', 'circle-user'], r('/contacts')], [Search, 'Search', 'magnifying-glass', r('/search')], diff --git a/src/view/screens/Menu.tsx b/src/view/screens/Menu.tsx deleted file mode 100644 index fab0f6dec..000000000 --- a/src/view/screens/Menu.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import React, {useEffect} from 'react' -import { - StyleProp, - StyleSheet, - Text, - TouchableOpacity, - View, - ViewStyle, -} from 'react-native' -import VersionNumber from 'react-native-version-number' -import {s, colors} from '../lib/styles' -import {ScreenParams} from '../routes' -import {useStores} from '../../state' -import { - HomeIcon, - UserGroupIcon, - BellIcon, - CogIcon, - MagnifyingGlassIcon, -} from '../lib/icons' -import {UserAvatar} from '../com/util/UserAvatar' -import {ViewHeader} from '../com/util/ViewHeader' -import {CreateSceneModel} from '../../state/models/shell-ui' - -export const Menu = ({navIdx, visible}: ScreenParams) => { - const store = useStores() - - useEffect(() => { - if (visible) { - store.nav.setTitle(navIdx, 'Menu') - // trigger a refresh in case memberships have changed recently - store.me.refreshMemberships() - } - }, [store, visible]) - - // events - // = - - const onNavigate = (url: string) => { - if (url === '/notifications') { - store.nav.switchTo(1, true) - } else { - store.nav.switchTo(0, true) - store.nav.navigate(url) - } - } - const onPressCreateScene = () => { - store.shell.openModal(new CreateSceneModel()) - } - - // rendering - // = - - const MenuItem = ({ - icon, - label, - count, - url, - bold, - onPress, - }: { - icon: JSX.Element - label: string - count?: number - url?: string - bold?: boolean - onPress?: () => void - }) => ( - onNavigate(url || '/')}> - - {icon} - {count ? ( - - {count} - - ) : undefined} - - - {label} - - - ) - - /*TODO */ - return ( - - - onNavigate('/search')}> - } - size={21} - /> - Search - - - - } - label={store.me.displayName || store.me.handle} - bold - url={`/profile/${store.me.handle}`} - /> - } - size="24" - /> - } - label="Home" - url="/" - /> - } - size="24" - /> - } - label="Notifications" - url="/notifications" - count={store.me.notificationCount} - /> - } - size="24" - strokeWidth={2} - /> - } - label="Settings" - url="/settings" - /> - - - Scenes - } - size="24" - /> - } - label="Create a scene" - onPress={onPressCreateScene} - /> - {store.me.memberships - ? store.me.memberships.memberships.map((membership, i) => ( - - } - label={membership.displayName || membership.handle} - url={`/profile/${membership.handle}`} - /> - )) - : undefined} - - - - Build version {VersionNumber.appVersion} ({VersionNumber.buildVersion} - ) - - - - ) -} - -const styles = StyleSheet.create({ - view: { - flex: 1, - backgroundColor: colors.white, - }, - section: { - paddingHorizontal: 10, - paddingTop: 10, - paddingBottom: 10, - borderBottomWidth: 1, - borderBottomColor: colors.gray1, - }, - heading: { - fontSize: 16, - fontWeight: 'bold', - paddingVertical: 8, - paddingHorizontal: 4, - }, - - searchBtn: { - flexDirection: 'row', - backgroundColor: colors.gray1, - borderRadius: 8, - margin: 10, - marginBottom: 0, - paddingVertical: 10, - paddingHorizontal: 12, - }, - searchBtnLabel: { - marginLeft: 8, - fontSize: 18, - color: colors.gray6, - }, - - menuItem: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 8, - paddingHorizontal: 2, - }, - menuItemIconWrapper: { - width: 30, - height: 30, - alignItems: 'center', - justifyContent: 'center', - marginRight: 10, - }, - menuItemLabel: { - fontSize: 17, - color: colors.gray7, - }, - menuItemLabelBold: { - fontWeight: 'bold', - }, - menuItemCount: { - position: 'absolute', - right: -6, - top: -2, - backgroundColor: colors.red3, - paddingHorizontal: 4, - paddingBottom: 1, - borderRadius: 6, - }, - menuItemCountLabel: { - fontSize: 12, - fontWeight: 'bold', - color: colors.white, - }, - - footer: { - paddingHorizontal: 14, - paddingVertical: 18, - }, -}) diff --git a/src/view/shell/mobile/Menu.tsx b/src/view/shell/mobile/Menu.tsx new file mode 100644 index 000000000..173f9563c --- /dev/null +++ b/src/view/shell/mobile/Menu.tsx @@ -0,0 +1,269 @@ +import React, {useEffect} from 'react' +import { + StyleProp, + StyleSheet, + Text, + TouchableOpacity, + View, + ViewStyle, +} from 'react-native' +import VersionNumber from 'react-native-version-number' +import {s, colors} from '../../lib/styles' +import {useStores} from '../../../state' +import { + HomeIcon, + UserGroupIcon, + BellIcon, + CogIcon, + MagnifyingGlassIcon, +} from '../../lib/icons' +import {UserAvatar} from '../../com/util/UserAvatar' +import {CreateSceneModel} from '../../../state/models/shell-ui' + +export const Menu = ({ + visible, + onClose, +}: { + visible: boolean + onClose: () => void +}) => { + const store = useStores() + + useEffect(() => { + if (visible) { + // trigger a refresh in case memberships have changed recently + store.me.refreshMemberships() + } + }, [store, visible]) + + // events + // = + + const onNavigate = (url: string) => { + onClose() + if (url === '/notifications') { + store.nav.switchTo(1, true) + } else { + store.nav.switchTo(0, true) + if (url !== '/') { + store.nav.navigate(url) + } + } + } + const onPressCreateScene = () => { + onClose() + store.shell.openModal(new CreateSceneModel()) + } + + // rendering + // = + + const MenuItem = ({ + icon, + label, + count, + url, + bold, + onPress, + }: { + icon: JSX.Element + label: string + count?: number + url?: string + bold?: boolean + onPress?: () => void + }) => ( + onNavigate(url || '/')}> + + {icon} + {count ? ( + + {count} + + ) : undefined} + + + {label} + + + ) + + return ( + + onNavigate('/search')}> + } + size={21} + /> + Search + + + + } + label={store.me.displayName || store.me.handle} + bold + url={`/profile/${store.me.handle}`} + /> + } + size="24" + /> + } + label="Home" + url="/" + /> + } + size="24" + /> + } + label="Notifications" + url="/notifications" + count={store.me.notificationCount} + /> + } + size="24" + strokeWidth={2} + /> + } + label="Settings" + url="/settings" + /> + + + Scenes + } + size="24" + /> + } + label="Create a scene" + onPress={onPressCreateScene} + /> + {store.me.memberships + ? store.me.memberships.memberships.map((membership, i) => ( + + } + label={membership.displayName || membership.handle} + url={`/profile/${membership.handle}`} + /> + )) + : undefined} + + + + Build version {VersionNumber.appVersion} ({VersionNumber.buildVersion} + ) + + + + ) +} + +const styles = StyleSheet.create({ + view: { + flex: 1, + backgroundColor: colors.white, + }, + section: { + paddingHorizontal: 10, + paddingTop: 10, + paddingBottom: 10, + borderBottomWidth: 1, + borderBottomColor: colors.gray1, + }, + heading: { + fontSize: 16, + fontWeight: 'bold', + paddingVertical: 8, + paddingHorizontal: 4, + }, + + searchBtn: { + flexDirection: 'row', + backgroundColor: colors.gray1, + borderRadius: 8, + margin: 10, + marginBottom: 0, + paddingVertical: 10, + paddingHorizontal: 12, + }, + searchBtnLabel: { + marginLeft: 8, + fontSize: 18, + color: colors.gray6, + }, + + menuItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 8, + paddingHorizontal: 2, + }, + menuItemIconWrapper: { + width: 30, + height: 30, + alignItems: 'center', + justifyContent: 'center', + marginRight: 10, + }, + menuItemLabel: { + fontSize: 17, + color: colors.gray7, + }, + menuItemLabelBold: { + fontWeight: 'bold', + }, + menuItemCount: { + position: 'absolute', + right: -6, + top: -2, + backgroundColor: colors.red3, + paddingHorizontal: 4, + paddingBottom: 1, + borderRadius: 6, + }, + menuItemCountLabel: { + fontSize: 12, + fontWeight: 'bold', + color: colors.white, + }, + + footer: { + paddingHorizontal: 14, + paddingVertical: 18, + }, +}) diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx index 9acbdeca9..ef54af171 100644 --- a/src/view/shell/mobile/index.tsx +++ b/src/view/shell/mobile/index.tsx @@ -24,6 +24,7 @@ import {useStores} from '../../../state' import {NavigationModel} from '../../../state/models/navigation' import {match, MatchResult} from '../../routes' import {Login} from '../../screens/Login' +import {Menu} from './Menu' import {Onboard} from '../../screens/Onboard' import {HorzSwipe} from '../../com/util/gestures/HorzSwipe' import {Modal} from '../../com/modals/Modal' @@ -109,6 +110,7 @@ const Btn = ({ export const MobileShell: React.FC = observer(() => { const store = useStores() + const [isMenuActive, setMenuActive] = useState(false) const [isTabsSelectorActive, setTabsSelectorActive] = useState(false) const scrollElRef = useRef() const winDim = useWindowDimensions() @@ -121,6 +123,9 @@ export const MobileShell: React.FC = observer(() => { const screenRenderDesc = constructScreenRenderDesc(store.nav) const onPressHome = () => { + if (isMenuActive) { + setMenuActive(false) + } if (store.nav.tab.fixedTabPurpose === 0) { if (store.nav.tab.current.url === '/') { scrollElRef.current?.scrollToOffset({offset: 0}) @@ -135,6 +140,9 @@ export const MobileShell: React.FC = observer(() => { } } const onPressNotifications = () => { + if (isMenuActive) { + setMenuActive(false) + } if (store.nav.tab.fixedTabPurpose === 1) { store.nav.tab.fixedTabReset() } else { @@ -203,15 +211,44 @@ export const MobileShell: React.FC = observer(() => { // navigation swipes // = + const canSwipeLeft = store.nav.tab.canGoBack || !isMenuActive + const canSwipeRight = isMenuActive const onNavSwipeEnd = (dx: number) => { - if (dx < 0 && store.nav.tab.canGoBack) { - store.nav.tab.goBack() + if (dx < 0) { + if (store.nav.tab.canGoBack) { + store.nav.tab.goBack() + } else { + setMenuActive(true) + } + } else if (dx > 0) { + if (isMenuActive) { + setMenuActive(false) + } } } - const swipeTransform = { - transform: [ - {translateX: Animated.multiply(swipeGestureInterp, winDim.width * -1)}, - ], + const swipeTranslateX = Animated.multiply( + swipeGestureInterp, + winDim.width * -1, + ) + const swipeTransform = store.nav.tab.canGoBack + ? {transform: [{translateX: swipeTranslateX}]} + : undefined + let menuTranslateX + if (isMenuActive) { + // menu is active, interpret swipes as closes + menuTranslateX = Animated.multiply(swipeGestureInterp, winDim.width * -1) + } else if (!store.nav.tab.canGoBack) { + // at back of history, interpret swipes as opens + menuTranslateX = Animated.subtract( + winDim.width * -1, + Animated.multiply(swipeGestureInterp, winDim.width), + ) + } else { + // not at back of history, leave off screen + menuTranslateX = winDim.width * -1 + } + const menuSwipeTransform = { + transform: [{translateX: menuTranslateX}], } const swipeOpacity = { opacity: swipeGestureInterp.interpolate({ @@ -219,12 +256,13 @@ export const MobileShell: React.FC = observer(() => { outputRange: [0, 0.6, 0], }), } - const tabMenuTransform = { - transform: [{translateY: Animated.multiply(tabMenuInterp.value, -320)}], - } - const newTabTransform = { - transform: [{scale: newTabInterp}], - } + // TODO + // const tabMenuTransform = { + // transform: [{translateY: Animated.multiply(tabMenuInterp, -320)}], + // } + // const newTabTransform = { + // transform: [{scale: newTabInterp}], + // } if (!store.session.hasSession) { return ( @@ -252,6 +290,7 @@ export const MobileShell: React.FC = observer(() => { const isAtHome = store.nav.tab.current.url === '/' const isAtNotifications = store.nav.tab.current.url === '/notifications' + return ( @@ -260,11 +299,21 @@ export const MobileShell: React.FC = observer(() => { useNativeDriver panX={swipeGestureInterp} swipeEnabled - canSwipeLeft={store.nav.tab.canGoBack} + canSwipeLeft={canSwipeLeft} + canSwipeRight={canSwipeRight} onSwipeEnd={onNavSwipeEnd}> {screenRenderDesc.screens.map( ({Com, navIdx, params, key, current, previous}) => { + if (isMenuActive) { + // HACK menu is active, treat current as previous + if (previous) { + previous = false + } else if (current) { + current = false + previous = true + } + } return ( { }, )} + + setMenuActive(false)} /> + {isTabsSelectorActive ? ( @@ -423,6 +475,17 @@ const styles = StyleSheet.create({ backgroundColor: '#000', opacity: 0.5, }, + menuDrawer: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + borderTopWidth: 1, + borderTopColor: colors.gray2, + borderRightWidth: 1, + borderRightColor: colors.gray2, + }, topBarProtector: { position: 'absolute', top: 0, -- cgit 1.4.1