Skip to content

Commit fecf449

Browse files
authored
Merge pull request Expensify#94817 from callstack-internal/decompose/ral-11
Decompose ReportActionsList: 11
2 parents 6b171d1 + 33752a6 commit fecf449

3 files changed

Lines changed: 58 additions & 7 deletions

File tree

src/hooks/useUnreadMarker.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,30 @@ import useOnyx from './useOnyx';
1313
import usePrevious from './usePrevious';
1414

1515
type UseUnreadMarkerParams = {
16+
/** The report whose unread marker is being computed */
1617
reportID: string;
18+
19+
/** The visible actions (FlatList `data` domain, newest-first) that the marker scan runs over */
1720
sortedVisibleReportActions: OnyxTypes.ReportAction[];
21+
22+
/** All sorted actions (the full chain); used to find the earliest-received-while-offline message index */
1823
sortedReportActions: OnyxTypes.ReportAction[];
24+
25+
/** The oldest unread action id used as the pagination anchor for marker placement before actions have fully loaded */
1926
oldestUnreadReportActionID: string | undefined;
27+
28+
/** Whether the list is scrolled past the threshold where incoming actions are treated as out of view */
2029
isScrolledOverThreshold: boolean;
30+
31+
/** Whether report actions have loaded at least once; once true, the pagination anchor is ignored in favor of the scan */
2132
hasOnceLoadedReportActions: boolean;
2233
};
2334

2435
type UseUnreadMarkerResult = {
36+
/** The reportActionID the unread marker should render above, or `null` if none qualifies */
2537
unreadMarkerReportActionID: string | null;
38+
39+
/** Index of that action within `sortedVisibleReportActions`, or `-1` if none */
2640
unreadMarkerReportActionIndex: number;
2741
};
2842

@@ -48,12 +62,6 @@ function useUnreadMarker({
4862

4963
const [unreadMarkerTime, setUnreadMarkerTime] = useState(reportLastReadTime);
5064

51-
const [trackedReportID, setTrackedReportID] = useState(reportID);
52-
if (trackedReportID !== reportID) {
53-
setTrackedReportID(reportID);
54-
setUnreadMarkerTime(reportLastReadTime);
55-
}
56-
5765
if (unreadMarkerTime === '' && reportLastReadTime !== '') {
5866
setUnreadMarkerTime(reportLastReadTime);
5967
}

tests/unit/ReportActionsUtilsTest.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5576,6 +5576,18 @@ describe('ReportActionsUtils', () => {
55765576
}),
55775577
).toEqual(['unread-newer', 1]);
55785578
});
5579+
5580+
it("clears the marker entirely when the only unread action is the current user's own new message", () => {
5581+
const ownNew = makeAction({reportActionID: 'own-new', actorAccountID: currentUserAccountID});
5582+
const olderRead = makeAction({reportActionID: 'older-read', created: '2023-01-01 09:00:00.000'});
5583+
expect(
5584+
getUnreadMarkerReportAction({
5585+
...baseScanParams,
5586+
visibleReportActions: [ownNew, olderRead],
5587+
prevSortedVisibleReportActionsObjects: {},
5588+
}),
5589+
).toEqual([null, -1]);
5590+
});
55795591
});
55805592

55815593
describe('getIntegrationSyncFailedMessage', () => {

tests/unit/useUnreadMarkerTest.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {act, renderHook} from '@testing-library/react-native';
22
import {DeviceEventEmitter} from 'react-native';
33
import useUnreadMarker from '@hooks/useUnreadMarker';
4+
import ONYXKEYS from '@src/ONYXKEYS';
45
import type * as OnyxTypes from '@src/types/onyx';
56
import {getFakeReportAction} from '../utils/ReportTestUtils';
67

@@ -11,6 +12,7 @@ const LAST_READ_TIME = '2023-01-01 10:00:00.000';
1112

1213
let mockIsAnonymousUser = false;
1314
let mockLastReadTime: string = LAST_READ_TIME;
15+
let mockLastReadTimeByReportID: Record<string, string> = {};
1416

1517
jest.mock('@hooks/useCurrentUserPersonalDetails', () => ({
1618
__esModule: true,
@@ -22,9 +24,13 @@ jest.mock('@hooks/useIsAnonymousUser', () => ({
2224
default: () => mockIsAnonymousUser,
2325
}));
2426

27+
// The hook subscribes to `${ONYXKEYS.COLLECTION.REPORT}${reportID}` with a selector that returns
28+
// `lastReadTime`. The implementation is set in beforeEach so it can use ONYXKEYS freely (a jest.mock
29+
// factory cannot reference out-of-scope variables).
30+
const mockUseOnyx = jest.fn<[string], [string]>();
2531
jest.mock('@hooks/useOnyx', () => ({
2632
__esModule: true,
27-
default: () => [mockLastReadTime],
33+
default: (key: string) => mockUseOnyx(key),
2834
}));
2935

3036
function makeAction(reportActionID: string, overrides: Partial<OnyxTypes.ReportAction> = {}): OnyxTypes.ReportAction {
@@ -55,6 +61,11 @@ describe('useUnreadMarker', () => {
5561
beforeEach(() => {
5662
mockIsAnonymousUser = false;
5763
mockLastReadTime = LAST_READ_TIME;
64+
mockLastReadTimeByReportID = {};
65+
mockUseOnyx.mockImplementation((key) => {
66+
const reportID = key.replace(ONYXKEYS.COLLECTION.REPORT, '');
67+
return [mockLastReadTimeByReportID[reportID] ?? mockLastReadTime];
68+
});
5869
});
5970

6071
it('returns [null, -1] for an anonymous user', () => {
@@ -107,4 +118,24 @@ describe('useUnreadMarker', () => {
107118
expect(result.current.unreadMarkerReportActionID).toBeNull();
108119
expect(result.current.unreadMarkerReportActionIndex).toBe(-1);
109120
});
121+
122+
it('seeds the marker from the switched-to report lastReadTime (one mount per report)', () => {
123+
// Setup: report 'A' was last read at 10:00 and 'B' at 12:00; one action from another user lands
124+
// at 11:00 — after A's read time (unread on A) but before B's read time (already read on B).
125+
mockLastReadTimeByReportID = {
126+
A: '2023-01-01 10:00:00.000',
127+
B: '2023-01-01 12:00:00.000',
128+
};
129+
const action = makeAction('msg', {created: '2023-01-01 11:00:00.000'});
130+
131+
// Mounted on A: 11:00 is after A's 10:00 read time → unread → marker lands on the action.
132+
const {result: resultA} = renderUnreadMarker({reportID: 'A', sortedVisibleReportActions: [action], sortedReportActions: [action]});
133+
expect(resultA.current.unreadMarkerReportActionID).toBe('msg');
134+
expect(resultA.current.unreadMarkerReportActionIndex).toBe(0);
135+
136+
// Mounted on B: 11:00 is before B's 12:00 read time → already read → no marker.
137+
const {result: resultB} = renderUnreadMarker({reportID: 'B', sortedVisibleReportActions: [action], sortedReportActions: [action]});
138+
expect(resultB.current.unreadMarkerReportActionID).toBeNull();
139+
expect(resultB.current.unreadMarkerReportActionIndex).toBe(-1);
140+
});
110141
});

0 commit comments

Comments
 (0)