Skip to content

Commit

Permalink
Merge pull request #19724 from kidroca/kidroca/fix/attachment-carouse…
Browse files Browse the repository at this point in the history
…l-concierge-attachments

Fix: Attachment Carousel not displaying images from Concierge
  • Loading branch information
Hayata Suenaga authored May 31, 2023
2 parents 36fe848 + fbbeddb commit 9f9f2ce
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 71 deletions.
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

0 comments on commit 9f9f2ce

Please sign in to comment.