diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5bf1bcf1f2c0b..b40f10051aa7c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -982,6 +982,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 */ @@ -1388,6 +1389,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/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 6dffd994d6a7c..df0d08946c195 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,13 @@ function extractAttachments( return attachments.reverse(); } + const reportID = report?.reportID; + if (!reportID) { + return attachments.reverse(); + } 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 5495efe579502..6c192cff143ad 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); } @@ -323,6 +326,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio isScreenFocused, localeCompare, translate, + visibleReportActionsData, ], ); diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index cb65a7de9f53e..a39dfec594f4c 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -46,11 +46,11 @@ import { getMostRecentIOURequestActionID, getOneTransactionThreadReportID, hasNextActionMadeBySameActor, + isActionableWhisperRequiringWritePermission, isConsecutiveChronosAutomaticTimerAction, 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,34 @@ 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/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 4afa797312021..03c7561536eec 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -77,12 +77,12 @@ import { isReimbursementDeQueuedOrCanceledAction, isReimbursementQueuedAction, isRenamedAction, + isReportActionVisible, isReportPreviewAction, isTaskAction, isThreadParentMessage, isUnapprovedAction, isWhisperAction, - shouldReportActionBeVisible, withDEWRoutedActionsArray, } from '@libs/ReportActionsUtils'; import {computeReportName} from '@libs/ReportNameUtils'; @@ -161,6 +161,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'; @@ -242,6 +243,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; @@ -256,6 +265,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); @@ -291,9 +308,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, ); @@ -599,6 +616,7 @@ function getLastMessageTextForReport({ isReportArchived = false, policyForMovingExpensesID, reportMetadata, + visibleReportActionsDataParam, }: { report: OnyxEntry; lastActorDetails: Partial | null; @@ -608,10 +626,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; @@ -647,10 +666,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 4ecbc68489641..4ed94fcfb29cc 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -13,7 +13,17 @@ 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, @@ -1132,6 +1142,61 @@ 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 (!reportAction?.reportActionID) { + return false; + } + 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; + if (!reportID) { + return false; + } + + 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. @@ -1163,6 +1228,7 @@ function getLastVisibleAction( canUserPerformWriteAction?: boolean, actionsToMerge: Record | null> = {}, reportActionsParam: OnyxCollection = allReportActions, + visibleReportActionsData?: VisibleReportActionsDerivedValue, ): OnyxEntry { let reportActions: Array = []; if (!isEmpty(actionsToMerge)) { @@ -1172,7 +1238,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; @@ -1200,8 +1266,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)) { @@ -1303,6 +1370,7 @@ function getSortedReportActionsForDisplay( reportActions: OnyxEntry | ReportAction[], canUserPerformWriteAction?: boolean, shouldIncludeInvisibleActions = false, + visibleReportActionsData?: VisibleReportActionsDerivedValue, ): ReportAction[] { let filteredReportActions: ReportAction[] = []; if (!reportActions) { @@ -1313,7 +1381,13 @@ 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; + if (!reportID) { + return false; + } + return isReportActionVisible(reportAction, reportID, canUserPerformWriteAction, visibleReportActionsData); + }) .map(([, reportAction]) => reportAction); } @@ -1612,9 +1686,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)); @@ -2291,6 +2370,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}`, ''); @@ -3590,6 +3685,7 @@ export { isActionableWhisper, isActionableJoinRequest, isActionableJoinRequestPending, + isActionableJoinRequestPendingReportAction, isActionableMentionWhisper, isActionableMentionInviteToSubmitExpenseConfirmWhisper, isActionableReportMentionWhisper, @@ -3644,6 +3740,7 @@ export { isTrackExpenseAction, isTransactionThread, isTripPreview, + isTravelUpdate, isHoldAction, isWhisperAction, isSubmittedAction, @@ -3661,14 +3758,18 @@ export { isTagModificationAction, isIOUActionMatchingTransactionList, isResolvedActionableWhisper, + isVisiblePreviewOrMoneyRequest, isReimbursementDirectionInformationRequiredAction, shouldHideNewMarker, shouldReportActionBeVisible, shouldReportActionBeVisibleAsLastAction, + isReportActionVisible, + isReportActionVisibleAsLastAction, wasActionTakenByCurrentUser, isInviteOrRemovedAction, isActionableAddPaymentCard, isActionableCardFraudAlert, + isActionableWhisperRequiringWritePermission, getExportIntegrationActionFragments, getExportIntegrationLastMessageText, getExportIntegrationMessageHTML, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4bc3e042d7800..096c278ae9ffd 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -59,6 +59,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'; @@ -251,6 +252,7 @@ import { isRenamedAction, isReopenedAction, isReportActionAttachment, + isReportActionVisible, isReportPreviewAction, isRetractedAction, isReversedTransaction, @@ -265,7 +267,6 @@ import { isTripPreview, isUnapprovedAction, isWhisperAction, - shouldReportActionBeVisible, wasActionTakenByCurrentUser, } from './ReportActionsUtils'; import type {LastVisibleMessage} from './ReportActionsUtils'; @@ -9044,14 +9045,23 @@ 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 isChildReportHasComment = Object.values(reportActions ?? {})?.some( - (reportAction) => - (reportAction?.childVisibleActionCount ?? 0) > 0 && shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction(report, isReportArchived)), - ); + 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; } @@ -10080,7 +10090,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({ @@ -10095,7 +10111,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 = !!reportID && 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 b3a020b9d4afb..d8daf347aa6e8 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 { @@ -1634,7 +1634,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) @@ -1664,8 +1664,10 @@ 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)) || + !reportID || + !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/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..060db71173b74 --- /dev/null +++ b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts @@ -0,0 +1,116 @@ +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 {ReportAction, ReportActions} from '@src/types/onyx'; +import type {VisibleReportActionsDerivedValue} from '@src/types/onyx/DerivedValues'; + +function getOrCreateReportVisibilityRecord(result: VisibleReportActionsDerivedValue, reportID: string): Record { + if (!result[reportID]) { + // eslint-disable-next-line no-param-reassign + result[reportID] = {}; + } + return result[reportID]; +} + +function doesActionDependOnReportExistence(action: ReportAction): boolean { + const isUnreportedTransaction = action.actionName === CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION; + const isMovedTransaction = isMovedTransactionAction(action as OnyxEntry); + + return isUnreportedTransaction || isMovedTransaction; +} + +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], {sourceValues, currentValue}): VisibleReportActionsDerivedValue => { + if (!allReportActions) { + return {}; + } + + 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] = shouldReportActionBeVisible(action, actionID); + } + } + } + + 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] = shouldReportActionBeVisible(action, actionID); + } + } + } + + return result; + } + + 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] = shouldReportActionBeVisible(action, actionID); + } + } + + return result; + }, +}); diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index e74df5914bbc3..47d8226856a77 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -200,6 +200,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'; @@ -319,6 +320,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, @@ -2050,7 +2059,7 @@ function deleteReportComment( (action) => action.reportActionID !== reportAction.reportActionID && ReportActionsUtils.didMessageMentionCurrentUser(action, currentEmail) && - 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 99e14c82338a5..fcd1aa8f6bafd 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]); @@ -455,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 && !shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, canUserPerformWriteAction(report, isReportArchived)), - [linkedAction, report, isReportArchived], - ); + 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); diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 3e5d37aa0e3fb..bc6c483a940e5 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -25,11 +25,11 @@ import { getMostRecentIOURequestActionID, getOriginalMessage, getSortedReportActionsForDisplay, + isActionableWhisperRequiringWritePermission, isCreatedAction, 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); @@ -218,13 +219,33 @@ function ReportActionsView({ 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], + 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; + }), + [reportActions, isOffline, canPerformWriteAction, reportTransactionIDs, visibleReportActionsData, reportID], ); const newestReportAction = useMemo(() => reportActions?.at(0), [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 b1c4c6e0cebfb..a8b43515f73aa 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'; @@ -305,6 +305,7 @@ export type { LastSearchParams, ReportTransactionsAndViolationsDerivedValue, OutstandingReportsByPolicyIDDerivedValue, + VisibleReportActionsDerivedValue, ScheduleCallDraft, ValidateUserAndGetAccessiblePolicies, VacationDelegate, 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', 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)}`);