Skip to content

Commit

Permalink
Merge pull request #24235 from Expensify/andrewli-displayreceipts
Browse files Browse the repository at this point in the history
Display receipts in chat
  • Loading branch information
luacmartins authored Aug 21, 2023
2 parents 64ecc7a + de09a89 commit bfda5ed
Show file tree
Hide file tree
Showing 28 changed files with 724 additions and 188 deletions.
3 changes: 3 additions & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,9 @@ const CONST = {
AMOUNT_MAX_LENGTH: 10,
RECEIPT_STATE: {
SCANREADY: 'SCANREADY',
SCANNING: 'SCANNING',
SCANCOMPLETE: 'SCANCOMPLETE',
SCANFAILED: 'SCANFAILED',
},
FILE_TYPES: {
HTML: 'html',
Expand Down
2 changes: 1 addition & 1 deletion src/ONYXKEYS.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default {
// Credentials to authenticate the user
CREDENTIALS: 'credentials',

// Contains loading data for the IOU feature (MoneyRequestModal, IOUDetail, & IOUPreview Components)
// Contains loading data for the IOU feature (MoneyRequestModal, IOUDetail, & MoneyRequestPreview Components)
IOU: 'iou',

// Keeps track if there is modal currently visible or not
Expand Down
7 changes: 5 additions & 2 deletions src/components/AttachmentModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ function AttachmentModal(props) {
const [shouldLoadAttachment, setShouldLoadAttachment] = useState(false);
const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false);
const [isAuthTokenRequired, setIsAuthTokenRequired] = useState(props.isAuthTokenRequired);
const [isAttachmentReceipt, setIsAttachmentReceipt] = useState(false);
const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState('');
const [attachmentInvalidReason, setAttachmentInvalidReason] = useState(null);
const [source, setSource] = useState(props.source);
Expand All @@ -118,12 +119,13 @@ function AttachmentModal(props) {

/**
* Keeps the attachment source in sync with the attachment displayed currently in the carousel.
* @param {{ source: String, isAuthTokenRequired: Boolean, file: { name: string } }} attachment
* @param {{ source: String, isAuthTokenRequired: Boolean, file: { name: string }, isReceipt: Boolean }} attachment
*/
const onNavigate = useCallback(
(attachment) => {
setSource(attachment.source);
setFile(attachment.file);
setIsAttachmentReceipt(attachment.isReceipt);
setIsAuthTokenRequired(attachment.isAuthTokenRequired);
onCarouselAttachmentChange(attachment);
},
Expand Down Expand Up @@ -314,6 +316,7 @@ function AttachmentModal(props) {
}, []);

const sourceForAttachmentView = props.source || source;

return (
<>
<Modal
Expand All @@ -334,7 +337,7 @@ function AttachmentModal(props) {
>
{props.isSmallScreenWidth && <HeaderGap />}
<HeaderWithBackButton
title={props.headerTitle || translate('common.attachment')}
title={props.headerTitle || translate(isAttachmentReceipt ? 'common.receipt' : 'common.attachment')}
shouldShowBorderBottom
shouldShowDownloadButton={props.allowDownload && shouldShowDownloadButton}
onDownloadButtonPress={() => downloadAttachment(source)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import {Parser as HtmlParser} from 'htmlparser2';
import _ from 'underscore';
import lodashGet from 'lodash/get';
import * as ReportActionsUtils from '../../../libs/ReportActionsUtils';
import * as TransactionUtils from '../../../libs/TransactionUtils';
import * as ReceiptUtils from '../../../libs/ReceiptUtils';
import CONST from '../../../CONST';
import tryResolveUrlFromApiRoot from '../../../libs/tryResolveUrlFromApiRoot';

Expand Down Expand Up @@ -28,6 +31,7 @@ function extractAttachmentsFromReport(report, reportActions) {
source: tryResolveUrlFromApiRoot(expensifySource || attribs.src),
isAuthTokenRequired: Boolean(expensifySource),
file: {name: attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE]},
isReceipt: false,
});
},
});
Expand All @@ -36,6 +40,28 @@ function extractAttachmentsFromReport(report, reportActions) {
if (!ReportActionsUtils.shouldReportActionBeVisible(action, key)) {
return;
}

// We're handling receipts differently here because receipt images are not
// part of the report action message, the images are constructed client-side
if (ReportActionsUtils.isMoneyRequestAction(action)) {
const transactionID = lodashGet(action, ['originalMessage', 'IOUTransactionID']);
if (!transactionID) {
return;
}

const transaction = TransactionUtils.getTransaction(transactionID);
if (TransactionUtils.hasReceipt(transaction)) {
const {image} = ReceiptUtils.getThumbnailAndImageURIs(transaction.receipt.source, transaction.filename);
attachments.unshift({
source: tryResolveUrlFromApiRoot(image),
isAuthTokenRequired: true,
file: {name: transaction.filename},
isReceipt: true,
});
return;
}
}

htmlParser.write(_.get(action, ['message', 0, 'html']));
});
htmlParser.end();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const customHTMLElementModels = {
'mention-here': defaultHTMLElementModels.span.extend({tagName: 'mention-here'}),
};

const defaultViewProps = {style: [styles.alignItemsStart, styles.userSelectText]};
const defaultViewProps = {style: [styles.alignItemsStart, styles.userSelectText, styles.w100, styles.h100]};

// We are using the explicit composite architecture for performance gains.
// Configuration for RenderHTML is handled in a top-level component providing
Expand Down
19 changes: 12 additions & 7 deletions src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,29 +33,33 @@ function ImageRenderer(props) {
// Concierge responder attachments are uploaded to S3 without any access
// control and thus require no authToken to verify access.
//
const isAttachment = Boolean(htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]);
const isAttachmentOrReceipt = Boolean(htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]);

