From 3019ec9ee0dbf8cf27d144f98db02c2deb793713 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 15 Jun 2026 17:52:42 +0800 Subject: [PATCH 1/5] pass whether report actions loaded or not from param to remove allReportActions usage --- .../MoneyRequestReportActionsList.tsx | 15 +++-- src/hooks/useIsReportActionsLoaded.tsx | 18 +++++ src/hooks/useMarkAsRead.ts | 18 +++-- src/libs/actions/Report/index.ts | 10 +-- src/pages/inbox/ReportFetchHandler.tsx | 6 +- src/pages/inbox/report/ReportActionsList.tsx | 15 +---- .../useReportUnreadMessageScrollTracking.ts | 12 +--- tests/actions/ReportTest.ts | 67 +------------------ tests/unit/useIsReportActionsLoadedTest.ts | 61 +++++++++++++++++ ...seReportUnreadMessageScrollTrackingTest.ts | 2 - 10 files changed, 106 insertions(+), 118 deletions(-) create mode 100644 src/hooks/useIsReportActionsLoaded.tsx create mode 100644 tests/unit/useIsReportActionsLoadedTest.ts diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index db0495c0b480..bb5f9a4fa535 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -8,6 +8,7 @@ import {DeviceEventEmitter, View} from 'react-native'; import FlatListWithScrollKey from '@components/FlatList/FlatListWithScrollKey'; import ScrollView from '@components/ScrollView'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useIsReportActionsLoaded from '@hooks/useIsReportActionsLoaded'; import useLoadReportActions from '@hooks/useLoadReportActions'; import useLocalize from '@hooks/useLocalize'; import useNetworkWithOfflineStatus from '@hooks/useNetworkWithOfflineStatus'; @@ -105,6 +106,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(report?.policyID)}`); const [reportLoadingState] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportIDFromRoute}`); const [reportPaginationState] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_PAGINATION_STATE}${reportIDFromRoute}`); + const isReportActionsLoaded = useIsReportActionsLoaded(reportIDFromRoute); const reportID = report?.reportID; const {reportActions: unfilteredReportActions, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID, route?.params?.reportActionID); @@ -351,7 +353,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) // 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) && scrollingVerticalBottomOffset.current < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD) { - readNewestAction(report?.reportID, !!reportLoadingState?.hasOnceLoadedReportActions); + readNewestAction(report?.reportID, isReportActionsLoaded); if (isFromNotification) { Navigation.setParams({referrer: undefined}); } @@ -360,7 +362,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [report?.lastVisibleActionCreated, transactionThreadReport?.lastVisibleActionCreated, report?.reportID, isVisible, reportLoadingState?.hasOnceLoadedReportActions]); + }, [report?.lastVisibleActionCreated, transactionThreadReport?.lastVisibleActionCreated, report?.reportID, isVisible, isReportActionsLoaded]); useEffect(() => { if (!isVisible || !isFocused) { @@ -384,7 +386,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) return; } - readNewestAction(report?.reportID, !!reportLoadingState?.hasOnceLoadedReportActions); + 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 @@ -392,7 +394,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) // 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-hooks/exhaustive-deps - }, [isFocused, isVisible, reportLoadingState?.hasOnceLoadedReportActions]); + }, [isFocused, isVisible]); /** * The index of the earliest message that was received while offline @@ -441,7 +443,6 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) // We additionally track the top offset to be able to scroll to the new transaction when it's added scrollingVerticalTopOffset.current = contentOffset.y; }, - hasOnceLoadedReportActions: !!reportLoadingState?.hasOnceLoadedReportActions, }); useScrollToEndOnNewMessageReceived({ @@ -601,8 +602,8 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) reportScrollManager.scrollToEnd(); readActionSkipped.current = false; - readNewestAction(reportID, !!reportLoadingState?.hasOnceLoadedReportActions); - }, [setIsFloatingMessageCounterVisible, hasNewestReportAction, reportScrollManager, reportID, reportLoadingState?.hasOnceLoadedReportActions, introSelected, betas]); + readNewestAction(reportID); + }, [setIsFloatingMessageCounterVisible, hasNewestReportAction, reportScrollManager, reportID, introSelected, betas]); const scrollToNewTransaction = useCallback( (pageY: number) => { diff --git a/src/hooks/useIsReportActionsLoaded.tsx b/src/hooks/useIsReportActionsLoaded.tsx new file mode 100644 index 000000000000..88a7fe88b4c6 --- /dev/null +++ b/src/hooks/useIsReportActionsLoaded.tsx @@ -0,0 +1,18 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {hasOnceLoadedReportActionsSelector} from '@src/selectors/ReportMetaData'; +import type {ReportActions} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import useOnyx from './useOnyx'; + +function hasReportActionsSelector(reportActions: OnyxEntry) { + return !isEmptyObject(reportActions); +} + +function useIsReportActionsLoaded(reportID: string | undefined) { + const [hasOnceLoadedReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportID}`, {selector: hasOnceLoadedReportActionsSelector}); + const [hasReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {selector: hasReportActionsSelector}); + return !!hasOnceLoadedReportActions || !!hasReportActions; +} + +export default useIsReportActionsLoaded; diff --git a/src/hooks/useMarkAsRead.ts b/src/hooks/useMarkAsRead.ts index 518a21a852ad..103d881eda88 100644 --- a/src/hooks/useMarkAsRead.ts +++ b/src/hooks/useMarkAsRead.ts @@ -12,12 +12,11 @@ import Visibility from '@libs/Visibility'; import type {ReportsSplitNavigatorParamList} from '@navigation/types'; import {readNewestAction} from '@userActions/Report'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; import useIsAnonymousUser from './useIsAnonymousUser'; -import useOnyx from './useOnyx'; +import useIsReportActionsLoaded from './useIsReportActionsLoaded'; import useReportIsArchived from './useReportIsArchived'; // useRef gets reset when the reportID changes (the list reuses the same instance per report), @@ -43,8 +42,7 @@ function useMarkAsRead({reportID, report, transactionThreadReport, sortedVisible const route = useRoute>(); const isFocused = useIsFocused(); const isReportArchived = useReportIsArchived(reportID); - - const [reportLoadingState] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportID}`); + const isReportActionsLoaded = useIsReportActionsLoaded(reportID); const [isVisible, setIsVisible] = useState(Visibility.isVisible); useEffect(() => { @@ -86,8 +84,8 @@ function useMarkAsRead({reportID, report, transactionThreadReport, sortedVisible } didMarkReportAsReadInitially.current = true; - readNewestAction(reportID, !!reportLoadingState?.hasOnceLoadedReportActions); - }, [isReportUnreadValue, reportID, reportLoadingState?.hasOnceLoadedReportActions]); + readNewestAction(reportID, isReportActionsLoaded); + }, [isReportUnreadValue, reportID, isReportActionsLoaded]); const didMarkOnReportChangeRef = useRef(false); @@ -104,7 +102,7 @@ function useMarkAsRead({reportID, report, transactionThreadReport, sortedVisible const isFromNotification = route?.params?.referrer === CONST.REFERRER.NOTIFICATION; if ((isVisible || isFromNotification) && !hasNewerActions && isScrolledToEnd) { - readNewestAction(reportID, !!reportLoadingState?.hasOnceLoadedReportActions); + readNewestAction(reportID, isReportActionsLoaded); if (isFromNotification) { Navigation.setParams({referrer: undefined}); } @@ -114,7 +112,7 @@ function useMarkAsRead({reportID, report, transactionThreadReport, sortedVisible readActionSkippedRef.current = true; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [report?.lastVisibleActionCreated, transactionThreadReport?.lastVisibleActionCreated, reportID, isVisible, reportLoadingState?.hasOnceLoadedReportActions]); + }, [report?.lastVisibleActionCreated, transactionThreadReport?.lastVisibleActionCreated, reportID, isVisible, isReportActionsLoaded]); useEffect(() => { if (didMarkOnReportChangeRef.current) { @@ -148,10 +146,10 @@ function useMarkAsRead({reportID, report, transactionThreadReport, sortedVisible return; } - readNewestAction(reportID, !!reportLoadingState?.hasOnceLoadedReportActions); + readNewestAction(reportID); userActiveSince.current = DateUtils.getDBTime(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isVisible, isFocused, reportLoadingState?.hasOnceLoadedReportActions]); + }, [isVisible, isFocused]); return {readActionSkippedRef}; } diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index c8d51a595aaa..2ae66b461c81 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -2716,7 +2716,7 @@ function expandURLPreview(reportID: string | undefined, reportActionID: string) * @param shouldResetUnreadMarker Indicates whether the unread indicator should be reset. * Currently, the unread indicator needs to be reset only when users mark a report as read. */ -function readNewestAction(reportID: string | undefined, hasOnceLoadedReportActions: boolean, shouldResetUnreadMarker = false) { +function readNewestAction(reportID: string | undefined, isReportActionsLoaded = true, shouldResetUnreadMarker = false) { if (!reportID) { return; } @@ -2724,12 +2724,8 @@ function readNewestAction(reportID: string | undefined, hasOnceLoadedReportActio // Do not try to mark the report as read if the report has not been loaded and shared with the user. // However, if report actions already exist in Onyx (e.g., delivered via Pusher), the report is // clearly shared with the user and we can proceed with marking it as read. - if (!hasOnceLoadedReportActions) { - const reportActions = allReportActions?.[reportID]; - const hasReportActions = !!reportActions && Object.keys(reportActions).length > 0; - if (!hasReportActions) { - return; - } + if (!isReportActionsLoaded) { + return; } const lastReadTime = getDBTimeWithSkew(); diff --git a/src/pages/inbox/ReportFetchHandler.tsx b/src/pages/inbox/ReportFetchHandler.tsx index 942b9dbf3be2..2709a2029f37 100644 --- a/src/pages/inbox/ReportFetchHandler.tsx +++ b/src/pages/inbox/ReportFetchHandler.tsx @@ -6,6 +6,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useIsAnonymousUser from '@hooks/useIsAnonymousUser'; import useIsInSidePanel from '@hooks/useIsInSidePanel'; import useIsOwnWorkspaceChatRef from '@hooks/useIsOwnWorkspaceChatRef'; +import useIsReportActionsLoaded from '@hooks/useIsReportActionsLoaded'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; @@ -87,6 +88,7 @@ function ReportFetchHandler() { const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportOnyx?.chatReportID}`); const [reportMetadata = defaultReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDFromRoute}`); const [reportLoadingState = defaultReportLoadingState] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportIDFromRoute}`); + const isReportActionsLoaded = useIsReportActionsLoaded(reportIDFromRoute); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [betas] = useOnyx(ONYXKEYS.BETAS); const [onboarding] = useOnyx(ONYXKEYS.NVP_ONBOARDING); @@ -341,8 +343,8 @@ function ReportFetchHandler() { 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, !!reportLoadingState?.hasOnceLoadedReportActions); - }, [report, reportLoadingState?.hasOnceLoadedReportActions]); + readNewestAction(report?.reportID, isReportActionsLoaded); + }, [report, isReportActionsLoaded]); useEffect(() => { hasCreatedLegacyThreadRef.current = false; diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx index 4bb59cd2d6e7..491cb09f565e 100644 --- a/src/pages/inbox/report/ReportActionsList.tsx +++ b/src/pages/inbox/report/ReportActionsList.tsx @@ -343,7 +343,6 @@ function ReportActionsList({ setHasScrolledOverThreshold(offset >= CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD); onScroll?.(event); }, - hasOnceLoadedReportActions: !!reportLoadingState?.hasOnceLoadedReportActions, actionBadgeTargetIndex, }); @@ -478,18 +477,8 @@ function ReportActionsList({ } reportScrollManager.scrollToBottom(); readActionSkippedRef.current = false; - readNewestAction(report.reportID, !!reportLoadingState?.hasOnceLoadedReportActions); - }, [ - setIsFloatingMessageCounterVisible, - hasNewestReportAction, - reportScrollManager, - report.reportID, - backTo, - introSelected, - reportLoadingState?.hasOnceLoadedReportActions, - betas, - readActionSkippedRef, - ]); + readNewestAction(report.reportID); + }, [setIsFloatingMessageCounterVisible, hasNewestReportAction, reportScrollManager, report.reportID, backTo, introSelected, betas, readActionSkippedRef]); const scrollToActionBadgeTarget = useCallback(() => { if (actionBadgeTargetIndex < 0) { diff --git a/src/pages/inbox/report/useReportUnreadMessageScrollTracking.ts b/src/pages/inbox/report/useReportUnreadMessageScrollTracking.ts index 8063380fce48..332c79896f17 100644 --- a/src/pages/inbox/report/useReportUnreadMessageScrollTracking.ts +++ b/src/pages/inbox/report/useReportUnreadMessageScrollTracking.ts @@ -27,9 +27,6 @@ type Args = { /** Callback to call on every scroll event */ onTrackScrolling: (event: NativeSyntheticEvent) => void; - /** Whether the report actions have been loaded at least once */ - hasOnceLoadedReportActions: boolean; - /** The index of the action badge target report action in the sorted visible actions list (-1 if none) */ actionBadgeTargetIndex?: number; }; @@ -42,7 +39,6 @@ export default function useReportUnreadMessageScrollTracking({ onTrackScrolling, unreadMarkerReportActionIndex, isInverted, - hasOnceLoadedReportActions, actionBadgeTargetIndex = -1, }: Args) { const [isFloatingMessageCounterVisible, setIsFloatingMessageCounterVisible] = useState(false); @@ -53,14 +49,12 @@ export default function useReportUnreadMessageScrollTracking({ reportID: string; unreadMarkerReportActionIndex: number; isFocused: boolean; - hasOnceLoadedReportActions: boolean; actionBadgeTargetIndex: number; }>({ reportID, unreadMarkerReportActionIndex, previousViewableItems: [], isFocused: true, - hasOnceLoadedReportActions, actionBadgeTargetIndex, }); // We want to save the updated value on ref to use it in onViewableItemsChanged @@ -74,10 +68,6 @@ export default function useReportUnreadMessageScrollTracking({ ref.current.isFocused = isFocused; }, [isFocused]); - useEffect(() => { - ref.current.hasOnceLoadedReportActions = hasOnceLoadedReportActions; - }, [hasOnceLoadedReportActions]); - /** * On every scroll event we want to: * Show/hide the latest message pill when user is scrolling back/forth in the history of messages. @@ -140,7 +130,7 @@ export default function useReportUnreadMessageScrollTracking({ if (unreadActionVisible && readActionSkippedRef.current) { // eslint-disable-next-line no-param-reassign readActionSkippedRef.current = false; - readNewestAction(ref.current.reportID, ref.current.hasOnceLoadedReportActions); + readNewestAction(ref.current.reportID); } // Track whether the action badge target is above the viewport (i.e., not visible and at a higher index in the inverted list) diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index b25f9e3ea898..bc501fec0a70 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -603,7 +603,7 @@ describe('actions/Report', () => { // When the user visits the report currentTime = DateUtils.getDBTime(); Report.openReport({reportID: REPORT_ID, introSelected: TEST_INTRO_SELECTED, betas: undefined}); - Report.readNewestAction(REPORT_ID, true); + Report.readNewestAction(REPORT_ID); waitForBatchedUpdates(); return waitForBatchedUpdates(); }) @@ -7694,71 +7694,6 @@ describe('actions/Report', () => { }); }); - describe('readNewestAction', () => { - it('should mark a report as read when hasOnceLoadedReportActions is false but report actions exist in Onyx', () => { - global.fetch = TestHelper.getGlobalFetchMock(); - const REPORT_ID = '1'; - const USER_1_LOGIN = 'user@test.com'; - const USER_1_ACCOUNT_ID = 1; - const USER_2_ACCOUNT_ID = 2; - - let report: OnyxEntry; - Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, - callback: (val) => (report = val), - }); - - const reportActionCreatedDate = DateUtils.getDBTime(); - - return TestHelper.signInWithTestUser(USER_1_ACCOUNT_ID, USER_1_LOGIN) - .then(waitForNetworkPromises) - .then(() => TestHelper.setPersonalDetails(USER_1_LOGIN, USER_1_ACCOUNT_ID)) - .then(() => { - // Set up a report with actions in Onyx (as if delivered via Pusher from a new user) - // but without hasOnceLoadedReportActions being set (simulating offline + new chat) - return Promise.all([ - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { - reportID: REPORT_ID, - participants: { - [USER_1_ACCOUNT_ID]: {notificationPreference: 'always'}, - }, - lastMessageText: 'Hello from new user', - lastActorAccountID: USER_2_ACCOUNT_ID, - lastVisibleActionCreated: reportActionCreatedDate, - lastReadTime: DateUtils.subtractMillisecondsFromDateTime(reportActionCreatedDate, 1), - }), - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { - 1: { - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - actorAccountID: USER_2_ACCOUNT_ID, - automatic: false, - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png', - message: [{type: 'COMMENT', html: 'Hello from new user', text: 'Hello from new user'}], - person: [{type: 'TEXT', style: 'strong', text: 'New User'}], - shouldShow: true, - created: reportActionCreatedDate, - reportActionID: '1', - }, - }), - ]); - }) - .then(waitForBatchedUpdates) - .then(() => { - // Verify the report is currently unread - expect(ReportUtils.isUnread(report, undefined, undefined)).toBe(true); - - // Call readNewestAction with hasOnceLoadedReportActions = false - // This simulates the scenario where a chat from a new user is opened offline - Report.readNewestAction(REPORT_ID, false); - return waitForBatchedUpdates(); - }) - .then(() => { - // The report should now be read because report actions exist in Onyx - expect(ReportUtils.isUnread(report, undefined, undefined)).toBe(false); - }); - }); - }); - describe('resolveActionableMentionWhisper', () => { it('should optimistically add invited users to report.participants when resolution is INVITE', async () => { global.fetch = TestHelper.getGlobalFetchMock(); diff --git a/tests/unit/useIsReportActionsLoadedTest.ts b/tests/unit/useIsReportActionsLoadedTest.ts new file mode 100644 index 000000000000..6e71d8548748 --- /dev/null +++ b/tests/unit/useIsReportActionsLoadedTest.ts @@ -0,0 +1,61 @@ +import {act, renderHook} from '@testing-library/react-native'; +import Onyx from 'react-native-onyx'; +import useIsReportActionsLoaded from '@hooks/useIsReportActionsLoaded'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportAction} from '@src/types/onyx'; + +const REPORT_ID = '1'; + +const REPORT_ACTION = { + reportActionID: '100', + actionName: 'ADDCOMMENT', + created: '2023-01-01 10:00:00.000', +} as ReportAction; + +describe('useIsReportActionsLoaded', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + afterEach(() => Onyx.clear()); + + it('returns false when neither the loading state nor report actions are loaded', async () => { + const {result} = renderHook(() => useIsReportActionsLoaded(REPORT_ID)); + expect(result.current).toBe(false); + }); + + it('returns true when hasOnceLoadedReportActions is true in the loading state', async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${REPORT_ID}`, {hasOnceLoadedReportActions: true}); + + const {result} = renderHook(() => useIsReportActionsLoaded(REPORT_ID)); + await expect(result.current).toBe(true); + }); + + it('returns true when the report has report actions even if it has never finished loading', async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {[REPORT_ACTION.reportActionID]: REPORT_ACTION}); + + const {result} = renderHook(() => useIsReportActionsLoaded(REPORT_ID)); + await expect(result.current).toBe(true); + }); + + it('returns false when the report actions object is empty', async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {}); + + const {result} = renderHook(() => useIsReportActionsLoaded(REPORT_ID)); + expect(result.current).toBe(false); + }); + + it('returns false when reportID is undefined', async () => { + const {result} = renderHook(() => useIsReportActionsLoaded(undefined)); + expect(result.current).toBe(false); + }); + + it('updates from false to true when report actions become available', async () => { + const {result} = renderHook(() => useIsReportActionsLoaded(REPORT_ID)); + expect(result.current).toBe(false); + + await act(async () => Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {[REPORT_ACTION.reportActionID]: REPORT_ACTION})); + + expect(result.current).toBe(true); + }); +}); diff --git a/tests/unit/useReportUnreadMessageScrollTrackingTest.ts b/tests/unit/useReportUnreadMessageScrollTrackingTest.ts index 627de01d2cb1..e1be2067d70d 100644 --- a/tests/unit/useReportUnreadMessageScrollTrackingTest.ts +++ b/tests/unit/useReportUnreadMessageScrollTrackingTest.ts @@ -40,7 +40,6 @@ describe('useReportUnreadMessageScrollTracking', () => { onTrackScrolling: onTrackScrollingMockFn, hasNewerActions: false, unreadMarkerReportActionIndex: -1, - hasOnceLoadedReportActions: true, isInverted: true, }), ); @@ -62,7 +61,6 @@ describe('useReportUnreadMessageScrollTracking', () => { isInverted: true, hasNewerActions: false, onTrackScrolling: onTrackScrollingMockFn, - hasOnceLoadedReportActions: true, }), ); From dd07d280262400929999d64b80455faa2d4dd5c3 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 15 Jun 2026 18:01:30 +0800 Subject: [PATCH 2/5] update comment --- src/libs/actions/Report/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 2ae66b461c81..0ce020476ab3 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -2722,8 +2722,6 @@ function readNewestAction(reportID: string | undefined, isReportActionsLoaded = } // Do not try to mark the report as read if the report has not been loaded and shared with the user. - // However, if report actions already exist in Onyx (e.g., delivered via Pusher), the report is - // clearly shared with the user and we can proceed with marking it as read. if (!isReportActionsLoaded) { return; } From e714a4345d7ebe37aefe1422516c5713e1d7ef62 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 15 Jun 2026 20:18:39 +0800 Subject: [PATCH 3/5] lint --- tests/unit/useIsReportActionsLoadedTest.ts | 4 ++-- .../unit/useReportUnreadMessageScrollTrackingTest.ts | 11 ----------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/tests/unit/useIsReportActionsLoadedTest.ts b/tests/unit/useIsReportActionsLoadedTest.ts index 6e71d8548748..5e0ca91246b4 100644 --- a/tests/unit/useIsReportActionsLoadedTest.ts +++ b/tests/unit/useIsReportActionsLoadedTest.ts @@ -28,14 +28,14 @@ describe('useIsReportActionsLoaded', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${REPORT_ID}`, {hasOnceLoadedReportActions: true}); const {result} = renderHook(() => useIsReportActionsLoaded(REPORT_ID)); - await expect(result.current).toBe(true); + expect(result.current).toBe(true); }); it('returns true when the report has report actions even if it has never finished loading', async () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, {[REPORT_ACTION.reportActionID]: REPORT_ACTION}); const {result} = renderHook(() => useIsReportActionsLoaded(REPORT_ID)); - await expect(result.current).toBe(true); + expect(result.current).toBe(true); }); it('returns false when the report actions object is empty', async () => { diff --git a/tests/unit/useReportUnreadMessageScrollTrackingTest.ts b/tests/unit/useReportUnreadMessageScrollTrackingTest.ts index e1be2067d70d..7f7a2b880b9e 100644 --- a/tests/unit/useReportUnreadMessageScrollTrackingTest.ts +++ b/tests/unit/useReportUnreadMessageScrollTrackingTest.ts @@ -90,7 +90,6 @@ describe('useReportUnreadMessageScrollTracking', () => { isInverted: true, unreadMarkerReportActionIndex: -1, onTrackScrolling: onTrackScrollingMockFn, - hasOnceLoadedReportActions: true, hasNewerActions: false, }), ); @@ -118,7 +117,6 @@ describe('useReportUnreadMessageScrollTracking', () => { isInverted: true, unreadMarkerReportActionIndex: 1, onTrackScrolling: onTrackScrollingMockFn, - hasOnceLoadedReportActions: true, hasNewerActions: false, }), ); @@ -152,7 +150,6 @@ describe('useReportUnreadMessageScrollTracking', () => { isInverted: true, hasNewerActions: false, onTrackScrolling: onTrackScrollingMockFn, - hasOnceLoadedReportActions: true, }), ); @@ -178,7 +175,6 @@ describe('useReportUnreadMessageScrollTracking', () => { unreadMarkerReportActionIndex: 1, isInverted: true, onTrackScrolling: onTrackScrollingMockFn, - hasOnceLoadedReportActions: true, hasNewerActions: false, }), ); @@ -209,7 +205,6 @@ describe('useReportUnreadMessageScrollTracking', () => { unreadMarkerReportActionIndex: 1, isInverted: true, onTrackScrolling: onTrackScrollingMockFn, - hasOnceLoadedReportActions: true, hasNewerActions: false, }), ); @@ -248,7 +243,6 @@ describe('useReportUnreadMessageScrollTracking', () => { onTrackScrolling: onTrackScrollingMockFn, hasNewerActions: false, unreadMarkerReportActionIndex: -1, - hasOnceLoadedReportActions: true, isInverted: true, actionBadgeTargetIndex: -1, }), @@ -267,7 +261,6 @@ describe('useReportUnreadMessageScrollTracking', () => { onTrackScrolling: onTrackScrollingMockFn, hasNewerActions: false, unreadMarkerReportActionIndex: -1, - hasOnceLoadedReportActions: true, isInverted: true, actionBadgeTargetIndex: 5, }), @@ -299,7 +292,6 @@ describe('useReportUnreadMessageScrollTracking', () => { onTrackScrolling: onTrackScrollingMockFn, hasNewerActions: false, unreadMarkerReportActionIndex: -1, - hasOnceLoadedReportActions: true, isInverted: true, actionBadgeTargetIndex: 2, }), @@ -330,7 +322,6 @@ describe('useReportUnreadMessageScrollTracking', () => { onTrackScrolling: onTrackScrollingMockFn, hasNewerActions: false, unreadMarkerReportActionIndex: -1, - hasOnceLoadedReportActions: true, isInverted: true, actionBadgeTargetIndex: -1, }), @@ -356,7 +347,6 @@ describe('useReportUnreadMessageScrollTracking', () => { onTrackScrolling: onTrackScrollingMockFn, hasNewerActions: false, unreadMarkerReportActionIndex: -1, - hasOnceLoadedReportActions: true, isInverted: true, actionBadgeTargetIndex: 5, }), @@ -389,7 +379,6 @@ describe('useReportUnreadMessageScrollTracking', () => { onTrackScrolling: onTrackScrollingMockFn, hasNewerActions: false, unreadMarkerReportActionIndex: -1, - hasOnceLoadedReportActions: true, isInverted: true, actionBadgeTargetIndex, }), From 4c2d03edbe35bc02b9ef614b6a83134ac7aa7d7d Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 19 Jun 2026 21:56:35 +0800 Subject: [PATCH 4/5] make it required param --- .../MoneyRequestReportView/MoneyRequestReportActionsList.tsx | 4 ++-- src/hooks/useMarkAsRead.ts | 4 ++-- src/libs/actions/Report/index.ts | 2 +- tests/actions/ReportTest.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index f2c902fe71aa..3cc3062dac33 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -394,7 +394,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) return; } - readNewestAction(report?.reportID); + readNewestAction(report?.reportID, true); 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 @@ -620,7 +620,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) reportScrollManager.scrollToEnd(); readActionSkipped.current = false; - readNewestAction(reportID); + readNewestAction(reportID, true); }, [setIsFloatingMessageCounterVisible, hasNewestReportAction, reportScrollManager, reportID, introSelected, betas]); const scrollToNewTransaction = useCallback( diff --git a/src/hooks/useMarkAsRead.ts b/src/hooks/useMarkAsRead.ts index 873bd8d1f97c..b50c5ba893d3 100644 --- a/src/hooks/useMarkAsRead.ts +++ b/src/hooks/useMarkAsRead.ts @@ -151,7 +151,7 @@ function useMarkAsRead({reportID, report, transactionThreadReport, sortedVisible return; } - readNewestAction(reportID); + readNewestAction(reportID, true); userActiveSince.current = DateUtils.getDBTime(); // This effect should only run when app visibility/focus changes; the helper reads the latest report/action values without making every action update mark the report as read. // eslint-disable-next-line react-hooks/exhaustive-deps @@ -159,7 +159,7 @@ function useMarkAsRead({reportID, report, transactionThreadReport, sortedVisible const markNewestActionAsRead = () => { readActionSkippedRef.current = false; - readNewestAction(reportID); + readNewestAction(reportID, true); }; const completeSkippedMarkAsRead = () => { diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 6a80b56cc76a..bf107cb07950 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -2730,7 +2730,7 @@ function expandURLPreview(reportID: string | undefined, reportActionID: string) * @param shouldResetUnreadMarker Indicates whether the unread indicator should be reset. * Currently, the unread indicator needs to be reset only when users mark a report as read. */ -function readNewestAction(reportID: string | undefined, isReportActionsLoaded = true, shouldResetUnreadMarker = false) { +function readNewestAction(reportID: string | undefined, isReportActionsLoaded: boolean, shouldResetUnreadMarker = false) { if (!reportID) { return; } diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index a2604b395390..374bd680bc24 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -604,7 +604,7 @@ describe('actions/Report', () => { // When the user visits the report currentTime = DateUtils.getDBTime(); Report.openReport({reportID: REPORT_ID, introSelected: TEST_INTRO_SELECTED, betas: undefined}); - Report.readNewestAction(REPORT_ID); + Report.readNewestAction(REPORT_ID, true); waitForBatchedUpdates(); return waitForBatchedUpdates(); }) From 6f80d0d7291e2e9599507535443797b460b38604 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 20 Jun 2026 15:01:02 +0800 Subject: [PATCH 5/5] fix test --- tests/unit/useMarkAsReadTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/useMarkAsReadTest.ts b/tests/unit/useMarkAsReadTest.ts index b44cdc6e00b3..804e9b48f5e5 100644 --- a/tests/unit/useMarkAsReadTest.ts +++ b/tests/unit/useMarkAsReadTest.ts @@ -103,7 +103,7 @@ describe('useMarkAsRead', () => { act(() => result.current.completeSkippedMarkAsRead()); - expect(readNewestAction).toHaveBeenCalledWith(REPORT_ID, false); + expect(readNewestAction).toHaveBeenCalledWith(REPORT_ID, true); }); it('does not complete a mark-as-read when none was skipped', () => {