Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Account for markdown using debounced comment counter #15501

Merged
merged 1 commit into from
Feb 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ const CONST = {
SHOW_LOADING_SPINNER_DEBOUNCE_TIME: 250,
TOOLTIP_SENSE: 1000,
TRIE_INITIALIZATION: 'trie_initialization',
COMMENT_LENGTH_DEBOUNCE_TIME: 500,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually I like to include the unit for time constants (e.g. append _MS) to make it clear, but we already don't do it for the other constants above, so not blocking on this.

},
PRIORITY_MODE: {
GSD: 'gsd',
Expand Down
61 changes: 48 additions & 13 deletions src/components/ExceededCommentLength.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,62 @@
import React from 'react';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {debounce} from 'lodash';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, in the rest of the codebase I see that we use UnderscoreJS's debounce function, not the one from lodash. AFAICT they must be pretty much the same though.

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 (
<Text style={[styles.textMicro, styles.textDanger, styles.chatItemComposeSecondaryRow, styles.mlAuto, styles.pl2]}>
{`${props.commentLength}/${CONST.MAX_COMMENT_LENGTH}`}
</Text>
);
};
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 (
<Text style={[styles.textMicro, styles.textDanger, styles.chatItemComposeSecondaryRow, styles.mlAuto, styles.pl2]}>
{`${this.state.commentLength}/${CONST.MAX_COMMENT_LENGTH}`}
</Text>
);
}
}

ExceededCommentLength.propTypes = propTypes;
ExceededCommentLength.displayName = 'ExceededCommentLength';

export default ExceededCommentLength;
19 changes: 14 additions & 5 deletions src/libs/ReportUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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;
Expand Down Expand Up @@ -1425,13 +1434,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;
}

/**
Expand Down
17 changes: 14 additions & 3 deletions src/pages/home/report/ReportActionCompose.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
};
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 (
<View style={[
Expand Down Expand Up @@ -709,7 +720,7 @@ class ReportActionCompose extends React.Component {
>
{!this.props.isSmallScreenWidth && <OfflineIndicator containerStyles={[styles.chatItemComposeSecondaryRow]} />}
<ReportTypingIndicator reportID={this.props.reportID} />
<ExceededCommentLength commentLength={encodedCommentLength} />
<ExceededCommentLength comment={this.comment} onExceededMaxCommentLength={this.setExceededMaxCommentLength} />
</View>
{this.state.isDraggingOver && <ReportDropUI />}
</View>
Expand Down
17 changes: 14 additions & 3 deletions src/pages/home/report/ReportActionItemMessageEdit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -84,6 +85,7 @@ class ReportActionItemMessageEdit extends React.Component {
end: draftMessage.length,
},
isFocused: false,
hasExceededMaxCommentLength: false,
};
}

Expand All @@ -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
*
Expand Down Expand Up @@ -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 (
<View style={styles.chatItemMessage}>
<View
Expand Down Expand Up @@ -290,7 +301,7 @@ class ReportActionItemMessageEdit extends React.Component {
onPress={this.publishDraft}
text={this.props.translate('common.saveChanges')}
/>
<ExceededCommentLength commentLength={draftLength} />
<ExceededCommentLength comment={this.state.draft} onExceededMaxCommentLength={this.setExceededMaxCommentLength} />
</View>
</View>
);
Expand Down