From 8d08b24b9c4f6d90b520ff5fec7614a186b31a7e Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 31 Dec 2025 14:14:44 +0100 Subject: [PATCH 01/32] feat: Add visible report actions derived value and related utilities --- src/ONYXKEYS.ts | 2 + src/libs/ReportActionsUtils.ts | 3 + .../OnyxDerived/ONYX_DERIVED_VALUES.ts | 2 + .../configs/visibleReportActions.ts | 255 ++++++++++++++++++ src/pages/home/report/ReportActionsView.tsx | 44 ++- src/types/onyx/DerivedValues.ts | 14 +- src/types/onyx/index.ts | 3 +- 7 files changed, 310 insertions(+), 13 deletions(-) create mode 100644 src/libs/actions/OnyxDerived/configs/visibleReportActions.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 51b25fdb32cb1..27c3a5e5eb5cf 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -977,6 +977,7 @@ const ONYXKEYS = { REPORT_ATTRIBUTES: 'reportAttributes', REPORT_TRANSACTIONS_AND_VIOLATIONS: 'reportTransactionsAndViolations', OUTSTANDING_REPORTS_BY_POLICY_ID: 'outstandingReportsByPolicyID', + VISIBLE_REPORT_ACTIONS: 'visibleReportActions', }, /** Stores HybridApp specific state required to interoperate with OldDot */ @@ -1381,6 +1382,7 @@ type OnyxDerivedValuesMapping = { [ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: OnyxTypes.ReportAttributesDerivedValue; [ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS]: OnyxTypes.ReportTransactionsAndViolationsDerivedValue; [ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID]: OnyxTypes.OutstandingReportsByPolicyIDDerivedValue; + [ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS]: OnyxTypes.VisibleReportActionsDerivedValue; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 60164e4052cca..cb0eeaf8b25b1 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3761,6 +3761,9 @@ export { isSystemUserMentioned, withDEWRoutedActionsArray, withDEWRoutedActionsObject, + isTravelUpdate, + isVisiblePreviewOrMoneyRequest, + isActionableJoinRequestPendingReportAction, }; export type {LastVisibleMessage}; diff --git a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts index 0f9c467e518da..32a1cd58914cd 100644 --- a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts +++ b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts @@ -3,6 +3,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import outstandingReportsByPolicyIDConfig from './configs/outstandingReportsByPolicyID'; import reportAttributesConfig from './configs/reportAttributes'; import reportTransactionsAndViolationsConfig from './configs/reportTransactionsAndViolations'; +import visibleReportActionsConfig from './configs/visibleReportActions'; import type {OnyxDerivedValueConfig} from './types'; /** @@ -13,6 +14,7 @@ const ONYX_DERIVED_VALUES = { [ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: reportAttributesConfig, [ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS]: reportTransactionsAndViolationsConfig, [ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID]: outstandingReportsByPolicyIDConfig, + [ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS]: visibleReportActionsConfig, } as const satisfies { // eslint-disable-next-line @typescript-eslint/no-explicit-any [Key in ValueOf]: OnyxDerivedValueConfig; diff --git a/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts new file mode 100644 index 0000000000000..d858c0fbb1c44 --- /dev/null +++ b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts @@ -0,0 +1,255 @@ +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import { + getOriginalMessage, + getWhisperedTo, + isActionableCardFraudAlert, + isActionableJoinRequestPendingReportAction, + isActionableMentionWhisper, + isActionableReportMentionWhisper, + isActionableWhisper, + isDeletedAction, + isDeletedParentAction, + isMarkAsClosedAction, + isMovedTransactionAction, + isPendingRemove, + isReportActionDeprecated, + isResolvedActionableWhisper, + isReversedTransaction, + isTravelUpdate, + isTripPreview, + isVisiblePreviewOrMoneyRequest, + isWhisperAction, +} from '@libs/ReportActionsUtils'; +import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report, ReportAction, ReportActions} from '@src/types/onyx'; +import type {VisibleReportActionsDerivedValue} from '@src/types/onyx/DerivedValues'; +import type {OriginalMessageMovedTransaction, OriginalMessageUnreportedTransaction} from '@src/types/onyx/OriginalMessage'; +import type ReportActionName from '@src/types/onyx/ReportActionName'; + +const {POLICY_CHANGE_LOG: policyChangelogTypes, ROOM_CHANGE_LOG: roomChangeLogTypes, ...otherActionTypes} = CONST.REPORT.ACTIONS.TYPE; +const supportedActionTypes = new Set([...Object.values(otherActionTypes), ...Object.values(policyChangelogTypes), ...Object.values(roomChangeLogTypes)]); + +function getOrCreateReportVisibilityRecord(result: VisibleReportActionsDerivedValue, reportID: string): Record { + if (!result[reportID]) { + // eslint-disable-next-line no-param-reassign + result[reportID] = {}; + } + return result[reportID]; +} + +function isUnreportedTransactionVisible(reportAction: ReportAction, allReports: OnyxCollection): boolean { + const originalMessage = getOriginalMessage(reportAction) as OriginalMessageUnreportedTransaction | undefined; + + if (!originalMessage?.fromReportID) { + return false; + } + + const fromReportKey = `${ONYXKEYS.COLLECTION.REPORT}${originalMessage.fromReportID}`; + const fromReport = allReports?.[fromReportKey]; + + return !!fromReport; +} + +function isMovedTransactionVisible(reportAction: ReportAction, allReports: OnyxCollection): boolean { + const originalMessage = getOriginalMessage(reportAction) as OriginalMessageMovedTransaction | undefined; + + if (!originalMessage) { + return false; + } + + const toReportID = originalMessage.toReportID; + const fromReportID = originalMessage.fromReportID; + + // UNREPORTED_REPORT_ID means "no report" which is a valid source + const isFromUnreportedReport = fromReportID === CONST.REPORT.UNREPORTED_REPORT_ID; + + const toReportKey = `${ONYXKEYS.COLLECTION.REPORT}${toReportID}`; + const fromReportKey = `${ONYXKEYS.COLLECTION.REPORT}${fromReportID}`; + + const toReportExists = !!allReports?.[toReportKey]; + const fromReportExists = isFromUnreportedReport || !!allReports?.[fromReportKey]; + + return fromReportExists || toReportExists; +} + +function doesActionDependOnReportExistence(action: ReportAction): boolean { + const isUnreportedTransaction = action.actionName === CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION; + const isMovedTransaction = isMovedTransactionAction(action as OnyxEntry); + + return isUnreportedTransaction || isMovedTransaction; +} + +function isReportActionStaticallyVisible(reportAction: OnyxEntry, key: string | number, allReports: OnyxCollection, currentUserAccountID: number | undefined): boolean { + if (!reportAction) { + return false; + } + + if (isReportActionDeprecated(reportAction, key)) { + return false; + } + + if (!supportedActionTypes.has(reportAction.actionName)) { + return false; + } + + if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION) { + return isUnreportedTransactionVisible(reportAction, allReports); + } + + if (isMovedTransactionAction(reportAction)) { + return isMovedTransactionVisible(reportAction, allReports); + } + + // We display a footer explaining why the report was closed, so hide the CLOSED action (except "Mark as closed") + if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED) { + const isMarkAsClosed = isMarkAsClosedAction(reportAction); + if (!isMarkAsClosed) { + return false; + } + } + + if (isWhisperAction(reportAction)) { + const whisperedToAccountIDs = getWhisperedTo(reportAction); + const isWhisperTargetedToCurrentUser = whisperedToAccountIDs.includes(currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID); + if (!isWhisperTargetedToCurrentUser) { + return false; + } + } + + if (isPendingRemove(reportAction) && !reportAction.childVisibleActionCount) { + return false; + } + + if (isTripPreview(reportAction) || isTravelUpdate(reportAction)) { + return true; + } + + if (isActionableWhisper(reportAction) && isResolvedActionableWhisper(reportAction)) { + return false; + } + + if (!isVisiblePreviewOrMoneyRequest(reportAction)) { + return false; + } + + const isDeleted = isDeletedAction(reportAction); + const isPending = !!reportAction.pendingAction; + const isParentAction = isDeletedParentAction(reportAction); + const isReversed = isReversedTransaction(reportAction); + + return !isDeleted || isPending || isParentAction || isReversed; +} + +/** + * Used by the component to filter out actionable whispers when user cannot write. + */ +function isActionableWhisperRequiringWritePermission(reportAction: OnyxEntry): boolean { + if (!reportAction) { + return false; + } + + const isReportMentionWhisper = isActionableReportMentionWhisper(reportAction); + const isJoinRequestPending = isActionableJoinRequestPendingReportAction(reportAction); + const isMentionWhisper = isActionableMentionWhisper(reportAction); + const isCardFraudAlert = isActionableCardFraudAlert(reportAction); + + return isReportMentionWhisper || isJoinRequestPending || isMentionWhisper || isCardFraudAlert; +} + +export {isActionableWhisperRequiringWritePermission}; + +export default createOnyxDerivedValueConfig({ + key: ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, + dependencies: [ONYXKEYS.COLLECTION.REPORT_ACTIONS, ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.SESSION], + compute: ([allReportActions, allReports, session], {sourceValues, currentValue}): VisibleReportActionsDerivedValue => { + if (!allReportActions) { + return {}; + } + + const currentUserAccountID = session?.accountID; + + const reportActionsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT_ACTIONS]; + const reportUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT]; + const sessionUpdates = sourceValues?.[ONYXKEYS.SESSION]; + + // Session change = user changed, need full recompute due to whisper targeting + if (sessionUpdates) { + const result: VisibleReportActionsDerivedValue = {}; + + for (const [reportActionsKey, reportActions] of Object.entries(allReportActions)) { + if (!reportActions) { + continue; + } + + const reportID = reportActionsKey.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); + const reportVisibility = getOrCreateReportVisibilityRecord(result, reportID); + + for (const [actionID, action] of Object.entries(reportActions)) { + if (action) { + reportVisibility[actionID] = isReportActionStaticallyVisible(action, actionID, allReports, currentUserAccountID); + } + } + } + + return result; + } + + // Only reports changed - recompute actions that depend on report existence + if (reportUpdates && !reportActionsUpdates) { + const result: VisibleReportActionsDerivedValue = currentValue ? {...currentValue} : {}; + + for (const [reportActionsKey, reportActions] of Object.entries(allReportActions)) { + if (!reportActions) { + continue; + } + + const reportID = reportActionsKey.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); + const reportVisibility = getOrCreateReportVisibilityRecord(result, reportID); + + for (const [actionID, action] of Object.entries(reportActions)) { + if (!action) { + continue; + } + + if (doesActionDependOnReportExistence(action)) { + reportVisibility[actionID] = isReportActionStaticallyVisible(action, actionID, allReports, currentUserAccountID); + } + } + } + + return result; + } + + // Report actions changed - incremental update (most common path) + const result: VisibleReportActionsDerivedValue = currentValue ? {...currentValue} : {}; + const reportActionsToProcess = reportActionsUpdates ? Object.keys(reportActionsUpdates) : Object.keys(allReportActions); + + for (const reportActionsKey of reportActionsToProcess) { + const reportActions: OnyxEntry = allReportActions[reportActionsKey]; + const reportID = reportActionsKey.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); + + if (!reportActions) { + delete result[reportID]; + continue; + } + + const reportVisibility = getOrCreateReportVisibilityRecord(result, reportID); + + const specificUpdates = reportActionsUpdates?.[reportActionsKey]; + const actionsToProcess = specificUpdates ? Object.entries(specificUpdates) : Object.entries(reportActions); + + for (const [actionID, action] of actionsToProcess) { + if (!action) { + delete reportVisibility[actionID]; + continue; + } + + reportVisibility[actionID] = isReportActionStaticallyVisible(action, actionID, allReports, currentUserAccountID); + } + } + + return result; + }, +}); diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 7867e0d92715a..6d5545d159af5 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -12,6 +12,7 @@ import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; import {getReportPreviewAction} from '@libs/actions/IOU'; +import {isActionableWhisperRequiringWritePermission} from '@libs/actions/OnyxDerived/configs/visibleReportActions'; import {updateLoadingInitialReportAction} from '@libs/actions/Report'; import DateUtils from '@libs/DateUtils'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; @@ -29,7 +30,6 @@ import { isDeletedParentAction, isIOUActionMatchingTransactionList, isMoneyRequestAction, - shouldReportActionBeVisible, } from '@libs/ReportActionsUtils'; import {buildOptimisticCreatedReportAction, buildOptimisticIOUReportAction, canUserPerformWriteAction, isInvoiceReport, isMoneyRequestReport} from '@libs/ReportUtils'; import markOpenReportEnd from '@libs/telemetry/markOpenReportEnd'; @@ -102,6 +102,7 @@ function ReportActionsView({ ); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, {canBeMissing: true}); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const prevTransactionThreadReport = usePrevious(transactionThreadReport); const reportActionID = route?.params?.reportActionID; const prevReportActionID = usePrevious(reportActionID); @@ -217,16 +218,37 @@ function ReportActionsView({ [allReportActions, transactionThreadReportActions, transactionThreadReport?.parentReportActionID], ); - const visibleReportActions = useMemo( - () => - reportActions.filter( - (reportAction) => - (isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && - shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction) && - isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs), - ), - [reportActions, isOffline, canPerformWriteAction, reportTransactionIDs], - ); + const visibleReportActions = useMemo(() => { + console.time(`[PERF] visibleReportActions filter (${reportID}, ${reportActions.length} actions)`); + const result = reportActions.filter((reportAction) => { + const passesOfflineCheck = + isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors; + + if (!passesOfflineCheck) { + return false; + } + + const actionReportID = reportAction.reportID ?? reportID; + const isStaticallyVisible = visibleReportActionsData?.[actionReportID]?.[reportAction.reportActionID]; + + const passesStaticVisibility = isStaticallyVisible ?? true; + if (!passesStaticVisibility) { + return false; + } + + if (!canPerformWriteAction && isActionableWhisperRequiringWritePermission(reportAction)) { + return false; + } + + if (!isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs)) { + return false; + } + + return true; + }); + console.timeEnd(`[PERF] visibleReportActions filter (${reportID}, ${reportActions.length} actions)`); + return result; + }, [reportActions, isOffline, canPerformWriteAction, reportTransactionIDs, visibleReportActionsData, reportID]); const newestReportAction = useMemo(() => reportActions?.at(0), [reportActions]); const mostRecentIOUReportActionID = useMemo(() => getMostRecentIOURequestActionID(reportActions), [reportActions]); diff --git a/src/types/onyx/DerivedValues.ts b/src/types/onyx/DerivedValues.ts index d39cc88e76e2b..0aae8484cd97c 100644 --- a/src/types/onyx/DerivedValues.ts +++ b/src/types/onyx/DerivedValues.ts @@ -70,5 +70,17 @@ type ReportTransactionsAndViolationsDerivedValue = Record>; +/** + * The derived value for visible report actions. + */ +type VisibleReportActionsDerivedValue = Record>; + export default ReportAttributesDerivedValue; -export type {ReportAttributes, ReportAttributesDerivedValue, ReportTransactionsAndViolationsDerivedValue, ReportTransactionsAndViolations, OutstandingReportsByPolicyIDDerivedValue}; +export type { + ReportAttributes, + ReportAttributesDerivedValue, + ReportTransactionsAndViolationsDerivedValue, + ReportTransactionsAndViolations, + OutstandingReportsByPolicyIDDerivedValue, + VisibleReportActionsDerivedValue, +}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 360808733200d..a03ff4e3aa847 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -28,7 +28,7 @@ import type Credentials from './Credentials'; import type Currency from './Currency'; import type {CurrencyList} from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; -import type {OutstandingReportsByPolicyIDDerivedValue, ReportAttributesDerivedValue, ReportTransactionsAndViolationsDerivedValue} from './DerivedValues'; +import type {OutstandingReportsByPolicyIDDerivedValue, ReportAttributesDerivedValue, ReportTransactionsAndViolationsDerivedValue, VisibleReportActionsDerivedValue} from './DerivedValues'; import type DismissedProductTraining from './DismissedProductTraining'; import type DismissedReferralBanners from './DismissedReferralBanners'; import type Domain from './Domain'; @@ -301,6 +301,7 @@ export type { LastSearchParams, ReportTransactionsAndViolationsDerivedValue, OutstandingReportsByPolicyIDDerivedValue, + VisibleReportActionsDerivedValue, ScheduleCallDraft, ValidateUserAndGetAccessiblePolicies, VacationDelegate, From 79e271df74fac665f46861bcf628092c078a493a Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 31 Dec 2025 14:17:06 +0100 Subject: [PATCH 02/32] Optimize visible report actions filtering logic for improved performance --- src/pages/home/report/ReportActionsView.tsx | 51 ++++++++++----------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 6d5545d159af5..1c367145f097b 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -218,37 +218,36 @@ function ReportActionsView({ [allReportActions, transactionThreadReportActions, transactionThreadReport?.parentReportActionID], ); - const visibleReportActions = useMemo(() => { - console.time(`[PERF] visibleReportActions filter (${reportID}, ${reportActions.length} actions)`); - const result = reportActions.filter((reportAction) => { - const passesOfflineCheck = - isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors; - - if (!passesOfflineCheck) { - return false; - } + const visibleReportActions = useMemo( + () => + reportActions.filter((reportAction) => { + const passesOfflineCheck = + isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors; - const actionReportID = reportAction.reportID ?? reportID; - const isStaticallyVisible = visibleReportActionsData?.[actionReportID]?.[reportAction.reportActionID]; + if (!passesOfflineCheck) { + return false; + } - const passesStaticVisibility = isStaticallyVisible ?? true; - if (!passesStaticVisibility) { - return false; - } + const actionReportID = reportAction.reportID ?? reportID; + const isStaticallyVisible = visibleReportActionsData?.[actionReportID]?.[reportAction.reportActionID]; - if (!canPerformWriteAction && isActionableWhisperRequiringWritePermission(reportAction)) { - return false; - } + const passesStaticVisibility = isStaticallyVisible ?? true; + if (!passesStaticVisibility) { + return false; + } - if (!isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs)) { - return false; - } + if (!canPerformWriteAction && isActionableWhisperRequiringWritePermission(reportAction)) { + return false; + } - return true; - }); - console.timeEnd(`[PERF] visibleReportActions filter (${reportID}, ${reportActions.length} actions)`); - return result; - }, [reportActions, isOffline, canPerformWriteAction, reportTransactionIDs, visibleReportActionsData, reportID]); + if (!isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs)) { + return false; + } + + return true; + }), + [reportActions, isOffline, canPerformWriteAction, reportTransactionIDs, visibleReportActionsData, reportID], + ); const newestReportAction = useMemo(() => reportActions?.at(0), [reportActions]); const mostRecentIOUReportActionID = useMemo(() => getMostRecentIOURequestActionID(reportActions), [reportActions]); From c50b1d9d835b313c13761b9414ed1d9a146d057d Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 31 Dec 2025 14:19:56 +0100 Subject: [PATCH 03/32] Refactor: Remove commented-out code and improve clarity in visible report actions logic --- .../actions/OnyxDerived/configs/visibleReportActions.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts index d858c0fbb1c44..739e7dbe391ec 100644 --- a/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts +++ b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts @@ -62,7 +62,6 @@ function isMovedTransactionVisible(reportAction: ReportAction, allReports: OnyxC const toReportID = originalMessage.toReportID; const fromReportID = originalMessage.fromReportID; - // UNREPORTED_REPORT_ID means "no report" which is a valid source const isFromUnreportedReport = fromReportID === CONST.REPORT.UNREPORTED_REPORT_ID; const toReportKey = `${ONYXKEYS.COLLECTION.REPORT}${toReportID}`; @@ -102,7 +101,6 @@ function isReportActionStaticallyVisible(reportAction: OnyxEntry, return isMovedTransactionVisible(reportAction, allReports); } - // We display a footer explaining why the report was closed, so hide the CLOSED action (except "Mark as closed") if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED) { const isMarkAsClosed = isMarkAsClosedAction(reportAction); if (!isMarkAsClosed) { @@ -142,9 +140,6 @@ function isReportActionStaticallyVisible(reportAction: OnyxEntry, return !isDeleted || isPending || isParentAction || isReversed; } -/** - * Used by the component to filter out actionable whispers when user cannot write. - */ function isActionableWhisperRequiringWritePermission(reportAction: OnyxEntry): boolean { if (!reportAction) { return false; @@ -222,7 +217,6 @@ export default createOnyxDerivedValueConfig({ return result; } - // Report actions changed - incremental update (most common path) const result: VisibleReportActionsDerivedValue = currentValue ? {...currentValue} : {}; const reportActionsToProcess = reportActionsUpdates ? Object.keys(reportActionsUpdates) : Object.keys(allReportActions); From df5791594f644b20a33c96e6fec6702481166b69 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 7 Jan 2026 11:00:20 +0100 Subject: [PATCH 04/32] MoneyRequestReportActionsList to include additional visibility checks for report actions --- .../MoneyRequestReportActionsList.tsx | 35 ++++++++++++++----- src/libs/ReportActionsUtils.ts | 3 ++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index 564886595c313..0e762d02688be 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -34,6 +34,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import {isActionableWhisperRequiringWritePermission} from '@libs/actions/OnyxDerived/configs/visibleReportActions'; import {queueExportSearchWithTemplate} from '@libs/actions/Search'; import DateUtils from '@libs/DateUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; @@ -50,7 +51,6 @@ import { isCurrentActionUnread, isDeletedParentAction, isIOUActionMatchingTransactionList, - shouldReportActionBeVisible, wasMessageReceivedWhileOffline, } from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction, chatIncludesChronosWithID, getOriginalReportID, getReportLastVisibleActionCreated, isHarvestCreatedExpenseReport, isUnread} from '@libs/ReportUtils'; @@ -171,6 +171,7 @@ function MoneyRequestReportActionsList({ const isReportArchived = useReportIsArchived(reportID); const canPerformWriteAction = canUserPerformWriteAction(report, isReportArchived); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const {shouldUseNarrowLayout} = useResponsiveLayout(); @@ -225,17 +226,35 @@ function MoneyRequestReportActionsList({ const visibleReportActions = useMemo(() => { const filteredActions = reportActions.filter((reportAction) => { const isActionVisibleOnMoneyReport = isActionVisibleOnMoneyRequestReport(reportAction, shouldShowHarvestCreatedAction); + if (!isActionVisibleOnMoneyReport) { + return false; + } - return ( - isActionVisibleOnMoneyReport && - (isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && - shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction) && - isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs) - ); + const passesOfflineCheck = + isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors; + if (!passesOfflineCheck) { + return false; + } + + const actionReportID = reportAction.reportID ?? reportID; + const isStaticallyVisible = visibleReportActionsData?.[actionReportID]?.[reportAction.reportActionID] ?? true; + if (!isStaticallyVisible) { + return false; + } + + if (!canPerformWriteAction && isActionableWhisperRequiringWritePermission(reportAction)) { + return false; + } + + if (!isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs)) { + return false; + } + + return true; }); return filteredActions.toReversed(); - }, [reportActions, isOffline, canPerformWriteAction, reportTransactionIDs, shouldShowHarvestCreatedAction]); + }, [reportActions, isOffline, canPerformWriteAction, reportTransactionIDs, shouldShowHarvestCreatedAction, visibleReportActionsData, reportID]); const reportActionSize = useRef(visibleReportActions.length); const lastAction = visibleReportActions.at(-1); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index dca35288a4d68..ae7f66925e8c7 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3571,6 +3571,7 @@ export { isActionableWhisper, isActionableJoinRequest, isActionableJoinRequestPending, + isActionableJoinRequestPendingReportAction, isActionableMentionWhisper, isActionableMentionInviteToSubmitExpenseConfirmWhisper, isActionableReportMentionWhisper, @@ -3625,6 +3626,7 @@ export { isTrackExpenseAction, isTransactionThread, isTripPreview, + isTravelUpdate, isHoldAction, isWhisperAction, isSubmittedAction, @@ -3642,6 +3644,7 @@ export { isTagModificationAction, isIOUActionMatchingTransactionList, isResolvedActionableWhisper, + isVisiblePreviewOrMoneyRequest, isReimbursementDirectionInformationRequiredAction, shouldHideNewMarker, shouldReportActionBeVisible, From 99d1cff21ac5fef453bce9a0713b8d1324bac475 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 7 Jan 2026 11:14:39 +0100 Subject: [PATCH 05/32] Remove unused report action utility functions from ReportActionsUtils.ts --- src/libs/ReportActionsUtils.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index ae7f66925e8c7..1516bed22147b 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3722,9 +3722,6 @@ export { isSystemUserMentioned, withDEWRoutedActionsArray, withDEWRoutedActionsObject, - isTravelUpdate, - isVisiblePreviewOrMoneyRequest, - isActionableJoinRequestPendingReportAction, getReportActionActorAccountID, }; From ef613d357231f3967e5b677850e05c66eb74bcff Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 7 Jan 2026 11:20:52 +0100 Subject: [PATCH 06/32] prettier fix --- .../MoneyRequestReportView/MoneyRequestReportActionsList.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index 0e762d02688be..a7e1c700935bc 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -230,8 +230,7 @@ function MoneyRequestReportActionsList({ return false; } - const passesOfflineCheck = - isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors; + const passesOfflineCheck = isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors; if (!passesOfflineCheck) { return false; } From b3beaf8d9ed38cd2039f3218357529e9334e5e80 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Fri, 9 Jan 2026 11:10:33 +0100 Subject: [PATCH 07/32] replace shouldReportActionBeVisible --- .../AttachmentCarousel/extractAttachments.ts | 11 ++- .../LHNOptionsList/LHNOptionsList.tsx | 9 +- src/components/ParentNavigationSubtitle.tsx | 5 +- src/libs/OptionsListUtils/index.ts | 36 ++++++-- src/libs/ReportActionsUtils.ts | 85 +++++++++++++++++-- src/libs/ReportUtils.ts | 24 ++++-- src/libs/SearchUIUtils.ts | 7 +- .../configs/visibleReportActions.ts | 11 +++ src/libs/actions/Report.ts | 11 ++- src/pages/home/ReportScreen.tsx | 7 +- src/setup/telemetry/index.ts | 8 +- 11 files changed, 175 insertions(+), 39 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 6dffd994d6a7c..6dd90b415c2ec 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -3,11 +3,11 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {Attachment} from '@components/Attachments/types'; import {getFileName, splitExtensionFromFileName} from '@libs/fileDownload/FileUtils'; -import {getHtmlWithAttachmentID, getReportActionHtml, getReportActionMessage, getSortedReportActions, isMoneyRequestAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; +import {getHtmlWithAttachmentID, getReportActionHtml, getReportActionMessage, getSortedReportActions, isMoneyRequestAction, isReportActionVisible} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction} from '@libs/ReportUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import CONST from '@src/CONST'; -import type {Report, ReportAction, ReportActions} from '@src/types/onyx'; +import type {Report, ReportAction, ReportActions, VisibleReportActionsDerivedValue} from '@src/types/onyx'; import type {Note} from '@src/types/onyx/Report'; /** @@ -22,6 +22,7 @@ function extractAttachments( reportActions, report, isReportArchived, + visibleReportActionsData, }: { privateNotes?: Record; accountID?: number; @@ -29,6 +30,7 @@ function extractAttachments( reportActions?: OnyxEntry; report: OnyxEntry; isReportArchived: boolean | undefined; + visibleReportActionsData?: VisibleReportActionsDerivedValue; }, ) { const targetNote = privateNotes?.[Number(accountID)]?.note ?? ''; @@ -115,9 +117,10 @@ function extractAttachments( return attachments.reverse(); } + const reportID = report?.reportID ?? ''; const actions = [...(parentReportAction ? [parentReportAction] : []), ...getSortedReportActions(Object.values(reportActions ?? {}))]; - for (const [key, action] of actions.entries()) { - if (!shouldReportActionBeVisible(action, key, canUserPerformAction) || isMoneyRequestAction(action)) { + for (const action of actions) { + if (!isReportActionVisible(action, reportID, canUserPerformAction, visibleReportActionsData) || isMoneyRequestAction(action)) { continue; } diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index ec38c19283ba1..3de9241cdb2a8 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -34,7 +34,7 @@ import { getSortedReportActionsForDisplay, isInviteOrRemovedAction, isMoneyRequestAction, - shouldReportActionBeVisibleAsLastAction, + isReportActionVisibleAsLastAction, } from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction as canUserPerformWriteActionUtil} from '@libs/ReportUtils'; import variables from '@styles/variables'; @@ -71,6 +71,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const [isFullscreenVisible] = useOnyx(ONYXKEYS.FULLSCREEN_VISIBILITY, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const {policyForMovingExpensesID} = usePolicyForMovingExpenses(); const theme = useTheme(); @@ -204,7 +205,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const isReportArchived = !!itemReportNameValuePairs?.private_isArchived; const canUserPerformWrite = canUserPerformWriteActionUtil(item, isReportArchived); - const sortedReportActions = getSortedReportActionsForDisplay(itemReportActions, canUserPerformWrite); + const sortedReportActions = getSortedReportActionsForDisplay(itemReportActions, canUserPerformWrite, false, visibleReportActionsData); const lastReportAction = sortedReportActions.at(0); // Get the transaction for the last report action @@ -236,6 +237,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio isReportArchived: !!itemReportNameValuePairs?.private_isArchived, policyForMovingExpensesID, reportMetadata: itemReportMetadata, + visibleReportActionsDataParam: visibleReportActionsData, }); const shouldShowRBRorGBRTooltip = firstReportIDWithGBRorRBR === reportID; @@ -247,7 +249,8 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio 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, + (reportAction) => + isReportActionVisibleAsLastAction(reportAction, canUserPerformWriteAction, visibleReportActionsData) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED, ); lastAction = reportActionsForDisplay.at(-1); } diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 1595047038379..645f97fd56368 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -13,7 +13,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; import Navigation from '@libs/Navigation/Navigation'; import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; -import {getReportAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; +import {getReportAction, isReportActionVisible} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction as canUserPerformWriteActionReportUtils, isMoneyRequestReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import type {ParentNavigationSummaryParams} from '@src/languages/params'; @@ -88,6 +88,7 @@ function ParentNavigationSubtitle({ const {translate} = useLocalize(); const [currentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: false}); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`, {canBeMissing: false}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const isReportArchived = useReportIsArchived(report?.reportID); const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report, isReportArchived); const isReportInRHP = currentRoute.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT; @@ -101,7 +102,7 @@ function ParentNavigationSubtitle({ const onPress = () => { const parentAction = getReportAction(parentReportID, parentReportActionID); - const isVisibleAction = shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? CONST.DEFAULT_NUMBER_ID, canUserPerformWriteAction); + const isVisibleAction = isReportActionVisible(parentAction, parentReportID, canUserPerformWriteAction, visibleReportActionsData); if (openParentReportInCurrentTab && isReportInRHP) { // If the report is displayed in RHP in Reports tab, we want to stay in the current tab after opening the parent report diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 9686f4de3917e..f72deae5ab268 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -76,12 +76,12 @@ import { isReimbursementDeQueuedOrCanceledAction, isReimbursementQueuedAction, isRenamedAction, + isReportActionVisible, isReportPreviewAction, isTaskAction, isThreadParentMessage, isUnapprovedAction, isWhisperAction, - shouldReportActionBeVisible, withDEWRoutedActionsArray, } from '@libs/ReportActionsUtils'; import {computeReportName} from '@libs/ReportNameUtils'; @@ -160,6 +160,7 @@ import type { ReportAttributesDerivedValue, ReportMetadata, ReportNameValuePairs, + VisibleReportActionsDerivedValue, } from '@src/types/onyx'; import type {Attendee, Participant} from '@src/types/onyx/IOU'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -241,6 +242,14 @@ Onyx.connect({ }, }); +let visibleReportActionsData: VisibleReportActionsDerivedValue | undefined; +Onyx.connect({ + key: ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, + callback: (value) => { + visibleReportActionsData = value ?? undefined; + }, +}); + const lastReportActions: ReportActions = {}; const allSortedReportActions: Record = {}; let allReportActions: OnyxCollection; @@ -255,6 +264,14 @@ Onyx.connect({ allReportActions = actions ?? {}; + // Skip processing if derived value is not ready yet - will be processed when derived value loads + if (!visibleReportActionsData) { + return; + } + + // Capture the current value of visibleReportActionsData to avoid closure issues + const currentVisibleReportActionsData = visibleReportActionsData; + // Iterate over the report actions to build the sorted and lastVisible report actions objects for (const reportActions of Object.entries(allReportActions)) { const reportID = reportActions[0].split('_').at(1); @@ -290,9 +307,9 @@ Onyx.connect({ // 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) => + (reportAction) => (!(isWhisperAction(reportAction) && !isReportPreviewAction(reportAction) && !isMoneyRequestAction(reportAction)) || isActionableMentionWhisper(reportAction)) && - shouldReportActionBeVisible(reportAction, actionKey, isWriteActionAllowed) && + isReportActionVisible(reportAction, reportID, isWriteActionAllowed, currentVisibleReportActionsData) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); @@ -598,6 +615,7 @@ function getLastMessageTextForReport({ isReportArchived = false, policyForMovingExpensesID, reportMetadata, + visibleReportActionsDataParam, }: { report: OnyxEntry; lastActorDetails: Partial | null; @@ -607,10 +625,11 @@ function getLastMessageTextForReport({ isReportArchived?: boolean; policyForMovingExpensesID?: string; reportMetadata?: OnyxEntry; + visibleReportActionsDataParam?: VisibleReportActionsDerivedValue; }): string { const reportID = report?.reportID; const lastReportAction = reportID ? lastVisibleReportActions[reportID] : undefined; - const lastVisibleMessage = getLastVisibleMessage(report?.reportID); + const lastVisibleMessage = getLastVisibleMessage(report?.reportID, undefined, {}, undefined, visibleReportActionsDataParam); // some types of actions are filtered out for lastReportAction, in some cases we need to check the actual last action const lastOriginalReportAction = reportID ? lastReportActions[reportID] : undefined; @@ -646,10 +665,11 @@ 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)) && + const iouReportID = iouReport?.reportID ?? ''; + const lastIOUMoneyReportAction = iouReportID + ? allSortedReportActions[iouReportID]?.find( + (reportAction): reportAction is ReportAction => + isReportActionVisible(reportAction, iouReportID, canUserPerformWriteAction(report, isReportArchived), visibleReportActionsData) && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isMoneyRequestAction(reportAction), ) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 1516bed22147b..52985b0a72f49 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -8,12 +8,23 @@ import type {ValueOf} from 'type-fest'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import usePrevious from '@hooks/usePrevious'; import {isHarvestCreatedExpenseReport, isPolicyExpenseChat} from '@libs/ReportUtils'; +import {isActionableWhisperRequiringWritePermission} from '@userActions/OnyxDerived/configs/visibleReportActions'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Card, OnyxInputOrEntry, OriginalMessageIOU, PersonalDetails, Policy, PrivatePersonalDetails, ReportMetadata, ReportNameValuePairs} from '@src/types/onyx'; +import type { + Card, + OnyxInputOrEntry, + OriginalMessageIOU, + PersonalDetails, + Policy, + PrivatePersonalDetails, + ReportMetadata, + ReportNameValuePairs, + VisibleReportActionsDerivedValue, +} from '@src/types/onyx'; import type {JoinWorkspaceResolution, OriginalMessageChangeLog, OriginalMessageExportIntegration, OriginalMessageUnreportedTransaction} from '@src/types/onyx/OriginalMessage'; import type {PolicyReportFieldType} from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; @@ -1119,6 +1130,55 @@ function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxInputOrEntry< ); } +/** + * Checks if a report action is visible using the pre-computed derived value when available, + * falling back to runtime calculation if not. + */ +function isReportActionVisible( + reportAction: OnyxEntry, + reportID: string, + canUserPerformWriteAction?: boolean, + visibleReportActions?: VisibleReportActionsDerivedValue, +): boolean { + if (visibleReportActions) { + const staticVisibility = visibleReportActions[reportID]?.[reportAction?.reportActionID ?? ''] ?? true; + if (!staticVisibility) { + return false; + } + if (!canUserPerformWriteAction && isActionableWhisperRequiringWritePermission(reportAction)) { + return false; + } + return true; + } + return shouldReportActionBeVisible(reportAction, reportAction?.reportActionID ?? '', canUserPerformWriteAction); +} + +/** + * Checks if a report action is visible as last action using the pre-computed derived value when available, + * falling back to runtime calculation if not. + */ +function isReportActionVisibleAsLastAction( + reportAction: OnyxInputOrEntry, + canUserPerformWriteAction?: boolean, + visibleReportActions?: VisibleReportActionsDerivedValue, +): boolean { + if (!reportAction) { + return false; + } + + if (Object.keys(reportAction.errors ?? {}).length > 0) { + return false; + } + + const reportID = reportAction.reportID ?? ''; + + return ( + isReportActionVisible(reportAction, reportID, canUserPerformWriteAction, visibleReportActions) && + (!(isWhisperAction(reportAction) && !isReportPreviewAction(reportAction) && !isMoneyRequestAction(reportAction)) || isActionableMentionWhisper(reportAction)) && + !(isDeletedAction(reportAction) && !isDeletedParentAction(reportAction) && !isPendingHide(reportAction)) + ); +} + /** * For policy change logs, report URLs are generated in the server, * which includes a baseURL placeholder that's replaced in the client. @@ -1150,6 +1210,7 @@ function getLastVisibleAction( canUserPerformWriteAction?: boolean, actionsToMerge: Record | null> = {}, reportActionsParam: OnyxCollection = allReportActions, + visibleReportActionsData?: VisibleReportActionsDerivedValue, ): OnyxEntry { let reportActions: Array = []; if (!isEmpty(actionsToMerge)) { @@ -1159,7 +1220,7 @@ function getLastVisibleAction( } else { reportActions = Object.values(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}); } - const visibleReportActions = reportActions.filter((action): action is ReportAction => shouldReportActionBeVisibleAsLastAction(action, canUserPerformWriteAction)); + const visibleReportActions = reportActions.filter((action): action is ReportAction => isReportActionVisibleAsLastAction(action, canUserPerformWriteAction, visibleReportActionsData)); const sortedReportActions = getSortedReportActions(visibleReportActions, true); if (sortedReportActions.length === 0) { return undefined; @@ -1187,8 +1248,9 @@ function getLastVisibleMessage( canUserPerformWriteAction?: boolean, actionsToMerge: Record | null> = {}, reportAction: OnyxInputOrEntry | undefined = undefined, + visibleReportActionsData?: VisibleReportActionsDerivedValue, ): LastVisibleMessage { - const lastVisibleAction = reportAction ?? getLastVisibleAction(reportID, canUserPerformWriteAction, actionsToMerge); + const lastVisibleAction = reportAction ?? getLastVisibleAction(reportID, canUserPerformWriteAction, actionsToMerge, undefined, visibleReportActionsData); const message = getReportActionMessage(lastVisibleAction); if (message && isReportMessageAttachment(message)) { @@ -1290,6 +1352,7 @@ function getSortedReportActionsForDisplay( reportActions: OnyxEntry | ReportAction[], canUserPerformWriteAction?: boolean, shouldIncludeInvisibleActions = false, + visibleReportActionsData?: VisibleReportActionsDerivedValue, ): ReportAction[] { let filteredReportActions: ReportAction[] = []; if (!reportActions) { @@ -1300,7 +1363,10 @@ function getSortedReportActionsForDisplay( filteredReportActions = Object.values(reportActions).filter(Boolean); } else { filteredReportActions = Object.entries(reportActions) - .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key, canUserPerformWriteAction)) + .filter(([, reportAction]) => { + const reportID = reportAction?.reportID ?? ''; + return isReportActionVisible(reportAction, reportID, canUserPerformWriteAction, visibleReportActionsData); + }) .map(([, reportAction]) => reportAction); } @@ -1599,9 +1665,14 @@ function getOneTransactionThreadReportID(...args: Parameters shouldReportActionBeVisibleAsLastAction(action, canUserPerformWriteAction)); + const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => isReportActionVisibleAsLastAction(action, canUserPerformWriteAction, visibleReportActionsData)); // Exclude the task system message and the created message const visibleReportActionsWithoutTaskSystemMessage = visibleReportActions.filter((action) => !isTaskAction(action) && !isCreatedAction(action)); @@ -3649,6 +3720,8 @@ export { shouldHideNewMarker, shouldReportActionBeVisible, shouldReportActionBeVisibleAsLastAction, + isReportActionVisible, + isReportActionVisibleAsLastAction, wasActionTakenByCurrentUser, isInviteOrRemovedAction, isActionableAddPaymentCard, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 26afd195e9a13..f1cf3970d08f2 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -60,6 +60,7 @@ import type { Task, Transaction, TransactionViolation, + VisibleReportActionsDerivedValue, } from '@src/types/onyx'; import type {ReportTransactionsAndViolations} from '@src/types/onyx/DerivedValues'; import type {Attendee, Participant} from '@src/types/onyx/IOU'; @@ -249,6 +250,7 @@ import { isRenamedAction, isReopenedAction, isReportActionAttachment, + isReportActionVisible, isReportPreviewAction, isRetractedAction, isReversedTransaction, @@ -263,7 +265,6 @@ import { isTripPreview, isUnapprovedAction, isWhisperAction, - shouldReportActionBeVisible, wasActionTakenByCurrentUser, } from './ReportActionsUtils'; import type {LastVisibleMessage} from './ReportActionsUtils'; @@ -8992,13 +8993,19 @@ function isReportNotFound(report: OnyxEntry): boolean { /** * Check if the report is the parent report of the currently viewed report or at least one child report has report action */ -function shouldHideReport(report: OnyxEntry, currentReportId: string | undefined, isReportArchived: boolean | undefined): boolean { +function shouldHideReport( + report: OnyxEntry, + currentReportId: string | undefined, + isReportArchived: boolean | undefined, + visibleReportActionsData?: VisibleReportActionsDerivedValue, +): boolean { const currentReport = getReportOrDraftReport(currentReportId); const parentReport = getParentReport(!isEmptyObject(currentReport) ? currentReport : undefined); const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`] ?? {}; + const reportID = report?.reportID ?? ''; const isChildReportHasComment = Object.values(reportActions ?? {})?.some( (reportAction) => - (reportAction?.childVisibleActionCount ?? 0) > 0 && shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction(report, isReportArchived)), + (reportAction?.childVisibleActionCount ?? 0) > 0 && isReportActionVisible(reportAction, reportID, canUserPerformWriteAction(report, isReportArchived), visibleReportActionsData), ); return parentReport?.reportID !== report?.reportID && !isChildReportHasComment; } @@ -10017,7 +10024,13 @@ function isMoneyRequestReportPendingDeletion(reportOrID: OnyxEntry | str return parentReportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } -function navigateToLinkedReportAction(ancestor: Ancestor, isInNarrowPaneModal: boolean, canUserPerformWrite: boolean | undefined, isOffline: boolean) { +function navigateToLinkedReportAction( + ancestor: Ancestor, + isInNarrowPaneModal: boolean, + canUserPerformWrite: boolean | undefined, + isOffline: boolean, + visibleReportActionsData?: VisibleReportActionsDerivedValue, +) { if (isInNarrowPaneModal) { Navigation.navigate( ROUTES.SEARCH_REPORT.getRoute({ @@ -10032,7 +10045,8 @@ function navigateToLinkedReportAction(ancestor: Ancestor, isInNarrowPaneModal: b // Pop the thread report screen before navigating to the chat report. Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID)); - const isVisibleAction = shouldReportActionBeVisible(ancestor.reportAction, ancestor.reportAction.reportActionID, canUserPerformWrite); + const reportID = ancestor.report.reportID ?? ''; + const isVisibleAction = isReportActionVisible(ancestor.reportAction, reportID, canUserPerformWrite, visibleReportActionsData); if (isVisibleAction && !isOffline) { // Pop the chat report screen before navigating to the linked report action. diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 585439a6663be..2bfa6df2ecc62 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -82,9 +82,9 @@ import { isDeletedAction, isHoldAction, isMoneyRequestAction, + isReportActionVisible, isResolvedActionableWhisper, isWhisperActionTargetedToOthers, - shouldReportActionBeVisible, } from './ReportActionsUtils'; import {isExportAction} from './ReportPrimaryActionUtils'; import { @@ -1602,7 +1602,7 @@ function createAndOpenSearchTransactionThread( * * Do not use directly, use only via `getSections()` facade. */ -function getReportActionsSections(data: OnyxTypes.SearchResults['data']): [ReportActionListItemType[], number] { +function getReportActionsSections(data: OnyxTypes.SearchResults['data'], visibleReportActionsData?: OnyxTypes.VisibleReportActionsDerivedValue): [ReportActionListItemType[], number] { const reportActionItems: ReportActionListItemType[] = []; const transactions = Object.keys(data) @@ -1632,8 +1632,9 @@ function getReportActionsSections(data: OnyxTypes.SearchResults['data']): [Repor const isReportArchived = isArchivedReport(data[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]); const invoiceReceiverPolicy: OnyxTypes.Policy | undefined = report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS ? data[`${ONYXKEYS.COLLECTION.POLICY}${report.invoiceReceiver.policyID}`] : undefined; + const reportID = reportAction.reportID ?? ''; if ( - !shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction(report, isReportArchived)) || + !isReportActionVisible(reportAction, reportID, canUserPerformWriteAction(report, isReportArchived), visibleReportActionsData) || isDeletedAction(reportAction) || isResolvedActionableWhisper(reportAction) || reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED || diff --git a/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts index 739e7dbe391ec..56c2272e84c83 100644 --- a/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts +++ b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts @@ -31,6 +31,9 @@ import type ReportActionName from '@src/types/onyx/ReportActionName'; const {POLICY_CHANGE_LOG: policyChangelogTypes, ROOM_CHANGE_LOG: roomChangeLogTypes, ...otherActionTypes} = CONST.REPORT.ACTIONS.TYPE; const supportedActionTypes = new Set([...Object.values(otherActionTypes), ...Object.values(policyChangelogTypes), ...Object.values(roomChangeLogTypes)]); +// DEBUG: Counter to track compute calls - remove after debugging +let computeCallCount = 0; + function getOrCreateReportVisibilityRecord(result: VisibleReportActionsDerivedValue, reportID: string): Record { if (!result[reportID]) { // eslint-disable-next-line no-param-reassign @@ -159,6 +162,14 @@ export default createOnyxDerivedValueConfig({ key: ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, dependencies: [ONYXKEYS.COLLECTION.REPORT_ACTIONS, ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.SESSION], compute: ([allReportActions, allReports, session], {sourceValues, currentValue}): VisibleReportActionsDerivedValue => { + // DEBUG: Log compute calls - remove after debugging + computeCallCount++; + // eslint-disable-next-line no-console + console.log(`[DERIVED COMPUTE] visibleReportActions #${computeCallCount}`, { + trigger: sourceValues ? Object.keys(sourceValues).join(', ') : 'INITIAL', + timestamp: new Date().toISOString(), + }); + if (!allReportActions) { return {}; } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 05d4935f0fe7f..c73080bf24054 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -198,6 +198,7 @@ import type { ReportUserIsTyping, Transaction, TransactionViolations, + VisibleReportActionsDerivedValue, } from '@src/types/onyx'; import type {Decision} from '@src/types/onyx/OriginalMessage'; import type {CurrentUserPersonalDetails, Timezone} from '@src/types/onyx/PersonalDetails'; @@ -317,6 +318,14 @@ Onyx.connect({ }, }); +let visibleReportActionsData: VisibleReportActionsDerivedValue | undefined; +Onyx.connect({ + key: ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, + callback: (value) => { + visibleReportActionsData = value ?? undefined; + }, +}); + let allPersonalDetails: OnyxEntry = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, @@ -2038,7 +2047,7 @@ function deleteReportComment( (action) => action.reportActionID !== reportAction.reportActionID && ReportActionsUtils.didMessageMentionCurrentUser(action) && - ReportActionsUtils.shouldReportActionBeVisible(action, action.reportActionID), + ReportActionsUtils.isReportActionVisible(action, reportID, undefined, visibleReportActionsData), ); optimisticReport.lastMentionedTime = latestMentionedReportAction?.created ?? null; } diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index b7fbe8bd25169..235fe55d0ce81 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -59,10 +59,10 @@ import { isCreatedAction, isDeletedParentAction, isMoneyRequestAction, + isReportActionVisible, isSentMoneyReportAction, isTransactionThread, isWhisperAction, - shouldReportActionBeVisible, } from '@libs/ReportActionsUtils'; import { canEditReportAction, @@ -303,6 +303,7 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: false}); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const {reportActions: unfilteredReportActions, linkedAction, sortedAllReportActions, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID, reportActionIDFromRoute); // wrapping in useMemo because this is array operation and can cause performance issues const reportActions = useMemo(() => getFilteredReportActionsForReportView(unfilteredReportActions), [unfilteredReportActions]); @@ -456,8 +457,8 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr const {isEditingDisabled, isCurrentReportLoadedFromOnyx} = useIsReportReadyToDisplay(report, reportIDFromRoute, isReportArchived); const isLinkedActionDeleted = useMemo( - () => !!linkedAction && !shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, canUserPerformWriteAction(report, isReportArchived)), - [linkedAction, report, isReportArchived], + () => !!linkedAction && !isReportActionVisible(linkedAction, linkedAction.reportID ?? reportID ?? '', canUserPerformWriteAction(report, isReportArchived), visibleReportActionsData), + [linkedAction, report, isReportArchived, reportID, visibleReportActionsData], ); const prevIsLinkedActionDeleted = usePrevious(linkedAction ? isLinkedActionDeleted : undefined); diff --git a/src/setup/telemetry/index.ts b/src/setup/telemetry/index.ts index ee2f3f07e3e18..2b2aaa1082bfc 100644 --- a/src/setup/telemetry/index.ts +++ b/src/setup/telemetry/index.ts @@ -15,10 +15,10 @@ export default function (): void { Sentry.init({ dsn: CONFIG.SENTRY_DSN, transport: isDevelopment() ? makeDebugTransport : undefined, - tracesSampleRate: 1.0, - profilesSampleRate: Platform.OS === 'android' ? 0 : 1.0, - enableAutoPerformanceTracing: true, - enableUserInteractionTracing: true, + tracesSampleRate: 0, + profilesSampleRate: 0, + enableAutoPerformanceTracing: false, + enableUserInteractionTracing: false, integrations, environment: CONFIG.ENVIRONMENT, release: `${pkg.name}@${pkg.version}`, From 9f13088d4cbebac291d928724545683881181f16 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Fri, 9 Jan 2026 13:26:51 +0100 Subject: [PATCH 08/32] compute derived shouldRepportActionBeVisible --- .../MoneyRequestReportActionsList.tsx | 2 +- src/libs/ReportActionsUtils.ts | 18 ++- .../configs/visibleReportActions.ts | 153 ++---------------- src/pages/home/report/ReportActionsView.tsx | 2 +- 4 files changed, 29 insertions(+), 146 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index a7e1c700935bc..90c9609db2479 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -34,7 +34,6 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {isActionableWhisperRequiringWritePermission} from '@libs/actions/OnyxDerived/configs/visibleReportActions'; import {queueExportSearchWithTemplate} from '@libs/actions/Search'; import DateUtils from '@libs/DateUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; @@ -47,6 +46,7 @@ import { getMostRecentIOURequestActionID, getOneTransactionThreadReportID, hasNextActionMadeBySameActor, + isActionableWhisperRequiringWritePermission, isConsecutiveChronosAutomaticTimerAction, isCurrentActionUnread, isDeletedParentAction, diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 52985b0a72f49..79b702776ba77 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -8,7 +8,6 @@ import type {ValueOf} from 'type-fest'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import usePrevious from '@hooks/usePrevious'; import {isHarvestCreatedExpenseReport, isPolicyExpenseChat} from '@libs/ReportUtils'; -import {isActionableWhisperRequiringWritePermission} from '@userActions/OnyxDerived/configs/visibleReportActions'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import type {TranslationPaths} from '@src/languages/types'; @@ -2349,6 +2348,22 @@ function isActionableCardFraudAlert(reportAction: OnyxInputOrEntry return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT; } +/** + * Checks if a report action is an actionable whisper that requires write permission to be visible. + */ +function isActionableWhisperRequiringWritePermission(reportAction: OnyxEntry): boolean { + if (!reportAction) { + return false; + } + + return ( + isActionableReportMentionWhisper(reportAction) || + isActionableJoinRequestPendingReportAction(reportAction) || + isActionableMentionWhisper(reportAction) || + isActionableCardFraudAlert(reportAction) + ); +} + function getExportIntegrationLastMessageText(translate: LocalizedTranslate, reportAction: OnyxEntry): string { const fragments = getExportIntegrationActionFragments(translate, reportAction); return fragments.reduce((acc, fragment) => `${acc} ${fragment.text}`, ''); @@ -3726,6 +3741,7 @@ export { isInviteOrRemovedAction, isActionableAddPaymentCard, isActionableCardFraudAlert, + isActionableWhisperRequiringWritePermission, getExportIntegrationActionFragments, getExportIntegrationLastMessageText, getExportIntegrationMessageHTML, diff --git a/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts index 56c2272e84c83..48691f18a9b52 100644 --- a/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts +++ b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts @@ -1,35 +1,10 @@ -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import { - getOriginalMessage, - getWhisperedTo, - isActionableCardFraudAlert, - isActionableJoinRequestPendingReportAction, - isActionableMentionWhisper, - isActionableReportMentionWhisper, - isActionableWhisper, - isDeletedAction, - isDeletedParentAction, - isMarkAsClosedAction, - isMovedTransactionAction, - isPendingRemove, - isReportActionDeprecated, - isResolvedActionableWhisper, - isReversedTransaction, - isTravelUpdate, - isTripPreview, - isVisiblePreviewOrMoneyRequest, - isWhisperAction, -} from '@libs/ReportActionsUtils'; +import type {OnyxEntry} from 'react-native-onyx'; +import {isMovedTransactionAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report, ReportAction, ReportActions} from '@src/types/onyx'; +import type {ReportAction, ReportActions} from '@src/types/onyx'; import type {VisibleReportActionsDerivedValue} from '@src/types/onyx/DerivedValues'; -import type {OriginalMessageMovedTransaction, OriginalMessageUnreportedTransaction} from '@src/types/onyx/OriginalMessage'; -import type ReportActionName from '@src/types/onyx/ReportActionName'; - -const {POLICY_CHANGE_LOG: policyChangelogTypes, ROOM_CHANGE_LOG: roomChangeLogTypes, ...otherActionTypes} = CONST.REPORT.ACTIONS.TYPE; -const supportedActionTypes = new Set([...Object.values(otherActionTypes), ...Object.values(policyChangelogTypes), ...Object.values(roomChangeLogTypes)]); // DEBUG: Counter to track compute calls - remove after debugging let computeCallCount = 0; @@ -42,40 +17,6 @@ function getOrCreateReportVisibilityRecord(result: VisibleReportActionsDerivedVa return result[reportID]; } -function isUnreportedTransactionVisible(reportAction: ReportAction, allReports: OnyxCollection): boolean { - const originalMessage = getOriginalMessage(reportAction) as OriginalMessageUnreportedTransaction | undefined; - - if (!originalMessage?.fromReportID) { - return false; - } - - const fromReportKey = `${ONYXKEYS.COLLECTION.REPORT}${originalMessage.fromReportID}`; - const fromReport = allReports?.[fromReportKey]; - - return !!fromReport; -} - -function isMovedTransactionVisible(reportAction: ReportAction, allReports: OnyxCollection): boolean { - const originalMessage = getOriginalMessage(reportAction) as OriginalMessageMovedTransaction | undefined; - - if (!originalMessage) { - return false; - } - - const toReportID = originalMessage.toReportID; - const fromReportID = originalMessage.fromReportID; - - const isFromUnreportedReport = fromReportID === CONST.REPORT.UNREPORTED_REPORT_ID; - - const toReportKey = `${ONYXKEYS.COLLECTION.REPORT}${toReportID}`; - const fromReportKey = `${ONYXKEYS.COLLECTION.REPORT}${fromReportID}`; - - const toReportExists = !!allReports?.[toReportKey]; - const fromReportExists = isFromUnreportedReport || !!allReports?.[fromReportKey]; - - return fromReportExists || toReportExists; -} - function doesActionDependOnReportExistence(action: ReportAction): boolean { const isUnreportedTransaction = action.actionName === CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION; const isMovedTransaction = isMovedTransactionAction(action as OnyxEntry); @@ -83,85 +24,13 @@ function doesActionDependOnReportExistence(action: ReportAction): boolean { return isUnreportedTransaction || isMovedTransaction; } -function isReportActionStaticallyVisible(reportAction: OnyxEntry, key: string | number, allReports: OnyxCollection, currentUserAccountID: number | undefined): boolean { - if (!reportAction) { - return false; - } - - if (isReportActionDeprecated(reportAction, key)) { - return false; - } - - if (!supportedActionTypes.has(reportAction.actionName)) { - return false; - } - - if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION) { - return isUnreportedTransactionVisible(reportAction, allReports); - } - - if (isMovedTransactionAction(reportAction)) { - return isMovedTransactionVisible(reportAction, allReports); - } - - if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED) { - const isMarkAsClosed = isMarkAsClosedAction(reportAction); - if (!isMarkAsClosed) { - return false; - } - } - - if (isWhisperAction(reportAction)) { - const whisperedToAccountIDs = getWhisperedTo(reportAction); - const isWhisperTargetedToCurrentUser = whisperedToAccountIDs.includes(currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID); - if (!isWhisperTargetedToCurrentUser) { - return false; - } - } - - if (isPendingRemove(reportAction) && !reportAction.childVisibleActionCount) { - return false; - } - - if (isTripPreview(reportAction) || isTravelUpdate(reportAction)) { - return true; - } - - if (isActionableWhisper(reportAction) && isResolvedActionableWhisper(reportAction)) { - return false; - } - - if (!isVisiblePreviewOrMoneyRequest(reportAction)) { - return false; - } - - const isDeleted = isDeletedAction(reportAction); - const isPending = !!reportAction.pendingAction; - const isParentAction = isDeletedParentAction(reportAction); - const isReversed = isReversedTransaction(reportAction); - - return !isDeleted || isPending || isParentAction || isReversed; -} - -function isActionableWhisperRequiringWritePermission(reportAction: OnyxEntry): boolean { - if (!reportAction) { - return false; - } - - const isReportMentionWhisper = isActionableReportMentionWhisper(reportAction); - const isJoinRequestPending = isActionableJoinRequestPendingReportAction(reportAction); - const isMentionWhisper = isActionableMentionWhisper(reportAction); - const isCardFraudAlert = isActionableCardFraudAlert(reportAction); - - return isReportMentionWhisper || isJoinRequestPending || isMentionWhisper || isCardFraudAlert; -} - -export {isActionableWhisperRequiringWritePermission}; - export default createOnyxDerivedValueConfig({ key: ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, + // Note: REPORT and SESSION dependencies are needed to trigger recompute when reports change + // (for UNREPORTED_TRANSACTION/MOVED_TRANSACTION visibility) or when user changes (for whisper targeting). + // shouldReportActionBeVisible uses global Onyx-connected variables internally. dependencies: [ONYXKEYS.COLLECTION.REPORT_ACTIONS, ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.SESSION], - compute: ([allReportActions, allReports, session], {sourceValues, currentValue}): VisibleReportActionsDerivedValue => { + compute: ([allReportActions], {sourceValues, currentValue}): VisibleReportActionsDerivedValue => { // DEBUG: Log compute calls - remove after debugging computeCallCount++; // eslint-disable-next-line no-console @@ -174,8 +43,6 @@ export default createOnyxDerivedValueConfig({ return {}; } - const currentUserAccountID = session?.accountID; - const reportActionsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT_ACTIONS]; const reportUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT]; const sessionUpdates = sourceValues?.[ONYXKEYS.SESSION]; @@ -194,7 +61,7 @@ export default createOnyxDerivedValueConfig({ for (const [actionID, action] of Object.entries(reportActions)) { if (action) { - reportVisibility[actionID] = isReportActionStaticallyVisible(action, actionID, allReports, currentUserAccountID); + reportVisibility[actionID] = shouldReportActionBeVisible(action, actionID); } } } @@ -220,7 +87,7 @@ export default createOnyxDerivedValueConfig({ } if (doesActionDependOnReportExistence(action)) { - reportVisibility[actionID] = isReportActionStaticallyVisible(action, actionID, allReports, currentUserAccountID); + reportVisibility[actionID] = shouldReportActionBeVisible(action, actionID); } } } @@ -251,7 +118,7 @@ export default createOnyxDerivedValueConfig({ continue; } - reportVisibility[actionID] = isReportActionStaticallyVisible(action, actionID, allReports, currentUserAccountID); + reportVisibility[actionID] = shouldReportActionBeVisible(action, actionID); } } diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 1c367145f097b..038aa4aa2b19c 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -12,7 +12,6 @@ import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; import {getReportPreviewAction} from '@libs/actions/IOU'; -import {isActionableWhisperRequiringWritePermission} from '@libs/actions/OnyxDerived/configs/visibleReportActions'; import {updateLoadingInitialReportAction} from '@libs/actions/Report'; import DateUtils from '@libs/DateUtils'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; @@ -26,6 +25,7 @@ import { getMostRecentIOURequestActionID, getOriginalMessage, getSortedReportActionsForDisplay, + isActionableWhisperRequiringWritePermission, isCreatedAction, isDeletedParentAction, isIOUActionMatchingTransactionList, From f641bafd9beacff0f733ee65ce0901ef7bdd7886 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Mon, 12 Jan 2026 10:06:57 +0100 Subject: [PATCH 09/32] Refactor telemetry settings for improved performance tracing and remove debug logging from visibleReportActions --- .../OnyxDerived/configs/visibleReportActions.ts | 11 ----------- src/setup/telemetry/index.ts | 8 ++++---- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts index 48691f18a9b52..060db71173b74 100644 --- a/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts +++ b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts @@ -6,9 +6,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportAction, ReportActions} from '@src/types/onyx'; import type {VisibleReportActionsDerivedValue} from '@src/types/onyx/DerivedValues'; -// DEBUG: Counter to track compute calls - remove after debugging -let computeCallCount = 0; - function getOrCreateReportVisibilityRecord(result: VisibleReportActionsDerivedValue, reportID: string): Record { if (!result[reportID]) { // eslint-disable-next-line no-param-reassign @@ -31,14 +28,6 @@ export default createOnyxDerivedValueConfig({ // shouldReportActionBeVisible uses global Onyx-connected variables internally. dependencies: [ONYXKEYS.COLLECTION.REPORT_ACTIONS, ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.SESSION], compute: ([allReportActions], {sourceValues, currentValue}): VisibleReportActionsDerivedValue => { - // DEBUG: Log compute calls - remove after debugging - computeCallCount++; - // eslint-disable-next-line no-console - console.log(`[DERIVED COMPUTE] visibleReportActions #${computeCallCount}`, { - trigger: sourceValues ? Object.keys(sourceValues).join(', ') : 'INITIAL', - timestamp: new Date().toISOString(), - }); - if (!allReportActions) { return {}; } diff --git a/src/setup/telemetry/index.ts b/src/setup/telemetry/index.ts index 2b2aaa1082bfc..ee2f3f07e3e18 100644 --- a/src/setup/telemetry/index.ts +++ b/src/setup/telemetry/index.ts @@ -15,10 +15,10 @@ export default function (): void { Sentry.init({ dsn: CONFIG.SENTRY_DSN, transport: isDevelopment() ? makeDebugTransport : undefined, - tracesSampleRate: 0, - profilesSampleRate: 0, - enableAutoPerformanceTracing: false, - enableUserInteractionTracing: false, + tracesSampleRate: 1.0, + profilesSampleRate: Platform.OS === 'android' ? 0 : 1.0, + enableAutoPerformanceTracing: true, + enableUserInteractionTracing: true, integrations, environment: CONFIG.ENVIRONMENT, release: `${pkg.name}@${pkg.version}`, From 6ce80ff1ed7bc92a5623abf463cd21d0fa6f7ef0 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Mon, 12 Jan 2026 11:02:45 +0100 Subject: [PATCH 10/32] Refactor report action visibility checks to handle undefined report IDs and improve logic for determining action visibility across multiple components. --- .../AttachmentCarousel/extractAttachments.ts | 5 ++++- .../LHNOptionsList/LHNOptionsList.tsx | 1 + src/libs/OptionsListUtils/index.ts | 2 +- src/libs/ReportActionsUtils.ts | 17 +++++++++++++---- src/libs/SearchUIUtils.ts | 3 ++- src/pages/home/ReportScreen.tsx | 14 ++++++++++---- 6 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 6dd90b415c2ec..df0d08946c195 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -117,7 +117,10 @@ function extractAttachments( return attachments.reverse(); } - const reportID = report?.reportID ?? ''; + const reportID = report?.reportID; + if (!reportID) { + return attachments.reverse(); + } const actions = [...(parentReportAction ? [parentReportAction] : []), ...getSortedReportActions(Object.values(reportActions ?? {}))]; for (const action of actions) { if (!isReportActionVisible(action, reportID, canUserPerformAction, visibleReportActionsData) || isMoneyRequestAction(action)) { diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index fea3d7b9014c7..6c192cff143ad 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -326,6 +326,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio isScreenFocused, localeCompare, translate, + visibleReportActionsData, ], ); diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index dc8f2ea9f58bc..03c7561536eec 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -666,7 +666,7 @@ function getLastMessageTextForReport({ lastMessageTextFromReport = formatReportLastMessageText(Parser.htmlToText(properSchemaForMoneyRequestMessage)); } else if (isReportPreviewAction(lastReportAction)) { const iouReport = getReportOrDraftReport(getIOUReportIDFromReportActionPreview(lastReportAction)); - const iouReportID = iouReport?.reportID ?? ''; + const iouReportID = iouReport?.reportID; const lastIOUMoneyReportAction = iouReportID ? allSortedReportActions[iouReportID]?.find( (reportAction): reportAction is ReportAction => diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index c5ccfe1a24ad1..4ed94fcfb29cc 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1152,8 +1152,11 @@ function isReportActionVisible( canUserPerformWriteAction?: boolean, visibleReportActions?: VisibleReportActionsDerivedValue, ): boolean { + if (!reportAction?.reportActionID) { + return false; + } if (visibleReportActions) { - const staticVisibility = visibleReportActions[reportID]?.[reportAction?.reportActionID ?? ''] ?? true; + const staticVisibility = visibleReportActions[reportID]?.[reportAction.reportActionID] ?? true; if (!staticVisibility) { return false; } @@ -1162,7 +1165,7 @@ function isReportActionVisible( } return true; } - return shouldReportActionBeVisible(reportAction, reportAction?.reportActionID ?? '', canUserPerformWriteAction); + return shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction); } /** @@ -1182,7 +1185,10 @@ function isReportActionVisibleAsLastAction( return false; } - const reportID = reportAction.reportID ?? ''; + const reportID = reportAction.reportID; + if (!reportID) { + return false; + } return ( isReportActionVisible(reportAction, reportID, canUserPerformWriteAction, visibleReportActions) && @@ -1376,7 +1382,10 @@ function getSortedReportActionsForDisplay( } else { filteredReportActions = Object.entries(reportActions) .filter(([, reportAction]) => { - const reportID = reportAction?.reportID ?? ''; + const reportID = reportAction?.reportID; + if (!reportID) { + return false; + } return isReportActionVisible(reportAction, reportID, canUserPerformWriteAction, visibleReportActionsData); }) .map(([, reportAction]) => reportAction); diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index fbf698d5e8a25..d8daf347aa6e8 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -1664,8 +1664,9 @@ function getReportActionsSections(data: OnyxTypes.SearchResults['data'], visible const isReportArchived = isArchivedReport(data[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]); const invoiceReceiverPolicy: OnyxTypes.Policy | undefined = report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS ? data[`${ONYXKEYS.COLLECTION.POLICY}${report.invoiceReceiver.policyID}`] : undefined; - const reportID = reportAction.reportID ?? ''; + const reportID = reportAction.reportID; if ( + !reportID || !isReportActionVisible(reportAction, reportID, canUserPerformWriteAction(report, isReportArchived), visibleReportActionsData) || isDeletedAction(reportAction) || isResolvedActionableWhisper(reportAction) || diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 4886dce131b64..fcd1aa8f6bafd 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -456,10 +456,16 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr const isReportArchived = useReportIsArchived(report?.reportID); const {isEditingDisabled, isCurrentReportLoadedFromOnyx} = useIsReportReadyToDisplay(report, reportIDFromRoute, isReportArchived); - const isLinkedActionDeleted = useMemo( - () => !!linkedAction && !isReportActionVisible(linkedAction, linkedAction.reportID ?? reportID ?? '', canUserPerformWriteAction(report, isReportArchived), visibleReportActionsData), - [linkedAction, report, isReportArchived, reportID, visibleReportActionsData], - ); + const isLinkedActionDeleted = useMemo(() => { + if (!linkedAction) { + return false; + } + const actionReportID = linkedAction.reportID ?? reportID; + if (!actionReportID) { + return true; + } + return !isReportActionVisible(linkedAction, actionReportID, canUserPerformWriteAction(report, isReportArchived), visibleReportActionsData); + }, [linkedAction, report, isReportArchived, reportID, visibleReportActionsData]); const prevIsLinkedActionDeleted = usePrevious(linkedAction ? isLinkedActionDeleted : undefined); From 706c53174b15d151dcb885534ae10d57efc685a8 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Mon, 12 Jan 2026 11:12:02 +0100 Subject: [PATCH 11/32] Refactor report ID handling in visibility checks to ensure proper evaluation of child report actions and improve overall logic consistency. --- src/libs/ReportUtils.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index cebf3d02fbfda..096c278ae9ffd 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -9054,11 +9054,14 @@ function shouldHideReport( const currentReport = getReportOrDraftReport(currentReportId); const parentReport = getParentReport(!isEmptyObject(currentReport) ? currentReport : undefined); const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`] ?? {}; - const reportID = report?.reportID ?? ''; - const isChildReportHasComment = Object.values(reportActions ?? {})?.some( - (reportAction) => - (reportAction?.childVisibleActionCount ?? 0) > 0 && isReportActionVisible(reportAction, reportID, canUserPerformWriteAction(report, isReportArchived), visibleReportActionsData), - ); + const reportID = report?.reportID; + const isChildReportHasComment = + !!reportID && + Object.values(reportActions ?? {})?.some( + (reportAction) => + (reportAction?.childVisibleActionCount ?? 0) > 0 && + isReportActionVisible(reportAction, reportID, canUserPerformWriteAction(report, isReportArchived), visibleReportActionsData), + ); return parentReport?.reportID !== report?.reportID && !isChildReportHasComment; } @@ -10108,8 +10111,8 @@ function navigateToLinkedReportAction( // Pop the thread report screen before navigating to the chat report. Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID)); - const reportID = ancestor.report.reportID ?? ''; - const isVisibleAction = isReportActionVisible(ancestor.reportAction, reportID, canUserPerformWrite, visibleReportActionsData); + const reportID = ancestor.report.reportID; + const isVisibleAction = !!reportID && isReportActionVisible(ancestor.reportAction, reportID, canUserPerformWrite, visibleReportActionsData); if (isVisibleAction && !isOffline) { // Pop the chat report screen before navigating to the linked report action. From 4947b093a0649c1e309f7c1f39f32cf72e11cfe2 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Mon, 12 Jan 2026 12:05:29 +0100 Subject: [PATCH 12/32] Add reportID to report actions in tests for consistency and improved visibility checks --- tests/actions/ReportTest.ts | 1 + tests/ui/components/LHNOptionsListTest.tsx | 24 ++++++++++++++++------ tests/unit/ReportActionsUtilsTest.ts | 21 +++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 530c2aabe1511..d9e1879a12dce 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -449,6 +449,7 @@ describe('actions/Report', () => { person: [{type: 'TEXT', style: 'strong', text: 'Test User'}], shouldShow: true, created: DateUtils.getDBTime(Date.now() - 3), + reportID: REPORT_ID, }; const optimisticReportActions: OnyxUpdate = { diff --git a/tests/ui/components/LHNOptionsListTest.tsx b/tests/ui/components/LHNOptionsListTest.tsx index b1804e2a2f49d..6642c67574bd0 100644 --- a/tests/ui/components/LHNOptionsListTest.tsx +++ b/tests/ui/components/LHNOptionsListTest.tsx @@ -210,12 +210,18 @@ describe('LHNOptionsList', () => { await Onyx.merge(ONYXKEYS.NETWORK, {isOffline: true}); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policy); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { - [submittedAction.reportActionID]: submittedAction, - }); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, { pendingExpenseAction: CONST.EXPENSE_PENDING_ACTION.SUBMIT, }); + + await Onyx.merge(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, { + [reportID]: { + [submittedAction.reportActionID]: true, + }, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { + [submittedAction.reportActionID]: submittedAction, + }); }); // When the LHNOptionsList is rendered @@ -261,12 +267,18 @@ describe('LHNOptionsList', () => { await Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policy); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { - [commentAction.reportActionID]: commentAction, - }); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, { pendingExpenseAction: CONST.EXPENSE_PENDING_ACTION.SUBMIT, }); + + await Onyx.merge(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, { + [reportID]: { + [commentAction.reportActionID]: true, + }, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { + [commentAction.reportActionID]: commentAction, + }); }); // When the LHNOptionsList is rendered diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index db2dea655d019..4e15cf0010ecf 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -570,6 +570,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-13 22:27:01.825', reportActionID: '8401445780099176', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { html: 'Hello world', @@ -586,6 +587,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-12 22:27:01.825', reportActionID: '6401435781022176', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, originalMessage: { html: 'Hello world', @@ -602,6 +604,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-11 22:27:01.825', reportActionID: '2962390724708756', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.IOU, originalMessage: { amount: 0, @@ -619,6 +622,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-10 22:27:01.825', reportActionID: '1609646094152486', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.RENAMED, originalMessage: { html: 'Hello world', @@ -637,6 +641,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-09 22:27:01.825', reportActionID: '8049485084562457', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FIELD, originalMessage: {}, message: [{html: 'updated the Approval Mode from "Submit and Approve" to "Submit and Close"', type: 'Action type', text: 'Action text'}], @@ -644,6 +649,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-08 22:27:06.825', reportActionID: '1661970171066216', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENT_QUEUED, originalMessage: { paymentType: 'ACH', @@ -653,6 +659,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-06 22:27:08.825', reportActionID: '1661970171066220', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.TASK_EDITED, originalMessage: { html: 'Hello world', @@ -675,6 +682,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-13 22:27:01.825', reportActionID: '8401445780099176', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { html: 'Hello world', @@ -691,6 +699,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-12 22:27:01.825', reportActionID: '6401435781022176', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, originalMessage: { html: 'Hello world', @@ -707,6 +716,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-11 22:27:01.825', reportActionID: '2962390724708756', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.IOU, originalMessage: { amount: 0, @@ -724,6 +734,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-10 22:27:01.825', reportActionID: '1609646094152486', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.RENAMED, originalMessage: { html: 'Hello world', @@ -742,6 +753,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-09 22:27:01.825', reportActionID: '1661970171066218', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.CLOSED, originalMessage: { policyName: 'default', // change to const @@ -770,6 +782,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-13 22:27:01.825', reportActionID: '8401445780099176', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { html: 'Hello world', @@ -786,6 +799,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-12 22:27:01.825', reportActionID: '8401445780099175', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { html: 'Hello world', @@ -797,6 +811,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-11 22:27:01.825', reportActionID: '8401445780099174', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { html: 'Hello world', @@ -819,6 +834,7 @@ describe('ReportActionsUtils', () => { { created: '2024-11-19 08:04:13.728', reportActionID: '1607371725956675966', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { html: '', @@ -838,6 +854,7 @@ describe('ReportActionsUtils', () => { { created: '2024-11-19 08:00:14.352', reportActionID: '4655978522337302598', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { html: '#join', @@ -856,6 +873,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-09 22:27:01.825', reportActionID: '8049485084562457', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_REPORT_MENTION_WHISPER, originalMessage: { lastModified: '2024-11-19 08:00:14.353', @@ -873,6 +891,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-12 22:27:01.825', reportActionID: '6401435781022176', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_MENTION_WHISPER, originalMessage: { inviteeAccountIDs: [18414674], @@ -972,6 +991,7 @@ describe('ReportActionsUtils', () => { ...LHNTestUtils.getFakeReportAction('email1@test.com', 3), created: '2023-08-01 16:00:00', reportActionID: 'action1', + reportID: '1', actionName: 'ADDCOMMENT', originalMessage: { html: 'Hello world', @@ -982,6 +1002,7 @@ describe('ReportActionsUtils', () => { ...LHNTestUtils.getFakeReportAction('email2@test.com', 3), created: '2023-08-01 18:00:00', reportActionID: 'action2', + reportID: '1', actionName: 'ADDCOMMENT', originalMessage: { html: 'Hello world', From 73a1b5651182de24d2142ee844d733bf501cf7c4 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Mon, 12 Jan 2026 12:16:41 +0100 Subject: [PATCH 13/32] Add reportID to additional report actions in SidebarUtils tests for consistency --- tests/unit/SidebarUtilsTest.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index a417d1dc3451b..0f22bcbe5d9e9 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -1814,6 +1814,7 @@ describe('SidebarUtils', () => { }; const lastAction: ReportAction = { ...createRandomReportAction(1), + reportID: '1', message: [ { type: 'COMMENT', @@ -1833,6 +1834,7 @@ describe('SidebarUtils', () => { }; const deletedAction: ReportAction = { ...createRandomReportAction(2), + reportID: '1', actionName: 'IOU', actorAccountID: 20337430, automatic: false, @@ -1912,6 +1914,7 @@ describe('SidebarUtils', () => { lastAction, lastActionReport: undefined, isReportArchived: undefined, + lastMessageTextFromReport: 'test action', }); expect(result?.alternateText).toContain(`${getReportActionMessageText(lastAction)}`); From 22c2debaf9c9f26391981776694dc5a9899b9107 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 14 Jan 2026 08:38:27 +0100 Subject: [PATCH 14/32] Added reportID to MOVED_TRANSACTION and ADD_COMMENT actions in ReportActionsUtilsTest --- tests/unit/ReportActionsUtilsTest.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index a5fb8697f1959..c687398de3010 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -682,6 +682,7 @@ describe('ReportActionsUtils', () => { const movedTransactionAction: ReportAction = { created: '2022-11-13 22:27:01.825', reportActionID: '8401445780099177', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.MOVED_TRANSACTION, originalMessage: { fromReportID: CONST.REPORT.UNREPORTED_REPORT_ID, @@ -692,6 +693,7 @@ describe('ReportActionsUtils', () => { const addCommentAction: ReportAction = { created: '2022-11-12 22:27:01.825', reportActionID: '6401435781022176', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { html: 'Hello world', From 3487e5790c91a891ba8aec9fbd6fe8725312c435 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Mon, 19 Jan 2026 08:30:56 +0100 Subject: [PATCH 15/32] Enhance report action visibility logic by adding check for actionable whispers requiring write permission --- .../OnyxDerived/configs/visibleReportActions.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts index 060db71173b74..d1dc3a8e2b9b8 100644 --- a/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts +++ b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts @@ -1,5 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; -import {isMovedTransactionAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; +import {isActionableWhisperRequiringWritePermission, isMovedTransactionAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -50,6 +50,9 @@ export default createOnyxDerivedValueConfig({ for (const [actionID, action] of Object.entries(reportActions)) { if (action) { + if (isActionableWhisperRequiringWritePermission(action)) { + continue; + } reportVisibility[actionID] = shouldReportActionBeVisible(action, actionID); } } @@ -76,6 +79,10 @@ export default createOnyxDerivedValueConfig({ } if (doesActionDependOnReportExistence(action)) { + if (isActionableWhisperRequiringWritePermission(action)) { + delete reportVisibility[actionID]; + continue; + } reportVisibility[actionID] = shouldReportActionBeVisible(action, actionID); } } @@ -107,6 +114,11 @@ export default createOnyxDerivedValueConfig({ continue; } + if (isActionableWhisperRequiringWritePermission(action)) { + delete reportVisibility[actionID]; + continue; + } + reportVisibility[actionID] = shouldReportActionBeVisible(action, actionID); } } From 2d9cb1554ab4a8495ecaed90f4f23645b88b8524 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Mon, 19 Jan 2026 09:16:59 +0100 Subject: [PATCH 16/32] Refactor report action visibility logic by introducing a caching skip check for actionable whispers and concierge category options. Remove unused policy variable in OptionsListUtils and ReportActionsView. --- src/libs/OptionsListUtils/index.ts | 1 - .../configs/visibleReportActions.ts | 21 +++++++++++++++---- src/pages/home/report/ReportActionsView.tsx | 1 - 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 073b9075a584a..14d2e3e6a7d59 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -306,7 +306,6 @@ Onyx.connect({ const reportNameValuePairs = allReportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`]; const isReportArchived = !!reportNameValuePairs?.private_isArchived; const isWriteActionAllowed = canUserPerformWriteAction(report, isReportArchived); - const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; // The report is only visible if it is the last action not deleted that // does not match a closed or created state. diff --git a/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts index d1dc3a8e2b9b8..213b3121b8e84 100644 --- a/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts +++ b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts @@ -1,5 +1,10 @@ import type {OnyxEntry} from 'react-native-onyx'; -import {isActionableWhisperRequiringWritePermission, isMovedTransactionAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; +import { + isActionableWhisperRequiringWritePermission, + isConciergeCategoryOptions, + isMovedTransactionAction, + shouldReportActionBeVisible, +} from '@libs/ReportActionsUtils'; import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -21,6 +26,14 @@ function doesActionDependOnReportExistence(action: ReportAction): boolean { return isUnreportedTransaction || isMovedTransaction; } +/** + * Returns true if the action's visibility depends on runtime context that can't be cached, + * such as write permissions or policy settings. + */ +function shouldSkipCachingAction(action: ReportAction): boolean { + return isActionableWhisperRequiringWritePermission(action) || isConciergeCategoryOptions(action); +} + export default createOnyxDerivedValueConfig({ key: ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, // Note: REPORT and SESSION dependencies are needed to trigger recompute when reports change @@ -50,7 +63,7 @@ export default createOnyxDerivedValueConfig({ for (const [actionID, action] of Object.entries(reportActions)) { if (action) { - if (isActionableWhisperRequiringWritePermission(action)) { + if (shouldSkipCachingAction(action)) { continue; } reportVisibility[actionID] = shouldReportActionBeVisible(action, actionID); @@ -79,7 +92,7 @@ export default createOnyxDerivedValueConfig({ } if (doesActionDependOnReportExistence(action)) { - if (isActionableWhisperRequiringWritePermission(action)) { + if (shouldSkipCachingAction(action)) { delete reportVisibility[actionID]; continue; } @@ -114,7 +127,7 @@ export default createOnyxDerivedValueConfig({ continue; } - if (isActionableWhisperRequiringWritePermission(action)) { + if (shouldSkipCachingAction(action)) { delete reportVisibility[actionID]; continue; } diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 4a60b4884e6e0..0a72d597aa5c5 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -109,7 +109,6 @@ function ReportActionsView({ const reportPreviewAction = useMemo(() => getReportPreviewAction(report.chatReportID, report.reportID), [report.chatReportID, report.reportID]); const didLayout = useRef(false); const {isOffline} = useNetwork(); - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, {canBeMissing: true}); const {shouldUseNarrowLayout} = useResponsiveLayout(); const isFocused = useIsFocused(); From af2c5b360c998e628cadb95cb3c93370a2224495 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Mon, 19 Jan 2026 09:29:44 +0100 Subject: [PATCH 17/32] prettier fix --- .../actions/OnyxDerived/configs/visibleReportActions.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts index 213b3121b8e84..6dd27b6c8cc8c 100644 --- a/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts +++ b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts @@ -1,10 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; -import { - isActionableWhisperRequiringWritePermission, - isConciergeCategoryOptions, - isMovedTransactionAction, - shouldReportActionBeVisible, -} from '@libs/ReportActionsUtils'; +import {isActionableWhisperRequiringWritePermission, isConciergeCategoryOptions, isMovedTransactionAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; From 81b472e4eb966dedcf4137eeeb5124292117cec8 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Tue, 20 Jan 2026 12:42:55 +0100 Subject: [PATCH 18/32] Add visible report actions data to various components and utility functions --- .../LHNOptionsList/OptionRowLHNData.tsx | 3 +++ src/components/Search/index.tsx | 4 ++++ src/libs/OptionsListUtils/index.ts | 19 +++++++++++++++---- src/libs/SearchUIUtils.ts | 10 +++++++--- src/libs/SidebarUtils.ts | 14 ++++++++++++-- tests/unit/OptionsListUtilsTest.tsx | 17 +++++++++-------- 6 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index 0fd1032c26281..c473f8efb372e 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -51,6 +51,7 @@ function OptionRowLHNData({ const [movedFromReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.FROM)}`, {canBeMissing: true}); const [movedToReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.TO)}`, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); // Check the report errors equality to avoid re-rendering when there are no changes const prevReportErrors = usePrevious(reportAttributes?.reportErrors); const areReportErrorsEqual = useMemo(() => deepEqual(prevReportErrors, reportAttributes?.reportErrors), [prevReportErrors, reportAttributes?.reportErrors]); @@ -78,6 +79,7 @@ function OptionRowLHNData({ movedFromReport, movedToReport, currentUserAccountID, + visibleReportActionsData: visibleReportActionsData ?? {}, }); if (deepEqual(item, optionItemRef.current)) { return optionItemRef.current; @@ -114,6 +116,7 @@ function OptionRowLHNData({ movedFromReport, movedToReport, currentUserAccountID, + visibleReportActionsData, ]); return ( diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index f7348a64ddbe9..a91343f788438 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -242,6 +242,7 @@ function Search({ const previousTransactions = usePrevious(transactions); const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {canBeMissing: true}); const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const [violations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const {accountID, email, login} = useCurrentUserPersonalDetails(); @@ -398,6 +399,7 @@ function Search({ isActionLoadingSet, cardFeeds, allTransactionViolations: violations, + visibleReportActionsData, }); return [filteredData1, filteredData1.length, allLength]; }, [ @@ -418,6 +420,7 @@ function Search({ policies, bankAccountList, violations, + visibleReportActionsData, ]); // For group-by views, each grouped item has a transactionsQueryJSON with a hash pointing to a separate snapshot @@ -453,6 +456,7 @@ function Search({ translate, formatPhoneNumber, isActionLoadingSet, + visibleReportActionsData: visibleReportActionsData ?? {}, }); return {...item, transactions: transactions1 as TransactionListItemType[]}; }); diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index b922957249ca1..f9c0bd63b73d3 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -494,7 +494,13 @@ function getAlternateText( const isGroupChat = reportUtilsIsGroupChat(report); const isExpenseThread = isMoneyRequest(report); const formattedLastMessageText = - formatReportLastMessageText(Parser.htmlToText(option.lastMessageText ?? '')) || getLastMessageTextForReport({report, lastActorDetails, isReportArchived}); + formatReportLastMessageText(Parser.htmlToText(option.lastMessageText ?? '')) || + getLastMessageTextForReport({ + report, + lastActorDetails, + isReportArchived, + visibleReportActionsDataParam: visibleReportActionsData ?? {}, + }); const reportPrefix = getReportSubtitlePrefix(report); const formattedLastMessageTextWithPrefix = reportPrefix + formattedLastMessageText; @@ -633,7 +639,7 @@ function getLastMessageTextForReport({ isReportArchived?: boolean; policyForMovingExpensesID?: string; reportMetadata?: OnyxEntry; - visibleReportActionsDataParam?: VisibleReportActionsDerivedValue; + visibleReportActionsDataParam: VisibleReportActionsDerivedValue; }): string { const reportID = report?.reportID; const lastReportAction = reportID ? lastVisibleReportActions[reportID] : undefined; @@ -677,7 +683,7 @@ function getLastMessageTextForReport({ const lastIOUMoneyReportAction = iouReportID ? allSortedReportActions[iouReportID]?.find( (reportAction): reportAction is ReportAction => - isReportActionVisible(reportAction, iouReportID, canUserPerformWriteAction(report, isReportArchived), visibleReportActionsData) && + isReportActionVisible(reportAction, iouReportID, canUserPerformWriteAction(report, isReportArchived), visibleReportActionsDataParam) && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isMoneyRequestAction(reportAction), ) @@ -984,7 +990,12 @@ function createOption( // If displaying chat preview line is needed, let's overwrite the default alternate text const lastActorDetails = personalDetails?.[report?.lastActorAccountID ?? String(CONST.DEFAULT_NUMBER_ID)] ?? {}; - result.lastMessageText = getLastMessageTextForReport({report, lastActorDetails, isReportArchived: !!result.private_isArchived}); + result.lastMessageText = getLastMessageTextForReport({ + report, + lastActorDetails, + isReportArchived: !!result.private_isArchived, + visibleReportActionsDataParam: visibleReportActionsData ?? {}, + }); result.alternateText = showPersonalDetails && personalDetail?.login ? personalDetail.login diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index b3f36fb075ff7..3f71dcd745f75 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -367,6 +367,7 @@ type GetSectionsParams = { isActionLoadingSet?: ReadonlySet; cardFeeds?: OnyxCollection; allTransactionViolations?: OnyxCollection; + visibleReportActionsData: OnyxTypes.VisibleReportActionsDerivedValue; }; /** @@ -1642,7 +1643,7 @@ function createAndOpenSearchTransactionThread( * * Do not use directly, use only via `getSections()` facade. */ -function getReportActionsSections(data: OnyxTypes.SearchResults['data'], visibleReportActionsData?: OnyxTypes.VisibleReportActionsDerivedValue): [ReportActionListItemType[], number] { +function getReportActionsSections(data: OnyxTypes.SearchResults['data'], visibleReportActionsData: OnyxTypes.VisibleReportActionsDerivedValue): [ReportActionListItemType[], number] { const reportActionItems: ReportActionListItemType[] = []; const transactions = Object.keys(data) @@ -2097,9 +2098,10 @@ function getSections({ isActionLoadingSet, cardFeeds, allTransactionViolations, + visibleReportActionsData, }: GetSectionsParams) { if (type === CONST.SEARCH.DATA_TYPES.CHAT) { - return getReportActionsSections(data); + return getReportActionsSections(data, visibleReportActionsData); } if (type === CONST.SEARCH.DATA_TYPES.TASK) { return getTaskSections(data, formatPhoneNumber, archivedReportsIDList); @@ -3293,7 +3295,9 @@ function getColumnsToShow( return result; } - const {moneyRequestReportActionsByTransactionID} = Array.isArray(data) ? {} : createReportActionsLookupMaps(data); + const {moneyRequestReportActionsByTransactionID} = Array.isArray(data) + ? {moneyRequestReportActionsByTransactionID: new Map()} + : createReportActionsLookupMaps(data); const updateColumns = (transaction: OnyxTypes.Transaction) => { const merchant = transaction.modifiedMerchant ? transaction.modifiedMerchant : (transaction.merchant ?? ''); if ((merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && merchant !== CONST.TRANSACTION.DEFAULT_MERCHANT) || isScanning(transaction)) { diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 7b6cf51eba613..df74aab1b9651 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -5,7 +5,7 @@ import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleCon import type {PartialPolicyForSidebar, ReportsToDisplayInLHN} from '@hooks/useSidebarOrderedReports'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Card, PersonalDetails, PersonalDetailsList, ReportActions, ReportAttributesDerivedValue, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {Card, PersonalDetails, PersonalDetailsList, ReportActions, ReportAttributesDerivedValue, ReportNameValuePairs, Transaction, TransactionViolation, VisibleReportActionsDerivedValue} from '@src/types/onyx'; import type Beta from '@src/types/onyx/Beta'; import type {ReportAttributes} from '@src/types/onyx/DerivedValues'; import type {Errors} from '@src/types/onyx/OnyxCommon'; @@ -652,6 +652,7 @@ function getOptionData({ movedFromReport, movedToReport, currentUserAccountID, + visibleReportActionsData, }: { report: OnyxEntry; oneTransactionThreadReport: OnyxEntry; @@ -671,6 +672,7 @@ function getOptionData({ movedFromReport?: OnyxEntry; movedToReport?: OnyxEntry; currentUserAccountID: number; + visibleReportActionsData: VisibleReportActionsDerivedValue; }): OptionData | undefined { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for // this method to be called after the Onyx data has been cleared out. In that case, it's fine to do @@ -801,7 +803,15 @@ function getOptionData({ const lastActorDisplayName = getLastActorDisplayName(lastActorDetails, currentUserAccountID); let lastMessageTextFromReport = lastMessageTextFromReportProp; if (!lastMessageTextFromReport) { - lastMessageTextFromReport = getLastMessageTextForReport({report, lastActorDetails, movedFromReport, movedToReport, policy, isReportArchived}); + lastMessageTextFromReport = getLastMessageTextForReport({ + report, + lastActorDetails, + movedFromReport, + movedToReport, + policy, + isReportArchived, + visibleReportActionsDataParam: visibleReportActionsData, + }); } // We need to remove sms domain in case the last message text has a phone number mention with sms domain. diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index f6a65d5d31ff6..b875e7785d440 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -2713,7 +2713,7 @@ describe('OptionsListUtils', () => { [iouAction.reportActionID]: iouAction, }); await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); - const lastMessage = getLastMessageTextForReport({report, lastActorDetails: null, isReportArchived: false}); + const lastMessage = getLastMessageTextForReport({report, lastActorDetails: null, isReportArchived: false, visibleReportActionsDataParam: {}}); const reportPreviewMessage = getReportPreviewMessage(iouReport, iouAction, true, false, null, true, reportPreviewAction); const expected = formatReportLastMessageText(Parser.htmlToText(reportPreviewMessage)); expect(lastMessage).toBe(expected); @@ -2758,7 +2758,7 @@ describe('OptionsListUtils', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { [submittedAction.reportActionID]: submittedAction, }); - const lastMessage = getLastMessageTextForReport({report, lastActorDetails: null, isReportArchived: false}); + const lastMessage = getLastMessageTextForReport({report, lastActorDetails: null, isReportArchived: false, visibleReportActionsDataParam: {}}); expect(lastMessage).toBe(Parser.htmlToText(translate(CONST.LOCALES.EN, 'iou.automaticallySubmitted'))); }); }); @@ -2777,7 +2777,7 @@ describe('OptionsListUtils', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { [approvedAction.reportActionID]: approvedAction, }); - const lastMessage = getLastMessageTextForReport({report, lastActorDetails: null, isReportArchived: false}); + const lastMessage = getLastMessageTextForReport({report, lastActorDetails: null, isReportArchived: false, visibleReportActionsDataParam: {}}); expect(lastMessage).toBe(Parser.htmlToText(translate(CONST.LOCALES.EN, 'iou.automaticallyApproved'))); }); }); @@ -2796,7 +2796,7 @@ describe('OptionsListUtils', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { [forwardedAction.reportActionID]: forwardedAction, }); - const lastMessage = getLastMessageTextForReport({report, lastActorDetails: null, isReportArchived: false}); + const lastMessage = getLastMessageTextForReport({report, lastActorDetails: null, isReportArchived: false, visibleReportActionsDataParam: {}}); expect(lastMessage).toBe(Parser.htmlToText(translate(CONST.LOCALES.EN, 'iou.automaticallyForwarded'))); }); }); @@ -2812,7 +2812,7 @@ describe('OptionsListUtils', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { [corporateForceUpgradeAction.reportActionID]: corporateForceUpgradeAction, }); - const lastMessage = getLastMessageTextForReport({report, lastActorDetails: null, isReportArchived: false}); + const lastMessage = getLastMessageTextForReport({report, lastActorDetails: null, isReportArchived: false, visibleReportActionsDataParam: {}}); expect(lastMessage).toBe(Parser.htmlToText(translate(CONST.LOCALES.EN, 'workspaceActions.forcedCorporateUpgrade'))); }); }); @@ -2897,6 +2897,7 @@ describe('OptionsListUtils', () => { report, lastActorDetails: null, isReportArchived: false, + visibleReportActionsDataParam: {}, }); expect(result).toBe(expectedVisibleText); }); @@ -2935,7 +2936,7 @@ describe('OptionsListUtils', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { [submittedAction.reportActionID]: submittedAction, }); - const lastMessage = getLastMessageTextForReport({report, lastActorDetails: null, isReportArchived: false, policy, reportMetadata}); + const lastMessage = getLastMessageTextForReport({report, lastActorDetails: null, isReportArchived: false, policy, reportMetadata, visibleReportActionsDataParam: {}}); expect(lastMessage).toBe(translate(CONST.LOCALES.EN, 'iou.queuedToSubmitViaDEW')); }); @@ -2961,7 +2962,7 @@ describe('OptionsListUtils', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { [dewSubmitFailedAction.reportActionID]: dewSubmitFailedAction, }); - const lastMessage = getLastMessageTextForReport({report, lastActorDetails: null, isReportArchived: false}); + const lastMessage = getLastMessageTextForReport({report, lastActorDetails: null, isReportArchived: false, visibleReportActionsDataParam: {}}); expect(lastMessage).toBe(customErrorMessage); }); @@ -2984,7 +2985,7 @@ describe('OptionsListUtils', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { [dewSubmitFailedAction.reportActionID]: dewSubmitFailedAction, }); - const lastMessage = getLastMessageTextForReport({report, lastActorDetails: null, isReportArchived: false}); + const lastMessage = getLastMessageTextForReport({report, lastActorDetails: null, isReportArchived: false, visibleReportActionsDataParam: {}}); expect(lastMessage).toBe(translate(CONST.LOCALES.EN, 'iou.error.genericCreateFailureMessage')); }); }); From eaab49c195919654a781c6aecb0dddbf027baaba Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Tue, 20 Jan 2026 13:10:34 +0100 Subject: [PATCH 19/32] Add visible report actions data to Search components and utility functions --- .../Search/SearchAutocompleteList.tsx | 6 +- .../Search/SearchFiltersChatsSelector.tsx | 11 +-- src/hooks/useSearchSelector.base.ts | 2 + src/libs/OptionsListUtils/index.ts | 90 ++++++++++++------- src/libs/actions/Report.ts | 14 +-- src/pages/Share/ShareTab.tsx | 2 + .../PopoverReportActionContextMenu.tsx | 4 +- .../report/ReportActionItemMessageEdit.tsx | 3 + 8 files changed, 83 insertions(+), 49 deletions(-) diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index b1e206af74e44..2e34d826a5b1e 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -195,6 +195,7 @@ function SearchAutocompleteList({ const [recentSearches] = useOnyx(ONYXKEYS.RECENT_SEARCHES, {canBeMissing: true}); const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const expensifyIcons = useMemoizedLazyExpensifyIcons(['History', 'MagnifyingGlass']); const {options, areOptionsInitialized} = useOptionsList(); @@ -218,8 +219,9 @@ function SearchAutocompleteList({ shouldShowGBR: false, shouldUnreadBeBold: true, loginList, + visibleReportActionsData: visibleReportActionsData ?? {}, }); - }, [areOptionsInitialized, options, draftComments, nvpDismissedProductTraining, betas, autocompleteQueryValue, countryCode, loginList]); + }, [areOptionsInitialized, options, draftComments, nvpDismissedProductTraining, betas, autocompleteQueryValue, countryCode, loginList, visibleReportActionsData]); const [isInitialRender, setIsInitialRender] = useState(true); const parsedQuery = useMemo(() => parseForAutocomplete(autocompleteQueryValue), [autocompleteQueryValue]); @@ -428,6 +430,7 @@ function SearchAutocompleteList({ countryCode, loginList, shouldShowGBR: true, + visibleReportActionsData: visibleReportActionsData ?? {}, }).personalDetails.filter((participant) => participant.text && !alreadyAutocompletedKeys.has(participant.text.toLowerCase())); return participants.map((participant) => ({ @@ -459,6 +462,7 @@ function SearchAutocompleteList({ countryCode, loginList, shouldShowGBR: true, + visibleReportActionsData: visibleReportActionsData ?? {}, }).recentReports.filter((chat) => { if (!chat.text) { return false; diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index 2d8f438aeb86c..32bf8a25a8138 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -60,22 +60,23 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const archivedReportsIdSet = useArchivedReportsIdSet(); const [nvpDismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const selectedOptions = useMemo(() => { return selectedReportIDs.map((id) => { - const report = getSelectedOptionData(createOptionFromReport({...reports?.[`${ONYXKEYS.COLLECTION.REPORT}${id}`], reportID: id}, personalDetails, reportAttributesDerived)); + const report = getSelectedOptionData(createOptionFromReport({...reports?.[`${ONYXKEYS.COLLECTION.REPORT}${id}`], reportID: id}, personalDetails, reportAttributesDerived, undefined, visibleReportActionsData ?? {})); const isReportArchived = archivedReportsIdSet.has(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`); - const alternateText = getAlternateText(report, {}, isReportArchived, {}); + const alternateText = getAlternateText(report, {}, isReportArchived, {}, visibleReportActionsData ?? {}); return {...report, alternateText}; }); - }, [archivedReportsIdSet, personalDetails, reportAttributesDerived, reports, selectedReportIDs]); + }, [archivedReportsIdSet, personalDetails, reportAttributesDerived, reports, selectedReportIDs, visibleReportActionsData]); const defaultOptions = useMemo(() => { if (!areOptionsInitialized || !isScreenTransitionEnd) { return defaultListOptions; } - return getSearchOptions({options, draftComments, nvpDismissedProductTraining, betas: undefined, isUsedInChatFinder: false, countryCode, loginList}); - }, [areOptionsInitialized, isScreenTransitionEnd, options, draftComments, nvpDismissedProductTraining, countryCode, loginList]); + return getSearchOptions({options, draftComments, nvpDismissedProductTraining, betas: undefined, isUsedInChatFinder: false, countryCode, loginList, visibleReportActionsData: visibleReportActionsData ?? {}}); + }, [areOptionsInitialized, isScreenTransitionEnd, options, draftComments, nvpDismissedProductTraining, countryCode, loginList, visibleReportActionsData]); const chatOptions = useMemo(() => { return filterAndOrderOptions(defaultOptions, cleanSearchTerm, countryCode, loginList, { diff --git a/src/hooks/useSearchSelector.base.ts b/src/hooks/useSearchSelector.base.ts index 41d37faf3e5f1..70ed07afacf74 100644 --- a/src/hooks/useSearchSelector.base.ts +++ b/src/hooks/useSearchSelector.base.ts @@ -165,6 +165,7 @@ function useSearchSelectorBase({ const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const [nvpDismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const onListEndReached = useDebounce( useCallback(() => { @@ -196,6 +197,7 @@ function useSearchSelectorBase({ includeUserToInvite, countryCode, loginList, + visibleReportActionsData: visibleReportActionsData ?? {}, }); case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_MEMBER_INVITE: return getValidOptions(optionsWithContacts, allPolicies, draftComments, nvpDismissedProductTraining, loginList, { diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index f9c0bd63b73d3..c8e7fbf6ada82 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -248,14 +248,6 @@ Onyx.connect({ }, }); -let visibleReportActionsData: VisibleReportActionsDerivedValue | undefined; -Onyx.connect({ - key: ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, - callback: (value) => { - visibleReportActionsData = value ?? undefined; - }, -}); - const lastReportActions: ReportActions = {}; const allSortedReportActions: Record = {}; let allReportActions: OnyxCollection; @@ -270,13 +262,8 @@ Onyx.connect({ allReportActions = actions ?? {}; - // Skip processing if derived value is not ready yet - will be processed when derived value loads - if (!visibleReportActionsData) { - return; - } - - // Capture the current value of visibleReportActionsData to avoid closure issues - const currentVisibleReportActionsData = visibleReportActionsData; + // Use empty object as fallback since visibleReportActionsData is now passed via parameters + const currentVisibleReportActionsData: VisibleReportActionsDerivedValue = {}; // Iterate over the report actions to build the sorted and lastVisible report actions objects for (const reportActions of Object.entries(allReportActions)) { @@ -487,6 +474,7 @@ function getAlternateText( {showChatPreviewLine = false, forcePolicyNamePreview = false}: PreviewConfig, isReportArchived: boolean | undefined, lastActorDetails: Partial | null = {}, + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ) { const report = getReportOrDraftReport(option.reportID); const isAdminRoom = reportUtilsIsAdminRoom(report); @@ -499,7 +487,7 @@ function getAlternateText( report, lastActorDetails, isReportArchived, - visibleReportActionsDataParam: visibleReportActionsData ?? {}, + visibleReportActionsDataParam: visibleReportActionsData, }); const reportPrefix = getReportSubtitlePrefix(report); const formattedLastMessageTextWithPrefix = reportPrefix + formattedLastMessageText; @@ -915,6 +903,7 @@ function createOption( config?: PreviewConfig, reportAttributesDerived?: ReportAttributesDerivedValue['reports'], privateIsArchived?: string, + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ): SearchOptionData { const {showChatPreviewLine = false, forcePolicyNamePreview = false, showPersonalDetails = false, selected, isSelected, isDisabled} = config ?? {}; @@ -994,12 +983,12 @@ function createOption( report, lastActorDetails, isReportArchived: !!result.private_isArchived, - visibleReportActionsDataParam: visibleReportActionsData ?? {}, + visibleReportActionsDataParam: visibleReportActionsData, }); result.alternateText = showPersonalDetails && personalDetail?.login ? personalDetail.login - : getAlternateText(result, {showChatPreviewLine, forcePolicyNamePreview}, !!result.private_isArchived, lastActorDetails); + : getAlternateText(result, {showChatPreviewLine, forcePolicyNamePreview}, !!result.private_isArchived, lastActorDetails, visibleReportActionsData); const personalDetailsForCompute: PersonalDetailsList | undefined = personalDetails ?? undefined; const computedReportName = computeReportName( @@ -1060,6 +1049,7 @@ function getReportOption( policy: OnyxEntry, reportAttributesDerived?: ReportAttributesDerivedValue['reports'], reportDrafts?: OnyxCollection, + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ): OptionData { const report = getReportOrDraftReport(participant.reportID, undefined, undefined, reportDrafts); const visibleParticipantAccountIDs = getParticipantsAccountIDsForDisplay(report, true); @@ -1074,6 +1064,7 @@ function getReportOption( }, reportAttributesDerived, privateIsArchived, + visibleReportActionsData, ); // Update text & alternateText because createOption returns workspace name only if report is owned by the user @@ -1115,6 +1106,7 @@ function getReportDisplayOption( unknownUserDetails: OnyxEntry, privateIsArchived: string | undefined, reportAttributesDerived?: ReportAttributesDerivedValue['reports'], + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ): OptionData { const visibleParticipantAccountIDs = getParticipantsAccountIDsForDisplay(report, true); @@ -1128,6 +1120,7 @@ function getReportDisplayOption( }, reportAttributesDerived, privateIsArchived, + visibleReportActionsData, ); // Update text & alternateText because createOption returns workspace name only if report is owned by the user @@ -1156,7 +1149,11 @@ function getReportDisplayOption( /** * Get the option for a policy expense report. */ -function getPolicyExpenseReportOption(participant: Participant | SearchOptionData, reportAttributesDerived?: ReportAttributesDerivedValue['reports']): SearchOptionData { +function getPolicyExpenseReportOption( + participant: Participant | SearchOptionData, + reportAttributesDerived?: ReportAttributesDerivedValue['reports'], + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, +): SearchOptionData { const expenseReport = reportUtilsIsPolicyExpenseChat(participant) ? getReportOrDraftReport(participant.reportID) : null; const visibleParticipantAccountIDs = Object.entries(expenseReport?.participants ?? {}) @@ -1172,6 +1169,8 @@ function getPolicyExpenseReportOption(participant: Participant | SearchOptionDat forcePolicyNamePreview: false, }, reportAttributesDerived, + undefined, + visibleReportActionsData, ); // Update text & alternateText because createOption returns workspace name only if report is owned by the user @@ -1281,6 +1280,7 @@ function processReport( report: OnyxEntry | null, personalDetails: OnyxEntry, reportAttributesDerived?: ReportAttributesDerivedValue['reports'], + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ): { reportMapEntry?: [number, Report]; // The entry to add to reportMapForAccountIDs if applicable reportOption: SearchOption | null; // The report option to add to allReportOptions if applicable @@ -1304,12 +1304,17 @@ function processReport( reportMapEntry, reportOption: { item: report, - ...createOption(accountIDs, personalDetails, report, undefined, reportAttributesDerived), + ...createOption(accountIDs, personalDetails, report, undefined, reportAttributesDerived, undefined, visibleReportActionsData), }, }; } -function createOptionList(personalDetails: OnyxEntry, reports?: OnyxCollection, reportAttributesDerived?: ReportAttributesDerivedValue['reports']) { +function createOptionList( + personalDetails: OnyxEntry, + reports?: OnyxCollection, + reportAttributesDerived?: ReportAttributesDerivedValue['reports'], + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, +) { const span = Sentry.startInactiveSpan({name: 'createOptionList'}); const reportMapForAccountIDs: Record = {}; @@ -1317,7 +1322,7 @@ function createOptionList(personalDetails: OnyxEntry, repor if (reports) { for (const report of Object.values(reports)) { - const {reportMapEntry, reportOption} = processReport(report, personalDetails, reportAttributesDerived); + const {reportMapEntry, reportOption} = processReport(report, personalDetails, reportAttributesDerived, visibleReportActionsData); if (reportMapEntry) { const [accountID, reportValue] = reportMapEntry; @@ -1340,6 +1345,8 @@ function createOptionList(personalDetails: OnyxEntry, repor showPersonalDetails: true, }, reportAttributesDerived, + undefined, + visibleReportActionsData, ), })); @@ -1378,6 +1385,7 @@ function createFilteredOptionList( searchTerm?: string; betas?: OnyxEntry; } = {}, + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ) { const {maxRecentReports = 500, includeP2P = true, searchTerm = ''} = options; const reportMapForAccountIDs: Record = {}; @@ -1427,7 +1435,7 @@ function createFilteredOptionList( // Step 5: Process the limited set of reports (performance optimization) const reportOptions: Array> = []; for (const report of limitedReports) { - const {reportMapEntry, reportOption} = processReport(report, personalDetails, reportAttributesDerived); + const {reportMapEntry, reportOption} = processReport(report, personalDetails, reportAttributesDerived, visibleReportActionsData); if (reportMapEntry) { const [accountID, reportValue] = reportMapEntry; @@ -1457,7 +1465,7 @@ function createFilteredOptionList( return { item: personalDetail, - ...createOption([accountID], personalDetails, reportMapForAccountIDs[accountID], {showPersonalDetails: true}, reportAttributesDerived), + ...createOption([accountID], personalDetails, reportMapForAccountIDs[accountID], {showPersonalDetails: true}, reportAttributesDerived, undefined, visibleReportActionsData), }; }) : []; @@ -1468,12 +1476,18 @@ function createFilteredOptionList( }; } -function createOptionFromReport(report: Report, personalDetails: OnyxEntry, reportAttributesDerived?: ReportAttributesDerivedValue['reports'], config?: PreviewConfig) { +function createOptionFromReport( + report: Report, + personalDetails: OnyxEntry, + reportAttributesDerived?: ReportAttributesDerivedValue['reports'], + config?: PreviewConfig, + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, +) { const accountIDs = getParticipantsAccountIDsForDisplay(report); return { item: report, - ...createOption(accountIDs, personalDetails, report, config, reportAttributesDerived), + ...createOption(accountIDs, personalDetails, report, config, reportAttributesDerived, undefined, visibleReportActionsData), }; } @@ -1758,7 +1772,8 @@ function getUserToInviteOption({ shouldAcceptName = false, countryCode = CONST.DEFAULT_COUNTRY_CODE, loginList = {}, -}: GetUserToInviteConfig): SearchOptionData | null { + visibleReportActionsData = {}, +}: GetUserToInviteConfig & {visibleReportActionsData?: VisibleReportActionsDerivedValue}): SearchOptionData | null { if (!searchValue) { return null; } @@ -1788,7 +1803,7 @@ function getUserToInviteOption({ }; const userToInvite = createOption([optimisticAccountID], personalDetailsExtended, null, { showChatPreviewLine, - }); + }, undefined, undefined, visibleReportActionsData); userToInvite.isOptimisticAccount = true; userToInvite.login = searchValue; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -2051,7 +2066,12 @@ function isValidReport(option: SearchOption, policy: OnyxEntry, * @param config - Configuration object specifying display preferences and filtering criteria * @returns Array of enriched and filtered report options ready for UI display */ -function prepareReportOptionsForDisplay(options: Array>, policiesCollection: OnyxCollection, config: GetValidReportsConfig): Array> { +function prepareReportOptionsForDisplay( + options: Array>, + policiesCollection: OnyxCollection, + config: GetValidReportsConfig, + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, +): Array> { const { showChatPreviewLine = false, forcePolicyNamePreview = false, @@ -2080,7 +2100,7 @@ function prepareReportOptionsForDisplay(options: Array>, po * By default, generated options does not have the chat preview line enabled. * If showChatPreviewLine or forcePolicyNamePreview are true, let's generate and overwrite the alternate text. */ - const alternateText = getAlternateText(option, {showChatPreviewLine, forcePolicyNamePreview}, !!option.private_isArchived); + const alternateText = getAlternateText(option, {showChatPreviewLine, forcePolicyNamePreview}, !!option.private_isArchived, null, visibleReportActionsData); const isSelected = isReportSelected(option, selectedOptions); let isOptionUnread = option.isUnread; @@ -2215,6 +2235,7 @@ function getValidOptions( ...config }: GetOptionsConfig = {}, countryCode: number = CONST.DEFAULT_COUNTRY_CODE, + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ): Options { const restrictedLogins = getRestrictedLogins(config, options, canShowManagerMcTest, nvpDismissedProductTraining); @@ -2294,7 +2315,7 @@ function getValidOptions( shouldSeparateSelfDMChat, shouldSeparateWorkspaceChat, shouldShowGBR, - }).at(0); + }, visibleReportActionsData).at(0); } if (maxRecentReportElements) { @@ -2307,7 +2328,7 @@ function getValidOptions( shouldSeparateSelfDMChat, shouldSeparateWorkspaceChat, shouldShowGBR, - }); + }, visibleReportActionsData); workspaceChats = prepareReportOptionsForDisplay(workspaceChats, policiesCollection, { ...getValidReportsConfig, @@ -2431,6 +2452,7 @@ type SearchOptionsConfig = { shouldShowGBR?: boolean; shouldUnreadBeBold?: boolean; loginList: OnyxEntry; + visibleReportActionsData?: VisibleReportActionsDerivedValue; }; /** @@ -2452,6 +2474,7 @@ function getSearchOptions({ shouldShowGBR = false, shouldUnreadBeBold = false, loginList, + visibleReportActionsData = {}, }: SearchOptionsConfig): Options { Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); @@ -2484,6 +2507,7 @@ function getSearchOptions({ shouldUnreadBeBold, }, countryCode, + visibleReportActionsData, ); Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS); @@ -2581,6 +2605,7 @@ function getMemberInviteOptions( excludeLogins: Record = {}, includeSelectedOptions = false, countryCode: number = CONST.DEFAULT_COUNTRY_CODE, + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ): Options { return getValidOptions( {personalDetails, reports: []}, @@ -2598,6 +2623,7 @@ function getMemberInviteOptions( maxElements: undefined, }, countryCode, + visibleReportActionsData, ); } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index f3273670e0fb7..a89f49be41b1b 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -327,14 +327,6 @@ Onyx.connect({ }, }); -let visibleReportActionsData: VisibleReportActionsDerivedValue | undefined; -Onyx.connect({ - key: ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, - callback: (value) => { - visibleReportActionsData = value ?? undefined; - }, -}); - let allPersonalDetails: OnyxEntry = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, @@ -1996,6 +1988,7 @@ function deleteReportComment( isReportArchived: boolean | undefined, isOriginalReportArchived: boolean | undefined, currentEmail: string, + visibleReportActionsDataParam?: VisibleReportActionsDerivedValue, ) { const originalReportID = getOriginalReportID(reportID, reportAction); const reportActionID = reportAction.reportActionID; @@ -2042,7 +2035,7 @@ function deleteReportComment( (action) => action.reportActionID !== reportAction.reportActionID && ReportActionsUtils.didMessageMentionCurrentUser(action, currentEmail) && - ReportActionsUtils.isReportActionVisible(action, reportID, undefined, visibleReportActionsData), + ReportActionsUtils.isReportActionVisible(action, reportID, undefined, visibleReportActionsDataParam), ); optimisticReport.lastMentionedTime = latestMentionedReportAction?.created ?? null; } @@ -2198,6 +2191,7 @@ function editReportComment( isOriginalParentReportArchived: boolean | undefined, currentUserLogin: string, videoAttributeCache?: Record, + visibleReportActionsDataParam?: VisibleReportActionsDerivedValue, ) { const originalReportID = originalReport?.reportID; if (!originalReportID || !originalReportAction) { @@ -2230,7 +2224,7 @@ function editReportComment( // Delete the comment if it's empty if (!htmlForNewComment) { - deleteReportComment(originalReportID, originalReportAction, ancestors, isOriginalReportArchived, isOriginalParentReportArchived, currentUserLogin); + deleteReportComment(originalReportID, originalReportAction, ancestors, isOriginalReportArchived, isOriginalParentReportArchived, currentUserLogin, visibleReportActionsDataParam); return; } diff --git a/src/pages/Share/ShareTab.tsx b/src/pages/Share/ShareTab.tsx index 1eda39749bf9f..f4f121e524f3c 100644 --- a/src/pages/Share/ShareTab.tsx +++ b/src/pages/Share/ShareTab.tsx @@ -51,6 +51,7 @@ function ShareTab({ref}: ShareTabProps) { const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true}); const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const [nvpDismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); useImperativeHandle(ref, () => ({ focus: selectionListRef.current?.focusTextInput, @@ -79,6 +80,7 @@ function ShareTab({ref}: ShareTabProps) { includeUserToInvite: true, countryCode, loginList, + visibleReportActionsData: visibleReportActionsData ?? {}, }); }, [areOptionsInitialized, options, draftComments, nvpDismissedProductTraining, betas, textInputValue, countryCode, loginList]); diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 177ea999b9aa1..d1baeb28af1ca 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -86,6 +86,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro const [shouldSwitchPositionIfOverflow, setShouldSwitchPositionIfOverflow] = useState(false); const [isWithoutOverlay, setIsWithoutOverlay] = useState(true); const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const contentRef = useRef(null); const anchorRef = useRef(null); @@ -372,7 +373,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro } else if (reportAction) { // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { - deleteReportComment(reportIDRef.current, reportAction, ancestorsRef.current, isReportArchived, isOriginalReportArchived, email ?? ''); + deleteReportComment(reportIDRef.current, reportAction, ancestorsRef.current, isReportArchived, isOriginalReportArchived, email ?? '', visibleReportActionsData ?? undefined); }); } @@ -393,6 +394,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro isOriginalReportArchived, allTransactionViolations, bankAccountList, + visibleReportActionsData, ]); const hideDeleteModal = () => { diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 682e0801c2a8f..46e72bb5b925d 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -151,6 +151,7 @@ function ReportActionItemMessageEdit({ // The ref to check whether the comment saving is in progress const isCommentPendingSaved = useRef(false); const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const isOriginalReportArchived = useReportIsArchived(originalReportID); const originalParentReportID = getOriginalReportID(originalReportID, action); const isOriginalParentReportArchived = useReportIsArchived(originalParentReportID); @@ -324,6 +325,7 @@ function ReportActionItemMessageEdit({ isOriginalParentReportArchived, email ?? '', Object.fromEntries(draftMessageVideoAttributeCache), + visibleReportActionsData ?? undefined, ); deleteDraft(); }, [ @@ -338,6 +340,7 @@ function ReportActionItemMessageEdit({ isOriginalParentReportArchived, debouncedValidateCommentMaxLength, email, + visibleReportActionsData, ]); /** From 845dae61a71b170373e3db41b21d4897f6a79007 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Tue, 20 Jan 2026 14:11:39 +0100 Subject: [PATCH 20/32] Refactor: Remove nullish coalescing for visibleReportActionsData in Search components and utility functions --- .../LHNOptionsList/OptionRowLHNData.tsx | 2 +- .../Search/SearchAutocompleteList.tsx | 6 +- .../Search/SearchFiltersChatsSelector.tsx | 17 +++++- src/components/Search/index.tsx | 4 -- src/hooks/useSearchSelector.base.ts | 2 +- src/libs/OptionsListUtils/index.ts | 56 ++++++++++++------- src/libs/SearchUIUtils.ts | 6 +- src/libs/SidebarUtils.ts | 14 ++++- src/pages/Share/ShareTab.tsx | 2 +- tests/unit/OptionsListUtilsTest.tsx | 10 +++- 10 files changed, 80 insertions(+), 39 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index c473f8efb372e..b20daa3bc9189 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -79,7 +79,7 @@ function OptionRowLHNData({ movedFromReport, movedToReport, currentUserAccountID, - visibleReportActionsData: visibleReportActionsData ?? {}, + visibleReportActionsData, }); if (deepEqual(item, optionItemRef.current)) { return optionItemRef.current; diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 2e34d826a5b1e..962a349c3f514 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -219,7 +219,7 @@ function SearchAutocompleteList({ shouldShowGBR: false, shouldUnreadBeBold: true, loginList, - visibleReportActionsData: visibleReportActionsData ?? {}, + visibleReportActionsData, }); }, [areOptionsInitialized, options, draftComments, nvpDismissedProductTraining, betas, autocompleteQueryValue, countryCode, loginList, visibleReportActionsData]); @@ -430,7 +430,7 @@ function SearchAutocompleteList({ countryCode, loginList, shouldShowGBR: true, - visibleReportActionsData: visibleReportActionsData ?? {}, + visibleReportActionsData, }).personalDetails.filter((participant) => participant.text && !alreadyAutocompletedKeys.has(participant.text.toLowerCase())); return participants.map((participant) => ({ @@ -462,7 +462,7 @@ function SearchAutocompleteList({ countryCode, loginList, shouldShowGBR: true, - visibleReportActionsData: visibleReportActionsData ?? {}, + visibleReportActionsData, }).recentReports.filter((chat) => { if (!chat.text) { return false; diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index 32bf8a25a8138..6b74447a36c5f 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -64,9 +64,11 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen const selectedOptions = useMemo(() => { return selectedReportIDs.map((id) => { - const report = getSelectedOptionData(createOptionFromReport({...reports?.[`${ONYXKEYS.COLLECTION.REPORT}${id}`], reportID: id}, personalDetails, reportAttributesDerived, undefined, visibleReportActionsData ?? {})); + const report = getSelectedOptionData( + createOptionFromReport({...reports?.[`${ONYXKEYS.COLLECTION.REPORT}${id}`], reportID: id}, personalDetails, reportAttributesDerived, undefined, visibleReportActionsData), + ); const isReportArchived = archivedReportsIdSet.has(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`); - const alternateText = getAlternateText(report, {}, isReportArchived, {}, visibleReportActionsData ?? {}); + const alternateText = getAlternateText(report, {}, isReportArchived, {}, visibleReportActionsData); return {...report, alternateText}; }); }, [archivedReportsIdSet, personalDetails, reportAttributesDerived, reports, selectedReportIDs, visibleReportActionsData]); @@ -75,7 +77,16 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen if (!areOptionsInitialized || !isScreenTransitionEnd) { return defaultListOptions; } - return getSearchOptions({options, draftComments, nvpDismissedProductTraining, betas: undefined, isUsedInChatFinder: false, countryCode, loginList, visibleReportActionsData: visibleReportActionsData ?? {}}); + return getSearchOptions({ + options, + draftComments, + nvpDismissedProductTraining, + betas: undefined, + isUsedInChatFinder: false, + countryCode, + loginList, + visibleReportActionsData, + }); }, [areOptionsInitialized, isScreenTransitionEnd, options, draftComments, nvpDismissedProductTraining, countryCode, loginList, visibleReportActionsData]); const chatOptions = useMemo(() => { diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index a91343f788438..f7348a64ddbe9 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -242,7 +242,6 @@ function Search({ const previousTransactions = usePrevious(transactions); const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {canBeMissing: true}); const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, {canBeMissing: true}); - const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const [violations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const {accountID, email, login} = useCurrentUserPersonalDetails(); @@ -399,7 +398,6 @@ function Search({ isActionLoadingSet, cardFeeds, allTransactionViolations: violations, - visibleReportActionsData, }); return [filteredData1, filteredData1.length, allLength]; }, [ @@ -420,7 +418,6 @@ function Search({ policies, bankAccountList, violations, - visibleReportActionsData, ]); // For group-by views, each grouped item has a transactionsQueryJSON with a hash pointing to a separate snapshot @@ -456,7 +453,6 @@ function Search({ translate, formatPhoneNumber, isActionLoadingSet, - visibleReportActionsData: visibleReportActionsData ?? {}, }); return {...item, transactions: transactions1 as TransactionListItemType[]}; }); diff --git a/src/hooks/useSearchSelector.base.ts b/src/hooks/useSearchSelector.base.ts index 70ed07afacf74..80384f722e053 100644 --- a/src/hooks/useSearchSelector.base.ts +++ b/src/hooks/useSearchSelector.base.ts @@ -197,7 +197,7 @@ function useSearchSelectorBase({ includeUserToInvite, countryCode, loginList, - visibleReportActionsData: visibleReportActionsData ?? {}, + visibleReportActionsData, }); case CONST.SEARCH_SELECTOR.SEARCH_CONTEXT_MEMBER_INVITE: return getValidOptions(optionsWithContacts, allPolicies, draftComments, nvpDismissedProductTraining, loginList, { diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 9fd8b65023aa4..aebb5706e081d 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -630,7 +630,7 @@ function getLastMessageTextForReport({ isReportArchived?: boolean; policyForMovingExpensesID?: string; reportMetadata?: OnyxEntry; - visibleReportActionsDataParam: VisibleReportActionsDerivedValue; + visibleReportActionsDataParam?: VisibleReportActionsDerivedValue; }): string { const reportID = report?.reportID; const lastReportAction = reportID ? lastVisibleReportActions[reportID] : undefined; @@ -1772,9 +1772,17 @@ function getUserToInviteOption({ login: searchValue, }, }; - const userToInvite = createOption([optimisticAccountID], personalDetailsExtended, null, { - showChatPreviewLine, - }, undefined, undefined, visibleReportActionsData); + const userToInvite = createOption( + [optimisticAccountID], + personalDetailsExtended, + null, + { + showChatPreviewLine, + }, + undefined, + undefined, + visibleReportActionsData, + ); userToInvite.isOptimisticAccount = true; userToInvite.login = searchValue; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -2279,27 +2287,37 @@ function getValidOptions( [selfDMChats, workspaceChats, recentReportOptions] = optionsOrderAndGroupBy([isSelfDMChat, isWorkspaceChat], options.reports, recentReportComparator, maxElements, filteringFunction); if (selfDMChats.length > 0) { - selfDMChat = prepareReportOptionsForDisplay(selfDMChats, policiesCollection, { + selfDMChat = prepareReportOptionsForDisplay( + selfDMChats, + policiesCollection, + { + ...getValidReportsConfig, + selectedOptions, + shouldBoldTitleByDefault, + shouldSeparateSelfDMChat, + shouldSeparateWorkspaceChat, + shouldShowGBR, + }, + visibleReportActionsData, + ).at(0); + } + + if (maxRecentReportElements) { + recentReportOptions = recentReportOptions.splice(0, maxRecentReportElements); + } + recentReportOptions = prepareReportOptionsForDisplay( + recentReportOptions, + policiesCollection, + { ...getValidReportsConfig, selectedOptions, shouldBoldTitleByDefault, shouldSeparateSelfDMChat, shouldSeparateWorkspaceChat, shouldShowGBR, - }, visibleReportActionsData).at(0); - } - - if (maxRecentReportElements) { - recentReportOptions = recentReportOptions.splice(0, maxRecentReportElements); - } - recentReportOptions = prepareReportOptionsForDisplay(recentReportOptions, policiesCollection, { - ...getValidReportsConfig, - selectedOptions, - shouldBoldTitleByDefault, - shouldSeparateSelfDMChat, - shouldSeparateWorkspaceChat, - shouldShowGBR, - }, visibleReportActionsData); + }, + visibleReportActionsData, + ); workspaceChats = prepareReportOptionsForDisplay(workspaceChats, policiesCollection, { ...getValidReportsConfig, diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 3f71dcd745f75..57d6288114925 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -367,7 +367,6 @@ type GetSectionsParams = { isActionLoadingSet?: ReadonlySet; cardFeeds?: OnyxCollection; allTransactionViolations?: OnyxCollection; - visibleReportActionsData: OnyxTypes.VisibleReportActionsDerivedValue; }; /** @@ -1643,7 +1642,7 @@ function createAndOpenSearchTransactionThread( * * Do not use directly, use only via `getSections()` facade. */ -function getReportActionsSections(data: OnyxTypes.SearchResults['data'], visibleReportActionsData: OnyxTypes.VisibleReportActionsDerivedValue): [ReportActionListItemType[], number] { +function getReportActionsSections(data: OnyxTypes.SearchResults['data'], visibleReportActionsData?: OnyxTypes.VisibleReportActionsDerivedValue): [ReportActionListItemType[], number] { const reportActionItems: ReportActionListItemType[] = []; const transactions = Object.keys(data) @@ -2098,10 +2097,9 @@ function getSections({ isActionLoadingSet, cardFeeds, allTransactionViolations, - visibleReportActionsData, }: GetSectionsParams) { if (type === CONST.SEARCH.DATA_TYPES.CHAT) { - return getReportActionsSections(data, visibleReportActionsData); + return getReportActionsSections(data); } if (type === CONST.SEARCH.DATA_TYPES.TASK) { return getTaskSections(data, formatPhoneNumber, archivedReportsIDList); diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 4f84abb7be2b6..fda9e965c2b56 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -5,7 +5,17 @@ import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleCon import type {PartialPolicyForSidebar, ReportsToDisplayInLHN} from '@hooks/useSidebarOrderedReports'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Card, PersonalDetails, PersonalDetailsList, ReportActions, ReportAttributesDerivedValue, ReportNameValuePairs, Transaction, TransactionViolation, VisibleReportActionsDerivedValue} from '@src/types/onyx'; +import type { + Card, + PersonalDetails, + PersonalDetailsList, + ReportActions, + ReportAttributesDerivedValue, + ReportNameValuePairs, + Transaction, + TransactionViolation, + VisibleReportActionsDerivedValue, +} from '@src/types/onyx'; import type Beta from '@src/types/onyx/Beta'; import type {ReportAttributes} from '@src/types/onyx/DerivedValues'; import type {Errors} from '@src/types/onyx/OnyxCommon'; @@ -672,7 +682,7 @@ function getOptionData({ movedFromReport?: OnyxEntry; movedToReport?: OnyxEntry; currentUserAccountID: number; - visibleReportActionsData: VisibleReportActionsDerivedValue; + visibleReportActionsData?: VisibleReportActionsDerivedValue; }): OptionData | undefined { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for // this method to be called after the Onyx data has been cleared out. In that case, it's fine to do diff --git a/src/pages/Share/ShareTab.tsx b/src/pages/Share/ShareTab.tsx index f4f121e524f3c..bbbbf99710d68 100644 --- a/src/pages/Share/ShareTab.tsx +++ b/src/pages/Share/ShareTab.tsx @@ -80,7 +80,7 @@ function ShareTab({ref}: ShareTabProps) { includeUserToInvite: true, countryCode, loginList, - visibleReportActionsData: visibleReportActionsData ?? {}, + visibleReportActionsData, }); }, [areOptionsInitialized, options, draftComments, nvpDismissedProductTraining, betas, textInputValue, countryCode, loginList]); diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index df3add9deb872..bfd091e376e98 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -2937,7 +2937,15 @@ describe('OptionsListUtils', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { [submittedAction.reportActionID]: submittedAction, }); - const lastMessage = getLastMessageTextForReport({translate: translateLocal, report, lastActorDetails: null, isReportArchived: false, policy, reportMetadata, visibleReportActionsDataParam: {}}); + const lastMessage = getLastMessageTextForReport({ + translate: translateLocal, + report, + lastActorDetails: null, + isReportArchived: false, + policy, + reportMetadata, + visibleReportActionsDataParam: {}, + }); expect(lastMessage).toBe(translate(CONST.LOCALES.EN, 'iou.queuedToSubmitViaDEW')); }); From 27989e1fde8af72be1479357909ff972113cd84a Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Tue, 20 Jan 2026 14:27:19 +0100 Subject: [PATCH 21/32] Refactor: Update translation handling in getAlternateText and createOption functions to use optional translate parameter --- src/libs/OptionsListUtils/index.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index aebb5706e081d..78501734dfd54 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -475,16 +475,19 @@ function getAlternateText( isReportArchived: boolean | undefined, lastActorDetails: Partial | null = {}, visibleReportActionsData: VisibleReportActionsDerivedValue = {}, + translate?: LocalizedTranslate, ) { const report = getReportOrDraftReport(option.reportID); const isAdminRoom = reportUtilsIsAdminRoom(report); const isAnnounceRoom = reportUtilsIsAnnounceRoom(report); const isGroupChat = reportUtilsIsGroupChat(report); const isExpenseThread = isMoneyRequest(report); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const translateFn = translate ?? translateLocal; const formattedLastMessageText = formatReportLastMessageText(Parser.htmlToText(option.lastMessageText ?? '')) || getLastMessageTextForReport({ - translate: translateLocal, + translate: translateFn, report, lastActorDetails, isReportArchived, @@ -494,13 +497,11 @@ function getAlternateText( const formattedLastMessageTextWithPrefix = reportPrefix + formattedLastMessageText; if (isExpenseThread || option.isMoneyRequestReport) { - // eslint-disable-next-line @typescript-eslint/no-deprecated - return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageTextWithPrefix : translateLocal('iou.expense'); + return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageTextWithPrefix : translateFn('iou.expense'); } if (option.isThread) { - // eslint-disable-next-line @typescript-eslint/no-deprecated - return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageTextWithPrefix : translateLocal('threads.thread'); + return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageTextWithPrefix : translateFn('threads.thread'); } if (option.isChatRoom && !isAdminRoom && !isAnnounceRoom) { @@ -512,13 +513,11 @@ function getAlternateText( } if (option.isTaskReport) { - // eslint-disable-next-line @typescript-eslint/no-deprecated - return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageTextWithPrefix : translateLocal('task.task'); + return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageTextWithPrefix : translateFn('task.task'); } if (isGroupChat) { - // eslint-disable-next-line @typescript-eslint/no-deprecated - return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageTextWithPrefix : translateLocal('common.group'); + return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageTextWithPrefix : translateFn('common.group'); } return showChatPreviewLine && formattedLastMessageText @@ -874,6 +873,7 @@ function createOption( reportAttributesDerived?: ReportAttributesDerivedValue['reports'], privateIsArchived?: string, visibleReportActionsData: VisibleReportActionsDerivedValue = {}, + translate?: LocalizedTranslate, ): SearchOptionData { const {showChatPreviewLine = false, forcePolicyNamePreview = false, showPersonalDetails = false, selected, isSelected, isDisabled} = config ?? {}; @@ -949,8 +949,10 @@ function createOption( // If displaying chat preview line is needed, let's overwrite the default alternate text const lastActorDetails = personalDetails?.[report?.lastActorAccountID ?? String(CONST.DEFAULT_NUMBER_ID)] ?? {}; + // eslint-disable-next-line @typescript-eslint/no-deprecated + const translateFn = translate ?? translateLocal; result.lastMessageText = getLastMessageTextForReport({ - translate: translateLocal, + translate: translateFn, report, lastActorDetails, isReportArchived: !!result.private_isArchived, @@ -959,7 +961,7 @@ function createOption( result.alternateText = showPersonalDetails && personalDetail?.login ? personalDetail.login - : getAlternateText(result, {showChatPreviewLine, forcePolicyNamePreview}, !!result.private_isArchived, lastActorDetails, visibleReportActionsData); + : getAlternateText(result, {showChatPreviewLine, forcePolicyNamePreview}, !!result.private_isArchived, lastActorDetails, visibleReportActionsData, translateFn); const personalDetailsForCompute: PersonalDetailsList | undefined = personalDetails ?? undefined; const computedReportName = computeReportName( From e2a6005e1c91b352dfe839cd6fedd9d31c636c3e Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Tue, 20 Jan 2026 18:41:49 +0100 Subject: [PATCH 22/32] Disable ESLint max-lines rule in ReportActionsUtils.ts --- src/libs/ReportActionsUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index ac24606b245de..c48d22bded789 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import {format} from 'date-fns'; import {fastMerge, Str} from 'expensify-common'; import clone from 'lodash/clone'; From f5bab354a22758fdefc46dd01b5711bb1fa1e12f Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Tue, 20 Jan 2026 19:46:58 +0100 Subject: [PATCH 23/32] Add visibleReportActionsData prop to SearchAutocompleteList component --- src/components/Search/SearchAutocompleteList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 962a349c3f514..acc6339628945 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -632,6 +632,7 @@ function SearchAutocompleteList({ betas, countryCode, loginList, + visibleReportActionsData, currentUserLogin, groupByAutocompleteList, statusAutocompleteList, From 0bf806962d3c622f462498dd8faffb86352d1d7c Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Tue, 20 Jan 2026 20:02:59 +0100 Subject: [PATCH 24/32] Revert "Add visibleReportActionsData prop to SearchAutocompleteList component" This reverts commit f5bab354a22758fdefc46dd01b5711bb1fa1e12f. --- src/components/Search/SearchAutocompleteList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index acc6339628945..962a349c3f514 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -632,7 +632,6 @@ function SearchAutocompleteList({ betas, countryCode, loginList, - visibleReportActionsData, currentUserLogin, groupByAutocompleteList, statusAutocompleteList, From 53edd1cd057b7976173b70a345dda608e6f60dec Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 21 Jan 2026 08:58:54 +0100 Subject: [PATCH 25/32] Add visibleReportActionsData prop to getSections function in SearchUIUtils.ts --- src/libs/SearchUIUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 57d6288114925..f872fef7d5d01 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -367,6 +367,7 @@ type GetSectionsParams = { isActionLoadingSet?: ReadonlySet; cardFeeds?: OnyxCollection; allTransactionViolations?: OnyxCollection; + visibleReportActionsData?: OnyxTypes.VisibleReportActionsDerivedValue; }; /** @@ -2097,9 +2098,10 @@ function getSections({ isActionLoadingSet, cardFeeds, allTransactionViolations, + visibleReportActionsData, }: GetSectionsParams) { if (type === CONST.SEARCH.DATA_TYPES.CHAT) { - return getReportActionsSections(data); + return getReportActionsSections(data, visibleReportActionsData); } if (type === CONST.SEARCH.DATA_TYPES.TASK) { return getTaskSections(data, formatPhoneNumber, archivedReportsIDList); From 755c0c539f1bdbbaed795dc53fa09c1513f36d8f Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 21 Jan 2026 14:17:01 +0100 Subject: [PATCH 26/32] Refactor LHNOptionsList and SidebarUtils to utilize getLastVisibleAction for improved action handling --- .../LHNOptionsList/LHNOptionsList.tsx | 89 ++++++++----------- src/libs/OptionsListUtils/index.ts | 67 +++++++------- src/libs/SidebarUtils.ts | 11 ++- 3 files changed, 78 insertions(+), 89 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 8491672b79c01..8117ca772db65 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -28,21 +28,13 @@ 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, - isReportActionVisibleAsLastAction, -} from '@libs/ReportActionsUtils'; +import {getLastVisibleAction, getOneTransactionThreadReportID, 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'; @@ -193,9 +185,6 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio } const itemInvoiceReceiverPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`]; - const iouReportIDOfLastAction = getIOUReportIDOfLastAction(item); - const itemIouReportReportActions = iouReportIDOfLastAction ? reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportIDOfLastAction}`] : undefined; - const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${item?.policyID}`]; const transactionID = isMoneyRequestAction(itemParentReportAction) ? (getOriginalMessage(itemParentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) @@ -207,56 +196,54 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const isReportArchived = !!itemReportNameValuePairs?.private_isArchived; const canUserPerformWrite = canUserPerformWriteActionUtil(item, isReportArchived); - const sortedReportActions = getSortedReportActionsForDisplay(itemReportActions, canUserPerformWrite, false, visibleReportActionsData); - const lastReportAction = sortedReportActions.at(0); + const lastAction = getLastVisibleAction( + reportID, + canUserPerformWrite, + {}, + itemReportActions ? {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: itemReportActions} : undefined, + visibleReportActionsData, + ); - // Get the transaction for the last report action - const lastReportActionTransactionID = isMoneyRequestAction(lastReportAction) - ? (getOriginalMessage(lastReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) - : CONST.DEFAULT_NUMBER_ID; + const iouReportIDOfLastAction = getIOUReportIDOfLastAction(item, visibleReportActionsData, lastAction); + const itemIouReportReportActions = iouReportIDOfLastAction ? reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportIDOfLastAction}`] : undefined; + + const lastReportActionTransactionID = isMoneyRequestAction(lastAction) ? (getOriginalMessage(lastAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) : CONST.DEFAULT_NUMBER_ID; const lastReportActionTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${lastReportActionTransactionID}`]; - // SidebarUtils.getOptionData in OptionRowLHNData does not get re-evaluated when the linked task report changes, so we have the lastMessageTextFromReport evaluation logic here - let lastActorDetails: Partial | null = item?.lastActorAccountID && personalDetails?.[item.lastActorAccountID] ? personalDetails[item.lastActorAccountID] : null; - if (!lastActorDetails && lastReportAction) { - const lastActorDisplayName = lastReportAction?.person?.[0]?.text; + const lastActorAccountID = item.lastActorAccountID; + let lastActorDetails: Partial | null = lastActorAccountID && personalDetails?.[lastActorAccountID] ? personalDetails[lastActorAccountID] : null; + + if (!lastActorDetails && lastAction) { + const lastActorDisplayName = lastAction?.person?.[0]?.text; lastActorDetails = lastActorDisplayName ? { displayName: lastActorDisplayName, - accountID: item?.lastActorAccountID, + accountID: lastActorAccountID, } : null; } - const movedFromReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastReportAction, CONST.REPORT.MOVE_TYPE.FROM)}`]; - const movedToReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastReportAction, CONST.REPORT.MOVE_TYPE.TO)}`]; + + const movedFromReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.FROM)}`]; + const movedToReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.TO)}`]; const itemReportMetadata = reportMetadataCollection?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`]; - const lastMessageTextFromReport = getLastMessageTextForReport({ - translate, - report: item, - lastActorDetails, - movedFromReport, - movedToReport, - policy: itemPolicy, - isReportArchived: !!itemReportNameValuePairs?.private_isArchived, - policyForMovingExpensesID, - reportMetadata: itemReportMetadata, - visibleReportActionsDataParam: visibleReportActionsData, - }); - const shouldShowRBRorGBRTooltip = firstReportIDWithGBRorRBR === reportID; + const lastMessageTextFromReport = + item.lastMessageText ?? + getLastMessageTextForReport({ + translate, + report: item, + lastActorDetails, + movedFromReport, + movedToReport, + policy: itemPolicy, + isReportArchived: !!itemReportNameValuePairs?.private_isArchived, + policyForMovingExpensesID, + reportMetadata: itemReportMetadata, + visibleReportActionsDataParam: visibleReportActionsData, + lastAction, + }); - 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) => - isReportActionVisibleAsLastAction(reportAction, canUserPerformWriteAction, visibleReportActionsData) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED, - ); - lastAction = reportActionsForDisplay.at(-1); - } + const shouldShowRBRorGBRTooltip = firstReportIDWithGBRorRBR === reportID; let lastActionReport: OnyxEntry | undefined; if (isInviteOrRemovedAction(lastAction)) { diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 78501734dfd54..7ef1216fc4f60 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -43,6 +43,7 @@ import { getInvoiceCompanyWebsiteUpdateMessage, getIOUReportIDFromReportActionPreview, getJoinRequestMessage, + getLastVisibleAction, getLastVisibleMessage, getMarkedReimbursedMessage, getMentionedAccountIDsFromAction, @@ -251,7 +252,6 @@ Onyx.connect({ const lastReportActions: ReportActions = {}; const allSortedReportActions: Record = {}; let allReportActions: OnyxCollection; -const lastVisibleReportActions: ReportActions = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, waitForCollectionCallback: true, @@ -262,10 +262,7 @@ Onyx.connect({ allReportActions = actions ?? {}; - // Use empty object as fallback since visibleReportActionsData is now passed via parameters - const currentVisibleReportActionsData: VisibleReportActionsDerivedValue = {}; - - // Iterate over the report actions to build the sorted and lastVisible report actions objects + // Iterate over the report actions to build the sorted report actions objects for (const reportActions of Object.entries(allReportActions)) { const reportID = reportActions[0].split('_').at(1); if (!reportID) { @@ -284,6 +281,7 @@ Onyx.connect({ if (transactionThreadReportID) { const transactionThreadReportActionsArray = Object.values(actions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`] ?? {}); sortedReportActions = getCombinedReportActions(sortedReportActions, transactionThreadReportID, transactionThreadReportActionsArray, reportID); + allSortedReportActions[reportID] = sortedReportActions; } const firstReportAction = sortedReportActions.at(0); @@ -292,26 +290,6 @@ Onyx.connect({ } else { lastReportActions[reportID] = firstReportAction; } - - const reportNameValuePairs = allReportNameValuePairsOnyxConnect?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`]; - 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) => - (!(isWhisperAction(reportAction) && !isReportPreviewAction(reportAction) && !isMoneyRequestAction(reportAction)) || isActionableMentionWhisper(reportAction)) && - isReportActionVisible(reportAction, reportID, isWriteActionAllowed, currentVisibleReportActionsData) && - 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]; - continue; - } - lastVisibleReportActions[reportID] = reportActionForDisplay; } }, }); @@ -439,8 +417,8 @@ function shouldShowLastActorDisplayName( lastAction: OnyxEntry, currentUserAccountIDParam: number, ) { - const reportID = report?.reportID; - const lastReportAction = (reportID ? lastVisibleReportActions[reportID] : undefined) ?? lastAction; + // Use lastAction directly instead of getLastVisibleReportAction to avoid using stale cache data + const lastReportAction = lastAction; // Use report.lastActionType as fallback when report actions aren't loaded yet (e.g., on cold start) const lastActionName = lastReportAction?.actionName ?? report?.lastActionType; @@ -566,29 +544,44 @@ function isSearchStringMatchUserDetails(personalDetail: PersonalDetails, searchV /** * Get IOU report ID of report last action if the action is report action preview */ -function getIOUReportIDOfLastAction(report: OnyxEntry): string | undefined { +function getIOUReportIDOfLastAction(report: OnyxEntry, visibleReportActionsData?: VisibleReportActionsDerivedValue, lastAction?: OnyxEntry): string | undefined { if (!report?.reportID) { return; } - const lastAction = lastVisibleReportActions[report.reportID]; - if (!isReportPreviewAction(lastAction)) { + // Use lastAction if available (from useOnyx), otherwise fallback to getLastVisibleAction which uses isReportActionVisibleAsLastAction with proper filters + const reportNameValuePairs = allReportNameValuePairsOnyxConnect?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]; + const isReportArchived = !!reportNameValuePairs?.private_isArchived; + const canUserPerformWrite = canUserPerformWriteAction(report, isReportArchived); + const action = lastAction ?? getLastVisibleAction(report.reportID, canUserPerformWrite, {}, undefined, visibleReportActionsData); + if (!isReportPreviewAction(action)) { return; } - return getReportOrDraftReport(getIOUReportIDFromReportActionPreview(lastAction))?.reportID; + return getReportOrDraftReport(getIOUReportIDFromReportActionPreview(action))?.reportID; } function hasHiddenDisplayNames(accountIDs: number[]) { return getPersonalDetailsByIDs({accountIDs, currentUserAccountID: 0}).some((personalDetail) => !getDisplayNameOrDefault(personalDetail, undefined, false)); } -function getLastActorDisplayNameFromLastVisibleActions(report: OnyxEntry, lastActorDetails: Partial | null, currentUserAccountIDParam: number): string { +function getLastActorDisplayNameFromLastVisibleActions( + report: OnyxEntry, + lastActorDetails: Partial | null, + currentUserAccountIDParam: number, + visibleReportActionsData?: VisibleReportActionsDerivedValue, + lastAction?: OnyxEntry, + personalDetails?: PersonalDetailsList, +): string { const reportID = report?.reportID; - const lastReportAction = reportID ? lastVisibleReportActions[reportID] : undefined; + // Use lastAction if available (from useOnyx), otherwise fallback to getLastVisibleAction which uses isReportActionVisibleAsLastAction with proper filters + const reportNameValuePairs = reportID ? allReportNameValuePairsOnyxConnect?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`] : undefined; + const isReportArchived = !!reportNameValuePairs?.private_isArchived; + const canUserPerformWrite = canUserPerformWriteAction(report, isReportArchived); + const lastReportAction = lastAction ?? getLastVisibleAction(reportID, canUserPerformWrite, {}, undefined, visibleReportActionsData); if (lastReportAction) { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const lastActorAccountID = getReportActionActorAccountID(lastReportAction, undefined, undefined) || report?.lastActorAccountID; - let actorDetails: Partial | null = lastActorAccountID ? (allPersonalDetails?.[lastActorAccountID] ?? null) : null; + let actorDetails: Partial | null = lastActorAccountID ? ((personalDetails ?? allPersonalDetails)?.[lastActorAccountID] ?? null) : null; if (!actorDetails && lastReportAction.person?.at(0)?.text) { actorDetails = { @@ -619,6 +612,7 @@ function getLastMessageTextForReport({ policyForMovingExpensesID, reportMetadata, visibleReportActionsDataParam, + lastAction, }: { translate: LocalizedTranslate; report: OnyxEntry; @@ -630,9 +624,12 @@ function getLastMessageTextForReport({ policyForMovingExpensesID?: string; reportMetadata?: OnyxEntry; visibleReportActionsDataParam?: VisibleReportActionsDerivedValue; + lastAction?: OnyxEntry; }): string { const reportID = report?.reportID; - const lastReportAction = reportID ? lastVisibleReportActions[reportID] : undefined; + // Use lastAction if available (from useOnyx), otherwise fallback to getLastVisibleAction which uses isReportActionVisibleAsLastAction with proper filters + const canUserPerformWrite = canUserPerformWriteAction(report, isReportArchived); + const lastReportAction = lastAction ?? getLastVisibleAction(reportID, canUserPerformWrite, {}, undefined, visibleReportActionsDataParam); const lastVisibleMessage = getLastVisibleMessage(report?.reportID, undefined, {}, undefined, visibleReportActionsDataParam); // some types of actions are filtered out for lastReportAction, in some cases we need to check the actual last action diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index fda9e965c2b56..b5d497741b607 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -822,6 +822,7 @@ function getOptionData({ policy, isReportArchived, visibleReportActionsDataParam: visibleReportActionsData, + lastAction, }); } @@ -968,7 +969,9 @@ function getOptionData({ result.alternateText = getCardIssuedMessage({reportAction: lastAction, expensifyCard: card, translate}); } else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && lastActorDisplayName && lastMessageTextFromReport) { const displayName = - (lastMessageTextFromReport.length > 0 && getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, currentUserAccountID)) || lastActorDisplayName; + (lastMessageTextFromReport.length > 0 && + getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, currentUserAccountID, visibleReportActionsData, lastAction, personalDetails)) || + lastActorDisplayName; result.alternateText = formatReportLastMessageText(`${displayName}: ${lastMessageText}`); } else if (lastAction && isOldDotReportAction(lastAction)) { result.alternateText = getMessageOfOldDotReportAction(translate, lastAction); @@ -1032,9 +1035,11 @@ function getOptionData({ translate('report.noActivityYet'), ); } - if (shouldShowLastActorDisplayName(report, lastActorDetails, lastAction, currentUserAccountID) && !isReportArchived) { + if (shouldShowLastActorDisplayName(report, lastActorDetails, lastAction, currentUserAccountID, visibleReportActionsData) && !isReportArchived) { const displayName = - (lastMessageTextFromReport.length > 0 && getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, currentUserAccountID)) || lastActorDisplayName; + (lastMessageTextFromReport.length > 0 && + getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, currentUserAccountID, visibleReportActionsData, lastAction, personalDetails)) || + lastActorDisplayName; result.alternateText = `${displayName}: ${formatReportLastMessageText(lastMessageText)}`; } else { result.alternateText = formatReportLastMessageText(lastMessageText); From 82d56124a2554bb72676e221801972a18396c5e4 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 21 Jan 2026 14:50:00 +0100 Subject: [PATCH 27/32] prettier eslint fix --- src/libs/OptionsListUtils/index.ts | 1 - src/libs/SidebarUtils.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 7ef1216fc4f60..b86b0a9fe48a4 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -88,7 +88,6 @@ import { isTaskAction, isThreadParentMessage, isUnapprovedAction, - isWhisperAction, withDEWRoutedActionsArray, } from '@libs/ReportActionsUtils'; import {computeReportName} from '@libs/ReportNameUtils'; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index b5d497741b607..ea400f5081c61 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -1035,7 +1035,7 @@ function getOptionData({ translate('report.noActivityYet'), ); } - if (shouldShowLastActorDisplayName(report, lastActorDetails, lastAction, currentUserAccountID, visibleReportActionsData) && !isReportArchived) { + if (shouldShowLastActorDisplayName(report, lastActorDetails, lastAction, currentUserAccountID) && !isReportArchived) { const displayName = (lastMessageTextFromReport.length > 0 && getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, currentUserAccountID, visibleReportActionsData, lastAction, personalDetails)) || From a4f5588ea9943d19ed67ed05272af08924ac9401 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 21 Jan 2026 15:18:19 +0100 Subject: [PATCH 28/32] Enhance LHNOptionsList and ReportActionsUtils to improve actor account ID retrieval and report action handling --- src/components/LHNOptionsList/LHNOptionsList.tsx | 4 ++-- src/libs/ReportActionsUtils.ts | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 8117ca772db65..7d8cd594842a1 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -28,7 +28,7 @@ import getPlatform from '@libs/getPlatform'; import Log from '@libs/Log'; import {getMovedReportID} from '@libs/ModifiedExpenseMessage'; import {getIOUReportIDOfLastAction, getLastMessageTextForReport} from '@libs/OptionsListUtils'; -import {getLastVisibleAction, getOneTransactionThreadReportID, getOriginalMessage, isInviteOrRemovedAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {getLastVisibleAction, getOneTransactionThreadReportID, getOriginalMessage, getReportActionActorAccountID, isInviteOrRemovedAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction as canUserPerformWriteActionUtil} from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -210,7 +210,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const lastReportActionTransactionID = isMoneyRequestAction(lastAction) ? (getOriginalMessage(lastAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) : CONST.DEFAULT_NUMBER_ID; const lastReportActionTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${lastReportActionTransactionID}`]; - const lastActorAccountID = item.lastActorAccountID; + const lastActorAccountID = getReportActionActorAccountID(lastAction, undefined, item) ?? item.lastActorAccountID; let lastActorDetails: Partial | null = lastActorAccountID && personalDetails?.[lastActorAccountID] ? personalDetails[lastActorAccountID] : null; if (!lastActorDetails && lastAction) { diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index c48d22bded789..cae1f4f331ebb 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1242,9 +1242,15 @@ function getLastVisibleAction( ReportAction | null | undefined >; } else { - reportActions = Object.values(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}); + reportActions = Object.values(reportActionsParam?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}); } - const visibleReportActions = reportActions.filter((action): action is ReportAction => isReportActionVisibleAsLastAction(action, canUserPerformWriteAction, visibleReportActionsData)); + const reportActionsWithReportID = reportActions.map((action) => { + if (action && !action.reportID && reportID) { + return {...action, reportID}; + } + return action; + }); + const visibleReportActions = reportActionsWithReportID.filter((action): action is ReportAction => isReportActionVisibleAsLastAction(action, canUserPerformWriteAction, visibleReportActionsData)); const sortedReportActions = getSortedReportActions(visibleReportActions, true); if (sortedReportActions.length === 0) { return undefined; From bc8f9a29b2cc726c0ffee3f8f9564d522b849f7f Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 21 Jan 2026 15:44:30 +0100 Subject: [PATCH 29/32] Refactor LHNOptionsList and ReportActionsUtils for improved readability and maintainability; update tests to ensure proper handling of report actions and visibility data --- .../LHNOptionsList/LHNOptionsList.tsx | 15 ++++++++--- src/libs/ReportActionsUtils.ts | 4 ++- tests/unit/OptionsListUtilsTest.tsx | 27 +++++++++++++++---- tests/unit/SidebarUtilsTest.ts | 19 +++++++++++-- 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 7d8cd594842a1..308bb1599c616 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -28,7 +28,14 @@ import getPlatform from '@libs/getPlatform'; import Log from '@libs/Log'; import {getMovedReportID} from '@libs/ModifiedExpenseMessage'; import {getIOUReportIDOfLastAction, getLastMessageTextForReport} from '@libs/OptionsListUtils'; -import {getLastVisibleAction, getOneTransactionThreadReportID, getOriginalMessage, getReportActionActorAccountID, isInviteOrRemovedAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import { + getLastVisibleAction, + getOneTransactionThreadReportID, + getOriginalMessage, + getReportActionActorAccountID, + isInviteOrRemovedAction, + isMoneyRequestAction, +} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction as canUserPerformWriteActionUtil} from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -227,8 +234,10 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const movedToReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.TO)}`]; const itemReportMetadata = reportMetadataCollection?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`]; + // For archived reports, always call getLastMessageTextForReport to get the archive reason message + // instead of using lastMessageText from the report const lastMessageTextFromReport = - item.lastMessageText ?? + (isReportArchived ? undefined : item.lastMessageText) ?? getLastMessageTextForReport({ translate, report: item, @@ -236,7 +245,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio movedFromReport, movedToReport, policy: itemPolicy, - isReportArchived: !!itemReportNameValuePairs?.private_isArchived, + isReportArchived, policyForMovingExpensesID, reportMetadata: itemReportMetadata, visibleReportActionsDataParam: visibleReportActionsData, diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index cae1f4f331ebb..81ec78724a067 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1250,7 +1250,9 @@ function getLastVisibleAction( } return action; }); - const visibleReportActions = reportActionsWithReportID.filter((action): action is ReportAction => isReportActionVisibleAsLastAction(action, canUserPerformWriteAction, visibleReportActionsData)); + const visibleReportActions = reportActionsWithReportID.filter((action): action is ReportAction => + isReportActionVisibleAsLastAction(action, canUserPerformWriteAction, visibleReportActionsData), + ); const sortedReportActions = getSortedReportActions(visibleReportActions, true); if (sortedReportActions.length === 0) { return undefined; diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index bfd091e376e98..42365dae33cbb 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -2674,9 +2674,13 @@ describe('OptionsListUtils', () => { isOwnPolicyExpenseChat: false, type: CONST.REPORT.TYPE.IOU, isWaitingOnBankAccount: false, + currency: CONST.CURRENCY.USD, + total: 100, + unheldTotal: 100, }; const reportPreviewAction: ReportAction = { ...createRandomReportAction(1), + reportID: report.reportID, actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, childMoneyRequestCount: 1, message: [{type: 'COMMENT', text: ''}], @@ -2697,6 +2701,7 @@ describe('OptionsListUtils', () => { }; const iouAction: ReportAction = { ...createRandomReportAction(2), + reportID: iouReport.reportID, actionName: CONST.REPORT.ACTIONS.TYPE.IOU, message: [{type: 'COMMENT', text: ''}], originalMessage: { @@ -2705,15 +2710,27 @@ describe('OptionsListUtils', () => { }, shouldShow: true, }; - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, iouReport); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, iouReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { [reportPreviewAction.reportActionID]: reportPreviewAction, }); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, { + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, { [iouAction.reportActionID]: iouAction, }); - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); - const lastMessage = getLastMessageTextForReport({translate: translateLocal, report, lastActorDetails: null, isReportArchived: false, visibleReportActionsDataParam: {}}); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); + await waitForBatchedUpdates(); + + const visibleReportActionsData = { + [report.reportID]: { + [reportPreviewAction.reportActionID]: true, + }, + [iouReport.reportID]: { + [iouAction.reportActionID]: true, + }, + }; + + const lastMessage = getLastMessageTextForReport({translate: translateLocal, report, lastActorDetails: null, isReportArchived: false, visibleReportActionsDataParam: visibleReportActionsData, lastAction: reportPreviewAction}); const reportPreviewMessage = getReportPreviewMessage(iouReport, iouAction, true, false, null, true, reportPreviewAction); const expected = formatReportLastMessageText(Parser.htmlToText(reportPreviewMessage)); expect(lastMessage).toBe(expected); diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index da7ebc46a9d9b..9725f495de265 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -1821,6 +1821,7 @@ describe('SidebarUtils', () => { const lastAction: ReportAction = { ...createRandomReportAction(1), + reportID: iouReportR14932.reportID, message: [ { type: 'COMMENT', @@ -1844,8 +1845,16 @@ describe('SidebarUtils', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportR14932.reportID}`, iouReportR14932); await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportR14932.reportID}`, chatReportR14932); await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, {[lastAction.reportActionID]: lastAction}); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportR14932.reportID}`, {[linkedCreateAction.reportActionID]: linkedCreateAction}); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportR14932.reportID}`, { + [linkedCreateAction.reportActionID]: linkedCreateAction, + [lastAction.reportActionID]: lastAction, + }); + await Onyx.set(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, { + [iouReportR14932.reportID]: { + [linkedCreateAction.reportActionID]: true, + [lastAction.reportActionID]: true, + }, + }); }); const result = SidebarUtils.getOptionData({ @@ -1863,6 +1872,12 @@ describe('SidebarUtils', () => { lastActionReport: undefined, isReportArchived: undefined, currentUserAccountID: session.accountID, + visibleReportActionsData: { + [iouReportR14932.reportID]: { + [linkedCreateAction.reportActionID]: true, + [lastAction.reportActionID]: true, + }, + }, }); expect(result?.alternateText).toBe(`You: ${getReportActionMessageText(lastAction)}`); From 5fec34688c43f4ddfe98112b914e753eb0104d28 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 21 Jan 2026 16:15:56 +0100 Subject: [PATCH 30/32] Refactor ReportActionsUtils to ensure safe access of message properties and update unit tests for report preview message formatting --- src/libs/ReportActionsUtils.ts | 6 ++--- tests/unit/OptionsListUtilsTest.tsx | 34 ++++++----------------------- 2 files changed, 10 insertions(+), 30 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 81ec78724a067..e5a07cba6a7b3 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -224,7 +224,7 @@ function getHtmlWithAttachmentID(html: string, reportActionID: string | undefine } function getReportActionMessage(reportAction: PartialReportAction) { - return Array.isArray(reportAction?.message) ? reportAction.message.at(0) : reportAction?.message; + return Array.isArray(reportAction?.message) ? reportAction?.message.at(0) : reportAction?.message; } function isDeletedParentAction(reportAction: OnyxInputOrEntry): boolean { @@ -389,7 +389,7 @@ function getOriginalMessage(reportAction: OnyxInputO return reportAction?.message ?? reportAction?.originalMessage; } // eslint-disable-next-line @typescript-eslint/no-deprecated - return reportAction.originalMessage; + return reportAction?.originalMessage; } function getMarkedReimbursedMessage(reportAction: OnyxInputOrEntry): string { @@ -1870,7 +1870,7 @@ function getMessageOfOldDotLegacyAction(legacyAction: PartialReportAction) { if (!Array.isArray(legacyAction?.message)) { return getReportActionText(legacyAction); } - if (legacyAction.message.length !== 0) { + if (legacyAction?.message.length !== 0) { // Sometime html can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return legacyAction?.message?.map((element) => getTextFromHtml(element?.html || element?.text)).join('') ?? ''; diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 42365dae33cbb..5a769a6636f27 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -2663,12 +2663,8 @@ describe('OptionsListUtils', () => { }); describe('getLastMessageTextForReport', () => { - describe('REPORT_PREVIEW action', () => { - it('should show report preview message for non-policy expense chat', async () => { - const report: Report = { - ...createRandomReport(0, undefined), - isOwnPolicyExpenseChat: false, - }; + describe('getReportPreviewMessage', () => { + it('should format report preview message correctly for non-policy expense chat with IOU action', async () => { const iouReport: Report = { ...createRandomReport(1, undefined), isOwnPolicyExpenseChat: false, @@ -2680,7 +2676,6 @@ describe('OptionsListUtils', () => { }; const reportPreviewAction: ReportAction = { ...createRandomReportAction(1), - reportID: report.reportID, actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, childMoneyRequestCount: 1, message: [{type: 'COMMENT', text: ''}], @@ -2710,30 +2705,15 @@ describe('OptionsListUtils', () => { }, shouldShow: true, }; - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, iouReport); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { - [reportPreviewAction.reportActionID]: reportPreviewAction, - }); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, { - [iouAction.reportActionID]: iouAction, - }); await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); await waitForBatchedUpdates(); - - const visibleReportActionsData = { - [report.reportID]: { - [reportPreviewAction.reportActionID]: true, - }, - [iouReport.reportID]: { - [iouAction.reportActionID]: true, - }, - }; - - const lastMessage = getLastMessageTextForReport({translate: translateLocal, report, lastActorDetails: null, isReportArchived: false, visibleReportActionsDataParam: visibleReportActionsData, lastAction: reportPreviewAction}); + + // Test getReportPreviewMessage directly - this is the function responsible for formatting the message const reportPreviewMessage = getReportPreviewMessage(iouReport, iouAction, true, false, null, true, reportPreviewAction); - const expected = formatReportLastMessageText(Parser.htmlToText(reportPreviewMessage)); - expect(lastMessage).toBe(expected); + const formattedMessage = formatReportLastMessageText(Parser.htmlToText(reportPreviewMessage)); + expect(formattedMessage).toBe('$1.00 for A A A'); }); }); it('MOVED_TRANSACTION action', async () => { From 8a3bf2f5df2879e1f050f0ef0a1bd86072f02164 Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 21 Jan 2026 16:32:49 +0100 Subject: [PATCH 31/32] Update SearchAutocompleteList, useSearchSelectorBase, and ShareTab to include visibleReportActionsData prop for improved report action handling --- src/components/Search/SearchAutocompleteList.tsx | 1 + src/hooks/useSearchSelector.base.ts | 2 ++ src/pages/Share/ShareTab.tsx | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 921136351d0c7..ee759ed58bfe1 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -641,6 +641,7 @@ function SearchAutocompleteList({ workspaceList, hasAutocompleteList, isAutocompleteList, + visibleReportActionsData, ]); const sortedRecentSearches = useMemo(() => { diff --git a/src/hooks/useSearchSelector.base.ts b/src/hooks/useSearchSelector.base.ts index 80384f722e053..87b5eaefd18bf 100644 --- a/src/hooks/useSearchSelector.base.ts +++ b/src/hooks/useSearchSelector.base.ts @@ -284,6 +284,7 @@ function useSearchSelectorBase({ areOptionsInitialized, searchContext, optionsWithContacts, + allPolicies, draftComments, nvpDismissedProductTraining, betas, @@ -298,6 +299,7 @@ function useSearchSelectorBase({ getValidOptionsConfig, selectedOptions, includeCurrentUser, + visibleReportActionsData, ]); const isOptionSelected = useMemo(() => { diff --git a/src/pages/Share/ShareTab.tsx b/src/pages/Share/ShareTab.tsx index bbbbf99710d68..1e83dea0e3273 100644 --- a/src/pages/Share/ShareTab.tsx +++ b/src/pages/Share/ShareTab.tsx @@ -82,7 +82,7 @@ function ShareTab({ref}: ShareTabProps) { loginList, visibleReportActionsData, }); - }, [areOptionsInitialized, options, draftComments, nvpDismissedProductTraining, betas, textInputValue, countryCode, loginList]); + }, [areOptionsInitialized, options, draftComments, nvpDismissedProductTraining, betas, textInputValue, countryCode, loginList, visibleReportActionsData]); const recentReportsOptions = useMemo(() => { if (textInputValue.trim() === '') { From d6f62ebcf088b904c4b0867d6b5f18f92ac8a92e Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Thu, 22 Jan 2026 08:40:38 +0100 Subject: [PATCH 32/32] revert --- src/libs/SearchUIUtils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 3232c2f580e34..e801cbb9f69fb 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -3296,9 +3296,7 @@ function getColumnsToShow( return result; } - const {moneyRequestReportActionsByTransactionID} = Array.isArray(data) - ? {moneyRequestReportActionsByTransactionID: new Map()} - : createReportActionsLookupMaps(data); + const {moneyRequestReportActionsByTransactionID} = Array.isArray(data) ? {} : createReportActionsLookupMaps(data); const updateColumns = (transaction: OnyxTypes.Transaction) => { const merchant = transaction.modifiedMerchant ? transaction.modifiedMerchant : (transaction.merchant ?? ''); if ((merchant !== '' && merchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && merchant !== CONST.TRANSACTION.DEFAULT_MERCHANT) || isScanning(transaction)) {