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);