Skip to content

perf: create report -> transactions map to optimize performance #55297

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 2 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
27 changes: 16 additions & 11 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,17 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
}
return reportActions.find((action): action is OnyxTypes.ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU> => action.reportActionID === transactionThreadReport.parentReportActionID);
}, [reportActions, transactionThreadReport?.parentReportActionID]);
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
selector: (_transactions) => ReportUtils.reportTransactionsSelector(_transactions, moneyRequestReport?.reportID),
initialValue: [],
});
const [transaction] = useOnyx(
`${ONYXKEYS.COLLECTION.TRANSACTION}${
ReportActionsUtils.isMoneyRequestAction(requestParentReportAction) && ReportActionsUtils.getOriginalMessage(requestParentReportAction)?.IOUTransactionID
}`,
);
const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true});
const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult);
const transaction =
transactions?.[
`${ONYXKEYS.COLLECTION.TRANSACTION}${
ReportActionsUtils.isMoneyRequestAction(requestParentReportAction) && ReportActionsUtils.getOriginalMessage(requestParentReportAction)?.IOUTransactionID
}`
] ?? undefined;

const styles = useThemeStyles();
const theme = useTheme();
Expand All @@ -105,16 +107,19 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false);
const [paymentType, setPaymentType] = useState<PaymentMethodType>();
const [requestType, setRequestType] = useState<ActionHandledType>();
const allTransactions = useMemo(() => TransactionUtils.getAllReportTransactions(moneyRequestReport?.reportID, transactions), [moneyRequestReport?.reportID, transactions]);
const canAllowSettlement = ReportUtils.hasUpdatedTotal(moneyRequestReport, policy);
const policyType = policy?.type;
const isDraft = ReportUtils.isOpenExpenseReport(moneyRequestReport);
const connectedIntegration = PolicyUtils.getConnectedIntegration(policy);
const navigateBackToAfterDelete = useRef<Route>();
const hasHeldExpenses = ReportUtils.hasHeldExpenses(moneyRequestReport?.reportID);
const hasScanningReceipt = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).some((t) => TransactionUtils.isReceiptBeingScanned(t));
const hasOnlyPendingTransactions = allTransactions.length > 0 && allTransactions.every((t) => TransactionUtils.isExpensifyCardTransaction(t) && TransactionUtils.isPending(t));
const transactionIDs = allTransactions.map((t) => t.transactionID);
const hasOnlyPendingTransactions =
transactions?.some((t) => {
Copy link
Contributor

@adhorodyski adhorodyski Jan 17, 2025

Choose a reason for hiding this comment

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

@rayane-djouah we have to triple check if this is logically the same as before - should now bail out earlier though because of the some() loop whenever possible.

const isPending = TransactionUtils.isExpensifyCardTransaction(t) && TransactionUtils.isPending(t);
return !isPending;
}) ?? false;
const transactionIDs = transactions?.map((t) => t.transactionID);
const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID]);
const shouldShowBrokenConnectionViolation = TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID, moneyRequestReport, policy);
const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(moneyRequestReport?.reportID);
Expand Down Expand Up @@ -494,7 +499,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
paymentType={paymentType}
chatReport={chatReport}
moneyRequestReport={moneyRequestReport}
transactionCount={transactionIDs.length}
transactionCount={transactionIDs?.length ?? 0}
/>
)}
<DelegateNoAccessModal
Expand Down
29 changes: 16 additions & 13 deletions src/components/ReportActionItem/ReportPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ import {
isReportApproved,
isReportOwner,
isSettled,
reportTransactionsSelector,
} from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
import {
getAllReportTransactions,
getDescription,
getMerchant,
getTransactionViolations,
Expand Down Expand Up @@ -145,7 +145,10 @@ function ReportPreview({
const policy = usePolicy(policyID);
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`);
const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`);
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
selector: (_transactions) => reportTransactionsSelector(_transactions, iouReportID),
initialValue: [],
});
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET);
const [invoiceReceiverPolicy] = useOnyx(
Expand All @@ -155,7 +158,6 @@ function ReportPreview({
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
const allTransactions = useMemo(() => getAllReportTransactions(iouReportID, transactions), [iouReportID, transactions]);

const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyTransactionsWithPendingRoutes, hasNonReimbursableTransactions} = useMemo(
() => ({
Expand All @@ -177,8 +179,8 @@ function ReportPreview({

const getCanIOUBePaid = useCallback(
(onlyShowPayElsewhere = false, shouldCheckApprovedState = true) =>
canIOUBePaidIOUActions(iouReport, chatReport, policy, allTransactions, onlyShowPayElsewhere, undefined, undefined, shouldCheckApprovedState),
[iouReport, chatReport, policy, allTransactions],
canIOUBePaidIOUActions(iouReport, chatReport, policy, transactions, onlyShowPayElsewhere, undefined, undefined, shouldCheckApprovedState),
[iouReport, chatReport, policy, transactions],
);

const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]);
Expand Down Expand Up @@ -216,7 +218,7 @@ function ReportPreview({
const isOpenExpenseReport = isPolicyExpenseChat && isOpenExpenseReportUtils(iouReport);

const canAllowSettlement = hasUpdatedTotal(iouReport, policy);
const numberOfRequests = allTransactions.length;
const numberOfRequests = transactions?.length ?? 0;
const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID);
const numberOfScanningReceipts = transactionsWithReceipts.filter((transaction) => isReceiptBeingScanned(transaction)).length;
const numberOfPendingRequests = transactionsWithReceipts.filter((transaction) => isPending(transaction) && isCardTransaction(transaction)).length;
Expand All @@ -231,12 +233,13 @@ function ReportPreview({
hasWarningTypeViolations(iouReportID, transactionViolations, true) ||
(isReportOwner(iouReport) && hasReportViolations(iouReportID)) ||
hasActionsWithErrors(iouReportID);
const lastThreeTransactions = allTransactions.slice(-3);
const lastThreeTransactions = transactions?.slice(-3) ?? [];
const lastTransaction = transactions?.at(0);
const lastThreeReceipts = lastThreeTransactions.map((transaction) => ({...getThumbnailAndImageURIs(transaction), transaction}));
const showRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(allTransactions.at(0), getTransactionViolations(allTransactions.at(0)?.transactionID, transactionViolations));
const shouldShowBrokenConnectionViolation = numberOfRequests === 1 && shouldShowBrokenConnectionViolationTransactionUtils(allTransactions.at(0)?.transactionID, iouReport, policy);
let formattedMerchant = numberOfRequests === 1 ? getMerchant(allTransactions.at(0)) : null;
const formattedDescription = numberOfRequests === 1 ? getDescription(allTransactions.at(0)) : null;
const showRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(lastTransaction, getTransactionViolations(lastTransaction?.transactionID, transactionViolations));
const shouldShowBrokenConnectionViolation = numberOfRequests === 1 && shouldShowBrokenConnectionViolationTransactionUtils(lastTransaction?.transactionID, iouReport, policy);
let formattedMerchant = numberOfRequests === 1 ? getMerchant(lastTransaction) : null;
const formattedDescription = numberOfRequests === 1 ? getDescription(lastTransaction) : null;

if (isPartialMerchant(formattedMerchant ?? '')) {
formattedMerchant = null;
Expand Down Expand Up @@ -427,7 +430,7 @@ function ReportPreview({
const shouldShowScanningSubtitle = (numberOfScanningReceipts === 1 && numberOfRequests === 1) || (numberOfScanningReceipts >= 1 && Number(nonHeldAmount) === 0);
const shouldShowPendingSubtitle = numberOfPendingRequests === 1 && numberOfRequests === 1;

const isPayAtEndExpense = isPayAtEndExpenseReport(iouReportID, allTransactions);
const isPayAtEndExpense = isPayAtEndExpenseReport(iouReportID, transactions);
const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, {selector: getArchiveReason});

const getPendingMessageProps: () => PendingMessageProps = () => {
Expand Down Expand Up @@ -549,7 +552,7 @@ function ReportPreview({
{lastThreeReceipts.length > 0 && (
<ReportActionItemImages
images={lastThreeReceipts}
total={allTransactions.length}
total={numberOfRequests}
size={CONST.RECEIPT.MAX_REPORT_PREVIEW_RECEIPTS}
/>
)}
Expand Down
5 changes: 3 additions & 2 deletions src/libs/IOUUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import type {IOUAction, IOUType} from '@src/CONST';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {OnyxInputOrEntry, PersonalDetails, Report, Transaction} from '@src/types/onyx';
import type {OnyxInputOrEntry, PersonalDetails, Report} from '@src/types/onyx';
import type {Attendee} from '@src/types/onyx/IOU';
import type {IOURequestType} from './actions/IOU';
import * as CurrencyUtils from './CurrencyUtils';
import DateUtils from './DateUtils';
import Navigation from './Navigation/Navigation';
import {getReportTransactions} from './ReportUtils';
import * as TransactionUtils from './TransactionUtils';

let lastLocationPermissionPrompt: string;
Expand Down Expand Up @@ -118,7 +119,7 @@ function updateIOUOwnerAndTotal<TReport extends OnyxInputOrEntry<Report>>(
* that are either created or cancelled offline, and thus haven't been converted to the report's currency yet
*/
function isIOUReportPendingCurrencyConversion(iouReport: Report): boolean {
const reportTransactions: Transaction[] = TransactionUtils.getAllReportTransactions(iouReport.reportID);
const reportTransactions = getReportTransactions(iouReport.reportID);
const pendingRequestsInDifferentCurrency = reportTransactions.filter((transaction) => transaction.pendingAction && TransactionUtils.getCurrency(transaction) !== iouReport.currency);
return pendingRequestsInDifferentCurrency.length > 0;
}
Expand Down
17 changes: 13 additions & 4 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ import {
wasActionTakenByCurrentUser,
} from './ReportActionsUtils';
import {
getAllReportTransactions,
getAttendees,
getBillable,
getCardID,
Expand Down Expand Up @@ -896,6 +895,14 @@ function getReportOrDraftReport(reportID: string | undefined): OnyxEntry<Report>
return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? allReportsDraft?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`];
}

function reportTransactionsSelector(transactions: OnyxCollection<Transaction>, reportID: string | undefined): Transaction[] {
if (!transactions || !reportID) {
return [];
}

return Object.values(transactions).filter((transaction): transaction is Transaction => !!transaction && transaction.reportID === reportID);
}

function getReportTransactions(reportID: string | undefined): Transaction[] {
if (!reportID) {
return [];
Expand Down Expand Up @@ -1891,7 +1898,7 @@ function isPayAtEndExpenseReport(reportID: string | undefined, transactions: Tra
return false;
}

return isPayAtEndExpense(transactions?.[0] ?? getAllReportTransactions(reportID).at(0));
return isPayAtEndExpense(transactions?.[0] ?? getReportTransactions(reportID).at(0));
}

/**
Expand Down Expand Up @@ -2988,7 +2995,7 @@ function getReasonAndReportActionThatRequiresAttention(

const iouReportActionToApproveOrPay = getIOUReportActionToApproveOrPay(optionOrReport, optionOrReport.reportID);
const iouReportID = getIOUReportIDFromReportActionPreview(iouReportActionToApproveOrPay);
const transactions = getAllReportTransactions(iouReportID);
const transactions = getReportTransactions(iouReportID);
const hasOnlyPendingTransactions = transactions.length > 0 && transactions.every((t) => isExpensifyCardTransaction(t) && isPending(t));

// Has a child report that is awaiting action (e.g. approve, pay, add bank account) from current user
Expand Down Expand Up @@ -3715,7 +3722,7 @@ function getReportPreviewMessage(
return reportActionMessage;
}

const allReportTransactions = getAllReportTransactions(report.reportID);
const allReportTransactions = getReportTransactions(report.reportID);
const transactionsWithReceipts = allReportTransactions.filter(hasReceiptTransactionUtils);
const numberOfScanningReceipts = transactionsWithReceipts.filter(isReceiptBeingScanned).length;

Expand Down Expand Up @@ -8943,6 +8950,8 @@ export {
getReportFieldKey,
getReportIDFromLink,
getReportName,
getReportTransactions,
reportTransactionsSelector,
getReportNotificationPreference,
getReportOfflinePendingActionAndErrors,
getReportParticipantsTitle,
Expand Down
13 changes: 3 additions & 10 deletions src/libs/TransactionUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
isPolicyAdmin,
} from '@libs/PolicyUtils';
import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {isOpenExpenseReport, isProcessingReport, isReportApproved, isSettled, isThread} from '@libs/ReportUtils';
import {getReportTransactions, isOpenExpenseReport, isProcessingReport, isReportApproved, isSettled, isThread} from '@libs/ReportUtils';
import type {IOURequestType} from '@userActions/IOU';
import CONST from '@src/CONST';
import type {IOUType} from '@src/CONST';
Expand Down Expand Up @@ -68,6 +68,7 @@ type BuildOptimisticTransactionParams = {
};

let allTransactions: OnyxCollection<Transaction> = {};

Onyx.connect({
key: ONYXKEYS.COLLECTION.TRANSACTION,
waitForCollectionCallback: true,
Expand Down Expand Up @@ -817,13 +818,6 @@ function hasRoute(transaction: OnyxEntry<Transaction>, isDistanceRequestType?: b
return !!transaction?.routes?.route0?.geometry?.coordinates || (!!isDistanceRequestType && !!transaction?.comment?.customUnit?.quantity);
}

function getAllReportTransactions(reportID?: string, transactions?: OnyxCollection<Transaction>): Transaction[] {
const reportTransactions: Transaction[] = Object.values(transactions ?? allTransactions ?? {}).filter(
(transaction): transaction is Transaction => !!transaction && transaction.reportID === reportID,
);
return reportTransactions;
}

function waypointHasValidAddress(waypoint: RecentWaypoint | Waypoint): boolean {
return !!waypoint?.address?.trim();
}
Expand Down Expand Up @@ -1328,7 +1322,7 @@ function getCategoryTaxCodeAndAmount(category: string, transaction: OnyxEntry<Tr
* Return the sorted list transactions of an iou report
*/
function getAllSortedTransactions(iouReportID?: string): Array<OnyxEntry<Transaction>> {
return getAllReportTransactions(iouReportID).sort((transA, transB) => {
return getReportTransactions(iouReportID).sort((transA, transB) => {
if (transA.created < transB.created) {
return -1;
}
Expand Down Expand Up @@ -1376,7 +1370,6 @@ export {
getTagArrayFromName,
getTagForDisplay,
getTransactionViolations,
getAllReportTransactions,
hasReceipt,
hasEReceipt,
hasRoute,
Expand Down
6 changes: 3 additions & 3 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import {
getPersonalDetailsForAccountID,
getReportNameValuePairs,
getReportOrDraftReport,
getReportTransactions,
getTransactionDetails,
hasHeldExpenses as hasHeldExpensesReportUtils,
hasNonReimbursableTransactions as hasNonReimbursableTransactionsReportUtils,
Expand Down Expand Up @@ -132,7 +133,6 @@ import playSound, {SOUNDS} from '@libs/Sound';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
import {
buildOptimisticTransaction,
getAllReportTransactions,
getAmount,
getCategoryTaxCodeAndAmount,
getCurrency,
Expand Down Expand Up @@ -7234,7 +7234,7 @@ function getPayMoneyRequestParams(

// Optimistically unhold all transactions if we pay all requests
if (full) {
const reportTransactions = getAllReportTransactions(iouReport?.reportID);
const reportTransactions = getReportTransactions(iouReport?.reportID);
for (const transaction of reportTransactions) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
Expand Down Expand Up @@ -7358,7 +7358,7 @@ function canApproveIOU(
const reportNameValuePairs = chatReportRNVP ?? getReportNameValuePairs(iouReport?.reportID);
const isArchivedExpenseReport = isArchivedReport(iouReport, reportNameValuePairs);
let isTransactionBeingScanned = false;
const reportTransactions = getAllReportTransactions(iouReport?.reportID);
const reportTransactions = getReportTransactions(iouReport?.reportID);
for (const transaction of reportTransactions) {
const hasReceipt = hasReceiptTransactionUtils(transaction);
const isReceiptBeingScanned = isReceiptBeingScannedTransactionUtils(transaction);
Expand Down
3 changes: 1 addition & 2 deletions src/libs/actions/Policy/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ import * as PhoneNumber from '@libs/PhoneNumber';
import * as PolicyUtils from '@libs/PolicyUtils';
import {navigateWhenEnableFeature} from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
import {getAllReportTransactions} from '@libs/TransactionUtils';
import type {PolicySelector} from '@pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover';
import * as PaymentMethods from '@userActions/PaymentMethods';
import * as PersistedRequests from '@userActions/PersistedRequests';
Expand Down Expand Up @@ -2550,7 +2549,7 @@ function createWorkspaceFromIOUPayment(iouReport: OnyxEntry<Report>): WorkspaceF
});

// The expense report transactions need to have the amount reversed to negative values
const reportTransactions = getAllReportTransactions(iouReportID);
const reportTransactions = ReportUtils.getReportTransactions(iouReportID);

// For performance reasons, we are going to compose a merge collection data for transactions
const transactionsOptimisticData: Record<string, Transaction> = {};
Expand Down
2 changes: 1 addition & 1 deletion src/libs/migrations/TransactionBackupsToCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {Transaction} from '@src/types/onyx';
/**
* This migration moves all the transaction backups stored in the transaction collection, ONYXKEYS.COLLECTION.TRANSACTION, to a reserved collection that only
* stores draft transactions, ONYXKEYS.COLLECTION.TRANSACTION_DRAFT. The purpose of the migration is that there is a possibility that transaction backups are
* not filtered by most functions, e.g, getAllReportTransactions (src/libs/TransactionUtils/index.ts). One problem that arose from storing transaction backups with
* not filtered by most functions, e.g, getReportTransactions (src/libs/ReportUtils/index.ts). One problem that arose from storing transaction backups with
* the other transactions is that for every distance expense which have their waypoints updated offline, we expect the ReportPreview component to display the
* default image of a pending map. However, due to the presence of the transaction backup, the previous map image will be displayed alongside the pending map.
* The problem was further discussed in this PR. https://github.com/Expensify/App/pull/30232#issuecomment-178110172
Expand Down
Loading
Loading