diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js index dd0813132a8e..4f09df7330ff 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js @@ -1,96 +1,59 @@ -import React from 'react'; +import React, {forwardRef, useEffect, useState, useRef, useImperativeHandle, useCallback} from 'react'; import {Dimensions} from 'react-native'; import _ from 'underscore'; -import lodashGet from 'lodash/get'; import * as Report from '../../../../libs/actions/Report'; -import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize'; import PopoverWithMeasuredContent from '../../../../components/PopoverWithMeasuredContent'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; import ConfirmModal from '../../../../components/ConfirmModal'; -import CONST from '../../../../CONST'; import * as ReportActionsUtils from '../../../../libs/ReportActionsUtils'; import * as IOU from '../../../../libs/actions/IOU'; +import useLocalize from '../../../../hooks/useLocalize'; -const propTypes = { - ...withLocalizePropTypes, -}; - -class PopoverReportActionContextMenu extends React.Component { - constructor(props) { - super(props); - - this.state = { - reportID: '0', - reportActionID: '0', - originalReportID: '0', - reportAction: {}, - selection: '', - reportActionDraftMessage: '', - isPopoverVisible: false, - isDeleteCommentConfirmModalVisible: false, - shouldSetModalVisibilityForDeleteConfirmation: true, - cursorRelativePosition: { - horizontal: 0, - vertical: 0, - }, - - // The horizontal and vertical position (relative to the screen) where the popover will display. - popoverAnchorPosition: { - horizontal: 0, - vertical: 0, - }, - isArchivedRoom: false, - isChronosReport: false, - isPinnedChat: false, - isUnreadChat: false, - }; - this.onPopoverShow = () => {}; - this.onPopoverHide = () => {}; - this.onPopoverHideActionCallback = () => {}; - this.contextMenuAnchor = undefined; - this.showContextMenu = this.showContextMenu.bind(this); - this.hideContextMenu = this.hideContextMenu.bind(this); - this.measureContextMenuAnchorPosition = this.measureContextMenuAnchorPosition.bind(this); - this.confirmDeleteAndHideModal = this.confirmDeleteAndHideModal.bind(this); - this.hideDeleteModal = this.hideDeleteModal.bind(this); - this.showDeleteModal = this.showDeleteModal.bind(this); - this.runAndResetOnPopoverShow = this.runAndResetOnPopoverShow.bind(this); - this.runAndResetOnPopoverHide = this.runAndResetOnPopoverHide.bind(this); - this.getContextMenuMeasuredLocation = this.getContextMenuMeasuredLocation.bind(this); - this.isActiveReportAction = this.isActiveReportAction.bind(this); - this.clearActiveReportAction = this.clearActiveReportAction.bind(this); - - this.dimensionsEventListener = null; - - this.contentRef = React.createRef(); - this.setContentRef = (ref) => { - this.contentRef.current = ref; - }; - this.setContentRef = this.setContentRef.bind(this); - this.anchorRef = React.createRef(); - } - - componentDidMount() { - this.dimensionsEventListener = Dimensions.addEventListener('change', this.measureContextMenuAnchorPosition); - } - - shouldComponentUpdate(nextProps, nextState) { - const previousLocale = lodashGet(this.props, 'preferredLocale', CONST.LOCALES.DEFAULT); - const nextLocale = lodashGet(nextProps, 'preferredLocale', CONST.LOCALES.DEFAULT); - return ( - this.state.isPopoverVisible !== nextState.isPopoverVisible || - this.state.popoverAnchorPosition !== nextState.popoverAnchorPosition || - this.state.isDeleteCommentConfirmModalVisible !== nextState.isDeleteCommentConfirmModalVisible || - previousLocale !== nextLocale - ); - } - - componentWillUnmount() { - if (!this.dimensionsEventListener) { - return; - } - this.dimensionsEventListener.remove(); - } +function PopoverReportActionContextMenu(_props, ref) { + const {translate} = useLocalize(); + const reportIDRef = useRef('0'); + const typeRef = useRef(''); + const reportActionRef = useRef({}); + const reportActionIDRef = useRef('0'); + const originalReportIDRef = useRef('0'); + const selectionRef = useRef(''); + const reportActionDraftMessageRef = useRef(''); + + const cursorRelativePosition = useRef({ + horizontal: 0, + vertical: 0, + }); + + // The horizontal and vertical position (relative to the screen) where the popover will display. + const popoverAnchorPosition = useRef({ + horizontal: 0, + vertical: 0, + }); + + const [instanceID, setInstanceID] = useState(''); + + const [isPopoverVisible, setIsPopoverVisible] = useState(false); + const [isDeleteCommentConfirmModalVisible, setIsDeleteCommentConfirmModalVisible] = useState(false); + const [shouldSetModalVisibilityForDeleteConfirmation, setShouldSetModalVisibilityForDeleteConfirmation] = useState(true); + + const [isRoomArchived, setIsRoomArchived] = useState(false); + const [isChronosReportEnabled, setIsChronosReportEnabled] = useState(false); + const [isChatPinned, setIsChatPinned] = useState(false); + const [hasUnreadMessages, setHasUnreadMessages] = useState(false); + + const contentRef = useRef(null); + const anchorRef = useRef(null); + const dimensionsEventListener = useRef(null); + const contextMenuAnchorRef = useRef(null); + const contextMenuTargetNode = useRef(null); + + const onPopoverShow = useRef(() => {}); + const onPopoverHide = useRef(() => {}); + const onCancelDeleteModal = useRef(() => {}); + const onComfirmDeleteModal = useRef(() => {}); + + const onPopoverHideActionCallback = useRef(() => {}); + const callbackWhenDeleteModalHide = useRef(() => {}); /** * Get the Context menu anchor position @@ -98,15 +61,48 @@ class PopoverReportActionContextMenu extends React.Component { * * @returns {Promise} */ - getContextMenuMeasuredLocation() { - return new Promise((resolve) => { - if (this.contextMenuAnchor) { - (this.contextMenuAnchor.current || this.contextMenuAnchor).measureInWindow((x, y) => resolve({x, y})); - } else { - resolve({x: 0, y: 0}); + const getContextMenuMeasuredLocation = useCallback( + () => + new Promise((resolve) => { + if (contextMenuAnchorRef.current && _.isFunction(contextMenuAnchorRef.current.measureInWindow)) { + contextMenuAnchorRef.current.measureInWindow((x, y) => resolve({x, y})); + } else { + resolve({x: 0, y: 0}); + } + }), + [], + ); + + /** + * This gets called on Dimensions change to find the anchor coordinates for the action context menu. + */ + const measureContextMenuAnchorPosition = useCallback(() => { + if (!isPopoverVisible) { + return; + } + + getContextMenuMeasuredLocation().then(({x, y}) => { + if (!x || !y) { + return; } + + popoverAnchorPosition.current = { + horizontal: cursorRelativePosition.horizontal + x, + vertical: cursorRelativePosition.vertical + y, + }; }); - } + }, [isPopoverVisible, getContextMenuMeasuredLocation]); + + useEffect(() => { + dimensionsEventListener.current = Dimensions.addEventListener('change', measureContextMenuAnchorPosition); + + return () => { + if (!dimensionsEventListener.current) { + return; + } + dimensionsEventListener.current.remove(); + }; + }, [measureContextMenuAnchorPosition]); /** * Whether Context Menu is active for the Report Action. @@ -114,13 +110,12 @@ class PopoverReportActionContextMenu extends React.Component { * @param {Number|String} actionID * @return {Boolean} */ - isActiveReportAction(actionID) { - return Boolean(actionID) && (this.state.reportActionID === actionID || this.state.reportAction.reportActionID === actionID); - } + const isActiveReportAction = (actionID) => Boolean(actionID) && (reportActionIDRef.current === actionID || reportActionRef.current.reportActionID === actionID); - clearActiveReportAction() { - this.setState({reportID: '0', reportAction: {}}); - } + const clearActiveReportAction = () => { + reportActionIDRef.current = '0'; + reportActionRef.current = {}; + }; /** * Show the ReportActionContextMenu modal popover. @@ -140,7 +135,7 @@ class PopoverReportActionContextMenu extends React.Component { * @param {Boolean} isPinnedChat - Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action * @param {Boolean} isUnreadChat - Flag to check if the chat is unread in the LHN. Used for the Mark as Read/Unread action */ - showContextMenu( + const showContextMenu = ( type, event, selection, @@ -155,130 +150,105 @@ class PopoverReportActionContextMenu extends React.Component { isChronosReport = false, isPinnedChat = false, isUnreadChat = false, - ) { + ) => { const nativeEvent = event.nativeEvent || {}; - this.contextMenuAnchor = contextMenuAnchor; - this.contextMenuTargetNode = nativeEvent.target; - - // Singleton behaviour of ContextMenu creates race conditions when user requests multiple contextMenus. - // But it is possible that every new request registers new callbacks thus instanceID is used to corelate those callbacks - this.instanceID = Math.random().toString(36).substr(2, 5); - - this.onPopoverShow = onShow; - this.onPopoverHide = onHide; - - this.getContextMenuMeasuredLocation().then(({x, y}) => { - this.setState({ - cursorRelativePosition: { - horizontal: nativeEvent.pageX - x, - vertical: nativeEvent.pageY - y, - }, - popoverAnchorPosition: { - horizontal: nativeEvent.pageX, - vertical: nativeEvent.pageY, - }, - type, - reportID, - reportActionID, - originalReportID, - selection, - isPopoverVisible: true, - reportActionDraftMessage: draftMessage, - isArchivedRoom, - isChronosReport, - isPinnedChat, - isUnreadChat, - }); - }); - } + contextMenuAnchorRef.current = contextMenuAnchor; + contextMenuTargetNode.current = nativeEvent.target; - /** - * This gets called on Dimensions change to find the anchor coordinates for the action context menu. - */ - measureContextMenuAnchorPosition() { - if (!this.state.isPopoverVisible) { - return; - } - this.getContextMenuMeasuredLocation().then(({x, y}) => { - if (!x || !y) { - return; - } - this.setState((prev) => ({ - popoverAnchorPosition: { - horizontal: prev.cursorRelativePosition.horizontal + x, - vertical: prev.cursorRelativePosition.vertical + y, - }, - })); + setInstanceID(Math.random().toString(36).substr(2, 5)); + + onPopoverShow.current = onShow; + onPopoverHide.current = onHide; + + getContextMenuMeasuredLocation().then(({x, y}) => { + popoverAnchorPosition.current = { + horizontal: nativeEvent.pageX - x, + vertical: nativeEvent.pageY - y, + }; + + popoverAnchorPosition.current = { + horizontal: nativeEvent.pageX, + vertical: nativeEvent.pageY, + }; + typeRef.current = type; + reportIDRef.current = reportID; + reportActionIDRef.current = reportActionID; + originalReportIDRef.current = originalReportID; + selectionRef.current = selection; + setIsPopoverVisible(true); + reportActionDraftMessageRef.current = draftMessage; + setIsRoomArchived(isArchivedRoom); + setIsChronosReportEnabled(isChronosReport); + setIsChatPinned(isPinnedChat); + setHasUnreadMessages(isUnreadChat); }); - } + }; /** * After Popover shows, call the registered onPopoverShow callback and reset it */ - runAndResetOnPopoverShow() { - this.onPopoverShow(); + const runAndResetOnPopoverShow = () => { + onPopoverShow.current(); // After we have called the action, reset it. - this.onPopoverShow = () => {}; - } + onPopoverShow.current = () => {}; + }; + + /** + * Run the callback and return a noop function to reset it + * @param {Function} callback + * @returns {Function} + */ + const runAndResetCallback = (callback) => { + callback(); + return () => {}; + }; /** * After Popover hides, call the registered onPopoverHide & onPopoverHideActionCallback callback and reset it */ - runAndResetOnPopoverHide() { - this.setState({reportID: '0', reportActionID: '0', originalReportID: '0'}, () => { - this.onPopoverHide = this.runAndResetCallback(this.onPopoverHide); - this.onPopoverHideActionCallback = this.runAndResetCallback(this.onPopoverHideActionCallback); - }); - } + const runAndResetOnPopoverHide = () => { + reportIDRef.current = '0'; + reportActionIDRef.current = '0'; + originalReportIDRef.current = '0'; + + onPopoverHide.current = runAndResetCallback(onPopoverHide.current); + onPopoverHideActionCallback.current = runAndResetCallback(onPopoverHideActionCallback.current); + }; /** * Hide the ReportActionContextMenu modal popover. * @param {Function} onHideActionCallback Callback to be called after popover is completely hidden */ - hideContextMenu(onHideActionCallback) { + const hideContextMenu = (onHideActionCallback) => { if (_.isFunction(onHideActionCallback)) { - this.onPopoverHideActionCallback = onHideActionCallback; + onPopoverHideActionCallback.current = onHideActionCallback; } - this.setState({ - selection: '', - reportActionDraftMessage: '', - isPopoverVisible: false, - }); - } - /** - * Run the callback and return a noop function to reset it - * @param {Function} callback - * @returns {Function} - */ - runAndResetCallback(callback) { - callback(); - return () => {}; - } - - confirmDeleteAndHideModal() { - this.callbackWhenDeleteModalHide = () => (this.onComfirmDeleteModal = this.runAndResetCallback(this.onComfirmDeleteModal)); + selectionRef.current = ''; + reportActionDraftMessageRef.current = ''; + setIsPopoverVisible(false); + }; - if (ReportActionsUtils.isMoneyRequestAction(this.state.reportAction)) { - IOU.deleteMoneyRequest(this.state.reportAction.originalMessage.IOUTransactionID, this.state.reportAction); + const confirmDeleteAndHideModal = useCallback(() => { + callbackWhenDeleteModalHide.current = () => (onComfirmDeleteModal.current = runAndResetCallback(onComfirmDeleteModal.current)); + if (ReportActionsUtils.isMoneyRequestAction(reportActionRef.current)) { + IOU.deleteMoneyRequest(reportActionRef.current.originalMessage.IOUTransactionID, reportActionRef.current); } else { - Report.deleteReportComment(this.state.reportID, this.state.reportAction); + Report.deleteReportComment(reportIDRef.current, reportActionRef.current); } - this.setState({isDeleteCommentConfirmModalVisible: false}); - } - - hideDeleteModal() { - this.callbackWhenDeleteModalHide = () => (this.onCancelDeleteModal = this.runAndResetCallback(this.onCancelDeleteModal)); - this.setState({ - isDeleteCommentConfirmModalVisible: false, - shouldSetModalVisibilityForDeleteConfirmation: true, - isArchivedRoom: false, - isChronosReport: false, - isPinnedChat: false, - isUnreadChat: false, - }); - } + setIsDeleteCommentConfirmModalVisible(false); + }, [reportActionRef]); + + const hideDeleteModal = () => { + callbackWhenDeleteModalHide.current = () => (onCancelDeleteModal.current = runAndResetCallback(onCancelDeleteModal.current)); + setIsDeleteCommentConfirmModalVisible(false); + setShouldSetModalVisibilityForDeleteConfirmation(true); + setIsRoomArchived(false); + setIsChronosReportEnabled(false); + setIsChatPinned(false); + setHasUnreadMessages(false); + }; /** * Opens the Confirm delete action modal @@ -288,67 +258,82 @@ class PopoverReportActionContextMenu extends React.Component { * @param {Function} [onConfirm] * @param {Function} [onCancel] */ - showDeleteModal(reportID, reportAction, shouldSetModalVisibility = true, onConfirm = () => {}, onCancel = () => {}) { - this.onCancelDeleteModal = onCancel; - this.onComfirmDeleteModal = onConfirm; - this.setState({ - reportID, - reportAction, - shouldSetModalVisibilityForDeleteConfirmation: shouldSetModalVisibility, - isDeleteCommentConfirmModalVisible: true, - }); - } - - render() { - return ( - <> - - - - {}, onCancel = () => {}) => { + onCancelDeleteModal.current = onCancel; + onComfirmDeleteModal.current = onConfirm; + + reportIDRef.current = reportID; + reportActionRef.current = reportAction; + + setShouldSetModalVisibilityForDeleteConfirmation(shouldSetModalVisibility); + setIsDeleteCommentConfirmModalVisible(true); + }; + + useImperativeHandle(ref, () => ({ + showContextMenu, + hideContextMenu, + showDeleteModal, + hideDeleteModal, + isActiveReportAction, + instanceID, + runAndResetOnPopoverHide, + clearActiveReportAction, + })); + + const reportAction = reportActionRef.current; + + return ( + <> + + - - ); - } + + { + reportIDRef.current = '0'; + reportActionRef.current = {}; + callbackWhenDeleteModalHide.current(); + }} + prompt={translate('reportActionContextMenu.deleteConfirmation', {action: reportAction})} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + + ); } -PopoverReportActionContextMenu.propTypes = propTypes; +PopoverReportActionContextMenu.displayName = 'PopoverReportActionContextMenu'; -export default withLocalize(PopoverReportActionContextMenu); +export default forwardRef(PopoverReportActionContextMenu);