Skip to content

Commit

Permalink
Refactor comment message rendering
Browse files Browse the repository at this point in the history
...so the logic for rendering attachments is clearly separated from the
logic for rendering textual comments.

This fixes #25415
  • Loading branch information
cubuspl42 committed Oct 2, 2023
1 parent dee1632 commit 5d914f1
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 56 deletions.
81 changes: 29 additions & 52 deletions src/pages/home/report/ReportActionItemFragment.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import React, {memo} from 'react';
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';
import * as EmojiUtils from '../../../libs/EmojiUtils';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
import * as DeviceCapabilities from '../../../libs/DeviceCapabilities';
import compose from '../../../libs/compose';
import convertToLTR from '../../../libs/convertToLTR';
import {withNetwork} from '../../../components/OnyxProvider';
import CONST from '../../../CONST';
import editedLabelStyles from '../../../styles/editedLabelStyles';
import UserDetailsTooltip from '../../../components/UserDetailsTooltip';
import avatarPropTypes from '../../../components/avatarPropTypes';
import * as ReportUtils from '../../../libs/ReportUtils';
import AttachmentCommentFragment from './comment/AttachmentCommentFragment';
import TextCommentFragment from './comment/TextCommentFragment';

const propTypes = {
/** Users accountID */
Expand Down Expand Up @@ -62,6 +58,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.isRequired,

...windowDimensionsPropTypes,

/** localization props */
Expand All @@ -85,62 +84,40 @@ 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>`} />;
// Does the fragment content represent an attachment?
const isFragmentAttachment = ReportUtils.isReportMessageAttachment(fragment);

if (isFragmentAttachment) {
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]}>
<Text
selectable={!DeviceCapabilities.canUseTouchScreen() || !props.isSmallScreenWidth}
style={[containsOnlyEmojis ? styles.onlyEmojisText : undefined, styles.ltr, ...props.style, isPendingDelete ? styles.offlineFeedback.deleted : undefined]}
>
{convertToLTR(props.iouMessage || text)}
</Text>
{Boolean(props.fragment.isEdited) && (
<>
<Text
selectable={false}
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}
style={props.style}
/>
);
}
case 'TEXT':
Expand All @@ -154,7 +131,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
8 changes: 4 additions & 4 deletions src/pages/home/report/ReportActionItemMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,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 @@ -48,9 +47,9 @@ function ReportActionItemMessage(props) {
}

return (
<View style={[styles.chatItemMessage, !props.displayAsGroup && isAttachment ? styles.mt2 : {}, ...props.style]}>
<View style={[styles.chatItemMessage, ...props.style]}>
{!props.isHidden ? (
_.map(messages, (fragment, index) => (
_.map(fragments, (fragment, index) => (
<ReportActionItemFragment
key={`actionFragment-${props.action.reportActionID}-${index}`}
fragment={fragment}
Expand All @@ -60,6 +59,7 @@ function ReportActionItemMessage(props) {
pendingAction={props.action.pendingAction}
source={lodashGet(props.action, 'originalMessage.source')}
accountID={props.action.actorAccountID}
displayAsGroup={props.displayAsGroup}
style={props.style}
/>
))
Expand Down
32 changes: 32 additions & 0 deletions src/pages/home/report/comment/AttachmentCommentFragment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import styles from '../../../../styles/styles';
import RenderCommentHTML from './RenderCommentHTML';

const propTypes = {
/** The reportAction's source */
source: PropTypes.oneOf(['Chronos', 'email', 'ios', 'android', 'web', 'email', '']).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(props) {
return (
<View style={props.addExtraMargin ? styles.mt2 : {}}>
<RenderCommentHTML
source={props.source}
html={props.html}
/>
</View>
);
}

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

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

const propTypes = {
/** The reportAction's source */
source: PropTypes.oneOf(['Chronos', 'email', 'ios', 'android', 'web', 'email', '']).isRequired,

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

function RenderCommentHTML(props) {
const html = props.html;

return <RenderHTML html={props.source === 'email' ? `<email-comment>${html}</email-comment>` : `<comment>${html}</comment>`} />;
}

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

export default RenderCommentHTML;
97 changes: 97 additions & 0 deletions src/pages/home/report/comment/TextCommentFragment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, {memo} from 'react';
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 Text from '../../../../components/Text';
import * as EmojiUtils from '../../../../libs/EmojiUtils';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../../../components/withWindowDimensions';
import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities';
import compose from '../../../../libs/compose';
import convertToLTR from '../../../../libs/convertToLTR';
import CONST from '../../../../CONST';
import editedLabelStyles from '../../../../styles/editedLabelStyles';
import RenderCommentHTML from './RenderCommentHTML';

const propTypes = {
/** The reportAction's source */
source: PropTypes.oneOf(['Chronos', 'email', 'ios', 'android', 'web', 'email', '']).isRequired,

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

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

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

...windowDimensionsPropTypes,

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

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]}>
<Text
selectable={!DeviceCapabilities.canUseTouchScreen() || !props.isSmallScreenWidth}
style={[containsOnlyEmojis ? styles.onlyEmojisText : undefined, styles.ltr, ...props.style, styleAsDeleted ? styles.offlineFeedback.deleted : undefined]}
>
{convertToLTR(props.iouMessage || text)}
</Text>
{Boolean(fragment.isEdited) && (
<>
<Text
selectable={false}
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.displayName = 'TextCommentFragment';

export default compose(withWindowDimensions, withLocalize)(memo(TextCommentFragment));

0 comments on commit 5d914f1

Please sign in to comment.