Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8234,6 +8234,7 @@ const CONST = {
},
},
DEFAULT_REPORT_METADATA: {isLoadingInitialReportActions: true},
DEFAULT_REPORT_LOADING_STATE: {isLoadingInitialReportActions: true},
UPGRADE_PATHS: {
CATEGORIES: 'categories',
REPORTS: 'reports',
Expand Down
20 changes: 17 additions & 3 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,10 @@ const ONYXKEYS = {
// Stores last visited path
LAST_VISITED_PATH: 'lastVisitedPath',

/** Map of reportID → DB-formatted timestamp for when the user last visited each report.
* Only consumed by `findLastAccessedReport` / `getMostRecentlyVisitedReport` for navigation fallbacks. */
REPORT_LAST_VISIT_TIMES: 'reportLastVisitTimes',

// Stores the recently used report fields
RECENTLY_USED_REPORT_FIELDS: 'recentlyUsedReportFields',

Expand Down Expand Up @@ -732,10 +736,17 @@ const ONYXKEYS = {
REPORT: 'report_',
REPORT_NAME_VALUE_PAIRS: 'reportNameValuePairs_',
REPORT_DRAFT: 'reportDraft_',
// REPORT_METADATA is a perf optimization used to hold loading states (isLoadingInitialReportActions, isLoadingOlderReportActions, isLoadingNewerReportActions).
// A lot of components are connected to the Report entity and do not care about the actions. Setting the loading state
// directly on the report caused a lot of unnecessary re-renders
// REPORT_METADATA holds report-level business state that is NOT the report itself
// (optimistic flag, pending chat members, report-level errors, DEW pendingExpenseAction).
// Loading flags / pagination cursors / last-visit timestamp live in dedicated
// keys below (REPORT_LOADING_STATE, REPORT_PAGINATION_STATE, REPORT_LAST_VISIT_TIMES)
// so they don't ripple to every subscriber of the report's business state.
REPORT_METADATA: 'reportMetadata_',
/** Session-scoped loading/error flags for a report's action list.
* Registered as RAM-only in `setup/index.ts`. */
RAM_ONLY_REPORT_LOADING_STATE: 'reportLoadingState_',
/** Pagination cursors for a report's action list. */
REPORT_PAGINATION_STATE: 'reportPaginationState_',
REPORT_ACTIONS: 'reportActions_',
REPORT_ACTIONS_DRAFTS: 'reportActionsDrafts_',
REPORT_ACTIONS_PAGES: 'reportActionsPages_',
Expand Down Expand Up @@ -1259,6 +1270,8 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS]: OnyxTypes.ReportNameValuePairs;
[ONYXKEYS.COLLECTION.REPORT_DRAFT]: OnyxTypes.Report;
[ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata;
[ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE]: OnyxTypes.ReportLoadingState;
[ONYXKEYS.COLLECTION.REPORT_PAGINATION_STATE]: OnyxTypes.ReportPaginationState;
[ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions;
[ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: OnyxTypes.ReportActionsDrafts;
[ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES]: OnyxTypes.Pages;
Expand Down Expand Up @@ -1453,6 +1466,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.ONBOARDING_LAST_VISITED_PATH]: string;
[ONYXKEYS.RAM_ONLY_IS_SEARCHING_FOR_REPORTS]: boolean;
[ONYXKEYS.LAST_VISITED_PATH]: string | undefined;
[ONYXKEYS.REPORT_LAST_VISIT_TIMES]: OnyxTypes.ReportLastVisitTimes;
[ONYXKEYS.RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields;
[ONYXKEYS.RAM_ONLY_UPDATE_REQUIRED]: boolean;
[ONYXKEYS.SUPPORTAL_PERMISSION_DENIED]: OnyxTypes.SupportalPermissionDenied | null;
Expand Down
4 changes: 2 additions & 2 deletions src/components/MoneyReportHeaderNextStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ type MoneyReportHeaderNextStepProps = {
*/
function MoneyReportHeaderNextStep({reportID}: MoneyReportHeaderNextStepProps) {
const {isOffline} = useNetwork();
const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`);
const isLoadingInitialReportActions = reportMetadata?.isLoadingInitialReportActions;
const [reportLoadingState] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportID}`);
const isLoadingInitialReportActions = reportLoadingState?.isLoadingInitialReportActions;
const optimisticNextStep = useOptimisticNextStep(reportID);

const showNextStepBar = !!optimisticNextStep && (('message' in optimisticNextStep && !!optimisticNextStep.message?.length) || 'messageKey' in optimisticNextStep);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
// report is guaranteed to exist — callers only render this component when report is loaded
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`) as unknown as [OnyxTypes.Report];
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(report?.policyID)}`);
const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDFromRoute}`);
const [reportLoadingState] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportIDFromRoute}`);
const [reportPaginationState] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_PAGINATION_STATE}${reportIDFromRoute}`);
const reportID = report?.reportID;

const {reportActions: unfilteredReportActions, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID, route?.params?.reportActionID);
Expand All @@ -116,8 +117,8 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
() => Object.values(allReportTransactions ?? {}).some((transaction) => transaction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE),
[allReportTransactions],
);
const newTransactions = useNewTransactions(reportMetadata?.hasOnceLoadedReportActions, reportTransactions);
const showReportActionsLoadingState = reportMetadata?.isLoadingInitialReportActions && !reportMetadata?.hasOnceLoadedReportActions;
const newTransactions = useNewTransactions(reportLoadingState?.hasOnceLoadedReportActions, reportTransactions);
const showReportActionsLoadingState = reportLoadingState?.isLoadingInitialReportActions && !reportLoadingState?.hasOnceLoadedReportActions;
const reportTransactionIDs = useMemo(() => transactions.map((transaction) => transaction.transactionID), [transactions]);
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.chatReportID)}`);

Expand Down Expand Up @@ -225,23 +226,31 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
transactionThreadReport,
hasOlderActions,
hasNewerActions,
newestFetchedReportActionID: reportMetadata?.newestFetchedReportActionID,
newestFetchedReportActionID: reportPaginationState?.newestFetchedReportActionID,
});

