diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f67878a71fe0..def420bba7ff 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1,11 +1,16 @@ +import type {RouteProp} from '@react-navigation/native'; import fastMerge from 'expensify-common/lib/fastMerge'; import _ from 'lodash'; import lodashFindLast from 'lodash/findLast'; +import {useCallback, useLayoutEffect, useMemo, useRef, useState} from 'react'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import getInitialPaginationSize from '@pages/home/report/getInitialPaginationSize'; 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 type { ActionName, ChangeLog, @@ -26,6 +31,7 @@ import isReportMessageAttachment from './isReportMessageAttachment'; import * as Localize from './Localize'; import Log from './Log'; import type {MessageElementBase, MessageTextElement} from './MessageElement'; +import type {CentralPaneNavigatorParamList} from './Navigation/types'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import type {OptimisticIOUReportAction} from './ReportUtils'; @@ -575,9 +581,7 @@ function getSortedReportActionsForDisplay(reportActions: ReportActions | null | } if (shouldIncludeInvisibleActions) { - filteredReportActions = Object.values(reportActions).filter( - (action): action is ReportAction => !(action?.originalMessage as OriginalMessageActionableMentionWhisper['originalMessage'])?.resolution, - ); + filteredReportActions = Object.values(reportActions); } else { filteredReportActions = Object.entries(reportActions) .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) @@ -976,6 +980,98 @@ function getReportActionMessageText(reportAction: OnyxEntry | Empt return reportAction?.message?.reduce((acc, curr) => `${acc}${curr.text}`, '') ?? ''; } +let listIDCount = Math.round(Math.random() * 100); +/** + * usePaginatedReportActionList manages the logic for handling a list of messages with pagination and dynamic loading. + * It determines the part of the message array to display ('visibleReportActions') based on the current linked message, + * and manages pagination through 'handleReportActionPagination' function. + * + * linkedID - ID of the linked message used for initial focus. + * allReportActions - Array of messages. + * fetchNewerReportActions - Function to fetch more messages. + * route - Current route, used to reset states on route change. + * isLoading - Loading state indicator. + * triggerListID - Used to trigger a listID change. + * returns {object} An object containing the sliced message array, the pagination function, + * index of the linked message, and a unique list ID. + */ +const usePaginatedReportActionList = ( + linkedID: string, + localAllReportActions: OnyxTypes.ReportAction[], + fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, + route: RouteProp, + isLoading: boolean, + triggerListID: boolean, +) => { + // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest, we don't maintain their position and instead trigger a recalculation of their positioning in the list. + // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned + const [currentReportActionID, setCurrentReportActionID] = useState(''); + const isFirstLinkedActionRender = useRef(true); + + useLayoutEffect(() => { + setCurrentReportActionID(''); + }, [route]); + + const listID = useMemo(() => { + isFirstLinkedActionRender.current = true; + listIDCount += 1; + return listIDCount; + // This needs to be triggered with each navigation to a comment. It happens when the route is changed. triggerListID is needed to trigger a listID change after initial mounting. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [route, triggerListID]); + + const index = useMemo(() => { + if (!linkedID || isLoading) { + return -1; + } + + return localAllReportActions.findIndex((obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? linkedID : currentReportActionID)); + }, [localAllReportActions, currentReportActionID, linkedID, isLoading]); + + const visibleReportActions = useMemo(() => { + if (!linkedID) { + return localAllReportActions; + } + if (isLoading || index === -1) { + return []; + } + + if (isFirstLinkedActionRender.current) { + return localAllReportActions.slice(index, localAllReportActions.length); + } + const paginationSize = getInitialPaginationSize(); + const newStartIndex = index >= paginationSize ? index - paginationSize : 0; + return newStartIndex ? localAllReportActions.slice(newStartIndex, localAllReportActions.length) : localAllReportActions; + // currentReportActionID is needed to trigger batching once the report action has been positioned + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [linkedID, localAllReportActions, index, isLoading, currentReportActionID]); + + const hasMoreCached = visibleReportActions.length < localAllReportActions?.length; + const newestReportAction = visibleReportActions?.[0]; + + const handleReportActionPagination = useCallback( + ({firstReportActionID}: {firstReportActionID: string}) => { + // This function is a placeholder as the actual pagination is handled by visibleReportActions + if (!hasMoreCached) { + isFirstLinkedActionRender.current = false; + fetchNewerReportActions(newestReportAction); + } + if (isFirstLinkedActionRender.current) { + isFirstLinkedActionRender.current = false; + } + setCurrentReportActionID(firstReportActionID); + }, + [fetchNewerReportActions, hasMoreCached, newestReportAction], + ); + + return { + visibleReportActions, + loadMoreReportActionsHandler: handleReportActionPagination, + linkedIdIndex: index, + listID, + }; +}; + export { extractLinksFromMessageHtml, getAllReportActions, @@ -1033,6 +1129,7 @@ export { isCurrentActionUnread, isActionableJoinRequest, isActionableJoinRequestPending, + usePaginatedReportActionList, }; export type {LastVisibleMessage}; diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index d046bce95d2b..a216aaeb2dc1 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -334,7 +334,7 @@ function ReportScreen({ return; } - // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that + // It is possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that // is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done. // If it doesn't exist, then we fetch the report from the API. if (report.reportID && report.reportID === reportIDFromRoute && !reportMetadata?.isLoadingInitialReportActions) { diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 403855fede39..6fd5c5ab52b8 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -1,7 +1,7 @@ import type {RouteProp} from '@react-navigation/native'; import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashIsEqual from 'lodash/isEqual'; -import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; import type {LayoutChangeEvent} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -25,7 +25,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import getInitialPaginationSize from './getInitialPaginationSize'; import PopoverReactionList from './ReactionList/PopoverReactionList'; import ReportActionsList from './ReportActionsList'; @@ -60,98 +59,6 @@ type ReportActionsViewProps = ReportActionsViewOnyxProps & { const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 120; const SPACER = 16; -let listIDCount = Math.round(Math.random() * 100); - -/** - * usePaginatedReportActionList manages the logic for handling a list of messages with pagination and dynamic loading. - * It determines the part of the message array to display ('visibleReportActions') based on the current linked message, - * and manages pagination through 'handleReportActionPagination' function. - * - * linkedID - ID of the linked message used for initial focus. - * allReportActions - Array of messages. - * fetchNewerReportActions - Function to fetch more messages. - * route - Current route, used to reset states on route change. - * isLoading - Loading state indicator. - * triggerListID - Used to trigger a listID change. - * returns {object} An object containing the sliced message array, the pagination function, - * index of the linked message, and a unique list ID. - */ -const usePaginatedReportActionList = ( - linkedID: string, - allReportActions: OnyxTypes.ReportAction[], - fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, - route: RouteProp, - isLoading: boolean, - triggerListID: boolean, -) => { - // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest, we don't maintain their position and instead trigger a recalculation of their positioning in the list. - // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned - const [currentReportActionID, setCurrentReportActionID] = useState(''); - const isFirstLinkedActionRender = useRef(true); - - useLayoutEffect(() => { - setCurrentReportActionID(''); - }, [route]); - - const listID = useMemo(() => { - isFirstLinkedActionRender.current = true; - listIDCount += 1; - return listIDCount; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [route, triggerListID]); - - const index = useMemo(() => { - if (!linkedID || isLoading) { - return -1; - } - - return allReportActions.findIndex((obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? linkedID : currentReportActionID)); - }, [allReportActions, currentReportActionID, linkedID, isLoading]); - - const visibleReportActions = useMemo(() => { - if (!linkedID) { - return allReportActions; - } - if (isLoading || index === -1) { - return []; - } - - if (isFirstLinkedActionRender.current) { - return allReportActions.slice(index, allReportActions.length); - } - const paginationSize = getInitialPaginationSize(); - const newStartIndex = index >= paginationSize ? index - paginationSize : 0; - return newStartIndex ? allReportActions.slice(newStartIndex, allReportActions.length) : allReportActions; - // currentReportActionID is needed to trigger batching once the report action has been positioned - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [linkedID, allReportActions, index, isLoading, currentReportActionID]); - - const hasMoreCached = visibleReportActions.length < allReportActions.length; - const newestReportAction = visibleReportActions?.[0]; - - const handleReportActionPagination = useCallback( - ({firstReportActionID}: {firstReportActionID: string}) => { - // This function is a placeholder as the actual pagination is handled by visibleReportActions - if (!hasMoreCached) { - isFirstLinkedActionRender.current = false; - fetchNewerReportActions(newestReportAction); - } - if (isFirstLinkedActionRender.current) { - isFirstLinkedActionRender.current = false; - } - setCurrentReportActionID(firstReportActionID); - }, - [fetchNewerReportActions, hasMoreCached, newestReportAction], - ); - - return { - visibleReportActions, - loadMoreReportActionsHandler: handleReportActionPagination, - linkedIdIndex: index, - listID, - }; -}; - function ReportActionsView({ report, session, @@ -202,7 +109,7 @@ function ReportActionsView({ loadMoreReportActionsHandler, linkedIdIndex, listID, - } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, isLoadingInitialReportActions); + } = ReportActionsUtils.usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, isLoadingInitialReportActions); const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); const hasCachedActions = useInitialValue(() => reportActions.length > 0); const hasNewestReportAction = reportActions[0]?.created === report.lastVisibleActionCreated; @@ -453,7 +360,6 @@ function ReportActionsView({ mostRecentIOUReportActionID={mostRecentIOUReportActionID} loadOlderChats={loadOlderChats} loadNewerChats={loadNewerChats} - // isLinkingLoader={!!reportActionID && isLoadingInitialReportActions} isLoadingInitialReportActions={isLoadingInitialReportActions} isLoadingOlderReportActions={isLoadingOlderReportActions} isLoadingNewerReportActions={isLoadingNewerReportActions} diff --git a/src/pages/home/report/getInitialNumReportActionsToRender/index.ts b/src/pages/home/report/getInitialNumReportActionsToRender/index.ts index 68ff8c4cab3f..cb1f0dfdcded 100644 --- a/src/pages/home/report/getInitialNumReportActionsToRender/index.ts +++ b/src/pages/home/report/getInitialNumReportActionsToRender/index.ts @@ -1,5 +1,7 @@ +const DEFAULT_NUM_TO_RENDER = 50; + function getInitialNumToRender(numToRender: number): number { // For web and desktop environments, it's crucial to set this value equal to or higher than the maxToRenderPerBatch setting. If it's set lower, the 'onStartReached' event will be triggered excessively, every time an additional item enters the virtualized list. - return Math.max(numToRender, 50); + return Math.max(numToRender, DEFAULT_NUM_TO_RENDER); } export default getInitialNumToRender;