From 5b49afe65c81db3aae54580b002b692abdf8f306 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Mon, 27 Oct 2025 21:25:28 +0000 Subject: [PATCH 01/14] Add getOptimisticDataForAncestors function and update related components - Implemented getOptimisticDataForAncestors to handle optimistic updates for ancestor reports. - Updated deleteReportComment and editReportComment functions to utilize the new ancestors parameter. - Modified PopoverReportActionContextMenu and ReportActionItemMessageEdit components to fetch ancestors. - Enhanced tests in ReportTest to validate changes with ancestors. --- src/libs/ReportUtils.ts | 26 +++++ src/libs/actions/Report.ts | 14 +-- .../PopoverReportActionContextMenu.tsx | 5 +- .../report/ReportActionItemMessageEdit.tsx | 4 +- tests/actions/ReportTest.ts | 104 +++++++++--------- 5 files changed, 90 insertions(+), 63 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 27d34ba5e9aba..47aa62ff54a4e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -10175,6 +10175,31 @@ function getOptimisticDataForParentReportAction(report: Report | undefined, last }; }); } +/** + * Get optimistic data the ancestor report actions + * @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 + */ +function getOptimisticDataForAncestors(ancestors: Ancestor[], lastVisibleActionCreated: string, type: string): Array { + return Array.from(ancestors, ({report: ancestorReport, reportAction: ancestorReportAction}) => { + if (!ancestorReport || isEmptyObject(ancestorReport)) { + return null; + } + + if (!ancestorReportAction?.reportActionID || isEmptyObject(ancestorReportAction)) { + return null; + } + + return { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReport.reportID}`, + value: { + [ancestorReportAction.reportActionID]: updateOptimisticParentReportAction(ancestorReportAction, lastVisibleActionCreated, type), + }, + }; + }); +} function canBeAutoReimbursed(report: OnyxInputOrEntry, policy: OnyxInputOrEntry | SearchPolicy): boolean { if (isEmptyObject(policy)) { @@ -12240,6 +12265,7 @@ export { getMoneyRequestOptions, getMoneyRequestSpendBreakdown, getNonHeldAndFullAmount, + getOptimisticDataForAncestors, getOptimisticDataForParentReportAction, getOriginalReportID, getOutstandingChildRequest, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 1b9277f9a8ab7..87cc87de8d969 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -98,7 +98,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, @@ -129,6 +129,7 @@ import { getFieldViolation, getLastVisibleMessage, getNextApproverAccountID, + getOptimisticDataForAncestors, getOptimisticDataForParentReportAction, getOriginalReportID, getOutstandingChildRequest, @@ -1931,7 +1932,7 @@ function handlePreexistingReport(report: Report) { } /** Deletes a comment from the report, basically sets it as empty string */ -function deleteReportComment(reportID: string | undefined, reportAction: ReportAction, isReportArchived = false, isOriginalReportArchived = false) { +function deleteReportComment(reportID: string | undefined, reportAction: ReportAction, ancestors: Ancestor[], isReportArchived = false, isOriginalReportArchived = false) { const originalReportID = getOriginalReportID(reportID, reportAction); const reportActionID = reportAction.reportActionID; @@ -2027,11 +2028,7 @@ function deleteReportComment(reportID: string | undefined, reportAction: ReportA 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, - ); + const optimisticParentReportData = getOptimisticDataForAncestors(ancestors, optimisticReport?.lastVisibleActionCreated ?? '', CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); optimisticParentReportData.forEach((parentReportData) => { if (isEmptyObject(parentReportData)) { return; @@ -2114,6 +2111,7 @@ function handleUserDeletedLinksInHtml(newCommentText: string, originalCommentMar function editReportComment( originalReport: OnyxEntry, originalReportAction: OnyxEntry, + ancestors: Ancestor[], textForNewComment: string, videoAttributeCache?: Record, isOriginalReportArchived = false, @@ -2150,7 +2148,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 4df956a4a266d..9dc655a4189f9 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -9,6 +9,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {Actions, ActionSheetAwareScrollViewContext} from '@components/ActionSheetAwareScrollView'; import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; +import useAncestors from '@hooks/useAncestors'; import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations'; import useGetIOUReportFromReportAction from '@hooks/useGetIOUReportFromReportAction'; import useLocalize from '@hooks/useLocalize'; @@ -56,7 +57,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro const isReportArchived = useReportIsArchived(reportIDRef.current); const isOriginalReportArchived = useReportIsArchived(getOriginalReportID(reportIDRef.current, reportActionRef.current)); const {iouReport, chatReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(reportActionRef.current); - const cursorRelativePosition = useRef({ horizontal: 0, vertical: 0, @@ -304,6 +304,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDRef.current}`, { canBeMissing: true, }); + const ancestors = useAncestors(report); const confirmDeleteAndHideModal = useCallback(() => { callbackWhenDeleteModalHide.current = runAndResetCallback(onConfirmDeleteModal.current); @@ -343,7 +344,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, ancestors, isReportArchived, isOriginalReportArchived); }); } diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 1a138ef8bef96..ea1b368dcad5f 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,7 +311,7 @@ function ReportActionItemMessageEdit({ ReportActionContextMenu.showDeleteModal(originalReportID ?? reportID, action, true, deleteDraft, () => focusEditAfterCancelDelete(textInputRef.current)); return; } - editReportComment(originalReport, action, trimmedNewDraft, Object.fromEntries(draftMessageVideoAttributeCache), isOriginalReportArchived, isOriginalParentReportArchived); + editReportComment(originalReport, action, ancestors, trimmedNewDraft, Object.fromEntries(draftMessageVideoAttributeCache), isOriginalReportArchived, isOriginalParentReportArchived); deleteDraft(); }, [reportID, action, deleteDraft, draft, originalReportID, isOriginalReportArchived, originalReport, isOriginalParentReportArchived]); diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 7a38197ad0679..a64e2bb4ac2ae 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'; @@ -314,6 +317,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; @@ -503,8 +508,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]}); + Report.deleteReportComment(REPORT_ID, {...reportActions[200]}, ancestors.current); return waitForBatchedUpdates(); }) .then(() => { @@ -521,8 +527,9 @@ describe('actions/Report', () => { expect(ReportUtils.isUnread(report, 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]}); + Report.deleteReportComment(REPORT_ID, {...reportActions[400]}, ancestors.current); return waitForBatchedUpdates(); }) .then(() => { @@ -917,7 +924,9 @@ describe('actions/Report', () => { const originalReport = { reportID: REPORT_ID, }; - Report.editReportComment(originalReport, newReportAction, 'Testing an edited comment'); + + const {result: ancestors, rerender} = renderHook(() => useAncestors(originalReport)); + Report.editReportComment(originalReport, newReportAction, ancestors.current, 'Testing an edited comment'); await waitForBatchedUpdates(); @@ -950,7 +959,8 @@ describe('actions/Report', () => { }); }); - Report.deleteReportComment(REPORT_ID, newReportAction); + rerender(originalReport); + Report.deleteReportComment(REPORT_ID, newReportAction, ancestors.current); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(0); @@ -996,7 +1006,9 @@ describe('actions/Report', () => { const originalReport = { reportID: REPORT_ID, }; - Report.editReportComment(originalReport, reportAction, 'Testing an edited comment'); + const {result: ancestors, rerender} = renderHook(() => useAncestors(originalReport)); + + Report.editReportComment(originalReport, reportAction, ancestors.current, 'Testing an edited comment'); await waitForBatchedUpdates(); @@ -1011,7 +1023,8 @@ describe('actions/Report', () => { }); }); - Report.deleteReportComment(REPORT_ID, reportAction); + rerender(originalReport); + Report.deleteReportComment(REPORT_ID, reportAction, ancestors.current); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(1); @@ -1060,7 +1073,7 @@ describe('actions/Report', () => { }), ); - Report.deleteReportComment(REPORT_ID, reportAction); + Report.deleteReportComment(REPORT_ID, reportAction, []); jest.runOnlyPendingTimers(); await waitForBatchedUpdates(); @@ -1122,7 +1135,7 @@ describe('actions/Report', () => { }); }); - Report.deleteReportComment(REPORT_ID, newReportAction); + Report.deleteReportComment(REPORT_ID, newReportAction, []); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(0); @@ -1191,7 +1204,7 @@ describe('actions/Report', () => { }); }); - Report.deleteReportComment(REPORT_ID, newReportAction); + Report.deleteReportComment(REPORT_ID, newReportAction, []); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(0); @@ -1386,7 +1399,7 @@ describe('actions/Report', () => { }); }); - Report.deleteReportComment(REPORT_ID, newReportAction); + Report.deleteReportComment(REPORT_ID, newReportAction, []); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(0); @@ -1469,7 +1482,7 @@ describe('actions/Report', () => { }); }); - Report.deleteReportComment(REPORT_ID, reportAction); + Report.deleteReportComment(REPORT_ID, reportAction, []); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(1); @@ -1513,26 +1526,20 @@ describe('actions/Report', () => { reportActionID, ); - Report.deleteReportComment(REPORT_ID, reportAction); + await waitForBatchedUpdates(); + + const {result: ancestors} = renderHook(() => useAncestors({reportID: REPORT_ID})); + + Report.deleteReportComment(REPORT_ID, reportAction, ancestors.current); 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(); @@ -1561,22 +1568,13 @@ describe('actions/Report', () => { const originalReport = { reportID: REPORT_ID, }; - Report.editReportComment(originalReport, reportAction, 'Testing an edited comment'); + Report.editReportComment(originalReport, reportAction, [], 'Testing an edited comment'); await waitForBatchedUpdates(); - await new Promise((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.PERSISTED_REQUESTS, - callback: (persistedRequests) => { - Onyx.disconnect(connection); + const persistedRequests = await OnyxUtils.get(ONYXKEYS.PERSISTED_REQUESTS); - expect(persistedRequests?.at(0)?.command).toBe(WRITE_COMMANDS.ADD_COMMENT); - - resolve(); - }, - }); - }); + expect(persistedRequests?.at(0)?.command).toBe(WRITE_COMMANDS.ADD_COMMENT); await waitForBatchedUpdates(); expect(PersistedRequests.getAll().length).toBe(1); @@ -1605,9 +1603,12 @@ describe('actions/Report', () => { const originalReport = { reportID, }; - Report.editReportComment(originalReport, action, 'value1'); - Report.editReportComment(originalReport, action, 'value2'); - Report.editReportComment(originalReport, action, 'value3'); + + const {result: ancestors} = renderHook(() => useAncestors(originalReport)); + + Report.editReportComment(originalReport, action, ancestors.current, 'value1'); + Report.editReportComment(originalReport, action, ancestors.current, 'value2'); + Report.editReportComment(originalReport, action, ancestors.current, 'value3'); const requests = PersistedRequests?.getAll(); @@ -1647,22 +1648,21 @@ describe('actions/Report', () => { [mentionActionID]: mentionAction, [mentionActionID2]: mentionAction2, }); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { + + let report = { ...createRandomReport(Number(reportID)), lastMentionedTime: mentionAction2.created, - }); + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); + + const {result: ancestors} = renderHook(() => useAncestors({reportID})); - Report.deleteReportComment(reportID, mentionAction); - Report.deleteReportComment(reportID, mentionAction2); + Report.deleteReportComment(reportID, mentionAction, ancestors.current); + Report.deleteReportComment(reportID, mentionAction2, ancestors.current); 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(); }); From 5f71e0a3a77699c8fae974f088e1bafada6597e5 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Tue, 28 Oct 2025 00:43:24 +0000 Subject: [PATCH 02/14] Refactor PopoverReportActionContextMenu to use original report data for ancestor calculations --- src/libs/actions/Report.ts | 1 - .../PopoverReportActionContextMenu.tsx | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 87cc87de8d969..d01319ba2f094 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2027,7 +2027,6 @@ 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 = getOptimisticDataForAncestors(ancestors, optimisticReport?.lastVisibleActionCreated ?? '', CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); optimisticParentReportData.forEach((parentReportData) => { if (isEmptyObject(parentReportData)) { diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 9dc655a4189f9..b150039e7fac0 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-compiler/react-compiler */ import type {ForwardedRef} from 'react'; -import React, {useCallback, useContext, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; /* eslint-disable no-restricted-imports */ import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View} from 'react-native'; @@ -304,8 +304,17 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDRef.current}`, { canBeMissing: true, }); - const ancestors = useAncestors(report); + const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getOriginalReportID(reportIDRef.current, reportActionRef.current)}`, { + canBeMissing: true, + }); + const ancestorsRef = useRef([]); + const ancestors = useAncestors(originalReport); + useEffect(() => { + if (originalReport) { + ancestorsRef.current = ancestors; + } + }, [originalReport, ancestors]); const confirmDeleteAndHideModal = useCallback(() => { callbackWhenDeleteModalHide.current = runAndResetCallback(onConfirmDeleteModal.current); const reportAction = reportActionRef.current; @@ -344,7 +353,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro } else if (reportAction) { // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { - deleteReportComment(reportIDRef.current, reportAction, ancestors, isReportArchived, isOriginalReportArchived); + deleteReportComment(reportIDRef.current, reportAction, ancestorsRef.current, isReportArchived, isOriginalReportArchived); }); } From c30db4bc37d7e3114c42c0f5d79cee7c6f18d171 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Tue, 28 Oct 2025 05:08:17 +0300 Subject: [PATCH 03/14] refactor: PopoverReportActionContextMenu for improved type safety and clarity --- src/libs/actions/Report.ts | 8 +++++++- .../ContextMenu/PopoverReportActionContextMenu.tsx | 9 +++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 65c08f98805d3..ce9a4ce314473 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1919,7 +1919,13 @@ function handlePreexistingReport(report: Report) { } /** Deletes a comment from the report, basically sets it as empty string */ -function deleteReportComment(reportID: string | undefined, reportAction: ReportAction, ancestors: Ancestor[], 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; diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 0f270f2b28b81..8842866c99923 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-compiler/react-compiler */ import type {ForwardedRef} from 'react'; -import React, {useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useImperativeHandle, useRef, useState} from 'react'; /* eslint-disable no-restricted-imports */ import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View} from 'react-native'; @@ -9,8 +9,8 @@ import type {OnyxEntry} from 'react-native-onyx'; import {Actions, ActionSheetAwareScrollViewContext} from '@components/ActionSheetAwareScrollView'; import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; -import useAncestors from '@hooks/useAncestors'; 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'; @@ -320,9 +320,10 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro const ancestorsRef = useRef([]); const ancestors = useAncestors(originalReport); useEffect(() => { - if (originalReport) { - ancestorsRef.current = ancestors; + if (!originalReport) { + return; } + ancestorsRef.current = ancestors; }, [originalReport, ancestors]); const confirmDeleteAndHideModal = useCallback(() => { callbackWhenDeleteModalHide.current = runAndResetCallback(onConfirmDeleteModal.current); From 29f63ea63aafda119f3b29d9e1209684bc3c6761 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Tue, 28 Oct 2025 05:52:04 +0300 Subject: [PATCH 04/14] fixed ancestors params in UnreadIndicatorsTest --- tests/ui/UnreadIndicatorsTest.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 45fbb677a9071..c2d5b860ea4fa 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(); } From 176da24410b2ea712121d00d91903331edda13ac Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Wed, 29 Oct 2025 23:58:00 +0000 Subject: [PATCH 05/14] refactor: getOptimisticDataForAncestors to use reduce for improved clarity and performance --- src/libs/ReportUtils.ts | 19 ++++++------------- src/libs/actions/Report.ts | 8 +------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 02a47974c9aaf..81447206e803d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -10270,24 +10270,17 @@ function getOptimisticDataForParentReportAction(report: Report | undefined, last * @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): Array { - return Array.from(ancestors, ({report: ancestorReport, reportAction: ancestorReportAction}) => { - if (!ancestorReport || isEmptyObject(ancestorReport)) { - return null; - } - - if (!ancestorReportAction?.reportActionID || isEmptyObject(ancestorReportAction)) { - return null; - } - - return { +function getOptimisticDataForAncestors(ancestors: Ancestor[], lastVisibleActionCreated: string, type: string): OnyxUpdate[] { + return ancestors.reduce((optimisticData, {report: ancestorReport, reportAction: ancestorReportAction}) => { + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReport.reportID}`, value: { [ancestorReportAction.reportActionID]: updateOptimisticParentReportAction(ancestorReportAction, lastVisibleActionCreated, type), }, - }; - }); + }); + return optimisticData; + }, []); } function canBeAutoReimbursed(report: OnyxInputOrEntry, policy: OnyxInputOrEntry | SearchPolicy): boolean { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index d22a0b4112758..4b838a432f667 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2012,13 +2012,7 @@ function deleteReportComment( // 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 optimisticParentReportData = getOptimisticDataForAncestors(ancestors, 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 = { From cf34c35431c0059a7e3b8b6fa9685fad6a6817e0 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Thu, 30 Oct 2025 00:16:03 +0000 Subject: [PATCH 06/14] fix: update dependency array in useEffect for correct behavior --- src/pages/home/report/ReportActionItemMessageEdit.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 4a3dc48486b80..9b359544dfa53 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -313,7 +313,7 @@ function ReportActionItemMessageEdit({ } editReportComment(originalReport, action, ancestors, trimmedNewDraft, isOriginalReportArchived, isOriginalParentReportArchived, Object.fromEntries(draftMessageVideoAttributeCache)); deleteDraft(); - }, [reportID, action, deleteDraft, draft, originalReportID, isOriginalReportArchived, originalReport, isOriginalParentReportArchived]); + }, [reportID, ancestors, action, deleteDraft, draft, originalReportID, isOriginalReportArchived, originalReport, isOriginalParentReportArchived]); /** * @param emoji From 4ab78178ad4a4922a9f5afcefc36c3237e7021f0 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Fri, 31 Oct 2025 21:49:16 +0300 Subject: [PATCH 07/14] update ancestors to use original parent report --- src/pages/home/report/ReportActionItemMessageEdit.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 3d9c635349a96..377785ed0d178 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -152,7 +152,8 @@ function ReportActionItemMessageEdit({ const isOriginalReportArchived = useReportIsArchived(originalReportID); const originalParentReportID = getOriginalReportID(originalReportID, action); const isOriginalParentReportArchived = useReportIsArchived(originalParentReportID); - const ancestors = useAncestors(originalReport); + const [originalParentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalParentReportID}`, {canBeMissing: true}); + const ancestors = useAncestors(originalParentReport); useEffect(() => { draftMessageVideoAttributeCache.clear(); From eaa496a00a0221cf172cea5eed817f69dfc61f59 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 31 Oct 2025 21:57:29 +0300 Subject: [PATCH 08/14] Revert "update ancestors to use original parent report" This reverts commit 4ab78178ad4a4922a9f5afcefc36c3237e7021f0. --- src/pages/home/report/ReportActionItemMessageEdit.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 377785ed0d178..3d9c635349a96 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -152,8 +152,7 @@ function ReportActionItemMessageEdit({ const isOriginalReportArchived = useReportIsArchived(originalReportID); const originalParentReportID = getOriginalReportID(originalReportID, action); const isOriginalParentReportArchived = useReportIsArchived(originalParentReportID); - const [originalParentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalParentReportID}`, {canBeMissing: true}); - const ancestors = useAncestors(originalParentReport); + const ancestors = useAncestors(originalReport); useEffect(() => { draftMessageVideoAttributeCache.clear(); From 956b64905647972a322b3bf714ea76ae17ee728a Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Fri, 31 Oct 2025 22:06:04 +0300 Subject: [PATCH 09/14] Adding new line --- .../home/report/ContextMenu/PopoverReportActionContextMenu.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 68497a7c1e10d..9b0558d105c8d 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -58,6 +58,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro const isReportArchived = useReportIsArchived(reportIDRef.current); const isOriginalReportArchived = useReportIsArchived(getOriginalReportID(reportIDRef.current, reportActionRef.current)); const {iouReport, chatReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(reportActionRef.current); + const cursorRelativePosition = useRef({ horizontal: 0, vertical: 0, From bc199bf69ceb39671b70efdd7ed01c6485309a83 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Fri, 31 Oct 2025 22:29:56 +0300 Subject: [PATCH 10/14] refactor: implemented previousActionDeleted in getOptimisticDataForAncestors from changes in recent main merge commit Refactor comments and update optimistic data handling for ancestor report actions. --- src/libs/ReportUtils.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d4d9ad096f0b0..c7c7692ae2b1d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -10269,7 +10269,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 @@ -10309,18 +10309,23 @@ function getOptimisticDataForParentReportAction(report: Report | undefined, last }); } /** - * Get optimistic data the ancestor report actions - * @param report The report that is updated + * Get optimistic ancestor report actions the + * @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[] { - return ancestors.reduce((optimisticData, {report: ancestorReport, reportAction: ancestorReportAction}) => { + let previousActionDeleted = false; + return ancestors.reduce((optimisticData, {report: ancestorReport, reportAction: ancestorReportAction}, index) => { + const updatedReportAction = updateOptimisticParentReportAction(ancestorReportAction, lastVisibleActionCreated, type, previousActionDeleted ? index + 1 : undefined); + + previousActionDeleted = isDeletedAction(ancestorReportAction) && updatedReportAction.childVisibleActionCount === 0; + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReport.reportID}`, value: { - [ancestorReportAction.reportActionID]: updateOptimisticParentReportAction(ancestorReportAction, lastVisibleActionCreated, type), + [ancestorReportAction.reportActionID]: updatedReportAction, }, }); return optimisticData; From 6621f5928288678127ab63e0fc652c9fa456827b Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 31 Oct 2025 22:53:57 +0300 Subject: [PATCH 11/14] prettier --- src/libs/ReportUtils.ts | 4 ++-- .../report/ContextMenu/PopoverReportActionContextMenu.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index c7c7692ae2b1d..81657f54c510b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -10309,7 +10309,7 @@ function getOptimisticDataForParentReportAction(report: Report | undefined, last }); } /** - * Get optimistic ancestor report actions the + * Get optimistic ancestor report actions the * @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 @@ -10320,7 +10320,7 @@ function getOptimisticDataForAncestors(ancestors: Ancestor[], lastVisibleActionC const updatedReportAction = updateOptimisticParentReportAction(ancestorReportAction, lastVisibleActionCreated, type, previousActionDeleted ? index + 1 : undefined); previousActionDeleted = isDeletedAction(ancestorReportAction) && updatedReportAction.childVisibleActionCount === 0; - + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReport.reportID}`, diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 9b0558d105c8d..9f6870c360334 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -58,7 +58,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro const isReportArchived = useReportIsArchived(reportIDRef.current); const isOriginalReportArchived = useReportIsArchived(getOriginalReportID(reportIDRef.current, reportActionRef.current)); const {iouReport, chatReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(reportActionRef.current); - + const cursorRelativePosition = useRef({ horizontal: 0, vertical: 0, From 03bd81d3219cb20060840edf1e951e2c0df9f182 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Mon, 3 Nov 2025 16:53:23 +0000 Subject: [PATCH 12/14] refactor: getOptimisticDataForAncestors to use map instead of reduce for clarity --- src/libs/ReportUtils.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index cea0a80d26277..7d1a9a9af853e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -10431,27 +10431,26 @@ function getOptimisticDataForParentReportAction(report: Report | undefined, last }); } /** - * Get optimistic ancestor report actions the + * 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.reduce((optimisticData, {report: ancestorReport, reportAction: ancestorReportAction}, index) => { + return ancestors.map(({report: ancestorReport, reportAction: ancestorReportAction}, index) => { const updatedReportAction = updateOptimisticParentReportAction(ancestorReportAction, lastVisibleActionCreated, type, previousActionDeleted ? index + 1 : undefined); previousActionDeleted = isDeletedAction(ancestorReportAction) && updatedReportAction.childVisibleActionCount === 0; - optimisticData.push({ + return { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${ancestorReport.reportID}`, value: { [ancestorReportAction.reportActionID]: updatedReportAction, }, - }); - return optimisticData; - }, []); + }; + }); } function canBeAutoReimbursed(report: OnyxInputOrEntry, policy: OnyxInputOrEntry | SearchPolicy): boolean { From 1d1f9c45c7a25460a9324234d8f8dc1ad94c0bd5 Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Mon, 3 Nov 2025 19:08:48 +0000 Subject: [PATCH 13/14] refactor:test report --- tests/actions/ReportTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index bdd0ba3efeb61..b032302c14c9a 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -1655,7 +1655,7 @@ describe('actions/Report', () => { }; await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); - const {result: ancestors} = renderHook(() => useAncestors({reportID})); + const {result: ancestors} = renderHook(() => useAncestors(report)); Report.deleteReportComment(reportID, mentionAction, ancestors.current, undefined, undefined); Report.deleteReportComment(reportID, mentionAction2, ancestors.current, undefined, undefined); From 86bc77e02bf81555307a0c68f41b157b3f3d5cca Mon Sep 17 00:00:00 2001 From: "Antony M. Kithinzi" Date: Mon, 3 Nov 2025 19:14:14 +0000 Subject: [PATCH 14/14] prettier --- tests/actions/ReportTest.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index d2edf41711b91..1f1eea189dd7c 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -1653,6 +1653,8 @@ describe('actions/Report', () => { }; await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); + await waitForBatchedUpdates(); + const {result: ancestors} = renderHook(() => useAncestors(report)); Report.deleteReportComment(reportID, mentionAction, ancestors.current, undefined, undefined);