diff --git a/NOTICE.txt b/NOTICE.txt index 700ea5a79..be76476b0 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1433,7 +1433,7 @@ SOFTWARE. ## react-native-drawer-layout -This product contains 'react-native-drawer-layout' by Brent Vatne. +This product contains a modified version of 'react-native-drawer-layout' by Brent Vatne. A platform-agnostic drawer layout. Pure JavaScript implementation on iOS and native implementation on Android. Why? Because the drawer layout is a useful component regardless of the platform! And if you can use it without changing any code, that's perfect diff --git a/app/components/sidebars/drawer_layout.js b/app/components/sidebars/drawer_layout.js new file mode 100644 index 000000000..bb4d0ee90 --- /dev/null +++ b/app/components/sidebars/drawer_layout.js @@ -0,0 +1,439 @@ +/* eslint-disable */ +// Original work: https://github.com/react-native-community/react-native-drawer-layout . +// Modified work: Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +// @flow + +import React, { Component } from 'react'; +import { + Animated, + Dimensions, + Keyboard, + PanResponder, + StyleSheet, + TouchableWithoutFeedback, + View, + I18nManager, +} from 'react-native'; + +const MIN_SWIPE_DISTANCE = 3; +const DEVICE_WIDTH = parseFloat(Dimensions.get('window').width); +const THRESHOLD = DEVICE_WIDTH / 2; +const VX_MAX = 0.1; + +const IDLE = 'Idle'; +const DRAGGING = 'Dragging'; +const SETTLING = 'Settling'; + +export type PropType = { + children: any, + drawerBackgroundColor?: string, + drawerLockMode?: 'unlocked' | 'locked-closed' | 'locked-open', + drawerPosition: 'left' | 'right', + drawerWidth: number, + keyboardDismissMode?: 'none' | 'on-drag', + onDrawerClose?: Function, + onDrawerOpen?: Function, + onDrawerSlide?: Function, + onDrawerStateChanged?: Function, + renderNavigationView: () => any, + statusBarBackgroundColor?: string, + useNativeAnimations?: boolean, +}; + +export type StateType = { + accessibilityViewIsModal: boolean, + drawerShown: boolean, + openValue: any, +}; + +export type EventType = { + stopPropagation: Function, +}; + +export type PanResponderEventType = { + dx: number, + dy: number, + moveX: number, + moveY: number, + vx: number, + vy: number, +}; + +export type DrawerMovementOptionType = { + velocity?: number, +}; + +export default class DrawerLayout extends Component { + props: PropType; + state: StateType; + _lastOpenValue: number; + _panResponder: any; + _isClosing: boolean; + _closingAnchorValue: number; + canClose: boolean; + + static defaultProps = { + drawerWidth: 0, + drawerPosition: 'left', + useNativeAnimations: false, + }; + + static positions = { + Left: 'left', + Right: 'right', + }; + + constructor(props: PropType, context: any) { + super(props, context); + + this.canClose = true; + this.state = { + accessibilityViewIsModal: false, + drawerShown: false, + openValue: new Animated.Value(0), + }; + } + + getDrawerPosition() { + const { drawerPosition } = this.props; + const rtl = I18nManager.isRTL; + return rtl + ? drawerPosition === 'left' ? 'right' : 'left' // invert it + : drawerPosition; + } + + componentWillMount() { + const { openValue } = this.state; + + openValue.addListener(({ value }) => { + const drawerShown = value > 0; + const accessibilityViewIsModal = drawerShown; + if (drawerShown !== this.state.drawerShown) { + this.setState({ drawerShown, accessibilityViewIsModal }); + } + + if (this.props.keyboardDismissMode === 'on-drag') { + Keyboard.dismiss(); + } + + this._lastOpenValue = value; + if (this.props.onDrawerSlide) { + this.props.onDrawerSlide({ nativeEvent: { offset: value } }); + } + }); + + this._panResponder = PanResponder.create({ + onMoveShouldSetPanResponder: this._shouldSetPanResponder, + onPanResponderGrant: this._panResponderGrant, + onPanResponderMove: this._panResponderMove, + onPanResponderTerminationRequest: () => false, + onPanResponderRelease: this._panResponderRelease, + onPanResponderTerminate: () => {}, + }); + } + + render() { + const { accessibilityViewIsModal, drawerShown, openValue } = this.state; + + const { + drawerBackgroundColor, + drawerWidth, + drawerPosition, + } = this.props; + + /** + * We need to use the "original" drawer position here + * as RTL turns position left and right on its own + **/ + const dynamicDrawerStyles = { + backgroundColor: drawerBackgroundColor, + width: drawerWidth, + left: drawerPosition === 'left' ? 0 : null, + right: drawerPosition === 'right' ? 0 : null, + }; + + /* Drawer styles */ + let outputRange; + + if (this.getDrawerPosition() === 'left') { + outputRange = [-drawerWidth, 0]; + } else { + outputRange = [drawerWidth, 0]; + } + + const drawerTranslateX = openValue.interpolate({ + inputRange: [0, 1], + outputRange, + extrapolate: 'clamp', + }); + const animatedDrawerStyles = { + transform: [{ translateX: drawerTranslateX }], + }; + + /* Overlay styles */ + const overlayOpacity = openValue.interpolate({ + inputRange: [0, 1], + outputRange: [0, 0.7], + extrapolate: 'clamp', + }); + const animatedOverlayStyles = { opacity: overlayOpacity }; + const pointerEvents = drawerShown ? 'auto' : 'none'; + + return ( + + + {this.props.children} + + + + + + {this.props.renderNavigationView()} + + + ); + } + + _onOverlayClick = (e: EventType) => { + e.stopPropagation(); + if (!this._isLockedClosed() && !this._isLockedOpen()) { + this.closeDrawer(); + } + }; + + _emitStateChanged = (newState: string) => { + if (this.props.onDrawerStateChanged) { + this.props.onDrawerStateChanged(newState); + } + }; + + openDrawer = (options: DrawerMovementOptionType = {}) => { + this._emitStateChanged(SETTLING); + Animated.spring(this.state.openValue, { + toValue: 1, + bounciness: 0, + restSpeedThreshold: 0.1, + useNativeDriver: this.props.useNativeAnimations, + ...options, + }).start(() => { + if (this.props.onDrawerOpen) { + this.props.onDrawerOpen(); + } + this._emitStateChanged(IDLE); + }); + }; + + closeDrawer = (options: DrawerMovementOptionType = {}) => { + this._emitStateChanged(SETTLING); + Animated.spring(this.state.openValue, { + toValue: 0, + bounciness: 0, + restSpeedThreshold: 1, + useNativeDriver: this.props.useNativeAnimations, + ...options, + }).start(() => { + if (this.props.onDrawerClose) { + this.props.onDrawerClose(); + } + this._emitStateChanged(IDLE); + }); + }; + + _handleDrawerOpen = () => { + if (this.props.onDrawerOpen) { + this.props.onDrawerOpen(); + } + }; + + _handleDrawerClose = () => { + if (this.props.onDrawerClose) { + this.props.onDrawerClose(); + } + }; + + _shouldSetPanResponder = ( + e: EventType, + { moveX, dx, dy }: PanResponderEventType, + ) => { + if (!dx || !dy || Math.abs(dx) < MIN_SWIPE_DISTANCE) { + return false; + } + + if (this._isLockedClosed() || this._isLockedOpen() || !this.canClose) { + return false; + } + + if (this.getDrawerPosition() === 'left') { + const overlayArea = DEVICE_WIDTH - + (DEVICE_WIDTH - this.props.drawerWidth); + + if (this._lastOpenValue === 1) { + if ( + (dx < 0 && Math.abs(dx) > Math.abs(dy) * 3) || + moveX > overlayArea + ) { + this._isClosing = true; + this._closingAnchorValue = this._getOpenValueForX(moveX); + return true; + } + } else { + if (moveX <= 35 && dx > 0) { + this._isClosing = false; + return true; + } + + return false; + } + } else { + const overlayArea = DEVICE_WIDTH - this.props.drawerWidth; + + if (this._lastOpenValue === 1) { + if ( + (dx > 0 && Math.abs(dx) > Math.abs(dy) * 3) || + moveX < overlayArea + ) { + this._isClosing = true; + this._closingAnchorValue = this._getOpenValueForX(moveX); + return true; + } + } else { + if (moveX >= DEVICE_WIDTH - 35 && dx < 0) { + this._isClosing = false; + return true; + } + + return false; + } + } + }; + + _panResponderGrant = () => { + this._emitStateChanged(DRAGGING); + }; + + _panResponderMove = (e: EventType, { moveX }: PanResponderEventType) => { + let openValue = this._getOpenValueForX(moveX); + + if (this._isClosing) { + openValue = 1 - (this._closingAnchorValue - openValue); + } + + if (openValue > 1) { + openValue = 1; + } else if (openValue < 0) { + openValue = 0; + } + + this.state.openValue.setValue(openValue); + }; + + _panResponderRelease = ( + e: EventType, + { moveX, vx }: PanResponderEventType, + ) => { + const previouslyOpen = this._isClosing; + const isWithinVelocityThreshold = vx < VX_MAX && vx > -VX_MAX; + + if (this.getDrawerPosition() === 'left') { + if ( + (vx > 0 && moveX > THRESHOLD) || + vx >= VX_MAX || + (isWithinVelocityThreshold && + previouslyOpen && + moveX > THRESHOLD) + ) { + this.openDrawer({ velocity: vx }); + } else if ( + (vx < 0 && moveX < THRESHOLD) || + vx < -VX_MAX || + (isWithinVelocityThreshold && !previouslyOpen) + ) { + this.closeDrawer({ velocity: vx }); + } else if (previouslyOpen) { + this.openDrawer(); + } else { + this.closeDrawer(); + } + } else { + if ( + (vx < 0 && moveX < THRESHOLD) || + vx <= -VX_MAX || + (isWithinVelocityThreshold && + previouslyOpen && + moveX < THRESHOLD) + ) { + this.openDrawer({ velocity: (-1) * vx }); + } else if ( + (vx > 0 && moveX > THRESHOLD) || + vx > VX_MAX || + (isWithinVelocityThreshold && !previouslyOpen) + ) { + this.closeDrawer({ velocity: (-1) * vx }); + } else if (previouslyOpen) { + this.openDrawer(); + } else { + this.closeDrawer(); + } + } + }; + + _isLockedClosed = () => { + return this.props.drawerLockMode === 'locked-closed' && + !this.state.drawerShown; + }; + + _isLockedOpen = () => { + return this.props.drawerLockMode === 'locked-open' && + this.state.drawerShown; + }; + + _getOpenValueForX(x: number): number { + const { drawerWidth } = this.props; + + if (this.getDrawerPosition() === 'left') { + return x / drawerWidth; + } + + // position === 'right' + return (DEVICE_WIDTH - x) / drawerWidth; + } +} + +const styles = StyleSheet.create({ + drawer: { + position: 'absolute', + top: 0, + bottom: 0, + zIndex: 1001, + }, + main: { + flex: 1, + zIndex: 0, + }, + overlay: { + backgroundColor: '#000', + position: 'absolute', + top: 0, + left: 0, + bottom: 0, + right: 0, + zIndex: 1000, + }, +}); diff --git a/app/components/sidebars/main/main_sidebar.js b/app/components/sidebars/main/main_sidebar.js index 4b37d9e13..1b28ba2e1 100644 --- a/app/components/sidebars/main/main_sidebar.js +++ b/app/components/sidebars/main/main_sidebar.js @@ -10,12 +10,12 @@ import { View, } from 'react-native'; import {intlShape} from 'react-intl'; -import DrawerLayout from 'react-native-drawer-layout'; import {General, WebsocketEvents} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; import SafeAreaView from 'app/components/safe_area_view'; +import DrawerLayout from 'app/components/sidebars/drawer_layout'; import tracker from 'app/utils/time_tracker'; import {t} from 'app/utils/i18n'; @@ -50,8 +50,6 @@ export default class ChannelSidebar extends Component { intl: intlShape.isRequired, }; - swiperIndex = 1; - constructor(props) { super(props); @@ -60,9 +58,9 @@ export default class ChannelSidebar extends Component { openDrawerOffset = DRAWER_LANDSCAPE_OFFSET; } + this.swiperIndex = 1; this.state = { show: false, - lockMode: 'unlocked', openDrawerOffset, drawerOpened: false, }; @@ -102,7 +100,7 @@ export default class ChannelSidebar extends Component { return nextProps.currentTeamId !== currentTeamId || nextProps.isLandscape !== isLandscape || nextProps.deviceWidth !== deviceWidth || - nextProps.teamsCount !== teamsCount || this.state.lockMode !== nextState.lockMode; + nextProps.teamsCount !== teamsCount; } componentWillUnmount() { @@ -257,10 +255,13 @@ export default class ChannelSidebar extends Component { onPageSelected = (index) => { this.swiperIndex = index; - if (this.swiperIndex === 0) { - this.setState({lockMode: 'locked-open'}); - } else { - this.setState({lockMode: 'unlocked'}); + + if (this.refs.drawer) { + if (this.swiperIndex === 0) { + this.refs.drawer.canClose = false; + } else { + this.refs.drawer.canClose = true; + } } }; @@ -272,10 +273,16 @@ export default class ChannelSidebar extends Component { if (isLandscape || isTablet) { openDrawerOffset = DRAWER_LANDSCAPE_OFFSET; } + if (this.refs.drawer) { + this.refs.drawer.canClose = true; + } this.setState({openDrawerOffset}); }; onSearchStart = () => { + if (this.refs.drawer) { + this.refs.drawer.canClose = false; + } this.setState({openDrawerOffset: 0}); }; @@ -372,11 +379,10 @@ export default class ChannelSidebar extends Component { render() { const {children, deviceWidth} = this.props; - const {lockMode, openDrawerOffset} = this.state; + const {openDrawerOffset} = this.state; return (