Skip to content
Merged
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
47 changes: 21 additions & 26 deletions src/pages/home/RecentlyAddedSection/useRecentlyAddedData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type {Report, ReportAction, Transaction} from '@src/types/onyx';
import type {PendingAction} from '@src/types/onyx/OnyxCommon';

import type {OnyxCollection} from 'react-native-onyx';

import {useIsFocused} from '@react-navigation/native';
import {useEffect, useEffectEvent, useMemo, useState} from 'react';

Expand Down Expand Up @@ -49,6 +51,14 @@ type RecentlyAddedExpense = {
report?: Report;
};

/** Selecting inside the subscription scans the (very large) collection once per update rather than once per render. */
const pendingAddTransactionIDsSelector = (transactions: OnyxCollection<Transaction>): string[] =>
Object.values(transactions ?? {})
.filter((transaction): transaction is Transaction => transaction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)
.map((transaction) => transaction.transactionID);

const getLocalTransaction = (localTransactions: OnyxCollection<Transaction>, transactionID: string) => localTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];

/**
* Returns the signed-in user's most recently added expenses, ordered by insertion timestamp (most recent first)
* and capped at CONST.HOME.SECTION_VISIBLE_LIMIT. Ordering is independent of the expense date.
Expand Down Expand Up @@ -83,20 +93,9 @@ function useRecentlyAddedData(): {transactions: RecentlyAddedExpense[]} {
const hash = queryJSON?.hash;

const [searchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`);
// Read by key only, never iterated: the collection holds tens of thousands of entries.
const [localTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);

// Maps transactionID -> local `transactions_` copy. When present it carries the freshest optimistic state
// (edited values and `pendingFields`) for an offline edit, which the server-backed snapshot doesn't yet reflect.
const localTransactionByID = useMemo(() => {
const map = new Map<string, Transaction>();
for (const [key, transaction] of Object.entries(localTransactions ?? {})) {
if (!transaction) {
continue;
}
map.set(transaction.transactionID ?? key.slice(ONYXKEYS.COLLECTION.TRANSACTION.length), transaction);
}
return map;
}, [localTransactions]);
const [pendingAddTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {selector: pendingAddTransactionIDsSelector});

// Holding a just-created expense here keeps it in the slot after `pendingAction` clears on sync but before the
// refreshed snapshot arrives (otherwise it briefly disappears and reappears).
Expand Down Expand Up @@ -168,23 +167,19 @@ function useRecentlyAddedData(): {transactions: RecentlyAddedExpense[]} {
// Merge in locally-pending expenses, skipping any already in the snapshot so a row never appears twice.
// A local optimistic ADD always belongs to the current user, so no ownership check is needed (unlike the snapshot path).
const snapshotTransactionIDs = new Set(snapshotTransactions.map((transaction) => transaction.transactionID));
const pendingAddIDs = Object.values(localTransactions ?? {}).reduce<string[]>((ids, transaction) => {
if (transaction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) {
ids.push(transaction.transactionID);
}
return ids;
}, []);
const nextUnconfirmed = new Set([...unconfirmedTransactionIDs, ...pendingAddIDs].filter((transactionID) => !snapshotTransactionIDs.has(transactionID)));
const nextUnconfirmed = new Set([...unconfirmedTransactionIDs, ...(pendingAddTransactionIDs ?? [])].filter((transactionID) => !snapshotTransactionIDs.has(transactionID)));
const combined = [
...filtered,
...Object.values(localTransactions ?? {}).filter(
(transaction): transaction is Transaction & {reportID: string} => !!transaction?.reportID && nextUnconfirmed.has(transaction.transactionID),
),
// Resolved by key, not from `pendingAddTransactionIDs`: an ID held over from `unconfirmedTransactionIDs` may
// have had its `pendingAction` cleared by a sync.
...[...nextUnconfirmed]
.map((transactionID) => getLocalTransaction(localTransactions, transactionID))
.filter((transaction): transaction is Transaction & {reportID: string} => !!transaction?.reportID),
]
// When an expense is split, its (local) copy is reassigned to the synthetic SPLIT_REPORT_ID and the
// resulting split children are added as new expenses. Drop the now-orphaned original so the slot shows
// only the splits. Prefer the local copy's reportID, which reflects the split even before the snapshot refreshes.
.filter((transaction) => (localTransactionByID.get(transaction.transactionID)?.reportID ?? transaction.reportID) !== CONST.REPORT.SPLIT_REPORT_ID);
.filter((transaction) => (getLocalTransaction(localTransactions, transaction.transactionID)?.reportID ?? transaction.reportID) !== CONST.REPORT.SPLIT_REPORT_ID);

// Order by the transaction's `inserted` timestamp (the immutable insertion time), most recent first.
const transactionsList = combined
Expand All @@ -201,7 +196,7 @@ function useRecentlyAddedData(): {transactions: RecentlyAddedExpense[]} {
// An offline edit only mutates the local `transactions_` copy (updated values + `pendingFields`); the
// snapshot keeps the stale, pre-edit copy. Prefer the local copy when present so the row reflects the
// edit and can render the offline pending treatment, matching how the Search transaction list behaves.
const sourceTransaction = localTransactionByID.get(transaction.transactionID) ?? transaction;
const sourceTransaction = getLocalTransaction(localTransactions, transaction.transactionID) ?? transaction;
const reportType = reportByReportID.get(transaction.reportID)?.type;
const isFromExpenseReport = reportType === CONST.REPORT.TYPE.EXPENSE;
// Self-DM and unreported (tracked) expenses support signed amounts like expense reports, so their
Expand All @@ -228,7 +223,7 @@ function useRecentlyAddedData(): {transactions: RecentlyAddedExpense[]} {
});

return {transactions: transactionsList, nextUnconfirmedTransactionIDs: nextUnconfirmed};
}, [snapshotData, unconfirmedTransactionIDs, accountID, localTransactions, localTransactionByID, translate]);
}, [snapshotData, unconfirmedTransactionIDs, accountID, localTransactions, pendingAddTransactionIDs, translate]);

const hasSameUnconfirmedIDs =
nextUnconfirmedTransactionIDs.size === unconfirmedTransactionIDs.size && [...nextUnconfirmedTransactionIDs].every((id) => unconfirmedTransactionIDs.has(id));
Expand Down
Loading