diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7077555d7f24..d80919b6ff6d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -216,6 +216,7 @@ import { isIntegrationMessageAction, isMoneyRequestAction, isMovedAction, + isMovedTransactionAction, isOlderReportAction, isPendingRemove, isReopenedAction, @@ -11686,6 +11687,7 @@ function getNonHeldAndFullAmount( * - The action is a split expense action * - The action is deleted and is not threaded * - The report is archived and the action is not threaded + * - The action is a moved system message and is not threaded (moved actions can't be threaded server-side, so offering "Reply in thread" leads to a failed thread-creation call) * - The action is a whisper action and it's neither a report preview nor IOU action * - The action is the thread's first chat */ @@ -11696,12 +11698,14 @@ function shouldDisableThread(reportAction: OnyxInputOrEntry, isThr const isIOUAction = isMoneyRequestAction(reportAction); const isWhisperActionLocal = isWhisperAction(reportAction) || isActionableTrackExpense(reportAction); const isDynamicWorkflowRoutedAction = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); + const isMovedSystemMessage = isMovedAction(reportAction) || isMovedTransactionAction(reportAction); const isActionDisabled = CONST.REPORT.ACTIONS.THREAD_DISABLED.some((action: string) => action === reportAction?.actionName); return ( isActionDisabled || isSplitBillAction || (isDeletedActionLocal && !reportAction?.childVisibleActionCount) || (isReportArchived && !reportAction?.childVisibleActionCount) || + (isMovedSystemMessage && !reportAction?.childVisibleActionCount) || (isWhisperActionLocal && !isReportPreviewActionLocal && !isIOUAction) || isThreadReportParentAction || isDynamicWorkflowRoutedAction diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 542ffc796a6a..6ce477655ab5 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -5325,6 +5325,36 @@ describe('ReportUtils', () => { expect(isThreadDisabled).toBeTruthy(); }); }); + + describe('moved system messages', () => { + it.each([CONST.REPORT.ACTIONS.TYPE.MOVED, CONST.REPORT.ACTIONS.TYPE.MOVED_TRANSACTION])('should be disabled for a %s action with no child visible action count', (actionName) => { + // Given a moved system message that has never been threaded + const reportAction = createMock({ + actionName, + childVisibleActionCount: 0, + }); + + // When it's checked to see if the thread should be disabled + const isThreadDisabled = shouldDisableThread(reportAction, false); + + // Then the thread should be disabled so the doomed thread-creation call is never made + expect(isThreadDisabled).toBeTruthy(); + }); + + it.each([CONST.REPORT.ACTIONS.TYPE.MOVED, CONST.REPORT.ACTIONS.TYPE.MOVED_TRANSACTION])('should be enabled for a %s action that has already been threaded', (actionName) => { + // Given a moved system message that already has a legitimately threaded child + const reportAction = createMock({ + actionName, + childVisibleActionCount: 1, + }); + + // When it's checked to see if the thread should be disabled + const isThreadDisabled = shouldDisableThread(reportAction, false); + + // Then the thread should stay enabled so the existing thread remains reachable + expect(isThreadDisabled).toBeFalsy(); + }); + }); }); describe('isChatUsedForOnboarding', () => {