From 89d94cc760cb5f8f5980f0b0cc57ba53cd312112 Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Mon, 6 Jul 2026 12:59:22 +0200 Subject: [PATCH 1/2] Sort Recently added expenses by snapshot's "inserted" field only --- .../useRecentlyAddedData.ts | 34 ++++--------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/src/pages/home/RecentlyAddedSection/useRecentlyAddedData.ts b/src/pages/home/RecentlyAddedSection/useRecentlyAddedData.ts index 06e3517e5ad0..a095e14b5db9 100644 --- a/src/pages/home/RecentlyAddedSection/useRecentlyAddedData.ts +++ b/src/pages/home/RecentlyAddedSection/useRecentlyAddedData.ts @@ -71,22 +71,6 @@ function useRecentlyAddedData(): {transactions: RecentlyAddedExpense[]} { const [searchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); - // The Search snapshot omits each transaction's `inserted` timestamp, so recency ordering must come from the - // local `transactions_` collection, which carries `inserted` for expenses the user has recently added. - const [localTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); - - // Maps transactionID -> local `inserted` timestamp, used as the recency key the snapshot can't provide. - const insertedByTransactionID = useMemo(() => { - const map = new Map(); - for (const [key, transaction] of Object.entries(localTransactions ?? {})) { - const transactionID = transaction?.transactionID ?? key.slice(ONYXKEYS.COLLECTION.TRANSACTION.length); - if (transaction?.inserted) { - map.set(transactionID, transaction.inserted); - } - } - return map; - }, [localTransactions]); - const fireSearch = useEffectEvent(() => { if (isOffline || !queryJSON) { return; @@ -153,19 +137,15 @@ function useRecentlyAddedData(): {transactions: RecentlyAddedExpense[]} { return ownerAccountID === undefined || ownerAccountID === accountID; }); - // Recency key: prefer the local `inserted` timestamp (full precision, present for recently added expenses), - // then any snapshot `inserted`, then fall back to the expense date. Newest first. - const getRecencyKey = (transaction: Transaction & {reportID: string}) => - insertedByTransactionID.get(transaction.transactionID) ?? transaction.inserted ?? getCreated(transaction) ?? ''; - + // Order by the snapshot's `inserted` timestamp (the immutable insertion time), most recent first. return filtered .sort((firstTransaction, secondTransaction) => { - const firstKey = getRecencyKey(firstTransaction); - const secondKey = getRecencyKey(secondTransaction); - if (firstKey === secondKey) { - return 0; + const firstInserted = firstTransaction.inserted ?? ''; + const secondInserted = secondTransaction.inserted ?? ''; + if (firstInserted !== secondInserted) { + return firstInserted < secondInserted ? 1 : -1; } - return firstKey < secondKey ? 1 : -1; + return firstTransaction.transactionID < secondTransaction.transactionID ? 1 : -1; }) .slice(0, CONST.HOME.SECTION_VISIBLE_LIMIT) .map((transaction) => { @@ -190,7 +170,7 @@ function useRecentlyAddedData(): {transactions: RecentlyAddedExpense[]} { transaction, }; }); - }, [snapshotData, accountID, insertedByTransactionID, translate]); + }, [snapshotData, accountID, translate]); return {transactions}; } From e6f8d636831c635a3a0acdb6a75f7599ef15e0eb Mon Sep 17 00:00:00 2001 From: mhawryluk Date: Mon, 6 Jul 2026 15:32:13 +0200 Subject: [PATCH 2/2] Update unit tests --- .../useRecentlyAddedDataTest.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/unit/HomePage/RecentlyAddedSection/useRecentlyAddedDataTest.ts b/tests/unit/HomePage/RecentlyAddedSection/useRecentlyAddedDataTest.ts index 35d688fe5494..28cd8fcc6e3b 100644 --- a/tests/unit/HomePage/RecentlyAddedSection/useRecentlyAddedDataTest.ts +++ b/tests/unit/HomePage/RecentlyAddedSection/useRecentlyAddedDataTest.ts @@ -4,7 +4,6 @@ * not the on-demand `transactions_` Onyx collection * - returns the current user's expenses, most recent first * - sorts strictly by the `inserted` (creation/insertion) timestamp, never by `created` (expense date); - * falls back to `created` only when `inserted` is missing * - caps the list at CONST.HOME.SECTION_VISIBLE_LIMIT (5) rows * - includes expenses regardless of report status (no recency-window / draft-only filter) * - defensively excludes expenses owned by another account when the snapshot carries the parent report @@ -140,19 +139,21 @@ describe('useRecentlyAddedData — ordering', () => { expect(resultTransactionIDs(result.current.transactions)).toEqual(['t3', 't2', 't1']); }); - it('falls back to the created date for ordering when an expense is missing its inserted timestamp', () => { + it('breaks ties between equal inserted timestamps deterministically by transactionID', () => { setupSnapshot( [ - makeTransaction({transactionID: 'withCreatedOnly', created: '2026-06-05', inserted: undefined}), - makeTransaction({transactionID: 'withInserted', created: '2026-01-01', inserted: '2026-06-04 10:00:00'}), + makeTransaction({transactionID: 'aaa', created: '2026-06-01', inserted: '2026-06-05 10:00:00'}), + makeTransaction({transactionID: 'ccc', created: '2026-06-09', inserted: '2026-06-05 10:00:00'}), + makeTransaction({transactionID: 'bbb', created: '2026-06-05', inserted: '2026-06-05 10:00:00'}), ], [makeReport('report_owned', ACCOUNT_ID)], ); const {result} = renderHook(() => useRecentlyAddedData()); - // withCreatedOnly has no inserted, so its created date (2026-06-05) is used and outranks withInserted (2026-06-04). - expect(resultTransactionIDs(result.current.transactions)).toEqual(['withCreatedOnly', 'withInserted']); + // Equal `inserted` timestamps tie-break on transactionID (descending) rather than the differing created dates, + // giving a stable order that never silently reshuffles across renders. + expect(resultTransactionIDs(result.current.transactions)).toEqual(['ccc', 'bbb', 'aaa']); }); it('ranks an old-dated expense first when it was inserted most recently', () => {