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 { >