Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Attachment Carousel not displaying images from Concierge #19724

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 35 additions & 60 deletions src/components/AttachmentCarousel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ 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';
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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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});
}

Expand All @@ -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 (
<AttachmentView
source={authSource}
file={item.file}
/>
);
}

return (
<AttachmentView
source={authSource}
source={item.source}
file={item.file}
onScaleChanged={this.updateZoomState}
onPress={this.toggleArrowsVisibility}
isAuthTokenRequired={item.isAuthTokenRequired}
onScaleChanged={this.canUseTouchScreen ? this.updateZoomState : undefined}
onPress={this.canUseTouchScreen ? this.toggleArrowsVisibility : undefined}
/>
);
}
Expand Down
28 changes: 17 additions & 11 deletions src/components/AttachmentModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -84,6 +85,7 @@ class AttachmentModal extends PureComponent {
isModalOpen: false,
shouldLoadAttachment: false,
isAttachmentInvalid: false,
isAuthTokenRequired: props.isAuthTokenRequired,
attachmentInvalidReasonTitle: null,
attachmentInvalidReason: null,
source: props.source,
Expand All @@ -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);
}

/**
Expand All @@ -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.
Expand Down Expand Up @@ -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})}
/>
<View style={styles.imageModalImageCenterContainer}>
Expand All @@ -291,7 +297,7 @@ class AttachmentModal extends PureComponent {
<AttachmentView
containerStyles={[styles.mh5]}
source={source}
isAuthTokenRequired={this.props.isAuthTokenRequired}
isAuthTokenRequired={this.state.isAuthTokenRequired}
file={this.state.file}
onToggleKeyboard={this.updateConfirmButtonVisibility}
/>
Expand Down
1 change: 1 addition & 0 deletions src/components/ImageView/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ class ImageView extends PureComponent {
>
<Image
source={{uri: this.props.url}}
isAuthTokenRequired={this.props.isAuthTokenRequired}
style={[styles.h100, styles.w100]}
resizeMode={Image.resizeMode.contain}
onLoadStart={this.imageLoadingStart}
Expand Down