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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 1 addition & 66 deletions src/components/LHNOptionsList/LHNOptionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,9 +23,6 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import getPlatform from '@libs/getPlatform';
import Log from '@libs/Log';
import {getIOUReportIDOfLastAction} from '@libs/OptionsListUtils';
import {getLastVisibleActionIncludingTransactionThread, getOriginalMessage, isActionableTrackExpense, isInviteOrRemovedAction, isMoneyRequestAction} 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';
Expand All @@ -51,17 +47,14 @@ 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 [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
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();
Expand Down Expand Up @@ -165,9 +158,6 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
const reportID = item.reportID;
const itemParentReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${item.parentReportID}`];
const itemReportNameValuePairs = reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`];
const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`];
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) {
Expand All @@ -179,63 +169,22 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
const itemInvoiceReceiverPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`];

const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${item?.policyID}`];
const transactionID = isMoneyRequestAction(itemParentReportAction)
? (getOriginalMessage(itemParentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID)
: CONST.DEFAULT_NUMBER_ID;
const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
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,
reportAttributes?.[reportID]?.oneTransactionThreadReportID,
);

const iouReportIDOfLastAction = getIOUReportIDOfLastAction(item, itemReportNameValuePairs?.private_isArchived, visibleReportActionsData, lastAction);
const itemIouReportReportActions = iouReportIDOfLastAction ? reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportIDOfLastAction}`] : undefined;

const lastReportActionTransactionID = isMoneyRequestAction(lastAction) ? (getOriginalMessage(lastAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) : CONST.DEFAULT_NUMBER_ID;
const lastReportActionTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${lastReportActionTransactionID}`];

// 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<Report> | undefined;
if (isInviteOrRemovedAction(lastAction)) {
const lastActionOriginalMessage = lastAction?.actionName ? getOriginalMessage(lastAction) : null;
lastActionReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${lastActionOriginalMessage?.reportID}`];
}

return (
<OptionRowLHNData
reportID={reportID}
fullReport={item}
reportNameValuePairs={itemReportNameValuePairs}
reportActions={itemReportActions}
parentReportAction={itemParentReportAction}
iouReportReportActions={itemIouReportReportActions}
policy={itemPolicy}
invoiceReceiverPolicy={itemInvoiceReceiverPolicy}
personalDetails={personalDetails ?? {}}
transaction={itemTransaction}
lastReportActionTransaction={lastReportActionTransaction}
viewMode={optionMode}
isOptionFocused={!shouldDisableFocusOptions}
lastMessageTextFromReport={lastMessageTextFromReport}
onSelectRow={onSelectRow}
preferredLocale={preferredLocale}
hasDraftComment={hasDraftComment}
Expand All @@ -251,20 +200,14 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
localeCompare={localeCompare}
translate={translate}
testID={index}
isReportArchived={isReportArchived}
lastAction={lastAction}
lastActionReport={lastActionReport}
currentUserAccountID={currentUserAccountID}
/>
);
},
[
reportAttributes,
reports,
reportNameValuePairs,
reportActions,
policy,
transactions,
draftComments,
personalDetails,
firstReportIDWithGBRorRBR,
Expand All @@ -282,14 +225,12 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
isScreenFocused,
localeCompare,
translate,
visibleReportActionsData,
currentUserAccountID,
],
);

const extraData = useMemo(
() => [
reportActions,
reports,
reportNameValuePairs,
transactionViolations,
Expand All @@ -299,14 +240,11 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
draftComments,
optionMode,
preferredLocale,
transactions,
isOffline,
isScreenFocused,
isReportsSplitNavigatorLast,
visibleReportActionsData,
],
[
reportActions,
reports,
reportNameValuePairs,
transactionViolations,
Expand All @@ -316,11 +254,9 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
draftComments,
optionMode,
preferredLocale,
transactions,
isOffline,
isScreenFocused,
isReportsSplitNavigatorLast,
visibleReportActionsData,
],
);

Expand Down Expand Up @@ -376,14 +312,13 @@ 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, route, reports, policy, personalDetails]);

