diff --git a/src/CONST.ts b/src/CONST.ts index 6de65ae74f63f..e23a8a6a4f49b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1784,6 +1784,7 @@ const CONST = { ATTACHMENT_MESSAGE_TEXT: '[Attachment]', ATTACHMENT_SOURCE_ATTRIBUTE: 'data-expensify-source', + ATTACHMENT_ID_ATTRIBUTE: 'data-attachment-id', ATTACHMENT_OPTIMISTIC_SOURCE_ATTRIBUTE: 'data-optimistic-src', ATTACHMENT_PREVIEW_ATTRIBUTE: 'src', ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE: 'data-name', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 4fbb5b9a3819b..ad18e60ddfd5c 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -403,6 +403,7 @@ const ROUTES = { route: 'attachment', getRoute: ( reportID: string | undefined, + attachmentID: string | undefined, type: ValueOf, url: string, accountID?: number, @@ -415,8 +416,11 @@ const ROUTES = { const authTokenParam = isAuthTokenRequired ? '&isAuthTokenRequired=true' : ''; const fileNameParam = fileName ? `&fileName=${fileName}` : ''; const attachmentLinkParam = attachmentLink ? `&attachmentLink=${attachmentLink}` : ''; + const attachmentIDParam = attachmentID ? `&attachmentID=${attachmentID}` : ''; - return `attachment?source=${encodeURIComponent(url)}&type=${type as string}${reportParam}${accountParam}${authTokenParam}${fileNameParam}${attachmentLinkParam}` as const; + return `attachment?source=${encodeURIComponent(url)}&type=${ + type as string + }${reportParam}${attachmentIDParam}${accountParam}${authTokenParam}${fileNameParam}${attachmentLinkParam}` as const; }, }, REPORT_PARTICIPANTS: { diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index f76ade0db8796..5dad6b47e3bfe 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -69,6 +69,9 @@ type AttachmentModalProps = { /** Optional source (URL, SVG function) for the image shown. If not passed in via props must be specified when modal is opened. */ source?: AvatarSource; + /** The id of the attachment. */ + attachmentID?: string; + /** Optional callback to fire when we want to preview an image and approve it for use. */ onConfirm?: ((file: FileObject) => void) | null; @@ -164,6 +167,7 @@ function AttachmentModal({ isLoading = false, shouldShowNotFoundPage = false, type = undefined, + attachmentID, accountID = undefined, shouldDisableSendButton = false, attachmentLink = '', @@ -558,6 +562,7 @@ function AttachmentModal({ , ) { const {handleTap, handleScaleChange, isScrollEnabled} = useCarouselContextEvents(setShouldShowArrows); @@ -88,7 +88,7 @@ function AttachmentCarouselPager( [activePageIndex, items], ); - const extractItemKey = useCallback((item: Attachment, index: number) => `reportActionID-${item.reportActionID}-${index}`, []); + const extractItemKey = useCallback((item: Attachment, index: number) => `attachmentID-${item.attachmentID}-${index}`, []); const contextValue = useMemo( () => ({ @@ -129,7 +129,7 @@ function AttachmentCarouselPager( > diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 1d4f8828400c5..20565c065f334 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -3,7 +3,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {Attachment} from '@components/Attachments/types'; import {getFileName, splitExtensionFromFileName} from '@libs/fileDownload/FileUtils'; -import {getReportActionHtml, getReportActionMessage, getSortedReportActions, isMoneyRequestAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; +import {getHtmlWithAttachmentID, getReportActionHtml, getReportActionMessage, getSortedReportActions, isMoneyRequestAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction} from '@libs/ReportUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import CONST from '@src/CONST'; @@ -27,11 +27,6 @@ function extractAttachments( const description = report?.description ?? ''; const attachments: Attachment[] = []; const canUserPerformAction = canUserPerformWriteAction(report); - - // We handle duplicate image sources by considering the first instance as original. Selecting any duplicate - // and navigating back (<) shows the image preceding the first instance, not the selected duplicate's position. - const uniqueSourcesAndLinks = new Set(); - let currentLink = ''; const htmlParser = new HtmlParser({ @@ -41,13 +36,10 @@ function extractAttachments( } if (name === 'video') { const source = tryResolveUrlFromApiRoot(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]); - if (uniqueSourcesAndLinks.has(source)) { - return; - } - uniqueSourcesAndLinks.add(source); const fileName = attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || getFileName(`${source}`); attachments.unshift({ + attachmentID: attribs[CONST.ATTACHMENT_ID_ATTRIBUTE], source: tryResolveUrlFromApiRoot(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]), isAuthTokenRequired: !!attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE], file: {name: fileName}, @@ -62,13 +54,6 @@ function extractAttachments( const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE] ?? (new RegExp(CONST.ATTACHMENT_OR_RECEIPT_LOCAL_URL, 'i').test(attribs.src) ? attribs.src : null); const source = tryResolveUrlFromApiRoot(expensifySource || attribs.src); const previewSource = tryResolveUrlFromApiRoot(attribs.src); - const sourceLinkKey = `${source}|${currentLink}`; - - if (uniqueSourcesAndLinks.has(sourceLinkKey)) { - return; - } - - uniqueSourcesAndLinks.add(sourceLinkKey); let fileName = attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || getFileName(`${source}`); @@ -87,6 +72,7 @@ function extractAttachments( // we ensure correct order of attachments even across actions with multiple attachments. attachments.unshift({ reportActionID: attribs['data-id'], + attachmentID: attribs[CONST.ATTACHMENT_ID_ATTRIBUTE], source, previewSource, isAuthTokenRequired: !!expensifySource, @@ -128,8 +114,8 @@ function extractAttachments( const decision = getReportActionMessage(action)?.moderationDecision?.decision; const hasBeenFlagged = decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN; - const html = getReportActionHtml(action).replace('/>', `data-flagged="${hasBeenFlagged}" data-id="${action.reportActionID}"/>`); - htmlParser.write(html); + const html = getReportActionHtml(action).replaceAll('/>', `data-flagged="${hasBeenFlagged}" data-id="${action.reportActionID}"/>`); + htmlParser.write(getHtmlWithAttachmentID(html, action.reportActionID)); }); htmlParser.end(); diff --git a/src/components/Attachments/AttachmentCarousel/index.native.tsx b/src/components/Attachments/AttachmentCarousel/index.native.tsx index 0cfa95c8393d5..4d81fda91494a 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.native.tsx @@ -18,7 +18,7 @@ import AttachmentCarouselPager from './Pager'; import type {AttachmentCarouselProps} from './types'; import useCarouselArrows from './useCarouselArrows'; -function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibility, onClose, type, accountID}: AttachmentCarouselProps) { +function AttachmentCarousel({report, source, attachmentID, onNavigate, setDownloadButtonVisibility, onClose, type, accountID}: AttachmentCarouselProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const pagerRef = useRef(null); @@ -27,8 +27,8 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi const [page, setPage] = useState(); const [attachments, setAttachments] = useState([]); const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows(); - const [activeSource, setActiveSource] = useState(source); - const compareImage = useCallback((attachment: Attachment) => attachment.source === source, [source]); + const [activeAttachmentID, setActiveAttachmentID] = useState(attachmentID ?? source); + const compareImage = useCallback((attachment: Attachment) => (attachmentID ? attachment.attachmentID === attachmentID : attachment.source === source), [attachmentID, source]); useEffect(() => { const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined; @@ -82,7 +82,7 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi setPage(newPageIndex); if (newPageIndex >= 0 && item) { - setActiveSource(item.source); + setActiveAttachmentID(item.attachmentID ?? item.source); if (onNavigate) { onNavigate(item); } @@ -144,7 +144,7 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi updatePage(newPage)} onClose={onClose} diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index b38a45a1bf63a..b3ec149db908b 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -9,7 +9,7 @@ import {useOnyx} from 'react-native-onyx'; import Animated, {scrollTo, useAnimatedRef, useSharedValue} from 'react-native-reanimated'; import type {Attachment, AttachmentSource} from '@components/Attachments/types'; import BlockingView from '@components/BlockingViews/BlockingView'; -import * as Illustrations from '@components/Icon/Illustrations'; +import {ToddBehindCloud} from '@components/Icon/Illustrations'; import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenContext'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -51,7 +51,7 @@ function DeviceAwareGestureDetector({canUseTouchScreen, gesture, children}: Devi return canUseTouchScreen ? {children} : children; } -function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibility, type, accountID, onClose, attachmentLink}: AttachmentCarouselProps) { +function AttachmentCarousel({report, attachmentID, source, onNavigate, setDownloadButtonVisibility, type, accountID, onClose, attachmentLink}: AttachmentCarouselProps) { const theme = useTheme(); const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); @@ -72,7 +72,7 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi ); const [page, setPage] = useState(0); const [attachments, setAttachments] = useState([]); - const [activeSource, setActiveSource] = useState(source); + const [activeAttachmentID, setActiveAttachmentID] = useState(attachmentID ?? source); const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows(); const {handleTap, handleScaleChange, isScrollEnabled} = useCarouselContextEvents(setShouldShowArrows); @@ -83,7 +83,11 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi setShouldShowArrows(true); }, [canUseTouchScreen, page, setShouldShowArrows]); - const compareImage = useCallback((attachment: Attachment) => attachment.source === source && (!attachmentLink || attachment.attachmentLink === attachmentLink), [attachmentLink, source]); + const compareImage = useCallback( + (attachment: Attachment) => + (attachmentID ? attachment.attachmentID === attachmentID : attachment.source === source) && (!attachmentLink || attachment.attachmentLink === attachmentLink), + [attachmentLink, attachmentID, source], + ); useEffect(() => { const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined; @@ -158,14 +162,14 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi // to get the index of the current page const entry = viewableItems.at(0); if (!entry) { - setActiveSource(null); + setActiveAttachmentID(null); return; } const item = entry.item as Attachment; if (entry.index !== null) { setPage(entry.index); - setActiveSource(item.source); + setActiveAttachmentID(item.attachmentID ?? item.source); } if (onNavigate) { @@ -195,7 +199,10 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi ); const extractItemKey = useCallback( - (item: Attachment) => (typeof item.source === 'string' || typeof item.source === 'number' ? `source-${item.source}|${item.attachmentLink}` : `reportActionID-${item.reportActionID}`), + (item: Attachment) => + !!item.attachmentID || (typeof item.source !== 'string' && typeof item.source !== 'number') + ? `attachmentID-${item.attachmentID}` + : `source-${item.source}|${item.attachmentLink}`, [], ); @@ -229,14 +236,14 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi ), - [activeSource, canUseTouchScreen, cellWidth, handleTap, report.reportID, shouldShowArrows, styles.h100], + [activeAttachmentID, canUseTouchScreen, cellWidth, handleTap, report.reportID, shouldShowArrows, styles.h100], ); /** Pan gesture handing swiping through attachments on touch screen devices */ const pan = useMemo( @@ -288,7 +295,7 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi > {page === -1 ? ( void; diff --git a/src/components/Attachments/types.ts b/src/components/Attachments/types.ts index 88b8420745eb6..f394799dfce40 100644 --- a/src/components/Attachments/types.ts +++ b/src/components/Attachments/types.ts @@ -7,6 +7,9 @@ type Attachment = { /** Report action ID of the attachment */ reportActionID?: string; + /** The attachment id, which is the concatenation of the report action id it is in and its order index within that report action. */ + attachmentID?: string; + /** Whether source url requires authentication */ isAuthTokenRequired?: boolean; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx index b1d5ac74540f0..4d89c7f9b83e8 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx @@ -5,7 +5,7 @@ import type {CustomRendererProps, TBlock} from 'react-native-render-html'; import {AttachmentContext} from '@components/AttachmentContext'; import {getButtonRole} from '@components/Button/utils'; import {isDeletedNode} from '@components/HTMLEngineProvider/htmlEngineUtils'; -import * as Expensicons from '@components/Icon/Expensicons'; +import {Document, GalleryNotFound} from '@components/Icon/Expensicons'; import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus'; import {ShowContextMenuContext, showContextMenuForReport} from '@components/ShowContextMenuContext'; import ThumbnailImage from '@components/ThumbnailImage'; @@ -57,6 +57,7 @@ function ImageRenderer({tnode}: ImageRendererProps) { const attachmentSourceAttribute = htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE] ?? (new RegExp(CONST.ATTACHMENT_OR_RECEIPT_LOCAL_URL, 'i').test(htmlAttribs.src) ? htmlAttribs.src : null); const isAttachmentOrReceipt = !!attachmentSourceAttribute; + const attachmentID = htmlAttribs[CONST.ATTACHMENT_ID_ATTRIBUTE]; // Files created/uploaded/hosted by App should resolve from API ROOT. Other URLs aren't modified const previewSource = tryResolveUrlFromApiRoot(htmlAttribs.src); @@ -72,7 +73,7 @@ function ImageRenderer({tnode}: ImageRendererProps) { const imagePreviewModalDisabled = htmlAttribs['data-expensify-preview-modal-disabled'] === 'true'; const fileType = getFileType(attachmentSourceAttribute); - const fallbackIcon = fileType === CONST.ATTACHMENT_FILE_TYPE.FILE ? Expensicons.Document : Expensicons.GalleryNotFound; + const fallbackIcon = fileType === CONST.ATTACHMENT_FILE_TYPE.FILE ? Document : GalleryNotFound; const theme = useTheme(); let fileName = htmlAttribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || getFileName(`${isAttachmentOrReceipt ? attachmentSourceAttribute : htmlAttribs.src}`); @@ -111,7 +112,7 @@ function ImageRenderer({tnode}: ImageRendererProps) { } const attachmentLink = tnode.parent?.attributes?.href; - const route = ROUTES.ATTACHMENTS?.getRoute(reportID, type, source, accountID, isAttachmentOrReceipt, fileName, attachmentLink); + const route = ROUTES.ATTACHMENTS?.getRoute(reportID, attachmentID, type, source, accountID, isAttachmentOrReceipt, fileName, attachmentLink); Navigation.navigate(route); }} onLongPress={(event) => { diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx index 4f37cdea52377..9fcd647a07e78 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VideoRenderer.tsx @@ -25,6 +25,7 @@ function VideoRenderer({tnode, key}: VideoRendererProps) { const height = Number(htmlAttribs[CONST.ATTACHMENT_THUMBNAIL_HEIGHT_ATTRIBUTE]); const duration = Number(htmlAttribs[CONST.ATTACHMENT_DURATION_ATTRIBUTE]); const isDeleted = isDeletedNode(tnode); + const attachmentID = htmlAttribs[CONST.ATTACHMENT_ID_ATTRIBUTE]; return ( @@ -45,7 +46,7 @@ function VideoRenderer({tnode, key}: VideoRendererProps) { return; } const isAuthTokenRequired = !!htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]; - const route = ROUTES.ATTACHMENTS.getRoute(report?.reportID, type, sourceURL, accountID, isAuthTokenRequired); + const route = ROUTES.ATTACHMENTS.getRoute(report?.reportID, attachmentID, type, sourceURL, accountID, isAuthTokenRequired); Navigation.navigate(route); }} /> diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index a1594e87c5c9a..39dcb374b7234 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1825,6 +1825,7 @@ type AuthScreensParamList = SharedScreensParamList & { [SCREENS.SUBMIT_EXPENSE]: undefined; [SCREENS.ATTACHMENTS]: { reportID: string; + attachmentID?: string; source: string; type: ValueOf; accountID: string; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index c05a16f2c992d..cbd08d973c735 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -162,6 +162,20 @@ function isDeletedAction(reportAction: OnyxInputOrEntry m.concat(`${CONST.ATTACHMENT_ID_ATTRIBUTE}="${reportActionID}_${++attachmentID}" `)); +} + function getReportActionMessage(reportAction: PartialReportAction) { return Array.isArray(reportAction?.message) ? reportAction.message.at(0) : reportAction?.message; } @@ -2287,6 +2301,7 @@ export { extractLinksFromMessageHtml, formatLastMessageText, isReportActionUnread, + getHtmlWithAttachmentID, getActionableMentionWhisperMessage, getAllReportActions, getCombinedReportActions, diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index 50721a5ffcad0..25058e16400ee 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -20,6 +20,9 @@ type ReportActionItemFragmentProps = { /** Users accountID */ accountID: number; + /** The report action's id */ + reportActionID?: string; + /** The message fragment needing to be displayed */ fragment: Message | undefined; @@ -76,6 +79,7 @@ const MUTED_ACTIONS = [ ] as ReportActionName[]; function ReportActionItemFragment({ + reportActionID, pendingAction, actionName, fragment, @@ -116,6 +120,7 @@ function ReportActionItemFragment({ if (isReportMessageAttachment(fragment)) { return ( { const routeToNavigate = ROUTES.ATTACHMENTS.getRoute( reportID, + attachment.attachmentID, type, String(attachment.source), Number(accountID), @@ -49,6 +51,7 @@ function ReportAttachments({route}: ReportAttachmentsProps) { allowDownload defaultOpen report={report} + attachmentID={attachmentID} source={source} onModalClose={() => { Navigation.dismissModal(); diff --git a/src/pages/home/report/comment/AttachmentCommentFragment.tsx b/src/pages/home/report/comment/AttachmentCommentFragment.tsx index 9f70f03f352b0..d7d0e5c2f3ac3 100644 --- a/src/pages/home/report/comment/AttachmentCommentFragment.tsx +++ b/src/pages/home/report/comment/AttachmentCommentFragment.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; +import {getHtmlWithAttachmentID} from '@libs/ReportActionsUtils'; import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; import RenderCommentHTML from './RenderCommentHTML'; @@ -8,12 +9,13 @@ type AttachmentCommentFragmentProps = { source: OriginalMessageSource; html: string; addExtraMargin: boolean; + reportActionID?: string; styleAsDeleted: boolean; }; -function AttachmentCommentFragment({addExtraMargin, html, source, styleAsDeleted}: AttachmentCommentFragmentProps) { +function AttachmentCommentFragment({addExtraMargin, html, source, styleAsDeleted, reportActionID}: AttachmentCommentFragmentProps) { const styles = useThemeStyles(); - const htmlContent = styleAsDeleted ? `${html}` : html; + const htmlContent = getHtmlWithAttachmentID(styleAsDeleted ? `${html}` : html, reportActionID); return ( diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index f8ea9b56871f1..7c5594e985ba2 100644 --- a/src/pages/home/report/comment/TextCommentFragment.tsx +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -9,10 +9,10 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import convertToLTR from '@libs/convertToLTR'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as EmojiUtils from '@libs/EmojiUtils'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; +import {containsOnlyEmojis as containsOnlyEmojisUtil, splitTextWithEmojis} from '@libs/EmojiUtils'; import Performance from '@libs/Performance'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import {getHtmlWithAttachmentID, getTextFromHtml} from '@libs/ReportActionsUtils'; import variables from '@styles/variables'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; @@ -26,6 +26,9 @@ type TextCommentFragmentProps = { /** The reportAction's source */ source: OriginalMessageSource; + /** The report action's id */ + reportActionID?: string; + /** The message fragment needing to be displayed */ fragment: Message | undefined; @@ -45,17 +48,17 @@ type TextCommentFragmentProps = { iouMessage?: string; }; -function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, source, style, displayAsGroup, iouMessage = ''}: TextCommentFragmentProps) { +function TextCommentFragment({fragment, styleAsDeleted, reportActionID, styleAsMuted = false, source, style, displayAsGroup, iouMessage = ''}: TextCommentFragmentProps) { const theme = useTheme(); const styles = useThemeStyles(); const {html = ''} = fragment ?? {}; - const text = ReportActionsUtils.getTextFromHtml(html); + const text = getTextFromHtml(html); const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const message = isEmpty(iouMessage) ? text : iouMessage; - const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(message), [message]); + const processedTextArray = useMemo(() => splitTextWithEmojis(message), [message]); useEffect(() => { Performance.markEnd(CONST.TIMING.SEND_MESSAGE, {message: text}); @@ -65,7 +68,7 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so // If the only difference between fragment.text and fragment.html is
tags and emoji tag // on native, we render it as text, not as html // on other device, only render it as text if the only difference is
tag - const containsOnlyEmojis = EmojiUtils.containsOnlyEmojis(text ?? ''); + const containsOnlyEmojis = containsOnlyEmojisUtil(text ?? ''); const containsEmojis = CONST.REGEX.ALL_EMOJIS.test(text ?? ''); if (!shouldRenderAsText(html, text ?? '') && !(containsOnlyEmojis && styleAsDeleted)) { const editedTag = fragment?.isEdited ? `` : ''; @@ -84,6 +87,8 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so htmlWithTag = `${htmlWithTag}`; } + htmlWithTag = getHtmlWithAttachmentID(htmlWithTag, reportActionID); + return ( ) : ( @@ -118,7 +123,7 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so style, styleAsDeleted ? styles.offlineFeedback.deleted : undefined, styleAsMuted ? styles.colorMuted : undefined, - !DeviceCapabilities.canUseTouchScreen() || !shouldUseNarrowLayout ? styles.userSelectText : styles.userSelectNone, + !canUseTouchScreen() || !shouldUseNarrowLayout ? styles.userSelectText : styles.userSelectNone, ]} > {convertToLTR(message ?? '')}