Skip to content

Commit f128a47

Browse files
authored
Merge pull request Expensify#66169 from software-mansion-labs/@szymczak/flatten-view-hierarchy-part2
Refactor TransactionItemRow for better performance
2 parents 0a33194 + 2e663de commit f128a47

14 files changed

Lines changed: 534 additions & 428 deletions

src/components/Checkbox.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ type CheckboxProps = Partial<ChildrenProps> & {
5353

5454
/** Whether the checkbox should be selected when pressing Enter key */
5555
shouldSelectOnPressEnter?: boolean;
56+
57+
/** Additional styles to add to checkbox wrapper */
58+
wrapperStyle?: StyleProp<ViewStyle>;
5659
};
5760

5861
function Checkbox(
@@ -72,6 +75,7 @@ function Checkbox(
7275
accessibilityLabel,
7376
shouldStopMouseDownPropagation,
7477
shouldSelectOnPressEnter,
78+
wrapperStyle,
7579
}: CheckboxProps,
7680
ref: ForwardedRef<View>,
7781
) {
@@ -123,6 +127,7 @@ function Checkbox(
123127
aria-checked={isIndeterminate ? 'mixed' : isChecked}
124128
accessibilityLabel={accessibilityLabel}
125129
pressDimmingValue={1}
130+
wrapperStyle={wrapperStyle}
126131
>
127132
{children ?? (
128133
<View
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {useIsFocused} from '@react-navigation/native';
2+
import React from 'react';
3+
import {View} from 'react-native';
4+
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated';
5+
import Text from '@components/Text';
6+
import useLocalize from '@hooks/useLocalize';
7+
import useResponsiveLayout from '@hooks/useResponsiveLayout';
8+
import useThemeStyles from '@hooks/useThemeStyles';
9+
import {convertToDisplayString} from '@libs/CurrencyUtils';
10+
import type * as OnyxTypes from '@src/types/onyx';
11+
12+
type MoneyRequestReportTotalSpendProps = {
13+
/** Report for which the total spend is being displayed */
14+
report: OnyxTypes.Report;
15+
16+
/** Whether the report has any comments */
17+
hasComments: boolean;
18+
19+
/** Whether the report is loading report actions */
20+
isLoadingReportActions: boolean;
21+
22+
/** Whether the report has any transactions */
23+
isEmptyTransactions: boolean;
24+
25+
/** The total display spend of the report */
26+
totalDisplaySpend: number;
27+
28+
/** Whether the report has any pending actions */
29+
hasPendingAction: boolean;
30+
};
31+
32+
function MoneyRequestReportTotalSpend({hasComments, isLoadingReportActions, isEmptyTransactions, totalDisplaySpend, report, hasPendingAction}: MoneyRequestReportTotalSpendProps) {
33+
const styles = useThemeStyles();
34+
const {translate} = useLocalize();
35+
const {shouldUseNarrowLayout} = useResponsiveLayout();
36+
const isFocused = useIsFocused();
37+
38+
return (
39+
<View style={[styles.dFlex, styles.flexRow, styles.ph5, styles.justifyContentBetween, styles.mb2]}>
40+
<Animated.Text
41+
style={[styles.textLabelSupporting]}
42+
entering={hasComments ? undefined : FadeIn}
43+
exiting={isFocused ? FadeOut : undefined}
44+
>
45+
{hasComments || isLoadingReportActions ? translate('common.comments') : ''}
46+
</Animated.Text>
47+
{!isEmptyTransactions && (
48+
<View style={[styles.dFlex, styles.flexRow, styles.alignItemsCenter, styles.pr3]}>
49+
<Text style={[styles.mr3, styles.textLabelSupporting]}>{translate('common.total')}</Text>
50+
<Text style={[shouldUseNarrowLayout ? styles.mnw64p : styles.mnw100p, styles.textAlignRight, styles.textBold, hasPendingAction && styles.opacitySemiTransparent]}>
51+
{convertToDisplayString(totalDisplaySpend, report?.currency)}
52+
</Text>
53+
</View>
54+
)}
55+
</View>
56+
);
57+
}
58+
59+
MoneyRequestReportTotalSpend.displayName = 'MoneyRequestReportTotalSpend';
60+
61+
export default MoneyRequestReportTotalSpend;
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import React, {useEffect, useRef} from 'react';
2+
import type {View} from 'react-native';
3+
import {getButtonRole} from '@components/Button/utils';
4+
import OfflineWithFeedback from '@components/OfflineWithFeedback';
5+
import {PressableWithFeedback} from '@components/Pressable';
6+
import type {TableColumnSize} from '@components/Search/types';
7+
import TransactionItemRow from '@components/TransactionItemRow';
8+
import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
9+
import useLocalize from '@hooks/useLocalize';
10+
import useResponsiveLayout from '@hooks/useResponsiveLayout';
11+
import useTheme from '@hooks/useTheme';
12+
import useThemeStyles from '@hooks/useThemeStyles';
13+
import ControlSelection from '@libs/ControlSelection';
14+
import canUseTouchScreen from '@libs/DeviceCapabilities/canUseTouchScreen';
15+
import {getTransactionPendingAction, isTransactionPendingDelete} from '@libs/TransactionUtils';
16+
import variables from '@styles/variables';
17+
import CONST from '@src/CONST';
18+
import type {TransactionWithOptionalHighlight} from './MoneyRequestReportTransactionList';
19+
20+
const allReportColumns = [
21+
CONST.REPORT.TRANSACTION_LIST.COLUMNS.RECEIPT,
22+
CONST.REPORT.TRANSACTION_LIST.COLUMNS.TYPE,
23+
CONST.REPORT.TRANSACTION_LIST.COLUMNS.DATE,
24+
CONST.REPORT.TRANSACTION_LIST.COLUMNS.MERCHANT,
25+
CONST.REPORT.TRANSACTION_LIST.COLUMNS.CATEGORY,
26+
CONST.REPORT.TRANSACTION_LIST.COLUMNS.TAG,
27+
CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS,
28+
CONST.REPORT.TRANSACTION_LIST.COLUMNS.TOTAL_AMOUNT,
29+
];
30+
31+
type MoneyRequestReportTransactionItemProps = {
32+
/** The transaction that is being displayed */
33+
transaction: TransactionWithOptionalHighlight;
34+
35+
/** Whether the mobile selection mode is enabled */
36+
isSelectionModeEnabled: boolean;
37+
38+
/** Callback function triggered upon pressing a transaction checkbox. */
39+
toggleTransaction: (transactionID: string) => void;
40+
41+
/** Callback function triggered upon pressing a transaction. */
42+
handleOnPress: (transactionID: string) => void;
43+
44+
/** Callback function triggered upon long pressing a transaction. */
45+
handleLongPress: (transactionID: string) => void;
46+
47+
/** Whether the transaction is selected */
48+
isSelected: boolean;
49+
50+
/** The size of the date column */
51+
dateColumnSize: TableColumnSize;
52+
53+
/** The size of the amount column */
54+
amountColumnSize: TableColumnSize;
55+
56+
/** The size of the tax amount column */
57+
taxAmountColumnSize: TableColumnSize;
58+
59+
/** Callback function that scrolls to this transaction in case it is newly added */
60+
scrollToNewTransaction?: (offset: number) => void;
61+
};
62+
63+
function MoneyRequestReportTransactionItem({
64+
transaction,
65+
isSelectionModeEnabled,
66+
toggleTransaction,
67+
isSelected,
68+
handleOnPress,
69+
handleLongPress,
70+
dateColumnSize,
71+
amountColumnSize,
72+
taxAmountColumnSize,
73+
scrollToNewTransaction,
74+
}: MoneyRequestReportTransactionItemProps) {
75+
const {translate} = useLocalize();
76+
const styles = useThemeStyles();
77+
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
78+
const {isSmallScreenWidth, isMediumScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout();
79+
const theme = useTheme();
80+
const isPendingDelete = isTransactionPendingDelete(transaction);
81+
const pendingAction = getTransactionPendingAction(transaction);
82+
83+
const viewRef = useRef<View>(null);
84+
85+
// This useEffect scrolls to this transaction when it is newly added to the report
86+
useEffect(() => {
87+
if (!transaction.shouldBeHighlighted || !scrollToNewTransaction) {
88+
return;
89+
}
90+
viewRef?.current?.measure((x, y, width, height, pageX, pageY) => {
91+
scrollToNewTransaction?.(pageY);
92+
});
93+
}, [scrollToNewTransaction, transaction.shouldBeHighlighted]);
94+
95+
const animatedHighlightStyle = useAnimatedHighlightStyle({
96+
borderRadius: variables.componentBorderRadius,
97+
shouldHighlight: transaction.shouldBeHighlighted ?? false,
98+
highlightColor: theme.messageHighlightBG,
99+
backgroundColor: theme.highlightBG,
100+
});
101+
102+
return (
103+
<OfflineWithFeedback pendingAction={pendingAction}>
104+
<PressableWithFeedback
105+
key={transaction.transactionID}
106+
onPress={() => {
107+
handleOnPress(transaction.transactionID);
108+
}}
109+
accessibilityLabel={translate('iou.viewDetails')}
110+
role={getButtonRole(true)}
111+
isNested
112+
id={transaction.transactionID}
113+
style={[styles.transactionListItemStyle]}
114+
hoverStyle={[!isPendingDelete && styles.hoveredComponentBG, isSelected && styles.activeComponentBG]}
115+
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
116+
onPressIn={() => canUseTouchScreen() && ControlSelection.block()}
117+
onPressOut={() => ControlSelection.unblock()}
118+
onLongPress={() => {
119+
handleLongPress(transaction.transactionID);
120+
}}
121+
disabled={isTransactionPendingDelete(transaction)}
122+
ref={viewRef}
123+
wrapperStyle={[animatedHighlightStyle, styles.userSelectNone]}
124+
>
125+
<TransactionItemRow
126+
transactionItem={transaction}
127+
isSelected={isSelected}
128+
dateColumnSize={dateColumnSize}
129+
amountColumnSize={amountColumnSize}
130+
taxAmountColumnSize={taxAmountColumnSize}
131+
shouldShowTooltip
132+
shouldUseNarrowLayout={shouldUseNarrowLayout || isMediumScreenWidth}
133+
shouldShowCheckbox={!!isSelectionModeEnabled || !isSmallScreenWidth}
134+
onCheckboxPress={toggleTransaction}
135+
columns={allReportColumns}
136+
isDisabled={isPendingDelete}
137+
/>
138+
</PressableWithFeedback>
139+
</OfflineWithFeedback>
140+
);
141+
}
142+
143+
MoneyRequestReportTransactionItem.displayName = 'MoneyRequestReportTransactionItem';
144+
145+
export default MoneyRequestReportTransactionItem;

0 commit comments

Comments
 (0)