const hasFinishedInitialLoad = reportMetadata?.isLoadingInitialReportActions === false;
const hasFinishedInitialLoad = reportLoadingState?.isLoadingInitialReportActions === false;
const prevNewestFetchedIDRef = useRef<string | undefined>(undefined);
useEffect(() => {
if (hasFinishedInitialLoad && hasNewerActions && reportActions.length > 0 && !isOffline && !reportMetadata?.isLoadingNewerReportActions) {
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 = reportMetadata?.newestFetchedReportActionID;
const currentCursor = reportPaginationState?.newestFetchedReportActionID;
if (prevNewestFetchedIDRef.current !== undefined && prevNewestFetchedIDRef.current === currentCursor) {
return;
}
prevNewestFetchedIDRef.current = currentCursor;
loadNewerChats(false);
}
}, [hasFinishedInitialLoad, reportActions.length, hasNewerActions, isOffline, reportMetadata?.isLoadingNewerReportActions, reportMetadata?.newestFetchedReportActionID, loadNewerChats]);
}, [
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
Expand All @@ -257,18 +266,18 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
isBackfillingRef.current = false;
}
useEffect(() => {
if (!hasFinishedInitialLoad || isOffline || hasNewerActions || reportMetadata?.isLoadingNewerReportActions || reportMetadata?.isLoadingOlderReportActions) {
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 || !reportMetadata?.newestFetchedReportActionID) {
if (!hasIOUActions || reportActions.length < BACKFILL_MIN_ACTIONS_THRESHOLD || !reportPaginationState?.newestFetchedReportActionID) {
return;
}
}

const cursor = isBackfillingRef.current ? reportMetadata?.oldestFetchedReportActionID : reportMetadata?.newestFetchedReportActionID;
const cursor = isBackfillingRef.current ? reportPaginationState?.oldestFetchedReportActionID : reportPaginationState?.newestFetchedReportActionID;
if (!cursor) {
return;
}
Expand All @@ -287,10 +296,10 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
hasFinishedInitialLoad,
isOffline,
hasNewerActions,
reportMetadata?.isLoadingNewerReportActions,
reportMetadata?.isLoadingOlderReportActions,
reportMetadata?.newestFetchedReportActionID,
reportMetadata?.oldestFetchedReportActionID,
reportLoadingState?.isLoadingNewerReportActions,
reportLoadingState?.isLoadingOlderReportActions,
reportPaginationState?.newestFetchedReportActionID,
reportPaginationState?.oldestFetchedReportActionID,
reportActions,
reportID,
]);
Expand Down Expand Up @@ -357,7 +366,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
// To handle this, we use the 'referrer' parameter to check if the current navigation is triggered from a notification.
const isFromNotification = route?.params?.referrer === CONST.REFERRER.NOTIFICATION;
if ((isVisible || isFromNotification) && scrollingVerticalBottomOffset.current < CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD) {
readNewestAction(report.reportID, !!reportMetadata?.hasOnceLoadedReportActions);
readNewestAction(report.reportID, !!reportLoadingState?.hasOnceLoadedReportActions);
if (isFromNotification) {
Navigation.setParams({referrer: undefined});
}
Expand All @@ -366,7 +375,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [report.lastVisibleActionCreated, transactionThreadReport?.lastVisibleActionCreated, report.reportID, isVisible, reportMetadata?.hasOnceLoadedReportActions]);
}, [report.lastVisibleActionCreated, transactionThreadReport?.lastVisibleActionCreated, report.reportID, isVisible, reportLoadingState?.hasOnceLoadedReportActions]);

