diff options
Diffstat (limited to 'src/view/shell')
-rw-r--r-- | src/view/shell/mobile/Menu.tsx | 269 | ||||
-rw-r--r-- | src/view/shell/mobile/index.tsx | 89 |
2 files changed, 345 insertions, 13 deletions
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 + }) => ( + <TouchableOpacity + style={styles.menuItem} + onPress={onPress ? onPress : () => onNavigate(url || '/')}> + <View style={[styles.menuItemIconWrapper]}> + {icon} + {count ? ( + <View style={styles.menuItemCount}> + <Text style={styles.menuItemCountLabel}>{count}</Text> + </View> + ) : undefined} + </View> + <Text + style={[ + styles.menuItemLabel, + bold ? styles.menuItemLabelBold : undefined, + ]} + numberOfLines={1}> + {label} + </Text> + </TouchableOpacity> + ) + + return ( + <View style={styles.view}> + <TouchableOpacity + style={styles.searchBtn} + onPress={() => onNavigate('/search')}> + <MagnifyingGlassIcon + style={{color: colors.gray5} as StyleProp<ViewStyle>} + size={21} + /> + <Text style={styles.searchBtnLabel}>Search</Text> + </TouchableOpacity> + <View style={styles.section}> + <MenuItem + icon={ + <UserAvatar + size={24} + displayName={store.me.displayName} + handle={store.me.handle} + avatar={store.me.avatar} + /> + } + label={store.me.displayName || store.me.handle} + bold + url={`/profile/${store.me.handle}`} + /> + <MenuItem + icon={ + <HomeIcon + style={{color: colors.gray5} as StyleProp<ViewStyle>} + size="24" + /> + } + label="Home" + url="/" + /> + <MenuItem + icon={ + <BellIcon + style={{color: colors.gray5} as StyleProp<ViewStyle>} + size="24" + /> + } + label="Notifications" + url="/notifications" + count={store.me.notificationCount} + /> + <MenuItem + icon={ + <CogIcon + style={{color: colors.gray6} as StyleProp<ViewStyle>} + size="24" + strokeWidth={2} + /> + } + label="Settings" + url="/settings" + /> + </View> + <View style={styles.section}> + <Text style={styles.heading}>Scenes</Text> + <MenuItem + icon={ + <UserGroupIcon + style={{color: colors.gray6} as StyleProp<ViewStyle>} + size="24" + /> + } + label="Create a scene" + onPress={onPressCreateScene} + /> + {store.me.memberships + ? store.me.memberships.memberships.map((membership, i) => ( + <MenuItem + key={i} + icon={ + <UserAvatar + size={24} + displayName={membership.displayName} + handle={membership.handle} + avatar={membership.avatar} + /> + } + label={membership.displayName || membership.handle} + url={`/profile/${membership.handle}`} + /> + )) + : undefined} + </View> + <View style={styles.footer}> + <Text style={s.gray4}> + Build version {VersionNumber.appVersion} ({VersionNumber.buildVersion} + ) + </Text> + </View> + </View> + ) +} + +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<FlatList | undefined>() 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 ( <View style={styles.outerContainer}> <SafeAreaView style={styles.innerContainer}> @@ -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}> <ScreenContainer style={styles.screenContainer}> {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 ( <Screen key={key} @@ -299,6 +348,9 @@ export const MobileShell: React.FC = observer(() => { }, )} </ScreenContainer> + <Animated.View style={[styles.menuDrawer, menuSwipeTransform]}> + <Menu visible={isMenuActive} onClose={() => setMenuActive(false)} /> + </Animated.View> </HorzSwipe> </SafeAreaView> {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, |