diff --git a/android/app/build.gradle b/android/app/build.gradle index ed6b297f493b..eff00b9238c9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -148,8 +148,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001005803 - versionName "1.0.58-3" + versionCode 1001005804 + versionName "1.0.58-4" } splits { abi { diff --git a/ios/ExpensifyCash/Info.plist b/ios/ExpensifyCash/Info.plist index a8cd5c17bd37..a06c83ca6ced 100644 --- a/ios/ExpensifyCash/Info.plist +++ b/ios/ExpensifyCash/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 1.0.58.3 + 1.0.58.4 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/ExpensifyCashTests/Info.plist b/ios/ExpensifyCashTests/Info.plist index f90b12caa93f..e2c75f224f37 100644 --- a/ios/ExpensifyCashTests/Info.plist +++ b/ios/ExpensifyCashTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.0.58.3 + 1.0.58.4 diff --git a/package-lock.json b/package-lock.json index 71d9532d60a2..eb849b66b455 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "expensify.cash", - "version": "1.0.58-3", + "version": "1.0.58-4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5c5cccd9cd81..2e78607c643a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "expensify.cash", - "version": "1.0.58-3", + "version": "1.0.58-4", "author": "Expensify, Inc.", "homepage": "https://expensify.cash", "description": "Expensify.cash is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/libs/reportUtils.js b/src/libs/reportUtils.js index 237b9519cc34..f553f794197c 100644 --- a/src/libs/reportUtils.js +++ b/src/libs/reportUtils.js @@ -1,5 +1,15 @@ import _ from 'underscore'; import Str from 'expensify-common/lib/str'; +import lodashGet from 'lodash/get'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../ONYXKEYS'; +import CONST from '../CONST'; + +let sessionEmail; +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: val => sessionEmail = val ? val.email : null, +}); /** * Returns the concatenated title for the PrimaryLogins of a report @@ -35,6 +45,22 @@ function sortReportsByLastVisited(reports) { .value(); } +/** + * 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 + * + * @param {Object} reportAction + * @param {String} sessionEmail + * @returns {Boolean} + */ +function canEditReportAction(reportAction) { + return reportAction.actorEmail === sessionEmail + && reportAction.reportActionID + && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT + && !isReportMessageAttachment(lodashGet(reportAction, ['message', 0, 'text'], '')); +} + /** * Given a collection of reports returns the most recently accessed one * @@ -49,5 +75,6 @@ export { getReportParticipantsTitle, isReportMessageAttachment, findLastAccessedReport, + canEditReportAction, sortReportsByLastVisited, }; diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index b6b2c8ac273d..e1f0fecebbf8 100755 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -27,7 +27,12 @@ import { Receipt, } from '../../../components/Icon/Expensicons'; import AttachmentPicker from '../../../components/AttachmentPicker'; -import {addAction, saveReportComment, broadcastUserIsTyping} from '../../../libs/actions/Report'; +import { + addAction, + saveReportComment, + saveReportActionDraft, + broadcastUserIsTyping, +} from '../../../libs/actions/Report'; import ReportTypingIndicator from './ReportTypingIndicator'; import AttachmentModal from '../../../components/AttachmentModal'; import compose from '../../../libs/compose'; @@ -44,6 +49,8 @@ import withLocalize, {withLocalizePropTypes} from '../../../components/withLocal import Permissions from '../../../libs/Permissions'; import Navigation from '../../../libs/Navigation/Navigation'; import ROUTES from '../../../ROUTES'; +import ReportActionPropTypes from './ReportActionPropTypes'; +import {canEditReportAction} from '../../../libs/reportUtils'; const propTypes = { /** A method to call when the form is submitted */ @@ -68,6 +75,9 @@ const propTypes = { participants: PropTypes.arrayOf(PropTypes.string), }), + /** Array of report actions for this report */ + reportActions: PropTypes.objectOf(PropTypes.shape(ReportActionPropTypes)), + /** Is the report view covered by the drawer */ isDrawerOpen: PropTypes.bool.isRequired, @@ -91,6 +101,7 @@ const defaultProps = { comment: '', modal: {}, report: {}, + reportActions: {}, network: {isOffline: false}, }; @@ -101,7 +112,7 @@ class ReportActionCompose extends React.Component { this.updateComment = this.updateComment.bind(this); this.debouncedSaveReportComment = _.debounce(this.debouncedSaveReportComment.bind(this), 1000, false); this.debouncedBroadcastUserIsTyping = _.debounce(this.debouncedBroadcastUserIsTyping.bind(this), 100, true); - this.triggerSubmitShortcut = this.triggerSubmitShortcut.bind(this); + this.triggerHotkeyActions = this.triggerHotkeyActions.bind(this); this.submitForm = this.submitForm.bind(this); this.setIsFocused = this.setIsFocused.bind(this); this.showEmojiPicker = this.showEmojiPicker.bind(this); @@ -231,15 +242,32 @@ class ReportActionCompose extends React.Component { } /** - * Listens for the keyboard shortcut and submits - * the form when we have enter + * Listens for keyboard shortcuts and applies the action * * @param {Object} e */ - triggerSubmitShortcut(e) { - if (e && e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - this.submitForm(); + triggerHotkeyActions(e) { + if (e) { + // Submit the form when Enter is pressed + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + this.submitForm(); + } + + // Trigger the edit box for last sent message if ArrowUp is pressed + if (e.key === 'ArrowUp' && this.state.isCommentEmpty) { + e.preventDefault(); + + const reportActionKey = _.find( + Object.keys(this.props.reportActions).reverse(), + key => canEditReportAction(this.props.reportActions[key]), + ); + + if (reportActionKey !== -1 && this.props.reportActions[reportActionKey]) { + const {reportActionID, message} = this.props.reportActions[reportActionKey]; + saveReportActionDraft(this.props.reportID, reportActionID, _.last(message).text); + } + } } } @@ -414,7 +442,7 @@ class ReportActionCompose extends React.Component { placeholder={this.props.translate('reportActionCompose.writeSomething')} placeholderTextColor={themeColors.placeholderText} onChangeText={this.updateComment} - onKeyPress={this.triggerSubmitShortcut} + onKeyPress={this.triggerHotkeyActions} onDragEnter={() => this.setState({isDraggingOver: true})} onDragLeave={() => this.setState({isDraggingOver: false})} onDrop={(e) => { @@ -534,6 +562,10 @@ export default compose( network: { key: ONYXKEYS.NETWORK, }, + reportActions: { + key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + canEvict: false, + }, report: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, }, diff --git a/src/pages/home/report/ReportActionContextMenu.js b/src/pages/home/report/ReportActionContextMenu.js index 32f9b10a3948..8f4079dc0d79 100755 --- a/src/pages/home/report/ReportActionContextMenu.js +++ b/src/pages/home/report/ReportActionContextMenu.js @@ -3,7 +3,6 @@ import React from 'react'; import {View} from 'react-native'; import PropTypes from 'prop-types'; import lodashGet from 'lodash/get'; -import {withOnyx} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import { Clipboard as ClipboardIcon, LinkCopy, Mail, Pencil, Trashcan, Checkmark, @@ -16,11 +15,9 @@ import ReportActionContextMenuItem from './ReportActionContextMenuItem'; import ReportActionPropTypes from './ReportActionPropTypes'; import Clipboard from '../../../libs/Clipboard'; import compose from '../../../libs/compose'; -import {isReportMessageAttachment} from '../../../libs/reportUtils'; -import ONYXKEYS from '../../../ONYXKEYS'; +import {isReportMessageAttachment, canEditReportAction} from '../../../libs/reportUtils'; 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. */ @@ -46,13 +43,6 @@ const propTypes = { /** Function to dismiss the popover containing this menu */ hidePopover: PropTypes.func.isRequired, - /* Onyx Props */ - - /** The session of the logged in person */ - session: PropTypes.shape({ - /** Email of the logged in person */ - email: PropTypes.string, - }), ...withLocalizePropTypes, }; @@ -60,7 +50,6 @@ const defaultProps = { isMini: false, isVisible: false, selection: '', - session: {}, draftMessage: '', }; @@ -71,7 +60,6 @@ class ReportActionContextMenu extends React.Component { 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.contextActions = [ @@ -122,10 +110,7 @@ class ReportActionContextMenu extends React.Component { { text: this.props.translate('reportActionContextMenu.editComment'), icon: Pencil, - shouldShow: () => ( - this.canEdit() - && !isReportMessageAttachment(this.getActionText()) - ), + shouldShow: () => canEditReportAction(this.props.reportAction), onPress: () => { this.props.hidePopover(); saveReportActionDraft( @@ -138,7 +123,7 @@ class ReportActionContextMenu extends React.Component { { text: this.props.translate('reportActionContextMenu.deleteComment'), icon: Trashcan, - shouldShow: this.canEdit, + shouldShow: () => canEditReportAction(this.props.reportAction), onPress: () => this.setState({isDeleteCommentConfirmModalVisible: true}), }, ]; @@ -160,20 +145,6 @@ 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}); @@ -216,9 +187,4 @@ ReportActionContextMenu.defaultProps = defaultProps; export default compose( withLocalize, - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - }), )(ReportActionContextMenu);