From b94531f038449883e042e1328330bae778a15470 Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Tue, 3 Jun 2025 14:09:18 +0200 Subject: [PATCH 01/19] fix: removed redundant `/ReadNewestAction` calls --- src/libs/actions/Report.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 2c002e960395..354469a7f170 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -357,6 +357,9 @@ Linking.getInitialURL().then((url) => { reportIDDeeplinkedFromOldDot = processReportIDDeeplink(url ?? ''); }); +// Track pending read requests to prevent duplicates +const pendingReadRequests = new Set(); + let allRecentlyUsedReportFields: OnyxEntry = {}; Onyx.connect({ key: ONYXKEYS.RECENTLY_USED_REPORT_FIELDS, @@ -1586,6 +1589,13 @@ function readNewestAction(reportID: string | undefined, shouldResetUnreadMarker return; } + // Prevent duplicate API calls for the same report + if (pendingReadRequests.has(reportID)) { + return; + } + + pendingReadRequests.add(reportID); + const lastReadTime = DateUtils.getDBTimeWithSkew(); const optimisticData: OnyxUpdate[] = [ @@ -1604,6 +1614,12 @@ function readNewestAction(reportID: string | undefined, shouldResetUnreadMarker }; API.write(WRITE_COMMANDS.READ_NEWEST_ACTION, parameters, {optimisticData}); + + // Clear the pending request after a short delay to allow for completion + setTimeout(() => { + pendingReadRequests.delete(reportID); + }, 1000); + if (shouldResetUnreadMarker) { DeviceEventEmitter.emit(`readNewestAction_${reportID}`, lastReadTime); } From afc0d9900ace6f6b203e723799b7eb33a7d8e4e0 Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Tue, 3 Jun 2025 14:12:09 +0200 Subject: [PATCH 02/19] fix: removed redundant `/ReadNewestAction` calls --- src/libs/actions/Report.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 354469a7f170..83eeeb0273fe 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1615,10 +1615,9 @@ function readNewestAction(reportID: string | undefined, shouldResetUnreadMarker API.write(WRITE_COMMANDS.READ_NEWEST_ACTION, parameters, {optimisticData}); - // Clear the pending request after a short delay to allow for completion - setTimeout(() => { + Promise.resolve().then(() => { pendingReadRequests.delete(reportID); - }, 1000); + }); if (shouldResetUnreadMarker) { DeviceEventEmitter.emit(`readNewestAction_${reportID}`, lastReadTime); From d02b26a9adc0ae8d95bbdc2e5f3d051f150b9009 Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Tue, 3 Jun 2025 14:43:54 +0200 Subject: [PATCH 03/19] fix: removed redundant `/ReadNewestAction` calls --- src/hooks/useReadNewestActionDebounced.ts | 62 +++++++++++++++++++ src/libs/actions/Report.ts | 14 ----- src/pages/home/ReportScreen.tsx | 8 ++- src/pages/home/report/ReportActionsList.tsx | 17 ++--- .../useReportUnreadMessageScrollTracking.ts | 5 +- 5 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 src/hooks/useReadNewestActionDebounced.ts diff --git a/src/hooks/useReadNewestActionDebounced.ts b/src/hooks/useReadNewestActionDebounced.ts new file mode 100644 index 000000000000..b93e49083696 --- /dev/null +++ b/src/hooks/useReadNewestActionDebounced.ts @@ -0,0 +1,62 @@ +import {useCallback, useRef} from 'react'; +import {readNewestAction} from '@userActions/Report'; + +const DEBOUNCE_MS = 300; +const THROTTLE_MS = 1000; + +/** + * Hook to prevent duplicate readNewestAction calls at the component level + * Uses both debouncing and throttling to ensure optimal API usage + */ +export default function useReadNewestActionDebounced() { + const debounceTimerRef = useRef(); + const lastCallTimeRef = useRef>({}); + const pendingCallsRef = useRef>({}); + + const debouncedReadNewestAction = useCallback( + (reportID: string | undefined, shouldResetUnreadMarker = false) => { + if (!reportID) { + return; + } + + const now = Date.now(); + const lastCallTime = lastCallTimeRef.current[reportID] || 0; + const timeSinceLastCall = now - lastCallTime; + + // Clear existing debounce timer for this report + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + + // If we're within throttle window and already have a pending call, skip + if (timeSinceLastCall < THROTTLE_MS && pendingCallsRef.current[reportID]) { + return; + } + + // If we're within throttle window but no pending call, execute immediately + if (timeSinceLastCall < THROTTLE_MS) { + lastCallTimeRef.current[reportID] = now; + pendingCallsRef.current[reportID] = true; + + readNewestAction(reportID, shouldResetUnreadMarker); + + // Clear pending flag after a short delay + setTimeout(() => { + pendingCallsRef.current[reportID] = false; + }, 100); + return; + } + + // Otherwise, debounce the call + pendingCallsRef.current[reportID] = true; + debounceTimerRef.current = setTimeout(() => { + lastCallTimeRef.current[reportID] = Date.now(); + readNewestAction(reportID, shouldResetUnreadMarker); + pendingCallsRef.current[reportID] = false; + }, DEBOUNCE_MS); + }, + [], + ); + + return debouncedReadNewestAction; +} diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 83eeeb0273fe..5e0db0a82bba 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -357,9 +357,6 @@ Linking.getInitialURL().then((url) => { reportIDDeeplinkedFromOldDot = processReportIDDeeplink(url ?? ''); }); -// Track pending read requests to prevent duplicates -const pendingReadRequests = new Set(); - let allRecentlyUsedReportFields: OnyxEntry = {}; Onyx.connect({ key: ONYXKEYS.RECENTLY_USED_REPORT_FIELDS, @@ -1589,13 +1586,6 @@ function readNewestAction(reportID: string | undefined, shouldResetUnreadMarker return; } - // Prevent duplicate API calls for the same report - if (pendingReadRequests.has(reportID)) { - return; - } - - pendingReadRequests.add(reportID); - const lastReadTime = DateUtils.getDBTimeWithSkew(); const optimisticData: OnyxUpdate[] = [ @@ -1615,10 +1605,6 @@ function readNewestAction(reportID: string | undefined, shouldResetUnreadMarker API.write(WRITE_COMMANDS.READ_NEWEST_ACTION, parameters, {optimisticData}); - Promise.resolve().then(() => { - pendingReadRequests.delete(reportID); - }); - if (shouldResetUnreadMarker) { DeviceEventEmitter.emit(`readNewestAction_${reportID}`, lastReadTime); } diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 1029581065ac..7ea948d392ba 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -74,11 +74,11 @@ import { clearDeleteTransactionNavigateBackUrl, navigateToConciergeChat, openReport, - readNewestAction, subscribeToReportLeavingEvents, unsubscribeFromLeavingRoomReportChannel, updateLastVisitTime, } from '@userActions/Report'; +import useReadNewestActionDebounced from '@hooks/useReadNewestActionDebounced'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -145,6 +145,8 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const {shouldUseNarrowLayout, isInNarrowPaneModal} = useResponsiveLayout(); const currentReportIDValue = useCurrentReportID(); + const debouncedReadNewestAction = useReadNewestActionDebounced(); + const [modal] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: false}); const [isComposerFullSize] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportIDFromRoute}`, {initialValue: false, canBeMissing: true}); const [accountManagerReportID] = useOnyx(ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID, {canBeMissing: true}); @@ -747,8 +749,8 @@ function ReportScreen({route, navigation}: ReportScreenProps) { return; } // After creating the task report then navigating to task detail we don't have any report actions and the last read time is empty so We need to update the initial last read time when opening the task report detail. - readNewestAction(report?.reportID); - }, [report]); + debouncedReadNewestAction(report?.reportID); + }, [report, debouncedReadNewestAction]); const lastRoute = usePrevious(route); diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 14bb612fa872..f153f92ffb24 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -52,13 +52,14 @@ import { import Visibility from '@libs/Visibility'; import type {ReportsSplitNavigatorParamList} from '@navigation/types'; import variables from '@styles/variables'; -import {getCurrentUserAccountID, openReport, readNewestAction, subscribeToNewActionEvent} from '@userActions/Report'; +import {getCurrentUserAccountID, openReport, subscribeToNewActionEvent} from '@userActions/Report'; import {PersonalDetailsContext} from '@src/components/OnyxProvider'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; +import useReadNewestActionDebounced from '@hooks/useReadNewestActionDebounced'; import FloatingMessageCounter from './FloatingMessageCounter'; import getInitialNumToRender from './getInitialNumReportActionsToRender'; import ListBoundaryLoader from './ListBoundaryLoader'; @@ -165,6 +166,8 @@ function ReportActionsList({ const [isVisible, setIsVisible] = useState(Visibility.isVisible); const isFocused = useIsFocused(); + const debouncedReadNewestAction = useReadNewestActionDebounced(); + const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: true}); const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.accountID, canBeMissing: true}); const participantsContext = useContext(PersonalDetailsContext); @@ -369,7 +372,7 @@ function ReportActionsList({ // To handle this, we use the 'referrer' parameter to check if the current navigation is triggered from a notification. const isFromNotification = route?.params?.referrer === CONST.REFERRER.NOTIFICATION; if ((isVisible || isFromNotification) && scrollingVerticalOffset.current < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD) { - readNewestAction(report.reportID); + debouncedReadNewestAction(report.reportID); if (isFromNotification) { Navigation.setParams({referrer: undefined}); } @@ -378,7 +381,7 @@ function ReportActionsList({ } } // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [report.lastVisibleActionCreated, transactionThreadReport?.lastVisibleActionCreated, report.reportID, isVisible]); + }, [report.lastVisibleActionCreated, transactionThreadReport?.lastVisibleActionCreated, report.reportID, isVisible, debouncedReadNewestAction]); useEffect(() => { if (linkedReportActionID) { @@ -486,8 +489,8 @@ function ReportActionsList({ } reportScrollManager.scrollToBottom(); readActionSkipped.current = false; - readNewestAction(report.reportID); - }, [setIsFloatingMessageCounterVisible, hasNewestReportAction, reportScrollManager, report.reportID]); + debouncedReadNewestAction(report.reportID); + }, [setIsFloatingMessageCounterVisible, hasNewestReportAction, reportScrollManager, report.reportID, debouncedReadNewestAction]); /** * Calculates the ideal number of report actions to render in the first render, based on the screen height and on @@ -562,7 +565,7 @@ function ReportActionsList({ return; } - readNewestAction(report.reportID); + debouncedReadNewestAction(report.reportID); userActiveSince.current = DateUtils.getDBTime(); // This effect logic to `mark as read` will only run when the report focused has new messages and the App visibility @@ -570,7 +573,7 @@ function ReportActionsList({ // We will mark the report as read in the above case which marks the LHN report item as read while showing the new message // marker for the chat messages received while the user wasn't focused on the report or on another browser tab for web. // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [isFocused, isVisible]); + }, [isFocused, isVisible, debouncedReadNewestAction]); const renderItem = useCallback( ({item: reportAction, index}: ListRenderItemInfo) => { diff --git a/src/pages/home/report/useReportUnreadMessageScrollTracking.ts b/src/pages/home/report/useReportUnreadMessageScrollTracking.ts index 76cd1ea43f7f..545eac6f7444 100644 --- a/src/pages/home/report/useReportUnreadMessageScrollTracking.ts +++ b/src/pages/home/report/useReportUnreadMessageScrollTracking.ts @@ -1,8 +1,8 @@ import {useState} from 'react'; import type {MutableRefObject} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; -import {readNewestAction} from '@userActions/Report'; import CONST from '@src/CONST'; +import useReadNewestActionDebounced from '@hooks/useReadNewestActionDebounced'; type Args = { /** The report ID */ @@ -33,6 +33,7 @@ export default function useReportUnreadMessageScrollTracking({ onTrackScrolling, }: Args) { const [isFloatingMessageCounterVisible, setIsFloatingMessageCounterVisible] = useState(floatingMessageVisibleInitialValue); + const debouncedReadNewestAction = useReadNewestActionDebounced(); /** * On every scroll event we want to: @@ -52,7 +53,7 @@ export default function useReportUnreadMessageScrollTracking({ if (readActionSkippedRef.current) { // eslint-disable-next-line react-compiler/react-compiler,no-param-reassign readActionSkippedRef.current = false; - readNewestAction(reportID); + debouncedReadNewestAction(reportID); } setIsFloatingMessageCounterVisible(false); From 0e97101c044ce05546b8038e6c127074533f3f13 Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Tue, 3 Jun 2025 16:14:51 +0200 Subject: [PATCH 04/19] fix: removed redundant `/ReadNewestAction` calls --- src/hooks/useReadNewestActionDebounced.ts | 81 +++++++++---------- src/pages/home/ReportScreen.tsx | 2 +- src/pages/home/report/ReportActionsList.tsx | 2 +- .../useReportUnreadMessageScrollTracking.ts | 2 +- 4 files changed, 42 insertions(+), 45 deletions(-) diff --git a/src/hooks/useReadNewestActionDebounced.ts b/src/hooks/useReadNewestActionDebounced.ts index b93e49083696..d098515c82ed 100644 --- a/src/hooks/useReadNewestActionDebounced.ts +++ b/src/hooks/useReadNewestActionDebounced.ts @@ -13,50 +13,47 @@ export default function useReadNewestActionDebounced() { const lastCallTimeRef = useRef>({}); const pendingCallsRef = useRef>({}); - const debouncedReadNewestAction = useCallback( - (reportID: string | undefined, shouldResetUnreadMarker = false) => { - if (!reportID) { - return; - } - - const now = Date.now(); - const lastCallTime = lastCallTimeRef.current[reportID] || 0; - const timeSinceLastCall = now - lastCallTime; - - // Clear existing debounce timer for this report - if (debounceTimerRef.current) { - clearTimeout(debounceTimerRef.current); - } - - // If we're within throttle window and already have a pending call, skip - if (timeSinceLastCall < THROTTLE_MS && pendingCallsRef.current[reportID]) { - return; - } - - // If we're within throttle window but no pending call, execute immediately - if (timeSinceLastCall < THROTTLE_MS) { - lastCallTimeRef.current[reportID] = now; - pendingCallsRef.current[reportID] = true; - - readNewestAction(reportID, shouldResetUnreadMarker); - - // Clear pending flag after a short delay - setTimeout(() => { - pendingCallsRef.current[reportID] = false; - }, 100); - return; - } - - // Otherwise, debounce the call + const debouncedReadNewestAction = useCallback((reportID: string | undefined, shouldResetUnreadMarker = false) => { + if (!reportID) { + return; + } + + const now = Date.now(); + const lastCallTime = lastCallTimeRef.current[reportID] || 0; + const timeSinceLastCall = now - lastCallTime; + + // Clear existing debounce timer for this report + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + + // If we're within throttle window and already have a pending call, skip + if (timeSinceLastCall < THROTTLE_MS && pendingCallsRef.current[reportID]) { + return; + } + + // If we're within throttle window but no pending call, execute immediately + if (timeSinceLastCall < THROTTLE_MS) { + lastCallTimeRef.current[reportID] = now; pendingCallsRef.current[reportID] = true; - debounceTimerRef.current = setTimeout(() => { - lastCallTimeRef.current[reportID] = Date.now(); - readNewestAction(reportID, shouldResetUnreadMarker); + + readNewestAction(reportID, shouldResetUnreadMarker); + + // Clear pending flag after a short delay + setTimeout(() => { pendingCallsRef.current[reportID] = false; - }, DEBOUNCE_MS); - }, - [], - ); + }, 100); + return; + } + + // Otherwise, debounce the call + pendingCallsRef.current[reportID] = true; + debounceTimerRef.current = setTimeout(() => { + lastCallTimeRef.current[reportID] = Date.now(); + readNewestAction(reportID, shouldResetUnreadMarker); + pendingCallsRef.current[reportID] = false; + }, DEBOUNCE_MS); + }, []); return debouncedReadNewestAction; } diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 7ea948d392ba..f8be2d2259ff 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -25,6 +25,7 @@ import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; +import useReadNewestActionDebounced from '@hooks/useReadNewestActionDebounced'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; @@ -78,7 +79,6 @@ import { unsubscribeFromLeavingRoomReportChannel, updateLastVisitTime, } from '@userActions/Report'; -import useReadNewestActionDebounced from '@hooks/useReadNewestActionDebounced'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index f153f92ffb24..449846a61a06 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -14,6 +14,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useLocalize from '@hooks/useLocalize'; import useNetworkWithOfflineStatus from '@hooks/useNetworkWithOfflineStatus'; import usePrevious from '@hooks/usePrevious'; +import useReadNewestActionDebounced from '@hooks/useReadNewestActionDebounced'; import useReportScrollManager from '@hooks/useReportScrollManager'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -59,7 +60,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; -import useReadNewestActionDebounced from '@hooks/useReadNewestActionDebounced'; import FloatingMessageCounter from './FloatingMessageCounter'; import getInitialNumToRender from './getInitialNumReportActionsToRender'; import ListBoundaryLoader from './ListBoundaryLoader'; diff --git a/src/pages/home/report/useReportUnreadMessageScrollTracking.ts b/src/pages/home/report/useReportUnreadMessageScrollTracking.ts index 545eac6f7444..81649b31f6e3 100644 --- a/src/pages/home/report/useReportUnreadMessageScrollTracking.ts +++ b/src/pages/home/report/useReportUnreadMessageScrollTracking.ts @@ -1,8 +1,8 @@ import {useState} from 'react'; import type {MutableRefObject} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; -import CONST from '@src/CONST'; import useReadNewestActionDebounced from '@hooks/useReadNewestActionDebounced'; +import CONST from '@src/CONST'; type Args = { /** The report ID */ From 8056112189ca6660ecd7deccde99ddf16e10f7a7 Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Tue, 3 Jun 2025 18:02:32 +0200 Subject: [PATCH 05/19] fix: removed redundant `/ReadNewestAction` calls --- src/CONST.ts | 1 + src/hooks/useReadNewestActionDebounced.ts | 59 ------------------- src/pages/home/ReportScreen.tsx | 10 +++- src/pages/home/report/ReportActionsList.tsx | 11 +++- .../useReportUnreadMessageScrollTracking.ts | 14 +++-- 5 files changed, 27 insertions(+), 68 deletions(-) delete mode 100644 src/hooks/useReadNewestActionDebounced.ts diff --git a/src/CONST.ts b/src/CONST.ts index 00bcd438856a..175ba3ae6434 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1665,6 +1665,7 @@ const CONST = { TRIE_INITIALIZATION: 'trie_initialization', COMMENT_LENGTH_DEBOUNCE_TIME: 1500, SEARCH_OPTION_LIST_DEBOUNCE_TIME: 300, + READ_NEWEST_ACTION_DEBOUNCE_TIME: 300, RESIZE_DEBOUNCE_TIME: 100, UNREAD_UPDATE_DEBOUNCE_TIME: 300, SEARCH_CONVERT_SEARCH_VALUES: 'search_convert_search_values', diff --git a/src/hooks/useReadNewestActionDebounced.ts b/src/hooks/useReadNewestActionDebounced.ts deleted file mode 100644 index d098515c82ed..000000000000 --- a/src/hooks/useReadNewestActionDebounced.ts +++ /dev/null @@ -1,59 +0,0 @@ -import {useCallback, useRef} from 'react'; -import {readNewestAction} from '@userActions/Report'; - -const DEBOUNCE_MS = 300; -const THROTTLE_MS = 1000; - -/** - * Hook to prevent duplicate readNewestAction calls at the component level - * Uses both debouncing and throttling to ensure optimal API usage - */ -export default function useReadNewestActionDebounced() { - const debounceTimerRef = useRef(); - const lastCallTimeRef = useRef>({}); - const pendingCallsRef = useRef>({}); - - const debouncedReadNewestAction = useCallback((reportID: string | undefined, shouldResetUnreadMarker = false) => { - if (!reportID) { - return; - } - - const now = Date.now(); - const lastCallTime = lastCallTimeRef.current[reportID] || 0; - const timeSinceLastCall = now - lastCallTime; - - // Clear existing debounce timer for this report - if (debounceTimerRef.current) { - clearTimeout(debounceTimerRef.current); - } - - // If we're within throttle window and already have a pending call, skip - if (timeSinceLastCall < THROTTLE_MS && pendingCallsRef.current[reportID]) { - return; - } - - // If we're within throttle window but no pending call, execute immediately - if (timeSinceLastCall < THROTTLE_MS) { - lastCallTimeRef.current[reportID] = now; - pendingCallsRef.current[reportID] = true; - - readNewestAction(reportID, shouldResetUnreadMarker); - - // Clear pending flag after a short delay - setTimeout(() => { - pendingCallsRef.current[reportID] = false; - }, 100); - return; - } - - // Otherwise, debounce the call - pendingCallsRef.current[reportID] = true; - debounceTimerRef.current = setTimeout(() => { - lastCallTimeRef.current[reportID] = Date.now(); - readNewestAction(reportID, shouldResetUnreadMarker); - pendingCallsRef.current[reportID] = false; - }, DEBOUNCE_MS); - }, []); - - return debouncedReadNewestAction; -} diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index f8be2d2259ff..7e703265d252 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -17,6 +17,7 @@ import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import ScreenWrapper from '@components/ScreenWrapper'; import useAppFocusEvent from '@hooks/useAppFocusEvent'; import useCurrentReportID from '@hooks/useCurrentReportID'; +import useDebounce from '@hooks/useDebounce'; import useDeepCompareRef from '@hooks/useDeepCompareRef'; import useIsReportReadyToDisplay from '@hooks/useIsReportReadyToDisplay'; import useLocalize from '@hooks/useLocalize'; @@ -25,7 +26,6 @@ import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; -import useReadNewestActionDebounced from '@hooks/useReadNewestActionDebounced'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; @@ -75,6 +75,7 @@ import { clearDeleteTransactionNavigateBackUrl, navigateToConciergeChat, openReport, + readNewestAction, subscribeToReportLeavingEvents, unsubscribeFromLeavingRoomReportChannel, updateLastVisitTime, @@ -145,7 +146,12 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const {shouldUseNarrowLayout, isInNarrowPaneModal} = useResponsiveLayout(); const currentReportIDValue = useCurrentReportID(); - const debouncedReadNewestAction = useReadNewestActionDebounced(); + const debouncedReadNewestAction = useDebounce( + useCallback((reportID: string | undefined) => { + readNewestAction(reportID); + }, []), + CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME, + ); const [modal] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: false}); const [isComposerFullSize] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportIDFromRoute}`, {initialValue: false, canBeMissing: true}); diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 449846a61a06..0195854c324a 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -11,10 +11,10 @@ import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@components/InvertedFlatList/BaseInve import {usePersonalDetails} from '@components/OnyxProvider'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDebounce from '@hooks/useDebounce'; import useLocalize from '@hooks/useLocalize'; import useNetworkWithOfflineStatus from '@hooks/useNetworkWithOfflineStatus'; import usePrevious from '@hooks/usePrevious'; -import useReadNewestActionDebounced from '@hooks/useReadNewestActionDebounced'; import useReportScrollManager from '@hooks/useReportScrollManager'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -53,7 +53,7 @@ import { import Visibility from '@libs/Visibility'; import type {ReportsSplitNavigatorParamList} from '@navigation/types'; import variables from '@styles/variables'; -import {getCurrentUserAccountID, openReport, subscribeToNewActionEvent} from '@userActions/Report'; +import {getCurrentUserAccountID, openReport, readNewestAction, subscribeToNewActionEvent} from '@userActions/Report'; import {PersonalDetailsContext} from '@src/components/OnyxProvider'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -166,7 +166,12 @@ function ReportActionsList({ const [isVisible, setIsVisible] = useState(Visibility.isVisible); const isFocused = useIsFocused(); - const debouncedReadNewestAction = useReadNewestActionDebounced(); + const debouncedReadNewestAction = useDebounce( + useCallback((reportID: string) => { + readNewestAction(reportID); + }, []), + CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME, + ); const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: true}); const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.accountID, canBeMissing: true}); diff --git a/src/pages/home/report/useReportUnreadMessageScrollTracking.ts b/src/pages/home/report/useReportUnreadMessageScrollTracking.ts index 81649b31f6e3..473bccdf7690 100644 --- a/src/pages/home/report/useReportUnreadMessageScrollTracking.ts +++ b/src/pages/home/report/useReportUnreadMessageScrollTracking.ts @@ -1,7 +1,8 @@ -import {useState} from 'react'; +import {useCallback, useState} from 'react'; import type {MutableRefObject} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; -import useReadNewestActionDebounced from '@hooks/useReadNewestActionDebounced'; +import useDebounce from '@hooks/useDebounce'; +import {readNewestAction} from '@userActions/Report'; import CONST from '@src/CONST'; type Args = { @@ -33,7 +34,12 @@ export default function useReportUnreadMessageScrollTracking({ onTrackScrolling, }: Args) { const [isFloatingMessageCounterVisible, setIsFloatingMessageCounterVisible] = useState(floatingMessageVisibleInitialValue); - const debouncedReadNewestAction = useReadNewestActionDebounced(); + const debouncedReadNewestAction = useDebounce( + useCallback(() => { + readNewestAction(reportID); + }, [reportID]), + CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME, + ); /** * On every scroll event we want to: @@ -53,7 +59,7 @@ export default function useReportUnreadMessageScrollTracking({ if (readActionSkippedRef.current) { // eslint-disable-next-line react-compiler/react-compiler,no-param-reassign readActionSkippedRef.current = false; - debouncedReadNewestAction(reportID); + debouncedReadNewestAction(); } setIsFloatingMessageCounterVisible(false); From 09ff60025a3bd919b1cab566d2c33e81b43ca3f7 Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Tue, 3 Jun 2025 18:06:50 +0200 Subject: [PATCH 06/19] tests --- .../unit/useReportUnreadMessageScrollTrackingTest.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/unit/useReportUnreadMessageScrollTrackingTest.ts b/tests/unit/useReportUnreadMessageScrollTrackingTest.ts index 62f7ebda0b4c..a594a032a0d2 100644 --- a/tests/unit/useReportUnreadMessageScrollTrackingTest.ts +++ b/tests/unit/useReportUnreadMessageScrollTrackingTest.ts @@ -1,5 +1,7 @@ import {act, renderHook} from '@testing-library/react-native'; +import {useCallback} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; +import useDebounce from '@hooks/useDebounce'; import useReportUnreadMessageScrollTracking from '@pages/home/report/useReportUnreadMessageScrollTracking'; import {readNewestAction} from '@userActions/Report'; import CONST from '@src/CONST'; @@ -152,8 +154,15 @@ describe('useReportUnreadMessageScrollTracking', () => { result.current.trackVerticalScrolling(emptyScrollEventMock); }); + const debouncedReadNewestAction = useDebounce( + useCallback(() => { + readNewestAction(reportID); + }, [reportID]), + CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME, + ); + // Then - expect(readNewestAction).toBeCalledTimes(1); + expect(debouncedReadNewestAction).toBeCalledTimes(1); expect(onTrackScrollingMockFn).toBeCalledWith(emptyScrollEventMock); expect(readActionRefFalse.current).toBe(false); expect(result.current.isFloatingMessageCounterVisible).toBe(false); From 0ea5b229916cb4b7bd738ba364642cd2499ee81d Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Tue, 3 Jun 2025 18:10:44 +0200 Subject: [PATCH 07/19] tests --- tests/unit/useReportUnreadMessageScrollTrackingTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/useReportUnreadMessageScrollTrackingTest.ts b/tests/unit/useReportUnreadMessageScrollTrackingTest.ts index a594a032a0d2..a222452a6787 100644 --- a/tests/unit/useReportUnreadMessageScrollTrackingTest.ts +++ b/tests/unit/useReportUnreadMessageScrollTrackingTest.ts @@ -157,7 +157,7 @@ describe('useReportUnreadMessageScrollTracking', () => { const debouncedReadNewestAction = useDebounce( useCallback(() => { readNewestAction(reportID); - }, [reportID]), + }, []), CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME, ); From 569ee9989146427be1eb3a734930b12c06f66b37 Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Tue, 3 Jun 2025 18:20:11 +0200 Subject: [PATCH 08/19] tests --- ...seReportUnreadMessageScrollTrackingTest.ts | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/unit/useReportUnreadMessageScrollTrackingTest.ts b/tests/unit/useReportUnreadMessageScrollTrackingTest.ts index a222452a6787..dcf2be00c7c1 100644 --- a/tests/unit/useReportUnreadMessageScrollTrackingTest.ts +++ b/tests/unit/useReportUnreadMessageScrollTrackingTest.ts @@ -123,22 +123,24 @@ describe('useReportUnreadMessageScrollTracking', () => { }); it('calls readAction when scrolling inside the threshold and the message and read action skipped is true', () => { + jest.useFakeTimers(); + // Given const offsetRef = {current: 0}; + const readActionSkippedRef = {current: true}; const {result, rerender} = renderHook(() => useReportUnreadMessageScrollTracking({ reportID, currentVerticalScrollingOffsetRef: offsetRef, - readActionSkippedRef: {current: true}, + readActionSkippedRef, floatingMessageVisibleInitialValue: false, hasUnreadMarkerReportAction: true, onTrackScrolling: onTrackScrollingMockFn, }), ); - // When + // When - scroll outside threshold first act(() => { - // offset greater, will set visible to true offsetRef.current = CONST.REPORT.ACTIONS.SCROLL_VERTICAL_OFFSET_THRESHOLD + 100; result.current.trackVerticalScrolling(emptyScrollEventMock); }); @@ -148,24 +150,25 @@ describe('useReportUnreadMessageScrollTracking', () => { rerender({}); + // When - scroll back inside threshold act(() => { - // scrolling into the offset, should call readNewestAction offsetRef.current = CONST.REPORT.ACTIONS.SCROLL_VERTICAL_OFFSET_THRESHOLD - 100; result.current.trackVerticalScrolling(emptyScrollEventMock); }); - const debouncedReadNewestAction = useDebounce( - useCallback(() => { - readNewestAction(reportID); - }, []), - CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME, - ); + // Advance timers to trigger debounced function + act(() => { + jest.advanceTimersByTime(CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME); + }); // Then - expect(debouncedReadNewestAction).toBeCalledTimes(1); + expect(readNewestAction).toBeCalledTimes(1); + expect(readNewestAction).toBeCalledWith(reportID); expect(onTrackScrollingMockFn).toBeCalledWith(emptyScrollEventMock); - expect(readActionRefFalse.current).toBe(false); + expect(readActionSkippedRef.current).toBe(false); expect(result.current.isFloatingMessageCounterVisible).toBe(false); + + jest.useRealTimers(); }); }); }); From ddb5064e5a4a665e48964fdfee248584fc0174aa Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Tue, 3 Jun 2025 18:23:57 +0200 Subject: [PATCH 09/19] tests --- tests/unit/useReportUnreadMessageScrollTrackingTest.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/unit/useReportUnreadMessageScrollTrackingTest.ts b/tests/unit/useReportUnreadMessageScrollTrackingTest.ts index dcf2be00c7c1..7108efb719c2 100644 --- a/tests/unit/useReportUnreadMessageScrollTrackingTest.ts +++ b/tests/unit/useReportUnreadMessageScrollTrackingTest.ts @@ -1,7 +1,5 @@ import {act, renderHook} from '@testing-library/react-native'; -import {useCallback} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; -import useDebounce from '@hooks/useDebounce'; import useReportUnreadMessageScrollTracking from '@pages/home/report/useReportUnreadMessageScrollTracking'; import {readNewestAction} from '@userActions/Report'; import CONST from '@src/CONST'; @@ -124,7 +122,7 @@ describe('useReportUnreadMessageScrollTracking', () => { it('calls readAction when scrolling inside the threshold and the message and read action skipped is true', () => { jest.useFakeTimers(); - + // Given const offsetRef = {current: 0}; const readActionSkippedRef = {current: true}; @@ -167,7 +165,7 @@ describe('useReportUnreadMessageScrollTracking', () => { expect(onTrackScrollingMockFn).toBeCalledWith(emptyScrollEventMock); expect(readActionSkippedRef.current).toBe(false); expect(result.current.isFloatingMessageCounterVisible).toBe(false); - + jest.useRealTimers(); }); }); From 29c09166887e9c8d7b7fa90747bcd1e9bc9d3fb6 Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Tue, 3 Jun 2025 19:22:49 +0200 Subject: [PATCH 10/19] tests --- tests/ui/UnreadIndicatorsTest.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 8ab34648abfa..12c943032c9c 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -368,6 +368,16 @@ describe('Unread Indicators', () => { .then(waitForBatchedUpdates) .then(async () => { await act(() => (NativeNavigation as NativeNavigationMock).triggerTransitionEnd()); + + // Wait for debounced read action to complete and update display names + await waitFor(() => { + const hintText = translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameTexts = screen.queryAllByLabelText(hintText, {includeHiddenElements: true}); + const firstReportFontWeight = (displayNameTexts.at(0)?.props?.style as TextStyle)?.fontWeight; + // Wait until the first report shows normal font weight (read state) + expect(firstReportFontWeight).toBe(FontUtils.fontWeight.normal); + }, { timeout: CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME + 1000 }); + // Verify that report we navigated to appears in a "read" state while the original unread report still shows as unread const hintText = translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNameTexts = screen.queryAllByLabelText(hintText, {includeHiddenElements: true}); From fb65ef9c05961221a68f39207d98b32f62fc9641 Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Tue, 3 Jun 2025 19:29:25 +0200 Subject: [PATCH 11/19] prettier --- tests/ui/UnreadIndicatorsTest.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 12c943032c9c..7b15e435e300 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -368,15 +368,18 @@ describe('Unread Indicators', () => { .then(waitForBatchedUpdates) .then(async () => { await act(() => (NativeNavigation as NativeNavigationMock).triggerTransitionEnd()); - + // Wait for debounced read action to complete and update display names - await waitFor(() => { - const hintText = translateLocal('accessibilityHints.chatUserDisplayNames'); - const displayNameTexts = screen.queryAllByLabelText(hintText, {includeHiddenElements: true}); - const firstReportFontWeight = (displayNameTexts.at(0)?.props?.style as TextStyle)?.fontWeight; - // Wait until the first report shows normal font weight (read state) - expect(firstReportFontWeight).toBe(FontUtils.fontWeight.normal); - }, { timeout: CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME + 1000 }); + await waitFor( + () => { + const hintText = translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameTexts = screen.queryAllByLabelText(hintText, {includeHiddenElements: true}); + const firstReportFontWeight = (displayNameTexts.at(0)?.props?.style as TextStyle)?.fontWeight; + // Wait until the first report shows normal font weight (read state) + expect(firstReportFontWeight).toBe(FontUtils.fontWeight.normal); + }, + {timeout: Number(CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME + 1000)}, + ); // Verify that report we navigated to appears in a "read" state while the original unread report still shows as unread const hintText = translateLocal('accessibilityHints.chatUserDisplayNames'); From 92b26bc5a73f9c476328a5d39b2dde8cb47735e5 Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Wed, 4 Jun 2025 13:38:04 +0200 Subject: [PATCH 12/19] remove redundant useCallback --- src/pages/home/ReportScreen.tsx | 7 +------ src/pages/home/report/ReportActionsList.tsx | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 00bc59112f07..b75034499de3 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -152,12 +152,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const {shouldUseNarrowLayout, isInNarrowPaneModal} = useResponsiveLayout(); const currentReportIDValue = useCurrentReportID(); - const debouncedReadNewestAction = useDebounce( - useCallback((reportID: string | undefined) => { - readNewestAction(reportID); - }, []), - CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME, - ); + const debouncedReadNewestAction = useDebounce(readNewestAction, CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME); const [modal] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: false}); const [isComposerFullSize] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportIDFromRoute}`, {initialValue: false, canBeMissing: true}); diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 0195854c324a..e49a4fb1786e 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -166,12 +166,7 @@ function ReportActionsList({ const [isVisible, setIsVisible] = useState(Visibility.isVisible); const isFocused = useIsFocused(); - const debouncedReadNewestAction = useDebounce( - useCallback((reportID: string) => { - readNewestAction(reportID); - }, []), - CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME, - ); + const debouncedReadNewestAction = useDebounce(readNewestAction, CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME); const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: true}); const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.accountID, canBeMissing: true}); From 8f0f52b41b7a9f5c86208126d31a9fdcd7353889 Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Wed, 4 Jun 2025 13:40:51 +0200 Subject: [PATCH 13/19] remove redundant deps --- src/pages/home/ReportScreen.tsx | 2 +- src/pages/home/report/ReportActionsList.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index b75034499de3..13ff78b6f55e 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -773,7 +773,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { } // After creating the task report then navigating to task detail we don't have any report actions and the last read time is empty so We need to update the initial last read time when opening the task report detail. debouncedReadNewestAction(report?.reportID); - }, [report, debouncedReadNewestAction]); + }, [report]); const lastRoute = usePrevious(route); diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index e49a4fb1786e..7ef843cc9bf7 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -381,7 +381,7 @@ function ReportActionsList({ } } // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [report.lastVisibleActionCreated, transactionThreadReport?.lastVisibleActionCreated, report.reportID, isVisible, debouncedReadNewestAction]); + }, [report.lastVisibleActionCreated, transactionThreadReport?.lastVisibleActionCreated, report.reportID, isVisible]); useEffect(() => { if (linkedReportActionID) { From b194ea3449c85d9fea9a524451d17c11b6d28c75 Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Wed, 4 Jun 2025 13:43:12 +0200 Subject: [PATCH 14/19] eslint --- src/pages/home/ReportScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 13ff78b6f55e..0f7c4de336e0 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -773,7 +773,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { } // After creating the task report then navigating to task detail we don't have any report actions and the last read time is empty so We need to update the initial last read time when opening the task report detail. debouncedReadNewestAction(report?.reportID); - }, [report]); + }, [debouncedReadNewestAction, report]); const lastRoute = usePrevious(route); From be01294fa35f56abf13616a41d6a6de33696defa Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Thu, 5 Jun 2025 20:31:02 +0200 Subject: [PATCH 15/19] replace useCallback usage with onyx conflicts resolver mechanism --- src/libs/actions/Report.ts | 5 ++- src/libs/actions/RequestConflictUtils.ts | 40 ++++++++++++++++++- src/pages/home/ReportScreen.tsx | 8 ++-- src/pages/home/report/ReportActionsList.tsx | 14 +++---- .../useReportUnreadMessageScrollTracking.ts | 11 ++--- 5 files changed, 56 insertions(+), 22 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 0c559e05dff6..012f10a18dfd 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -199,6 +199,7 @@ import { resolveDuplicationConflictAction, resolveEditCommentWithNewAddCommentRequest, resolveOpenReportDuplicationConflictAction, + resolveReadNewestActionConflicts, } from './RequestConflictUtils'; import {canAnonymousUserAccessRoute, hasAuthToken, isAnonymousUser, signOutAndRedirectToSignIn, waitForUserSignIn} from './Session'; import {isOnboardingFlowCompleted, onServerDataReady, setOnboardingErrorMessage} from './Welcome'; @@ -1597,7 +1598,9 @@ function readNewestAction(reportID: string | undefined, shouldResetUnreadMarker lastReadTime, }; - API.write(WRITE_COMMANDS.READ_NEWEST_ACTION, parameters, {optimisticData}); + API.write(WRITE_COMMANDS.READ_NEWEST_ACTION, parameters, {optimisticData}, { + checkAndFixConflictingRequest: (persistedRequests) => resolveReadNewestActionConflicts(persistedRequests, parameters), + }); if (shouldResetUnreadMarker) { DeviceEventEmitter.emit(`readNewestAction_${reportID}`, lastReadTime); diff --git a/src/libs/actions/RequestConflictUtils.ts b/src/libs/actions/RequestConflictUtils.ts index 1cbeb2eb82de..4f2a1a1fac5c 100644 --- a/src/libs/actions/RequestConflictUtils.ts +++ b/src/libs/actions/RequestConflictUtils.ts @@ -1,7 +1,7 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {TupleToUnion} from 'type-fest'; -import type {OpenReportParams, UpdateCommentParams} from '@libs/API/parameters'; +import type {OpenReportParams, ReadNewestActionParams, UpdateCommentParams} from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; import type {ApiRequestCommandParameters} from '@libs/API/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -193,6 +193,43 @@ function resolveEditCommentWithNewAddCommentRequest(persistedRequests: OnyxReque } as ConflictActionData; } +function resolveReadNewestActionConflicts(persistedRequests: OnyxRequest[], parameters: ReadNewestActionParams): ConflictActionData { + const reportID = parameters.reportID; + const newLastReadTime = parameters.lastReadTime; + + const existingRequestIndex = persistedRequests.findIndex( + (request) => request.command === WRITE_COMMANDS.READ_NEWEST_ACTION && request.data?.reportID === reportID + ); + + if (existingRequestIndex === -1) { + return { + conflictAction: { + type: 'push', + }, + }; + } + + const existingRequest = persistedRequests.at(existingRequestIndex); + const existingLastReadTime = existingRequest?.data?.lastReadTime; + + // Keep the request with the latest lastReadTime + if (!existingLastReadTime || newLastReadTime > existingLastReadTime) { + return { + conflictAction: { + type: 'replace', + index: existingRequestIndex, + }, + }; + } + + // Existing request has newer or equal lastReadTime, ignore the new request + return { + conflictAction: { + type: 'noAction', + }, + }; +} + function resolveEnableFeatureConflicts( command: EnablePolicyFeatureCommand, persistedRequests: OnyxRequest[], @@ -224,6 +261,7 @@ export { resolveOpenReportDuplicationConflictAction, resolveCommentDeletionConflicts, resolveEditCommentWithNewAddCommentRequest, + resolveReadNewestActionConflicts, createUpdateCommentMatcher, resolveEnableFeatureConflicts, enablePolicyFeatureCommand, diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 0f7c4de336e0..225fb6d720af 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -17,7 +17,7 @@ import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import ScreenWrapper from '@components/ScreenWrapper'; import useAppFocusEvent from '@hooks/useAppFocusEvent'; import useCurrentReportID from '@hooks/useCurrentReportID'; -import useDebounce from '@hooks/useDebounce'; + import useDeepCompareRef from '@hooks/useDeepCompareRef'; import useIsReportReadyToDisplay from '@hooks/useIsReportReadyToDisplay'; import useLocalize from '@hooks/useLocalize'; @@ -152,7 +152,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const {shouldUseNarrowLayout, isInNarrowPaneModal} = useResponsiveLayout(); const currentReportIDValue = useCurrentReportID(); - const debouncedReadNewestAction = useDebounce(readNewestAction, CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME); + const [modal] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: false}); const [isComposerFullSize] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportIDFromRoute}`, {initialValue: false, canBeMissing: true}); @@ -772,8 +772,8 @@ function ReportScreen({route, navigation}: ReportScreenProps) { return; } // After creating the task report then navigating to task detail we don't have any report actions and the last read time is empty so We need to update the initial last read time when opening the task report detail. - debouncedReadNewestAction(report?.reportID); - }, [debouncedReadNewestAction, report]); + readNewestAction(report?.reportID); + }, [report]); const lastRoute = usePrevious(route); diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 7ef843cc9bf7..50b620f2e5a9 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -11,7 +11,7 @@ import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@components/InvertedFlatList/BaseInve import {usePersonalDetails} from '@components/OnyxProvider'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useDebounce from '@hooks/useDebounce'; + import useLocalize from '@hooks/useLocalize'; import useNetworkWithOfflineStatus from '@hooks/useNetworkWithOfflineStatus'; import usePrevious from '@hooks/usePrevious'; @@ -166,8 +166,6 @@ function ReportActionsList({ const [isVisible, setIsVisible] = useState(Visibility.isVisible); const isFocused = useIsFocused(); - const debouncedReadNewestAction = useDebounce(readNewestAction, CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME); - const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: true}); const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.accountID, canBeMissing: true}); const participantsContext = useContext(PersonalDetailsContext); @@ -372,7 +370,7 @@ function ReportActionsList({ // To handle this, we use the 'referrer' parameter to check if the current navigation is triggered from a notification. const isFromNotification = route?.params?.referrer === CONST.REFERRER.NOTIFICATION; if ((isVisible || isFromNotification) && scrollingVerticalOffset.current < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD) { - debouncedReadNewestAction(report.reportID); + readNewestAction(report.reportID); if (isFromNotification) { Navigation.setParams({referrer: undefined}); } @@ -489,8 +487,8 @@ function ReportActionsList({ } reportScrollManager.scrollToBottom(); readActionSkipped.current = false; - debouncedReadNewestAction(report.reportID); - }, [setIsFloatingMessageCounterVisible, hasNewestReportAction, reportScrollManager, report.reportID, debouncedReadNewestAction]); + readNewestAction(report.reportID); + }, [setIsFloatingMessageCounterVisible, hasNewestReportAction, reportScrollManager, report.reportID]); /** * Calculates the ideal number of report actions to render in the first render, based on the screen height and on @@ -565,7 +563,7 @@ function ReportActionsList({ return; } - debouncedReadNewestAction(report.reportID); + readNewestAction(report.reportID); userActiveSince.current = DateUtils.getDBTime(); // This effect logic to `mark as read` will only run when the report focused has new messages and the App visibility @@ -573,7 +571,7 @@ function ReportActionsList({ // We will mark the report as read in the above case which marks the LHN report item as read while showing the new message // marker for the chat messages received while the user wasn't focused on the report or on another browser tab for web. // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [isFocused, isVisible, debouncedReadNewestAction]); + }, [isFocused, isVisible]); const renderItem = useCallback( ({item: reportAction, index}: ListRenderItemInfo) => { diff --git a/src/pages/home/report/useReportUnreadMessageScrollTracking.ts b/src/pages/home/report/useReportUnreadMessageScrollTracking.ts index 473bccdf7690..4882356b7745 100644 --- a/src/pages/home/report/useReportUnreadMessageScrollTracking.ts +++ b/src/pages/home/report/useReportUnreadMessageScrollTracking.ts @@ -1,7 +1,7 @@ import {useCallback, useState} from 'react'; import type {MutableRefObject} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; -import useDebounce from '@hooks/useDebounce'; + import {readNewestAction} from '@userActions/Report'; import CONST from '@src/CONST'; @@ -34,12 +34,7 @@ export default function useReportUnreadMessageScrollTracking({ onTrackScrolling, }: Args) { const [isFloatingMessageCounterVisible, setIsFloatingMessageCounterVisible] = useState(floatingMessageVisibleInitialValue); - const debouncedReadNewestAction = useDebounce( - useCallback(() => { - readNewestAction(reportID); - }, [reportID]), - CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME, - ); + /** * On every scroll event we want to: @@ -59,7 +54,7 @@ export default function useReportUnreadMessageScrollTracking({ if (readActionSkippedRef.current) { // eslint-disable-next-line react-compiler/react-compiler,no-param-reassign readActionSkippedRef.current = false; - debouncedReadNewestAction(); + readNewestAction(reportID); } setIsFloatingMessageCounterVisible(false); From 98258ee6b52325cb509d686a038b203a50750b26 Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Thu, 5 Jun 2025 20:35:11 +0200 Subject: [PATCH 16/19] prettier --- src/libs/actions/Report.ts | 11 ++++++++--- src/libs/actions/RequestConflictUtils.ts | 4 +--- src/pages/home/ReportScreen.tsx | 3 --- src/pages/home/report/ReportActionsList.tsx | 1 - .../useReportUnreadMessageScrollTracking.ts | 4 +--- tests/ui/UnreadIndicatorsTest.tsx | 13 ------------- ...seReportUnreadMessageScrollTrackingTest.ts | 19 +++++-------------- 7 files changed, 15 insertions(+), 40 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 012f10a18dfd..c23f427c01c8 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1598,9 +1598,14 @@ function readNewestAction(reportID: string | undefined, shouldResetUnreadMarker lastReadTime, }; - API.write(WRITE_COMMANDS.READ_NEWEST_ACTION, parameters, {optimisticData}, { - checkAndFixConflictingRequest: (persistedRequests) => resolveReadNewestActionConflicts(persistedRequests, parameters), - }); + API.write( + WRITE_COMMANDS.READ_NEWEST_ACTION, + parameters, + {optimisticData}, + { + checkAndFixConflictingRequest: (persistedRequests) => resolveReadNewestActionConflicts(persistedRequests, parameters), + }, + ); if (shouldResetUnreadMarker) { DeviceEventEmitter.emit(`readNewestAction_${reportID}`, lastReadTime); diff --git a/src/libs/actions/RequestConflictUtils.ts b/src/libs/actions/RequestConflictUtils.ts index 4f2a1a1fac5c..e727fd3cfddb 100644 --- a/src/libs/actions/RequestConflictUtils.ts +++ b/src/libs/actions/RequestConflictUtils.ts @@ -197,9 +197,7 @@ function resolveReadNewestActionConflicts(persistedRequests: OnyxRequest[], para const reportID = parameters.reportID; const newLastReadTime = parameters.lastReadTime; - const existingRequestIndex = persistedRequests.findIndex( - (request) => request.command === WRITE_COMMANDS.READ_NEWEST_ACTION && request.data?.reportID === reportID - ); + const existingRequestIndex = persistedRequests.findIndex((request) => request.command === WRITE_COMMANDS.READ_NEWEST_ACTION && request.data?.reportID === reportID); if (existingRequestIndex === -1) { return { diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 225fb6d720af..2abc3760b3e6 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -17,7 +17,6 @@ import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import ScreenWrapper from '@components/ScreenWrapper'; import useAppFocusEvent from '@hooks/useAppFocusEvent'; import useCurrentReportID from '@hooks/useCurrentReportID'; - import useDeepCompareRef from '@hooks/useDeepCompareRef'; import useIsReportReadyToDisplay from '@hooks/useIsReportReadyToDisplay'; import useLocalize from '@hooks/useLocalize'; @@ -152,8 +151,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const {shouldUseNarrowLayout, isInNarrowPaneModal} = useResponsiveLayout(); const currentReportIDValue = useCurrentReportID(); - - const [modal] = useOnyx(ONYXKEYS.MODAL, {canBeMissing: false}); const [isComposerFullSize] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportIDFromRoute}`, {initialValue: false, canBeMissing: true}); const [accountManagerReportID] = useOnyx(ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID, {canBeMissing: true}); diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 50b620f2e5a9..14bb612fa872 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -11,7 +11,6 @@ import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@components/InvertedFlatList/BaseInve import {usePersonalDetails} from '@components/OnyxProvider'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; - import useLocalize from '@hooks/useLocalize'; import useNetworkWithOfflineStatus from '@hooks/useNetworkWithOfflineStatus'; import usePrevious from '@hooks/usePrevious'; diff --git a/src/pages/home/report/useReportUnreadMessageScrollTracking.ts b/src/pages/home/report/useReportUnreadMessageScrollTracking.ts index 4882356b7745..76cd1ea43f7f 100644 --- a/src/pages/home/report/useReportUnreadMessageScrollTracking.ts +++ b/src/pages/home/report/useReportUnreadMessageScrollTracking.ts @@ -1,7 +1,6 @@ -import {useCallback, useState} from 'react'; +import {useState} from 'react'; import type {MutableRefObject} from 'react'; import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; - import {readNewestAction} from '@userActions/Report'; import CONST from '@src/CONST'; @@ -35,7 +34,6 @@ export default function useReportUnreadMessageScrollTracking({ }: Args) { const [isFloatingMessageCounterVisible, setIsFloatingMessageCounterVisible] = useState(floatingMessageVisibleInitialValue); - /** * On every scroll event we want to: * Show/hide the new floating message counter when user is scrolling back/forth in the history of messages. diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 7b15e435e300..8ab34648abfa 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -368,19 +368,6 @@ describe('Unread Indicators', () => { .then(waitForBatchedUpdates) .then(async () => { await act(() => (NativeNavigation as NativeNavigationMock).triggerTransitionEnd()); - - // Wait for debounced read action to complete and update display names - await waitFor( - () => { - const hintText = translateLocal('accessibilityHints.chatUserDisplayNames'); - const displayNameTexts = screen.queryAllByLabelText(hintText, {includeHiddenElements: true}); - const firstReportFontWeight = (displayNameTexts.at(0)?.props?.style as TextStyle)?.fontWeight; - // Wait until the first report shows normal font weight (read state) - expect(firstReportFontWeight).toBe(FontUtils.fontWeight.normal); - }, - {timeout: Number(CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME + 1000)}, - ); - // Verify that report we navigated to appears in a "read" state while the original unread report still shows as unread const hintText = translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNameTexts = screen.queryAllByLabelText(hintText, {includeHiddenElements: true}); diff --git a/tests/unit/useReportUnreadMessageScrollTrackingTest.ts b/tests/unit/useReportUnreadMessageScrollTrackingTest.ts index 7108efb719c2..98dfd0de70e4 100644 --- a/tests/unit/useReportUnreadMessageScrollTrackingTest.ts +++ b/tests/unit/useReportUnreadMessageScrollTrackingTest.ts @@ -121,24 +121,22 @@ describe('useReportUnreadMessageScrollTracking', () => { }); it('calls readAction when scrolling inside the threshold and the message and read action skipped is true', () => { - jest.useFakeTimers(); - // Given const offsetRef = {current: 0}; - const readActionSkippedRef = {current: true}; const {result, rerender} = renderHook(() => useReportUnreadMessageScrollTracking({ reportID, currentVerticalScrollingOffsetRef: offsetRef, - readActionSkippedRef, + readActionSkippedRef: {current: true}, floatingMessageVisibleInitialValue: false, hasUnreadMarkerReportAction: true, onTrackScrolling: onTrackScrollingMockFn, }), ); - // When - scroll outside threshold first + // When act(() => { + // offset greater, will set visible to true offsetRef.current = CONST.REPORT.ACTIONS.SCROLL_VERTICAL_OFFSET_THRESHOLD + 100; result.current.trackVerticalScrolling(emptyScrollEventMock); }); @@ -148,25 +146,18 @@ describe('useReportUnreadMessageScrollTracking', () => { rerender({}); - // When - scroll back inside threshold act(() => { + // scrolling into the offset, should call readNewestAction offsetRef.current = CONST.REPORT.ACTIONS.SCROLL_VERTICAL_OFFSET_THRESHOLD - 100; result.current.trackVerticalScrolling(emptyScrollEventMock); }); - // Advance timers to trigger debounced function - act(() => { - jest.advanceTimersByTime(CONST.TIMING.READ_NEWEST_ACTION_DEBOUNCE_TIME); - }); - // Then expect(readNewestAction).toBeCalledTimes(1); expect(readNewestAction).toBeCalledWith(reportID); expect(onTrackScrollingMockFn).toBeCalledWith(emptyScrollEventMock); - expect(readActionSkippedRef.current).toBe(false); + expect(readActionRefFalse.current).toBe(false); expect(result.current.isFloatingMessageCounterVisible).toBe(false); - - jest.useRealTimers(); }); }); }); From 21314d117f32a748be157da7dfd1059a7c761a3e Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Thu, 5 Jun 2025 20:36:13 +0200 Subject: [PATCH 17/19] tests --- src/CONST.ts | 1 - tests/unit/useReportUnreadMessageScrollTrackingTest.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index c5529e648a18..90e118cf4afa 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1658,7 +1658,6 @@ const CONST = { TRIE_INITIALIZATION: 'trie_initialization', COMMENT_LENGTH_DEBOUNCE_TIME: 1500, SEARCH_OPTION_LIST_DEBOUNCE_TIME: 300, - READ_NEWEST_ACTION_DEBOUNCE_TIME: 300, RESIZE_DEBOUNCE_TIME: 100, UNREAD_UPDATE_DEBOUNCE_TIME: 300, SEARCH_CONVERT_SEARCH_VALUES: 'search_convert_search_values', diff --git a/tests/unit/useReportUnreadMessageScrollTrackingTest.ts b/tests/unit/useReportUnreadMessageScrollTrackingTest.ts index 98dfd0de70e4..62f7ebda0b4c 100644 --- a/tests/unit/useReportUnreadMessageScrollTrackingTest.ts +++ b/tests/unit/useReportUnreadMessageScrollTrackingTest.ts @@ -154,7 +154,6 @@ describe('useReportUnreadMessageScrollTracking', () => { // Then expect(readNewestAction).toBeCalledTimes(1); - expect(readNewestAction).toBeCalledWith(reportID); expect(onTrackScrollingMockFn).toBeCalledWith(emptyScrollEventMock); expect(readActionRefFalse.current).toBe(false); expect(result.current.isFloatingMessageCounterVisible).toBe(false); From 4071a2065853b9fc6cc518cbf77382aae25a03f8 Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Thu, 12 Jun 2025 08:23:31 +0200 Subject: [PATCH 18/19] remove redundant last read comparison --- src/libs/actions/RequestConflictUtils.ts | 34 ++---------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/src/libs/actions/RequestConflictUtils.ts b/src/libs/actions/RequestConflictUtils.ts index e727fd3cfddb..93442da1eee9 100644 --- a/src/libs/actions/RequestConflictUtils.ts +++ b/src/libs/actions/RequestConflictUtils.ts @@ -194,38 +194,8 @@ function resolveEditCommentWithNewAddCommentRequest(persistedRequests: OnyxReque } function resolveReadNewestActionConflicts(persistedRequests: OnyxRequest[], parameters: ReadNewestActionParams): ConflictActionData { - const reportID = parameters.reportID; - const newLastReadTime = parameters.lastReadTime; - - const existingRequestIndex = persistedRequests.findIndex((request) => request.command === WRITE_COMMANDS.READ_NEWEST_ACTION && request.data?.reportID === reportID); - - if (existingRequestIndex === -1) { - return { - conflictAction: { - type: 'push', - }, - }; - } - - const existingRequest = persistedRequests.at(existingRequestIndex); - const existingLastReadTime = existingRequest?.data?.lastReadTime; - - // Keep the request with the latest lastReadTime - if (!existingLastReadTime || newLastReadTime > existingLastReadTime) { - return { - conflictAction: { - type: 'replace', - index: existingRequestIndex, - }, - }; - } - - // Existing request has newer or equal lastReadTime, ignore the new request - return { - conflictAction: { - type: 'noAction', - }, - }; + const requestMatcher = (request: OnyxRequest) => request.command === WRITE_COMMANDS.READ_NEWEST_ACTION && request.data?.reportID === parameters.reportID; + return resolveDuplicationConflictAction(persistedRequests, requestMatcher); } function resolveEnableFeatureConflicts( From 711a8ea9288c5effde4a05be8d63d2e1a096c6af Mon Sep 17 00:00:00 2001 From: "marta.sudol" Date: Thu, 12 Jun 2025 12:50:55 +0200 Subject: [PATCH 19/19] prettier & eslint --- src/libs/actions/Report.ts | 4 ++-- src/libs/actions/RequestConflictUtils.ts | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index ecfd39a3734c..e7b2687163f2 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -199,7 +199,6 @@ import { resolveDuplicationConflictAction, resolveEditCommentWithNewAddCommentRequest, resolveOpenReportDuplicationConflictAction, - resolveReadNewestActionConflicts, } from './RequestConflictUtils'; import {canAnonymousUserAccessRoute, hasAuthToken, isAnonymousUser, signOutAndRedirectToSignIn, waitForUserSignIn} from './Session'; import {isOnboardingFlowCompleted, onServerDataReady, setOnboardingErrorMessage} from './Welcome'; @@ -1603,7 +1602,8 @@ function readNewestAction(reportID: string | undefined, shouldResetUnreadMarker parameters, {optimisticData}, { - checkAndFixConflictingRequest: (persistedRequests) => resolveReadNewestActionConflicts(persistedRequests, parameters), + checkAndFixConflictingRequest: (persistedRequests) => + resolveDuplicationConflictAction(persistedRequests, (request) => request.command === WRITE_COMMANDS.READ_NEWEST_ACTION && request.data?.reportID === parameters.reportID), }, ); diff --git a/src/libs/actions/RequestConflictUtils.ts b/src/libs/actions/RequestConflictUtils.ts index 93442da1eee9..1cbeb2eb82de 100644 --- a/src/libs/actions/RequestConflictUtils.ts +++ b/src/libs/actions/RequestConflictUtils.ts @@ -1,7 +1,7 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {TupleToUnion} from 'type-fest'; -import type {OpenReportParams, ReadNewestActionParams, UpdateCommentParams} from '@libs/API/parameters'; +import type {OpenReportParams, UpdateCommentParams} from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; import type {ApiRequestCommandParameters} from '@libs/API/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -193,11 +193,6 @@ function resolveEditCommentWithNewAddCommentRequest(persistedRequests: OnyxReque } as ConflictActionData; } -function resolveReadNewestActionConflicts(persistedRequests: OnyxRequest[], parameters: ReadNewestActionParams): ConflictActionData { - const requestMatcher = (request: OnyxRequest) => request.command === WRITE_COMMANDS.READ_NEWEST_ACTION && request.data?.reportID === parameters.reportID; - return resolveDuplicationConflictAction(persistedRequests, requestMatcher); -} - function resolveEnableFeatureConflicts( command: EnablePolicyFeatureCommand, persistedRequests: OnyxRequest[], @@ -229,7 +224,6 @@ export { resolveOpenReportDuplicationConflictAction, resolveCommentDeletionConflicts, resolveEditCommentWithNewAddCommentRequest, - resolveReadNewestActionConflicts, createUpdateCommentMatcher, resolveEnableFeatureConflicts, enablePolicyFeatureCommand,