From da55621c0260259fdcb17396134ba521b9ca2e6b Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Wed, 6 Nov 2024 17:10:06 -0500 Subject: [PATCH 1/8] Move logic to start report actions list at index to BaseInvertedFlatList --- .../getInitialPaginationSize/index.native.ts | 0 .../getInitialPaginationSize/index.ts | 0 .../BaseInvertedFlatList/index.tsx | 54 ++++++++- .../InvertedFlatList/index.native.tsx | 5 +- src/components/InvertedFlatList/index.tsx | 9 +- src/pages/home/report/ReportActionsList.tsx | 16 ++- src/pages/home/report/ReportActionsView.tsx | 114 ++++-------------- .../perf-test/ReportActionsList.perf-test.tsx | 1 - 8 files changed, 90 insertions(+), 109 deletions(-) rename src/{pages/home/report => components/InvertedFlatList/BaseInvertedFlatList}/getInitialPaginationSize/index.native.ts (100%) rename src/{pages/home/report => components/InvertedFlatList/BaseInvertedFlatList}/getInitialPaginationSize/index.ts (100%) 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({ From 5a71c7baafb6f4b3f0f315283ec0b774d29e9e0a Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Tue, 19 Nov 2024 02:48:47 -0500 Subject: [PATCH 2/8] Fix jumping during pagination --- .../BaseInvertedFlatList/index.tsx | 52 ++++++++++++++----- src/pages/home/report/ReportActionsList.tsx | 9 +--- src/pages/home/report/ReportActionsView.tsx | 3 -- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx index c09b20b676df..79814b3a4ebc 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx @@ -5,40 +5,67 @@ import FlatList from '@components/FlatList'; import usePrevious from '@hooks/usePrevious'; import getInitialPaginationSize from './getInitialPaginationSize'; -type BaseInvertedFlatListProps = Omit, 'data' | 'renderItem'> & { +// Adapted from https://github.com/facebook/react-native/blob/29a0d7c3b201318a873db0d1b62923f4ce720049/packages/virtualized-lists/Lists/VirtualizeUtils.js#L237 +function defaultKeyExtractor(item: T | {key: string} | {id: string}, index: number): string { + if (item != null) { + if (typeof item === 'object' && 'key' in item) { + return item.key; + } + if (typeof item === 'object' && 'id' in item) { + return item.id; + } + } + return String(index); +} + +type BaseInvertedFlatListProps = Omit, 'data' | 'renderItem' | 'initialScrollIndex'> & { shouldEnableAutoScrollToTopThreshold?: boolean; data: T[]; renderItem: ListRenderItem; + initialScrollKey?: string | null; }; const AUTOSCROLL_TO_TOP_THRESHOLD = 250; function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { - const {shouldEnableAutoScrollToTopThreshold, initialScrollIndex, data, onStartReached, renderItem, ...rest} = props; - + const {shouldEnableAutoScrollToTopThreshold, initialScrollKey, data, onStartReached, renderItem, keyExtractor = defaultKeyExtractor, ...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); + // What we do is start rendering the list from `initialScrollKey` and then whenever we reach the start we render more + // previous items, until everything is rendered. We also progressively render new data that is added at the start of the + // list to make sure `maintainVisibleContentPosition` works as expected. + const [currentDataId, setCurrentDataId] = useState(() => { + if (initialScrollKey) { + return initialScrollKey; + } + const initialItem = data.at(0); + return initialItem ? keyExtractor(initialItem, 0) : null; + }); + const [isInitialData, setIsInitialData] = useState(true); + const currentDataIndex = useMemo(() => data.findIndex((item, index) => keyExtractor(item, index) === currentDataId), [currentDataId, data, keyExtractor]); const displayedData = useMemo(() => { + if (currentDataIndex === -1) { + return []; + } if (currentDataIndex > 0) { - return data.slice(currentDataIndex); + return data.slice(Math.max(0, currentDataIndex - (isInitialData ? 0 : getInitialPaginationSize))); } return data; - }, [data, currentDataIndex]); + }, [currentDataIndex, data, isInitialData]); + 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 { + if (!isLoadingData) { onStartReached?.(info); } + setIsInitialData(false); + const firstDisplayedItem = displayedData.at(0); + setCurrentDataId(firstDisplayedItem ? keyExtractor(firstDisplayedItem, currentDataIndex) : ''); }, - [onStartReached, isLoadingData], + [isLoadingData, keyExtractor, displayedData, currentDataIndex, onStartReached], ); const handleRenderItem = useCallback( @@ -72,6 +99,7 @@ function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: Forwa data={displayedData} onStartReached={handleStartReached} renderItem={handleRenderItem} + keyExtractor={keyExtractor} /> ); } diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 621b67e88ca1..39a7e185a17f 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -702,13 +702,6 @@ function ReportActionsList({ loadOlderChats(false); }, [loadOlderChats]); - const indexOfLinkedAction = useMemo(() => { - 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; @@ -743,7 +736,7 @@ function ReportActionsList({ extraData={extraData} key={listID} shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScrollToTopThreshold} - initialScrollIndex={indexOfLinkedAction} + initialScrollKey={route?.params?.reportActionID} /> diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 76adadd27dd1..4ee02b02c5a6 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -100,8 +100,6 @@ function ReportActionsView({ const didLoadNewerChats = useRef(false); const {isOffline} = useNetwork(); - const isFirstLinkedActionRender = useRef(true); - const network = useNetwork(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const contentListHeight = useRef(0); @@ -133,7 +131,6 @@ function ReportActionsView({ // Keep the old list ID since we're not in the Comment Linking flow return listOldID; } - isFirstLinkedActionRender.current = true; const newID = generateNewRandomInt(listOldID, 1, Number.MAX_SAFE_INTEGER); // eslint-disable-next-line react-compiler/react-compiler listOldID = newID; From 55bfa619ade9ee56494c7a241bbb803f3f328f9c Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Sat, 7 Dec 2024 13:33:33 -0500 Subject: [PATCH 3/8] Fix doesCreatedActionExists to account for current page only --- src/pages/home/ReportScreen.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 4c3ed5c705a5..20ad3991c062 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -249,8 +249,8 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro ); const isPendingActionExist = !!reportActions.at(0)?.pendingAction; - const doesCreatedActionExists = useCallback(() => !!sortedAllReportActions?.findLast((action) => ReportActionsUtils.isCreatedAction(action)), [sortedAllReportActions]); - const isLinkedMessageAvailable = useMemo(() => indexOfLinkedMessage > -1, [indexOfLinkedMessage]); + const doesCreatedActionExists = useCallback(() => !!reportActions?.findLast((action) => ReportActionsUtils.isCreatedAction(action)), [reportActions]); + const isLinkedMessageAvailable = indexOfLinkedMessage > -1; // The linked report actions should have at least 15 messages (counting as 1 page) above them to fill the screen. // If the count is too high (equal to or exceeds the web pagination size / 50) and there are no cached messages in the report, From 2454a4744917bbc305cf4f8551cb6b2d41e79d62 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Sat, 7 Dec 2024 18:17:27 -0500 Subject: [PATCH 4/8] Limit how fast new items are added --- .../BaseInvertedFlatList/index.tsx | 62 ++++++++++++++----- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx index 79814b3a4ebc..2d299c5b9fb8 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx @@ -1,5 +1,5 @@ -import type {ForwardedRef} from 'react'; -import React, {forwardRef, useCallback, useMemo, useState} from 'react'; +import type {ForwardedRef, MutableRefObject} from 'react'; +import React, {forwardRef, useCallback, useEffect, useMemo, useRef, 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'; @@ -26,6 +26,7 @@ type BaseInvertedFlatListProps = Omit, 'data' | 'renderItem' }; const AUTOSCROLL_TO_TOP_THRESHOLD = 250; +const RENDER_DELAY = 500; function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { const {shouldEnableAutoScrollToTopThreshold, initialScrollKey, data, onStartReached, renderItem, keyExtractor = defaultKeyExtractor, ...rest} = props; @@ -46,27 +47,56 @@ function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: Forwa if (currentDataIndex === -1) { return []; } - if (currentDataIndex > 0) { - return data.slice(Math.max(0, currentDataIndex - (isInitialData ? 0 : getInitialPaginationSize))); + if (currentDataIndex === 0) { + return data; } - return data; + return data.slice(Math.max(0, currentDataIndex - (isInitialData ? 0 : getInitialPaginationSize))); }, [currentDataIndex, data, isInitialData]); const isLoadingData = data.length > displayedData.length; const wasLoadingData = usePrevious(isLoadingData); const dataIndexDifference = data.length - displayedData.length; - const handleStartReached = useCallback( - (info: {distanceFromStart: number}) => { - if (!isLoadingData) { - onStartReached?.(info); - } - setIsInitialData(false); - const firstDisplayedItem = displayedData.at(0); - setCurrentDataId(firstDisplayedItem ? keyExtractor(firstDisplayedItem, currentDataIndex) : ''); - }, - [isLoadingData, keyExtractor, displayedData, currentDataIndex, onStartReached], - ); + // Queue up updates to the displayed data to avoid adding too many at once and cause jumps in the list. + const queuedRenders = useRef>([]); + const isRendering = useRef(false); + + const renderTimeout = useRef(); + useEffect(() => { + return () => { + clearTimeout(renderTimeout.current); + }; + }, []); + + // Use a ref here to make sure we always operate on the latest state. + const updateDisplayedDataRef = useRef() as MutableRefObject<() => void>; + // eslint-disable-next-line react-compiler/react-compiler + updateDisplayedDataRef.current = () => { + const info = queuedRenders.current.shift(); + if (!info) { + isRendering.current = false; + return; + } + isRendering.current = true; + + if (!isLoadingData) { + onStartReached?.(info); + } + setIsInitialData(false); + const firstDisplayedItem = displayedData.at(0); + setCurrentDataId(firstDisplayedItem ? keyExtractor(firstDisplayedItem, currentDataIndex) : ''); + + renderTimeout.current = setTimeout(() => { + updateDisplayedDataRef.current(); + }, RENDER_DELAY); + }; + + const handleStartReached = useCallback((info: {distanceFromStart: number}) => { + queuedRenders.current.push(info); + if (!isRendering.current) { + updateDisplayedDataRef.current(); + } + }, []); const handleRenderItem = useCallback( ({item, index, separators}: ListRenderItemInfo) => { From 2d8c6e973afe08f39fb5383e4df0536aafb1d129 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Sun, 12 Jan 2025 21:09:28 -0500 Subject: [PATCH 5/8] Fix case when current item is removed --- .../InvertedFlatList/BaseInvertedFlatList/index.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx index 2d299c5b9fb8..6e8c6c2d4227 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx @@ -44,10 +44,7 @@ function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: Forwa const [isInitialData, setIsInitialData] = useState(true); const currentDataIndex = useMemo(() => data.findIndex((item, index) => keyExtractor(item, index) === currentDataId), [currentDataId, data, keyExtractor]); const displayedData = useMemo(() => { - if (currentDataIndex === -1) { - return []; - } - if (currentDataIndex === 0) { + if (currentDataIndex <= 0) { return data; } return data.slice(Math.max(0, currentDataIndex - (isInitialData ? 0 : getInitialPaginationSize))); From b438b3697e69f07f4c5ef030c3a23a0e709c268a Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 16 Jan 2025 16:03:42 -0500 Subject: [PATCH 6/8] Abstract batch logic to mock in tests --- jest/setup.ts | 18 +++++++ .../BaseInvertedFlatList/RenderTaskQueue.tsx | 51 +++++++++++++++++++ .../BaseInvertedFlatList/index.tsx | 45 +++++----------- 3 files changed, 83 insertions(+), 31 deletions(-) create mode 100644 src/components/InvertedFlatList/BaseInvertedFlatList/RenderTaskQueue.tsx diff --git a/jest/setup.ts b/jest/setup.ts index c575054f7dac..260888fcc1ec 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -101,3 +101,21 @@ jest.mock( dispose() {} }, ); + +jest.mock( + '@components/InvertedFlatList/BaseInvertedFlatList/RenderTaskQueue', + () => + class SyncRenderTaskQueue { + private handler: (info: unknown) => void = () => {}; + + add(info: unknown) { + this.handler(info); + } + + setHandler(handler: () => void) { + this.handler = handler; + } + + cancel() {} + }, +); diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList/RenderTaskQueue.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList/RenderTaskQueue.tsx new file mode 100644 index 000000000000..0cc086c9cdf3 --- /dev/null +++ b/src/components/InvertedFlatList/BaseInvertedFlatList/RenderTaskQueue.tsx @@ -0,0 +1,51 @@ +const RENDER_DELAY = 500; + +type RenderInfo = { + distanceFromStart: number; +}; + +class RenderTaskQueue { + private renderInfos: RenderInfo[] = []; + + private isRendering = false; + + private handler: (info: RenderInfo) => void = () => {}; + + private timeout: NodeJS.Timeout | null = null; + + add(info: RenderInfo) { + this.renderInfos.push(info); + + if (!this.isRendering) { + this.render(); + } + } + + setHandler(handler: (info: RenderInfo) => void) { + this.handler = handler; + } + + cancel() { + if (this.timeout == null) { + return; + } + clearTimeout(this.timeout); + } + + private render() { + const info = this.renderInfos.shift(); + if (!info) { + this.isRendering = false; + return; + } + this.isRendering = true; + + this.handler(info); + + this.timeout = setTimeout(() => { + this.render(); + }, RENDER_DELAY); + } +} + +export default RenderTaskQueue; diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx index 6e8c6c2d4227..7790763c521b 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx @@ -1,9 +1,10 @@ -import type {ForwardedRef, MutableRefObject} from 'react'; -import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {ForwardedRef} from 'react'; +import React, {forwardRef, useCallback, useEffect, 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'; +import RenderTaskQueue from './RenderTaskQueue'; // Adapted from https://github.com/facebook/react-native/blob/29a0d7c3b201318a873db0d1b62923f4ce720049/packages/virtualized-lists/Lists/VirtualizeUtils.js#L237 function defaultKeyExtractor(item: T | {key: string} | {id: string}, index: number): string { @@ -26,7 +27,6 @@ type BaseInvertedFlatListProps = Omit, 'data' | 'renderItem' }; const AUTOSCROLL_TO_TOP_THRESHOLD = 250; -const RENDER_DELAY = 500; function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { const {shouldEnableAutoScrollToTopThreshold, initialScrollKey, data, onStartReached, renderItem, keyExtractor = defaultKeyExtractor, ...rest} = props; @@ -55,45 +55,28 @@ function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: Forwa const dataIndexDifference = data.length - displayedData.length; // Queue up updates to the displayed data to avoid adding too many at once and cause jumps in the list. - const queuedRenders = useRef>([]); - const isRendering = useRef(false); - - const renderTimeout = useRef(); + const renderQueue = useMemo(() => new RenderTaskQueue(), []); useEffect(() => { return () => { - clearTimeout(renderTimeout.current); + renderQueue.cancel(); }; - }, []); - - // Use a ref here to make sure we always operate on the latest state. - const updateDisplayedDataRef = useRef() as MutableRefObject<() => void>; - // eslint-disable-next-line react-compiler/react-compiler - updateDisplayedDataRef.current = () => { - const info = queuedRenders.current.shift(); - if (!info) { - isRendering.current = false; - return; - } - isRendering.current = true; + }, [renderQueue]); + renderQueue.setHandler((info) => { if (!isLoadingData) { onStartReached?.(info); } setIsInitialData(false); const firstDisplayedItem = displayedData.at(0); setCurrentDataId(firstDisplayedItem ? keyExtractor(firstDisplayedItem, currentDataIndex) : ''); + }); - renderTimeout.current = setTimeout(() => { - updateDisplayedDataRef.current(); - }, RENDER_DELAY); - }; - - const handleStartReached = useCallback((info: {distanceFromStart: number}) => { - queuedRenders.current.push(info); - if (!isRendering.current) { - updateDisplayedDataRef.current(); - } - }, []); + const handleStartReached = useCallback( + (info: {distanceFromStart: number}) => { + renderQueue.add(info); + }, + [renderQueue], + ); const handleRenderItem = useCallback( ({item, index, separators}: ListRenderItemInfo) => { From ce4dfcc0955cd7d295e1be2d9f29bc4e65394abb Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 16 Jan 2025 16:31:17 -0500 Subject: [PATCH 7/8] Always render all items if initialKey is null --- .../InvertedFlatList/BaseInvertedFlatList/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx index 7790763c521b..3753453aa4b2 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx @@ -38,11 +38,10 @@ function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: Forwa if (initialScrollKey) { return initialScrollKey; } - const initialItem = data.at(0); - return initialItem ? keyExtractor(initialItem, 0) : null; + return null; }); const [isInitialData, setIsInitialData] = useState(true); - const currentDataIndex = useMemo(() => data.findIndex((item, index) => keyExtractor(item, index) === currentDataId), [currentDataId, data, keyExtractor]); + const currentDataIndex = useMemo(() => (currentDataId === null ? 0 : data.findIndex((item, index) => keyExtractor(item, index) === currentDataId)), [currentDataId, data, keyExtractor]); const displayedData = useMemo(() => { if (currentDataIndex <= 0) { return data; From 02fc6b02bb3813d8397b1127b28e58782a864382 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Fri, 17 Jan 2025 12:02:10 -0500 Subject: [PATCH 8/8] Fix lint --- src/pages/home/ReportScreen.tsx | 147 +++++++++++++------- src/pages/home/report/ReportActionsList.tsx | 90 +++++++----- src/pages/home/report/ReportActionsView.tsx | 72 +++++----- 3 files changed, 187 insertions(+), 122 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 35a6df14e6ec..6d501f3f4708 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -32,15 +32,55 @@ import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import clearReportNotifications from '@libs/Notification/clearReportNotifications'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; +import {getDisplayNameOrDefault, isPersonalDetailsEmpty} from '@libs/PersonalDetailsUtils'; +import { + getCombinedReportActions, + getOneTransactionThreadReportID, + isActionOfType, + isCreatedAction, + isDeletedAction, + isDeletedParentAction, + isMoneyRequestAction, + isWhisperAction, + shouldReportActionBeVisible, +} from '@libs/ReportActionsUtils'; +import { + canAccessReport, + canEditReportAction, + canUserPerformWriteAction, + findLastAccessedReport, + getParticipantsAccountIDsForDisplay, + getReportIDFromLink, + getReportOfflinePendingActionAndErrors, + isChatThread, + isConciergeChatReport, + isExpenseReport, + isGroupChat, + isHiddenForCurrentUser, + isInvoiceReport, + isMoneyRequest, + isMoneyRequestReport, + isMoneyRequestReportPendingDeletion, + isOneTransactionThread, + isPolicyExpenseChat, + isTaskReport, + isTrackExpenseReport, + isValidReportIDFromPath, +} from '@libs/ReportUtils'; import shouldFetchReport from '@libs/shouldFetchReport'; -import * as ValidationUtils from '@libs/ValidationUtils'; +import {isNumeric} from '@libs/ValidationUtils'; import type {AuthScreensParamList} from '@navigation/types'; -import * as ComposerActions from '@userActions/Composer'; -import * as Report from '@userActions/Report'; +import {setShouldShowComposeInput} from '@userActions/Composer'; +import { + clearDeleteTransactionNavigateBackUrl, + navigateToConciergeChat, + openReport, + readNewestAction, + subscribeToReportLeavingEvents, + unsubscribeFromLeavingRoomReportChannel, + updateLastVisitTime, +} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -133,8 +173,8 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP); const wasLoadingApp = usePrevious(isLoadingApp); const finishedLoadingApp = wasLoadingApp && !isLoadingApp; - const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(parentReportAction); - const prevIsDeletedParentAction = usePrevious(isDeletedParentAction); + const deletedParentAction = isDeletedParentAction(parentReportAction); + const prevDeletedParentAction = usePrevious(deletedParentAction); const isLoadingReportOnyx = isLoadingOnyxValue(reportResult); const permissions = useDeepCompareRef(reportOnyx?.permissions); @@ -143,14 +183,14 @@ function ReportScreen({route, navigation}: ReportScreenProps) { // Don't update if there is a reportID in the params already if (route.params.reportID) { const reportActionID = route?.params?.reportActionID; - const isValidReportActionID = ValidationUtils.isNumeric(reportActionID); + const isValidReportActionID = isNumeric(reportActionID); if (reportActionID && !isValidReportActionID) { navigation.setParams({reportActionID: ''}); } return; } - const lastAccessedReportID = ReportUtils.findLastAccessedReport(!canUseDefaultRooms, !!route.params.openOnAdminRoom, activeWorkspaceID)?.reportID; + const lastAccessedReportID = findLastAccessedReport(!canUseDefaultRooms, !!route.params.openOnAdminRoom, activeWorkspaceID)?.reportID; // It's possible that reports aren't fully loaded yet // in that case the reportID is undefined @@ -165,10 +205,10 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); const chatWithAccountManagerText = useMemo(() => { if (accountManagerReportID) { - const participants = ReportUtils.getParticipantsAccountIDsForDisplay(accountManagerReport, false, true); - const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs([participants?.at(0) ?? -1], personalDetails); + const participants = getParticipantsAccountIDsForDisplay(accountManagerReport, false, true); + const participantPersonalDetails = getPersonalDetailsForAccountIDs([participants?.at(0) ?? -1], personalDetails); const participantPersonalDetail = Object.values(participantPersonalDetails).at(0); - const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(participantPersonalDetail); + const displayName = getDisplayNameOrDefault(participantPersonalDetail); const login = participantPersonalDetail?.login; if (displayName && login) { return translate('common.chatWithAccountManager', {accountManagerDisplayName: `${displayName} (${login})`}); @@ -250,7 +290,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const shouldAdjustScrollView = useMemo(() => isComposerFocus && !modal?.willAlertModalBecomeVisible, [isComposerFocus, modal]); const viewportOffsetTop = useViewportOffsetTop(shouldAdjustScrollView); - const {reportPendingAction, reportErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); + const {reportPendingAction, reportErrors} = getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; const isOptimisticDelete = report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED; const indexOfLinkedMessage = useMemo( @@ -259,7 +299,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { ); const isPendingActionExist = !!reportActions.at(0)?.pendingAction; - const doesCreatedActionExists = useCallback(() => !!reportActions?.findLast((action) => ReportActionsUtils.isCreatedAction(action)), [reportActions]); + const doesCreatedActionExists = useCallback(() => !!reportActions?.findLast((action) => isCreatedAction(action)), [reportActions]); const isLinkedMessageAvailable = indexOfLinkedMessage > -1; // The linked report actions should have at least 15 messages (counting as 1 page) above them to fill the screen. @@ -269,13 +309,13 @@ function ReportScreen({route, navigation}: ReportScreenProps) { // If there's a non-404 error for the report we should show it instead of blocking the screen const hasHelpfulErrors = Object.keys(report?.errorFields ?? {}).some((key) => key !== 'notFound'); - const shouldHideReport = !hasHelpfulErrors && !ReportUtils.canAccessReport(report, policies, betas); + const shouldHideReport = !hasHelpfulErrors && !canAccessReport(report, policies, betas); - const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(reportID ?? '', reportActions ?? [], isOffline); + const transactionThreadReportID = getOneTransactionThreadReportID(reportID ?? '', reportActions ?? [], isOffline); const [transactionThreadReportActions = {}] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`); - const combinedReportActions = ReportActionsUtils.getCombinedReportActions(reportActions, transactionThreadReportID ?? null, Object.values(transactionThreadReportActions)); - const lastReportAction = [...combinedReportActions, parentReportAction].find((action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action)); - const isSingleTransactionView = ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report); + const combinedReportActions = getCombinedReportActions(reportActions, transactionThreadReportID ?? null, Object.values(transactionThreadReportActions)); + const lastReportAction = [...combinedReportActions, parentReportAction].find((action) => canEditReportAction(action) && !isMoneyRequestAction(action)); + const isSingleTransactionView = isMoneyRequest(report) || isTrackExpenseReport(report); const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? '-1'}`]; const isTopMostReportId = currentReportIDValue?.currentReportID === reportIDFromRoute; const didSubscribeToReportLeavingEvents = useRef(false); @@ -319,13 +359,13 @@ function ReportScreen({route, navigation}: ReportScreenProps) { } useEffect(() => { - if (!transactionThreadReportID || !route?.params?.reportActionID || !ReportUtils.isOneTransactionThread(linkedAction?.childReportID ?? '-1', reportID ?? '', linkedAction)) { + if (!transactionThreadReportID || !route?.params?.reportActionID || !isOneTransactionThread(linkedAction?.childReportID ?? '-1', reportID ?? '', linkedAction)) { return; } Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(route?.params?.reportID)); }, [transactionThreadReportID, route?.params?.reportActionID, route?.params?.reportID, linkedAction, reportID]); - if (ReportUtils.isMoneyRequestReport(report) || ReportUtils.isInvoiceReport(report)) { + if (isMoneyRequestReport(report) || isInvoiceReport(report)) { headerView = ( = CONST.REPORT.MIN_INITIAL_REPORT_ACTION_COUNT || isPendingActionExist || (doesCreatedActionExists() && reportActions.length > 0); const isLinkedActionDeleted = useMemo( - () => !!linkedAction && !ReportActionsUtils.shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, ReportUtils.canUserPerformWriteAction(report)), + () => !!linkedAction && !shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, canUserPerformWriteAction(report)), [linkedAction, report], ); const prevIsLinkedActionDeleted = usePrevious(linkedAction ? isLinkedActionDeleted : undefined); const isLinkedActionInaccessibleWhisper = useMemo( - () => !!linkedAction && ReportActionsUtils.isWhisperAction(linkedAction) && !(linkedAction?.whisperedToAccountIDs ?? []).includes(currentUserAccountID), + () => !!linkedAction && isWhisperAction(linkedAction) && !(linkedAction?.whisperedToAccountIDs ?? []).includes(currentUserAccountID), [currentUserAccountID, linkedAction], ); const [deleteTransactionNavigateBackUrl] = useOnyx(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL); @@ -368,12 +408,12 @@ function ReportScreen({route, navigation}: ReportScreenProps) { // Clear the URL after all interactions are processed to ensure all updates are completed before hiding the skeleton InteractionManager.runAfterInteractions(() => { requestAnimationFrame(() => { - Report.clearDeleteTransactionNavigateBackUrl(); + clearDeleteTransactionNavigateBackUrl(); }); }); }, [isFocused, deleteTransactionNavigateBackUrl]); - const isLoading = isLoadingApp ?? (!reportIDFromRoute || (!isSidebarLoaded && !isInNarrowPaneModal) || PersonalDetailsUtils.isPersonalDetailsEmpty()); + const isLoading = isLoadingApp ?? (!reportIDFromRoute || (!isSidebarLoaded && !isInNarrowPaneModal) || isPersonalDetailsEmpty()); const shouldShowSkeleton = (isLinkingToMessage && !isLinkedMessagePageReady) || @@ -382,7 +422,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { isLoadingReportOnyx || !isCurrentReportLoadedFromOnyx || // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - (deleteTransactionNavigateBackUrl && ReportUtils.getReportIDFromLink(deleteTransactionNavigateBackUrl) === report?.reportID) || + (deleteTransactionNavigateBackUrl && getReportIDFromLink(deleteTransactionNavigateBackUrl) === report?.reportID) || (!reportMetadata.isOptimisticReport && isLoading); const isLinkedActionBecomesDeleted = prevIsLinkedActionDeleted !== undefined && !prevIsLinkedActionDeleted && isLinkedActionDeleted; @@ -426,7 +466,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { if (shouldHideReport) { return true; } - return !!currentReportIDFormRoute && !ReportUtils.isValidReportIDFromPath(currentReportIDFormRoute); + return !!currentReportIDFormRoute && !isValidReportIDFromPath(currentReportIDFormRoute); }, [ shouldShowNotFoundLinkedAction, isLoadingApp, @@ -440,14 +480,14 @@ function ReportScreen({route, navigation}: ReportScreenProps) { ]); const fetchReport = useCallback(() => { - Report.openReport(reportIDFromRoute, reportActionIDFromRoute); + openReport(reportIDFromRoute, reportActionIDFromRoute); }, [reportIDFromRoute, reportActionIDFromRoute]); useEffect(() => { if (!reportID || !isFocused) { return; } - Report.updateLastVisitTime(reportID); + updateLastVisitTime(reportID); }, [reportID, isFocused]); useEffect(() => { @@ -466,7 +506,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const fetchReportIfNeeded = useCallback(() => { // Report ID will be empty when the reports collection is empty. // This could happen when we are loading the collection for the first time after logging in. - if (!ReportUtils.isValidReportIDFromPath(reportIDFromRoute)) { + if (!isValidReportIDFromPath(reportIDFromRoute)) { return; } @@ -518,7 +558,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { useEffect(() => { const interactionTask = InteractionManager.runAfterInteractions(() => { - ComposerActions.setShouldShowComposeInput(true); + setShouldShowComposeInput(true); }); return () => { interactionTask.cancel(); @@ -526,7 +566,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { return; } - Report.unsubscribeFromLeavingRoomReportChannel(reportID ?? ''); + unsubscribeFromLeavingRoomReportChannel(reportID ?? ''); }; // I'm disabling the warning, as it expects to use exhaustive deps, even though we want this useEffect to run only on the first render. @@ -556,10 +596,10 @@ function ReportScreen({route, navigation}: ReportScreenProps) { // If a user has chosen to leave a thread, and then returns to it (e.g. with the back button), we need to call `openReport` again in order to allow the user to rejoin and to receive real-time updates useEffect(() => { - if (!shouldUseNarrowLayout || !isFocused || prevIsFocused || !ReportUtils.isChatThread(report) || !ReportUtils.isHiddenForCurrentUser(report) || isSingleTransactionView) { + if (!shouldUseNarrowLayout || !isFocused || prevIsFocused || !isChatThread(report) || !isHiddenForCurrentUser(report) || isSingleTransactionView) { return; } - Report.openReport(reportID ?? ''); + openReport(reportID ?? ''); // We don't want to run this useEffect every time `report` is changed // Excluding shouldUseNarrowLayout from the dependency list to prevent re-triggering on screen resize events. @@ -577,8 +617,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const prevOnyxReportID = prevReport?.reportID; const wasReportRemoved = !!prevOnyxReportID && prevOnyxReportID === reportIDFromRoute && !onyxReportID; const isRemovalExpectedForReportType = - isEmpty(report) && - (ReportUtils.isMoneyRequest(prevReport) || ReportUtils.isMoneyRequestReport(prevReport) || ReportUtils.isPolicyExpenseChat(prevReport) || ReportUtils.isGroupChat(prevReport)); + isEmpty(report) && (isMoneyRequest(prevReport) || isMoneyRequestReport(prevReport) || isPolicyExpenseChat(prevReport) || isGroupChat(prevReport)); const didReportClose = wasReportRemoved && prevReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED; const isTopLevelPolicyRoomWithNoStatus = !report?.statusNum && !prevReport?.parentReportID && prevReport?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ROOM; const isClosedTopLevelPolicyRoom = wasReportRemoved && prevReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN && isTopLevelPolicyRoomWithNoStatus; @@ -589,7 +628,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { didReportClose || isRemovalExpectedForReportType || isClosedTopLevelPolicyRoom || - (prevIsDeletedParentAction && !isDeletedParentAction) + (prevDeletedParentAction && !deletedParentAction) ) { // Early return if the report we're passing isn't in a focused state. We only want to navigate to Concierge if the user leaves the room from another device or gets removed from the room while the report is in a focused state. // Prevent auto navigation for report in RHP @@ -603,14 +642,14 @@ function ReportScreen({route, navigation}: ReportScreenProps) { } if (prevReport?.parentReportID) { // Prevent navigation to the IOU/Expense Report if it is pending deletion. - if (ReportUtils.isMoneyRequestReportPendingDeletion(prevReport.parentReportID)) { + if (isMoneyRequestReportPendingDeletion(prevReport.parentReportID)) { return; } Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(prevReport.parentReportID)); return; } - Report.navigateToConciergeChat(); + navigateToConciergeChat(); return; } @@ -623,7 +662,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { } fetchReportIfNeeded(); - ComposerActions.setShouldShowComposeInput(true); + setShouldShowComposeInput(true); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [ route, @@ -640,12 +679,12 @@ function ReportScreen({route, navigation}: ReportScreenProps) { reportIDFromRoute, lastReportIDFromRoute, isFocused, - isDeletedParentAction, - prevIsDeletedParentAction, + deletedParentAction, + prevDeletedParentAction, ]); useEffect(() => { - if (!ReportUtils.isValidReportIDFromPath(reportIDFromRoute)) { + if (!isValidReportIDFromPath(reportIDFromRoute)) { return; } // Ensures the optimistic report is created successfully @@ -660,7 +699,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { let interactionTask: ReturnType | null = null; if (!didSubscribeToReportLeavingEvents.current && didCreateReportSuccessfully) { interactionTask = InteractionManager.runAfterInteractions(() => { - Report.subscribeToReportLeavingEvents(reportIDFromRoute); + subscribeToReportLeavingEvents(reportIDFromRoute); didSubscribeToReportLeavingEvents.current = true; }); } @@ -719,23 +758,23 @@ function ReportScreen({route, navigation}: ReportScreenProps) { }, [isLinkedActionInaccessibleWhisper]); useEffect(() => { - if (!!report?.lastReadTime || !ReportUtils.isTaskReport(report)) { + if (!!report?.lastReadTime || !isTaskReport(report)) { return; } // After creating the task report then navigating to task detail we don't have any report actions and the last read time is empty so We need to update the initial last read time when opening the task report detail. - Report.readNewestAction(report?.reportID ?? ''); + readNewestAction(report?.reportID ?? ''); }, [report]); const mostRecentReportAction = reportActions.at(0); const isMostRecentReportIOU = mostRecentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; const isSingleIOUReportAction = reportActions.filter((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU).length === 1; - const isSingleExpenseReport = ReportUtils.isExpenseReport(report) && isMostRecentReportIOU && isSingleIOUReportAction; - const isSingleInvoiceReport = ReportUtils.isInvoiceReport(report) && isMostRecentReportIOU && isSingleIOUReportAction; + const isSingleExpenseReport = isExpenseReport(report) && isMostRecentReportIOU && isSingleIOUReportAction; + const isSingleInvoiceReport = isInvoiceReport(report) && isMostRecentReportIOU && isSingleIOUReportAction; const shouldShowMostRecentReportAction = !!mostRecentReportAction && !isSingleExpenseReport && !isSingleInvoiceReport && - !ReportActionsUtils.isActionOfType(mostRecentReportAction, CONST.REPORT.ACTIONS.TYPE.CREATED) && - !ReportActionsUtils.isDeletedAction(mostRecentReportAction); + !isActionOfType(mostRecentReportAction, CONST.REPORT.ACTIONS.TYPE.CREATED) && + !isDeletedAction(mostRecentReportAction); const lastRoute = usePrevious(route); const lastReportActionIDFromRoute = usePrevious(reportActionIDFromRoute); @@ -777,7 +816,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { > {headerView} - {!!accountManagerReportID && ReportUtils.isConciergeChatReport(report) && isBannerVisible && ( + {!!accountManagerReportID && isConciergeChatReport(report) && isBannerVisible && ( )} - + - !ReportActionsUtils.wasActionTakenByCurrentUser(message) && - ReportActionsUtils.wasActionCreatedWhileOffline(message, isOffline, lastOfflineAt.current, lastOnlineAt.current, preferredLocale), + !wasActionTakenByCurrentUser(message) && wasActionCreatedWhileOffline(message, isOffline, lastOfflineAt.current, lastOnlineAt.current, preferredLocale), [isOffline, lastOfflineAt, lastOnlineAt, preferredLocale], ); @@ -236,7 +258,7 @@ function ReportActionsList({ */ const earliestReceivedOfflineMessageIndex = useMemo(() => { // Create a list of (sorted) indices of message that were received while offline - const receviedOfflineMessages = sortedReportActions.reduce((acc, message, index) => { + const receivedOfflineMessages = sortedReportActions.reduce((acc, message, index) => { if (wasMessageReceivedWhileOffline(message)) { acc[index] = index; } @@ -245,7 +267,7 @@ function ReportActionsList({ }, []); // The last index in the list is the earliest message that was received while offline - return receviedOfflineMessages.at(-1); + return receivedOfflineMessages.at(-1); }, [sortedReportActions, wasMessageReceivedWhileOffline]); /** @@ -254,7 +276,7 @@ function ReportActionsList({ const unreadMarkerReportActionID = useMemo(() => { const shouldDisplayNewMarker = (message: OnyxTypes.ReportAction, index: number): boolean => { const nextMessage = sortedVisibleReportActions.at(index + 1); - const isNextMessageUnread = !!nextMessage && ReportActionsUtils.isReportActionUnread(nextMessage, unreadMarkerTime); + const isNextMessageUnread = !!nextMessage && isReportActionUnread(nextMessage, unreadMarkerTime); // If the current message is the earliest message received while offline, we want to display the unread marker above this message. const isEarliestReceivedOfflineMessage = index === earliestReceivedOfflineMessageIndex; @@ -263,11 +285,11 @@ function ReportActionsList({ } // If the unread marker should be hidden or is not within the visible area, don't show the unread marker. - if (ReportActionsUtils.shouldHideNewMarker(message)) { + if (shouldHideNewMarker(message)) { return false; } - const isCurrentMessageUnread = ReportActionsUtils.isReportActionUnread(message, unreadMarkerTime); + const isCurrentMessageUnread = isReportActionUnread(message, unreadMarkerTime); // If the current message is read or the next message is unread, don't show the unread marker. if (!isCurrentMessageUnread || isNextMessageUnread) { @@ -279,7 +301,7 @@ function ReportActionsList({ }; // If no unread marker exists, don't set an unread marker for newly added messages from the current user. - const isFromCurrentUser = accountID === (ReportActionsUtils.isReportPreviewAction(message) ? message.childLastActorAccountID : message.actorAccountID); + const isFromCurrentUser = accountID === (isReportPreviewAction(message) ? message.childLastActorAccountID : message.actorAccountID); const isNewMessage = !prevSortedVisibleReportActionsObjects[message.reportActionID]; // The unread marker will show if the action's `created` time is later than `unreadMarkerTime`. @@ -394,13 +416,13 @@ function ReportActionsList({ return; } - if (ReportUtils.isUnread(report)) { + if (isUnread(report)) { // On desktop, when the notification center is displayed, isVisible will return false. // Currently, there's no programmatic way to dismiss the notification center panel. // 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) && scrollingVerticalOffset.current < MSG_VISIBLE_THRESHOLD) { - Report.readNewestAction(report.reportID); + readNewestAction(report.reportID); if (isFromNotification) { Navigation.setParams({referrer: undefined}); } @@ -453,7 +475,7 @@ function ReportActionsList({ // This callback is triggered when a new action arrives via Pusher and the event is emitted from Report.js. This allows us to maintain // a single source of truth for the "new action" event instead of trying to derive that a new action has appeared from looking at props. - const unsubscribe = Report.subscribeToNewActionEvent(report.reportID, scrollToBottomForCurrentUserAction); + const unsubscribe = subscribeToNewActionEvent(report.reportID, scrollToBottomForCurrentUserAction); const cleanup = () => { if (!unsubscribe) { @@ -480,7 +502,7 @@ function ReportActionsList({ if (scrollingVerticalOffset.current < VERTICAL_OFFSET_THRESHOLD && isFloatingMessageCounterVisible) { if (readActionSkipped.current) { readActionSkipped.current = false; - Report.readNewestAction(report.reportID); + readNewestAction(report.reportID); } setIsFloatingMessageCounterVisible(false); } @@ -495,13 +517,13 @@ function ReportActionsList({ const scrollToBottomAndMarkReportAsRead = () => { if (!hasNewestReportAction) { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID)); - Report.openReport(report.reportID); + openReport(report.reportID); reportScrollManager.scrollToBottom(); return; } reportScrollManager.scrollToBottom(); readActionSkipped.current = false; - Report.readNewestAction(report.reportID); + readNewestAction(report.reportID); }; /** @@ -523,11 +545,11 @@ function ReportActionsList({ * This is so that it will not be conflicting with header's separator line. */ const shouldHideThreadDividerLine = useMemo( - (): boolean => ReportActionsUtils.getFirstVisibleReportActionID(sortedReportActions, isOffline) === unreadMarkerReportActionID, + (): boolean => getFirstVisibleReportActionID(sortedReportActions, isOffline) === unreadMarkerReportActionID, [sortedReportActions, isOffline, unreadMarkerReportActionID], ); - const firstVisibleReportActionID = useMemo(() => ReportActionsUtils.getFirstVisibleReportActionID(sortedReportActions, isOffline), [sortedReportActions, isOffline]); + const firstVisibleReportActionID = useMemo(() => getFirstVisibleReportActionID(sortedReportActions, isOffline), [sortedReportActions, isOffline]); const shouldUseThreadDividerLine = useMemo(() => { const topReport = sortedVisibleReportActions.length > 0 ? sortedVisibleReportActions.at(sortedVisibleReportActions.length - 1) : null; @@ -536,15 +558,15 @@ function ReportActionsList({ return false; } - if (ReportActionsUtils.isTransactionThread(parentReportAction)) { - return !ReportActionsUtils.isDeletedParentAction(parentReportAction) && !ReportActionsUtils.isReversedTransaction(parentReportAction); + if (isTransactionThread(parentReportAction)) { + return !isDeletedParentAction(parentReportAction) && !isReversedTransaction(parentReportAction); } - if (ReportUtils.isTaskReport(report)) { - return !ReportUtils.isCanceledTaskReport(report, parentReportAction); + if (isTaskReport(report)) { + return !isCanceledTaskReport(report, parentReportAction); } - return ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report) || ReportUtils.isInvoiceReport(report); + return isExpenseReport(report) || isIOUReport(report) || isInvoiceReport(report); }, [parentReportAction, report, sortedVisibleReportActions]); useEffect(() => { @@ -564,20 +586,20 @@ function ReportActionsList({ const newMessageTimeReference = lastMessageTime.current && report.lastReadTime && lastMessageTime.current > report.lastReadTime ? userActiveSince.current : report.lastReadTime; lastMessageTime.current = null; - const isArchivedReport = ReportUtils.isArchivedRoom(report); + const isArchivedReport = isArchivedRoom(report); const hasNewMessagesInView = scrollingVerticalOffset.current < MSG_VISIBLE_THRESHOLD; const hasUnreadReportAction = sortedVisibleReportActions.some( (reportAction) => newMessageTimeReference && newMessageTimeReference < reportAction.created && - (ReportActionsUtils.isReportPreviewAction(reportAction) ? reportAction.childLastActorAccountID : reportAction.actorAccountID) !== Report.getCurrentUserAccountID(), + (isReportPreviewAction(reportAction) ? reportAction.childLastActorAccountID : reportAction.actorAccountID) !== getCurrentUserAccountID(), ); if (!isArchivedReport && (!hasNewMessagesInView || !hasUnreadReportAction)) { return; } - Report.readNewestAction(report.reportID); + readNewestAction(report.reportID); 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 @@ -599,8 +621,8 @@ function ReportActionsList({ transactionThreadReport={transactionThreadReport} linkedReportActionID={linkedReportActionID} displayAsGroup={ - !ReportActionsUtils.isConsecutiveChronosAutomaticTimerAction(sortedVisibleReportActions, index, ReportUtils.chatIncludesChronosWithID(reportAction?.reportID)) && - ReportActionsUtils.isConsecutiveActionMadeByPreviousActor(sortedVisibleReportActions, index) + !isConsecutiveChronosAutomaticTimerAction(sortedVisibleReportActions, index, chatIncludesChronosWithID(reportAction?.reportID)) && + isConsecutiveActionMadeByPreviousActor(sortedVisibleReportActions, index) } mostRecentIOUReportActionID={mostRecentIOUReportActionID} shouldHideThreadDividerLine={shouldHideThreadDividerLine} @@ -629,11 +651,11 @@ function ReportActionsList({ // Native mobile does not render updates flatlist the changes even though component did update called. // To notify there something changes we can use extraData prop to flatlist const extraData = useMemo( - () => [shouldUseNarrowLayout ? unreadMarkerReportActionID : undefined, ReportUtils.isArchivedRoom(report, reportNameValuePairs)], + () => [shouldUseNarrowLayout ? unreadMarkerReportActionID : undefined, isArchivedRoom(report, reportNameValuePairs)], [unreadMarkerReportActionID, shouldUseNarrowLayout, report, reportNameValuePairs], ); - const hideComposer = !ReportUtils.canUserPerformWriteAction(report); - const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(personalDetailsList, report, currentUserPersonalDetails.accountID) && !isComposerFullSize; + const hideComposer = !canUserPerformWriteAction(report); + const shouldShowReportRecipientLocalTime = canShowReportRecipientLocalTime(personalDetailsList, report, currentUserPersonalDetails.accountID) && !isComposerFullSize; // eslint-disable-next-line react-compiler/react-compiler const canShowHeader = isOffline || hasHeaderRendered.current; diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 2cf042e46b41..ee7f54262b09 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -8,21 +8,29 @@ import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import {getNewerActions, getOlderActions, openReport, updateLoadingInitialReportAction} from '@libs/actions/Report'; +import Timing from '@libs/actions/Timing'; import DateUtils from '@libs/DateUtils'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {AuthScreensParamList} from '@libs/Navigation/types'; -import * as NumberUtils from '@libs/NumberUtils'; -import {generateNewRandomInt} from '@libs/NumberUtils'; +import {generateNewRandomInt, rand64} from '@libs/NumberUtils'; import Performance from '@libs/Performance'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; +import { + getCombinedReportActions, + getMostRecentIOURequestActionID, + getOriginalMessage, + getReportPreviewAction, + getSortedReportActionsForDisplay, + isCreatedAction, + isDeletedParentAction, + isMoneyRequestAction, + shouldReportActionBeVisible, +} from '@libs/ReportActionsUtils'; +import {buildOptimisticCreatedReportAction, buildOptimisticIOUReportAction, canUserPerformWriteAction, isMoneyRequestReport, isUserCreatedPolicyRoom} from '@libs/ReportUtils'; import {didUserLogInDuringSession} from '@libs/SessionUtils'; import shouldFetchReport from '@libs/shouldFetchReport'; import {ReactionListContext} from '@pages/home/ReportScreenContext'; -import * as Report from '@userActions/Report'; -import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -88,8 +96,7 @@ function ReportActionsView({ const route = useRoute>(); const [session] = useOnyx(ONYXKEYS.SESSION); const [transactionThreadReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID ?? CONST.DEFAULT_NUMBER_ID}`, { - selector: (reportActions: OnyxEntry) => - ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, ReportUtils.canUserPerformWriteAction(report), true), + selector: (reportActions: OnyxEntry) => getSortedReportActionsForDisplay(reportActions, canUserPerformWriteAction(report), true), }); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID ?? CONST.DEFAULT_NUMBER_ID}`); const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report.reportID}`); @@ -115,7 +122,7 @@ function ReportActionsView({ return; } - Report.openReport(reportID, reportActionID); + openReport(reportID, reportActionID); }; useEffect(() => { @@ -123,7 +130,7 @@ function ReportActionsView({ if (!reportActionID || !isOffline) { return; } - Report.updateLoadingInitialReportAction(report.reportID); + updateLoadingInitialReportAction(report.reportID); }, [isOffline, report.reportID, reportActionID]); // Change the list ID only for comment linking to get the positioning right @@ -146,24 +153,24 @@ function ReportActionsView({ // and we also generate an expense action if the number of expenses in allReportActions is less than the total number of expenses // to display at least one expense action to match the total data. const reportActionsToDisplay = useMemo(() => { - if (!ReportUtils.isMoneyRequestReport(report) || !allReportActions.length) { + if (!isMoneyRequestReport(report) || !allReportActions.length) { return allReportActions; } const actions = [...allReportActions]; const lastAction = allReportActions.at(-1); - if (lastAction && !ReportActionsUtils.isCreatedAction(lastAction)) { - const optimisticCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(String(report?.ownerAccountID), DateUtils.subtractMillisecondsFromDateTime(lastAction.created, 1)); + if (lastAction && !isCreatedAction(lastAction)) { + const optimisticCreatedAction = buildOptimisticCreatedReportAction(String(report?.ownerAccountID), DateUtils.subtractMillisecondsFromDateTime(lastAction.created, 1)); optimisticCreatedAction.pendingAction = null; actions.push(optimisticCreatedAction); } - const reportPreviewAction = ReportActionsUtils.getReportPreviewAction(report.chatReportID, report.reportID); + const reportPreviewAction = getReportPreviewAction(report.chatReportID, report.reportID); const moneyRequestActions = allReportActions.filter((action) => { - const originalMessage = ReportActionsUtils.isMoneyRequestAction(action) ? ReportActionsUtils.getOriginalMessage(action) : undefined; + const originalMessage = isMoneyRequestAction(action) ? getOriginalMessage(action) : undefined; return ( - ReportActionsUtils.isMoneyRequestAction(action) && + isMoneyRequestAction(action) && originalMessage && (originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || !!(originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && originalMessage?.IOUDetails) || @@ -172,13 +179,13 @@ function ReportActionsView({ }); if (report.total && moneyRequestActions.length < (reportPreviewAction?.childMoneyRequestCount ?? 0) && isEmptyObject(transactionThreadReport)) { - const optimisticIOUAction = ReportUtils.buildOptimisticIOUReportAction( + const optimisticIOUAction = buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.CREATE, 0, CONST.CURRENCY.USD, '', [], - NumberUtils.rand64(), + rand64(), undefined, report.reportID, false, @@ -203,7 +210,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 reportActions = useMemo( - () => ReportActionsUtils.getCombinedReportActions(reportActionsToDisplay, transactionThreadReportID ?? null, transactionThreadReportActions ?? []), + () => getCombinedReportActions(reportActionsToDisplay, transactionThreadReportID ?? null, transactionThreadReportActions ?? []), [reportActionsToDisplay, transactionThreadReportActions, transactionThreadReportID], ); @@ -215,18 +222,15 @@ function ReportActionsView({ [allReportActions, transactionThreadReportActions, transactionThreadReport?.parentReportActionID], ); - const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); + const canPerformWriteAction = canUserPerformWriteAction(report); const visibleReportActions = useMemo( () => reportActions.filter( (reportAction) => - (isOffline || - ReportActionsUtils.isDeletedParentAction(reportAction) || - reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || - reportAction.errors) && - ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction), + (isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && + shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction), ), - [reportActions, isOffline, canUserPerformWriteAction], + [reportActions, isOffline, canPerformWriteAction], ); const reportActionIDMap = useMemo(() => { @@ -238,7 +242,7 @@ function ReportActionsView({ }, [allReportActions, reportID, transactionThreadReport, reportActions]); const newestReportAction = useMemo(() => reportActions?.at(0), [reportActions]); - const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); + const mostRecentIOUReportActionID = useMemo(() => getMostRecentIOURequestActionID(reportActions), [reportActions]); const hasNewestReportAction = visibleReportActions.at(0)?.created === report.lastVisibleActionCreated || visibleReportActions.at(0)?.created === transactionThreadReport?.lastVisibleActionCreated; const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]); @@ -297,14 +301,14 @@ function ReportActionsView({ if (!isEmptyObject(transactionThreadReport)) { // Get older actions based on the oldest reportAction for the current report const oldestActionCurrentReport = reportActionIDMap.findLast((item) => item.reportID === reportID); - Report.getOlderActions(oldestActionCurrentReport?.reportID, oldestActionCurrentReport?.reportActionID); + getOlderActions(oldestActionCurrentReport?.reportID, oldestActionCurrentReport?.reportActionID); // Get older actions based on the oldest reportAction for the transaction thread report const oldestActionTransactionThreadReport = reportActionIDMap.findLast((item) => item.reportID === transactionThreadReport.reportID); - Report.getOlderActions(oldestActionTransactionThreadReport?.reportID, oldestActionTransactionThreadReport?.reportActionID); + getOlderActions(oldestActionTransactionThreadReport?.reportID, oldestActionTransactionThreadReport?.reportActionID); } else { // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments - Report.getOlderActions(reportID, oldestReportAction.reportActionID); + getOlderActions(reportID, oldestReportAction.reportActionID); } }, [ @@ -345,13 +349,13 @@ function ReportActionsView({ 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, newestActionCurrentReport?.reportActionID); + getNewerActions(newestActionCurrentReport?.reportID, newestActionCurrentReport?.reportActionID); // 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, newestActionTransactionThreadReport?.reportActionID); + getNewerActions(newestActionTransactionThreadReport?.reportID, newestActionTransactionThreadReport?.reportActionID); } else if (newestReportAction) { - Report.getNewerActions(reportID, newestReportAction.reportActionID); + getNewerActions(reportID, newestReportAction.reportActionID); } }, [