useEffect(() => {
if (!isVisible || !isFocused) {
Expand All @@ -390,15 +399,15 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
return;
}

readNewestAction(report.reportID, !!reportMetadata?.hasOnceLoadedReportActions);
readNewestAction(report.reportID, !!reportLoadingState?.hasOnceLoadedReportActions);
userActiveSince.current = DateUtils.getDBTime();

// This effect logic to `mark as read` will only run when the report focused has new messages and the App visibility
// is changed to visible(meaning user switched to app/web, while user was previously using different tab or application).
// We will mark the report as read in the above case which marks the LHN report item as read while showing the new message
// marker for the chat messages received while the user wasn't focused on the report or on another browser tab for web.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFocused, isVisible, reportMetadata?.hasOnceLoadedReportActions]);
}, [isFocused, isVisible, reportLoadingState?.hasOnceLoadedReportActions]);

/**
* The index of the earliest message that was received while offline
Expand Down Expand Up @@ -448,7 +457,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
// We additionally track the top offset to be able to scroll to the new transaction when it's added
scrollingVerticalTopOffset.current = contentOffset.y;
},
hasOnceLoadedReportActions: !!reportMetadata?.hasOnceLoadedReportActions,
hasOnceLoadedReportActions: !!reportLoadingState?.hasOnceLoadedReportActions,
});

useScrollToEndOnNewMessageReceived({
Expand Down Expand Up @@ -617,8 +626,8 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)

reportScrollManager.scrollToEnd();
readActionSkipped.current = false;
readNewestAction(report.reportID, !!reportMetadata?.hasOnceLoadedReportActions);
}, [setIsFloatingMessageCounterVisible, hasNewestReportAction, reportScrollManager, report.reportID, reportMetadata?.hasOnceLoadedReportActions, introSelected, betas]);
readNewestAction(report.reportID, !!reportLoadingState?.hasOnceLoadedReportActions);
}, [setIsFloatingMessageCounterVisible, hasNewestReportAction, reportScrollManager, report.reportID, reportLoadingState?.hasOnceLoadedReportActions, introSelected, betas]);

const scrollToNewTransaction = useCallback(
(pageY: number) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ type MoneyRequestReportViewProps = {
/** The report */
report: OnyxEntry<OnyxTypes.Report>;

/** Metadata for report */
reportMetadata: OnyxEntry<OnyxTypes.ReportMetadata>;
/** Loading state for report */
reportLoadingState: OnyxEntry<OnyxTypes.ReportLoadingState>;

/** Whether Report footer (that includes Composer) should be displayed */
shouldDisplayReportFooter: boolean;
Expand Down Expand Up @@ -104,7 +104,7 @@ function InitialLoadingSkeleton({styles, onLayout, reasonAttributes}: {styles: T
);
}

