From c9029d34c86ab9b13b24cc33bbd6c65b744866c8 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Mon, 16 Feb 2026 15:16:32 +0100 Subject: [PATCH 1/8] Refactor LHNOptionsList to use cached report IDs and improve action retrieval logic. Introduce new utility functions for finding last report actions and caching one-transaction thread report IDs. --- .../LHNOptionsList/LHNOptionsList.tsx | 32 ++------ src/libs/OptionsListUtils/index.ts | 34 ++++++--- src/libs/ReportActionsUtils.ts | 76 +++++++++++++++++++ 3 files changed, 105 insertions(+), 37 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 41047755eb59..71cf3c14110f 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -27,22 +27,14 @@ import useThemeStyles from '@hooks/useThemeStyles'; import getPlatform from '@libs/getPlatform'; import Log from '@libs/Log'; import {getMovedReportID} from '@libs/ModifiedExpenseMessage'; -import {getIOUReportIDOfLastAction, getLastMessageTextForReport} from '@libs/OptionsListUtils'; -import { - getOneTransactionThreadReportID, - getOriginalMessage, - getSortedReportActions, - getSortedReportActionsForDisplay, - isInviteOrRemovedAction, - isMoneyRequestAction, - shouldReportActionBeVisibleAsLastAction, -} from '@libs/ReportActionsUtils'; +import {getCachedOneTransactionThreadReportID, getIOUReportIDOfLastAction, getLastMessageTextForReport} from '@libs/OptionsListUtils'; +import {findLastReportActions, getOriginalMessage, isInviteOrRemovedAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction as canUserPerformWriteActionUtil} from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, Report, ReportAction} from '@src/types/onyx'; +import type {PersonalDetails, Report} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import OptionRowLHNData from './OptionRowLHNData'; import OptionRowRendererComponent from './OptionRowRendererComponent'; @@ -177,9 +169,8 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const reportID = item.reportID; const itemParentReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${item.parentReportID}`]; const itemReportNameValuePairs = reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`]; - const chatReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${item.chatReportID}`]; const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; - const itemOneTransactionThreadReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getOneTransactionThreadReportID(item, chatReport, itemReportActions, isOffline)}`]; + const itemOneTransactionThreadReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getCachedOneTransactionThreadReportID(reportID)}`]; const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${item?.parentReportID}`]; const itemParentReportAction = item?.parentReportActionID ? itemParentReportActions?.[item?.parentReportActionID] : undefined; const itemReportAttributes = reportAttributes?.[reportID]; @@ -207,8 +198,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const isReportArchived = !!itemReportNameValuePairs?.private_isArchived; const canUserPerformWrite = canUserPerformWriteActionUtil(item, isReportArchived); - const sortedReportActions = getSortedReportActionsForDisplay(itemReportActions, canUserPerformWrite); - const lastReportAction = sortedReportActions.at(0); + const {lastVisibleAction: lastReportAction, lastActionForDisplay: lastAction} = findLastReportActions(itemReportActions, canUserPerformWrite); // Get the transaction for the last report action const lastReportActionTransactionID = isMoneyRequestAction(lastReportAction) @@ -245,18 +235,6 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const shouldShowRBRorGBRTooltip = firstReportIDWithGBRorRBR === reportID; - let lastAction: ReportAction | undefined; - if (!itemReportActions || !item) { - lastAction = undefined; - } else { - const canUserPerformWriteAction = canUserPerformWriteActionUtil(item, isReportArchived); - const actionsArray = getSortedReportActions(Object.values(itemReportActions)); - const reportActionsForDisplay = actionsArray.filter( - (reportAction) => shouldReportActionBeVisibleAsLastAction(reportAction, canUserPerformWriteAction) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED, - ); - lastAction = reportActionsForDisplay.at(-1); - } - let lastActionReport: OnyxEntry | undefined; if (isInviteOrRemovedAction(lastAction)) { const lastActionOriginalMessage = lastAction?.actionName ? getOriginalMessage(lastAction) : null; diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 37db6ad9d467..c4ddc6b8089b 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -243,6 +243,8 @@ const lastReportActions: ReportActions = {}; const allSortedReportActions: Record = {}; let allReportActions: OnyxCollection; const lastVisibleReportActions: ReportActions = {}; +const lastVisibleIOUMoneyActions: ReportActions = {}; +const cachedOneTransactionThreadReportIDs: Record = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, waitForCollectionCallback: true, @@ -266,9 +268,10 @@ Onyx.connect({ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`]; - // If the report is a one-transaction report and has , we need to return the combined reportActions so that the LHN can display modifications - // to the transaction thread or the report itself + // If the report is a one-transaction report, we need to return the combined reportActions so that the LHN can display modifications + // to the transaction thread or the report itself. Cache the result to avoid O(n) recomputation in renderItem. const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, actions[reportActions[0]]); + cachedOneTransactionThreadReportIDs[reportID] = transactionThreadReportID; if (transactionThreadReportID) { const transactionThreadReportActionsArray = Object.values(actions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`] ?? {}); sortedReportActions = getCombinedReportActions(sortedReportActions, transactionThreadReportID, transactionThreadReportActionsArray, reportID); @@ -300,6 +303,19 @@ Onyx.connect({ continue; } lastVisibleReportActions[reportID] = reportActionForDisplay; + + // Pre-compute the first visible IOU money request action (newest, since sorted descending) + const iouMoneyAction = sortedReportActions.find( + (reportAction) => + shouldReportActionBeVisible(reportAction, reportAction.reportActionID, isWriteActionAllowed) && + reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && + isMoneyRequestAction(reportAction), + ); + if (iouMoneyAction) { + lastVisibleIOUMoneyActions[reportID] = iouMoneyAction; + } else { + delete lastVisibleIOUMoneyActions[reportID]; + } } }, }); @@ -550,6 +566,10 @@ function isSearchStringMatchUserDetails(personalDetail: PersonalDetails, searchV /** * Get IOU report ID of report last action if the action is report action preview */ +function getCachedOneTransactionThreadReportID(reportID: string | undefined): string | undefined { + return reportID ? cachedOneTransactionThreadReportIDs[reportID] : undefined; +} + function getIOUReportIDOfLastAction(report: OnyxEntry): string | undefined { if (!report?.reportID) { return; @@ -655,14 +675,7 @@ function getLastMessageTextForReport({ lastMessageTextFromReport = formatReportLastMessageText(Parser.htmlToText(properSchemaForMoneyRequestMessage)); } else if (isReportPreviewAction(lastReportAction)) { const iouReport = getReportOrDraftReport(getIOUReportIDFromReportActionPreview(lastReportAction)); - const lastIOUMoneyReportAction = iouReport?.reportID - ? allSortedReportActions[iouReport.reportID]?.find( - (reportAction, key): reportAction is ReportAction => - shouldReportActionBeVisible(reportAction, key, canUserPerformWriteAction(report, isReportArchived)) && - reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && - isMoneyRequestAction(reportAction), - ) - : undefined; + const lastIOUMoneyReportAction = iouReport?.reportID ? lastVisibleIOUMoneyActions[iouReport.reportID] : undefined; // For workspace chats, use the report title if (reportUtilsIsPolicyExpenseChat(report) && !isEmptyObject(iouReport)) { const reportName = computeReportName(iouReport, undefined, undefined, undefined, undefined, undefined, undefined, currentUserAccountID); @@ -3190,6 +3203,7 @@ export { getHeaderMessage, getHeaderMessageForNonUserList, getIOUConfirmationOptionsFromPayeePersonalDetail, + getCachedOneTransactionThreadReportID, getIOUReportIDOfLastAction, getIsUserSubmittedExpenseOrScannedReceipt, getLastActorDisplayName, diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index ef470867c662..fbd053e3758d 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1378,6 +1378,81 @@ function getSortedReportActionsForDisplay( return getSortedReportActions(withDEWRoutedActionsArray(baseURLAdjustedReportActions), true); } +/** + * Returns true if action `a` is newer than action `b`, + * matching getSortedReportActions order (descending by timestamp, with CREATED always oldest). + */ +function isNewerReportAction(a: ReportAction, b: ReportAction): boolean { + if (a.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { + return false; + } + if (b.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { + return true; + } + + // Undefined created is treated as oldest (mirrors getSortedReportActions which places undefined first in ascending order) + if (a.created === undefined && b.created !== undefined) { + return false; + } + if (a.created !== undefined && b.created === undefined) { + return true; + } + + if (a.created !== b.created) { + return a.created > b.created; + } + + if (a.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && b.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) { + return true; + } + if (b.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && a.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) { + return false; + } + + return a.reportActionID > b.reportActionID; +} + +/** + * Finds the newest report action matching each of two filter criteria in a single pass. + * Returns: + * - lastVisibleAction: newest visible action + * - lastActionForDisplay: newest displayable action (not CREATED) + */ +function findLastReportActions( + reportActions: OnyxEntry, + canUserPerformWriteAction?: boolean, +): { + lastVisibleAction: ReportAction | undefined; + lastActionForDisplay: ReportAction | undefined; +} { + if (!reportActions) { + return {lastVisibleAction: undefined, lastActionForDisplay: undefined}; + } + + let lastVisibleAction: ReportAction | undefined; + let lastActionForDisplay: ReportAction | undefined; + + for (const [key, action] of Object.entries(reportActions)) { + if (!action) { + continue; + } + + if (shouldReportActionBeVisible(action, key, canUserPerformWriteAction)) { + if (!lastVisibleAction || isNewerReportAction(action, lastVisibleAction)) { + lastVisibleAction = action; + } + } + + if (shouldReportActionBeVisibleAsLastAction(action, canUserPerformWriteAction) && action.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { + if (!lastActionForDisplay || isNewerReportAction(action, lastActionForDisplay)) { + lastActionForDisplay = action; + } + } + } + + return {lastVisibleAction, lastActionForDisplay}; +} + /** * The first visible action is the second last action in sortedReportActions which satisfy following conditions: * 1. That is not pending deletion as pending deletion actions are kept in sortedReportActions in memory. @@ -4325,6 +4400,7 @@ export { getCardIssuedMessage, getRemovedConnectionMessage, getActionableJoinRequestPendingReportAction, + findLastReportActions, getFilteredReportActionsForReportView, wasMessageReceivedWhileOffline, shouldShowAddMissingDetails, From 3971b517cecb0a35fa918ac1ef68ef64826beca3 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Tue, 17 Feb 2026 13:56:43 +0100 Subject: [PATCH 2/8] Refactor ReportActionsUtils and OptionsListUtils for improved type safety and code clarity. Updated function signatures to use optional parameters and refined comments for better understanding. --- src/libs/OptionsListUtils/index.ts | 4 ++-- src/libs/ReportActionsUtils.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 2e5bb8d33169..bf43500d9274 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -263,7 +263,7 @@ Onyx.connect({ const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`]; // If the report is a one-transaction report, we need to return the combined reportActions so that the LHN can display modifications - // to the transaction thread or the report itself. Cache the result to avoid O(n) recomputation in renderItem. + // to the transaction thread or the report itself. Cache the result to avoid recomputation in renderItem. const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, actions[reportActions[0]]); cachedOneTransactionThreadReportIDs[reportID] = transactionThreadReportID; if (transactionThreadReportID) { @@ -561,7 +561,7 @@ function isSearchStringMatchUserDetails(personalDetail: PersonalDetails, searchV /** * Get IOU report ID of report last action if the action is report action preview */ -function getCachedOneTransactionThreadReportID(reportID: string | undefined): string | undefined { +function getCachedOneTransactionThreadReportID(reportID?: string): string | undefined { return reportID ? cachedOneTransactionThreadReportIDs[reportID] : undefined; } diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index e9c73caef01d..5b0625acb6ac 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1421,8 +1421,8 @@ function findLastReportActions( reportActions: OnyxEntry, canUserPerformWriteAction?: boolean, ): { - lastVisibleAction: ReportAction | undefined; - lastActionForDisplay: ReportAction | undefined; + lastVisibleAction?: ReportAction; + lastActionForDisplay?: ReportAction; } { if (!reportActions) { return {lastVisibleAction: undefined, lastActionForDisplay: undefined}; From 08084c453f2276228b7fb7120e9ea0b6b9f7189a Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 18 Feb 2026 09:14:33 +0100 Subject: [PATCH 3/8] Refactor LHNOptionsList to utilize reportAttributes for one-transaction thread report ID. Remove cached function and improve report action visibility logic in OptionsListUtils. Add isNewerReportAction utility for comparing report actions based on timestamps. --- .../LHNOptionsList/LHNOptionsList.tsx | 6 +- src/libs/OptionsListUtils/index.ts | 59 ++++++++++--------- src/libs/ReportActionsUtils.ts | 1 + .../OnyxDerived/configs/reportAttributes.ts | 3 +- src/types/onyx/DerivedValues.ts | 4 ++ tests/unit/ReportActionsUtilsTest.ts | 55 +++++++++++++++++ 6 files changed, 96 insertions(+), 32 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 38183d1a45aa..d187557e5039 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -27,7 +27,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import getPlatform from '@libs/getPlatform'; import Log from '@libs/Log'; import {getMovedReportID} from '@libs/ModifiedExpenseMessage'; -import {getCachedOneTransactionThreadReportID, getIOUReportIDOfLastAction, getLastMessageTextForReport} from '@libs/OptionsListUtils'; +import {getIOUReportIDOfLastAction, getLastMessageTextForReport} from '@libs/OptionsListUtils'; import {findLastReportActions, getOriginalMessage, isInviteOrRemovedAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction as canUserPerformWriteActionUtil} from '@libs/ReportUtils'; import variables from '@styles/variables'; @@ -167,13 +167,13 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const renderItem = useCallback( ({item, index}: RenderItemProps): ReactElement => { const reportID = item.reportID; + const itemReportAttributes = reportAttributes?.[reportID]; const itemParentReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${item.parentReportID}`]; const itemReportNameValuePairs = reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`]; const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; - const itemOneTransactionThreadReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getCachedOneTransactionThreadReportID(reportID)}`]; + const itemOneTransactionThreadReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${itemReportAttributes?.oneTransactionThreadReportID}`]; const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${item?.parentReportID}`]; const itemParentReportAction = item?.parentReportActionID ? itemParentReportActions?.[item?.parentReportActionID] : undefined; - const itemReportAttributes = reportAttributes?.[reportID]; let invoiceReceiverPolicyID = '-1'; if (item?.invoiceReceiver && 'policyID' in item.invoiceReceiver) { diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index bf43500d9274..9941ebfc8d3b 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -238,7 +238,6 @@ const allSortedReportActions: Record = {}; let allReportActions: OnyxCollection; const lastVisibleReportActions: ReportActions = {}; const lastVisibleIOUMoneyActions: ReportActions = {}; -const cachedOneTransactionThreadReportIDs: Record = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, waitForCollectionCallback: true, @@ -263,9 +262,8 @@ Onyx.connect({ const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`]; // If the report is a one-transaction report, we need to return the combined reportActions so that the LHN can display modifications - // to the transaction thread or the report itself. Cache the result to avoid recomputation in renderItem. + // to the transaction thread or the report itself. const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, actions[reportActions[0]]); - cachedOneTransactionThreadReportIDs[reportID] = transactionThreadReportID; if (transactionThreadReportID) { const transactionThreadReportActionsArray = Object.values(actions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`] ?? {}); sortedReportActions = getCombinedReportActions(sortedReportActions, transactionThreadReportID, transactionThreadReportActionsArray, reportID); @@ -282,29 +280,42 @@ Onyx.connect({ const isReportArchived = !!reportNameValuePairs?.private_isArchived; const isWriteActionAllowed = canUserPerformWriteAction(report, isReportArchived); - // The report is only visible if it is the last action not deleted that - // does not match a closed or created state. - const reportActionsForDisplay = sortedReportActions.filter( - (reportAction, actionKey) => - (!(isWhisperAction(reportAction) && !isReportPreviewAction(reportAction) && !isMoneyRequestAction(reportAction)) || isActionableMentionWhisper(reportAction)) && - shouldReportActionBeVisible(reportAction, actionKey, isWriteActionAllowed) && - reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED && - reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - ); - const reportActionForDisplay = reportActionsForDisplay.at(0); + // Single pass: find the first visible action for display and the first visible IOU money request action. + // sortedReportActions is sorted descending (newest first), so the first match is the newest. + let reportActionForDisplay: ReportAction | undefined; + let iouMoneyAction: ReportAction | undefined; + for (let i = 0; i < sortedReportActions.length; i++) { + const reportAction = sortedReportActions.at(i); + if (!reportAction) { + continue; + } + const isPending = reportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + + if (!reportActionForDisplay) { + const isDisplayable = + (!(isWhisperAction(reportAction) && !isReportPreviewAction(reportAction) && !isMoneyRequestAction(reportAction)) || isActionableMentionWhisper(reportAction)) && + reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED; + if (isDisplayable && shouldReportActionBeVisible(reportAction, i, isWriteActionAllowed) && !isPending) { + reportActionForDisplay = reportAction; + } + } + + if (!iouMoneyAction && shouldReportActionBeVisible(reportAction, reportAction.reportActionID, isWriteActionAllowed) && !isPending && isMoneyRequestAction(reportAction)) { + iouMoneyAction = reportAction; + } + + if (reportActionForDisplay && iouMoneyAction) { + break; + } + } + if (!reportActionForDisplay) { delete lastVisibleReportActions[reportID]; + delete lastVisibleIOUMoneyActions[reportID]; continue; } lastVisibleReportActions[reportID] = reportActionForDisplay; - // Pre-compute the first visible IOU money request action (newest, since sorted descending) - const iouMoneyAction = sortedReportActions.find( - (reportAction) => - shouldReportActionBeVisible(reportAction, reportAction.reportActionID, isWriteActionAllowed) && - reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && - isMoneyRequestAction(reportAction), - ); if (iouMoneyAction) { lastVisibleIOUMoneyActions[reportID] = iouMoneyAction; } else { @@ -558,13 +569,6 @@ function isSearchStringMatchUserDetails(personalDetail: PersonalDetails, searchV return isSearchStringMatch(searchValue.trim(), memberDetails.toLowerCase()); } -/** - * Get IOU report ID of report last action if the action is report action preview - */ -function getCachedOneTransactionThreadReportID(reportID?: string): string | undefined { - return reportID ? cachedOneTransactionThreadReportIDs[reportID] : undefined; -} - function getIOUReportIDOfLastAction(report: OnyxEntry): string | undefined { if (!report?.reportID) { return; @@ -3244,7 +3248,6 @@ export { getHeaderMessage, getHeaderMessageForNonUserList, getIOUConfirmationOptionsFromPayeePersonalDetail, - getCachedOneTransactionThreadReportID, getIOUReportIDOfLastAction, getIsUserSubmittedExpenseOrScannedReceipt, getLastActorDisplayName, diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 5b0625acb6ac..c204ba7f687a 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -4306,6 +4306,7 @@ export { isAddCommentAction, isApprovedOrSubmittedReportAction, isIOURequestReportAction, + isNewerReportAction, isChronosOOOListAction, isClosedAction, isConsecutiveActionMadeByPreviousActor, diff --git a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts index e998c18507f5..69be37203a37 100644 --- a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts +++ b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts @@ -201,7 +201,7 @@ export default createOnyxDerivedValueConfig({ const reportNameValuePair = reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]; const reportActionsList = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]; const isReportArchived = isArchivedReport(reportNameValuePair); - const {hasAnyViolations, requiresAttention, reportErrors} = generateReportAttributes({ + const {hasAnyViolations, requiresAttention, reportErrors, oneTransactionThreadReportID} = generateReportAttributes({ report, chatReport, reportActions, @@ -227,6 +227,7 @@ export default createOnyxDerivedValueConfig({ brickRoadStatus, requiresAttention, reportErrors, + oneTransactionThreadReportID, }; return acc; diff --git a/src/types/onyx/DerivedValues.ts b/src/types/onyx/DerivedValues.ts index 9f000585d883..d0e4df545f8d 100644 --- a/src/types/onyx/DerivedValues.ts +++ b/src/types/onyx/DerivedValues.ts @@ -33,6 +33,10 @@ type ReportAttributes = { * The errors of the report. */ reportErrors: Errors; + /** + * The reportID of the one-transaction thread report, if applicable. + */ + oneTransactionThreadReportID?: string; }; /** diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 27ea3b8fae82..77705968681c 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -27,6 +27,7 @@ import { getSendMoneyFlowAction, getUpdateACHAccountMessage, isIOUActionMatchingTransactionList, + isNewerReportAction, } from '../../src/libs/ReportActionsUtils'; import {buildOptimisticCreatedReportForUnapprovedAction} from '../../src/libs/ReportUtils'; import ONYXKEYS from '../../src/ONYXKEYS'; @@ -3433,4 +3434,58 @@ describe('ReportActionsUtils', () => { expect(result).toBe('changed the auto-pay approved reports threshold to "$1,000.00" (previously "$500.00")'); }); }); + + describe('isNewerReportAction', () => { + const makeAction = (overrides: Partial): ReportAction => + ({ + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + reportActionID: '1', + created: '2024-01-01 00:00:00.000', + ...overrides, + }) as ReportAction; + + it('should return true when a has a later timestamp than b', () => { + const a = makeAction({created: '2024-01-02 00:00:00.000', reportActionID: '1'}); + const b = makeAction({created: '2024-01-01 00:00:00.000', reportActionID: '2'}); + expect(isNewerReportAction(a, b)).toBeTruthy(); + }); + + it('should return false when a has an earlier timestamp than b', () => { + const a = makeAction({created: '2024-01-01 00:00:00.000', reportActionID: '1'}); + const b = makeAction({created: '2024-01-02 00:00:00.000', reportActionID: '2'}); + expect(isNewerReportAction(a, b)).toBeFalsy(); + }); + + it('should treat CREATED action as always oldest', () => { + const created = makeAction({actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, created: '2024-12-31 00:00:00.000', reportActionID: '1'}); + const comment = makeAction({created: '2024-01-01 00:00:00.000', reportActionID: '2'}); + + expect(isNewerReportAction(created, comment)).toBeFalsy(); + expect(isNewerReportAction(comment, created)).toBeTruthy(); + }); + + it('should treat undefined created as oldest', () => { + const withUndefined = makeAction({created: undefined, reportActionID: '1'}); + const withDate = makeAction({created: '2024-01-01 00:00:00.000', reportActionID: '2'}); + + expect(isNewerReportAction(withUndefined, withDate)).toBeFalsy(); + expect(isNewerReportAction(withDate, withUndefined)).toBeTruthy(); + }); + + it('should prefer REPORT_PREVIEW over other actions when timestamps match', () => { + const preview = makeAction({actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, created: '2024-01-01 00:00:00.000', reportActionID: '1'}); + const comment = makeAction({actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, created: '2024-01-01 00:00:00.000', reportActionID: '2'}); + + expect(isNewerReportAction(preview, comment)).toBeTruthy(); + expect(isNewerReportAction(comment, preview)).toBeFalsy(); + }); + + it('should fall back to reportActionID comparison when timestamps and action types match', () => { + const higher = makeAction({created: '2024-01-01 00:00:00.000', reportActionID: '200'}); + const lower = makeAction({created: '2024-01-01 00:00:00.000', reportActionID: '100'}); + + expect(isNewerReportAction(higher, lower)).toBeTruthy(); + expect(isNewerReportAction(lower, higher)).toBeFalsy(); + }); + }); }); From 76135be7a5fe6ffee02a1f57725e01e283924687 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 18 Feb 2026 13:58:08 +0100 Subject: [PATCH 4/8] Refactor report action visibility logic in OptionsListUtils. Remove unused lastVisibleIOUMoneyActions and streamline the process for determining the first visible report action for display. --- src/libs/OptionsListUtils/index.ts | 56 ++++++++++-------------------- 1 file changed, 18 insertions(+), 38 deletions(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 9941ebfc8d3b..32f0f18f3b90 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -237,7 +237,6 @@ const lastReportActions: ReportActions = {}; const allSortedReportActions: Record = {}; let allReportActions: OnyxCollection; const lastVisibleReportActions: ReportActions = {}; -const lastVisibleIOUMoneyActions: ReportActions = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, waitForCollectionCallback: true, @@ -280,47 +279,21 @@ Onyx.connect({ const isReportArchived = !!reportNameValuePairs?.private_isArchived; const isWriteActionAllowed = canUserPerformWriteAction(report, isReportArchived); - // Single pass: find the first visible action for display and the first visible IOU money request action. - // sortedReportActions is sorted descending (newest first), so the first match is the newest. - let reportActionForDisplay: ReportAction | undefined; - let iouMoneyAction: ReportAction | undefined; - for (let i = 0; i < sortedReportActions.length; i++) { - const reportAction = sortedReportActions.at(i); - if (!reportAction) { - continue; - } - const isPending = reportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - - if (!reportActionForDisplay) { - const isDisplayable = - (!(isWhisperAction(reportAction) && !isReportPreviewAction(reportAction) && !isMoneyRequestAction(reportAction)) || isActionableMentionWhisper(reportAction)) && - reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED; - if (isDisplayable && shouldReportActionBeVisible(reportAction, i, isWriteActionAllowed) && !isPending) { - reportActionForDisplay = reportAction; - } - } - - if (!iouMoneyAction && shouldReportActionBeVisible(reportAction, reportAction.reportActionID, isWriteActionAllowed) && !isPending && isMoneyRequestAction(reportAction)) { - iouMoneyAction = reportAction; - } - - if (reportActionForDisplay && iouMoneyAction) { - break; - } - } - + // The report is only visible if it is the last action not deleted that + // does not match a closed or created state. + const reportActionsForDisplay = sortedReportActions.filter( + (reportAction, actionKey) => + (!(isWhisperAction(reportAction) && !isReportPreviewAction(reportAction) && !isMoneyRequestAction(reportAction)) || isActionableMentionWhisper(reportAction)) && + shouldReportActionBeVisible(reportAction, actionKey, isWriteActionAllowed) && + reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED && + reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + ); + const reportActionForDisplay = reportActionsForDisplay.at(0); if (!reportActionForDisplay) { delete lastVisibleReportActions[reportID]; - delete lastVisibleIOUMoneyActions[reportID]; continue; } lastVisibleReportActions[reportID] = reportActionForDisplay; - - if (iouMoneyAction) { - lastVisibleIOUMoneyActions[reportID] = iouMoneyAction; - } else { - delete lastVisibleIOUMoneyActions[reportID]; - } } }, }); @@ -674,7 +647,14 @@ function getLastMessageTextForReport({ lastMessageTextFromReport = formatReportLastMessageText(Parser.htmlToText(properSchemaForMoneyRequestMessage)); } else if (isReportPreviewAction(lastReportAction)) { const iouReport = getReportOrDraftReport(getIOUReportIDFromReportActionPreview(lastReportAction)); - const lastIOUMoneyReportAction = iouReport?.reportID ? lastVisibleIOUMoneyActions[iouReport.reportID] : undefined; + const lastIOUMoneyReportAction = iouReport?.reportID + ? allSortedReportActions[iouReport.reportID]?.find( + (reportAction, key): reportAction is ReportAction => + shouldReportActionBeVisible(reportAction, key, canUserPerformWriteAction(report, isReportArchived)) && + reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && + isMoneyRequestAction(reportAction), + ) + : undefined; // For workspace chats, use the report title if (reportUtilsIsPolicyExpenseChat(report) && !isEmptyObject(iouReport)) { const reportName = reportAttributesDerived?.[iouReport.reportID]?.reportName ?? ''; From 934de483b31a9b9c13c3b8c29d7664997269d919 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Thu, 19 Feb 2026 09:51:54 +0100 Subject: [PATCH 5/8] Add isOffline parameter to generateReportAttributes and update reportAttributes config --- src/libs/ReportUtils.ts | 4 +++- src/libs/actions/OnyxDerived/configs/reportAttributes.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0712d2280b1a..18c243832f43 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -12525,12 +12525,14 @@ function generateReportAttributes({ reportActions, transactionViolations, isReportArchived = false, + isOffline, }: { report: OnyxEntry; chatReport: OnyxEntry; reportActions?: OnyxCollection; transactionViolations: OnyxCollection; isReportArchived: boolean; + isOffline?: boolean; }) { const reportActionsList = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`]; const parentReportActionsList = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`]; @@ -12541,7 +12543,7 @@ function generateReportAttributes({ const hasAnyTypeOfViolations = hasViolationsToDisplayInLHN || (!isReportSettled && isCurrentUserReportOwner && doesReportHasViolations); const reportErrors = getAllReportErrors(report, reportActionsList, isReportArchived); const hasErrors = Object.entries(reportErrors ?? {}).length > 0; - const oneTransactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActionsList); + const oneTransactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActionsList, isOffline); const parentReportAction = report?.parentReportActionID ? parentReportActionsList?.[report.parentReportActionID] : undefined; const requiresAttention = requiresAttentionFromCurrentUser(report, parentReportAction, isReportArchived); diff --git a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts index 69be37203a37..7edacb314b5a 100644 --- a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts +++ b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts @@ -71,9 +71,10 @@ export default createOnyxDerivedValueConfig({ ONYXKEYS.SESSION, ONYXKEYS.COLLECTION.POLICY, ONYXKEYS.COLLECTION.REPORT_METADATA, + ONYXKEYS.NETWORK, ], compute: ( - [reports, preferredLocale, transactionViolations, reportActions, reportNameValuePairs, transactions, personalDetails, session, policies], + [reports, preferredLocale, transactionViolations, reportActions, reportNameValuePairs, transactions, personalDetails, session, policies, network], {currentValue, sourceValues, areAllConnectionsSet}, ) => { if (!areAllConnectionsSet) { @@ -207,6 +208,7 @@ export default createOnyxDerivedValueConfig({ reportActions, transactionViolations, isReportArchived, + isOffline: network?.isOffline, }); let brickRoadStatus; From 7955c219746c68b1bc40625054b191009d4feeda Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Thu, 19 Feb 2026 13:46:48 +0100 Subject: [PATCH 6/8] Revert "Add isOffline parameter to generateReportAttributes and update reportAttributes config" This reverts commit 934de483b31a9b9c13c3b8c29d7664997269d919. Co-Authored-By: Claude Sonnet 4.6 --- src/libs/ReportUtils.ts | 4 +--- src/libs/actions/OnyxDerived/configs/reportAttributes.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 18c243832f43..0712d2280b1a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -12525,14 +12525,12 @@ function generateReportAttributes({ reportActions, transactionViolations, isReportArchived = false, - isOffline, }: { report: OnyxEntry; chatReport: OnyxEntry; reportActions?: OnyxCollection; transactionViolations: OnyxCollection; isReportArchived: boolean; - isOffline?: boolean; }) { const reportActionsList = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`]; const parentReportActionsList = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`]; @@ -12543,7 +12541,7 @@ function generateReportAttributes({ const hasAnyTypeOfViolations = hasViolationsToDisplayInLHN || (!isReportSettled && isCurrentUserReportOwner && doesReportHasViolations); const reportErrors = getAllReportErrors(report, reportActionsList, isReportArchived); const hasErrors = Object.entries(reportErrors ?? {}).length > 0; - const oneTransactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActionsList, isOffline); + const oneTransactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActionsList); const parentReportAction = report?.parentReportActionID ? parentReportActionsList?.[report.parentReportActionID] : undefined; const requiresAttention = requiresAttentionFromCurrentUser(report, parentReportAction, isReportArchived); diff --git a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts index 7edacb314b5a..69be37203a37 100644 --- a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts +++ b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts @@ -71,10 +71,9 @@ export default createOnyxDerivedValueConfig({ ONYXKEYS.SESSION, ONYXKEYS.COLLECTION.POLICY, ONYXKEYS.COLLECTION.REPORT_METADATA, - ONYXKEYS.NETWORK, ], compute: ( - [reports, preferredLocale, transactionViolations, reportActions, reportNameValuePairs, transactions, personalDetails, session, policies, network], + [reports, preferredLocale, transactionViolations, reportActions, reportNameValuePairs, transactions, personalDetails, session, policies], {currentValue, sourceValues, areAllConnectionsSet}, ) => { if (!areAllConnectionsSet) { @@ -208,7 +207,6 @@ export default createOnyxDerivedValueConfig({ reportActions, transactionViolations, isReportArchived, - isOffline: network?.isOffline, }); let brickRoadStatus; From 2fffc6908f781e27962a1a6e32e6a1f4daa678a6 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Fri, 20 Feb 2026 13:45:11 +0100 Subject: [PATCH 7/8] Add tests for findLastReportActions utility function --- tests/unit/ReportActionsUtilsTest.ts | 133 +++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index e0d0a5b01f8a..74ac7e1e567b 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -11,6 +11,7 @@ import {chatReportR14932 as mockChatReport, iouReportR14932 as mockIOUReport} fr import CONST from '../../src/CONST'; import * as ReportActionsUtils from '../../src/libs/ReportActionsUtils'; import { + findLastReportActions, getAutoPayApprovedReportsEnabledMessage, getAutoReimbursementMessage, getCardIssuedMessage, @@ -25,9 +26,12 @@ import { getPolicyChangeLogMaxExpenseAmountNoReceiptMessage, getReportActionActorAccountID, getSendMoneyFlowAction, + getSortedReportActions, + getSortedReportActionsForDisplay, getUpdateACHAccountMessage, isIOUActionMatchingTransactionList, isNewerReportAction, + shouldReportActionBeVisibleAsLastAction, } from '../../src/libs/ReportActionsUtils'; import {buildOptimisticCreatedReportForUnapprovedAction} from '../../src/libs/ReportUtils'; import ONYXKEYS from '../../src/ONYXKEYS'; @@ -3484,4 +3488,133 @@ describe('ReportActionsUtils', () => { expect(isNewerReportAction(lower, higher)).toBeFalsy(); }); }); + + describe('findLastReportActions', () => { + const makeAction = (overrides: Partial): ReportAction => + ({ + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + reportActionID: '1', + created: '2024-01-01 00:00:00.000', + person: [{type: 'TEXT', style: 'strong', text: 'Actor'}], + message: [{html: 'hello', text: 'hello', type: 'COMMENT'}], + ...overrides, + }) as ReportAction; + + it('returns undefined for both when reportActions is undefined', () => { + const result = findLastReportActions(undefined); + expect(result.lastVisibleAction).toBeUndefined(); + expect(result.lastActionForDisplay).toBeUndefined(); + }); + + it('returns undefined for both when reportActions is empty', () => { + const result = findLastReportActions({}); + expect(result.lastVisibleAction).toBeUndefined(); + expect(result.lastActionForDisplay).toBeUndefined(); + }); + + it('returns the single visible action for both when there is only one ADD_COMMENT action', () => { + const action = makeAction({reportActionID: '1', created: '2024-01-01 00:00:00.000'}); + const result = findLastReportActions({[action.reportActionID]: action}); + expect(result.lastVisibleAction).toBe(action); + expect(result.lastActionForDisplay).toBe(action); + }); + + it('returns undefined for lastActionForDisplay but not lastVisibleAction when only action is CREATED', () => { + const created = makeAction({actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, reportActionID: '1', created: '2024-01-01 00:00:00.000'}); + const result = findLastReportActions({[created.reportActionID]: created}); + expect(result.lastVisibleAction).toBe(created); + expect(result.lastActionForDisplay).toBeUndefined(); + }); + + it('returns the newest of multiple visible actions', () => { + const older = makeAction({reportActionID: '1', created: '2024-01-01 00:00:00.000'}); + const newer = makeAction({reportActionID: '2', created: '2024-01-02 00:00:00.000'}); + const result = findLastReportActions({ + [older.reportActionID]: older, + [newer.reportActionID]: newer, + }); + expect(result.lastVisibleAction).toBe(newer); + expect(result.lastActionForDisplay).toBe(newer); + }); + + it('skips deleted actions (no pendingAction, empty html) for both results', () => { + const visible = makeAction({reportActionID: '1', created: '2024-01-01 00:00:00.000'}); + const deleted = makeAction({ + reportActionID: '2', + created: '2024-01-02 00:00:00.000', + message: [{html: '', text: '', type: 'COMMENT'}], + pendingAction: undefined, + }); + const result = findLastReportActions({ + [visible.reportActionID]: visible, + [deleted.reportActionID]: deleted, + }); + expect(result.lastVisibleAction).toBe(visible); + expect(result.lastActionForDisplay).toBe(visible); + }); + + it('excludes actions with errors from lastActionForDisplay but not from lastVisibleAction', () => { + const clean = makeAction({reportActionID: '1', created: '2024-01-01 00:00:00.000'}); + const withErrors = makeAction({ + reportActionID: '2', + created: '2024-01-02 00:00:00.000', + errors: {someError: 'error message'}, + }); + const result = findLastReportActions({ + [clean.reportActionID]: clean, + [withErrors.reportActionID]: withErrors, + }); + expect(result.lastVisibleAction).toBe(withErrors); + expect(result.lastActionForDisplay).toBe(clean); + }); + + it('agrees with getSortedReportActionsForDisplay for lastVisibleAction across multiple actions', () => { + const actionA = makeAction({reportActionID: 'actionA', created: '2024-01-01 00:00:00.000'}); + const actionB = makeAction({reportActionID: 'actionB', created: '2024-01-03 00:00:00.000'}); + const actionC = makeAction({reportActionID: 'actionC', created: '2024-01-02 00:00:00.000'}); + const actions: ReportActions = { + actionA, + actionB, + actionC, + }; + const {lastVisibleAction} = findLastReportActions(actions); + const fromSort = getSortedReportActionsForDisplay(actions).at(0); + expect(lastVisibleAction?.reportActionID).toBe(fromSort?.reportActionID); + }); + + it('agrees with the old getSortedReportActions+filter approach for lastActionForDisplay', () => { + const actionA = makeAction({reportActionID: 'actionA', created: '2024-01-01 00:00:00.000'}); + const actionB = makeAction({reportActionID: 'actionB', created: '2024-01-03 00:00:00.000'}); + const actionC = makeAction({ + reportActionID: 'actionC', + created: '2024-01-02 00:00:00.000', + errors: {someError: 'error'}, + }); + const actions: ReportActions = {actionA, actionB, actionC}; + const {lastActionForDisplay} = findLastReportActions(actions); + const fromOldApproach = getSortedReportActions(Object.values(actions)).findLast( + (a) => shouldReportActionBeVisibleAsLastAction(a) && a.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED, + ); + expect(lastActionForDisplay?.reportActionID).toBe(fromOldApproach?.reportActionID); + }); + + it('respects canUserPerformWriteAction when determining visibility', () => { + const normalAction = makeAction({reportActionID: '1', created: '2024-01-01 00:00:00.000'}); + // An actionable join request whisper is hidden when canUserPerformWriteAction is false + const joinRequestAction = makeAction({ + reportActionID: '2', + created: '2024-01-02 00:00:00.000', + actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_JOIN_REQUEST, + originalMessage: {choice: '', inviterEmail: ''}, + }); + + const withWrite = findLastReportActions({[normalAction.reportActionID]: normalAction, [joinRequestAction.reportActionID]: joinRequestAction}, true); + const withoutWrite = findLastReportActions({[normalAction.reportActionID]: normalAction, [joinRequestAction.reportActionID]: joinRequestAction}, false); + + // With write permission: join request is visible, so it should be selected as newer + expect(withWrite.lastVisibleAction?.reportActionID).toBe(joinRequestAction.reportActionID); + // Without write permission: join request hidden, so only the normal action remains + expect(withoutWrite.lastVisibleAction?.reportActionID).toBe(normalAction.reportActionID); + }); + }); }); From 851dffec1e400c74f14fa08b4ece9c95849dc8b6 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Fri, 20 Feb 2026 15:30:02 +0100 Subject: [PATCH 8/8] Refactor findLastReportActions function to simplify parameters and update test case for actionable mention whisper visibility --- src/libs/ReportActionsUtils.ts | 8 +------- tests/unit/ReportActionsUtilsTest.ts | 5 ++--- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 9373fdbe01a6..310b5bfdcf3d 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1427,13 +1427,7 @@ function isNewerReportAction(a: ReportAction, b: ReportAction): boolean { * - lastVisibleAction: newest visible action * - lastActionForDisplay: newest displayable action (not CREATED) */ -function findLastReportActions( - reportActions: OnyxEntry, - canUserPerformWriteAction?: boolean, -): { - lastVisibleAction?: ReportAction; - lastActionForDisplay?: ReportAction; -} { +function findLastReportActions(reportActions: OnyxEntry, canUserPerformWriteAction?: boolean) { if (!reportActions) { return {lastVisibleAction: undefined, lastActionForDisplay: undefined}; } diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 74ac7e1e567b..3a0074d838f8 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -3600,12 +3600,11 @@ describe('ReportActionsUtils', () => { it('respects canUserPerformWriteAction when determining visibility', () => { const normalAction = makeAction({reportActionID: '1', created: '2024-01-01 00:00:00.000'}); - // An actionable join request whisper is hidden when canUserPerformWriteAction is false + // An actionable mention whisper is hidden when canUserPerformWriteAction is false const joinRequestAction = makeAction({ reportActionID: '2', created: '2024-01-02 00:00:00.000', - actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_JOIN_REQUEST, - originalMessage: {choice: '', inviterEmail: ''}, + actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_MENTION_WHISPER, }); const withWrite = findLastReportActions({[normalAction.reportActionID]: normalAction, [joinRequestAction.reportActionID]: joinRequestAction}, true);