From 8d08b24b9c4f6d90b520ff5fec7614a186b31a7e Mon Sep 17 00:00:00 2001 From: bartlomiej obudzinski Date: Wed, 31 Dec 2025 14:14:44 +0100 Subject: [PATCH 01/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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)}`);