Skip to content

Commit

Permalink
Merge pull request Expensify#55297 from callstack-internal/perf/creat…
Browse files Browse the repository at this point in the history
…e-map-transactions

perf: create report -> transactions map to optimize performance
  • Loading branch information
carlosmiceli authored Jan 22, 2025
2 parents 4a0d4e6 + 596e731 commit 523edfe
Show file tree
Hide file tree
Showing 10 changed files with 249 additions and 171 deletions.
20 changes: 11 additions & 9 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ import {
isCurrentUserSubmitter,
isInvoiceReport,
navigateBackOnDeleteTransaction,
reportTransactionsSelector,
} from '@libs/ReportUtils';
import {
allHavePendingRTERViolation,
getAllReportTransactions,
isDuplicate as isDuplicateTransactionUtils,
isExpensifyCardTransaction,
isOnHold as isOnHoldTransactionUtils,
Expand Down Expand Up @@ -115,12 +115,13 @@ 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) => reportTransactionsSelector(_transactions, moneyRequestReport?.reportID),
initialValue: [],
});
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${isMoneyRequestAction(requestParentReportAction) && 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}${isMoneyRequestAction(requestParentReportAction) && getOriginalMessage(requestParentReportAction)?.IOUTransactionID}`] ??
undefined;

const styles = useThemeStyles();
const theme = useTheme();
Expand All @@ -139,15 +140,16 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false);
const [paymentType, setPaymentType] = useState<PaymentMethodType>();
const [requestType, setRequestType] = useState<ActionHandledType>();
const allTransactions = useMemo(() => getAllReportTransactions(moneyRequestReport?.reportID, transactions), [moneyRequestReport?.reportID, transactions]);
const canAllowSettlement = hasUpdatedTotal(moneyRequestReport, policy);
const policyType = policy?.type;
const connectedIntegration = getConnectedIntegration(policy);
const navigateBackToAfterDelete = useRef<Route>();
const hasHeldExpenses = hasHeldExpensesReportUtils(moneyRequestReport?.reportID);
const hasScanningReceipt = getTransactionsWithReceipts(moneyRequestReport?.reportID).some((t) => isReceiptBeingScanned(t));
const hasOnlyPendingTransactions = allTransactions.length > 0 && allTransactions.every((t) => isExpensifyCardTransaction(t) && isPending(t));
const transactionIDs = allTransactions.map((t) => t.transactionID) ?? [];
const hasOnlyPendingTransactions = useMemo(() => {
return !!transactions && transactions.length > 0 && transactions.every((t) => isExpensifyCardTransaction(t) && isPending(t));
}, [transactions]);
const transactionIDs = transactions?.map((t) => t.transactionID) ?? [];
const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDs);
const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationTransactionUtils(transactionIDs, moneyRequestReport, policy);
const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(moneyRequestReport?.reportID);
Expand Down Expand Up @@ -502,7 +504,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 @@ -64,10 +64,10 @@ import {
isReportApproved,
isReportOwner,
isSettled,
reportTransactionsSelector,
} from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
import {
getAllReportTransactions,
getDescription,
getMerchant,
getTransactionViolations,
Expand Down Expand Up @@ -143,7 +143,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 @@ -157,7 +160,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 @@ -179,8 +181,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 @@ -217,7 +219,7 @@ function ReportPreview({
const isInvoiceRoom = isInvoiceRoomReportUtils(chatReport);

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 @@ -232,13 +234,14 @@ 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 transactionIDList = [allTransactions.at(0)?.transactionID].filter((transactionID): transactionID is string => transactionID !== undefined);
const showRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(lastTransaction, getTransactionViolations(lastTransaction?.transactionID, transactionViolations));
const transactionIDList = [lastTransaction?.transactionID].filter((transactionID): transactionID is string => transactionID !== undefined);
const shouldShowBrokenConnectionViolation = numberOfRequests === 1 && shouldShowBrokenConnectionViolationTransactionUtils(transactionIDList, iouReport, policy);
let formattedMerchant = numberOfRequests === 1 ? getMerchant(allTransactions.at(0)) : null;
const formattedDescription = numberOfRequests === 1 ? getDescription(allTransactions.at(0)) : null;
let formattedMerchant = numberOfRequests === 1 ? getMerchant(lastTransaction) : null;
const formattedDescription = numberOfRequests === 1 ? getDescription(lastTransaction) : null;

if (isPartialMerchant(formattedMerchant ?? '')) {
formattedMerchant = null;
Expand Down Expand Up @@ -423,7 +426,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 @@ -545,7 +548,7 @@ function ReportPreview({
{lastThreeReceipts.length > 0 && (
<ReportActionItemImages
images={lastThreeReceipts}
total={allTransactions.length}
total={numberOfRequests}
size={CONST.RECEIPT.MAX_REPORT_PREVIEW_RECEIPTS}
/>
)}
Expand Down
15 changes: 8 additions & 7 deletions src/libs/IOUUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ 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 {getCurrencyUnit} from './CurrencyUtils';
import DateUtils from './DateUtils';
import Navigation from './Navigation/Navigation';
import * as TransactionUtils from './TransactionUtils';
import {getReportTransactions} from './ReportUtils';
import {getCurrency, getTagArrayFromName} from './TransactionUtils';

let lastLocationPermissionPrompt: string;
Onyx.connect({
Expand Down Expand Up @@ -47,7 +48,7 @@ function navigateToStartMoneyRequestStep(requestType: IOURequestType, iouType: I
function calculateAmount(numberOfParticipants: number, total: number, currency: string, isDefaultUser = false): number {
// Since the backend can maximum store 2 decimal places, any currency with more than 2 decimals
// has to be capped to 2 decimal places
const currencyUnit = Math.min(100, CurrencyUtils.getCurrencyUnit(currency));
const currencyUnit = Math.min(100, getCurrencyUnit(currency));
const totalInCurrencySubunit = (total / 100) * currencyUnit;
const totalParticipants = numberOfParticipants + 1;
const amountPerPerson = Math.round(totalInCurrencySubunit / totalParticipants);
Expand Down Expand Up @@ -118,8 +119,8 @@ 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 pendingRequestsInDifferentCurrency = reportTransactions.filter((transaction) => transaction.pendingAction && TransactionUtils.getCurrency(transaction) !== iouReport.currency);
const reportTransactions = getReportTransactions(iouReport.reportID);
const pendingRequestsInDifferentCurrency = reportTransactions.filter((transaction) => transaction.pendingAction && getCurrency(transaction) !== iouReport.currency);
return pendingRequestsInDifferentCurrency.length > 0;
}

Expand Down Expand Up @@ -150,7 +151,7 @@ function isValidMoneyRequestType(iouType: string): boolean {
* @returns
*/
function insertTagIntoTransactionTagsString(transactionTags: string, tag: string, tagIndex: number): string {
const tagArray = TransactionUtils.getTagArrayFromName(transactionTags);
const tagArray = getTagArrayFromName(transactionTags);
tagArray[tagIndex] = tag;

while (tagArray.length > 0 && !tagArray.at(-1)) {
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 @@ -900,6 +899,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 @@ -1895,7 +1902,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 @@ -2992,7 +2999,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 @@ -3736,7 +3743,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 @@ -8975,6 +8982,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 @@ -69,6 +69,7 @@ type BuildOptimisticTransactionParams = {
};

let allTransactions: OnyxCollection<Transaction> = {};

Onyx.connect({
key: ONYXKEYS.COLLECTION.TRANSACTION,
waitForCollectionCallback: true,
Expand Down Expand Up @@ -836,13 +837,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 @@ -1359,7 +1353,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 @@ -1407,7 +1401,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 @@ -107,6 +107,7 @@ import {
getPersonalDetailsForAccountID,
getReportNameValuePairs,
getReportOrDraftReport,
getReportTransactions,
getTransactionDetails,
hasHeldExpenses as hasHeldExpensesReportUtils,
hasNonReimbursableTransactions as hasNonReimbursableTransactionsReportUtils,
Expand Down Expand Up @@ -135,7 +136,6 @@ import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
import {
allHavePendingRTERViolation,
buildOptimisticTransaction,
getAllReportTransactions,
getAmount,
getCategoryTaxCodeAndAmount,
getCurrency,
Expand Down Expand Up @@ -7634,7 +7634,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 @@ -7758,7 +7758,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 @@ -75,7 +75,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 @@ -2588,7 +2587,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
Loading

0 comments on commit 523edfe

Please sign in to comment.