Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import {useIsReportLoadPending} from '@hooks/useInFlightRequests';
import useLoadReportActions from '@hooks/useLoadReportActions';
import useLocalize from '@hooks/useLocalize';
import useMarkAsRead from '@hooks/useMarkAsRead';
import useNetwork from '@hooks/useNetwork';
Expand All @@ -25,12 +24,10 @@ import REPORT_LINK_ROUTE_PARAMS from '@libs/Navigation/reportLinkRouteParams';
import TransitionTracker from '@libs/Navigation/TransitionTracker';
import type {ReportsSplitNavigatorParamList} from '@libs/Navigation/types';
import {isTrackOnboardingChoice} from '@libs/OnboardingUtils';
import {getFilteredReportActionsForReportView, getOneTransactionThreadReportID, hasNextActionMadeBySameActor, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {getFilteredReportActionsForReportView, getOneTransactionThreadReportID, hasNextActionMadeBySameActor} from '@libs/ReportActionsUtils';
import {canUserPerformWriteAction, chatIncludesChronosWithID, getReportLastVisibleActionCreated, isHarvestCreatedExpenseReport, shouldShowMarkAsDone} from '@libs/ReportUtils';
import markOpenReportEnd from '@libs/telemetry/markOpenReportEnd';

import isSearchTopmostFullScreenRoute from '@navigation/helpers/isSearchTopmostFullScreenRoute';

import ConciergeThinkingMessage from '@pages/home/report/ConciergeThinkingMessage';
import {useActionListContext, useActionListRef} from '@pages/inbox/ActionListContext';
import {useAgentZeroStatus} from '@pages/inbox/AgentZeroStatusContext';
Expand All @@ -40,7 +37,7 @@ import {ReportActionPositionContextProvider, ReportActionScrollToNewestContext}
import ReportActionsListItemRenderer from '@pages/inbox/report/ReportActionsListItemRenderer';
import useReportUnreadMessageScrollTracking from '@pages/inbox/report/useReportUnreadMessageScrollTracking';

import {getOlderActions, openReport, subscribeToNewActionEvent} from '@userActions/Report';
import {openReport, subscribeToNewActionEvent} from '@userActions/Report';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -51,7 +48,6 @@ import type * as OnyxTypes from '@src/types/onyx';

import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent} from 'react-native';

/* eslint-disable rulesdir/prefer-early-return */
import {useIsFocused, useRoute} from '@react-navigation/native';
import {guidedSetupAndTourStatusSelector} from '@selectors/Onboarding';
import isEmpty from 'lodash/isEmpty';
Expand All @@ -61,6 +57,7 @@ import {View} from 'react-native';
import MoneyRequestReportEmptyStateView from './MoneyRequestReportEmptyStateView';
import MoneyRequestReportTransactionList from './MoneyRequestReportTransactionList';
import SelectionToolbar from './SelectionToolbar';
import useMoneyRequestReportPagination from './useMoneyRequestReportPagination';
import useMoneyRequestReportVisibleActions from './useMoneyRequestReportVisibleActions';

