Skip to content

Commit 50c1875

Browse files
committed
Merge branch 'main' into feat/reanimated-modal-for-compare-plans-modal
2 parents b3cf368 + 5cb0345 commit 50c1875

19 files changed

Lines changed: 342 additions & 27 deletions

src/CONST/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,6 +1038,7 @@ const CONST = {
10381038
SHUTTER_SIZE: 90,
10391039
MAX_REPORT_PREVIEW_RECEIPTS: 3,
10401040
},
1041+
RECEIPT_PREVIEW_TOP_BOTTOM_MARGIN: 120,
10411042
REPORT: {
10421043
ROLE: {
10431044
ADMIN: 'admin',
@@ -1512,6 +1513,8 @@ const CONST = {
15121513
SEARCH_MOST_RECENT_OPTIONS: 'search_most_recent_options',
15131514
DEBOUNCE_HANDLE_SEARCH: 'debounce_handle_search',
15141515
FAST_SEARCH_TREE_CREATION: 'fast_search_tree_creation',
1516+
SHOW_HOVER_PREVIEW_DELAY: 270,
1517+
SHOW_HOVER_PREVIEW_ANIMATION_DURATION: 200,
15151518
},
15161519
PRIORITY_MODE: {
15171520
GSD: 'gsd',

src/components/DistanceEReceipt.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ import Text from './Text';
2222
type DistanceEReceiptProps = {
2323
/** The transaction for the distance expense */
2424
transaction: Transaction;
25+
26+
/** Whether the distanceEReceipt is shown as hover preview */
27+
hoverPreview?: boolean;
2528
};
2629

27-
function DistanceEReceipt({transaction}: DistanceEReceiptProps) {
30+
function DistanceEReceipt({transaction, hoverPreview = false}: DistanceEReceiptProps) {
2831
const styles = useThemeStyles();
2932
const {translate} = useLocalize();
3033
const thumbnail = hasReceipt(transaction) ? getThumbnailAndImageURIs(transaction).thumbnail : null;
@@ -42,7 +45,7 @@ function DistanceEReceipt({transaction}: DistanceEReceiptProps) {
4245
[waypoints],
4346
);
4447
return (
45-
<View style={[styles.flex1, styles.alignItemsCenter]}>
48+
<View style={[styles.flex1, styles.alignItemsCenter, hoverPreview && styles.mhv5]}>
4649
<ScrollView
4750
style={styles.w100}
4851
contentContainerStyle={[styles.flexGrow1, styles.justifyContentCenter, styles.alignItemsCenter]}

src/components/SelectionList/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,11 +273,20 @@ type TransactionListItemType = ListItem &
273273
/** Key used internally by React */
274274
keyForList: string;
275275

276+
/** The name of the file used for a receipt */
277+
filename?: string;
278+
276279
/** Attendees in the transaction */
277280
attendees?: Attendee[];
278281

279282
/** Precomputed violations */
280283
violations?: TransactionViolation[];
284+
285+
/** The CC for this transaction */
286+
cardID?: number;
287+
288+
/** The display name of the purchaser card, if any */
289+
cardName?: string;
281290
};
282291

283292
type ReportActionListItemType = ListItem &

src/components/TransactionItemRow/DataCells/ReceiptCell.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {View} from 'react-native';
33
import type {ViewStyle} from 'react-native';
44
import {Receipt} from '@components/Icon/Expensicons';
55
import ReceiptImage from '@components/ReceiptImage';
6+
import ReceiptPreview from '@components/TransactionItemRow/ReceiptPreview';
7+
import useHover from '@hooks/useHover';
68
import useStyleUtils from '@hooks/useStyleUtils';
79
import useTheme from '@hooks/useTheme';
810
import useThemeStyles from '@hooks/useThemeStyles';
@@ -18,10 +20,10 @@ function ReceiptCell({transactionItem, isSelected, style}: {transactionItem: Tra
1820
const styles = useThemeStyles();
1921
const StyleUtils = useStyleUtils();
2022
const backgroundStyles = isSelected ? StyleUtils.getBackgroundColorStyle(theme.buttonHoveredBG) : StyleUtils.getBackgroundColorStyle(theme.border);
21-
23+
const {hovered, bind} = useHover();
24+
const isEReceipt = transactionItem.hasEReceipt && !hasReceiptSource(transactionItem);
2225
let source = transactionItem?.receipt?.source ?? '';
23-
24-
if (source && typeof source === 'string') {
26+
if (source) {
2527
const filename = getFileName(source);
2628
const receiptURIs = getThumbnailAndImageURIs(transactionItem, null, filename);
2729
source = tryResolveUrlFromApiRoot(receiptURIs.thumbnail ?? receiptURIs.image ?? '');
@@ -36,10 +38,12 @@ function ReceiptCell({transactionItem, isSelected, style}: {transactionItem: Tra
3638
backgroundStyles,
3739
style,
3840
]}
41+
onMouseEnter={bind.onMouseEnter}
42+
onMouseLeave={bind.onMouseLeave}
3943
>
4044
<ReceiptImage
4145
source={source}
42-
isEReceipt={transactionItem.hasEReceipt && !hasReceiptSource(transactionItem)}
46+
isEReceipt={isEReceipt}
4347
transactionID={transactionItem.transactionID}
4448
shouldUseThumbnailImage={!transactionItem?.receipt?.source}
4549
isAuthTokenRequired
@@ -52,6 +56,12 @@ function ReceiptCell({transactionItem, isSelected, style}: {transactionItem: Tra
5256
loadingIndicatorStyles={styles.bgTransparent}
5357
transactionItem={transactionItem}
5458
/>
59+
<ReceiptPreview
60+
source={source}
61+
hovered={hovered}
62+
isEReceipt={!!isEReceipt}
63+
transactionItem={transactionItem}
64+
/>
5565
</View>
5666
);
5767
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
function ReceiptPreview() {
2+
return null;
3+
}
4+
5+
export default ReceiptPreview;
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import React, {useCallback, useEffect, useRef, useState} from 'react';
2+
import ReactDOM from 'react-dom';
3+
import type {LayoutChangeEvent} from 'react-native';
4+
import {View} from 'react-native';
5+
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated';
6+
import DistanceEReceipt from '@components/DistanceEReceipt';
7+
import EReceipt from '@components/EReceipt';
8+
import BaseImage from '@components/Image/BaseImage';
9+
import type {ImageOnLoadEvent} from '@components/Image/types';
10+
import useDebouncedState from '@hooks/useDebouncedState';
11+
import useResponsiveLayout from '@hooks/useResponsiveLayout';
12+
import useThemeStyles from '@hooks/useThemeStyles';
13+
import useWindowDimensions from '@hooks/useWindowDimensions';
14+
import {isDistanceRequest} from '@libs/TransactionUtils';
15+
import variables from '@styles/variables';
16+
import CONST from '@src/CONST';
17+
import type {Transaction} from '@src/types/onyx';
18+
19+
const eReceiptAspectRatio = variables.eReceiptBGHWidth / variables.eReceiptBGHeight;
20+
21+
type ReceiptPreviewProps = {
22+
/** Path to the image to be opened in the preview */
23+
source: string;
24+
25+
/** Whether the preview should be shown (e.g. if we are hovered over certain ReceiptCell) */
26+
hovered: boolean;
27+
28+
/** Is preview for an e-receipt */
29+
isEReceipt: boolean;
30+
31+
/** Transaction object related to the preview */
32+
transactionItem: Transaction;
33+
};
34+
35+
function ReceiptPreview({source, hovered, isEReceipt = false, transactionItem}: ReceiptPreviewProps) {
36+
const isDistanceEReceipt = isDistanceRequest(transactionItem);
37+
const styles = useThemeStyles();
38+
const [eReceiptScaleFactor, setEReceiptScaleFactor] = useState(0);
39+
const [imageAspectRatio, setImageAspectRatio] = useState<string | number | undefined>(undefined);
40+
const [distanceEReceiptAspectRatio, setDistanceEReceiptAspectRatio] = useState<string | number | undefined>(undefined);
41+
const [shouldShow, debounceShouldShow, setShouldShow] = useDebouncedState(false, CONST.TIMING.SHOW_HOVER_PREVIEW_DELAY);
42+
const {shouldUseNarrowLayout} = useResponsiveLayout();
43+
const hasMeasured = useRef(false);
44+
const {windowHeight} = useWindowDimensions();
45+
46+
const handleDistanceEReceiptLayout = (e: LayoutChangeEvent) => {
47+
if (hasMeasured.current) {
48+
return;
49+
}
50+
hasMeasured.current = true;
51+
52+
const {height, width} = e.nativeEvent.layout;
53+
if (height === 0) {
54+
// on the initial layout, measured height is 0, so we want to set everything on the second one
55+
hasMeasured.current = false;
56+
return;
57+
}
58+
if (height * eReceiptScaleFactor > windowHeight - CONST.RECEIPT_PREVIEW_TOP_BOTTOM_MARGIN) {
59+
setDistanceEReceiptAspectRatio(variables.eReceiptBGHWidth / (windowHeight - CONST.RECEIPT_PREVIEW_TOP_BOTTOM_MARGIN));
60+
return;
61+
}
62+
setDistanceEReceiptAspectRatio(variables.eReceiptBGHWidth / height);
63+
setEReceiptScaleFactor(width / variables.eReceiptBGHWidth);
64+
};
65+
66+
const updateImageAspectRatio = useCallback(
67+
(width: number, height: number) => {
68+
if (!source) {
69+
return;
70+
}
71+
72+
setImageAspectRatio(height ? width / height : 'auto');
73+
},
74+
[source],
75+
);
76+
77+
const handleLoad = useCallback(
78+
(event: ImageOnLoadEvent) => {
79+
const {width, height} = event.nativeEvent;
80+
81+
updateImageAspectRatio(width, height);
82+
},
83+
[updateImageAspectRatio],
84+
);
85+
86+
const handleEReceiptLayout = (e: LayoutChangeEvent) => {
87+
const {width} = e.nativeEvent.layout;
88+
setEReceiptScaleFactor(width / variables.eReceiptBGHWidth);
89+
};
90+
91+
useEffect(() => {
92+
setShouldShow(hovered);
93+
}, [hovered, setShouldShow]);
94+
95+
if (shouldUseNarrowLayout || !debounceShouldShow || !shouldShow || (!source && !isEReceipt && !isDistanceEReceipt)) {
96+
return null;
97+
}
98+
99+
const shouldShowImage = source && !(isEReceipt || isDistanceEReceipt);
100+
const shouldShowDistanceEReceipt = isDistanceEReceipt && !isEReceipt;
101+
102+
return ReactDOM.createPortal(
103+
<Animated.View
104+
entering={FadeIn.duration(CONST.TIMING.SHOW_HOVER_PREVIEW_ANIMATION_DURATION)}
105+
exiting={FadeOut.duration(CONST.TIMING.SHOW_HOVER_PREVIEW_ANIMATION_DURATION)}
106+
style={[styles.receiptPreview, styles.flexColumn, styles.alignItemsCenter, styles.justifyContentStart]}
107+
>
108+
{shouldShowImage ? (
109+
<View style={[styles.w100]}>
110+
<BaseImage
111+
source={{uri: source}}
112+
style={[styles.w100, {aspectRatio: imageAspectRatio}]}
113+
onLoad={handleLoad}
114+
/>
115+
</View>
116+
) : (
117+
<View style={styles.receiptPreviewEReceiptsContainer}>
118+
{shouldShowDistanceEReceipt ? (
119+
<View
120+
onLayout={handleDistanceEReceiptLayout}
121+
style={[{transformOrigin: 'center', scale: eReceiptScaleFactor, aspectRatio: distanceEReceiptAspectRatio}]}
122+
>
123+
<DistanceEReceipt
124+
transaction={transactionItem}
125+
hoverPreview
126+
/>
127+
</View>
128+
) : (
129+
<View
130+
onLayout={handleEReceiptLayout}
131+
style={[styles.receiptPreviewEReceipt, {aspectRatio: eReceiptAspectRatio, scale: eReceiptScaleFactor}]}
132+
>
133+
<EReceipt
134+
transactionID={transactionItem.transactionID}
135+
transactionItem={transactionItem}
136+
/>
137+
</View>
138+
)}
139+
</View>
140+
)}
141+
</Animated.View>,
142+
document.body,
143+
);
144+
}
145+
146+
export default ReceiptPreview;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {useMemo} from 'react';
2+
import type {ValueOf} from 'type-fest';
3+
import {canEditFieldOfMoneyRequest} from '@libs/ReportUtils';
4+
import type {IOUAction, IOUType} from '@src/CONST';
5+
import CONST from '@src/CONST';
6+
import ONYXKEYS from '@src/ONYXKEYS';
7+
import type {OnyxInputOrEntry, Report} from '@src/types/onyx';
8+
import useOnyx from './useOnyx';
9+
10+
// eslint-disable-next-line rulesdir/no-negated-variables
11+
const useShowNotFoundPageInIOUStep = (action: IOUAction, iouType: IOUType, report: OnyxInputOrEntry<Report>, fieldToEdit: ValueOf<typeof CONST.EDIT_REQUEST_FIELD>) => {
12+
const isEditing = action === CONST.IOU.ACTION.EDIT;
13+
const reportActionsReportID = useMemo(() => {
14+
let actionsReportID;
15+
if (isEditing) {
16+
actionsReportID = iouType === CONST.IOU.TYPE.SPLIT ? report?.reportID : report?.parentReportID;
17+
}
18+
return actionsReportID;
19+
}, [isEditing, iouType, report?.reportID, report?.parentReportID]);
20+
21+
const [reportAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportActionsReportID}`, {
22+
canEvict: false,
23+
canBeMissing: true,
24+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
25+
selector: (reportActions) => reportActions?.[`${report?.parentReportActionID}`],
26+
});
27+
return isEditing && !canEditFieldOfMoneyRequest(reportAction, fieldToEdit);
28+
};
29+
30+
export default useShowNotFoundPageInIOUStep;

src/libs/SearchUIUtils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -752,12 +752,10 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data'], metadata
752752
const report = data[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`];
753753
const policy = data[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`];
754754
const shouldShowBlankTo = !report || isOpenExpenseReport(report);
755-
756755
const transactionViolations = getTransactionViolations(allViolations, transactionItem);
757756
// Use Map.get() for faster lookups with default values
758757
const from = personalDetailsMap.get(transactionItem.accountID.toString()) ?? emptyPersonalDetails;
759758
const to = transactionItem.managerID && !shouldShowBlankTo ? (personalDetailsMap.get(transactionItem.managerID.toString()) ?? emptyPersonalDetails) : emptyPersonalDetails;
760-
761759
const {formattedFrom, formattedTo, formattedTotal, formattedMerchant, date} = getTransactionItemCommonFormattedProperties(transactionItem, from, to, policy);
762760

763761
const transactionSection: TransactionListItemType = {
@@ -781,7 +779,7 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data'], metadata
781779
isAmountColumnWide: shouldShowAmountInWideColumn,
782780
isTaxAmountColumnWide: shouldShowTaxAmountInWideColumn,
783781
violations: transactionViolations,
784-
782+
filename: transactionItem.filename,
785783
// Manually copying all the properties from transactionItem
786784
transactionID: transactionItem.transactionID,
787785
created: transactionItem.created,
@@ -819,6 +817,8 @@ function getTransactionsSections(data: OnyxTypes.SearchResults['data'], metadata
819817
errors: transactionItem.errors,
820818
isActionLoading: transactionItem.isActionLoading,
821819
hasViolation: transactionItem.hasViolation,
820+
cardID: transactionItem.cardID,
821+
cardName: transactionItem.cardName,
822822
};
823823

824824
transactionsSections.push(transactionSection);

src/libs/TransactionUtils/index.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {toLocaleDigit} from '@libs/LocaleDigitUtils';
1818
import {translateLocal} from '@libs/Localize';
1919
import Log from '@libs/Log';
2020
import {rand64, roundToTwoDecimalPlaces} from '@libs/NumberUtils';
21-
import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
21+
import {getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils';
2222
import {
2323
getCommaSeparatedTagNameWithSanitizedColons,
2424
getDistanceRateCustomUnitRate,
@@ -731,18 +731,29 @@ function getMerchantOrDescription(transaction: OnyxEntry<Transaction>) {
731731
*/
732732
function getAttendees(transaction: OnyxInputOrEntry<Transaction>): Attendee[] {
733733
const attendees = transaction?.modifiedAttendees ? transaction.modifiedAttendees : (transaction?.comment?.attendees ?? []);
734-
if (attendees.length === 0) {
735-
const details = getPersonalDetailByEmail(currentUserEmail);
736-
attendees.push({
737-
email: currentUserEmail,
738-
login: details?.login ?? currentUserEmail,
739-
displayName: details?.displayName ?? currentUserEmail,
740-
accountID: currentUserAccountID,
741-
text: details?.displayName ?? currentUserEmail,
742-
searchText: details?.displayName ?? currentUserEmail,
743-
avatarUrl: details?.avatarThumbnail ?? '',
744-
selected: true,
745-
});
734+
if (attendees.length === 0 && transaction?.reportID) {
735+
// Get the creator of the transaction by looking at the owner of the report linked to the transaction
736+
const report = getReportOrDraftReport(transaction.reportID);
737+
const creatorAccountID = report?.ownerAccountID;
738+
739+
if (creatorAccountID) {
740+
const [creatorDetails] = getPersonalDetailsByIDs({accountIDs: [creatorAccountID], currentUserAccountID});
741+
const creatorEmail = creatorDetails?.login ?? '';
742+
const creatorDisplayName = creatorDetails?.displayName ?? creatorEmail;
743+
744+
if (creatorEmail) {
745+
attendees.push({
746+
email: creatorEmail,
747+
login: creatorEmail,
748+
displayName: creatorDisplayName,
749+
accountID: creatorAccountID,
750+
text: creatorDisplayName,
751+
searchText: creatorDisplayName,
752+
avatarUrl: creatorDetails?.avatarThumbnail ?? '',
753+
selected: true,
754+
});
755+
}
756+
}
746757
}
747758
return attendees;
748759
}

0 commit comments

Comments
 (0)