diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
index 56f592bc5865..6ae3214cd32a 100644
--- a/src/components/AttachmentCarousel/index.js
+++ b/src/components/AttachmentCarousel/index.js
@@ -3,6 +3,7 @@ import {View, FlatList, PixelRatio} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
+import {Parser as HtmlParser} from 'htmlparser2';
import * as Expensicons from '../Icon/Expensicons';
import styles from '../../styles/styles';
import themeColors from '../../styles/themes/default';
@@ -10,7 +11,6 @@ import CarouselActions from './CarouselActions';
import Button from '../Button';
import * as ReportActionsUtils from '../../libs/ReportActionsUtils';
import AttachmentView from '../AttachmentView';
-import addEncryptedAuthTokenToURL from '../../libs/addEncryptedAuthTokenToURL';
import * as DeviceCapabilities from '../../libs/DeviceCapabilities';
import CONST from '../../CONST';
import ONYXKEYS from '../../ONYXKEYS';
@@ -62,28 +62,13 @@ class AttachmentCarousel extends React.Component {
this.updateZoomState = this.updateZoomState.bind(this);
this.toggleArrowsVisibility = this.toggleArrowsVisibility.bind(this);
- this.state = this.makeInitialState();
+ this.state = this.createInitialState();
}
componentDidMount() {
this.autoHideArrow();
}
- /**
- * Helps to navigate between next/previous attachments
- * @param {Object} attachmentItem
- * @returns {Object}
- */
- getAttachment(attachmentItem) {
- const source = _.get(attachmentItem, 'source', '');
- const file = _.get(attachmentItem, 'file', {name: ''});
-
- return {
- source,
- file,
- };
- }
-
/**
* Calculate items layout information to optimize scrolling performance
* @param {*} data
@@ -159,39 +144,39 @@ class AttachmentCarousel extends React.Component {
}
/**
- * Map report actions to attachment items and sets the initial carousel state
+ * Constructs the initial component state from report actions
* @returns {{page: Number, attachments: Array, shouldShowArrow: Boolean, containerWidth: Number, isZoomed: Boolean}}
*/
- makeInitialState() {
- let page = 0;
- const actions = ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions), true);
-
- /**
- * Looping to filter out attachments and retrieve the src URL and name of attachments.
- */
+ createInitialState() {
+ const actions = ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions));
const attachments = [];
- _.forEach(actions, ({originalMessage, message}) => {
- // Check for attachment which hasn't been deleted
- if (!originalMessage || !originalMessage.html || _.some(message, (m) => m.isEdited)) {
- return;
- }
- const matches = [...originalMessage.html.matchAll(CONST.REGEX.ATTACHMENT_DATA)];
-
- // matchAll captured both source url and name of the attachment
- if (matches.length === 2) {
- const [originalSource, name] = _.map(matches, (m) => m[2]);
-
- // Update the image URL so the images can be accessed depending on the config environment.
- // Eg: while using Ngrok the image path is from an Ngrok URL and not an Expensify URL.
- const source = tryResolveUrlFromApiRoot(originalSource);
- if (source === this.props.source) {
- page = attachments.length;
+
+ const htmlParser = new HtmlParser({
+ onopentag: (name, attribs) => {
+ if (name !== 'img' || !attribs.src) {
+ return;
}
- attachments.push({source, file: {name}});
- }
+ const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE];
+
+ // By iterating actions in chronological order and prepending each attachment
+ // we ensure correct order of attachments even across actions with multiple attachments.
+ attachments.unshift({
+ source: tryResolveUrlFromApiRoot(expensifySource || attribs.src),
+ isAuthTokenRequired: Boolean(expensifySource),
+ file: {name: attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || attribs.src.split('/').pop()},
+ });
+ },
});
+ _.forEach(actions, (action) => htmlParser.write(_.get(action, ['message', 0, 'html'])));
+ htmlParser.end();
+
+ const page = _.findIndex(attachments, (a) => a.source === this.props.source);
+ if (page === -1) {
+ throw new Error('Attachment not found');
+ }
+
return {
page,
attachments,
@@ -220,7 +205,7 @@ class AttachmentCarousel extends React.Component {
/**
* Updates the page state when the user navigates between attachments
- * @param {Array<{item: *, index: Number}>} viewableItems
+ * @param {Array<{item: {source, file}, index: Number}>} viewableItems
*/
updatePage({viewableItems}) {
// Since we can have only one item in view at a time, we can use the first item in the array
@@ -231,8 +216,7 @@ class AttachmentCarousel extends React.Component {
}
const page = entry.index;
- const {source, file} = this.getAttachment(entry.item);
- this.props.onNavigate({source: addEncryptedAuthTokenToURL(source), file});
+ this.props.onNavigate(entry.item);
this.setState({page, isZoomed: false});
}
@@ -258,26 +242,17 @@ class AttachmentCarousel extends React.Component {
/**
* Defines how a single attachment should be rendered
- * @param {{ source: String, file: { name: String } }} item
+ * @param {{ isAuthTokenRequired: Boolean, source: String, file: { name: String } }} item
* @returns {JSX.Element}
*/
renderItem({item}) {
- const authSource = addEncryptedAuthTokenToURL(item.source);
- if (!this.canUseTouchScreen) {
- return (
-
- );
- }
-
return (
);
}
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index 642004913303..24a6ecfb3152 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -22,6 +22,7 @@ import withLocalize, {withLocalizePropTypes} from './withLocalize';
import ConfirmModal from './ConfirmModal';
import HeaderGap from './HeaderGap';
import SafeAreaConsumer from './SafeAreaConsumer';
+import addEncryptedAuthTokenToURL from '../libs/addEncryptedAuthTokenToURL';
/**
* Modal render prop component that exposes modal launching triggers that can be used
@@ -84,6 +85,7 @@ class AttachmentModal extends PureComponent {
isModalOpen: false,
shouldLoadAttachment: false,
isAttachmentInvalid: false,
+ isAuthTokenRequired: props.isAuthTokenRequired,
attachmentInvalidReasonTitle: null,
attachmentInvalidReason: null,
source: props.source,
@@ -100,17 +102,17 @@ class AttachmentModal extends PureComponent {
this.submitAndClose = this.submitAndClose.bind(this);
this.closeConfirmModal = this.closeConfirmModal.bind(this);
this.onNavigate = this.onNavigate.bind(this);
+ this.downloadAttachment = this.downloadAttachment.bind(this);
this.validateAndDisplayFileToUpload = this.validateAndDisplayFileToUpload.bind(this);
this.updateConfirmButtonVisibility = this.updateConfirmButtonVisibility.bind(this);
}
/**
- * Helps to navigate between next/previous attachments
- * by setting sourceURL and file in state
- * @param {Object} attachmentData
+ * Keeps the attachment source in sync with the attachment displayed currently in the carousel.
+ * @param {{ source: String, isAuthTokenRequired: Boolean, file: { name: string } }} attachment
*/
- onNavigate(attachmentData) {
- this.setState(attachmentData);
+ onNavigate(attachment) {
+ this.setState(attachment);
}
/**
@@ -126,11 +128,15 @@ class AttachmentModal extends PureComponent {
}
/**
- * @param {String} sourceURL
+ * Download the currently viewed attachment.
*/
- downloadAttachment(sourceURL) {
- const originalFileName = lodashGet(this.state, 'file.name') || this.props.originalFileName;
- fileDownload(sourceURL, originalFileName);
+ downloadAttachment() {
+ let sourceURL = this.state.source;
+ if (this.state.isAuthTokenRequired) {
+ sourceURL = addEncryptedAuthTokenToURL(sourceURL);
+ }
+
+ fileDownload(sourceURL, this.state.file.name);
// At ios, if the keyboard is open while opening the attachment, then after downloading
// the attachment keyboard will show up. So, to fix it we need to dismiss the keyboard.
@@ -274,7 +280,7 @@ class AttachmentModal extends PureComponent {
title={this.props.headerTitle || this.props.translate('common.attachment')}
shouldShowBorderBottom
shouldShowDownloadButton={this.props.allowDownload}
- onDownloadButtonPress={() => this.downloadAttachment(this.state.source)}
+ onDownloadButtonPress={this.downloadAttachment}
onCloseButtonPress={() => this.setState({isModalOpen: false})}
/>
@@ -291,7 +297,7 @@ class AttachmentModal extends PureComponent {
diff --git a/src/components/ImageView/index.js b/src/components/ImageView/index.js
index f1ec25485991..7138f4087ed4 100644
--- a/src/components/ImageView/index.js
+++ b/src/components/ImageView/index.js
@@ -282,6 +282,7 @@ class ImageView extends PureComponent {
>