return (
<View style={[style ?? styles.flex1, shouldShowEmptyLHN ? styles.emptyLHNWrapper : undefined]}>
Expand Down
93 changes: 84 additions & 9 deletions src/components/LHNOptionsList/OptionRowLHNData.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import {deepEqual} from 'fast-equals';
import React, {useCallback, useMemo, useRef} 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 usePrevious from '@hooks/usePrevious';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getIOUReportIDOfLastAction} from '@libs/OptionsListUtils';
import {getLastVisibleActionIncludingTransactionThread, getOriginalMessage, isActionableTrackExpense, isInviteOrRemovedAction, isMoneyRequestAction} 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 type {OptionData} from '@src/libs/ReportUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ReportActions as ReportActionsType, VisibleReportActionsDerivedValue} from '@src/types/onyx';
import type {ReportAttributesDerivedValue} from '@src/types/onyx/DerivedValues';
import type {Icon} from '@src/types/onyx/OnyxCommon';
import OptionRowLHN from './OptionRowLHN';
Expand All @@ -27,22 +32,13 @@ function OptionRowLHNData({
isOptionFocused = false,
fullReport,
reportNameValuePairs,
reportActions,
personalDetails = {},
preferredLocale = CONST.LOCALES.DEFAULT,
policy,
invoiceReceiverPolicy,
parentReportAction,
iouReportReportActions,
transaction,
lastReportActionTransaction,
transactionViolations,
lastMessageTextFromReport,
localeCompare,
translate,
isReportArchived = false,
lastAction,
lastActionReport,
currentUserAccountID,
...propsToForward
}: OptionRowLHNDataProps) {
Expand All @@ -51,12 +47,91 @@ function OptionRowLHNData({
const isReportFocused = isOptionFocused && currentReportIDValue === reportID;
const optionItemRef = useRef<OptionData | undefined>(undefined);

// Per-item scoped subscriptions
const reportAttributesSelector = useCallback((data: ReportAttributesDerivedValue | undefined) => data?.reports?.[reportID], [reportID]);
const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: reportAttributesSelector});

// Look up the one-transaction thread report using the ID from our own attributes.
const [oneTransactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportAttributes?.oneTransactionThreadReportID)}`);

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(oneTransactionThreadReport?.reportID)}`);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use derived thread ID when subscribing to thread actions

Subscribe to transaction-thread actions using oneTransactionThreadReport?.reportID can miss updates whenever the child report object is not yet present in Onyx (but oneTransactionThreadReportID is already known from derived attributes, including optimistic/FAKE_REPORT_ID flows). In that case this key resolves to REPORT_ACTIONSundefined, so lastAction stops considering transaction-thread activity and the LHN preview/unread state can remain stale until the child report record is later hydrated. The previous code path used the derived thread ID directly and did not depend on loading the full child report first.

Useful? React with 👍 / 👎.


// Scoped VISIBLE_REPORT_ACTIONS selector — only picks entries for this report and its transaction thread
const visibleActionsSelector = useCallback(
(data: VisibleReportActionsDerivedValue | undefined) => {
if (!data) {
return undefined;
}
const result: VisibleReportActionsDerivedValue = {};
const reportEntry = data[reportID];
if (reportEntry) {
result[reportID] = reportEntry;
}
const txThreadID = oneTransactionThreadReport?.reportID;
if (txThreadID) {
const txThreadEntry = data[txThreadID];
if (txThreadEntry) {
result[txThreadID] = txThreadEntry;
}
}
return result;
},
[reportID, oneTransactionThreadReport?.reportID],
);
const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {selector: visibleActionsSelector});

const [reportNameValuePairsEntry] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`);

const parentReportAction = fullReport?.parentReportActionID ? parentReportActions?.[fullReport.parentReportActionID] : undefined;

const transactionID = isMoneyRequestAction(parentReportAction) ? (getOriginalMessage(parentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) : CONST.DEFAULT_NUMBER_ID;
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transactionID !== CONST.DEFAULT_NUMBER_ID ? String(transactionID) : undefined)}`);

const isReportArchived = !!(reportNameValuePairsEntry ?? reportNameValuePairs)?.private_isArchived;
const canUserPerformWrite = canUserPerformWriteActionUtil(fullReport, isReportArchived);

const lastAction = useMemo(() => {
const actionsCollection: OnyxCollection<ReportActionsType> = {
[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: reportActions ?? undefined,
};
if (oneTransactionThreadReport?.reportID) {
actionsCollection[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oneTransactionThreadReport.reportID}`] = transactionThreadReportActions ?? undefined;
}
return getLastVisibleActionIncludingTransactionThread(reportID, canUserPerformWrite, actionsCollection, visibleReportActionsData, oneTransactionThreadReport?.reportID);
}, [reportID, canUserPerformWrite, reportActions, transactionThreadReportActions, visibleReportActionsData, oneTransactionThreadReport?.reportID]);

const iouReportIDOfLastAction = useMemo(
() => getIOUReportIDOfLastAction(fullReport, (reportNameValuePairsEntry ?? reportNameValuePairs)?.private_isArchived, visibleReportActionsData, lastAction),
[fullReport, reportNameValuePairsEntry, reportNameValuePairs, visibleReportActionsData, lastAction],
);
const [iouReportReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(iouReportIDOfLastAction)}`);

const lastReportActionTransactionID = isMoneyRequestAction(lastAction) ? (getOriginalMessage(lastAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) : CONST.DEFAULT_NUMBER_ID;
const [lastReportActionTransaction] = useOnyx(
`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(lastReportActionTransactionID !== CONST.DEFAULT_NUMBER_ID ? String(lastReportActionTransactionID) : undefined)}`,
);

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 [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);
Expand Down
Loading
Loading