diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index fcaf56f1fefa3..b599fdc6d0f10 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -266,8 +266,14 @@ Onyx.connect({ } const reportID = CollectionUtils.extractCollectionItemID(key); allReportActions[reportID] = actions; - const sortedReportActions = ReportActionUtils.getSortedReportActions(Object.values(actions), true); + let sortedReportActions = ReportActionUtils.getSortedReportActions(Object.values(actions), true); allSortedReportActions[reportID] = sortedReportActions; + + const transactionThreadReportID = ReportActionUtils.getOneTransactionThreadReportID(reportID, allReportActions[reportID], true); + if (transactionThreadReportID) { + sortedReportActions = ReportActionUtils.getCombinedReportActions(allSortedReportActions[reportID], allSortedReportActions[transactionThreadReportID]); + } + lastReportActions[reportID] = sortedReportActions[0]; // The report is only visible if it is the last action not deleted that @@ -554,6 +560,7 @@ function getAlternateText( */ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails: Partial | null, policy?: OnyxEntry): string { const lastReportAction = visibleReportActionItems[report?.reportID ?? ''] ?? null; + // some types of actions are filtered out for lastReportAction, in some cases we need to check the actual last action const lastOriginalReportAction = lastReportActions[report?.reportID ?? ''] ?? null; let lastMessageTextFromReport = ''; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index dc07f31c10672..64c4ed61f7fdf 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -225,46 +225,6 @@ function isTransactionThread(parentReportAction: OnyxEntry | Empty ); } -/** - * Returns the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions with a childReportID. Returns a reportID if there is exactly one transaction thread for the report, and null otherwise. - */ -function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[], isOffline: boolean | undefined = undefined): string | null { - // If the report is not an IOU, Expense report or an Invoice, it shouldn't be treated as one-transaction report. - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - if (report?.type !== CONST.REPORT.TYPE.IOU && report?.type !== CONST.REPORT.TYPE.EXPENSE && report?.type !== CONST.REPORT.TYPE.INVOICE) { - return null; - } - - const reportActionsArray = Object.values(reportActions ?? {}); - - if (!reportActionsArray.length) { - return null; - } - - // Get all IOU report actions for the report. - const iouRequestTypes: Array> = [ - CONST.IOU.REPORT_ACTION_TYPE.CREATE, - CONST.IOU.REPORT_ACTION_TYPE.SPLIT, - CONST.IOU.REPORT_ACTION_TYPE.PAY, - CONST.IOU.REPORT_ACTION_TYPE.TRACK, - ]; - const iouRequestActions = reportActionsArray.filter( - (action) => - action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && - (iouRequestTypes.includes(action.originalMessage.type) ?? []) && - action.childReportID && - (Boolean(action.originalMessage.IOUTransactionID) || (action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && (isOffline ?? isNetworkOffline))), - ); - - // If we don't have any IOU request actions, or we have more than one IOU request actions, this isn't a oneTransaction report - if (!iouRequestActions.length || iouRequestActions.length > 1) { - return null; - } - - // Ensure we have a childReportID associated with the IOU report action - return iouRequestActions[0].childReportID ?? null; -} - /** * Sort an array of reportActions by their created timestamp first, and reportActionID second * This gives us a stable order even in the case of multiple reportActions created on the same millisecond @@ -301,6 +261,27 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort return sortedActions; } +/** + * Returns a sorted and filtered list of report actions from a report and it's associated child + * transaction thread report in order to correctly display reportActions from both reports in the one-transaction report view. + */ +function getCombinedReportActions(reportActions: ReportAction[], transactionThreadReportActions: ReportAction[]): ReportAction[] { + if (isEmptyObject(transactionThreadReportActions)) { + return reportActions; + } + + // Filter out the created action from the transaction thread report actions, since we already have the parent report's created action in `reportActions` + const filteredTransactionThreadReportActions = transactionThreadReportActions?.filter((action) => action.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED); + + // Filter out request and send money request actions because we don't want to show any preview actions for one transaction reports + const filteredReportActions = [...reportActions, ...filteredTransactionThreadReportActions].filter((action) => { + const actionType = (action as OriginalMessageIOU).originalMessage?.type ?? ''; + return actionType !== CONST.IOU.REPORT_ACTION_TYPE.CREATE && actionType !== CONST.IOU.REPORT_ACTION_TYPE.TRACK && !isSentMoneyReportAction(action); + }); + + return getSortedReportActions(filteredReportActions, true); +} + /** * Returns the largest gapless range of reportActions including a the provided reportActionID, where a "gap" is defined as a reportAction's `previousReportActionID` not matching the previous reportAction in the sortedReportActions array. * See unit tests for example of inputs and expected outputs. @@ -821,6 +802,67 @@ function isTaskAction(reportAction: OnyxEntry): boolean { ); } +/** + * Gets the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions. + * Returns a reportID if there is exactly one transaction thread for the report, and null otherwise. + */ +function getOneTransactionThreadReportID( + reportID: string, + reportActions: OnyxEntry | ReportAction[], + skipReportTypeCheck: boolean | undefined = undefined, + isOffline: boolean | undefined = undefined, +): string | null { + if (!skipReportTypeCheck) { + // If the report is not an IOU, Expense report, or Invoice, it shouldn't be treated as one-transaction report. + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + if (report?.type !== CONST.REPORT.TYPE.IOU && report?.type !== CONST.REPORT.TYPE.EXPENSE && report?.type !== CONST.REPORT.TYPE.INVOICE) { + return null; + } + } + + const reportActionsArray = Object.values(reportActions ?? {}); + if (!reportActionsArray.length) { + return null; + } + + // Get all IOU report actions for the report. + const iouRequestTypes: Array> = [ + CONST.IOU.REPORT_ACTION_TYPE.CREATE, + CONST.IOU.REPORT_ACTION_TYPE.SPLIT, + CONST.IOU.REPORT_ACTION_TYPE.PAY, + CONST.IOU.REPORT_ACTION_TYPE.TRACK, + ]; + + const iouRequestActions = reportActionsArray.filter( + (action) => + action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && + (iouRequestTypes.includes(action.originalMessage.type) ?? []) && + action.childReportID && + // Include deleted IOU reportActions if: + // - they have an assocaited IOU transaction ID or + // - they have visibile childActions (like comments) that we'd want to display + // - the action is pending deletion and the user is offline + (Boolean(action.originalMessage.IOUTransactionID) || + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (isMessageDeleted(action) && action.childVisibleActionCount) || + (action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && (isOffline ?? isNetworkOffline))), + ); + + // If we don't have any IOU request actions, or we have more than one IOU request actions, this isn't a oneTransaction report + if (!iouRequestActions.length || iouRequestActions.length > 1) { + return null; + } + + // If there's only one IOU request action associated with the report but it's been deleted, then we don't consider this a oneTransaction report + // and want to display it using the standard view + if (((iouRequestActions[0] as OriginalMessageIOU).originalMessage?.deleted ?? '') !== '') { + return null; + } + + // Ensure we have a childReportID associated with the IOU report action + return iouRequestActions[0].childReportID ?? null; +} + /** * When we delete certain reports, we want to check whether there are any visible actions left to display. * If there are no visible actions left (including system messages), we can hide the report from view entirely @@ -1142,6 +1184,7 @@ export { isApprovedOrSubmittedReportAction, getReportPreviewAction, getSortedReportActions, + getCombinedReportActions, getSortedReportActionsForDisplay, isConsecutiveActionMadeByPreviousActor, isCreatedAction, diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index d84d610a171a9..b82137756a284 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -347,7 +347,7 @@ function ReportScreen({ } const transactionThreadReportID = useMemo( - () => ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, reportActions ?? [], isOffline), + () => ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, reportActions ?? [], false, isOffline), [report.reportID, reportActions, isOffline], ); diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index cb904327e6253..02008a464859e 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -138,22 +138,18 @@ function ReportActionsView({ // Get a sorted array of reportActions for both the current report and the transaction thread report associated with this report (if there is one) // so that we display transaction-level and report-level report actions in order in the one-transaction view - const [combinedReportActions, parentReportActionForTransactionThread] = useMemo(() => { - if (isEmptyObject(transactionThreadReportActions)) { - return [allReportActions, null]; - } - - // Filter out the created action from the transaction thread report actions, since we already have the parent report's created action in `reportActions` - const filteredTransactionThreadReportActions = transactionThreadReportActions?.filter((action) => action.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED); - const moneyRequestAction = allReportActions.find((action) => action.reportActionID === transactionThreadReport?.parentReportActionID); + const combinedReportActions = useMemo( + () => ReportActionsUtils.getCombinedReportActions(allReportActions, transactionThreadReportActions), + [allReportActions, transactionThreadReportActions], + ); - // Filter out the expense actions because we don't want to show any preview actions for one-transaction reports - const filteredReportActions = [...allReportActions, ...filteredTransactionThreadReportActions].filter((action) => { - const actionType = (action as OnyxTypes.OriginalMessageIOU).originalMessage?.type ?? ''; - return actionType !== CONST.IOU.REPORT_ACTION_TYPE.CREATE && actionType !== CONST.IOU.REPORT_ACTION_TYPE.TRACK && !ReportActionsUtils.isSentMoneyReportAction(action); - }); - return [ReportActionsUtils.getSortedReportActions(filteredReportActions, true), moneyRequestAction ?? null]; - }, [allReportActions, transactionThreadReportActions, transactionThreadReport?.parentReportActionID]); + const parentReportActionForTransactionThread = useMemo( + () => + isEmptyObject(transactionThreadReportActions) + ? null + : (allReportActions.find((action) => action.reportActionID === transactionThreadReport?.parentReportActionID) as OnyxEntry), + [allReportActions, transactionThreadReportActions, transactionThreadReport?.parentReportActionID], + ); const indexOfLinkedAction = useMemo(() => { if (!reportActionID) { @@ -322,13 +318,13 @@ function ReportActionsView({ } if (!isEmptyObject(transactionThreadReport)) { - // Get newer actions based on the newest reportAction for the current report + // Get older actions based on the oldest reportAction for the current report const oldestActionCurrentReport = reportActionIDMap.findLast((item) => item.reportID === reportID); - Report.getNewerActions(oldestActionCurrentReport?.reportID ?? '0', oldestActionCurrentReport?.reportActionID ?? '0'); + Report.getOlderActions(oldestActionCurrentReport?.reportID ?? '0', oldestActionCurrentReport?.reportActionID ?? '0'); - // Get newer actions based on the newest reportAction for the transaction thread report + // Get older actions based on the oldest reportAction for the transaction thread report const oldestActionTransactionThreadReport = reportActionIDMap.findLast((item) => item.reportID === transactionThreadReport.reportID); - Report.getNewerActions(oldestActionTransactionThreadReport?.reportID ?? '0', oldestActionTransactionThreadReport?.reportActionID ?? '0'); + Report.getOlderActions(oldestActionTransactionThreadReport?.reportID ?? '0', oldestActionTransactionThreadReport?.reportActionID ?? '0'); } else { // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index a746053be64e8..d49db3e7ba0f7 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -67,6 +67,7 @@ type IOUMessage = { type: ValueOf; cancellationReason?: string; paymentType?: PaymentMethodType; + deleted?: string; /** Only exists when we are sending money */ IOUDetails?: IOUDetails; };