Skip to content

Commit

Permalink
Merge pull request Expensify#25524 from cubuspl42/fix-name-jump-1
Browse files Browse the repository at this point in the history
Refactor comment message rendering
  • Loading branch information
flodnv authored Nov 15, 2023
2 parents 181aeb2 + a9eb468 commit 48782c7
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 67 deletions.
91 changes: 28 additions & 63 deletions src/pages/home/report/ReportActionItemFragment.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Str from 'expensify-common/lib/str';
import PropTypes from 'prop-types';
import React, {memo} from 'react';
import avatarPropTypes from '@components/avatarPropTypes';
Expand All @@ -8,16 +7,13 @@ import Text from '@components/Text';
import UserDetailsTooltip from '@components/UserDetailsTooltip';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
import ZeroWidthView from '@components/ZeroWidthView';
import compose from '@libs/compose';
import convertToLTR from '@libs/convertToLTR';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as EmojiUtils from '@libs/EmojiUtils';
import editedLabelStyles from '@styles/editedLabelStyles';
import * as ReportUtils from '@libs/ReportUtils';
import styles from '@styles/styles';
import themeColors from '@styles/themes/default';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import AttachmentCommentFragment from './comment/AttachmentCommentFragment';
import TextCommentFragment from './comment/TextCommentFragment';
import reportActionFragmentPropTypes from './reportActionFragmentPropTypes';