/**
Expand All @@ -71,10 +68,6 @@ const EmptyParentReportActionForTransactionThread = undefined;
// Amount of time to wait until all list items should be rendered and scrollToEnd will behave well
const DELAY_FOR_SCROLLING_TO_END = 100;

// The server page size for report actions is ~50. Gaps from IOU prioritization only happen
// when the initial load is truncated, so skip backfill for smaller reports.
const BACKFILL_MIN_ACTIONS_THRESHOLD = 50;

type MoneyRequestReportListProps = {
onLayout?: (event: LayoutChangeEvent) => void;
};
Expand Down Expand Up @@ -193,101 +186,16 @@ function MoneyRequestReportActionsListContent({reportIDFromRoute, onLayout}: Mon
const lastVisibleActionCreated = getReportLastVisibleActionCreated(report, transactionThreadReport);
const hasNewestReportAction = lastAction?.created === lastVisibleActionCreated;

const reportActionIDs = useMemo(() => {
return reportActions?.map((action) => action.reportActionID) ?? [];
}, [reportActions]);

const {loadOlderChats, loadNewerChats} = useLoadReportActions({
const {onStartReached, onEndReached} = useMoneyRequestReportPagination({
reportID,
reportActions,
allReportActionIDs: reportActionIDs,
transactionThreadReportID,
hasOlderActions,
hasNewerActions,
newestFetchedReportActionID: reportPaginationState?.newestFetchedReportActionID,
});

const hasFinishedInitialLoad = reportLoadingState?.isLoadingInitialReportActions === false;
const prevNewestFetchedIDRef = useRef<string | undefined>(undefined);
useEffect(() => {
if (hasFinishedInitialLoad && hasNewerActions && reportActions.length > 0 && !isOffline && !reportLoadingState?.isLoadingNewerReportActions) {
// Safety guard: if the cursor hasn't advanced since the last call, the server
// isn't returning new data. Stop to prevent an infinite request loop.
const currentCursor = reportPaginationState?.newestFetchedReportActionID;
if (prevNewestFetchedIDRef.current !== undefined && prevNewestFetchedIDRef.current === currentCursor) {
return;
}
prevNewestFetchedIDRef.current = currentCursor;
loadNewerChats(false);
}
}, [
hasFinishedInitialLoad,
reportActions.length,
hasNewerActions,
isOffline,
reportLoadingState?.isLoadingNewerReportActions,
reportPaginationState?.newestFetchedReportActionID,
loadNewerChats,
]);

// Backfill loop: the backend prioritizes IOU actions in OpenReport/GetNewerActions for money
// request reports, which can leave non-IOU chat messages in a gap between the IOU-biased cursor
// and older messages. After auto-pagination finishes, walk backwards from the IOU cursor using
// getOlderActions. Each response advances oldestFetchedReportActionID so the next call picks up
// where the previous one left off, until the cursor stops advancing (gap filled).
const prevBackfillCursorRef = useRef<string | undefined>(undefined);
const isBackfillingRef = useRef(false);
useEffect(() => {
if (!hasFinishedInitialLoad || isOffline || hasNewerActions || reportLoadingState?.isLoadingNewerReportActions || reportLoadingState?.isLoadingOlderReportActions) {
return;
}

if (!isBackfillingRef.current) {
const hasIOUActions = reportActions.some((action) => isMoneyRequestAction(action));
if (!hasIOUActions || reportActions.length < BACKFILL_MIN_ACTIONS_THRESHOLD || !reportPaginationState?.newestFetchedReportActionID) {
return;
}
}

const cursor = isBackfillingRef.current ? reportPaginationState?.oldestFetchedReportActionID : reportPaginationState?.newestFetchedReportActionID;
if (!cursor) {
return;
}

if (prevBackfillCursorRef.current === cursor) {
return;
}

isBackfillingRef.current = true;
prevBackfillCursorRef.current = cursor;
const handle = TransitionTracker.runAfterTransitions({callback: () => getOlderActions(reportID, cursor)});

return () => handle.cancel();
}, [
hasFinishedInitialLoad,
isOffline,
hasNewerActions,
reportLoadingState?.isLoadingNewerReportActions,
reportLoadingState?.isLoadingOlderReportActions,
reportPaginationState?.newestFetchedReportActionID,
reportPaginationState?.oldestFetchedReportActionID,
reportActions,
reportID,
]);

const onStartReached = useCallback(() => {
if (!isSearchTopmostFullScreenRoute()) {
loadOlderChats(false);
return;
}
TransitionTracker.runAfterTransitions({
callback: () => loadOlderChats(false),
});
}, [loadOlderChats]);

const onEndReached = useCallback(() => {
loadNewerChats(false);
}, [loadNewerChats]);
reportPaginationState,
reportLoadingState,
});

const [hasScrolledOverThreshold, setHasScrolledOverThreshold] = useState(false);
const listLayoutHeightRef = useRef(0);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import useLoadReportActions from '@hooks/useLoadReportActions';

import TransitionTracker from '@libs/Navigation/TransitionTracker';
import {isMoneyRequestAction} from '@libs/ReportActionsUtils';

import isSearchTopmostFullScreenRoute from '@navigation/helpers/isSearchTopmostFullScreenRoute';

import {getOlderActions} from '@userActions/Report';

import type * as OnyxTypes from '@src/types/onyx';

import {useEffect, useRef} from 'react';

// The server page size for report actions is ~50. Gaps from IOU prioritization only happen
// when the initial load is truncated, so skip backfill for smaller reports.
const BACKFILL_MIN_ACTIONS_THRESHOLD = 50;

type UseMoneyRequestReportPaginationParams = {
/** The report whose actions are being paginated */
reportID: string | undefined;

/** Paginated report actions, newest-first */
reportActions: OnyxTypes.ReportAction[];

/** The single-transaction thread report ID, when one exists */
transactionThreadReportID: string | undefined;

/** Whether older actions exist beyond the loaded page */
hasOlderActions: boolean;

/** Whether newer actions exist beyond the loaded page */
hasNewerActions: boolean;

/** Whether the network is offline */
isOffline: boolean;

