diff --git a/src/components/ScreenWrapper/BaseScreenWrapper.js b/src/components/ScreenWrapper/BaseScreenWrapper.js index a3ebdd4eb6a2..83408e6a36fe 100644 --- a/src/components/ScreenWrapper/BaseScreenWrapper.js +++ b/src/components/ScreenWrapper/BaseScreenWrapper.js @@ -43,6 +43,18 @@ class BaseScreenWrapper extends React.Component { }); } + /** + * We explicitly want to ignore if props.modal changes, and only want to rerender if + * any of the other props **used for the rendering output** is changed. + * @param {Object} nextProps + * @param {Object} nextState + * @returns {boolean} + */ + shouldComponentUpdate(nextProps, nextState) { + return !_.isEqual(this.state, nextState) + || !_.isEqual(_.omit(this.props, 'modal'), _.omit(nextProps, 'modal')); + } + componentWillUnmount() { if (this.unsubscribeEscapeKey) { this.unsubscribeEscapeKey(); diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js index 9f200ec9356d..14ca7bc531e5 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js @@ -1,145 +1,42 @@ -import lodashGet from 'lodash/get'; -import _ from 'underscore'; import React, {Component} from 'react'; import {View} from 'react-native'; -import PropTypes from 'prop-types'; import styles from '../../../../styles/styles'; import SidebarLinks from '../SidebarLinks'; -import PopoverMenu from '../../../../components/PopoverMenu'; -import FloatingActionButton from '../../../../components/FloatingActionButton'; import ScreenWrapper from '../../../../components/ScreenWrapper'; -import compose from '../../../../libs/compose'; import Navigation from '../../../../libs/Navigation/Navigation'; import ROUTES from '../../../../ROUTES'; import Timing from '../../../../libs/actions/Timing'; import CONST from '../../../../CONST'; -import * as Expensicons from '../../../../components/Icon/Expensicons'; -import Permissions from '../../../../libs/Permissions'; -import * as Policy from '../../../../libs/actions/Policy'; import Performance from '../../../../libs/Performance'; -import * as Welcome from '../../../../libs/actions/Welcome'; -import {sidebarPropTypes, sidebarDefaultProps} from './sidebarPropTypes'; import withDrawerState from '../../../../components/withDrawerState'; -import withNavigationFocus from '../../../../components/withNavigationFocus'; import KeyboardShortcutsModal from '../../../../components/KeyboardShortcutsModal'; +import withWindowDimensions, {windowDimensionsPropTypes} from '../../../../components/withWindowDimensions'; +import compose from '../../../../libs/compose'; +import sidebarPropTypes from './sidebarPropTypes'; const propTypes = { - - /** Callback function when the menu is shown */ - onShowCreateMenu: PropTypes.func, - - /** Callback function before the menu is hidden */ - onHideCreateMenu: PropTypes.func, - - /** reportID in the current navigation state */ - reportIDFromRoute: PropTypes.string, - ...sidebarPropTypes, -}; -const defaultProps = { - onHideCreateMenu: () => {}, - onShowCreateMenu: () => {}, - ...sidebarDefaultProps, + ...windowDimensionsPropTypes, }; class BaseSidebarScreen extends Component { constructor(props) { super(props); - this.hideCreateMenu = this.hideCreateMenu.bind(this); this.startTimer = this.startTimer.bind(this); - this.showCreateMenu = this.showCreateMenu.bind(this); - - this.state = { - isCreateMenuActive: false, - }; + this.navigateToSettings = this.navigateToSettings.bind(this); } componentDidMount() { Performance.markStart(CONST.TIMING.SIDEBAR_LOADED); Timing.start(CONST.TIMING.SIDEBAR_LOADED, true); - - const routes = lodashGet(this.props.navigation.getState(), 'routes', []); - Welcome.show({routes, showCreateMenu: this.showCreateMenu}); - } - - componentDidUpdate(prevProps) { - if (!this.didScreenBecomeInactive(prevProps)) { - return; - } - - // Hide menu manually when other pages are opened using shortcut key - this.hideCreateMenu(); } /** - * Check if LHN status changed from active to inactive. - * Used to close already opened FAB menu when open any other pages (i.e. Press Command + K on web). - * - * @param {Object} prevProps - * @return {Boolean} + * Method called when avatar is clicked */ - didScreenBecomeInactive(prevProps) { - // When the Drawer gets closed and ReportScreen is shown - if (!this.props.isDrawerOpen && prevProps.isDrawerOpen) { - return true; - } - - // When any other page is opened over LHN - if (!this.props.isFocused && prevProps.isFocused) { - return true; - } - - return false; - } - - /** - * Check if LHN is inactive. - * Used to prevent FAB menu showing after opening any other pages. - * - * @return {Boolean} - */ - isScreenInactive() { - // When drawer is closed and Report page is open - if (this.props.isSmallScreenWidth && !this.props.isDrawerOpen) { - return true; - } - - // When any other page is open - if (!this.props.isFocused) { - return true; - } - - return false; - } - - /** - * Method called when we click the floating action button - */ - showCreateMenu() { - if (this.isScreenInactive()) { - // Prevent showing menu when click FAB icon quickly after opening other pages - return; - } - this.setState({ - isCreateMenuActive: true, - }); - this.props.onShowCreateMenu(); - } - - /** - * Method called either when: - * Pressing the floating action button to open the CreateMenu modal - * Selecting an item on CreateMenu or closing it by clicking outside of the modal component - */ - hideCreateMenu() { - if (!this.state.isCreateMenuActive) { - return; - } - this.props.onHideCreateMenu(); - this.setState({ - isCreateMenuActive: false, - }); + navigateToSettings() { + Navigation.navigate(ROUTES.SETTINGS); } /** @@ -151,8 +48,6 @@ class BaseSidebarScreen extends Component { } render() { - // Workspaces are policies with type === 'free' - const workspaces = _.filter(this.props.allPolicies, policy => policy && policy.type === CONST.POLICY.TYPE.FREE); return ( - - Navigation.navigate(ROUTES.NEW_CHAT), - }, - { - icon: Expensicons.Users, - text: this.props.translate('sidebarScreen.newGroup'), - onSelected: () => Navigation.navigate(ROUTES.NEW_GROUP), - }, - ...(Permissions.canUsePolicyRooms(this.props.betas) && workspaces.length ? [ - { - icon: Expensicons.Hashtag, - text: this.props.translate('sidebarScreen.newRoom'), - onSelected: () => Navigation.navigate(ROUTES.WORKSPACE_NEW_ROOM), - }, - ] : []), - ...(Permissions.canUseIOUSend(this.props.betas) ? [ - { - icon: Expensicons.Send, - text: this.props.translate('iou.sendMoney'), - onSelected: () => Navigation.navigate(ROUTES.IOU_SEND), - }, - ] : []), - ...(Permissions.canUseIOU(this.props.betas) ? [ - { - icon: Expensicons.MoneyCircle, - text: this.props.translate('iou.requestMoney'), - onSelected: () => Navigation.navigate(ROUTES.IOU_REQUEST), - }, - ] : []), - ...(Permissions.canUseIOU(this.props.betas) ? [ - { - icon: Expensicons.Receipt, - text: this.props.translate('iou.splitBill'), - onSelected: () => Navigation.navigate(ROUTES.IOU_BILL), - }, - ] : []), - ...(!Policy.hasActiveFreePolicy(this.props.allPolicies) ? [ - { - icon: Expensicons.NewWorkspace, - iconWidth: 46, - iconHeight: 40, - text: this.props.translate('workspace.new.newWorkspace'), - description: this.props.translate('workspace.new.getTheExpensifyCardAndMore'), - onSelected: () => Policy.createWorkspace(), - }, - ] : []), - ]} - /> + {this.props.children} )} @@ -242,9 +75,8 @@ class BaseSidebarScreen extends Component { } BaseSidebarScreen.propTypes = propTypes; -BaseSidebarScreen.defaultProps = defaultProps; export default compose( + withWindowDimensions, withDrawerState, - withNavigationFocus, )(BaseSidebarScreen); diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js new file mode 100644 index 000000000000..465902df23d0 --- /dev/null +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -0,0 +1,252 @@ +import React from 'react'; +import _ from 'underscore'; +import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; +import lodashGet from 'lodash/get'; +import {View} from 'react-native'; +import styles from '../../../../styles/styles'; +import * as Expensicons from '../../../../components/Icon/Expensicons'; +import Navigation from '../../../../libs/Navigation/Navigation'; +import ROUTES from '../../../../ROUTES'; +import Permissions from '../../../../libs/Permissions'; +import * as Policy from '../../../../libs/actions/Policy'; +import PopoverMenu from '../../../../components/PopoverMenu'; +import CONST from '../../../../CONST'; +import FloatingActionButton from '../../../../components/FloatingActionButton'; +import compose from '../../../../libs/compose'; +import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize'; +import withWindowDimensions from '../../../../components/withWindowDimensions'; +import ONYXKEYS from '../../../../ONYXKEYS'; +import withNavigation from '../../../../components/withNavigation'; +import * as Welcome from '../../../../libs/actions/Welcome'; +import withNavigationFocus from '../../../../components/withNavigationFocus'; +import withDrawerState from '../../../../components/withDrawerState'; + +/** + * @param {Object} [policy] + * @returns {Object|undefined} + */ +const policySelector = policy => policy && ({ + type: policy.type, + role: policy.role, +}); + +const propTypes = { + /* Callback function when the menu is shown */ + onShowCreateMenu: PropTypes.func, + + /* Callback function before the menu is hidden */ + onHideCreateMenu: PropTypes.func, + + /** The list of policies the user has access to. */ + allPolicies: PropTypes.shape({ + /** The policy name */ + name: PropTypes.string, + }), + + /* Beta features list */ + betas: PropTypes.arrayOf(PropTypes.string), + + ...withLocalizePropTypes, +}; +const defaultProps = { + onHideCreateMenu: () => {}, + onShowCreateMenu: () => {}, + allPolicies: {}, + betas: [], +}; + +/** + * Responsible for rendering the {@link PopoverMenu}, and the accompanying + * FAB that can open or close the menu. + */ +class FloatingActionButtonAndPopover extends React.Component { + constructor(props) { + super(props); + + this.showCreateMenu = this.showCreateMenu.bind(this); + this.hideCreateMenu = this.hideCreateMenu.bind(this); + + this.state = { + isCreateMenuActive: false, + }; + } + + componentDidMount() { + const routes = lodashGet(this.props.navigation.getState(), 'routes', []); + Welcome.show({routes, showCreateMenu: this.showCreateMenu}); + } + + componentDidUpdate(prevProps) { + if (!this.didScreenBecomeInactive(prevProps)) { + return; + } + + // Hide menu manually when other pages are opened using shortcut key + this.hideCreateMenu(); + } + + /** + * Check if LHN status changed from active to inactive. + * Used to close already opened FAB menu when open any other pages (i.e. Press Command + K on web). + * + * @param {Object} prevProps + * @return {Boolean} + */ + didScreenBecomeInactive(prevProps) { + // When the Drawer gets closed and ReportScreen is shown + if (!this.props.isDrawerOpen && prevProps.isDrawerOpen) { + return true; + } + + // When any other page is opened over LHN + if (!this.props.isFocused && prevProps.isFocused) { + return true; + } + + return false; + } + + /** + * Check if LHN is inactive. + * Used to prevent FAB menu showing after opening any other pages. + * + * @return {Boolean} + */ + isScreenInactive() { + // When drawer is closed and Report page is open + if (this.props.isSmallScreenWidth && !this.props.isDrawerOpen) { + return true; + } + + // When any other page is open + if (!this.props.isFocused) { + return true; + } + + return false; + } + + /** + * Method called when we click the floating action button + */ + showCreateMenu() { + if (this.isScreenInactive()) { + // Prevent showing menu when click FAB icon quickly after opening other pages + return; + } + this.setState({ + isCreateMenuActive: true, + }); + this.props.onShowCreateMenu(); + } + + /** + * Method called either when: + * - Pressing the floating action button to open the CreateMenu modal + * - Selecting an item on CreateMenu or closing it by clicking outside of the modal component + */ + hideCreateMenu() { + if (this.isScreenInactive()) { + // Prevent showing menu when click FAB icon quickly after opening other pages + return; + } + this.props.onHideCreateMenu(); + this.setState({ + isCreateMenuActive: false, + }); + } + + render() { + // Workspaces are policies with type === 'free' + const workspaces = _.filter(this.props.allPolicies, policy => policy && policy.type === CONST.POLICY.TYPE.FREE); + + return ( + + Navigation.navigate(ROUTES.NEW_CHAT), + }, + { + icon: Expensicons.Users, + text: this.props.translate('sidebarScreen.newGroup'), + onSelected: () => Navigation.navigate(ROUTES.NEW_GROUP), + }, + ...(Permissions.canUsePolicyRooms(this.props.betas) && workspaces.length ? [ + { + icon: Expensicons.Hashtag, + text: this.props.translate('sidebarScreen.newRoom'), + onSelected: () => Navigation.navigate(ROUTES.WORKSPACE_NEW_ROOM), + }, + ] : []), + ...(Permissions.canUseIOUSend(this.props.betas) ? [ + { + icon: Expensicons.Send, + text: this.props.translate('iou.sendMoney'), + onSelected: () => Navigation.navigate(ROUTES.IOU_SEND), + }, + ] : []), + ...(Permissions.canUseIOU(this.props.betas) ? [ + { + icon: Expensicons.MoneyCircle, + text: this.props.translate('iou.requestMoney'), + onSelected: () => Navigation.navigate(ROUTES.IOU_REQUEST), + }, + ] : []), + ...(Permissions.canUseIOU(this.props.betas) ? [ + { + icon: Expensicons.Receipt, + text: this.props.translate('iou.splitBill'), + onSelected: () => Navigation.navigate(ROUTES.IOU_BILL), + }, + ] : []), + ...(!Policy.hasActiveFreePolicy(this.props.allPolicies) ? [ + { + icon: Expensicons.NewWorkspace, + iconWidth: 46, + iconHeight: 40, + text: this.props.translate('workspace.new.newWorkspace'), + description: this.props.translate('workspace.new.getTheExpensifyCardAndMore'), + onSelected: () => Policy.createWorkspace(), + }, + ] : []), + ]} + /> + + + ); + } +} + +FloatingActionButtonAndPopover.propTypes = propTypes; +FloatingActionButtonAndPopover.defaultProps = defaultProps; + +export default compose( + withLocalize, + withNavigation, + withNavigationFocus, + withDrawerState, + withWindowDimensions, + withOnyx({ + allPolicies: { + key: ONYXKEYS.COLLECTION.POLICY, + selector: policySelector, + }, + betas: { + key: ONYXKEYS.BETAS, + }, + }), +)(FloatingActionButtonAndPopover); diff --git a/src/pages/home/sidebar/SidebarScreen/PopoverModal.js b/src/pages/home/sidebar/SidebarScreen/PopoverModal.js deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/src/pages/home/sidebar/SidebarScreen/index.js b/src/pages/home/sidebar/SidebarScreen/index.js index 584bcc49f8e4..552b0e2dc180 100755 --- a/src/pages/home/sidebar/SidebarScreen/index.js +++ b/src/pages/home/sidebar/SidebarScreen/index.js @@ -1,52 +1,40 @@ -import React from 'react'; -import {withOnyx} from 'react-native-onyx'; -import compose from '../../../../libs/compose'; -import withWindowDimensions from '../../../../components/withWindowDimensions'; -import withLocalize from '../../../../components/withLocalize'; -import ONYXKEYS from '../../../../ONYXKEYS'; -import {sidebarPropTypes, sidebarDefaultProps} from './sidebarPropTypes'; +import React, {useRef} from 'react'; +import sidebarPropTypes from './sidebarPropTypes'; import BaseSidebarScreen from './BaseSidebarScreen'; +import FloatingActionButtonAndPopover from './FloatingActionButtonAndPopover'; const SidebarScreen = (props) => { - let baseSidebarScreen = null; + const popoverModal = useRef(null); /** * Method create event listener */ const createDragoverListener = () => { - document.addEventListener('dragover', baseSidebarScreen.hideCreateMenu); + document.addEventListener('dragover', popoverModal.current.hideCreateMenu); }; /** * Method remove event listener. */ const removeDragoverListener = () => { - document.removeEventListener('dragover', baseSidebarScreen.hideCreateMenu); + document.removeEventListener('dragover', popoverModal.current.hideCreateMenu); }; + return ( baseSidebarScreen = el} - onShowCreateMenu={createDragoverListener} - onHideCreateMenu={removeDragoverListener} // eslint-disable-next-line react/jsx-props-no-spreading {...props} - /> + > + + ); }; SidebarScreen.propTypes = sidebarPropTypes; -SidebarScreen.defaultProps = sidebarDefaultProps; SidebarScreen.displayName = 'SidebarScreen'; -export default compose( - withLocalize, - withWindowDimensions, - withOnyx({ - allPolicies: { - key: ONYXKEYS.COLLECTION.POLICY, - }, - betas: { - key: ONYXKEYS.BETAS, - }, - }), -)(SidebarScreen); +export default SidebarScreen; diff --git a/src/pages/home/sidebar/SidebarScreen/index.native.js b/src/pages/home/sidebar/SidebarScreen/index.native.js index e2cb2838efe8..145c841b1d2f 100755 --- a/src/pages/home/sidebar/SidebarScreen/index.native.js +++ b/src/pages/home/sidebar/SidebarScreen/index.native.js @@ -1,28 +1,18 @@ import React from 'react'; -import {withOnyx} from 'react-native-onyx'; -import compose from '../../../../libs/compose'; -import withWindowDimensions from '../../../../components/withWindowDimensions'; -import withLocalize from '../../../../components/withLocalize'; -import ONYXKEYS from '../../../../ONYXKEYS'; -import {sidebarPropTypes, sidebarDefaultProps} from './sidebarPropTypes'; +import sidebarPropTypes from './sidebarPropTypes'; import BaseSidebarScreen from './BaseSidebarScreen'; +import FloatingActionButtonAndPopover from './FloatingActionButtonAndPopover'; -// eslint-disable-next-line react/jsx-props-no-spreading -const SidebarScreen = props => ; +const SidebarScreen = props => ( + + + +); SidebarScreen.propTypes = sidebarPropTypes; -SidebarScreen.defaultProps = sidebarDefaultProps; SidebarScreen.displayName = 'SidebarScreen'; -export default compose( - withLocalize, - withWindowDimensions, - withOnyx({ - allPolicies: { - key: ONYXKEYS.COLLECTION.POLICY, - }, - betas: { - key: ONYXKEYS.BETAS, - }, - }), -)(SidebarScreen); +export default SidebarScreen; diff --git a/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js b/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js index 996bba9d676b..3affaa2d00be 100644 --- a/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js +++ b/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js @@ -1,26 +1,8 @@ import PropTypes from 'prop-types'; -import {windowDimensionsPropTypes} from '../../../../components/withWindowDimensions'; -import {withLocalizePropTypes} from '../../../../components/withLocalize'; const sidebarPropTypes = { - /** The list of policies the user has access to. */ - allPolicies: PropTypes.shape({ - /** The policy name */ - name: PropTypes.string, - }), - - /* Beta features list */ - betas: PropTypes.arrayOf(PropTypes.string), - - ...windowDimensionsPropTypes, - - ...withLocalizePropTypes, + /** reportID in the current navigation state */ + reportIDFromRoute: PropTypes.string, }; - -const sidebarDefaultProps = { - allPolicies: {}, - betas: [], -}; - -export {sidebarPropTypes, sidebarDefaultProps}; +export default sidebarPropTypes;