diff --git a/src/Expensify.js b/src/Expensify.js index 1588c6c60f5c..c48e078d4e98 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -23,7 +23,7 @@ Onyx.init({ initialKeyStates: { // Clear any loading and error messages so they do not appear on app startup - [ONYXKEYS.SESSION]: {loading: false}, + [ONYXKEYS.SESSION]: {loading: false, shouldShowComposeInput: true}, [ONYXKEYS.ACCOUNT]: CONST.DEFAULT_ACCOUNT_DATA, [ONYXKEYS.NETWORK]: {isOffline: false}, [ONYXKEYS.IOU]: {loading: false}, diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 7794a5b8852c..e2efceeadaf8 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -68,6 +68,7 @@ export default { REPORT: 'report_', REPORT_ACTIONS: 'reportActions_', REPORT_DRAFT_COMMENT: 'reportDraftComment_', + REPORT_ACTIONS_DRAFTS: 'reportActionsDrafts_', REPORT_USER_IS_TYPING: 'reportUserIsTyping_', REPORT_IOUS: 'reportIOUs_', }, diff --git a/src/components/InvertedFlatList/index.js b/src/components/InvertedFlatList/index.js index 6f9a861cb8a1..2bf5a3844f9a 100644 --- a/src/components/InvertedFlatList/index.js +++ b/src/components/InvertedFlatList/index.js @@ -5,11 +5,15 @@ import React, { forwardRef, } from 'react'; import PropTypes from 'prop-types'; +import {FlatList} from 'react-native'; +import _ from 'underscore'; import BaseInvertedFlatList from './BaseInvertedFlatList'; const propTypes = { // Passed via forwardRef so we can access the FlatList ref - innerRef: PropTypes.func.isRequired, + innerRef: PropTypes.shape({ + current: PropTypes.instanceOf(FlatList), + }).isRequired, }; // This is copied from https://codesandbox.io/s/react-native-dsyse @@ -23,7 +27,12 @@ const InvertedFlatList = (props) => { }, []); useEffect(() => { - props.innerRef(ref.current); + if (!_.isFunction(props.innerRef)) { + // eslint-disable-next-line no-param-reassign + props.innerRef.current = ref.current; + } else { + props.innerRef(ref.current); + } }, []); useEffect(() => { diff --git a/src/components/TextInputFocusable/index.js b/src/components/TextInputFocusable/index.js index a66d8ef8ff0f..a99e23c8ab78 100755 --- a/src/components/TextInputFocusable/index.js +++ b/src/components/TextInputFocusable/index.js @@ -18,7 +18,7 @@ const propTypes = { onPasteFile: PropTypes.func, // A ref to forward to the text input - forwardedRef: PropTypes.func.isRequired, + forwardedRef: PropTypes.func, // General styles to apply to the text input // eslint-disable-next-line react/forbid-prop-types @@ -62,6 +62,7 @@ const defaultProps = { onDrop: () => {}, isDisabled: false, autoFocus: false, + forwardedRef: null, }; const IMAGE_EXTENSIONS = { diff --git a/src/components/TextInputFocusable/index.native.js b/src/components/TextInputFocusable/index.native.js index f54704f2669b..fab67ddb7308 100644 --- a/src/components/TextInputFocusable/index.native.js +++ b/src/components/TextInputFocusable/index.native.js @@ -13,7 +13,7 @@ const propTypes = { shouldClear: PropTypes.bool, // A ref to forward to the text input - forwardedRef: PropTypes.func.isRequired, + forwardedRef: PropTypes.func, // When the input has cleared whoever owns this input should know about it onClear: PropTypes.func, @@ -31,6 +31,7 @@ const defaultProps = { onClear: () => {}, autoFocus: false, isDisabled: false, + forwardedRef: null, }; class TextInputFocusable extends React.Component { diff --git a/src/libs/API.js b/src/libs/API.js index a119a3ebe6aa..fea29cac0659 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -544,6 +544,19 @@ function Report_TogglePinned(parameters) { return Network.post(commandName, parameters); } +/** + * @param {Object} parameters + * @param {Number} parameters.reportID + * @param {String} parameters.reportActionID + * @param {String} parameters.reportComment + * @return {Promise} + */ +function Report_EditComment(parameters) { + const commandName = 'Report_EditComment'; + requireParameters(['reportID', 'reportActionID', 'reportComment'], parameters, commandName); + return Network.post(commandName, parameters); +} + /** * @param {Object} parameters * @param {Number} parameters.accountID @@ -725,6 +738,7 @@ export { Report_AddComment, Report_GetHistory, Report_TogglePinned, + Report_EditComment, Report_UpdateLastRead, ResendValidateCode, ResetPassword, diff --git a/src/libs/Pusher/EventType.js b/src/libs/Pusher/EventType.js index 866c96110388..0810261af524 100644 --- a/src/libs/Pusher/EventType.js +++ b/src/libs/Pusher/EventType.js @@ -4,5 +4,6 @@ */ export default { REPORT_COMMENT: 'reportComment', + REPORT_COMMENT_EDIT: 'reportCommentEdit', REPORT_TOGGLE_PINNED: 'reportTogglePinned', }; diff --git a/src/libs/ReportScrollManager/index.js b/src/libs/ReportScrollManager/index.js new file mode 100644 index 000000000000..ddd34b1bf6e6 --- /dev/null +++ b/src/libs/ReportScrollManager/index.js @@ -0,0 +1,25 @@ +import React from 'react'; + +// This ref is created using React.createRef here because this function is used by a component that doesn't have access +// to the original ref. +const flatListRef = React.createRef(); + +/** + * Scroll to the provided index. On non-native implementations we do not want to scroll when we are scrolling because + * we are editing a comment. + * + * @param {Object} index + * @param {Boolean} isEditing + */ +function scrollToIndex(index, isEditing) { + if (isEditing) { + return; + } + + flatListRef.current.scrollToIndex(index); +} + +export { + flatListRef, + scrollToIndex, +}; diff --git a/src/libs/ReportScrollManager/index.native.js b/src/libs/ReportScrollManager/index.native.js new file mode 100644 index 000000000000..a52e78c5a9b5 --- /dev/null +++ b/src/libs/ReportScrollManager/index.native.js @@ -0,0 +1,19 @@ +import React from 'react'; + +// This ref is created using React.createRef here because this function is used by a component that doesn't have access +// to the original ref. +const flatListRef = React.createRef(); + +/** + * Scroll to the provided index. + * + * @param {Object} index + */ +function scrollToIndex(index) { + flatListRef.current.scrollToIndex(index); +} + +export { + flatListRef, + scrollToIndex, +}; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 253e8e39a9ee..ff1222aef310 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -2,6 +2,7 @@ import moment from 'moment'; import _ from 'underscore'; import lodashGet from 'lodash/get'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; +import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; import * as Pusher from '../Pusher/pusher'; @@ -424,6 +425,19 @@ function removeOptimisticActions(reportID) { }); } +/** + * Updates a report action's message to be a new value. + * + * @param {Number} reportID + * @param {Number} sequenceNumber + * @param {Object} message + */ +function updateReportActionMessage(reportID, sequenceNumber, message) { + const actionToMerge = {}; + actionToMerge[sequenceNumber] = {message: [message]}; + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, actionToMerge); +} + /** * Updates a report in the store with a new report action * @@ -578,6 +592,26 @@ function subscribeToUserEvents() { ); }); + // Live-update a report's actions when an 'edit comment' event is received. + Pusher.subscribe(pusherChannelName, Pusher.TYPE.REPORT_COMMENT_EDIT, (pushJSON) => { + Log.info( + `[Report] Handled ${Pusher.TYPE.REPORT_COMMENT_EDIT} event sent by Pusher`, true, { + reportActionID: pushJSON.reportActionID, + }, + ); + updateReportActionMessage(pushJSON.reportID, pushJSON.sequenceNumber, pushJSON.message); + }, false, + () => { + NetworkConnection.triggerReconnectionCallbacks('pusher re-subscribed to private user channel'); + }) + .catch((error) => { + Log.info( + '[Report] Failed to subscribe to Pusher channel', + true, + {error, pusherChannelName, eventName: Pusher.TYPE.REPORT_COMMENT_EDIT}, + ); + }); + // Live-update a report's pinned state when a 'report toggle pinned' event is received. Pusher.subscribe(pusherChannelName, Pusher.TYPE.REPORT_TOGGLE_PINNED, (pushJSON) => { Log.info( @@ -1047,6 +1081,49 @@ Onyx.connect({ // When the app reconnects from being offline, fetch all of the reports and their actions NetworkConnection.onReconnect(fetchAllReports); +/** + * Saves a new message for a comment. Marks the comment as edited, which will be reflected in the UI. + * + * @param {Number} reportID + * @param {Object} originalReportAction + * @param {String} htmlForNewComment + */ +function editReportComment(reportID, originalReportAction, htmlForNewComment) { + // Optimistically update the report action with the new message + const sequenceNumber = originalReportAction.sequenceNumber; + const newReportAction = {...originalReportAction}; + const actionToMerge = {}; + newReportAction.message[0].isEdited = true; + newReportAction.message[0].html = htmlForNewComment; + newReportAction.message[0].text = Str.stripHTML(htmlForNewComment); + actionToMerge[sequenceNumber] = newReportAction; + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, actionToMerge); + + // Persist the updated report comment + API.Report_EditComment({ + reportID, + reportActionID: originalReportAction.reportActionID, + reportComment: htmlForNewComment, + sequenceNumber, + }) + .catch(() => { + // If it fails, reset Onyx + actionToMerge[sequenceNumber] = originalReportAction; + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, actionToMerge); + }); +} + +/** + * Saves the draft for a comment report action. This will put the comment into "edit mode" + * + * @param {Number} reportID + * @param {Number} reportActionID + * @param {String} draftMessage + */ +function saveReportActionDraft(reportID, reportActionID, draftMessage) { + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}_${reportActionID}`, draftMessage); +} + export { fetchAllReports, fetchActions, @@ -1061,6 +1138,8 @@ export { broadcastUserIsTyping, togglePinnedState, updateCurrentlyViewedReportID, + editReportComment, + saveReportActionDraft, getSimplifiedIOUReport, getSimplifiedReportObject, }; diff --git a/src/libs/toggleReportActionComposeView.js b/src/libs/toggleReportActionComposeView.js new file mode 100644 index 000000000000..3b43305ebafb --- /dev/null +++ b/src/libs/toggleReportActionComposeView.js @@ -0,0 +1,8 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../ONYXKEYS'; + +export default (shouldShowComposeInput, isSmallScreenWidth) => { + if (isSmallScreenWidth) { + Onyx.merge(ONYXKEYS.SESSION, {shouldShowComposeInput}); + } +}; diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index b738c5436dba..d1e999a2e83a 100755 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -170,7 +170,7 @@ class ReportActionCompose extends React.Component { * Focus the composer text input */ focus() { - if (this.textInput) { + if (this.textInput && this.textInput.focus) { // There could be other animations running while we trigger manual focus. // This prevents focus from making those animations janky. InteractionManager.runAfterInteractions(() => { diff --git a/src/pages/home/report/ReportActionContextMenu.js b/src/pages/home/report/ReportActionContextMenu.js index ee5a75f8b12d..92cbbd7df18c 100755 --- a/src/pages/home/report/ReportActionContextMenu.js +++ b/src/pages/home/report/ReportActionContextMenu.js @@ -3,15 +3,18 @@ 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 { Clipboard as ClipboardIcon, LinkCopy, Mail, Pencil, Trashcan, Checkmark, } from '../../../components/Icon/Expensicons'; import getReportActionContextMenuStyles from '../../../styles/getReportActionContextMenuStyles'; -import {setNewMarkerPosition, updateLastReadActionID} from '../../../libs/actions/Report'; +import {setNewMarkerPosition, updateLastReadActionID, saveReportActionDraft} from '../../../libs/actions/Report'; 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 withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; const propTypes = { @@ -29,12 +32,26 @@ const propTypes = { // Controls the visibility of this component. isVisible: PropTypes.bool, + // Draft message - if this is set the comment is in 'edit' mode + draftMessage: PropTypes.string, + + // 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, }; const defaultProps = { isMini: false, isVisible: false, + session: {}, + draftMessage: '', }; class ReportActionContextMenu extends React.Component { @@ -90,8 +107,17 @@ class ReportActionContextMenu extends React.Component { { text: this.props.translate('reportActionContextMenu.editComment'), icon: Pencil, - shouldShow: false, - onPress: () => {}, + shouldShow: this.props.reportAction.actorEmail === this.props.session.email + && !isReportMessageAttachment(this.getActionText()) + && this.props.reportAction.reportActionID, + onPress: () => { + this.props.hidePopover(); + saveReportActionDraft( + this.props.reportID, + this.props.reportAction.reportActionID, + _.isEmpty(this.props.draftMessage) ? this.getActionText() : '', + ); + }, }, { @@ -103,6 +129,18 @@ class ReportActionContextMenu extends React.Component { ]; this.wrapperStyle = getReportActionContextMenuStyles(this.props.isMini); + + this.getActionText = this.getActionText.bind(this); + } + + /** + * Gets the text (not HTML) portion of the message in an action. + * + * @return {String} + */ + getActionText() { + const message = _.last(lodashGet(this.props.reportAction, 'message', null)); + return lodashGet(message, 'text', ''); } render() { @@ -116,7 +154,7 @@ class ReportActionContextMenu extends React.Component { successText={contextAction.successText} isMini={this.props.isMini} key={contextAction.text} - onPress={contextAction.onPress} + onPress={() => contextAction.onPress(this.props.reportAction)} /> ))} @@ -127,4 +165,11 @@ class ReportActionContextMenu extends React.Component { ReportActionContextMenu.propTypes = propTypes; ReportActionContextMenu.defaultProps = defaultProps; -export default withLocalize(ReportActionContextMenu); +export default compose( + withLocalize, + withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, + }), +)(ReportActionContextMenu); diff --git a/src/pages/home/report/ReportActionContextMenuItem.js b/src/pages/home/report/ReportActionContextMenuItem.js index 590510650b14..77406ef4d041 100644 --- a/src/pages/home/report/ReportActionContextMenuItem.js +++ b/src/pages/home/report/ReportActionContextMenuItem.js @@ -86,7 +86,6 @@ class ReportActionContextMenuItem extends Component { } } - ReportActionContextMenuItem.propTypes = propTypes; ReportActionContextMenuItem.defaultProps = defaultProps; ReportActionContextMenuItem.displayName = 'ReportActionContextMenuItem'; diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 9ff91edb14ac..50930f2741c4 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -2,6 +2,8 @@ import _ from 'underscore'; import React, {Component} from 'react'; import {View} from 'react-native'; import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; +import ONYXKEYS from '../../../ONYXKEYS'; import ReportActionPropTypes from './ReportActionPropTypes'; import { getReportActionItemStyle, @@ -16,6 +18,7 @@ import ReportActionContextMenu from './ReportActionContextMenu'; import ReportActionItemIOUPreview from '../../../components/ReportActionItemIOUPreview'; import ReportActionItemMessage from './ReportActionItemMessage'; import UnreadActionIndicator from '../../../components/UnreadActionIndicator'; +import ReportActionItemMessageEdit from './ReportActionItemMessageEdit'; const propTypes = { // The ID of the report this action is on. @@ -39,11 +42,19 @@ const propTypes = { // Should we display the new indicator on top of the comment? shouldDisplayNewIndicator: PropTypes.bool.isRequired, + // Position index of the report action in the overall report FlatList view + index: PropTypes.number.isRequired, + + /* --- Onyx Props --- */ + // Draft message - if this is set the comment is in 'edit' mode + draftMessage: PropTypes.string, + // Runs when the view enclosing the chat message lays out indicating it has rendered onLayout: PropTypes.func.isRequired, }; const defaultProps = { + draftMessage: '', iouReportID: undefined, hasOutstandingIOU: false, }; @@ -69,10 +80,11 @@ class ReportActionItem extends Component { shouldComponentUpdate(nextProps, nextState) { return this.state.isPopoverVisible !== nextState.isPopoverVisible || this.props.displayAsGroup !== nextProps.displayAsGroup + || this.props.draftMessage !== nextProps.draftMessage || this.props.isMostRecentIOUReportAction !== nextProps.isMostRecentIOUReportAction || this.props.hasOutstandingIOU !== nextProps.hasOutstandingIOU || this.props.iouReportID !== nextProps.iouReportID - || (this.props.shouldDisplayNewIndicator !== nextProps.shouldDisplayNewIndicator) + || this.props.shouldDisplayNewIndicator !== nextProps.shouldDisplayNewIndicator || !_.isEqual(this.props.action, nextProps.action); } @@ -107,16 +119,28 @@ class ReportActionItem extends Component { } render() { - const children = this.props.action.actionName === 'IOU' - ? ( + let children; + if (this.props.action.actionName === 'IOU') { + children = ( - ) - : ; + ); + } else { + children = !this.props.draftMessage + ? + : ( + + ); + } return ( @@ -125,7 +149,10 @@ class ReportActionItem extends Component { {this.props.shouldDisplayNewIndicator && ( )} - + {!this.props.displayAsGroup ? ( @@ -146,6 +173,8 @@ class ReportActionItem extends Component { hovered && !this.state.isPopoverVisible } + draftMessage={this.props.draftMessage} + hidePopover={this.hidePopover} isMini /> @@ -160,6 +189,7 @@ class ReportActionItem extends Component { isVisible reportID={this.props.reportID} reportAction={this.props.action} + hidePopover={this.hidePopover} /> )} > @@ -167,6 +197,8 @@ class ReportActionItem extends Component { isVisible reportID={this.props.reportID} reportAction={this.props.action} + draftMessage={this.props.draftMessage} + hidePopover={this.hidePopover} /> @@ -179,4 +211,9 @@ class ReportActionItem extends Component { ReportActionItem.propTypes = propTypes; ReportActionItem.defaultProps = defaultProps; -export default ReportActionItem; + +export default withOnyx({ + draftMessage: { + key: ({reportID, action}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}_${action.reportActionID}`, + }, +})(ReportActionItem); diff --git a/src/pages/home/report/ReportActionItemFragment.js b/src/pages/home/report/ReportActionItemFragment.js index a94cfebdb49c..d512f04b33f3 100644 --- a/src/pages/home/report/ReportActionItemFragment.js +++ b/src/pages/home/report/ReportActionItemFragment.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import Str from 'expensify-common/lib/str'; import ReportActionFragmentPropTypes from './ReportActionFragmentPropTypes'; import styles from '../../../styles/styles'; +import variables from '../../../styles/variables'; import themeColors from '../../../styles/themes/default'; import RenderHTML from '../../../components/RenderHTML'; import Text from '../../../components/Text'; @@ -48,10 +49,23 @@ class ReportActionItemFragment extends React.PureComponent { } // Only render HTML if we have html in the fragment - return fragment.html !== fragment.text ? ( - - ) : ( - {Str.htmlDecode(fragment.text)} + return ( + + {fragment.html !== fragment.text ? ( + + ) : ( + {Str.htmlDecode(fragment.text)} + )} + {fragment.isEdited && ( + + (edited) + + )} + ); case 'TEXT': return ( diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js new file mode 100644 index 000000000000..3ad2abd25ce9 --- /dev/null +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -0,0 +1,129 @@ +import React from 'react'; +import {View, Pressable, Text} from 'react-native'; +import PropTypes from 'prop-types'; +import _ from 'underscore'; +import ReportActionPropTypes from './ReportActionPropTypes'; +import styles from '../../../styles/styles'; +import TextInputFocusable from '../../../components/TextInputFocusable'; +import {editReportComment, saveReportActionDraft} from '../../../libs/actions/Report'; +import {scrollToIndex} from '../../../libs/ReportScrollManager'; +import toggleReportActionComposeView from '../../../libs/toggleReportActionComposeView'; +import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; + +const propTypes = { + // All the data of the action + action: PropTypes.shape(ReportActionPropTypes).isRequired, + + // Draft message + draftMessage: PropTypes.string.isRequired, + + // ReportID that holds the comment we're editing + reportID: PropTypes.number.isRequired, + + // Position index of the report action in the overall report FlatList view + index: PropTypes.number.isRequired, + + /* Window Dimensions Props */ + ...windowDimensionsPropTypes, +}; + +class ReportActionItemMessageEdit extends React.Component { + constructor(props) { + super(props); + this.updateDraft = this.updateDraft.bind(this); + this.deleteDraft = this.deleteDraft.bind(this); + this.debouncedSaveDraft = _.debounce(this.debouncedSaveDraft.bind(this), 1000, true); + this.publishDraft = this.publishDraft.bind(this); + this.triggerSaveOrCancel = this.triggerSaveOrCancel.bind(this); + + this.state = { + draft: this.props.draftMessage, + }; + } + + /** + * Update the value of the draft in Onyx + * + * @param {String} newDraft + */ + updateDraft(newDraft) { + const trimmedNewDraft = newDraft.trim(); + this.setState({draft: trimmedNewDraft}); + this.debouncedSaveDraft(trimmedNewDraft); + } + + /** + * Delete the draft of the comment being edited. This will take the comment out of "edit mode" with the old content. + */ + deleteDraft() { + saveReportActionDraft(this.props.reportID, this.props.action.reportActionID, ''); + toggleReportActionComposeView(true, this.props.isSmallScreenWidth); + } + + /** + * Save the draft of the comment. This debounced so that we're not ceaselessly saving your edit. Saving the draft + * allows one to navigate somewhere else and come back to the comment and still have it in edit mode. + */ + debouncedSaveDraft() { + saveReportActionDraft(this.props.reportID, this.props.action.reportActionID, this.state.draft); + } + + /** + * Save the draft of the comment to be the new comment message. This will take the comment out of "edit mode" with + * the new content. + */ + publishDraft() { + editReportComment(this.props.reportID, this.props.action, this.state.draft); + this.deleteDraft(); + } + + /** + * Key event handlers that short cut to saving/canceling. + * + * @param {Event} e + */ + triggerSaveOrCancel(e) { + if (e && e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + this.publishDraft(); + } else if (e && e.key === 'Escape') { + e.preventDefault(); + this.deleteDraft(); + } + } + + render() { + return ( + + { + scrollToIndex({animated: true, index: this.props.index}, true); + toggleReportActionComposeView(false); + }} + autoFocus + /> + + + + Cancel + + + + + Save Changes + + + + + ); + } +} + +ReportActionItemMessageEdit.propTypes = propTypes; +export default withWindowDimensions(ReportActionItemMessageEdit); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 3e4fc52259f0..87d950da39a9 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -30,6 +30,7 @@ import themeColors from '../../../styles/themes/default'; import compose from '../../../libs/compose'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; import withDrawerState, {withDrawerPropTypes} from '../../../components/withDrawerState'; +import {flatListRef, scrollToIndex} from '../../../libs/ReportScrollManager'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; const propTypes = { @@ -303,9 +304,7 @@ class ReportActionsView extends React.Component { * scroll the list to the end. As a report can contain non-message actions, we should confirm that list data exists. */ scrollToListBottom() { - if (this.actionListElement) { - this.actionListElement.scrollToIndex({animated: false, index: 0}); - } + scrollToIndex({animated: false, index: 0}); updateLastReadActionID(this.props.reportID); } @@ -365,6 +364,7 @@ class ReportActionsView extends React.Component { isMostRecentIOUReportAction={item.action.sequenceNumber === this.mostRecentIOUReportSequenceNumber} iouReportID={this.props.report.iouReportID} hasOutstandingIOU={this.props.report.hasOutstandingIOU} + index={index} onLayout={this.recordTimeToMeasureItemLayout} /> ); @@ -389,7 +389,7 @@ class ReportActionsView extends React.Component { return ( this.actionListElement = el} + ref={flatListRef} data={this.sortedReportActions} renderItem={this.renderItem} CellRendererComponent={this.renderCell} @@ -401,6 +401,7 @@ class ReportActionsView extends React.Component { ListFooterComponent={this.state.isLoadingMoreChats ? : null} + keyboardShouldPersistTaps="handled" /> ); } diff --git a/src/pages/home/report/ReportView.js b/src/pages/home/report/ReportView.js index b00a8fb50628..f6d132a451ae 100644 --- a/src/pages/home/report/ReportView.js +++ b/src/pages/home/report/ReportView.js @@ -1,31 +1,53 @@ import React from 'react'; import {Keyboard, View} from 'react-native'; import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; import ReportActionsView from './ReportActionsView'; import ReportActionCompose from './ReportActionCompose'; import {addAction} from '../../../libs/actions/Report'; import KeyboardSpacer from '../../../components/KeyboardSpacer'; import styles from '../../../styles/styles'; import SwipeableView from '../../../components/SwipeableView'; +import ONYXKEYS from '../../../ONYXKEYS'; const propTypes = { /* The ID of the report the selected report */ reportID: PropTypes.number.isRequired, + + /* Onyx Keys */ + // Whether or not to show the Compose Input + session: PropTypes.shape({ + shouldShowComposeInput: PropTypes.bool, + }), +}; + +const defaultProps = { + session: { + shouldShowComposeInput: true, + }, }; -const ReportView = ({reportID}) => ( +const ReportView = ({reportID, session}) => ( - Keyboard.dismiss()}> - addAction(reportID, text)} - reportID={reportID} - /> - + {session.shouldShowComposeInput && ( + Keyboard.dismiss()}> + addAction(reportID, text)} + reportID={reportID} + /> + + )} ); ReportView.propTypes = propTypes; -export default ReportView; +ReportView.defaultProps = defaultProps; + +export default withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, +})(ReportView);