Skip to content

Commit de00d0b

Browse files
Use pagination metadata for report actions
1 parent 3047c1b commit de00d0b

File tree

10 files changed

+146
-67
lines changed

10 files changed

+146
-67
lines changed

src/hooks/usePaginatedReportActions.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {useMemo} from 'react';
22
import {useOnyx} from 'react-native-onyx';
33
import PaginationUtils from '@libs/PaginationUtils';
44
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
5+
import CONST from '@src/CONST';
56
import ONYXKEYS from '@src/ONYXKEYS';
67

78
/**
@@ -18,9 +19,13 @@ function usePaginatedReportActions(reportID?: string, reportActionID?: string) {
1819
});
1920
const [reportActionPages] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportIDWithDefault}`);
2021

21-
const reportActions = useMemo(() => {
22+
const {
23+
data: reportActions,
24+
hasNextPage,
25+
hasPreviousPage,
26+
} = useMemo(() => {
2227
if (!sortedAllReportActions?.length) {
23-
return [];
28+
return {data: [], hasNextPage: false, hasPreviousPage: false};
2429
}
2530
return PaginationUtils.getContinuousChain(sortedAllReportActions, reportActionPages ?? [], (reportAction) => reportAction.reportActionID, reportActionID);
2631
}, [reportActionID, reportActionPages, sortedAllReportActions]);
@@ -34,6 +39,8 @@ function usePaginatedReportActions(reportID?: string, reportActionID?: string) {
3439
reportActions,
3540
linkedAction,
3641
sortedAllReportActions,
42+
hasOlderActions: hasNextPage,
43+
hasNewerActions: hasPreviousPage,
3744
};
3845
}
3946

src/libs/Middleware/Pagination.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ type PaginationCommonConfig<TResourceKey extends OnyxCollectionKey = OnyxCollect
1717
pageCollectionKey: TPageKey;
1818
sortItems: (items: OnyxValues[TResourceKey]) => Array<PagedResource<TResourceKey>>;
1919
getItemID: (item: PagedResource<TResourceKey>) => string;
20-
isLastItem: (item: PagedResource<TResourceKey>) => boolean;
2120
};
2221

2322
type PaginationConfig<TResourceKey extends OnyxCollectionKey, TPageKey extends OnyxPagesKey> = PaginationCommonConfig<TResourceKey, TPageKey> & {
@@ -85,7 +84,7 @@ const Pagination: Middleware = (requestResponse, request) => {
8584
return requestResponse;
8685
}
8786

88-
const {resourceCollectionKey, pageCollectionKey, sortItems, getItemID, isLastItem, type} = paginationConfig;
87+
const {resourceCollectionKey, pageCollectionKey, sortItems, getItemID, type} = paginationConfig;
8988
const {resourceID, cursorID} = request;
9089
return requestResponse.then((response) => {
9190
if (!response?.onyxData) {
@@ -106,12 +105,10 @@ const Pagination: Middleware = (requestResponse, request) => {
106105

107106
const newPage = sortedPageItems.map((item) => getItemID(item));
108107

109-
// Detect if we are at the start of the list. This will always be the case for the initial request with no cursor.
110-
// For previous requests we check that no new data is returned. Ideally the server would return that info.
111-
if ((type === 'initial' && !cursorID) || (type === 'next' && newPage.length === 1 && newPage[0] === cursorID)) {
108+
if (response.hasNewerActions === false) {
112109
newPage.unshift(CONST.PAGINATION_START_ID);
113110
}
114-
if (isLastItem(sortedPageItems[sortedPageItems.length - 1])) {
111+
if (response.hasOlderActions === false) {
115112
newPage.push(CONST.PAGINATION_END_ID);
116113
}
117114

src/libs/PaginationUtils.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,19 @@ function mergeAndSortContinuousPages<TResource>(sortedItems: TResource[], pages:
156156

157157
/**
158158
* Returns the page of items that contains the item with the given ID, or the first page if null.
159+
* Also returns whether next / previous pages can be fetched.
159160
* See unit tests for example of inputs and expected outputs.
160161
*
161162
* Note: sortedItems should be sorted in descending order.
162163
*/
163-
function getContinuousChain<TResource>(sortedItems: TResource[], pages: Pages, getID: (item: TResource) => string, id?: string): TResource[] {
164+
function getContinuousChain<TResource>(
165+
sortedItems: TResource[],
166+
pages: Pages,
167+
getID: (item: TResource) => string,
168+
id?: string,
169+
): {data: TResource[]; hasNextPage: boolean; hasPreviousPage: boolean} {
164170
if (pages.length === 0) {
165-
return id ? [] : sortedItems;
171+
return {data: id ? [] : sortedItems, hasNextPage: false, hasPreviousPage: false};
166172
}
167173

168174
const pagesWithIndexes = getPagesWithIndexes(sortedItems, pages, getID);
@@ -174,22 +180,26 @@ function getContinuousChain<TResource>(sortedItems: TResource[], pages: Pages, g
174180

175181
// If we are linking to an action that doesn't exist in Onyx, return an empty array
176182
if (index === -1) {
177-
return [];
183+
return {data: [], hasNextPage: false, hasPreviousPage: false};
178184
}
179185

180186
const linkedPage = pagesWithIndexes.find((pageIndex) => index >= pageIndex.firstIndex && index <= pageIndex.lastIndex);
181187

182188
// If we are linked to an action in a gap return it by itself
183189
if (!linkedPage) {
184-
return [sortedItems[index]];
190+
return {data: [sortedItems[index]], hasNextPage: false, hasPreviousPage: false};
185191
}
186192

187193
page = linkedPage;
188194
} else {
189195
page = pagesWithIndexes[0];
190196
}
191197

192-
return page ? sortedItems.slice(page.firstIndex, page.lastIndex + 1) : sortedItems;
198+
if (!page) {
199+
return {data: sortedItems, hasNextPage: false, hasPreviousPage: false};
200+
}
201+
202+
return {data: sortedItems.slice(page.firstIndex, page.lastIndex + 1), hasNextPage: page.lastID !== CONST.PAGINATION_END_ID, hasPreviousPage: page.firstID !== CONST.PAGINATION_START_ID};
193203
}
194204

195205
export default {mergeAndSortContinuousPages, getContinuousChain};

src/libs/actions/Report.ts

-1
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,6 @@ registerPaginationConfig({
276276
pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES,
277277
sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true),
278278
getItemID: (reportAction) => reportAction.reportActionID,
279-
isLastItem: (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED,
280279
});
281280

282281
function clearGroupChat() {

src/pages/home/ReportScreen.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro
222222
const [isLinkingToMessage, setIsLinkingToMessage] = useState(!!reportActionIDFromRoute);
223223

224224
const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: (value) => value?.accountID});
225-
const {reportActions, linkedAction, sortedAllReportActions} = usePaginatedReportActions(reportID, reportActionIDFromRoute);
225+
const {reportActions, linkedAction, sortedAllReportActions, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID, reportActionIDFromRoute);
226226

