diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 8a5d5f2bfac62..4a2c127f64289 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -5,7 +5,6 @@ import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; import React, {memo, useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {StyleSheet, View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; import type {BlockingViewProps} from '@components/BlockingViews/BlockingView'; import BlockingView from '@components/BlockingViews/BlockingView'; import Icon from '@components/Icon'; @@ -24,8 +23,6 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import getPlatform from '@libs/getPlatform'; import Log from '@libs/Log'; -import {getLastVisibleActionIncludingTransactionThread, getOriginalMessage, isActionableTrackExpense, isInviteOrRemovedAction} from '@libs/ReportActionsUtils'; -import {canUserPerformWriteAction as canUserPerformWriteActionUtil} from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; @@ -49,16 +46,11 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const reportAttributes = useReportAttributes(); - const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS); - const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); const [policy] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); - const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [onboarding] = useOnyx(ONYXKEYS.NVP_ONBOARDING); const [isFullscreenVisible] = useOnyx(ONYXKEYS.FULLSCREEN_VISIBILITY); - const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const theme = useTheme(); @@ -162,10 +154,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const reportID = item.reportID; const itemReportAttributes = reportAttributes?.[reportID]; const itemParentReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${item.parentReportID}`]; - const itemReportNameValuePairs = reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`]; const itemOneTransactionThreadReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${itemReportAttributes?.oneTransactionThreadReportID}`]; - const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${item?.parentReportID}`]; - const itemParentReportAction = item?.parentReportActionID ? itemParentReportActions?.[item?.parentReportActionID] : undefined; let invoiceReceiverPolicyID = '-1'; if (item?.invoiceReceiver && 'policyID' in item.invoiceReceiver) { @@ -177,37 +166,8 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const itemInvoiceReceiverPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`]; const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${item?.policyID}`]; - const hasDraftComment = - !!draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] && - !draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]?.match(CONST.REGEX.EMPTY_COMMENT); - - const isReportArchived = !!itemReportNameValuePairs?.private_isArchived; - const canUserPerformWrite = canUserPerformWriteActionUtil(item, isReportArchived); - - const lastAction = getLastVisibleActionIncludingTransactionThread( - reportID, - canUserPerformWrite, - reportActions, - visibleReportActionsData, - itemOneTransactionThreadReport?.reportID, - ); - - // Only override lastMessageTextFromReport when a track expense whisper's transaction has been deleted, to prevent showing stale text. - let lastMessageTextFromReport: string | undefined; - if (isActionableTrackExpense(lastAction)) { - const whisperTransactionID = getOriginalMessage(lastAction)?.transactionID; - if (whisperTransactionID && !transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${whisperTransactionID}`]) { - lastMessageTextFromReport = ''; - } - } const shouldShowRBRorGBRTooltip = firstReportIDWithGBRorRBR === reportID; - let lastActionReport: OnyxEntry | undefined; - if (isInviteOrRemovedAction(lastAction)) { - const lastActionOriginalMessage = lastAction?.actionName ? getOriginalMessage(lastAction) : null; - lastActionReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${lastActionOriginalMessage?.reportID}`]; - } - return ( ); @@ -245,11 +198,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio [ reportAttributes, reports, - reportNameValuePairs, - reportActions, policy, - transactions, - draftComments, personalDetails, firstReportIDWithGBRorRBR, isFullscreenVisible, @@ -263,45 +212,13 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio isScreenFocused, localeCompare, translate, - visibleReportActionsData, currentUserAccountID, ], ); const extraData = useMemo( - () => [ - reportActions, - reportAttributes, - reports, - reportAttributes, - reportNameValuePairs, - policy, - personalDetails, - data.length, - optionMode, - transactions, - draftComments, - isOffline, - isScreenFocused, - isReportsSplitNavigatorLast, - visibleReportActionsData, - ], - [ - reportActions, - reportAttributes, - reports, - reportNameValuePairs, - policy, - personalDetails, - data.length, - optionMode, - transactions, - draftComments, - isOffline, - isScreenFocused, - isReportsSplitNavigatorLast, - visibleReportActionsData, - ], + () => [reports, reportAttributes, policy, personalDetails, data.length, optionMode, isOffline, isScreenFocused, isReportsSplitNavigatorLast], + [reports, reportAttributes, policy, personalDetails, data.length, optionMode, isOffline, isScreenFocused, isReportsSplitNavigatorLast], ); const previousOptionMode = usePrevious(optionMode); @@ -356,14 +273,12 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio if (shouldShowEmptyLHN) { Log.info('Woohoo! All caught up. Was rendered', false, { reportsCount: Object.keys(reports ?? {}).length, - reportActionsCount: Object.keys(reportActions ?? {}).length, policyCount: Object.keys(policy ?? {}).length, personalDetailsCount: Object.keys(personalDetails ?? {}).length, - route, reportsIDsFromUseReportsCount: data.length, }); } - }, [data.length, shouldShowEmptyLHN, route, reports, reportActions, policy, personalDetails]); + }, [data.length, shouldShowEmptyLHN, reports, policy, personalDetails]); return ( diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index 32b2417010847..2285de6b66fda 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -1,14 +1,19 @@ -import React, {useMemo} from 'react'; +import React, {useCallback, useMemo} from 'react'; +import type {OnyxCollection} from 'react-native-onyx'; import useReportPreviewSenderID from '@components/ReportActionAvatars/useReportPreviewSenderID'; import {useCurrentReportIDState} from '@hooks/useCurrentReportID'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromReportAction'; import useOnyx from '@hooks/useOnyx'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; +import {getLastVisibleActionIncludingTransactionThread, getOriginalMessage, isActionableTrackExpense, isInviteOrRemovedAction} from '@libs/ReportActionsUtils'; +import {canUserPerformWriteAction as canUserPerformWriteActionUtil} from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import {getMovedReportID} from '@src/libs/ModifiedExpenseMessage'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportActions as ReportActionsType} from '@src/types/onyx'; +import type {VisibleReportActionsDerivedValue} from '@src/types/onyx/DerivedValues'; import type {Icon} from '@src/types/onyx/OnyxCommon'; import OptionRowLHN from './OptionRowLHN'; import type {OptionRowLHNDataProps} from './types'; @@ -25,17 +30,11 @@ function OptionRowLHNData({ reportAttributes, reportAttributesDerived, oneTransactionThreadReport, - reportNameValuePairs, personalDetails = {}, policy, invoiceReceiverPolicy, - parentReportAction, - lastMessageTextFromReport, localeCompare, translate, - isReportArchived = false, - lastAction, - lastActionReport, currentUserAccountID, ...propsToForward }: OptionRowLHNDataProps) { @@ -43,6 +42,76 @@ function OptionRowLHNData({ const {currentReportID: currentReportIDValue} = useCurrentReportIDState(); const isReportFocused = isOptionFocused && currentReportIDValue === reportID; + const oneTransactionThreadReportID = oneTransactionThreadReport?.reportID; + + // Per-item report actions subscriptions (scoped by specific report ID) + const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`); + const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(fullReport?.parentReportID)}`); + const [transactionThreadReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(oneTransactionThreadReportID)}`); + + // Scoped VISIBLE_REPORT_ACTIONS selector + const visibleActionsSelector = useCallback( + (data: VisibleReportActionsDerivedValue | undefined) => { + if (!data) { + return undefined; + } + const result: VisibleReportActionsDerivedValue = {}; + const reportEntry = data[reportID]; + if (reportEntry) { + result[reportID] = reportEntry; + } + if (oneTransactionThreadReportID) { + const txThreadEntry = data[oneTransactionThreadReportID]; + if (txThreadEntry) { + result[oneTransactionThreadReportID] = txThreadEntry; + } + } + return result; + }, + [reportID, oneTransactionThreadReportID], + ); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {selector: visibleActionsSelector}); + + // Per-item NVP subscription instead of collection-level subscription in parent + const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`); + + const parentReportAction = fullReport?.parentReportActionID ? parentReportActions?.[fullReport.parentReportActionID] : undefined; + + const isReportArchived = !!reportNameValuePairs?.private_isArchived; + const canUserPerformWrite = canUserPerformWriteActionUtil(fullReport, isReportArchived); + + const lastAction = useMemo(() => { + const actionsCollection: OnyxCollection = { + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: reportActions ?? undefined, + }; + if (oneTransactionThreadReportID) { + actionsCollection[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oneTransactionThreadReportID}`] = transactionThreadReportActions ?? undefined; + } + return getLastVisibleActionIncludingTransactionThread(reportID, canUserPerformWrite, actionsCollection, visibleReportActionsData, oneTransactionThreadReportID); + }, [reportID, canUserPerformWrite, reportActions, transactionThreadReportActions, visibleReportActionsData, oneTransactionThreadReportID]); + + const whisperTransactionID = isActionableTrackExpense(lastAction) ? getOriginalMessage(lastAction)?.transactionID : undefined; + const [whisperTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(whisperTransactionID)}`); + + const lastMessageTextFromReport = useMemo(() => { + if (whisperTransactionID && !whisperTransaction) { + return ''; + } + return undefined; + }, [whisperTransactionID, whisperTransaction]); + + const lastActionReportID = useMemo(() => { + if (isInviteOrRemovedAction(lastAction)) { + const lastActionOriginalMessage = lastAction?.actionName ? getOriginalMessage(lastAction) : null; + return lastActionOriginalMessage?.reportID; + } + return undefined; + }, [lastAction]); + const [lastActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(lastActionReportID ? String(lastActionReportID) : undefined)}`); + + const [draftComment] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`); + const hasDraftComment = !!draftComment && !draftComment.match(CONST.REGEX.EMPTY_COMMENT); + const [movedFromReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.FROM)}`); const [movedToReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.TO)}`); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); @@ -107,6 +176,7 @@ function OptionRowLHNData({ isOptionFocused={isReportFocused} optionItem={finalOptionItem} report={fullReport} + hasDraftComment={hasDraftComment} conciergeReportID={conciergeReportID} /> ); diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index f2eb64cefe861..abed19c3267ce 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -5,7 +5,7 @@ import type {ValueOf} from 'type-fest'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import type CONST from '@src/CONST'; import type {OptionData} from '@src/libs/ReportUtils'; -import type {Onboarding, OnboardingPurpose, PersonalDetailsList, Policy, Report, ReportAction, ReportNameValuePairs} from '@src/types/onyx'; +import type {Onboarding, OnboardingPurpose, PersonalDetailsList, Policy, Report} from '@src/types/onyx'; import type {ReportAttributes, ReportAttributesDerivedValue} from '@src/types/onyx/DerivedValues'; type OptionMode = ValueOf; @@ -60,30 +60,18 @@ type OptionRowLHNDataProps = { /** The transaction thread report associated with the current report, if any */ oneTransactionThreadReport: OnyxEntry; - /** Array of report name value pairs for this report */ - reportNameValuePairs: OnyxEntry; - /** The policy which the user has access to and which the report could be tied to */ policy?: OnyxEntry; /** Invoice receiver policy */ invoiceReceiverPolicy?: OnyxEntry; - /** The action from the parent report */ - parentReportAction?: OnyxEntry; - - /** Whether a report contains a draft */ - hasDraftComment: boolean; - /** The reportID of the report */ reportID: string; /** Toggle between compact and default view */ viewMode?: OptionMode; - /** The last message text from the report */ - lastMessageTextFromReport?: string; - /** A function that is called when an option is selected. Selected option is passed as a param */ onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; @@ -111,14 +99,6 @@ type OptionRowLHNDataProps = { /** TestID of the row, indicating order */ testID: number; - /** Whether the report is archived */ - isReportArchived: boolean; - - /** The last action should be displayed */ - lastAction: ReportAction | undefined; - - lastActionReport: OnyxEntry | undefined; - /** The current user's account ID */ currentUserAccountID: number; };