// Files created/uploaded/hosted by App should resolve from API ROOT. Other URLs aren't modified
const previewSource = tryResolveUrlFromApiRoot(htmlAttribs.src);
const source = tryResolveUrlFromApiRoot(isAttachment ? htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE] : htmlAttribs.src);
const source = tryResolveUrlFromApiRoot(isAttachmentOrReceipt ? htmlAttribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE] : htmlAttribs.src);

const imageWidth = htmlAttribs['data-expensify-width'] ? parseInt(htmlAttribs['data-expensify-width'], 10) : undefined;
const imageHeight = htmlAttribs['data-expensify-height'] ? parseInt(htmlAttribs['data-expensify-height'], 10) : undefined;
const imagePreviewModalDisabled = htmlAttribs['data-expensify-preview-modal-disabled'] === 'true';

const shouldFitContainer = htmlAttribs['data-expensify-fit-container'] === 'true';
const sizingStyle = shouldFitContainer ? [styles.w100, styles.h100] : [];

return imagePreviewModalDisabled ? (
<ThumbnailImage
previewSourceURL={previewSource}
style={styles.webViewStyles.tagStyles.img}
isAuthTokenRequired={isAttachment}
style={shouldFitContainer ? [styles.w100, styles.h100] : styles.webViewStyles.tagStyles.img}
isAuthTokenRequired={isAttachmentOrReceipt}
imageWidth={imageWidth}
imageHeight={imageHeight}
shouldDynamicallyResize={!shouldFitContainer}
/>
) : (
<ShowContextMenuContext.Consumer>
{({anchor, report, action, checkIfContextMenuActive}) => (
<PressableWithoutFocus
style={[styles.noOutline]}
style={[styles.noOutline, ...sizingStyle]}
onPress={() => {
const route = ROUTES.getReportAttachmentRoute(report.reportID, source);
Navigation.navigate(route);
Expand All @@ -66,10 +70,11 @@ function ImageRenderer(props) {
>
<ThumbnailImage
previewSourceURL={previewSource}
style={styles.webViewStyles.tagStyles.img}
isAuthTokenRequired={isAttachment}
style={shouldFitContainer ? [styles.w100, styles.h100] : styles.webViewStyles.tagStyles.img}
isAuthTokenRequired={isAttachmentOrReceipt}
imageWidth={imageWidth}
imageHeight={imageHeight}
shouldDynamicallyResize={!shouldFitContainer}
/>
</PressableWithoutFocus>
)}
Expand Down
39 changes: 2 additions & 37 deletions src/components/MoneyRequestConfirmationList.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {withOnyx} from 'react-native-onyx';
import {format} from 'date-fns';
import _ from 'underscore';
import {View} from 'react-native';
import Str from 'expensify-common/lib/str';
import styles from '../styles/styles';
import * as ReportUtils from '../libs/ReportUtils';
import * as OptionsListUtils from '../libs/OptionsListUtils';
Expand All @@ -26,12 +25,8 @@ import Button from './Button';
import * as Expensicons from './Icon/Expensicons';
import themeColors from '../styles/themes/default';
import Image from './Image';
import ReceiptHTML from '../../assets/images/receipt-html.png';
import ReceiptDoc from '../../assets/images/receipt-doc.png';
import ReceiptGeneric from '../../assets/images/receipt-generic.png';
import ReceiptSVG from '../../assets/images/receipt-svg.png';
import * as FileUtils from '../libs/fileDownload/FileUtils';
import useLocalize from '../hooks/useLocalize';
import * as ReceiptUtils from '../libs/ReceiptUtils';

const propTypes = {
/** Callback to inform parent modal of success */
Expand Down Expand Up @@ -319,36 +314,6 @@ function MoneyRequestConfirmationList(props) {
);
}, [confirm, props.selectedParticipants, props.bankAccountRoute, props.iouCurrencyCode, props.iouType, props.isReadOnly, props.policyID, selectedParticipants, splitOrRequestOptions]);

/**
* Grab the appropriate image URI based on file type
*
* @param {String} receiptPath
* @param {String} receiptSource
* @returns {*}
*/
const getImageURI = (receiptPath, receiptSource) => {
const {fileExtension} = FileUtils.splitExtensionFromFileName(receiptSource);
const isReceiptImage = Str.isImage(props.receiptSource);

if (isReceiptImage) {
return receiptPath;
}

if (fileExtension === CONST.IOU.FILE_TYPES.HTML) {
return ReceiptHTML;
}

if (fileExtension === CONST.IOU.FILE_TYPES.DOC || fileExtension === CONST.IOU.FILE_TYPES.DOCX) {
return ReceiptDoc;
}

if (fileExtension === CONST.IOU.FILE_TYPES.SVG) {
return ReceiptSVG;
}

return ReceiptGeneric;
};

return (
<OptionsSelector
sections={optionSelectorSections}
Expand All @@ -369,7 +334,7 @@ function MoneyRequestConfirmationList(props) {
{!_.isEmpty(props.receiptPath) ? (
<Image
style={styles.moneyRequestImage}
source={{uri: getImageURI(props.receiptPath, props.receiptSource)}}
source={{uri: ReceiptUtils.getThumbnailAndImageURIs(props.receiptPath, props.receiptSource).image}}
/>
) : (
<MenuItemWithTopDescription
Expand Down
7 changes: 6 additions & 1 deletion src/components/MoneyRequestHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import * as IOU from '../libs/actions/IOU';
import * as ReportActionsUtils from '../libs/ReportActionsUtils';
import ConfirmModal from './ConfirmModal';
import useLocalize from '../hooks/useLocalize';
import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar';
import * as TransactionUtils from '../libs/TransactionUtils';

const propTypes = {
/** The report currently being looked at */
Expand Down Expand Up @@ -70,6 +72,9 @@ function MoneyRequestHeader(props) {
setIsDeleteModalVisible(false);
}, [parentReportAction, setIsDeleteModalVisible]);

const transaction = TransactionUtils.getLinkedTransaction(parentReportAction);
const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction);

return (
<>
<View style={[styles.pl0]}>
Expand All @@ -90,8 +95,8 @@ function MoneyRequestHeader(props) {
personalDetails={props.personalDetails}
shouldShowBackButton={props.isSmallScreenWidth}
onBackButtonPress={() => Navigation.goBack(ROUTES.HOME, false, true)}
shouldShowBorderBottom
/>
{isScanning && <MoneyRequestHeaderStatusBar />}
</View>
<ConfirmModal
title={translate('iou.deleteRequest')}
Expand Down
37 changes: 37 additions & 0 deletions src/components/MoneyRequestHeaderStatusBar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import {View} from 'react-native';
import styles from '../styles/styles';
import useLocalize from '../hooks/useLocalize';
import Text from './Text';

function MoneyRequestHeaderStatusBar() {
const {translate} = useLocalize();

return (
<View
style={[
styles.dFlex,
styles.flexRow,
styles.alignItemsCenter,
styles.flexGrow1,
styles.justifyContentBetween,
styles.overflowHidden,
styles.ph5,
styles.pv3,
styles.borderBottom,
styles.w100,
]}
>
<View style={[styles.moneyRequestHeaderStatusBarBadge]}>
<Text style={[styles.textStrong, styles.textLabel]}>{translate('iou.receiptStatusTitle')}</Text>
</View>
<View style={[styles.flexShrink1]}>
<Text style={[styles.textLabelSupporting]}>{translate('iou.receiptStatusText')}</Text>
</View>
</View>
);
}

MoneyRequestHeaderStatusBar.displayName = 'MoneyRequestHeaderStatusBar';

export default MoneyRequestHeaderStatusBar;
12 changes: 6 additions & 6 deletions src/components/ReportActionItem/MoneyRequestAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import compose from '../../libs/compose';
import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes';
import networkPropTypes from '../networkPropTypes';
import iouReportPropTypes from '../../pages/iouReportPropTypes';
import IOUPreview from './IOUPreview';
import MoneyRequestPreview from './MoneyRequestPreview';
import Navigation from '../../libs/Navigation/Navigation';
import ROUTES from '../../ROUTES';
import styles from '../../styles/styles';
Expand Down Expand Up @@ -87,7 +87,7 @@ const defaultProps = {
function MoneyRequestAction(props) {
const isSplitBillAction = lodashGet(props.action, 'originalMessage.type', '') === CONST.IOU.REPORT_ACTION_TYPE.SPLIT;

const onIOUPreviewPressed = () => {
const onMoneyRequestPreviewPressed = () => {
if (isSplitBillAction) {
const reportActionID = lodashGet(props.action, 'reportActionID', '0');
Navigation.navigate(ROUTES.getSplitBillDetailsRoute(props.chatReportID, reportActionID));
Expand Down Expand Up @@ -138,16 +138,16 @@ function MoneyRequestAction(props) {
return isDeletedParentAction ? (
<RenderHTML html={`<comment>${props.translate('parentReportAction.deletedRequest')}</comment>`} />
) : (
<IOUPreview
chatReportID={props.chatReportID}
<MoneyRequestPreview
iouReportID={props.requestReportID}
chatReportID={props.chatReportID}
isBillSplit={isSplitBillAction}
action={props.action}
contextMenuAnchor={props.contextMenuAnchor}
checkIfContextMenuActive={props.checkIfContextMenuActive}
shouldShowPendingConversionMessage={shouldShowPendingConversionMessage}
onPreviewPressed={onIOUPreviewPressed}
containerStyles={[styles.cursorPointer, props.isHovered ? styles.iouPreviewBoxHover : undefined, ...props.style]}
onPreviewPressed={onMoneyRequestPreviewPressed}
containerStyles={[styles.cursorPointer, props.isHovered ? styles.reportPreviewBoxHoverBorder : undefined, ...props.style]}
isHovered={props.isHovered}
/>
);
Expand Down
Loading

0 comments on commit bfda5ed

Please sign in to comment.