From 078c2a177d7209533c3a0c74fdc1b8ba22b0b903 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 5 May 2026 12:11:56 +0300 Subject: [PATCH] fix: Reports - Highlight animation is visible on added expense after duplication --- .../MoneyRequestReportView.tsx | 12 +++++++++--- src/hooks/useLatchedTransactionIDs.ts | 19 +++++++++++++++++++ .../Search/SearchMoneyRequestReportPage.tsx | 9 ++++++++- src/pages/inbox/ReportActionsList.tsx | 9 ++++++++- src/pages/inbox/WideRHPReceiptPanel.tsx | 12 ++++++++++-- src/pages/inbox/report/ReportActionsView.tsx | 14 ++++++++++++-- 6 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 src/hooks/useLatchedTransactionIDs.ts diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx index 6f40fae7d5b0..8636f6efd2e5 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx @@ -12,6 +12,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import MoneyRequestReceiptView from '@components/ReportActionItem/MoneyRequestReceiptView'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import ReportHeaderSkeletonView from '@components/ReportHeaderSkeletonView'; +import useLatchedTransactionIDs from '@hooks/useLatchedTransactionIDs'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; @@ -139,7 +140,12 @@ function MoneyRequestReportView({report, reportLoadingState, shouldDisplayReport return transactions.filter((transaction) => transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); }, [transactions, isOffline]); const reportTransactionIDs = visibleTransactions.map((transaction) => transaction.transactionID); - const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions ?? [], isOffline, reportTransactionIDs); + + const latchedTransactionIDs = useLatchedTransactionIDs(reportTransactionIDs, reportID); + const reportTransactionIDsLatched = latchedTransactionIDs ? reportTransactionIDs.filter((id) => latchedTransactionIDs.has(id)) : reportTransactionIDs; + const visibleTransactionsLatched = latchedTransactionIDs ? visibleTransactions.filter((t) => latchedTransactionIDs.has(t.transactionID)) : visibleTransactions; + + const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions ?? [], isOffline, reportTransactionIDsLatched); const isLoadingInitialReportActions = reportLoadingState?.isLoadingInitialReportActions; const dismissReportCreationError = useCallback(() => { @@ -159,10 +165,10 @@ function MoneyRequestReportView({report, reportLoadingState, shouldDisplayReport const shouldShowOpenReportLoadingSkeleton = !!(isLoadingInitialReportActions && reportActions.length === 0 && !isOffline) || shouldWaitForTransactions; const isEmptyTransactionReport = visibleTransactions?.length === 0 && transactionThreadReportID === undefined; - const shouldDisplayMoneyRequestActionsList = !!isEmptyTransactionReport || shouldDisplayReportTableView(report, visibleTransactions ?? []); + const shouldDisplayMoneyRequestActionsList = !!isEmptyTransactionReport || shouldDisplayReportTableView(report, visibleTransactionsLatched); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`); - const shouldShowWideRHPReceipt = visibleTransactions.length === 1 && !isSmallScreenWidth && !!transactionThreadReport; + const shouldShowWideRHPReceipt = visibleTransactionsLatched.length === 1 && !isSmallScreenWidth && !!transactionThreadReport; const reportHeaderView = useMemo( () => diff --git a/src/hooks/useLatchedTransactionIDs.ts b/src/hooks/useLatchedTransactionIDs.ts new file mode 100644 index 000000000000..0c601102a65f --- /dev/null +++ b/src/hooks/useLatchedTransactionIDs.ts @@ -0,0 +1,19 @@ +import {useState} from 'react'; + +/** + * Snapshots the first non-empty transaction-ID set so layout decisions driven by + * `transactions.length` stay stable across optimistic additions (e.g. Duplicate). Pass `resetKey` + * (typically the report ID) to re-snapshot when the component is reused for a different report + * via setParams without remount. + */ +function useLatchedTransactionIDs(transactionIDs: ReadonlyArray | undefined, resetKey?: string): Set | undefined { + const [latched, setLatched] = useState<{key: string | undefined; ids: Set} | undefined>(undefined); + const isEmpty = !transactionIDs || transactionIDs.length === 0; + const keyChanged = latched !== undefined && latched.key !== resetKey; + if (!isEmpty && (latched === undefined || keyChanged)) { + setLatched({key: resetKey, ids: new Set(transactionIDs)}); + } + return latched?.ids; +} + +export default useLatchedTransactionIDs; diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index b67fb6248ca0..2ff286e1bee7 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -16,6 +16,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useDismissOnMoneyRequestReportRemoval from '@hooks/useDismissOnMoneyRequestReportRemoval'; import useDocumentTitle from '@hooks/useDocumentTitle'; import useIsReportReadyToDisplay from '@hooks/useIsReportReadyToDisplay'; +import useLatchedTransactionIDs from '@hooks/useLatchedTransactionIDs'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; @@ -178,8 +179,14 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { return {snapshotTransaction: transaction, snapshotViolations: violations}; }, [snapshot?.data, allReportTransactions]); + const latchedIDsForLayout = useLatchedTransactionIDs( + visibleTransactions.map((t) => t.transactionID), + reportIDFromRoute, + ); + const layoutLatchedTransactions = latchedIDsForLayout ? visibleTransactions.filter((t) => latchedIDsForLayout.has(t.transactionID)) : visibleTransactions; + // If there is more than one transaction, display the report in Super Wide RHP, otherwise it will be shown in Wide RHP - const shouldShowSuperWideRHP = visibleTransactions.length > 1; + const shouldShowSuperWideRHP = layoutLatchedTransactions.length > 1; useShowSuperWideRHPVersion(shouldShowSuperWideRHP); diff --git a/src/pages/inbox/ReportActionsList.tsx b/src/pages/inbox/ReportActionsList.tsx index 38b9741ec5fd..75cd4eb2f1bb 100644 --- a/src/pages/inbox/ReportActionsList.tsx +++ b/src/pages/inbox/ReportActionsList.tsx @@ -2,6 +2,7 @@ import {useRoute} from '@react-navigation/native'; import React from 'react'; import MoneyRequestReportActionsList from '@components/MoneyRequestReportView/MoneyRequestReportActionsList'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; +import useLatchedTransactionIDs from '@hooks/useLatchedTransactionIDs'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; @@ -40,9 +41,15 @@ function ReportActionsList() { const allReportTransactions = useReportTransactionsCollection(reportIDFromRoute); const reportTransactions = getAllNonDeletedTransactions(allReportTransactions, reportActions, isOffline, true); + const latchedIDs = useLatchedTransactionIDs( + reportTransactions.map((t) => t.transactionID), + reportIDFromRoute, + ); + const transactionsForViewDecision = latchedIDs ? reportTransactions.filter((t) => latchedIDs.has(t.transactionID)) : reportTransactions; + const isMoneyRequestOrInvoiceReport = isMoneyRequestReport(report) || isInvoiceReport(report); const shouldWaitForTransactions = shouldWaitForTransactionsUtil(report, reportTransactions, reportLoadingState, isOffline); - const shouldDisplayMoneyRequestActionsList = isMoneyRequestOrInvoiceReport && shouldDisplayReportTableView(report, reportTransactions); + const shouldDisplayMoneyRequestActionsList = isMoneyRequestOrInvoiceReport && shouldDisplayReportTableView(report, transactionsForViewDecision); if (!report || shouldWaitForTransactions) { return ; diff --git a/src/pages/inbox/WideRHPReceiptPanel.tsx b/src/pages/inbox/WideRHPReceiptPanel.tsx index 5e0a87524b0f..45475871a9a7 100644 --- a/src/pages/inbox/WideRHPReceiptPanel.tsx +++ b/src/pages/inbox/WideRHPReceiptPanel.tsx @@ -5,6 +5,7 @@ import {Animated} from 'react-native'; import MoneyRequestReceiptView from '@components/ReportActionItem/MoneyRequestReceiptView'; import ScrollView from '@components/ScrollView'; import useShowWideRHPVersion from '@components/WideRHPContextProvider/useShowWideRHPVersion'; +import useLatchedTransactionIDs from '@hooks/useLatchedTransactionIDs'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; @@ -53,12 +54,19 @@ function WideRHPReceiptPanelGate() { const allReportTransactions = useReportTransactionsCollection(reportIDFromRoute); const reportTransactions = getAllNonDeletedTransactions(allReportTransactions, reportActions, isOffline, true); const visibleTransactions = reportTransactions?.filter((transaction) => isOffline || transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); - const reportTransactionIDs = visibleTransactions?.map((transaction) => transaction.transactionID); + + const latchedIDs = useLatchedTransactionIDs( + visibleTransactions.map((t) => t.transactionID), + reportIDFromRoute, + ); + const transactionsForViewDecision = latchedIDs ? visibleTransactions.filter((t) => latchedIDs.has(t.transactionID)) : visibleTransactions; + + const reportTransactionIDs = transactionsForViewDecision.map((transaction) => transaction.transactionID); const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions ?? [], isOffline, reportTransactionIDs); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`); const isMoneyRequestOrInvoiceReport = isMoneyRequestReport(report) || isInvoiceReport(report); - const shouldDisplayMoneyRequestActionsList = isMoneyRequestOrInvoiceReport && shouldDisplayReportTableView(report, visibleTransactions ?? []); + const shouldDisplayMoneyRequestActionsList = isMoneyRequestOrInvoiceReport && shouldDisplayReportTableView(report, transactionsForViewDecision); const shouldShowWideRHP = !shouldDisplayMoneyRequestActionsList && diff --git a/src/pages/inbox/report/ReportActionsView.tsx b/src/pages/inbox/report/ReportActionsView.tsx index a4a02894fc57..ae086e2c94b1 100755 --- a/src/pages/inbox/report/ReportActionsView.tsx +++ b/src/pages/inbox/report/ReportActionsView.tsx @@ -6,6 +6,7 @@ import useConciergeSidePanelReportActions from '@hooks/useConciergeSidePanelRepo import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useIsInSidePanel from '@hooks/useIsInSidePanel'; +import useLatchedTransactionIDs from '@hooks/useLatchedTransactionIDs'; import useLoadReportActions from '@hooks/useLoadReportActions'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -112,9 +113,18 @@ function ReportActionsView({reportID, onLayout}: ReportActionsViewProps) { [reportTransactionsForThreadID, isOffline], ); const reportTransactionIDsForThread = useMemo(() => visibleTransactionsForThreadID?.map((t) => t.transactionID), [visibleTransactionsForThreadID]); + + const latchedThreadIDs = useLatchedTransactionIDs(reportTransactionIDsForThread, reportID); + const reportTransactionIDsForThreadLatched = useMemo(() => { + if (!latchedThreadIDs) { + return reportTransactionIDsForThread; + } + return reportTransactionIDsForThread?.filter((id) => latchedThreadIDs.has(id)); + }, [latchedThreadIDs, reportTransactionIDsForThread]); + const transactionThreadReportID = useMemo( - () => getOneTransactionThreadReportID(report, chatReport, allReportActions ?? [], isOffline, reportTransactionIDsForThread), - [report, chatReport, allReportActions, isOffline, reportTransactionIDsForThread], + () => getOneTransactionThreadReportID(report, chatReport, allReportActions ?? [], isOffline, reportTransactionIDsForThreadLatched), + [report, chatReport, allReportActions, isOffline, reportTransactionIDsForThreadLatched], ); const isReportArchived = useReportIsArchived(reportID);