From 58c34fa3f81a966ff61f1981c28bd6f65b7c37a4 Mon Sep 17 00:00:00 2001
From: rory <rory@expensify.com>
Date: Tue, 9 Jul 2024 13:06:53 -0700
Subject: [PATCH 1/2] DRY up React logic to get sorted report actions

---
 src/hooks/usePaginatedReportActions.ts | 39 ++++++++++++++++++
 src/pages/home/ReportScreen.tsx        | 57 ++++++--------------------
 2 files changed, 52 insertions(+), 44 deletions(-)
 create mode 100644 src/hooks/usePaginatedReportActions.ts

diff --git a/src/hooks/usePaginatedReportActions.ts b/src/hooks/usePaginatedReportActions.ts
new file mode 100644
index 000000000000..b806c0dea95a
--- /dev/null
+++ b/src/hooks/usePaginatedReportActions.ts
@@ -0,0 +1,39 @@
+import {useMemo} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import PaginationUtils from '@libs/PaginationUtils';
+import * as ReportActionsUtils from '@libs/ReportActionsUtils';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+/**
+ * Get the longest continuous chunk of reportActions including the linked reportAction. If not linking to a specific action, returns the continuous chunk of newest reportActions.
+ */
+function usePaginatedReportActions(reportID?: string, reportActionID?: string) {
+    // Use `||` instead of `??` to handle empty string.
+    // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+    const reportIDWithDefault = reportID || '-1';
+
+    const [sortedAllReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportIDWithDefault}`, {
+        canEvict: false,
+        selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true),
+    });
+    const [reportActionPages] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportIDWithDefault}`);
+
+    const reportActions = useMemo(() => {
+        if (!sortedAllReportActions?.length) {
+            return [];
+        }
+        return PaginationUtils.getContinuousChain(sortedAllReportActions, reportActionPages ?? [], (reportAction) => reportAction.reportActionID, reportActionID);
+    }, [reportActionID, reportActionPages, sortedAllReportActions]);
+
+    const linkedAction = useMemo(
+        () => sortedAllReportActions?.find((reportAction) => String(reportAction.reportActionID) === String(reportActionID)),
+        [reportActionID, sortedAllReportActions],
+    );
+
+    return {
+        reportActions,
+        linkedAction,
+    };
+}
+
+export default usePaginatedReportActions;
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index 414e324484f9..54e2bb4a0e7e 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -26,17 +26,16 @@ import useDeepCompareRef from '@hooks/useDeepCompareRef';
 import useIsReportOpenInRHP from '@hooks/useIsReportOpenInRHP';
 import useLocalize from '@hooks/useLocalize';
 import useNetwork from '@hooks/useNetwork';
+import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
 import usePermissions from '@hooks/usePermissions';
 import usePrevious from '@hooks/usePrevious';
 import useThemeStyles from '@hooks/useThemeStyles';
 import useViewportOffsetTop from '@hooks/useViewportOffsetTop';
 import useWindowDimensions from '@hooks/useWindowDimensions';
-import {getCurrentUserAccountID} from '@libs/actions/Report';
 import Timing from '@libs/actions/Timing';
 import Log from '@libs/Log';
 import Navigation from '@libs/Navigation/Navigation';
 import clearReportNotifications from '@libs/Notification/clearReportNotifications';
-import PaginationUtils from '@libs/PaginationUtils';
 import Performance from '@libs/Performance';
 import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
 import * as ReportActionsUtils from '@libs/ReportActionsUtils';