function MoneyRequestReportView({report, reportMetadata, shouldDisplayReportFooter, backToRoute, onLayout}: MoneyRequestReportViewProps) {
function MoneyRequestReportView({report, reportLoadingState, shouldDisplayReportFooter, backToRoute, onLayout}: MoneyRequestReportViewProps) {
const styles = useThemeStyles();
const {isOffline} = useNetwork();

Expand Down Expand Up @@ -141,7 +141,7 @@ function MoneyRequestReportView({report, reportMetadata, shouldDisplayReportFoot
const reportTransactionIDs = visibleTransactions.map((transaction) => transaction.transactionID);
const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions ?? [], isOffline, reportTransactionIDs);

const isLoadingInitialReportActions = reportMetadata?.isLoadingInitialReportActions;
const isLoadingInitialReportActions = reportLoadingState?.isLoadingInitialReportActions;
const dismissReportCreationError = useCallback(() => {
goBackFromSearchMoneyRequest();
// eslint-disable-next-line @typescript-eslint/no-deprecated
Expand All @@ -154,7 +154,7 @@ function MoneyRequestReportView({report, reportMetadata, shouldDisplayReportFoot

// Prevent the empty state flash by ensuring transaction data is fully loaded before deciding which view to render
// We need to wait for both the selector to finish AND ensure we're not in a loading state where transactions could still populate
const shouldWaitForTransactions = shouldWaitForTransactionsUtil(report, transactions, reportMetadata, isOffline);
const shouldWaitForTransactions = shouldWaitForTransactionsUtil(report, transactions, reportLoadingState, isOffline);

const shouldShowOpenReportLoadingSkeleton = !!(isLoadingInitialReportActions && reportActions.length === 0 && !isOffline) || shouldWaitForTransactions;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ function MoneyRequestReportPreviewContent({
originalReportID,
}: MoneyRequestReportPreviewContentProps) {
const [chatReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${chatReportID}`);
const [chatReportLoadingState] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${chatReportID}`);

const [isTransitionPending, setIsTransitionPending] = useState(() => {
const pending = getPendingSubmitFollowUpAction();
Expand All @@ -126,7 +127,7 @@ function MoneyRequestReportPreviewContent({
return () => handle.cancel();
}, [isTransitionPending]),
);
const shouldShowLoading = !chatReportMetadata?.hasOnceLoadedReportActions && transactions.length === 0 && !chatReportMetadata?.isOptimisticReport;
const shouldShowLoading = !chatReportLoadingState?.hasOnceLoadedReportActions && transactions.length === 0 && !chatReportMetadata?.isOptimisticReport;
// `hasOnceLoadedReportActions` becomes true before transactions populate fully,
// so we defer the loading state update to ensure transactions are loaded
const shouldShowLoadingDeferred = useDeferredValue(shouldShowLoading);
Expand All @@ -141,7 +142,7 @@ function MoneyRequestReportPreviewContent({
const shouldShowPreviewLoading = isTransitionPending || shouldShowLoading || shouldShowLoadingDeferred || (!currentWidth && !shouldShowPreviewPlaceholder);
const skeletonReasonAttributes: SkeletonSpanReasonAttributes = {
context: 'MoneyRequestReportPreviewContent',
hasOnceLoadedReportActions: chatReportMetadata?.hasOnceLoadedReportActions,
hasOnceLoadedReportActions: chatReportLoadingState?.hasOnceLoadedReportActions,
isTransactionsEmpty: transactions.length === 0,
isOptimisticReport: chatReportMetadata?.isOptimisticReport,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ function MoneyRequestReportPreview({
Navigation.navigate(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: iouReportID, backTo: Navigation.getActiveRoute()}));
}
}, [iouReportID, isSmallScreenWidth]);
const [hasOnceLoadedReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${chatReportID}`, {
const [hasOnceLoadedReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${chatReportID}`, {
selector: hasOnceLoadedReportActionsSelector,
});
const newTransactions = useNewTransactions(hasOnceLoadedReportActions, transactions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function ExpenseReportListItem<TItem extends ListItem>({
const {translate} = useLocalize();
const {isLargeScreenWidth} = useResponsiveLayout();
const {currentSearchHash, currentSearchKey, currentSearchResults} = useSearchStateContext();
const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportItem.reportID}`, {selector: isActionLoadingSelector});
const [isActionLoading] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportItem.reportID}`, {selector: isActionLoadingSelector});
const expensifyIcons = useMemoizedLazyExpensifyIcons(['DotIndicator']);
const currentUserDetails = useCurrentUserPersonalDetails();

Expand Down
Loading
Loading