diff --git a/src/CONST.js b/src/CONST.js index b98cf6cf70ec..bb628a57b521 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -42,6 +42,12 @@ const CONST = { LIMIT: 50, TYPE: { IOU: 'IOU', + ADDCOMMENT: 'ADDCOMMENT', + }, + }, + MESSAGE: { + TYPE: { + COMMENT: 'COMMENT', }, }, TYPE: { diff --git a/src/components/Hoverable/HoverablePropTypes.js b/src/components/Hoverable/HoverablePropTypes.js index 2ed9094ffe3a..f6ebf25fa883 100644 --- a/src/components/Hoverable/HoverablePropTypes.js +++ b/src/components/Hoverable/HoverablePropTypes.js @@ -16,12 +16,16 @@ const propTypes = { /** Function that executes when the mouse leaves the children. */ onHoverOut: PropTypes.func, + + // If the mouse clicks outside, should we dismiss hover? + resetsOnClickOutside: PropTypes.bool, }; const defaultProps = { containerStyle: {}, onHoverIn: () => {}, onHoverOut: () => {}, + resetsOnClickOutside: false, }; export { diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js index 50823936d9ef..e11b63aea8c0 100644 --- a/src/components/Hoverable/index.js +++ b/src/components/Hoverable/index.js @@ -72,7 +72,7 @@ class Hoverable extends Component { if (!this.state.isHovered) { return; } - if (this.wrapperView && !this.wrapperView.contains(event.target)) { + if (this.wrapperView && !this.wrapperView.contains(event.target) && this.props.resetsOnClickOutside) { this.setIsHovered(false); } } diff --git a/src/languages/en.js b/src/languages/en.js index 078e46d16310..3013f21ad119 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -26,6 +26,7 @@ export default { email: 'Email', and: 'and', details: 'Details', + delete: 'Delete', contacts: 'Contacts', recents: 'Recents', }, @@ -84,6 +85,7 @@ export default { markAsUnread: 'Mark as Unread', editComment: 'Edit Comment', deleteComment: 'Delete Comment', + deleteConfirmation: 'Are you sure you want to delete this comment?', }, reportActionsView: { beFirstPersonToComment: 'Be the first person to comment', diff --git a/src/libs/API.js b/src/libs/API.js index b1ef25c70a9c..36ab90483c5a 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -535,9 +535,9 @@ function Report_TogglePinned(parameters) { /** * @param {Object} parameters * @param {Number} parameters.reportID - * @param {String} parameters.reportActionID + * @param {Number} parameters.reportActionID * @param {String} parameters.reportComment - * @return {Promise} + * @returns {Promise} */ function Report_EditComment(parameters) { const commandName = 'Report_EditComment'; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index da4fafc484db..b9c1008958c4 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1004,7 +1004,7 @@ function addAction(reportID, text, file) { // Optimistically add the new comment to the store before waiting to save it to the server Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { [optimisticReportActionID]: { - actionName: 'ADDCOMMENT', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, actorEmail: currentUserEmail, actorAccountID: currentUserAccountID, person: [ @@ -1023,7 +1023,7 @@ function addAction(reportID, text, file) { timestamp: moment().unix(), message: [ { - type: 'COMMENT', + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, html: htmlForNewComment, text: textForNewComment, }, @@ -1049,6 +1049,48 @@ function addAction(reportID, text, file) { .then(({reportAction}) => updateReportWithNewAction(reportID, reportAction)); } +/** + * Deletes a comment from the report, basically sets it as empty string + * + * @param {Number} reportID + * @param {Object} reportAction + */ +function deleteReportComment(reportID, reportAction) { + // Optimistic Response + const reportActionsToMerge = {}; + const oldMessage = {...reportAction.message}; + reportActionsToMerge[reportAction.sequenceNumber] = { + ...reportAction, + message: [ + { + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, + html: '', + text: '', + }, + ], + }; + + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, reportActionsToMerge); + + // Try to delete the comment by calling the API + API.Report_EditComment({ + reportID, + reportActionID: reportAction.reportActionID, + reportComment: '', + }) + .then((response) => { + if (response.jsonCode !== 200) { + // Reverse Optimistic Response + reportActionsToMerge[reportAction.sequenceNumber] = { + ...reportAction, + message: oldMessage, + }; + + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, reportActionsToMerge); + } + }); +} + /** * Updates the last read action ID on the report. It optimistically makes the change to the store, and then let's the * network layer handle the delayed write. @@ -1219,5 +1261,6 @@ export { updateCurrentlyViewedReportID, editReportComment, saveReportActionDraft, + deleteReportComment, getSimplifiedIOUReport, }; diff --git a/src/pages/home/report/ReportActionContextMenu.js b/src/pages/home/report/ReportActionContextMenu.js index 99c1486923c2..c3822e3cc896 100755 --- a/src/pages/home/report/ReportActionContextMenu.js +++ b/src/pages/home/report/ReportActionContextMenu.js @@ -8,7 +8,9 @@ import { Clipboard as ClipboardIcon, LinkCopy, Mail, Pencil, Trashcan, Checkmark, } from '../../../components/Icon/Expensicons'; import getReportActionContextMenuStyles from '../../../styles/getReportActionContextMenuStyles'; -import {setNewMarkerPosition, updateLastReadActionID, saveReportActionDraft} from '../../../libs/actions/Report'; +import { + setNewMarkerPosition, updateLastReadActionID, saveReportActionDraft, deleteReportComment, +} from '../../../libs/actions/Report'; import ReportActionContextMenuItem from './ReportActionContextMenuItem'; import ReportActionPropTypes from './ReportActionPropTypes'; import Clipboard from '../../../libs/Clipboard'; @@ -16,6 +18,8 @@ import compose from '../../../libs/compose'; import {isReportMessageAttachment} from '../../../libs/reportUtils'; import ONYXKEYS from '../../../ONYXKEYS'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; +import ConfirmModal from '../../../components/ConfirmModal'; +import CONST from '../../../CONST'; const propTypes = { /** The ID of the report this report action is attached to. */ @@ -63,8 +67,13 @@ class ReportActionContextMenu extends React.Component { constructor(props) { super(props); + this.confirmDeleteAndHideModal = this.confirmDeleteAndHideModal.bind(this); + this.hideDeleteConfirmModal = this.hideDeleteConfirmModal.bind(this); + this.getActionText = this.getActionText.bind(this); + this.canEdit = this.canEdit.bind(this); + // A list of all the context actions in this menu. - this.CONTEXT_ACTIONS = [ + this.contextActions = [ // Copy to clipboard { text: this.props.translate('reportActionContextMenu.copyToClipboard'), @@ -112,9 +121,10 @@ class ReportActionContextMenu extends React.Component { { text: this.props.translate('reportActionContextMenu.editComment'), icon: Pencil, - shouldShow: this.props.reportAction.actorEmail === this.props.session.email + shouldShow: () => ( + this.canEdit() && !isReportMessageAttachment(this.getActionText()) - && this.props.reportAction.reportActionID, + ), onPress: () => { this.props.hidePopover(); saveReportActionDraft( @@ -124,18 +134,19 @@ class ReportActionContextMenu extends React.Component { ); }, }, - { text: this.props.translate('reportActionContextMenu.deleteComment'), icon: Trashcan, - shouldShow: false, - onPress: () => {}, + shouldShow: this.canEdit, + onPress: () => this.setState({isDeleteCommentConfirmModalVisible: true}), }, ]; this.wrapperStyle = getReportActionContextMenuStyles(this.props.isMini); - this.getActionText = this.getActionText.bind(this); + this.state = { + isDeleteCommentConfirmModalVisible: false, + }; } /** @@ -148,10 +159,33 @@ class ReportActionContextMenu extends React.Component { return lodashGet(message, 'text', ''); } + /** + * Can the current user edit this report action? + * + * @return {Boolean} + */ + canEdit() { + // Can only edit if it's a ADDCOMMENT, the author is this user and it's not a optimistic response. + // If it's an optimistic response comment it will not have a reportActionID, + // and we should wait until it does before we show the actions + return this.props.reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT + && this.props.reportAction.actorEmail === this.props.session.email + && this.props.reportAction.reportActionID; + } + + confirmDeleteAndHideModal() { + deleteReportComment(this.props.reportID, this.props.reportAction); + this.setState({isDeleteCommentConfirmModalVisible: false}); + } + + hideDeleteConfirmModal() { + this.setState({isDeleteCommentConfirmModalVisible: false}); + } + render() { return this.props.isVisible && ( - {this.CONTEXT_ACTIONS.map(contextAction => contextAction.shouldShow && ( + {this.contextActions.map(contextAction => _.result(contextAction, 'shouldShow', false) && ( contextAction.onPress(this.props.reportAction)} /> ))} + ); } diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index adf69996b0e8..58b20f50743f 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -19,6 +19,7 @@ import ReportActionItemIOUAction from '../../../components/ReportActionItemIOUAc import ReportActionItemMessage from './ReportActionItemMessage'; import UnreadActionIndicator from '../../../components/UnreadActionIndicator'; import ReportActionItemMessageEdit from './ReportActionItemMessageEdit'; +import CONST from '../../../CONST'; const propTypes = { /** The ID of the report this action is on. */ @@ -197,7 +198,7 @@ class ReportActionItem extends Component { render() { let children; - if (this.props.action.actionName === 'IOU') { + if (this.props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { children = ( this.popoverAnchor = el} onSecondaryInteraction={this.showPopover} > - + {hovered => ( {this.props.shouldDisplayNewIndicator && ( diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index fb4555fff4f0..39d215aed6e2 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -248,7 +248,14 @@ class ReportActionsView extends React.Component { updateSortedReportActions(reportActions) { this.sortedReportActions = _.chain(reportActions) .sortBy('sequenceNumber') - .filter(action => action.actionName === 'ADDCOMMENT' || action.actionName === 'IOU') + .filter((action) => { + // Only show non-empty ADDCOMMENT actions or IOU actions + // Empty ADDCOMMENT actions typically mean they have been deleted and should not be shown + const message = _.first(lodashGet(action, 'message', null)); + const html = lodashGet(message, 'html', ''); + return action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU + || (action.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && html !== ''); + }) .map((item, index) => ({action: item, index})) .value() .reverse(); @@ -291,7 +298,7 @@ class ReportActionsView extends React.Component { updateMostRecentIOUReportActionNumber(reportActions) { this.mostRecentIOUReportSequenceNumber = _.chain(reportActions) .sortBy('sequenceNumber') - .filter(action => action.actionName === 'IOU') + .filter(action => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) .max(action => action.sequenceNumber) .value().sequenceNumber; } diff --git a/src/pages/iou/IOUTransactions.js b/src/pages/iou/IOUTransactions.js index 0a5e9347ba2e..a505b21172d0 100644 --- a/src/pages/iou/IOUTransactions.js +++ b/src/pages/iou/IOUTransactions.js @@ -7,6 +7,7 @@ import styles from '../../styles/styles'; import ONYXKEYS from '../../ONYXKEYS'; import ReportActionPropTypes from '../home/report/ReportActionPropTypes'; import ReportTransaction from '../../components/ReportTransaction'; +import CONST from '../../CONST'; const propTypes = { /** Actions from the ChatReport */ @@ -30,7 +31,7 @@ const IOUTransactions = ({ }) => ( {_.map(reportActions, (reportAction) => { - if (reportAction.actionName === 'IOU' + if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction.originalMessage.IOUReportID === iouReportID) { return ( {}; PushNotification.deregister = () => {}; @@ -42,7 +43,7 @@ describe('actions/Report', () => { const REPORT_ID = 1; const ACTION_ID = 1; const REPORT_ACTION = { - actionName: 'ADDCOMMENT', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, actorAccountID: TEST_USER_ACCOUNT_ID, actorEmail: TEST_USER_LOGIN, automatic: false,