@@ -70,12 +69,6 @@ type ReportScreenOnyxProps = {
     /** The policies which the user has access to */
     policies: OnyxCollection<OnyxTypes.Policy>;
 
-    /** An array containing all report actions related to this report, sorted based on a date criterion */
-    sortedAllReportActions: OnyxTypes.ReportAction[];
-
-    /** Pagination data for sortedAllReportActions */
-    reportActionPages: OnyxEntry<OnyxTypes.Pages>;
-
     /** Additional report details */
     reportNameValuePairs: OnyxEntry<OnyxTypes.ReportNameValuePairs>;
 
@@ -124,8 +117,6 @@ function ReportScreen({
     betas = [],
     route,
     reportNameValuePairs,
-    sortedAllReportActions,
-    reportActionPages,
     reportMetadata = {
         isLoadingInitialReportActions: true,
         isLoadingOlderReportActions: false,
@@ -291,12 +282,9 @@ function ReportScreen({
     const prevReport = usePrevious(report);
     const prevUserLeavingStatus = usePrevious(userLeavingStatus);
     const [isLinkingToMessage, setIsLinkingToMessage] = useState(!!reportActionIDFromRoute);
-    const reportActions = useMemo(() => {
-        if (!sortedAllReportActions.length) {
-            return [];
-        }
-        return PaginationUtils.getContinuousChain(sortedAllReportActions, reportActionPages ?? [], (reportAction) => reportAction.reportActionID, reportActionIDFromRoute);
-    }, [reportActionIDFromRoute, sortedAllReportActions, reportActionPages]);
+
+    const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: (value) => value?.accountID});
+    const {reportActions, linkedAction} = usePaginatedReportActions(report.reportID, reportActionIDFromRoute);
 
     // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger.
     // If we have cached reportActions, they will be shown immediately.
@@ -321,10 +309,6 @@ function ReportScreen({
     const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}];
     const isEmptyChat = useMemo(() => ReportUtils.isEmptyReport(report), [report]);
     const isOptimisticDelete = report.statusNum === CONST.REPORT.STATUS_NUM.CLOSED;
-    const isLinkedMessageAvailable = useMemo(
-        (): boolean => sortedAllReportActions.findIndex((obj) => String(obj.reportActionID) === String(reportActionIDFromRoute)) > -1,
-        [sortedAllReportActions, reportActionIDFromRoute],
-    );
 
     // 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');
@@ -422,7 +406,7 @@ function ReportScreen({
     // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
     const isLoading = isLoadingApp || !reportIDFromRoute || (!isSidebarLoaded && !isReportOpenInRHP) || PersonalDetailsUtils.isPersonalDetailsEmpty();
     const shouldShowSkeleton =
-        !isLinkedMessageAvailable &&
+        !linkedAction &&
         (isLinkingToMessage ||
             !isCurrentReportLoadedFromOnyx ||
             (reportActions.length === 0 && !!reportMetadata?.isLoadingInitialReportActions) ||
@@ -692,28 +676,22 @@ function ReportScreen({
         fetchReport();
     }, [fetchReport]);
 
-    const {isLinkedReportActionDeleted, isInaccessibleWhisper} = useMemo(() => {
-        const currentUserAccountID = getCurrentUserAccountID();
-        if (!reportActionIDFromRoute || !sortedAllReportActions) {
-            return {isLinkedReportActionDeleted: false, isInaccessibleWhisper: false};
-        }
-        const action = sortedAllReportActions.find((item) => item.reportActionID === reportActionIDFromRoute);
-        return {
-            isLinkedReportActionDeleted: action && !ReportActionsUtils.shouldReportActionBeVisible(action, action.reportActionID),
-            isInaccessibleWhisper: action && ReportActionsUtils.isWhisperAction(action) && !(action?.whisperedToAccountIDs ?? []).includes(currentUserAccountID),
-        };
-    }, [reportActionIDFromRoute, sortedAllReportActions]);
+    const isLinkedActionDeleted = useMemo(() => !!linkedAction && !ReportActionsUtils.shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID), [linkedAction]);
+    const isLinkedActionInaccessibleWhisper = useMemo(
+        () => !!linkedAction && ReportActionsUtils.isWhisperAction(linkedAction) && !(linkedAction?.whisperedToAccountIDs ?? []).includes(currentUserAccountID),
+        [currentUserAccountID, linkedAction],
+    );
 
     // If user redirects to an inaccessible whisper via a deeplink, on a report they have access to,
     // then we set reportActionID as empty string, so we display them the report and not the "Not found page".
     useEffect(() => {
-        if (!isInaccessibleWhisper) {
+        if (!isLinkedActionInaccessibleWhisper) {
             return;
         }
         Navigation.isNavigationReady().then(() => {
             Navigation.setParams({reportActionID: ''});
         });
-    }, [isInaccessibleWhisper]);
+    }, [isLinkedActionInaccessibleWhisper]);
 
     useEffect(() => {
         if (!!report.lastReadTime || !ReportUtils.isTaskReport(report)) {
@@ -723,7 +701,7 @@ function ReportScreen({
         Report.readNewestAction(report.reportID);
     }, [report]);
 
-    if ((!isInaccessibleWhisper && isLinkedReportActionDeleted) ?? (!shouldShowSkeleton && reportActionIDFromRoute && reportActions?.length === 0 && !isLinkingToMessage)) {
+    if ((!isLinkedActionInaccessibleWhisper && isLinkedActionDeleted) ?? (!shouldShowSkeleton && reportActionIDFromRoute && reportActions?.length === 0 && !isLinkingToMessage)) {
         return (
             <BlockingView
                 icon={Illustrations.ToddBehindCloud}
@@ -838,14 +816,6 @@ export default withCurrentReportID(
             isSidebarLoaded: {
                 key: ONYXKEYS.IS_SIDEBAR_LOADED,
             },
-            sortedAllReportActions: {
-                key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`,
-                canEvict: false,
-                selector: (allReportActions: OnyxEntry<OnyxTypes.ReportActions>) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true),
-            },
-            reportActionPages: {
-                key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${getReportID(route)}`,
-            },
             reportNameValuePairs: {
                 key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${getReportID(route)}`,
                 allowStaleData: true,
@@ -874,7 +844,6 @@ export default withCurrentReportID(
             ReportScreen,
             (prevProps, nextProps) =>
                 prevProps.isSidebarLoaded === nextProps.isSidebarLoaded &&
-                lodashIsEqual(prevProps.sortedAllReportActions, nextProps.sortedAllReportActions) &&
                 lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) &&
                 lodashIsEqual(prevProps.betas, nextProps.betas) &&
                 lodashIsEqual(prevProps.policies, nextProps.policies) &&

From 99afbbe6ee3785133d737afa5dc30ff8e7d8c1c3 Mon Sep 17 00:00:00 2001
From: rory <rory@expensify.com>
Date: Tue, 9 Jul 2024 13:08:42 -0700
Subject: [PATCH 2/2] Use usePaginatedReportActions in ReportDetailsPage

---
 src/pages/ReportDetailsPage.tsx | 15 ++-------------
 1 file changed, 2 insertions(+), 13 deletions(-)

diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx
index c9440ee548af..6f3ca95102cd 100644
--- a/src/pages/ReportDetailsPage.tsx
+++ b/src/pages/ReportDetailsPage.tsx
@@ -24,11 +24,11 @@ import ScrollView from '@components/ScrollView';
 import Text from '@components/Text';
 import useLocalize from '@hooks/useLocalize';
 import useNetwork from '@hooks/useNetwork';
+import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
 import useThemeStyles from '@hooks/useThemeStyles';
 import Navigation from '@libs/Navigation/Navigation';
 import type {ReportDetailsNavigatorParamList} from '@libs/Navigation/types';
 import * as OptionsListUtils from '@libs/OptionsListUtils';
-import PaginationUtils from '@libs/PaginationUtils';
 import * as PolicyUtils from '@libs/PolicyUtils';
 import * as ReportActionsUtils from '@libs/ReportActionsUtils';
 import * as ReportUtils from '@libs/ReportUtils';
@@ -86,18 +86,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
     // The app would crash due to subscribing to the entire report collection if parentReportID is an empty string. So we should have a fallback ID here.
     // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
     const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID || '-1'}`);
-    const [sortedAllReportActions = []] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID || '-1'}`, {
-        canEvict: false,
-        selector: (allReportActions: OnyxEntry<OnyxTypes.ReportActions>) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true),
-    });
-    const [reportActionPages = []] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${report.reportID || '-1'}`);
-
-    const reportActions = useMemo(() => {
-        if (!sortedAllReportActions.length) {
-            return [];
-        }
-        return PaginationUtils.getContinuousChain(sortedAllReportActions, reportActionPages, (reportAction) => reportAction.reportActionID);
-    }, [sortedAllReportActions, reportActionPages]);
+    const {reportActions} = usePaginatedReportActions(report.reportID || '-1');
 
     const transactionThreadReportID = useMemo(
         () => ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, reportActions ?? [], isOffline),
