diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 4da91c2e7d19..8ea8a1bb6f64 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -124,3 +124,4 @@ function Avatar({ Avatar.displayName = 'Avatar'; export default Avatar; +export {type AvatarProps}; diff --git a/src/components/InvertedFlatList/CellRendererComponent.tsx b/src/components/InvertedFlatList/CellRendererComponent.tsx index b95fbf42cbb4..1199fb2a594c 100644 --- a/src/components/InvertedFlatList/CellRendererComponent.tsx +++ b/src/components/InvertedFlatList/CellRendererComponent.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import type {StyleProp, ViewProps} from 'react-native'; +import type {StyleProp, ViewProps, ViewStyle} from 'react-native'; import {View} from 'react-native'; type CellRendererComponentProps = ViewProps & { index: number; - style?: StyleProp; + style?: StyleProp; }; function CellRendererComponent(props: CellRendererComponentProps) { diff --git a/src/pages/home/report/ReportActionItemFragment.js b/src/pages/home/report/ReportActionItemFragment.js deleted file mode 100644 index f05b3decc6d7..000000000000 --- a/src/pages/home/report/ReportActionItemFragment.js +++ /dev/null @@ -1,179 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {memo} from 'react'; -import avatarPropTypes from '@components/avatarPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; -import RenderHTML from '@components/RenderHTML'; -import Text from '@components/Text'; -import UserDetailsTooltip from '@components/UserDetailsTooltip'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import convertToLTR from '@libs/convertToLTR'; -import * as ReportUtils from '@libs/ReportUtils'; -import CONST from '@src/CONST'; -import AttachmentCommentFragment from './comment/AttachmentCommentFragment'; -import TextCommentFragment from './comment/TextCommentFragment'; -import reportActionFragmentPropTypes from './reportActionFragmentPropTypes'; - -const propTypes = { - /** Users accountID */ - accountID: PropTypes.number.isRequired, - - /** The message fragment needing to be displayed */ - fragment: reportActionFragmentPropTypes.isRequired, - - /** If this fragment is attachment than has info? */ - attachmentInfo: PropTypes.shape({ - /** The file name of attachment */ - name: PropTypes.string, - - /** The file size of the attachment in bytes. */ - size: PropTypes.number, - - /** The MIME type of the attachment. */ - type: PropTypes.string, - - /** Attachment's URL represents the specified File object or Blob object */ - source: PropTypes.string, - }), - - /** Message(text) of an IOU report action */ - iouMessage: PropTypes.string, - - /** The reportAction's source */ - source: PropTypes.oneOf(['Chronos', 'email', 'ios', 'android', 'web', 'email', '']), - - /** Should this fragment be contained in a single line? */ - isSingleLine: PropTypes.bool, - - // Additional styles to add after local styles - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - - /** The accountID of the copilot who took this action on behalf of the user */ - delegateAccountID: PropTypes.number, - - /** icon */ - actorIcon: avatarPropTypes, - - /** 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, - - /** Used to format RTL display names in Old Dot system messages e.g. Arabic */ - isFragmentContainingDisplayName: PropTypes.bool, - - ...windowDimensionsPropTypes, - - /** localization props */ - ...withLocalizePropTypes, -}; - -const defaultProps = { - attachmentInfo: { - name: '', - size: 0, - type: '', - source: '', - }, - iouMessage: '', - isSingleLine: false, - source: '', - style: [], - delegateAccountID: 0, - actorIcon: {}, - isThreadParentMessage: false, - isApprovedOrSubmittedReportAction: false, - isFragmentContainingDisplayName: false, - displayAsGroup: false, -}; - -function ReportActionItemFragment(props) { - const styles = useThemeStyles(); - const fragment = props.fragment; - - switch (fragment.type) { - case 'COMMENT': { - 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 && isPendingDelete) || props.fragment.isDeletedParentAction) { - return ${props.translate('parentReportAction.deletedMessage')}`} />; - } - - if (ReportUtils.isReportMessageAttachment(fragment)) { - return ( - - ); - } - - return ( - - ); - } - case 'TEXT': { - return props.isApprovedOrSubmittedReportAction ? ( - - {props.isFragmentContainingDisplayName ? convertToLTR(props.fragment.text) : props.fragment.text} - - ) : ( - - - {fragment.text} - - - ); - } - case 'LINK': - return LINK; - case 'INTEGRATION_COMMENT': - return REPORT_LINK; - case 'REPORT_LINK': - return REPORT_LINK; - case 'POLICY_LINK': - return POLICY_LINK; - - // If we have a message fragment type of OLD_MESSAGE this means we have not yet converted this over to the - // new data structure. So we simply set this message as inner html and render it like we did before. - // This wil allow us to convert messages over to the new structure without needing to do it all at once. - case 'OLD_MESSAGE': - return OLD_MESSAGE; - default: - return props.fragment.text; - } -} - -ReportActionItemFragment.propTypes = propTypes; -ReportActionItemFragment.defaultProps = defaultProps; -ReportActionItemFragment.displayName = 'ReportActionItemFragment'; - -export default compose(withWindowDimensions, withLocalize, withNetwork())(memo(ReportActionItemFragment)); diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx new file mode 100644 index 000000000000..01918b377c62 --- /dev/null +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -0,0 +1,156 @@ +import React, {memo} from 'react'; +import type {StyleProp, TextStyle} from 'react-native'; +import type {AvatarProps} from '@components/Avatar'; +import RenderHTML from '@components/RenderHTML'; +import Text from '@components/Text'; +import UserDetailsTooltip from '@components/UserDetailsTooltip'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useThemeStyles from '@hooks/useThemeStyles'; +import convertToLTR from '@libs/convertToLTR'; +import * as ReportUtils from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; +import type {Message} from '@src/types/onyx/ReportAction'; +import AttachmentCommentFragment from './comment/AttachmentCommentFragment'; +import TextCommentFragment from './comment/TextCommentFragment'; + +type ReportActionItemFragmentProps = { + /** Users accountID */ + accountID: number; + + /** The message fragment needing to be displayed */ + fragment: Message; + + /** Message(text) of an IOU report action */ + iouMessage?: string; + + /** The reportAction's source */ + source?: OriginalMessageSource; + + /** Should this fragment be contained in a single line? */ + isSingleLine?: boolean; + + /** Additional styles to add after local styles */ + style?: StyleProp; + + /** The accountID of the copilot who took this action on behalf of the user */ + delegateAccountID?: number; + + /** icon */ + actorIcon?: AvatarProps; + + /** Whether the comment is a thread parent message/the first message in a thread */ + isThreadParentMessage?: boolean; + + /** Should the comment have the appearance of being grouped with the previous comment? */ + displayAsGroup?: boolean; + + /** Whether the report action type is 'APPROVED' or 'SUBMITTED'. Used to style system messages from Old Dot */ + isApprovedOrSubmittedReportAction?: boolean; + + /** Used to format RTL display names in Old Dot system messages e.g. Arabic */ + isFragmentContainingDisplayName?: boolean; + + /** The pending action for the report action */ + pendingAction?: OnyxCommon.PendingAction; +}; + +function ReportActionItemFragment({ + pendingAction, + fragment, + accountID, + iouMessage = '', + isSingleLine = false, + source = '', + style = [], + delegateAccountID = 0, + actorIcon = {}, + isThreadParentMessage = false, + isApprovedOrSubmittedReportAction = false, + isFragmentContainingDisplayName = false, + displayAsGroup = false, +}: ReportActionItemFragmentProps) { + const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + const {translate} = useLocalize(); + + switch (fragment.type) { + case 'COMMENT': { + const isPendingDelete = 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 ((!isOffline && isThreadParentMessage && isPendingDelete) || fragment.isDeletedParentAction) { + return ${translate('parentReportAction.deletedMessage')}`} />; + } + + if (ReportUtils.isReportMessageAttachment(fragment)) { + return ( + + ); + } + + return ( + + ); + } + case 'TEXT': { + return isApprovedOrSubmittedReportAction ? ( + + {isFragmentContainingDisplayName ? convertToLTR(fragment.text) : fragment.text} + + ) : ( + + + {fragment.text} + + + ); + } + case 'LINK': + return LINK; + case 'INTEGRATION_COMMENT': + return REPORT_LINK; + case 'REPORT_LINK': + return REPORT_LINK; + case 'POLICY_LINK': + return POLICY_LINK; + + // If we have a message fragment type of OLD_MESSAGE this means we have not yet converted this over to the + // new data structure. So we simply set this message as inner html and render it like we did before. + // This wil allow us to convert messages over to the new structure without needing to do it all at once. + case 'OLD_MESSAGE': + return OLD_MESSAGE; + default: + return fragment.text; + } +} + +ReportActionItemFragment.displayName = 'ReportActionItemFragment'; + +export default memo(ReportActionItemFragment); diff --git a/src/pages/home/report/ReportActionItemMessage.tsx b/src/pages/home/report/ReportActionItemMessage.tsx index 6fc70c78e605..d68fbf889f29 100644 --- a/src/pages/home/report/ReportActionItemMessage.tsx +++ b/src/pages/home/report/ReportActionItemMessage.tsx @@ -1,6 +1,6 @@ import type {ReactElement} from 'react'; import React from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; +import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import {View} from 'react-native'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; @@ -9,7 +9,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; -import type {OriginalMessageAddComment} from '@src/types/onyx/OriginalMessage'; +import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; import TextCommentFragment from './comment/TextCommentFragment'; import ReportActionItemFragment from './ReportActionItemFragment'; @@ -21,7 +21,7 @@ type ReportActionItemMessageProps = { displayAsGroup: boolean; /** Additional styles to add after local styles. */ - style?: StyleProp; + style?: StyleProp; /** Whether or not the message is hidden by moderation */ isHidden?: boolean; @@ -77,10 +77,9 @@ function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHid fragment={fragment} iouMessage={iouMessage} isThreadParentMessage={ReportActionsUtils.isThreadParentMessage(action, reportID)} - attachmentInfo={action.attachmentInfo} pendingAction={action.pendingAction} - source={(action.originalMessage as OriginalMessageAddComment['originalMessage'])?.source} - accountID={action.actorAccountID} + source={action.originalMessage as OriginalMessageSource} + accountID={action.actorAccountID ?? 0} style={style} displayAsGroup={displayAsGroup} isApprovedOrSubmittedReportAction={isApprovedOrSubmittedReportAction} diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 6c4e2b39a329..ae5c3d75cfff 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -155,7 +155,7 @@ function ReportActionItemSingle({ Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(iouReportID)); return; } - showUserDetails(action.delegateAccountID ? action.delegateAccountID : String(actorAccountID)); + showUserDetails(action.delegateAccountID ? String(action.delegateAccountID) : String(actorAccountID)); } }, [isWorkspaceActor, reportID, actorAccountID, action.delegateAccountID, iouReportID, displayAllActors]); @@ -238,8 +238,8 @@ function ReportActionItemSingle({ @@ -28,7 +22,6 @@ function AttachmentCommentFragment({addExtraMargin, html, source}) { ); } -AttachmentCommentFragment.propTypes = propTypes; AttachmentCommentFragment.displayName = 'AttachmentCommentFragment'; export default AttachmentCommentFragment; diff --git a/src/pages/home/report/comment/RenderCommentHTML.js b/src/pages/home/report/comment/RenderCommentHTML.js deleted file mode 100644 index 14039af21189..000000000000 --- a/src/pages/home/report/comment/RenderCommentHTML.js +++ /dev/null @@ -1,23 +0,0 @@ -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' ? `${html}` : `${html}`; - - return ; -} - -RenderCommentHTML.propTypes = propTypes; -RenderCommentHTML.displayName = 'RenderCommentHTML'; - -export default RenderCommentHTML; diff --git a/src/pages/home/report/comment/RenderCommentHTML.tsx b/src/pages/home/report/comment/RenderCommentHTML.tsx new file mode 100644 index 000000000000..e730ae061519 --- /dev/null +++ b/src/pages/home/report/comment/RenderCommentHTML.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import RenderHTML from '@components/RenderHTML'; +import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; + +type RenderCommentHTMLProps = { + source: OriginalMessageSource; + html: string; +}; + +function RenderCommentHTML({html, source}: RenderCommentHTMLProps) { + const commentHtml = source === 'email' ? `${html}` : `${html}`; + + return ; +} + +RenderCommentHTML.displayName = 'RenderCommentHTML'; + +export default RenderCommentHTML; diff --git a/src/pages/home/report/comment/TextCommentFragment.js b/src/pages/home/report/comment/TextCommentFragment.tsx similarity index 59% rename from src/pages/home/report/comment/TextCommentFragment.js rename to src/pages/home/report/comment/TextCommentFragment.tsx index 3d6482344450..998ed9f6616f 100644 --- a/src/pages/home/report/comment/TextCommentFragment.js +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -1,56 +1,48 @@ import Str from 'expensify-common/lib/str'; -import PropTypes from 'prop-types'; +import {isEmpty} from 'lodash'; import React, {memo} from 'react'; +import type {StyleProp, TextStyle} from 'react-native'; import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import ZeroWidthView from '@components/ZeroWidthView'; +import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; +import useWindowDimensions from '@hooks/useWindowDimensions'; 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 variables from '@styles/variables'; import CONST from '@src/CONST'; +import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; +import type {Message} from '@src/types/onyx/ReportAction'; import RenderCommentHTML from './RenderCommentHTML'; -const propTypes = { +type TextCommentFragmentProps = { /** The reportAction's source */ - source: reportActionSourcePropType.isRequired, + source: OriginalMessageSource; /** The message fragment needing to be displayed */ - fragment: reportActionFragmentPropTypes.isRequired, + fragment: Message; /** Should this message fragment be styled as deleted? */ - styleAsDeleted: PropTypes.bool.isRequired, - - /** Text of an IOU report action */ - iouMessage: PropTypes.string, + styleAsDeleted: boolean; /** Should the comment have the appearance of being grouped with the previous comment? */ - displayAsGroup: PropTypes.bool.isRequired, + displayAsGroup: boolean; /** Additional styles to add after local styles. */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]).isRequired, - - ...windowDimensionsPropTypes, + style: StyleProp; - /** localization props */ - ...withLocalizePropTypes, -}; - -const defaultProps = { - iouMessage: undefined, + /** Text of an IOU report action */ + iouMessage?: string; }; -function TextCommentFragment(props) { +function TextCommentFragment({fragment, styleAsDeleted, source, style, displayAsGroup, iouMessage = ''}: TextCommentFragmentProps) { const theme = useTheme(); const styles = useThemeStyles(); - const {fragment, styleAsDeleted} = props; - const {html, text} = fragment; + const {html = '', text} = fragment; + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); // If the only difference between fragment.text and fragment.html is
tags // we render it as text, not as html. @@ -66,35 +58,36 @@ function TextCommentFragment(props) { return ( ); } const containsOnlyEmojis = EmojiUtils.containsOnlyEmojis(text); + const message = isEmpty(iouMessage) ? text : iouMessage; return ( - + - {convertToLTR(props.iouMessage || text)} + {convertToLTR(message)} - {Boolean(fragment.isEdited) && ( + {fragment.isEdited && ( <> {' '} @@ -102,9 +95,9 @@ function TextCommentFragment(props) { - {props.translate('reportActionCompose.edited')} + {translate('reportActionCompose.edited')} )} @@ -112,8 +105,6 @@ function TextCommentFragment(props) { ); } -TextCommentFragment.propTypes = propTypes; -TextCommentFragment.defaultProps = defaultProps; TextCommentFragment.displayName = 'TextCommentFragment'; -export default compose(withWindowDimensions, withLocalize)(memo(TextCommentFragment)); +export default memo(TextCommentFragment); diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index f00fd8c4c972..19c62f692c84 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -282,5 +282,6 @@ export type { OriginalMessageIOU, OriginalMessageCreated, OriginalMessageAddComment, + OriginalMessageSource, OriginalMessageReimbursementDequeued, }; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 3dbd4c1e3667..d2f0afad5b7a 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -175,7 +175,7 @@ type ReportActionBase = { /** Is this action pending? */ pendingAction?: OnyxCommon.PendingAction; - delegateAccountID?: string; + delegateAccountID?: number; /** Server side errors keyed by microtime */ errors?: OnyxCommon.Errors; diff --git a/tests/utils/collections/reportActions.ts b/tests/utils/collections/reportActions.ts index 747cbe5b6a1a..cc258e89c041 100644 --- a/tests/utils/collections/reportActions.ts +++ b/tests/utils/collections/reportActions.ts @@ -65,7 +65,7 @@ export default function createRandomReportAction(index: number): ReportAction { shouldShow: randBoolean(), lastModified: randPastDate().toISOString(), pendingAction: rand(Object.values(CONST.RED_BRICK_ROAD_PENDING_ACTION)), - delegateAccountID: index.toString(), + delegateAccountID: index, errors: {}, isAttachment: randBoolean(), };