diff --git a/src/ROUTES.ts b/src/ROUTES.ts index af34b8b7e9ea..a49df16b570a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -204,7 +204,7 @@ const ROUTES = { }, REPORT_ATTACHMENTS: { route: 'r/:reportID/attachment', - getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURI(source)}` as const, + getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURIComponent(source)}` as const, }, REPORT_PARTICIPANTS: { route: 'r/:reportID/participants', diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js index b934bdfdd738..9524c5203110 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js +++ b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js @@ -15,10 +15,19 @@ import CONST from '@src/CONST'; function extractAttachmentsFromReport(parentReportAction, reportActions) { const actions = [parentReportAction, ...ReportActionsUtils.getSortedReportActions(_.values(reportActions))]; const attachments = []; + // 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 uniqueSources = new Set(); const htmlParser = new HtmlParser({ onopentag: (name, attribs) => { if (name === 'video') { + const source = tryResolveUrlFromApiRoot(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]); + if (uniqueSources.has(source)) { + return; + } + + uniqueSources.add(source); const splittedUrl = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE].split('/'); attachments.unshift({ reportActionID: null, @@ -35,7 +44,20 @@ function extractAttachmentsFromReport(parentReportAction, reportActions) { if (name === 'img' && attribs.src) { const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]; const source = tryResolveUrlFromApiRoot(expensifySource || attribs.src); - const fileName = attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || FileUtils.getFileName(`${source}`); + if (uniqueSources.has(source)) { + return; + } + + uniqueSources.add(source); + let fileName = attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || FileUtils.getFileName(`${source}`); + + // Public image URLs might lack a file extension in the source URL, without an extension our + // AttachmentView fails to recognize them as images and renders fallback content instead. + // We apply this small hack to add an image extension and ensure AttachmentView renders the image. + const fileInfo = FileUtils.splitExtensionFromFileName(fileName); + if (!fileInfo.fileExtension) { + fileName = `${fileInfo.fileName || 'image'}.jpg`; + } // By iterating actions in chronological order and prepending each attachment // we ensure correct order of attachments even across actions with multiple attachments. diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index 461548f0d2b1..9fe37734e8ee 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -79,6 +79,7 @@ const defaultProps = { reportActionID: '', isHovered: false, optionalVideoDuration: 0, + fallbackSource: Expensicons.Gallery, }; function AttachmentView({ @@ -201,6 +202,21 @@ function AttachmentView({ // We also check for numeric source since this is how static images (used for preview) are represented in RN. const isImage = typeof source === 'number' || Str.isImage(source); if (isImage || (file && Str.isImage(file.name))) { + if (imageError) { + // AttachmentViewImage can't handle icon fallbacks, so we need to handle it here + if (typeof fallbackSource === 'number' || _.isFunction(fallbackSource)) { + return ( + + ); + } + } + return ( {