const propTypes = {
Expand Down Expand Up @@ -63,6 +59,9 @@ const propTypes = {
/** Whether the comment is a thread parent message/the first message in a thread */
isThreadParentMessage: PropTypes.bool,

/** Should the comment have the appearance of being grouped with the previous comment? */
displayAsGroup: PropTypes.bool,

/** Whether the report action type is 'APPROVED' or 'SUBMITTED'. Used to style system messages from Old Dot */
isApprovedOrSubmittedReportAction: PropTypes.bool,

Expand All @@ -73,9 +72,6 @@ const propTypes = {

/** localization props */
...withLocalizePropTypes,

/** Should the comment have the appearance of being grouped with the previous comment? */
displayAsGroup: PropTypes.bool,
};

const defaultProps = {
Expand All @@ -98,70 +94,39 @@ const defaultProps = {
};

function ReportActionItemFragment(props) {
switch (props.fragment.type) {
const fragment = props.fragment;

switch (fragment.type) {
case 'COMMENT': {
const {html, text} = props.fragment;
const isPendingDelete = props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && props.network.isOffline;
const isPendingDelete = props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;

// Threaded messages display "[Deleted message]" instead of being hidden altogether.
// While offline we display the previous message with a strikethrough style. Once online we want to
// immediately display "[Deleted message]" while the delete action is pending.

if ((!props.network.isOffline && props.isThreadParentMessage && props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) || props.fragment.isDeletedParentAction) {
if ((!props.network.isOffline && props.isThreadParentMessage && isPendingDelete) || props.fragment.isDeletedParentAction) {
return <RenderHTML html={`<comment>${props.translate('parentReportAction.deletedMessage')}</comment>`} />;
}

// If the only difference between fragment.text and fragment.html is <br /> tags
// we render it as text, not as html.
// This is done to render emojis with line breaks between them as text.
const differByLineBreaksOnly = Str.replaceAll(html, '<br />', '\n') === text;

// Only render HTML if we have html in the fragment
if (!differByLineBreaksOnly) {
const editedTag = props.fragment.isEdited ? `<edited ${isPendingDelete ? 'deleted' : ''}></edited>` : '';
const htmlContent = isPendingDelete ? `<del>${html}</del>` : html;

const htmlWithTag = editedTag ? `${htmlContent}${editedTag}` : htmlContent;

return <RenderHTML html={props.source === 'email' ? `<email-comment>${htmlWithTag}</email-comment>` : `<comment>${htmlWithTag}</comment>`} />;
if (ReportUtils.isReportMessageAttachment(fragment)) {
return (
<AttachmentCommentFragment
source={props.source}
html={fragment.html}
addExtraMargin={!props.displayAsGroup}
/>
);
}
const containsOnlyEmojis = EmojiUtils.containsOnlyEmojis(text);

return (
<Text style={[containsOnlyEmojis ? styles.onlyEmojisText : undefined, styles.ltr, ...props.style]}>
<ZeroWidthView
text={text}
displayAsGroup={props.displayAsGroup}
/>
<Text
style={[
containsOnlyEmojis ? styles.onlyEmojisText : undefined,
styles.ltr,
...props.style,
isPendingDelete ? styles.offlineFeedback.deleted : undefined,
!DeviceCapabilities.canUseTouchScreen() || !props.isSmallScreenWidth ? styles.userSelectText : styles.userSelectNone,
]}
>
{convertToLTR(props.iouMessage || text)}
</Text>
{Boolean(props.fragment.isEdited) && (
<>
<Text
style={[containsOnlyEmojis ? styles.onlyEmojisTextLineHeight : undefined, styles.userSelectNone]}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
{' '}
</Text>
<Text
fontSize={variables.fontSizeSmall}
color={themeColors.textSupporting}
style={[editedLabelStyles, isPendingDelete ? styles.offlineFeedback.deleted : undefined, ...props.style]}
>
{props.translate('reportActionCompose.edited')}
</Text>
</>
)}
</Text>
<TextCommentFragment
source={props.source}
fragment={fragment}
styleAsDeleted={isPendingDelete && props.network.isOffline}
iouMessage={props.iouMessage}
displayAsGroup={props.displayAsGroup}
style={props.style}
/>
);
}
case 'TEXT': {
Expand All @@ -182,7 +147,7 @@ function ReportActionItemFragment(props) {
numberOfLines={props.isSingleLine ? 1 : undefined}
style={[styles.chatItemMessageHeaderSender, props.isSingleLine ? styles.pre : styles.preWrap]}
>
{props.fragment.text}
{fragment.text}
</Text>
</UserDetailsTooltip>
);
Expand Down
7 changes: 3 additions & 4 deletions src/pages/home/report/ReportActionItemMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ const defaultProps = {
};

function ReportActionItemMessage(props) {
const messages = _.compact(props.action.previousMessage || props.action.message);
const isAttachment = ReportUtils.isReportMessageAttachment(_.last(messages));
const fragments = _.compact(props.action.previousMessage || props.action.message);
const isIOUReport = ReportActionsUtils.isMoneyRequestAction(props.action);
let iouMessage;
if (isIOUReport) {
Expand All @@ -56,7 +55,7 @@ function ReportActionItemMessage(props) {
* @returns {Object} report action item fragments
*/
const renderReportActionItemFragments = (shouldWrapInText) => {
const reportActionItemFragments = _.map(messages, (fragment, index) => (
const reportActionItemFragments = _.map(fragments, (fragment, index) => (
<ReportActionItemFragment
key={`actionFragment-${props.action.reportActionID}-${index}`}
fragment={fragment}
Expand Down Expand Up @@ -84,7 +83,7 @@ function ReportActionItemMessage(props) {
};

return (
<View style={[styles.chatItemMessage, !props.displayAsGroup && isAttachment ? styles.mt2 : {}, ...props.style]}>
<View style={[styles.chatItemMessage, ...props.style]}>
{!props.isHidden ? (
renderReportActionItemFragments(isApprovedOrSubmittedReportAction)
) : (
Expand Down
33 changes: 33 additions & 0 deletions src/pages/home/report/comment/AttachmentCommentFragment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import reportActionSourcePropType from '@pages/home/report/reportActionSourcePropType';
import styles from '@styles/styles';
import RenderCommentHTML from './RenderCommentHTML';

const propTypes = {
/** The reportAction's source */
source: reportActionSourcePropType.isRequired,

/** The message fragment's HTML */
html: PropTypes.string.isRequired,

/** Should extra margin be added on top of the component? */
addExtraMargin: PropTypes.bool.isRequired,
};

function AttachmentCommentFragment({addExtraMargin, html, source}) {
return (
<View style={addExtraMargin ? styles.mt2 : {}}>
<RenderCommentHTML
source={source}
html={html}
/>
</View>
);
}

AttachmentCommentFragment.propTypes = propTypes;
AttachmentCommentFragment.displayName = 'AttachmentCommentFragment';

export default AttachmentCommentFragment;
23 changes: 23 additions & 0 deletions src/pages/home/report/comment/RenderCommentHTML.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import PropTypes from 'prop-types';
import React from 'react';
import RenderHTML from '@components/RenderHTML';
import reportActionSourcePropType from '@pages/home/report/reportActionSourcePropType';

const propTypes = {
/** The reportAction's source */
source: reportActionSourcePropType.isRequired,

/** The comment's HTML */
html: PropTypes.string.isRequired,
};

function RenderCommentHTML({html, source}) {
const commentHtml = source === 'email' ? `<email-comment>${html}</email-comment>` : `<comment>${html}</comment>`;

return <RenderHTML html={commentHtml} />;
}

RenderCommentHTML.propTypes = propTypes;
RenderCommentHTML.displayName = 'RenderCommentHTML';

export default RenderCommentHTML;
118 changes: 118 additions & 0 deletions src/pages/home/report/comment/TextCommentFragment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import Str from 'expensify-common/lib/str';
import PropTypes from 'prop-types';
import React, {memo} from 'react';
import Text from '@components/Text';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
import ZeroWidthView from '@components/ZeroWidthView';
import compose from '@libs/compose';
import convertToLTR from '@libs/convertToLTR';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as EmojiUtils from '@libs/EmojiUtils';
import reportActionFragmentPropTypes from '@pages/home/report/reportActionFragmentPropTypes';
import reportActionSourcePropType from '@pages/home/report/reportActionSourcePropType';
import editedLabelStyles from '@styles/editedLabelStyles';
import styles from '@styles/styles';
import themeColors from '@styles/themes/default';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import RenderCommentHTML from './RenderCommentHTML';

const propTypes = {
/** The reportAction's source */
source: reportActionSourcePropType.isRequired,

/** The message fragment needing to be displayed */
fragment: reportActionFragmentPropTypes.isRequired,

/** Should this message fragment be styled as deleted? */
styleAsDeleted: PropTypes.bool.isRequired,

/** Text of an IOU report action */
iouMessage: PropTypes.string,

/** Should the comment have the appearance of being grouped with the previous comment? */
displayAsGroup: PropTypes.bool.isRequired,

/** Additional styles to add after local styles. */
style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]).isRequired,

...windowDimensionsPropTypes,

/** localization props */
...withLocalizePropTypes,
};

const defaultProps = {
iouMessage: undefined,
};

function TextCommentFragment(props) {
const {fragment, styleAsDeleted} = props;
const {html, text} = fragment;

// If the only difference between fragment.text and fragment.html is <br /> tags
// we render it as text, not as html.
// This is done to render emojis with line breaks between them as text.
const differByLineBreaksOnly = Str.replaceAll(html, '<br />', '\n') === text;

// Only render HTML if we have html in the fragment
if (!differByLineBreaksOnly) {
const editedTag = fragment.isEdited ? `<edited ${styleAsDeleted ? 'deleted' : ''}></edited>` : '';
const htmlContent = styleAsDeleted ? `<del>${html}</del>` : html;

const htmlWithTag = editedTag ? `${htmlContent}${editedTag}` : htmlContent;

return (
<RenderCommentHTML
source={props.source}
html={htmlWithTag}
/>
);
}

const containsOnlyEmojis = EmojiUtils.containsOnlyEmojis(text);

return (
<Text style={[containsOnlyEmojis ? styles.onlyEmojisText : undefined, styles.ltr, ...props.style]}>
<ZeroWidthView
text={text}
displayAsGroup={props.displayAsGroup}
/>
<Text
style={[
containsOnlyEmojis ? styles.onlyEmojisText : undefined,
styles.ltr,
...props.style,
styleAsDeleted ? styles.offlineFeedback.deleted : undefined,
!DeviceCapabilities.canUseTouchScreen() || !props.isSmallScreenWidth ? styles.userSelectText : styles.userSelectNone,
]}
>
{convertToLTR(props.iouMessage || text)}
</Text>
{Boolean(fragment.isEdited) && (
<>
<Text
style={[containsOnlyEmojis ? styles.onlyEmojisTextLineHeight : undefined, styles.userSelectNone]}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
{' '}
</Text>
<Text
fontSize={variables.fontSizeSmall}
color={themeColors.textSupporting}
style={[editedLabelStyles, styleAsDeleted ? styles.offlineFeedback.deleted : undefined, ...props.style]}
>
{props.translate('reportActionCompose.edited')}
</Text>
</>
)}
</Text>
);
}

TextCommentFragment.propTypes = propTypes;
TextCommentFragment.defaultProps = defaultProps;
TextCommentFragment.displayName = 'TextCommentFragment';

export default compose(withWindowDimensions, withLocalize)(memo(TextCommentFragment));
3 changes: 3 additions & 0 deletions src/pages/home/report/reportActionSourcePropType.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import PropTypes from 'prop-types';

export default PropTypes.oneOf(['Chronos', 'email', 'ios', 'android', 'web', 'email', '']);

0 comments on commit 48782c7

Please sign in to comment.