diff --git a/src/CONST.js b/src/CONST.js index accd263483f4..30f9e24ae3e7 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -402,6 +402,7 @@ const CONST = { SHOW_LOADING_SPINNER_DEBOUNCE_TIME: 250, TOOLTIP_SENSE: 1000, TRIE_INITIALIZATION: 'trie_initialization', + COMMENT_LENGTH_DEBOUNCE_TIME: 500, }, PRIORITY_MODE: { GSD: 'gsd', diff --git a/src/components/ExceededCommentLength.js b/src/components/ExceededCommentLength.js index 33f557c66d99..4ef6a5027e73 100644 --- a/src/components/ExceededCommentLength.js +++ b/src/components/ExceededCommentLength.js @@ -1,27 +1,62 @@ -import React from 'react'; +import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; +import {debounce} from 'lodash'; import CONST from '../CONST'; +import * as ReportUtils from '../libs/ReportUtils'; import Text from './Text'; import styles from '../styles/styles'; const propTypes = { - /** The current length of the comment */ - commentLength: PropTypes.number.isRequired, + /** Text Comment */ + comment: PropTypes.string.isRequired, + + /** Update UI on parent when comment length is exceeded */ + onExceededMaxCommentLength: PropTypes.func.isRequired, }; -const ExceededCommentLength = (props) => { - if (props.commentLength <= CONST.MAX_COMMENT_LENGTH) { - return null; +class ExceededCommentLength extends PureComponent { + constructor(props) { + super(props); + + this.state = { + commentLength: 0, + }; + + // By debouncing, we defer the calculation until there is a break in typing + this.updateCommentLength = debounce(this.updateCommentLength.bind(this), CONST.TIMING.COMMENT_LENGTH_DEBOUNCE_TIME); } - return ( - - {`${props.commentLength}/${CONST.MAX_COMMENT_LENGTH}`} - - ); -}; + componentDidMount() { + this.updateCommentLength(); + } + + componentDidUpdate(prevProps) { + if (prevProps.comment === this.props.comment) { + return; + } + + this.updateCommentLength(); + } + + updateCommentLength() { + const commentLength = ReportUtils.getCommentLength(this.props.comment); + this.setState({commentLength}); + this.props.onExceededMaxCommentLength(commentLength > CONST.MAX_COMMENT_LENGTH); + } + + render() { + if (this.state.commentLength <= CONST.MAX_COMMENT_LENGTH) { + return null; + } + + return ( + + {`${this.state.commentLength}/${CONST.MAX_COMMENT_LENGTH}`} + + ); + } +} ExceededCommentLength.propTypes = propTypes; -ExceededCommentLength.displayName = 'ExceededCommentLength'; export default ExceededCommentLength; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index c824ea3e180a..c2049dd87516 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -774,6 +774,15 @@ function hasReportNameError(report) { return !_.isEmpty(lodashGet(report, 'errorFields.reportName', {})); } +/** + * @param {String} text + * @returns {String} + */ +function getParsedComment(text) { + const parser = new ExpensiMark(); + return text.length < CONST.MAX_MARKUP_LENGTH ? parser.replace(text) : text; +} + /** * @param {String} [text] * @param {File} [file] @@ -783,7 +792,7 @@ function buildOptimisticAddCommentReportAction(text, file) { // For comments shorter than 10k chars, convert the comment from MD into HTML because that's how it is stored in the database // For longer comments, skip parsing and display plaintext for performance reasons. It takes over 40s to parse a 100k long string!! const parser = new ExpensiMark(); - const commentText = text.length < CONST.MAX_MARKUP_LENGTH ? parser.replace(text) : text; + const commentText = getParsedComment(text); const isAttachment = _.isEmpty(text) && file !== undefined; const attachmentInfo = isAttachment ? file : {}; const htmlForNewComment = isAttachment ? 'Uploading Attachment...' : commentText; @@ -1424,13 +1433,13 @@ function getNewMarkerReportActionID(report, sortedAndFilteredReportActions) { } /** - * Replace code points > 127 with C escape sequences, and return the resulting string's overall length - * Used for compatibility with the backend auth validator for AddComment + * Performs the markdown conversion, and replaces code points > 127 with C escape sequences + * Used for compatibility with the backend auth validator for AddComment, and to account for MD in comments * @param {String} textComment - * @returns {Number} + * @returns {Number} The comment's total length as seen from the backend */ function getCommentLength(textComment) { - return textComment.replace(/[^ -~]/g, '\\u????').length; + return getParsedComment(textComment).replace(/[^ -~]/g, '\\u????').trim().length; } /** diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 0efb256e8144..dd6d208e1123 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -132,6 +132,7 @@ class ReportActionCompose extends React.Component { this.getInputPlaceholder = this.getInputPlaceholder.bind(this); this.getIOUOptions = this.getIOUOptions.bind(this); this.addAttachment = this.addAttachment.bind(this); + this.setExceededMaxCommentLength = this.setExceededMaxCommentLength.bind(this); this.comment = props.comment; // React Native will retain focus on an input for native devices but web/mWeb behave differently so we have some focus management @@ -153,6 +154,7 @@ class ReportActionCompose extends React.Component { // If we are on a small width device then don't show last 3 items from conciergePlaceholderOptions conciergePlaceholderRandomIndex: _.random(this.props.translate('reportActionCompose.conciergePlaceholderOptions').length - (this.props.isSmallScreenWidth ? 4 : 1)), + hasExceededMaxCommentLength: false, }; } @@ -302,6 +304,16 @@ class ReportActionCompose extends React.Component { this.setState({maxLines}); } + /** + * Updates the composer when the comment length is exceeded + * Shows red borders and prevents the comment from being sent + * + * @param {Boolean} hasExceededMaxCommentLength + */ + setExceededMaxCommentLength(hasExceededMaxCommentLength) { + this.setState({hasExceededMaxCommentLength}); + } + isEmptyChat() { return _.size(this.props.reportActions) === 1; } @@ -513,8 +525,7 @@ class ReportActionCompose extends React.Component { const isComposeDisabled = this.props.isDrawerOpen && this.props.isSmallScreenWidth; const isBlockedFromConcierge = ReportUtils.chatIncludesConcierge(this.props.report) && User.isBlockedFromConcierge(this.props.blockedFromConcierge); const inputPlaceholder = this.getInputPlaceholder(); - const encodedCommentLength = ReportUtils.getCommentLength(this.comment); - const hasExceededMaxCommentLength = encodedCommentLength > CONST.MAX_COMMENT_LENGTH; + const hasExceededMaxCommentLength = this.state.hasExceededMaxCommentLength; return ( {!this.props.isSmallScreenWidth && } - + {this.state.isDraggingOver && } diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index abef7beb65ca..37369611f190 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -69,6 +69,7 @@ class ReportActionItemMessageEdit extends React.Component { this.triggerSaveOrCancel = this.triggerSaveOrCancel.bind(this); this.onSelectionChange = this.onSelectionChange.bind(this); this.addEmojiToTextBox = this.addEmojiToTextBox.bind(this); + this.setExceededMaxCommentLength = this.setExceededMaxCommentLength.bind(this); this.saveButtonID = 'saveButton'; this.cancelButtonID = 'cancelButton'; this.emojiButtonID = 'emojiButton'; @@ -84,6 +85,7 @@ class ReportActionItemMessageEdit extends React.Component { end: draftMessage.length, }, isFocused: false, + hasExceededMaxCommentLength: false, }; } @@ -96,6 +98,16 @@ class ReportActionItemMessageEdit extends React.Component { this.setState({selection: e.nativeEvent.selection}); } + /** + * Updates the composer when the comment length is exceeded + * Shows red borders and prevents the comment from being sent + * + * @param {Boolean} hasExceededMaxCommentLength + */ + setExceededMaxCommentLength(hasExceededMaxCommentLength) { + this.setState({hasExceededMaxCommentLength}); + } + /** * Update the value of the draft in Onyx * @@ -217,8 +229,7 @@ class ReportActionItemMessageEdit extends React.Component { } render() { - const draftLength = ReportUtils.getCommentLength(this.state.draft); - const hasExceededMaxCommentLength = draftLength > CONST.MAX_COMMENT_LENGTH; + const hasExceededMaxCommentLength = this.state.hasExceededMaxCommentLength; return ( - + );