diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b715240b98f5a..7d1a9a9af853e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -10391,7 +10391,7 @@ function getAllAncestorReportActionIDs(report: Report | null | undefined, includ } /** - * Get optimistic data of parent report action + * Get optimistic data of the parent report action * @param report The report that is updated * @param lastVisibleActionCreated Last visible action created of the child report * @param type The type of action in the child report @@ -10430,6 +10430,28 @@ function getOptimisticDataForParentReportAction(report: Report | undefined, last }; }); } +/** + * Get optimistic data of the ancestor report actions + * @param ancestors The thread report ancestors + * @param lastVisibleActionCreated Last visible action created of the child report + * @param type The type of action in the child report + */ +function getOptimisticDataForAncestors(ancestors: Ancestor[], lastVisibleActionCreated: string, type: string): OnyxUpdate[] { + let previousActionDeleted = false; + return ancestors.map(({report: ancestorReport, reportAction: ancestorReportAction}, index) => { + const updatedReportAction = updateOptimisticParentReportAction(ancestorReportAction, lastVisibleActionCreated, type, previousActionDeleted ? index + 1 : undefined); + + previousActionDeleted = isDeletedAction(ancestorReportAction) && updatedReportAction.childVisibleActionCount === 0; + + return { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReport.reportID}`, + value: { + [ancestorReportAction.reportActionID]: updatedReportAction, + }, + }; + }); +} function canBeAutoReimbursed(report: OnyxInputOrEntry, policy: OnyxInputOrEntry | SearchPolicy): boolean { if (isEmptyObject(policy)) { @@ -12480,6 +12502,7 @@ export { getMoneyRequestOptions, getMoneyRequestSpendBreakdown, getNonHeldAndFullAmount, + getOptimisticDataForAncestors, getOptimisticDataForParentReportAction, getOriginalReportID, getOutstandingChildRequest, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 3779020084bda..ea72dc2ee95af 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -97,7 +97,7 @@ import Pusher from '@libs/Pusher'; import type {UserIsLeavingRoomEvent, UserIsTypingEvent} from '@libs/Pusher/types'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import {updateTitleFieldToMatchPolicy} from '@libs/ReportTitleUtils'; -import type {OptimisticAddCommentReportAction, OptimisticChatReport, SelfDMParameters} from '@libs/ReportUtils'; +import type {Ancestor, OptimisticAddCommentReportAction, OptimisticChatReport, SelfDMParameters} from '@libs/ReportUtils'; import { buildOptimisticAddCommentReportAction, buildOptimisticChangeFieldAction, @@ -128,6 +128,7 @@ import { getFieldViolation, getLastVisibleMessage, getNextApproverAccountID, + getOptimisticDataForAncestors, getOptimisticDataForParentReportAction, getOriginalReportID, getOutstandingChildRequest, @@ -1937,7 +1938,13 @@ function handlePreexistingReport(report: Report) { } /** Deletes a comment from the report, basically sets it as empty string */ -function deleteReportComment(reportID: string | undefined, reportAction: ReportAction, isReportArchived: boolean | undefined, isOriginalReportArchived: boolean | undefined) { +function deleteReportComment( + reportID: string | undefined, + reportAction: ReportAction, + ancestors: Ancestor[], + isReportArchived: boolean | undefined, + isOriginalReportArchived: boolean | undefined, +) { const originalReportID = getOriginalReportID(reportID, reportAction); const reportActionID = reportAction.reportActionID; @@ -2032,18 +2039,7 @@ function deleteReportComment(reportID: string | undefined, reportAction: ReportA // Update optimistic data for parent report action if the report is a child report and the reportAction has no visible child const childVisibleActionCount = reportAction.childVisibleActionCount ?? 0; if (childVisibleActionCount === 0) { - const originalReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`]; - const optimisticParentReportData = getOptimisticDataForParentReportAction( - originalReport, - optimisticReport?.lastVisibleActionCreated ?? '', - CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - ); - optimisticParentReportData.forEach((parentReportData) => { - if (isEmptyObject(parentReportData)) { - return; - } - optimisticData.push(parentReportData); - }); + optimisticData.push(...getOptimisticDataForAncestors(ancestors, optimisticReport?.lastVisibleActionCreated ?? '', CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)); } const parameters: DeleteCommentParams = { @@ -2120,6 +2116,7 @@ function handleUserDeletedLinksInHtml(newCommentText: string, originalCommentMar function editReportComment( originalReport: OnyxEntry, originalReportAction: OnyxEntry, + ancestors: Ancestor[], textForNewComment: string, isOriginalReportArchived: boolean | undefined, isOriginalParentReportArchived: boolean | undefined, @@ -2156,7 +2153,7 @@ function editReportComment( // Delete the comment if it's empty if (!htmlForNewComment) { - deleteReportComment(originalReportID, originalReportAction, isOriginalReportArchived, isOriginalParentReportArchived); + deleteReportComment(originalReportID, originalReportAction, ancestors, isOriginalReportArchived, isOriginalParentReportArchived); return; } diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 17dac6d6ec5e4..9f6870c360334 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -10,6 +10,7 @@ import {Actions, ActionSheetAwareScrollViewContext} from '@components/ActionShee import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; import {useSearchContext} from '@components/Search/SearchContext'; +import useAncestors from '@hooks/useAncestors'; import useDeleteTransactions from '@hooks/useDeleteTransactions'; import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations'; import useGetIOUReportFromReportAction from '@hooks/useGetIOUReportFromReportAction'; @@ -328,6 +329,17 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro policy, }); + const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getOriginalReportID(reportIDRef.current, reportActionRef.current)}`, { + canBeMissing: true, + }); + const ancestorsRef = useRef([]); + const ancestors = useAncestors(originalReport); + useEffect(() => { + if (!originalReport) { + return; + } + ancestorsRef.current = ancestors; + }, [originalReport, ancestors]); const confirmDeleteAndHideModal = useCallback(() => { callbackWhenDeleteModalHide.current = runAndResetCallback(onConfirmDeleteModal.current); const reportAction = reportActionRef.current; @@ -355,7 +367,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro } else if (reportAction) { // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { - deleteReportComment(reportIDRef.current, reportAction, isReportArchived, isOriginalReportArchived); + deleteReportComment(reportIDRef.current, reportAction, ancestorsRef.current, isReportArchived, isOriginalReportArchived); }); } diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 45cfc6dc7745f..3d9c635349a96 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -15,6 +15,7 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Tooltip from '@components/Tooltip'; +import useAncestors from '@hooks/useAncestors'; import useHandleExceedMaxCommentLength from '@hooks/useHandleExceedMaxCommentLength'; import useIsScrollLikelyLayoutTriggered from '@hooks/useIsScrollLikelyLayoutTriggered'; import useKeyboardState from '@hooks/useKeyboardState'; @@ -151,6 +152,7 @@ function ReportActionItemMessageEdit({ const isOriginalReportArchived = useReportIsArchived(originalReportID); const originalParentReportID = getOriginalReportID(originalReportID, action); const isOriginalParentReportArchived = useReportIsArchived(originalParentReportID); + const ancestors = useAncestors(originalReport); useEffect(() => { draftMessageVideoAttributeCache.clear(); @@ -309,9 +311,9 @@ function ReportActionItemMessageEdit({ ReportActionContextMenu.showDeleteModal(originalReportID ?? reportID, action, true, deleteDraft, () => focusEditAfterCancelDelete(textInputRef.current)); return; } - editReportComment(originalReport, action, trimmedNewDraft, isOriginalReportArchived, isOriginalParentReportArchived, Object.fromEntries(draftMessageVideoAttributeCache)); + editReportComment(originalReport, action, ancestors, trimmedNewDraft, isOriginalReportArchived, isOriginalParentReportArchived, Object.fromEntries(draftMessageVideoAttributeCache)); deleteDraft(); - }, [reportID, action, deleteDraft, draft, originalReportID, isOriginalReportArchived, originalReport, isOriginalParentReportArchived, debouncedValidateCommentMaxLength]); + }, [reportID, action, ancestors, deleteDraft, draft, originalReportID, isOriginalReportArchived, originalReport, isOriginalParentReportArchived, debouncedValidateCommentMaxLength]); /** * @param emoji diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 7b87242f9b906..1f1eea189dd7c 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -1,10 +1,13 @@ /* eslint-disable @typescript-eslint/naming-convention */ import {afterEach, beforeAll, beforeEach, describe, expect, it} from '@jest/globals'; +import {renderHook} from '@testing-library/react-native'; import {addSeconds, format, subMinutes} from 'date-fns'; import {toZonedTime} from 'date-fns-tz'; import type {Mock} from 'jest-mock'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; +import useAncestors from '@hooks/useAncestors'; import {getOnboardingMessages} from '@libs/actions/Welcome/OnboardingFlow'; import {WRITE_COMMANDS} from '@libs/API/types'; import HttpUtils from '@libs/HttpUtils'; @@ -312,6 +315,8 @@ describe('actions/Report', () => { callback: (val) => (reportActions = val ?? {}), }); + const {result: ancestors, rerender} = renderHook(() => useAncestors(report)); + const USER_1_LOGIN = 'user@test.com'; const USER_1_ACCOUNT_ID = 1; const USER_2_ACCOUNT_ID = 2; @@ -501,8 +506,9 @@ describe('actions/Report', () => { return waitForNetworkPromises(); }) .then(() => { + rerender(report); // If the user deletes a comment that is before the last read - Report.deleteReportComment(REPORT_ID, {...reportActions[200]}, undefined, undefined); + Report.deleteReportComment(REPORT_ID, {...reportActions[200]}, ancestors.current, undefined, undefined); return waitForBatchedUpdates(); }) .then(() => { @@ -519,8 +525,9 @@ describe('actions/Report', () => { expect(ReportUtils.isUnread(report, undefined, undefined)).toBe(true); expect(report?.lastReadTime).toBe(DateUtils.subtractMillisecondsFromDateTime(reportActions[400].created, 1)); + rerender(report); // If the user deletes the last comment after the lastReadTime the lastMessageText will reflect the new last comment - Report.deleteReportComment(REPORT_ID, {...reportActions[400]}, undefined, undefined); + Report.deleteReportComment(REPORT_ID, {...reportActions[400]}, ancestors.current, undefined, undefined); return waitForBatchedUpdates(); }) .then(() => { @@ -915,7 +922,9 @@ describe('actions/Report', () => { const originalReport = { reportID: REPORT_ID, }; - Report.editReportComment(originalReport, newReportAction, 'Testing an edited comment', undefined, undefined); + + const {result: ancestors, rerender} = renderHook(() => useAncestors(originalReport)); + Report.editReportComment(originalReport, newReportAction, ancestors.current, 'Testing an edited comment', undefined, undefined); await waitForBatchedUpdates(); @@ -948,7 +957,8 @@ describe('actions/Report', () => { }); }); - Report.deleteReportComment(REPORT_ID, newReportAction, undefined, undefined); + rerender(originalReport); + Report.deleteReportComment(REPORT_ID, newReportAction, ancestors.current, undefined, undefined); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(0); @@ -994,7 +1004,9 @@ describe('actions/Report', () => { const originalReport = { reportID: REPORT_ID, }; - Report.editReportComment(originalReport, reportAction, 'Testing an edited comment', undefined, undefined); + const {result: ancestors, rerender} = renderHook(() => useAncestors(originalReport)); + + Report.editReportComment(originalReport, reportAction, ancestors.current, 'Testing an edited comment', undefined, undefined); await waitForBatchedUpdates(); @@ -1009,7 +1021,8 @@ describe('actions/Report', () => { }); }); - Report.deleteReportComment(REPORT_ID, reportAction, undefined, undefined); + rerender(originalReport); + Report.deleteReportComment(REPORT_ID, reportAction, ancestors.current, undefined, undefined); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(1); @@ -1058,7 +1071,7 @@ describe('actions/Report', () => { }), ); - Report.deleteReportComment(REPORT_ID, reportAction, undefined, undefined); + Report.deleteReportComment(REPORT_ID, reportAction, [], undefined, undefined); jest.runOnlyPendingTimers(); await waitForBatchedUpdates(); @@ -1120,7 +1133,7 @@ describe('actions/Report', () => { }); }); - Report.deleteReportComment(REPORT_ID, newReportAction, undefined, undefined); + Report.deleteReportComment(REPORT_ID, newReportAction, [], undefined, undefined); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(0); @@ -1189,7 +1202,7 @@ describe('actions/Report', () => { }); }); - Report.deleteReportComment(REPORT_ID, newReportAction, undefined, undefined); + Report.deleteReportComment(REPORT_ID, newReportAction, [], undefined, undefined); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(0); @@ -1384,7 +1397,7 @@ describe('actions/Report', () => { }); }); - Report.deleteReportComment(REPORT_ID, newReportAction, undefined, undefined); + Report.deleteReportComment(REPORT_ID, newReportAction, [], undefined, undefined); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(0); @@ -1467,7 +1480,7 @@ describe('actions/Report', () => { }); }); - Report.deleteReportComment(REPORT_ID, reportAction, undefined, undefined); + Report.deleteReportComment(REPORT_ID, reportAction, [], undefined, undefined); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(1); @@ -1511,26 +1524,20 @@ describe('actions/Report', () => { reportActionID, ); - Report.deleteReportComment(REPORT_ID, reportAction, undefined, undefined); + await waitForBatchedUpdates(); + + const {result: ancestors} = renderHook(() => useAncestors({reportID: REPORT_ID})); + + Report.deleteReportComment(REPORT_ID, reportAction, ancestors.current, undefined, undefined); expect(PersistedRequests.getAll().length).toBe(3); - await new Promise((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.PERSISTED_REQUESTS, - callback: (persistedRequests) => { - if (persistedRequests?.length !== 3) { - return; - } - Onyx.disconnect(connection); + await waitForBatchedUpdates(); - expect(persistedRequests?.at(0)?.command).toBe(WRITE_COMMANDS.ADD_COMMENT); - expect(persistedRequests?.at(1)?.command).toBe(WRITE_COMMANDS.OPEN_REPORT); - expect(persistedRequests?.at(2)?.command).toBe(WRITE_COMMANDS.DELETE_COMMENT); - resolve(); - }, - }); - }); + const persistedRequests = await OnyxUtils.get(ONYXKEYS.PERSISTED_REQUESTS); + expect(persistedRequests?.at(0)?.command).toBe(WRITE_COMMANDS.ADD_COMMENT); + expect(persistedRequests?.at(1)?.command).toBe(WRITE_COMMANDS.OPEN_REPORT); + expect(persistedRequests?.at(2)?.command).toBe(WRITE_COMMANDS.DELETE_COMMENT); Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); await waitForBatchedUpdates(); @@ -1559,22 +1566,13 @@ describe('actions/Report', () => { const originalReport = { reportID: REPORT_ID, }; - Report.editReportComment(originalReport, reportAction, 'Testing an edited comment', undefined, undefined); + Report.editReportComment(originalReport, reportAction, [], 'Testing an edited comment', undefined, undefined); await waitForBatchedUpdates(); - await new Promise((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.PERSISTED_REQUESTS, - callback: (persistedRequests) => { - Onyx.disconnect(connection); - - expect(persistedRequests?.at(0)?.command).toBe(WRITE_COMMANDS.ADD_COMMENT); + const persistedRequests = await OnyxUtils.get(ONYXKEYS.PERSISTED_REQUESTS); - resolve(); - }, - }); - }); + expect(persistedRequests?.at(0)?.command).toBe(WRITE_COMMANDS.ADD_COMMENT); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(1); @@ -1603,9 +1601,12 @@ describe('actions/Report', () => { const originalReport = { reportID, }; - Report.editReportComment(originalReport, action, 'value1', undefined, undefined); - Report.editReportComment(originalReport, action, 'value2', undefined, undefined); - Report.editReportComment(originalReport, action, 'value3', undefined, undefined); + + const {result: ancestors} = renderHook(() => useAncestors(originalReport)); + + Report.editReportComment(originalReport, action, ancestors.current, 'value1', undefined, undefined); + Report.editReportComment(originalReport, action, ancestors.current, 'value2', undefined, undefined); + Report.editReportComment(originalReport, action, ancestors.current, 'value3', undefined, undefined); const requests = PersistedRequests?.getAll(); @@ -1645,22 +1646,23 @@ describe('actions/Report', () => { [mentionActionID]: mentionAction, [mentionActionID2]: mentionAction2, }); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { + + let report = { ...createRandomReport(Number(reportID), undefined), lastMentionedTime: mentionAction2.created, - }); + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); - Report.deleteReportComment(reportID, mentionAction, undefined, undefined); - Report.deleteReportComment(reportID, mentionAction2, undefined, undefined); + await waitForBatchedUpdates(); + + const {result: ancestors} = renderHook(() => useAncestors(report)); + + Report.deleteReportComment(reportID, mentionAction, ancestors.current, undefined, undefined); + Report.deleteReportComment(reportID, mentionAction2, ancestors.current, undefined, undefined); await waitForBatchedUpdates(); - const report = await new Promise>((resolve) => { - Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - callback: resolve, - }); - }); + report = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); expect(report?.lastMentionedTime).toBeUndefined(); }); diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index a274a03aab40d..beea382be1eac 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -579,7 +579,7 @@ describe('Unread Indicators', () => { expect(screen.getAllByText('Current User Comment 1').at(0)).toBeOnTheScreen(); if (lastReportAction) { - deleteReportComment(REPORT_ID, lastReportAction, undefined, undefined); + deleteReportComment(REPORT_ID, lastReportAction, [], undefined, undefined); } return waitForBatchedUpdates(); }) @@ -616,7 +616,7 @@ describe('Unread Indicators', () => { await waitForBatchedUpdates(); - deleteReportComment(REPORT_ID, firstNewReportAction, undefined, undefined); + deleteReportComment(REPORT_ID, firstNewReportAction, [], undefined, undefined); await waitForBatchedUpdates(); }