/** The report's pagination cursor state */
reportPaginationState: OnyxTypes.ReportPaginationState | undefined;

/** The report's loading state */
reportLoadingState: OnyxTypes.ReportLoadingState | undefined;
};

type UseMoneyRequestReportPaginationResult = {
/** FlashList onStartReached handler that loads older actions */
onStartReached: () => void;

/** FlashList onEndReached handler that loads newer actions */
onEndReached: () => void;
};

/**
* Owns loading more actions for the money-request report view: the list-edge handlers, the
* auto-load-newer loop, and the IOU backfill loop.
*
* The consuming component remounts per report (keyed by reportID), so the cursor refs below reset
* naturally on report switch and never need render-phase resets.
*/
function useMoneyRequestReportPagination({
reportID,
reportActions,
transactionThreadReportID,
hasOlderActions,
hasNewerActions,
isOffline,
reportPaginationState,
reportLoadingState,
}: UseMoneyRequestReportPaginationParams): UseMoneyRequestReportPaginationResult {
const {loadOlderChats, loadNewerChats} = useLoadReportActions({
reportID,
reportActions,
allReportActionIDs: reportActions.map((action) => action.reportActionID),
transactionThreadReportID,
hasOlderActions,
hasNewerActions,
newestFetchedReportActionID: reportPaginationState?.newestFetchedReportActionID,
});

const hasFinishedInitialLoad = reportLoadingState?.isLoadingInitialReportActions === false;
const prevNewestFetchedIDRef = useRef<string | undefined>(undefined);
useEffect(() => {
if (!hasFinishedInitialLoad || !hasNewerActions || reportActions.length === 0 || isOffline || reportLoadingState?.isLoadingNewerReportActions) {
return;
}
// Safety guard: if the cursor hasn't advanced since the last call, the server
// isn't returning new data. Stop to prevent an infinite request loop.
const currentCursor = reportPaginationState?.newestFetchedReportActionID;
if (prevNewestFetchedIDRef.current !== undefined && prevNewestFetchedIDRef.current === currentCursor) {
return;
}
prevNewestFetchedIDRef.current = currentCursor;
loadNewerChats(false);
}, [
hasFinishedInitialLoad,
reportActions.length,
hasNewerActions,
isOffline,
reportLoadingState?.isLoadingNewerReportActions,
reportPaginationState?.newestFetchedReportActionID,
loadNewerChats,
]);

// Backfill loop: the backend prioritizes IOU actions in OpenReport/GetNewerActions for money
// request reports, which can leave non-IOU chat messages in a gap between the IOU-biased cursor
// and older messages. After auto-pagination finishes, walk backwards from the IOU cursor using
// getOlderActions. Each response advances oldestFetchedReportActionID so the next call picks up
// where the previous one left off, until the cursor stops advancing (gap filled).
const prevBackfillCursorRef = useRef<string | undefined>(undefined);
const isBackfillingRef = useRef(false);
useEffect(() => {
if (!hasFinishedInitialLoad || isOffline || hasNewerActions || reportLoadingState?.isLoadingNewerReportActions || reportLoadingState?.isLoadingOlderReportActions) {
return;
}

if (!isBackfillingRef.current) {
const hasIOUActions = reportActions.some((action) => isMoneyRequestAction(action));
if (!hasIOUActions || reportActions.length < BACKFILL_MIN_ACTIONS_THRESHOLD || !reportPaginationState?.newestFetchedReportActionID) {
return;
}
}

const cursor = isBackfillingRef.current ? reportPaginationState?.oldestFetchedReportActionID : reportPaginationState?.newestFetchedReportActionID;
if (!cursor) {
return;
}

if (prevBackfillCursorRef.current === cursor) {
return;
}

isBackfillingRef.current = true;
prevBackfillCursorRef.current = cursor;
const handle = TransitionTracker.runAfterTransitions({callback: () => getOlderActions(reportID, cursor)});

return () => handle.cancel();
}, [
hasFinishedInitialLoad,
isOffline,
hasNewerActions,
reportLoadingState?.isLoadingNewerReportActions,
reportLoadingState?.isLoadingOlderReportActions,
reportPaginationState?.newestFetchedReportActionID,
reportPaginationState?.oldestFetchedReportActionID,
reportActions,
reportID,
]);

const onStartReached = () => {
if (!isSearchTopmostFullScreenRoute()) {
loadOlderChats(false);
return;
}
TransitionTracker.runAfterTransitions({
callback: () => loadOlderChats(false),
});
};

const onEndReached = () => {
loadNewerChats(false);
};

return {onStartReached, onEndReached};
}

export default useMoneyRequestReportPagination;
Loading
Loading