diff --git a/src/pages/home/report/getInitialPaginationSize/index.native.ts b/src/components/InvertedFlatList/BaseInvertedFlatList/getInitialPaginationSize/index.native.ts similarity index 100% rename from src/pages/home/report/getInitialPaginationSize/index.native.ts rename to src/components/InvertedFlatList/BaseInvertedFlatList/getInitialPaginationSize/index.native.ts diff --git a/src/pages/home/report/getInitialPaginationSize/index.ts b/src/components/InvertedFlatList/BaseInvertedFlatList/getInitialPaginationSize/index.ts similarity index 100% rename from src/pages/home/report/getInitialPaginationSize/index.ts rename to src/components/InvertedFlatList/BaseInvertedFlatList/getInitialPaginationSize/index.ts diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx index 5c48652f8cc5..c09b20b676df 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx @@ -1,16 +1,53 @@ import type {ForwardedRef} from 'react'; -import React, {forwardRef, useMemo} from 'react'; -import type {FlatListProps, FlatList as RNFlatList, ScrollViewProps} from 'react-native'; +import React, {forwardRef, useCallback, useMemo, useState} from 'react'; +import type {FlatListProps, ListRenderItem, ListRenderItemInfo, FlatList as RNFlatList, ScrollViewProps} from 'react-native'; import FlatList from '@components/FlatList'; +import usePrevious from '@hooks/usePrevious'; +import getInitialPaginationSize from './getInitialPaginationSize'; -type BaseInvertedFlatListProps = FlatListProps & { +type BaseInvertedFlatListProps = Omit, 'data' | 'renderItem'> & { shouldEnableAutoScrollToTopThreshold?: boolean; + data: T[]; + renderItem: ListRenderItem; }; const AUTOSCROLL_TO_TOP_THRESHOLD = 250; function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { - const {shouldEnableAutoScrollToTopThreshold, ...rest} = props; + const {shouldEnableAutoScrollToTopThreshold, initialScrollIndex, data, onStartReached, renderItem, ...rest} = props; + + // `initialScrollIndex` doesn't work properly with FlatList, this uses an alternative approach to achieve the same effect. + // What we do is start rendering the list from `initialScrollIndex` and then whenever we reach the start we render more + // previous items, until everything is rendered. + const [currentDataIndex, setCurrentDataIndex] = useState(initialScrollIndex ?? 0); + const displayedData = useMemo(() => { + if (currentDataIndex > 0) { + return data.slice(currentDataIndex); + } + return data; + }, [data, currentDataIndex]); + const isLoadingData = data.length > displayedData.length; + const wasLoadingData = usePrevious(isLoadingData); + const dataIndexDifference = data.length - displayedData.length; + + const handleStartReached = useCallback( + (info: {distanceFromStart: number}) => { + if (isLoadingData) { + setCurrentDataIndex((prevIndex) => prevIndex - getInitialPaginationSize); + } else { + onStartReached?.(info); + } + }, + [onStartReached, isLoadingData], + ); + + const handleRenderItem = useCallback( + ({item, index, separators}: ListRenderItemInfo) => { + // Adjust the index passed here so it matches the original data. + return renderItem({item, index: index + dataIndexDifference, separators}); + }, + [renderItem, dataIndexDifference], + ); const maintainVisibleContentPosition = useMemo(() => { const config: ScrollViewProps['maintainVisibleContentPosition'] = { @@ -18,12 +55,12 @@ function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: Forwa minIndexForVisible: 1, }; - if (shouldEnableAutoScrollToTopThreshold) { + if (shouldEnableAutoScrollToTopThreshold && !isLoadingData && !wasLoadingData) { config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD; } return config; - }, [shouldEnableAutoScrollToTopThreshold]); + }, [shouldEnableAutoScrollToTopThreshold, isLoadingData, wasLoadingData]); return ( (props: BaseInvertedFlatListProps, ref: Forwa ref={ref} maintainVisibleContentPosition={maintainVisibleContentPosition} inverted + data={displayedData} + onStartReached={handleStartReached} + renderItem={handleRenderItem} /> ); } @@ -41,3 +81,5 @@ BaseInvertedFlatList.displayName = 'BaseInvertedFlatList'; export default forwardRef(BaseInvertedFlatList); export {AUTOSCROLL_TO_TOP_THRESHOLD}; + +export type {BaseInvertedFlatListProps}; diff --git a/src/components/InvertedFlatList/index.native.tsx b/src/components/InvertedFlatList/index.native.tsx index 70cabf5a536a..68110627c3b6 100644 --- a/src/components/InvertedFlatList/index.native.tsx +++ b/src/components/InvertedFlatList/index.native.tsx @@ -1,10 +1,11 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef} from 'react'; -import type {FlatList, FlatListProps} from 'react-native'; +import type {FlatList} from 'react-native'; import BaseInvertedFlatList from './BaseInvertedFlatList'; +import type {BaseInvertedFlatListProps} from './BaseInvertedFlatList'; import CellRendererComponent from './CellRendererComponent'; -function BaseInvertedFlatListWithRef(props: FlatListProps, ref: ForwardedRef) { +function BaseInvertedFlatListWithRef(props: BaseInvertedFlatListProps, ref: ForwardedRef) { return ( = FlatListProps & { - shouldEnableAutoScrollToTopThreshold?: boolean; -}; - // This is adapted from https://codesandbox.io/s/react-native-dsyse // It's a HACK alert since FlatList has inverted scrolling on web -function InvertedFlatList({onScroll: onScrollProp = () => {}, ...props}: InvertedFlatListProps, ref: ForwardedRef) { +function InvertedFlatList({onScroll: onScrollProp = () => {}, ...props}: BaseInvertedFlatListProps, ref: ForwardedRef) { const lastScrollEvent = useRef(null); const scrollEndTimeout = useRef(null); const updateInProgress = useRef(false); diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 58e7fe319359..621b67e88ca1 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -46,9 +46,6 @@ type ReportActionsListProps = { /** The transaction thread report associated with the current report, if any */ transactionThreadReport: OnyxEntry; - /** Array of report actions for the current report */ - reportActions: OnyxTypes.ReportAction[]; - /** The report's parentReportAction */ parentReportAction: OnyxEntry; @@ -128,7 +125,6 @@ const onScrollToIndexFailed = () => {}; function ReportActionsList({ report, transactionThreadReport, - reportActions = [], parentReportAction, isLoadingInitialReportActions = false, isLoadingOlderReportActions = false, @@ -582,7 +578,7 @@ function ReportActionsList({ ({item: reportAction, index}: ListRenderItemInfo) => ( { + if (!linkedReportActionID) { + return -1; + } + return sortedVisibleReportActions.findIndex((obj) => String(obj.reportActionID) === linkedReportActionID); + }, [sortedVisibleReportActions, linkedReportActionID]); + // When performing comment linking, initially 25 items are added to the list. Subsequent fetches add 15 items from the cache or 50 items from the server. // This is to ensure that the user is able to see the 'scroll to newer comments' button when they do comment linking and have not reached the end of the list yet. const canScrollToNewerComments = !isLoadingInitialReportActions && !hasNewestReportAction && sortedReportActions.length > 25 && !isLastPendingActionIsDelete; @@ -740,6 +743,7 @@ function ReportActionsList({ extraData={extraData} key={listID} shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScrollToTopThreshold} + initialScrollIndex={indexOfLinkedAction} /> diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 8896611905ca..76adadd27dd1 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -29,7 +29,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'; import UserTypingEventListener from './UserTypingEventListener'; @@ -101,9 +100,6 @@ function ReportActionsView({ const didLoadNewerChats = useRef(false); const {isOffline} = useNetwork(); - // 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); const network = useNetwork(); @@ -142,8 +138,6 @@ function ReportActionsView({ // eslint-disable-next-line react-compiler/react-compiler listOldID = newID; - setCurrentReportActionID(''); - return newID; // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [route, reportActionID]); @@ -210,7 +204,7 @@ function ReportActionsView({ // Get a sorted array of reportActions for both the current report and the transaction thread report associated with this report (if there is one) // so that we display transaction-level and report-level report actions in order in the one-transaction view - const combinedReportActions = useMemo( + const reportActions = useMemo( () => ReportActionsUtils.getCombinedReportActions(reportActionsToDisplay, transactionThreadReportID ?? null, transactionThreadReportActions ?? []), [reportActionsToDisplay, transactionThreadReportActions, transactionThreadReportID], ); @@ -223,31 +217,6 @@ function ReportActionsView({ [allReportActions, transactionThreadReportActions, transactionThreadReport?.parentReportActionID], ); - const indexOfLinkedAction = useMemo(() => { - if (!reportActionID) { - return -1; - } - return combinedReportActions.findIndex((obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? reportActionID : currentReportActionID)); - }, [combinedReportActions, currentReportActionID, reportActionID]); - - const reportActions = useMemo(() => { - if (!reportActionID) { - return combinedReportActions; - } - if (indexOfLinkedAction === -1) { - return []; - } - - if (isFirstLinkedActionRender.current) { - return combinedReportActions.slice(indexOfLinkedAction); - } - const paginationSize = getInitialPaginationSize; - return combinedReportActions.slice(Math.max(indexOfLinkedAction - paginationSize, 0)); - - // currentReportActionID is needed to trigger batching once the report action has been positioned - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [reportActionID, combinedReportActions, indexOfLinkedAction, currentReportActionID]); - const reportActionIDMap = useMemo(() => { const reportActionIDs = allReportActions.map((action) => action.reportActionID); return reportActions.map((action) => ({ @@ -256,33 +225,6 @@ function ReportActionsView({ })); }, [allReportActions, reportID, transactionThreadReport, reportActions]); - /** - * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently - * displaying. - */ - const fetchNewerAction = useCallback( - (newestReportAction: OnyxTypes.ReportAction) => { - if (!hasNewerActions || isLoadingNewerReportActions || isLoadingInitialReportActions || (reportActionID && isOffline)) { - return; - } - - // If this is a one transaction report, ensure we load newer actions for both this report and the report associated with the transaction - if (!isEmptyObject(transactionThreadReport)) { - // Get newer actions based on the newest reportAction for the current report - const newestActionCurrentReport = reportActionIDMap.find((item) => item.reportID === reportID); - Report.getNewerActions(newestActionCurrentReport?.reportID ?? '-1', newestActionCurrentReport?.reportActionID ?? '-1'); - - // Get newer actions based on the newest reportAction for the transaction thread report - const newestActionTransactionThreadReport = reportActionIDMap.find((item) => item.reportID === transactionThreadReport.reportID); - Report.getNewerActions(newestActionTransactionThreadReport?.reportID ?? '-1', newestActionTransactionThreadReport?.reportActionID ?? '-1'); - } else { - Report.getNewerActions(reportID, newestReportAction.reportActionID); - } - }, - [isLoadingNewerReportActions, isLoadingInitialReportActions, reportActionID, isOffline, transactionThreadReport, reportActionIDMap, reportID, hasNewerActions], - ); - - const hasMoreCached = reportActions.length < combinedReportActions.length; const newestReportAction = useMemo(() => reportActions?.at(0), [reportActions]); const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); const hasCachedActionOnFirstRender = useInitialValue(() => reportActions.length > 0); @@ -315,23 +257,6 @@ function ReportActionsView({ contentListHeight.current = h; }, []); - const handleReportActionPagination = useCallback( - ({firstReportActionID}: {firstReportActionID: string}) => { - // This function is a placeholder as the actual pagination is handled by visibleReportActions - if (!hasMoreCached && !hasNewestReportAction) { - isFirstLinkedActionRender.current = false; - if (newestReportAction) { - fetchNewerAction(newestReportAction); - } - } - if (isFirstLinkedActionRender.current) { - isFirstLinkedActionRender.current = false; - } - setCurrentReportActionID(firstReportActionID); - }, - [fetchNewerAction, hasMoreCached, newestReportAction, hasNewestReportAction], - ); - /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. @@ -389,32 +314,46 @@ function ReportActionsView({ !force && (!reportActionID || !isFocused || - (isLoadingInitialReportActions && !hasMoreCached) || + !newestReportAction || + isLoadingInitialReportActions || isLoadingNewerReportActions || + !hasNewerActions || + isOffline || // If there was an error only try again once on initial mount. We should also still load // more in case we have cached messages. - (!hasMoreCached && didLoadNewerChats.current && hasLoadingNewerReportActionsError) || - newestReportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) + (didLoadNewerChats.current && hasLoadingNewerReportActionsError) || + newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) ) { return; } didLoadNewerChats.current = true; - if ((reportActionID && indexOfLinkedAction > -1) || !reportActionID) { - handleReportActionPagination({firstReportActionID: newestReportAction?.reportActionID ?? '-1'}); + // If this is a one transaction report, ensure we load newer actions for both this report and the report associated with the transaction + if (!isEmptyObject(transactionThreadReport)) { + // Get newer actions based on the newest reportAction for the current report + const newestActionCurrentReport = reportActionIDMap.find((item) => item.reportID === reportID); + Report.getNewerActions(newestActionCurrentReport?.reportID ?? '-1', newestActionCurrentReport?.reportActionID ?? '-1'); + + // Get newer actions based on the newest reportAction for the transaction thread report + const newestActionTransactionThreadReport = reportActionIDMap.find((item) => item.reportID === transactionThreadReport.reportID); + Report.getNewerActions(newestActionTransactionThreadReport?.reportID ?? '-1', newestActionTransactionThreadReport?.reportActionID ?? '-1'); + } else if (newestReportAction) { + Report.getNewerActions(reportID, newestReportAction.reportActionID); } }, [ - isLoadingInitialReportActions, - isLoadingNewerReportActions, reportActionID, - indexOfLinkedAction, - handleReportActionPagination, - newestReportAction, isFocused, + newestReportAction, + isLoadingInitialReportActions, + isLoadingNewerReportActions, + hasNewerActions, + isOffline, hasLoadingNewerReportActionsError, - hasMoreCached, + transactionThreadReport, + reportActionIDMap, + reportID, ], ); @@ -477,7 +416,6 @@ function ReportActionsView({