227227
const [isBannerVisible, setIsBannerVisible] = useState(true);
228228
const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({});
@@ -756,6 +756,8 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro
756756
{!shouldShowSkeleton && report && (
757757
<ReportActionsView
758758
reportActions={reportActions}
759+
hasNewerActions={hasNewerActions}
760+
hasOlderActions={hasOlderActions}
759761
report={report}
760762
parentReportAction={parentReportAction}
761763
isLoadingInitialReportActions={reportMetadata?.isLoadingInitialReportActions}

src/pages/home/report/ReportActionsView.tsx

+20-5
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ type ReportActionsViewProps = {
6262
/** The reportID of the transaction thread report associated with this current report, if any */
6363
// eslint-disable-next-line react/no-unused-prop-types
6464
transactionThreadReportID?: string | null;
65+
66+
/** If the report has newer actions to load */
67+
hasNewerActions: boolean;
68+
69+
/** If the report has older actions to load */
70+
hasOlderActions: boolean;
6571
};
6672

6773
let listOldID = Math.round(Math.random() * 100);
@@ -76,6 +82,8 @@ function ReportActionsView({
7682
isLoadingNewerReportActions = false,
7783
hasLoadingNewerReportActionsError = false,
7884
transactionThreadReportID,
85+
hasNewerActions,
86+
hasOlderActions,
7987
}: ReportActionsViewProps) {
8088
useCopySelectionHelper();
8189
const reactionListRef = useContext(ReactionListContext);
@@ -253,7 +261,7 @@ function ReportActionsView({
253261
*/
254262
const fetchNewerAction = useCallback(
255263
(newestReportAction: OnyxTypes.ReportAction) => {
256-
if (isLoadingNewerReportActions || isLoadingInitialReportActions || (reportActionID && isOffline)) {
264+
if (!hasNewerActions || isLoadingNewerReportActions || isLoadingInitialReportActions || (reportActionID && isOffline)) {
257265
return;
258266
}
259267

@@ -270,7 +278,7 @@ function ReportActionsView({
270278
Report.getNewerActions(reportID, newestReportAction.reportActionID);
271279
}
272280
},
273-
[isLoadingNewerReportActions, isLoadingInitialReportActions, reportActionID, isOffline, transactionThreadReport, reportActionIDMap, reportID],
281+
[isLoadingNewerReportActions, isLoadingInitialReportActions, reportActionID, isOffline, transactionThreadReport, reportActionIDMap, reportID, hasNewerActions],
274282
);
275283

276284
const hasMoreCached = reportActions.length < combinedReportActions.length;
@@ -279,7 +287,6 @@ function ReportActionsView({
279287
const hasCachedActionOnFirstRender = useInitialValue(() => reportActions.length > 0);
280288
const hasNewestReportAction = reportActions[0]?.created === report.lastVisibleActionCreated || reportActions[0]?.created === transactionThreadReport?.lastVisibleActionCreated;
281289
const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]);
282-
const hasCreatedAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED;
283290

284291
useEffect(() => {
285292
const wasLoginChangedDetected = prevAuthTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS && !session?.authTokenType;
@@ -341,7 +348,7 @@ function ReportActionsView({
341348
}
342349

343350
// Don't load more chats if we're already at the beginning of the chat history
344-
if (!oldestReportAction || hasCreatedAction) {
351+
if (!oldestReportAction || !hasOlderActions) {
345352
return;
346353
}
347354

@@ -365,11 +372,11 @@ function ReportActionsView({
365372
isLoadingOlderReportActions,
366373
isLoadingInitialReportActions,
367374
oldestReportAction,
368-
hasCreatedAction,
369375
reportID,
370376
reportActionIDMap,
371377
transactionThreadReport,
372378
hasLoadingOlderReportActionsError,
379+
hasOlderActions,
373380
],
374381
);
375382

@@ -522,6 +529,14 @@ function arePropsEqual(oldProps: ReportActionsViewProps, newProps: ReportActions
522529
return false;
523530
}
524531

532+
if (oldProps.hasNewerActions !== newProps.hasNewerActions) {
533+
return false;
534+
}
535+
536+
if (oldProps.hasOlderActions !== newProps.hasOlderActions) {
537+
return false;
538+
}
539+
525540
return lodashIsEqual(oldProps.report, newProps.report);
526541
}
527542

src/types/onyx/Response.ts

+6
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ type Response = {
6868
/** Base64 key to decrypt messages from Pusher encrypted channels */
6969
// eslint-disable-next-line @typescript-eslint/naming-convention
7070
shared_secret?: string;
71+
72+
/** If there is older data to load for pagination commands */
73+
hasOlderActions?: boolean;
74+
75+
/** If there is newer data to load for pagination commands */
76+
hasNewerActions?: boolean;
7177
};
7278

7379
export default Response;

tests/ui/PaginationTest.tsx

+62-35
Original file line numberDiff line numberDiff line change
@@ -128,47 +128,74 @@ function buildReportComments(count: number, initialID: string, reverse = false)
128128
}
129129

130130
function mockOpenReport(messageCount: number, initialID: string) {
131-
fetchMock.mockAPICommand('OpenReport', ({reportID}) =>
132-
reportID === REPORT_ID
133-
? [
134-
{
135-
onyxMethod: 'merge',
136-
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`,
137-
value: buildReportComments(messageCount, initialID),
138-
},
139-
]
140-
: [],
141-
);
131+
fetchMock.mockAPICommand('OpenReport', ({reportID}) => {
132+
const comments = buildReportComments(messageCount, initialID);
133+
console.log({
134+
onyxData:
135+
reportID === REPORT_ID
136+
? [
137+
{
138+
onyxMethod: 'merge',
139+
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`,
140+
value: comments,
141+
},
142+
]
143+
: [],
144+
hasOlderActions: comments['1'] != null,
145+
hasNewerActions: reportID != null,
146+
});
147+
return {
148+
onyxData:
149+
reportID === REPORT_ID
150+
? [
151+
{
152+
onyxMethod: 'merge',
153+
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`,
154+
value: comments,
155+
},
156+
]
157+
: [],
158+
hasOlderActions: !comments['1'],
159+
hasNewerActions: !!reportID,
160+
};
161+
});
142162
}
143163

144164
function mockGetOlderActions(messageCount: number) {
145-
fetchMock.mockAPICommand('GetOlderActions', ({reportID, reportActionID}) =>
146-
reportID === REPORT_ID
147-
? [
148-
{
149-
onyxMethod: 'merge',
150-
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`,
151-
// The API also returns the action that was requested with the reportActionID.
152-
value: buildReportComments(messageCount + 1, reportActionID),
153-
},
154-
]
155-
: [],
156-
);
165+
fetchMock.mockAPICommand('GetOlderActions', ({reportID, reportActionID}) => {
166+
// The API also returns the action that was requested with the reportActionID.
167+
const comments = buildReportComments(messageCount + 1, reportActionID);
168+
return {
169+
onyxData:
170+
reportID === REPORT_ID
171+
? [
172+
{
173+
onyxMethod: 'merge',
174+
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`,
175+
value: comments,
176+
},
177+
]
178+
: [],
179+
hasOlderActions: comments['1'] != null,
180+
};
181+
});
157182
}
158183

159184
function mockGetNewerActions(messageCount: number) {
160-
fetchMock.mockAPICommand('GetNewerActions', ({reportID, reportActionID}) =>
161-
reportID === REPORT_ID
162-
? [
163-
{
164-
onyxMethod: 'merge',
165-
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`,
166-
// The API also returns the action that was requested with the reportActionID.
167-
value: buildReportComments(messageCount + 1, reportActionID, true),
168-
},
169-
]
170-
: [],
171-
);
185+
fetchMock.mockAPICommand('GetNewerActions', ({reportID, reportActionID}) => ({
186+
onyxData:
187+
reportID === REPORT_ID
188+
? [
189+
{
190+
onyxMethod: 'merge',
191+
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`,
192+
// The API also returns the action that was requested with the reportActionID.
193+
value: buildReportComments(messageCount + 1, reportActionID, true),
194+
},
195+
]
196+
: [],
197+
hasNewerActions: messageCount > 0,
198+
}));
172199
}
173200

174201
/**

0 commit comments

Comments
 (0)