Skip to content

Commit 4c6a31e

Browse files
authored
Merge pull request Expensify#90065 from Expensify/revert-51366-@janic/open-at-new
Revert "Open reports at first unread action"
2 parents fc1a858 + d944ae0 commit 4c6a31e

27 files changed

Lines changed: 410 additions & 1119 deletions

jest/setup.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import mockStorage from 'react-native-onyx/dist/storage/__mocks__';
1111
import type Animated from 'react-native-reanimated';
1212
import 'setimmediate';
1313
import {TextDecoder, TextEncoder} from 'util';
14-
import type {RenderInfo} from '@components/FlatList/RenderTaskQueue';
1514
import '@src/polyfills/PromiseWithResolvers';
1615
import '@src/polyfills/requestIdleCallback';
1716
import mockFSLibrary from './setupMockFullstoryLib';
@@ -266,7 +265,7 @@ jest.mock(
266265
class SyncRenderTaskQueue {
267266
private handler: (info: unknown) => void = () => {};
268267

269-
add(info: RenderInfo) {
268+
add(info: unknown) {
270269
this.handler(info);
271270
}
272271

src/components/FlashList/InvertedFlashList/index.tsx

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,34 +22,15 @@ type InvertedFlashListProps<T> = FlashListProps<T> & {
2222
shouldMaintainVisibleContentPosition?: boolean;
2323
};
2424

25-
function InvertedFlashList<T>({
26-
data,
27-
keyExtractor,
28-
initialScrollKey,
29-
onStartReached: onStartReachedProp,
30-
maintainVisibleContentPosition: maintainVisibleContentPositionProp,
31-
shouldMaintainVisibleContentPosition,
32-
...restProps
33-
}: InvertedFlashListProps<T>) {
34-
const {
35-
displayedData,
36-
onStartReached,
37-
maintainVisibleContentPosition: maintainVisibleContentPositionForScrollKey,
38-
} = useFlashListScrollKey<T>({
25+
function InvertedFlashList<T>({data, keyExtractor, initialScrollKey, onStartReached: onStartReachedProp, shouldMaintainVisibleContentPosition, ...restProps}: InvertedFlashListProps<T>) {
26+
const {displayedData, onStartReached, maintainVisibleContentPosition} = useFlashListScrollKey<T>({
3927
data,
4028
keyExtractor,
4129
initialScrollKey,
4230
onStartReached: onStartReachedProp,
4331
shouldMaintainVisibleContentPosition,
4432
});
4533

46-
const maintainVisibleContentPosition = maintainVisibleContentPositionProp
47-
? {
48-
...maintainVisibleContentPositionForScrollKey,
49-
...maintainVisibleContentPositionProp,
50-
}
51-
: maintainVisibleContentPositionForScrollKey;
52-
5334
return (
5435
<FlashList<T>
5536
// eslint-disable-next-line react/jsx-props-no-spreading

src/components/FlashList/useFlashListScrollKey.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,28 +19,17 @@ type FlashListScrollKeyProps<T> = {
1919
};
2020

2121
export default function useFlashListScrollKey<T>({data, keyExtractor, initialScrollKey, onStartReached, shouldMaintainVisibleContentPosition}: FlashListScrollKeyProps<T>) {
22-
const [isInitialRender, setIsInitialRender] = useState(!!initialScrollKey);
22+
const [isInitialRender, setIsInitialRender] = useState(true);
2323
const [hasLinkingSettled, setHasLinkingSettled] = useState(!initialScrollKey);
2424

2525
// Two-frame handoff for deep-link:
2626
// RAF 1: switch from sliced data to the full array — FlashList's default MVCP pins the
2727
// linked item through the data swap.
2828
// RAF 2: pinning has happened, disable MVCP so it doesn't cause later jumps.
2929
useEffect(() => {
30-
if (!isInitialRender) {
30+
if (!isInitialRender || !initialScrollKey) {
3131
return;
3232
}
33-
34-
// Without an anchor on this frame, we are not doing the deep-link slice handoff; clear the flag so a key that
35-
// appears later (e.g. marking a message unread) cannot reuse the "first paint" slice path.
36-
if (!initialScrollKey) {
37-
// If the initial scroll key gets unset, we need to disable the initial render flag,
38-
// otherwise the list will not render..
39-
// eslint-disable-next-line react-hooks/set-state-in-effect
40-
setIsInitialRender(false);
41-
return;
42-
}
43-
4433
requestAnimationFrame(() => {
4534
setIsInitialRender(false);
4635
requestAnimationFrame(() => setHasLinkingSettled(true));

src/components/FlatList/FlatListWithScrollKey/BaseFlatListWithScrollKey.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function BaseFlatListWithScrollKey<T>({ref, ...props}: BaseFlatListWithScrollKey
1919
onScrollBeginDrag,
2020
onWheel,
2121
onTouchStartCapture,
22-
...restProps
22+
...rest
2323
} = props;
2424
const {displayedData, maintainVisibleContentPosition, handleStartReached, isInitialData, handleRenderItem, listRef} = useFlatListScrollKey<T>({
2525
data,
@@ -52,7 +52,7 @@ function BaseFlatListWithScrollKey<T>({ref, ...props}: BaseFlatListWithScrollKey
5252
<FlatList
5353
ref={listRef}
5454
// eslint-disable-next-line react/jsx-props-no-spreading
55-
{...restProps}
55+
{...rest}
5656
data={displayedData}
5757
maintainVisibleContentPosition={maintainVisibleContentPosition}
5858
onStartReached={handleStartReached}
@@ -61,8 +61,8 @@ function BaseFlatListWithScrollKey<T>({ref, ...props}: BaseFlatListWithScrollKey
6161
// Since ListHeaderComponent is always prioritized for rendering before the data,
6262
// it will be rendered once the data has finished loading.
6363
// This prevents an unnecessary empty space above the highlighted item.
64-
ListHeaderComponent={!isInitialData ? restProps.ListHeaderComponent : undefined}
65-
contentContainerStyle={!isInitialData ? restProps.contentContainerStyle : undefined}
64+
ListHeaderComponent={!isInitialData ? rest.ListHeaderComponent : undefined}
65+
contentContainerStyle={!isInitialData ? rest.contentContainerStyle : undefined}
6666
onContentSizeChange={(width, height) => onContentSizeChange?.(width, height, isInitialData)}
6767
onViewableItemsChanged={(info) => {
6868
onViewableItemsChanged?.(info);

src/components/FlatList/RenderTaskQueue.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,3 @@ class RenderTaskQueue {
5858
}
5959

6060
export default RenderTaskQueue;
61-
export type {RenderInfo};

src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,6 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
434434
readActionSkippedRef: readActionSkipped,
435435
unreadMarkerReportActionIndex,
436436
isInverted: false,
437-
hasNewerActions,
438437
onTrackScrolling: (event: NativeSyntheticEvent<NativeScrollEvent>) => {
439438
const {layoutMeasurement, contentSize, contentOffset} = event.nativeEvent;
440439
const fullContentHeight = contentSize.height;
@@ -461,7 +460,6 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
461460
hasNewestReportAction,
462461
setIsFloatingMessageCounterVisible,
463462
scrollToEnd: reportScrollManager.scrollToEnd,
464-
resetKey: report.reportID,
465463
});
466464

467465
/**
@@ -610,15 +608,15 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
610608
setIsFloatingMessageCounterVisible(false);
611609

612610
if (!hasNewestReportAction) {
613-
openReport({reportID, introSelected, betas});
611+
openReport({reportID: report?.reportID, introSelected, betas});
614612
reportScrollManager.scrollToEnd();
615613
return;
616614
}
617615

618616
reportScrollManager.scrollToEnd();
619617
readActionSkipped.current = false;
620-
readNewestAction(reportID, !!reportLoadingState?.hasOnceLoadedReportActions);
621-
}, [setIsFloatingMessageCounterVisible, hasNewestReportAction, reportScrollManager, reportID, reportLoadingState?.hasOnceLoadedReportActions, introSelected, betas]);
618+
readNewestAction(report?.reportID, !!reportLoadingState?.hasOnceLoadedReportActions);
619+
}, [setIsFloatingMessageCounterVisible, hasNewestReportAction, reportScrollManager, report?.reportID, reportLoadingState?.hasOnceLoadedReportActions, introSelected, betas]);
622620

623621
const scrollToNewTransaction = useCallback(
624622
(pageY: number) => {

src/hooks/usePaginatedReportActions.ts

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,13 @@ import useReportIsArchived from './useReportIsArchived';
1212
type UsePaginatedReportActionsOptions = {
1313
/** Whether to link to the oldest unread report action, if no other report action id is provided. */
1414
shouldLinkToOldestUnreadReportAction?: boolean;
15-
16-
/** When true, pagination anchors to the newest window only (ignores route and unread-derived anchors). */
17-
treatAsNoPaginationAnchor?: boolean;
1815
};
1916

2017
/**
2118
* Get the longest continuous chunk of reportActions including the linked reportAction. If not linking to a specific action, returns the continuous chunk of newest reportActions.
2219
*/
2320
function usePaginatedReportActions(reportID: string | undefined, reportActionID?: string, options?: UsePaginatedReportActionsOptions) {
24-
const {shouldLinkToOldestUnreadReportAction = false, treatAsNoPaginationAnchor = false} = options ?? {};
21+
const {shouldLinkToOldestUnreadReportAction = false} = options ?? {};
2522

2623
const nonEmptyStringReportID = getNonEmptyStringOnyxID(reportID);
2724
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${nonEmptyStringReportID}`);
@@ -47,11 +44,6 @@ function usePaginatedReportActions(reportID: string | undefined, reportActionID?
4744
const initialReportLastReadTime = useRef(report?.lastReadTime);
4845

4946
const id = useMemo(() => {
50-
/* eslint-disable react-hooks/refs -- initialReportLastReadTime snapshots lastRead at first render for stable unread deep-link anchor */
51-
if (treatAsNoPaginationAnchor) {
52-
return undefined;
53-
}
54-
5547
if (reportActionID) {
5648
return reportActionID;
5749
}
@@ -60,14 +52,14 @@ function usePaginatedReportActions(reportID: string | undefined, reportActionID?
6052
return undefined;
6153
}
6254

63-
const initialLastReadTime = initialReportLastReadTime.current;
64-
if (!initialLastReadTime || !sortedAllReportActions?.length) {
65-
return undefined;
66-
}
55+
return sortedAllReportActions?.findLast((reportAction) => {
56+
if (!initialReportLastReadTime.current) {
57+
return false;
58+
}
6759

68-
return sortedAllReportActions.findLast((reportAction) => reportAction.created > initialLastReadTime)?.reportActionID;
69-
/* eslint-enable react-hooks/refs */
70-
}, [treatAsNoPaginationAnchor, reportActionID, shouldLinkToOldestUnreadReportAction, sortedAllReportActions]);
60+
return reportAction.created > initialReportLastReadTime.current;
61+
})?.reportActionID;
62+
}, [reportActionID, shouldLinkToOldestUnreadReportAction, sortedAllReportActions]);
7163

7264
const {
7365
data: reportActions,
@@ -82,27 +74,14 @@ function usePaginatedReportActions(reportID: string | undefined, reportActionID?
8274
return PaginationUtils.getContinuousChain(sortedAllReportActions, reportActionPages ?? [], (reportAction) => reportAction.reportActionID, id);
8375
}, [id, reportActionPages, sortedAllReportActions]);
8476

85-
// When `treatAsNoPaginationAnchor` is set, we intentionally ignore `reportActionID` for pagination
86-
// (same as `id` above), so we must not surface a "linked" action from that id either.
87-
const linkedAction = useMemo(() => {
88-
if (treatAsNoPaginationAnchor) {
89-
return undefined;
90-
}
91-
if (!reportActionID) {
92-
return undefined;
93-
}
94-
return resourceItem?.item;
95-
}, [resourceItem?.item, reportActionID, treatAsNoPaginationAnchor]);
77+
const linkedAction = useMemo(() => (reportActionID ? resourceItem?.item : undefined), [resourceItem?.item, reportActionID]);
9678

9779
const oldestUnreadReportAction = useMemo(() => {
98-
if (treatAsNoPaginationAnchor) {
99-
return undefined;
100-
}
10180
if (shouldLinkToOldestUnreadReportAction && resourceItem && !reportActionID) {
10281
return resourceItem.item;
10382
}
10483
return undefined;
105-
}, [resourceItem, shouldLinkToOldestUnreadReportAction, reportActionID, treatAsNoPaginationAnchor]);
84+
}, [resourceItem, shouldLinkToOldestUnreadReportAction, reportActionID]);
10685

10786
return {
10887
reportActions,

src/hooks/useScrollToEndOnNewMessageReceived.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useEffect, useLayoutEffect, useRef} from 'react';
1+
import {useEffect, useRef} from 'react';
22
import type React from 'react';
33
import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@components/FlatList/hooks/useFlatListScrollKey';
44
import usePrevious from './usePrevious';
@@ -43,22 +43,8 @@ function useScrollToEndOnNewMessageReceived({
4343
}: UseScrollToEndOnPaginationMergeParams) {
4444
const previousLastIndex = useRef(lastActionID);
4545
const reportActionSize = useRef(visibleActionsLength);
46-
const previousResetKeyRef = useRef<unknown>(undefined);
4746
const prevHasNewestReportAction = usePrevious(hasNewestReportAction);
4847

49-
// When the hook is used across report navigations, baselines from the previous report must not drive scroll logic.
50-
useLayoutEffect(() => {
51-
if (resetKey === undefined) {
52-
return;
53-
}
54-
if (previousResetKeyRef.current === resetKey) {
55-
return;
56-
}
57-
previousResetKeyRef.current = resetKey;
58-
previousLastIndex.current = lastActionID;
59-
reportActionSize.current = visibleActionsLength;
60-
}, [resetKey, lastActionID, visibleActionsLength]);
61-
6248
useEffect(() => {
6349
const didListSizeChange = sizeChangeType === 'grewFromReportActions' ? reportActionSize.current > (reportActionsLength ?? 0) : reportActionSize.current !== visibleActionsLength;
6450

src/libs/API/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,9 @@ function paginate<TRequestType extends ApiRequestType, TCommand extends CommandO
291291
const request: PaginatedRequest<TKey> = {
292292
...prepareRequest(command, type, apiCommandParameters, onyxData, conflictResolver),
293293
...config,
294-
isPaginated: true,
294+
...{
295+
isPaginated: true,
296+
},
295297
};
296298

297299
switch (type) {

src/libs/Middleware/Pagination.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@ import type {OnyxCollection, OnyxKey} from 'react-native-onyx';
33
import Onyx from 'react-native-onyx';
44
import type {ApiCommand} from '@libs/API/types';
55
import Log from '@libs/Log';
6-
import {mergeAndSortContinuousPages, mergePagesByIDOverlap} from '@libs/PaginationUtils';
6+
import PaginationUtils from '@libs/PaginationUtils';
77
import CONST from '@src/CONST';
88
import type {OnyxCollectionKey, OnyxPagesKey, OnyxValues} from '@src/ONYXKEYS';
99
import ONYXKEYS from '@src/ONYXKEYS';
1010
import type {Request} from '@src/types/onyx';
11-
import type Pages from '@src/types/onyx/Pages';
1211
import type {AnyOnyxUpdate, PaginatedRequest} from '@src/types/onyx/Request';
1312
import type Middleware from './types';
1413

@@ -125,18 +124,14 @@ const Pagination: Middleware = (requestResponse, request) => {
125124
const sortedAllItems = sortItems(allItems, resourceID);
126125

127126
const pagesCollections = pages.get(pageCollectionKey) ?? {};
128-
const existingPages: Pages = pagesCollections[pageKey] ?? [];
129-
130-
const isMiddleInitialSlice = type === 'initial' && !cursorID && response.hasNewerActions === true && response.hasOlderActions === true;
127+
const existingPages = pagesCollections[pageKey] ?? [];
131128

132129
// Only strip PAGINATION_START_ID from cached pages when the server explicitly confirms newer actions exist.
133130
// Some commands (e.g. GetOlderActions) don't return hasNewerActions at all — in that case, preserve the existing boundary.
134131
const shouldStripStartMarker = response.hasNewerActions === true;
135132
const sanitizedExistingPages = shouldStripStartMarker ? existingPages.map((page) => page.filter((id) => id !== CONST.PAGINATION_START_ID)) : existingPages;
136133

137-
const mergedPages: Pages = isMiddleInitialSlice
138-
? mergePagesByIDOverlap(sortedAllItems, [...sanitizedExistingPages, newPage], getItemID)
139-
: mergeAndSortContinuousPages(sortedAllItems, [...sanitizedExistingPages, newPage], getItemID);
134+
const mergedPages = PaginationUtils.mergeAndSortContinuousPages(sortedAllItems, [...sanitizedExistingPages, newPage], getItemID);
140135

141136
(response.onyxData as AnyOnyxUpdate[]).push({
142137
key: pageKey,

0 commit comments

Comments
 (0)