Skip to content
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
4 changes: 4 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,7 @@ const CONST = {
SHUTTER_SIZE: 90,
MAX_REPORT_PREVIEW_RECEIPTS: 3,
},
RECEIPT_PREVIEW_TOP_BOTTOM_MARGIN: 120,
REPORT: {
ROLE: {
ADMIN: 'admin',
Expand Down Expand Up @@ -1541,6 +1542,9 @@ const CONST = {
SKELETON_ANIMATION_SPEED: 3,
SEARCH_MOST_RECENT_OPTIONS: 'search_most_recent_options',
DEBOUNCE_HANDLE_SEARCH: 'debounce_handle_search',
FAST_SEARCH_TREE_CREATION: 'fast_search_tree_creation',
SHOW_HOVER_PREVIEW_DELAY: 270,
SHOW_HOVER_PREVIEW_ANIMATION_DURATION: 250,
},
PRIORITY_MODE: {
GSD: 'gsd',
Expand Down
9 changes: 6 additions & 3 deletions src/components/DistanceEReceipt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ import Text from './Text';
type DistanceEReceiptProps = {
/** The transaction for the distance expense */
transaction: Transaction;

/** Whether the distanceEReceipt is shown as hover preview */
hoverPreview?: boolean;
};

function DistanceEReceipt({transaction}: DistanceEReceiptProps) {
function DistanceEReceipt({transaction, hoverPreview = false}: DistanceEReceiptProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const thumbnail = hasReceipt(transaction) ? getThumbnailAndImageURIs(transaction).thumbnail : null;
Expand All @@ -42,7 +45,7 @@ function DistanceEReceipt({transaction}: DistanceEReceiptProps) {
[waypoints],
);
return (
<View style={[styles.flex1, styles.alignItemsCenter]}>
<View style={[styles.flex1, styles.alignItemsCenter, hoverPreview && styles.mhv5]}>
<ScrollView
style={styles.w100}
contentContainerStyle={[styles.flexGrow1, styles.justifyContentCenter, styles.alignItemsCenter]}
Expand All @@ -67,7 +70,7 @@ function DistanceEReceipt({transaction}: DistanceEReceiptProps) {
</View>
<View style={[styles.mb10, styles.gap5, styles.ph2, styles.flexColumn, styles.alignItemsCenter]}>
{transactionAmount !== null && transactionAmount !== undefined && <Text style={styles.eReceiptAmount}>{formattedTransactionAmount}</Text>}
<Text style={styles.eReceiptMerchant}>{transactionMerchant}</Text>
<Text style={styles.eReceiptMerchant}>{transactionMerchant !== translate('iou.fieldPending') ? transactionMerchant : transaction.merchant}</Text>
</View>
<View style={[styles.mb10, styles.gap5, styles.ph2]}>
{Object.entries(sortedWaypoints).map(([key, waypoint]) => {
Expand Down
1 change: 0 additions & 1 deletion src/components/EReceipt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ function EReceipt({transactionID, transactionItem, isThumbnail = false}: EReceip
const currency = getCurrencySymbol(transactionCurrency ?? '');
const amount = currency ? formattedAmount.replace(currency, '') : formattedAmount;
const cardDescription = getCompanyCardDescription(transactionCardName, transactionCardID, cardList) ?? (transactionCardID ? getCardDescription(cardList?.[transactionCardID]) : '');

const secondaryBgcolorStyle = secondaryColor ? StyleUtils.getBackgroundColorStyle(secondaryColor) : undefined;
const primaryTextColorStyle = primaryColor ? StyleUtils.getColorStyle(primaryColor) : undefined;
const titleTextColorStyle = titleColor ? StyleUtils.getColorStyle(titleColor) : undefined;
Expand Down
8 changes: 7 additions & 1 deletion src/components/EReceiptWithSizeCalculation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ type EReceiptWithSizeCalculationProps = {
transactionItem?: TransactionListItemType | Transaction;
};

const eReceiptAspectRatio = variables.eReceiptBGHWidth / variables.eReceiptBGHeight;

function EReceiptWithSizeCalculation(props: EReceiptWithSizeCalculationProps) {
const [scaleFactor, setScaleFactor] = useState(0);
const styles = useThemeStyles();
Expand All @@ -28,7 +30,11 @@ function EReceiptWithSizeCalculation(props: EReceiptWithSizeCalculationProps) {
onLayout={onLayout}
// We are applying transform of 0 translateZ to avoid a sub-pixel rendering error of a thin 1px line
// appearing on EReceipts on web, specifically in chrome. More details in https://github.com/Expensify/App/pull/59944#issuecomment-2797249923.
style={[styles.w100, styles.h100, {transform: `scale(${scaleFactor}) ${styles.translateZ0.transform as string}`, transformOrigin: 'top left'}]}
style={[
styles.w100,
styles.h100,
{transform: `scale(${scaleFactor}) ${styles.translateZ0.transform as string}`, transformOrigin: 'top left', aspectRatio: eReceiptAspectRatio},
]}
>
<EReceipt
// eslint-disable-next-line react/jsx-props-no-spreading
Expand Down
9 changes: 9 additions & 0 deletions src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,11 +276,20 @@ type TransactionListItemType = ListItem &
/** Key used internally by React */
keyForList: string;

/** The name of the file used for a receipt */
filename?: string;

/** Attendees in the transaction */
attendees?: Attendee[];

/** Precomputed violations */
violations?: TransactionViolation[];

/** The CC for this transaction */
cardID?: number;

/** The display name of the purchaser card, if any */
cardName?: string;
};

type ReportActionListItemType = ListItem &
Expand Down
24 changes: 20 additions & 4 deletions src/components/TransactionItemRow/DataCells/ReceiptCell.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {Str} from 'expensify-common';
import React from 'react';
import {View} from 'react-native';
import type {ViewStyle} from 'react-native';
import {Receipt} from '@components/Icon/Expensicons';
import ReceiptImage from '@components/ReceiptImage';
import ReceiptPreview from '@components/TransactionItemRow/ReceiptPreview';
import useHover from '@hooks/useHover';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
Expand All @@ -18,13 +21,17 @@ function ReceiptCell({transactionItem, isSelected, style}: {transactionItem: Tra
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const backgroundStyles = isSelected ? StyleUtils.getBackgroundColorStyle(theme.buttonHoveredBG) : StyleUtils.getBackgroundColorStyle(theme.border);

const {hovered, bind} = useHover();
const isEReceipt = transactionItem.hasEReceipt && !hasReceiptSource(transactionItem);
let source = transactionItem?.receipt?.source ?? '';
let previewSource = transactionItem?.receipt?.source ?? '';

if (source && typeof source === 'string') {
if (source) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from #69278, removing the typeof check resulted in a crash when creating a distance expense. This is because we on native the source is set to a number for optimistically created expenses.

const filename = getFileName(source);
const receiptURIs = getThumbnailAndImageURIs(transactionItem, null, filename);
source = tryResolveUrlFromApiRoot(receiptURIs.thumbnail ?? receiptURIs.image ?? '');
const previewImageURI = Str.isImage(filename) ? receiptURIs.image : receiptURIs.thumbnail;
previewSource = tryResolveUrlFromApiRoot(previewImageURI ?? '');
}

return (
Expand All @@ -36,12 +43,14 @@ function ReceiptCell({transactionItem, isSelected, style}: {transactionItem: Tra
backgroundStyles,
style,
]}
onMouseEnter={bind.onMouseEnter}
onMouseLeave={bind.onMouseLeave}
>
<ReceiptImage
source={source}
isEReceipt={transactionItem.hasEReceipt && !hasReceiptSource(transactionItem)}
isEReceipt={isEReceipt}
transactionID={transactionItem.transactionID}
shouldUseThumbnailImage={!transactionItem?.receipt?.source}
shouldUseThumbnailImage

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from #70651, we should handle the loading style for expenses that have receipt here. More details: #70651 (comment)

isAuthTokenRequired
fallbackIcon={Receipt}
fallbackIconSize={20}
Expand All @@ -51,6 +60,13 @@ function ReceiptCell({transactionItem, isSelected, style}: {transactionItem: Tra
loadingIconSize="small"
loadingIndicatorStyles={styles.bgTransparent}
transactionItem={transactionItem}
shouldUseInitialObjectPosition
/>
<ReceiptPreview
source={previewSource}
hovered={hovered}
isEReceipt={!!isEReceipt}
transactionItem={transactionItem}
/>
</View>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function ReceiptPreview() {
return null;
}

export default ReceiptPreview;
168 changes: 168 additions & 0 deletions src/components/TransactionItemRow/ReceiptPreview/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import React, {useCallback, useEffect, useRef, useState} from 'react';
import ReactDOM from 'react-dom';
import type {LayoutChangeEvent} from 'react-native';
import {ActivityIndicator, StyleSheet, View} from 'react-native';
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated';
import DistanceEReceipt from '@components/DistanceEReceipt';
import EReceiptWithSizeCalculation from '@components/EReceiptWithSizeCalculation';
import type {ImageOnLoadEvent} from '@components/Image/types';
import useDebouncedState from '@hooks/useDebouncedState';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {isDistanceRequest} from '@libs/TransactionUtils';
import variables from '@styles/variables';
import Image from '@src/components/Image';
import CONST from '@src/CONST';
import type {Transaction} from '@src/types/onyx';

type ReceiptPreviewProps = {
/** Path to the image to be opened in the preview */
source: string;

/** Whether the preview should be shown (e.g. if we are hovered over certain ReceiptCell) */
hovered: boolean;

/** Is preview for an e-receipt */
isEReceipt: boolean;

/** Transaction object related to the preview */
transactionItem: Transaction;
};

function ReceiptPreview({source, hovered, isEReceipt = false, transactionItem}: ReceiptPreviewProps) {
const isDistanceEReceipt = isDistanceRequest(transactionItem);
const styles = useThemeStyles();
const theme = useTheme();
const [eReceiptScaleFactor, setEReceiptScaleFactor] = useState(0);
const [imageAspectRatio, setImageAspectRatio] = useState<string | number | undefined>(undefined);
const [distanceEReceiptAspectRatio, setDistanceEReceiptAspectRatio] = useState<string | number | undefined>(undefined);
const [shouldShow, debounceShouldShow, setShouldShow] = useDebouncedState(false, CONST.TIMING.SHOW_HOVER_PREVIEW_DELAY);
const {shouldUseNarrowLayout} = useResponsiveLayout();
const hasMeasured = useRef(false);
const {windowHeight} = useWindowDimensions();
const [isLoading, setIsLoading] = useState(true);

const handleDistanceEReceiptLayout = (e: LayoutChangeEvent) => {
if (hasMeasured.current) {
return;
}
hasMeasured.current = true;

const {height, width} = e.nativeEvent.layout;
if (height === 0) {
// on the initial layout, measured height is 0, so we want to set everything on the second one
hasMeasured.current = false;
return;
}
if (height * eReceiptScaleFactor > windowHeight - CONST.RECEIPT_PREVIEW_TOP_BOTTOM_MARGIN) {
setDistanceEReceiptAspectRatio(variables.eReceiptBGHWidth / (windowHeight - CONST.RECEIPT_PREVIEW_TOP_BOTTOM_MARGIN));
return;
}
setDistanceEReceiptAspectRatio(variables.eReceiptBGHWidth / height);
setEReceiptScaleFactor(width / variables.eReceiptBGHWidth);
};

const updateImageAspectRatio = useCallback(
(width: number, height: number) => {
if (!source) {
return;
}

setImageAspectRatio(height ? width / height : 'auto');
},
[source],
);

const handleLoad = useCallback(
(e: ImageOnLoadEvent) => {
const {width, height} = e.nativeEvent;
updateImageAspectRatio(width, height);
setIsLoading(false);
},
[updateImageAspectRatio],
);

const handleError = () => {
setIsLoading(false);
};

useEffect(() => {
setShouldShow(hovered);
}, [hovered, setShouldShow]);

if (shouldUseNarrowLayout || !debounceShouldShow || !shouldShow || (!source && !isEReceipt && !isDistanceEReceipt)) {
return null;
}

const shouldShowImage = source && !(isEReceipt || isDistanceEReceipt);
const shouldShowDistanceEReceipt = isDistanceEReceipt && !isEReceipt;

return ReactDOM.createPortal(
<Animated.View
entering={FadeIn.duration(CONST.TIMING.SHOW_HOVER_PREVIEW_ANIMATION_DURATION)}
exiting={FadeOut.duration(CONST.TIMING.SHOW_HOVER_PREVIEW_ANIMATION_DURATION)}
style={[styles.receiptPreview, styles.flexColumn, styles.alignItemsCenter, styles.justifyContentStart]}
>
{shouldShowImage ? (
<View style={[styles.w100]}>
{isLoading && (
<View style={[StyleSheet.absoluteFillObject, styles.justifyContentCenter, styles.alignItemsCenter]}>
<ActivityIndicator
color={theme.spinner}
size="large"
/>
</View>
)}

<Image
source={{uri: source}}
style={[
styles.w100,
{aspectRatio: imageAspectRatio ?? 1},
isLoading && {opacity: 0}, // hide until loaded
]}
onLoadStart={() => {
if (isLoading) {
return;
}
setIsLoading(true);
}}
onError={handleError}
onLoad={handleLoad}
isAuthTokenRequired
/>
</View>
) : (
<View style={styles.receiptPreviewEReceiptsContainer}>
{shouldShowDistanceEReceipt ? (
<View
onLayout={handleDistanceEReceiptLayout}
style={[
{
transformOrigin: 'center',
scale: eReceiptScaleFactor,
aspectRatio: distanceEReceiptAspectRatio,
},
]}
>
<DistanceEReceipt
transaction={transactionItem}
hoverPreview
/>
</View>
) : (
<EReceiptWithSizeCalculation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from this issue #72414 , we are handling image receipts and distance eReceipts here, so per diem was falling back to EReceiptWithSizeCalculation with the card-style view instead of using its dedicated per diem PerDiemEReceipt view. That caused the inconsistency, and we’ve fixed it here: #73911

transactionID={transactionItem.transactionID}
transactionItem={transactionItem}
/>
)}
</View>
)}
</Animated.View>,
document.body,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Display name missing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall I create a pr with it?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add it to some follow up in future, no need to do it now. Also in case we need to revert it might cause more problems


export default ReceiptPreview;
4 changes: 3 additions & 1 deletion src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,7 @@ function getTransactionsSections(
isAmountColumnWide: shouldShowAmountInWideColumn,
isTaxAmountColumnWide: shouldShowTaxAmountInWideColumn,
violations: transactionViolations,

filename: transactionItem.filename,
// Manually copying all the properties from transactionItem
transactionID: transactionItem.transactionID,
created: transactionItem.created,
Expand Down Expand Up @@ -941,6 +941,8 @@ function getTransactionsSections(
errors: transactionItem.errors,
isActionLoading: transactionItem.isActionLoading,
hasViolation: transactionItem.hasViolation,
cardID: transactionItem.cardID,
cardName: transactionItem.cardName,
};

transactionsSections.push(transactionSection);
Expand Down
20 changes: 20 additions & 0 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5731,6 +5731,26 @@ const styles = (theme: ThemeColors) =>
aspectRatio: 1.7,
},

receiptPreview: {
position: 'absolute',
left: 60,
top: 60,
width: 380,
maxHeight: 'calc(100vh - 120px)',
borderRadius: variables.componentBorderRadiusLarge,
borderWidth: 1,
borderColor: theme.border,
overflow: 'hidden',
boxShadow: theme.shadow,
backgroundColor: theme.appBG,
},

receiptPreviewEReceiptsContainer: {
...sizing.w100,
...sizing.h100,
backgroundColor: colors.green800,
},

topBarWrapper: {
zIndex: 15,
},
Expand Down
Loading
Loading