Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import useReportScrollManager from '@hooks/useReportScrollManager';
import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection';
import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
import useScrollToEndOnNewMessageReceived from '@hooks/useScrollToEndOnNewMessageReceived';
import useStableReportForReportActionItem from '@hooks/useStableReportForReportActionItem';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {isConsecutiveChronosAutomaticTimerAction} from '@libs/ChronosUtils';
Expand Down Expand Up @@ -51,6 +52,7 @@ import Visibility from '@libs/Visibility';
import isSearchTopmostFullScreenRoute from '@navigation/helpers/isSearchTopmostFullScreenRoute';
import FloatingMessageCounter from '@pages/inbox/report/FloatingMessageCounter';
import getInitialNumToRender from '@pages/inbox/report/getInitialNumReportActionsToRender';
import ReportActionIndexContext from '@pages/inbox/report/ReportActionIndexContext';
import ReportActionsListItemRenderer from '@pages/inbox/report/ReportActionsListItemRenderer';
import {getUnreadMarkerReportAction} from '@pages/inbox/report/shouldDisplayNewMarkerOnReportAction';
import useReportUnreadMessageScrollTracking from '@pages/inbox/report/useReportUnreadMessageScrollTracking';
Expand Down Expand Up @@ -99,6 +101,8 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
// Self-subscribe to report, policy, metadata, actions, transactions
// report is guaranteed to exist — callers only render this component when report is loaded
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`) as unknown as [OnyxTypes.Report];
// Stable reference of `report` for action items
const reportStable = useStableReportForReportActionItem(report);
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(report?.policyID)}`);
const [reportLoadingState] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportIDFromRoute}`);
const [reportPaginationState] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_PAGINATION_STATE}${reportIDFromRoute}`);
Expand Down Expand Up @@ -559,34 +563,35 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
!isConsecutiveChronosAutomaticTimerAction(visibleReportActions, index, chatIncludesChronosWithID(reportAction?.reportID), isOffline) &&
hasNextActionMadeBySameActor(visibleReportActions, index, isOffline);

const originalReportID = getOriginalReportID(report?.reportID, reportAction, reportActionsObject);
const originalReportID = getOriginalReportID(reportStable?.reportID, reportAction, reportActionsObject);

return (
<ReportActionsListItemRenderer
reportAction={reportAction}
parentReportAction={parentReportAction}
parentReportActionForTransactionThread={EmptyParentReportActionForTransactionThread}
index={index}
report={report}
transactionThreadReport={transactionThreadReport}
displayAsGroup={displayAsGroup}
shouldDisplayNewMarker={reportAction.reportActionID === unreadMarkerReportActionID}
shouldDisplayReplyDivider={visibleReportActions.length > 1}
isFirstVisibleReportAction={firstVisibleReportActionID === reportAction.reportActionID}
shouldHideThreadDividerLine
linkedReportActionID={linkedReportActionID}
personalDetails={personalDetails}
originalReportID={originalReportID}
isReportArchived={isReportArchived}
isHarvestCreatedExpenseReport={shouldShowHarvestCreatedAction}
/>
<ReportActionIndexContext.Provider value={index}>
<ReportActionsListItemRenderer
reportAction={reportAction}
parentReportAction={parentReportAction}
parentReportActionForTransactionThread={EmptyParentReportActionForTransactionThread}
report={reportStable}
transactionThreadReport={transactionThreadReport}
displayAsGroup={displayAsGroup}
shouldDisplayNewMarker={reportAction.reportActionID === unreadMarkerReportActionID}
shouldDisplayReplyDivider={visibleReportActions.length > 1}
isFirstVisibleReportAction={firstVisibleReportActionID === reportAction.reportActionID}
shouldHideThreadDividerLine
linkedReportActionID={linkedReportActionID}
personalDetails={personalDetails}
originalReportID={originalReportID}
isReportArchived={isReportArchived}
isHarvestCreatedExpenseReport={shouldShowHarvestCreatedAction}
/>
</ReportActionIndexContext.Provider>
);
},
[
visibleReportActions,
reportActionsObject,
parentReportAction,
report,
reportStable,
isOffline,
transactionThreadReport,
unreadMarkerReportActionID,
Expand Down
2 changes: 0 additions & 2 deletions src/components/Search/SearchList/ListItem/ChatListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,8 @@ function ChatListItem<TItem extends ListItem>({
parentReportAction={undefined}
displayAsGroup={false}
shouldDisplayNewMarker={false}
index={item.index ?? 0}
isFirstVisibleReportAction={false}
shouldDisplayContextMenu={false}
shouldShowDraftMessage={false}
shouldShowBorder
personalDetails={personalDetails}
/>
Expand Down
109 changes: 109 additions & 0 deletions src/hooks/useStableReportActionForReportActionItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {useMemo} from 'react';
import {getOriginalMessage} from '@libs/ReportActionsUtils';
import type {ReportAction} from '@src/types/onyx';

/**
* Stable `ReportAction` projection for the `ReportActionItem` subtree, with `originalMessage` precomputed.
* Per-field `useMemo` deps so the ref holds when consumed fields are unchanged.
* Allow-list — when a descendant reads a new `reportAction.*` field, add it here.
*/
function useStableReportActionForReportActionItem(reportAction: ReportAction): ReportAction {
const reportActionID = reportAction.reportActionID;
const message = reportAction.message;
const pendingAction = reportAction.pendingAction;
const actionName = reportAction.actionName;
const errors = reportAction.errors;
const childCommenterCount = reportAction.childCommenterCount;
const linkMetadata = reportAction.linkMetadata;
const childReportID = reportAction.childReportID;
const childLastVisibleActionCreated = reportAction.childLastVisibleActionCreated;
const error = reportAction.error;
const created = reportAction.created;
const actorAccountID = reportAction.actorAccountID;
const adminAccountID = reportAction.adminAccountID;
const childVisibleActionCount = reportAction.childVisibleActionCount;
const childOldestFourAccountIDs = reportAction.childOldestFourAccountIDs;
const childType = reportAction.childType;
const person = reportAction.person;
const isOptimisticAction = reportAction.isOptimisticAction;
const delegateAccountID = reportAction.delegateAccountID;
const previousMessage = reportAction.previousMessage;
const isAttachmentWithText = reportAction.isAttachmentWithText;
const isOriginalReportDeleted = reportAction.isOriginalReportDeleted;
const childStateNum = reportAction.childStateNum;
const childStatusNum = reportAction.childStatusNum;
const childReportName = reportAction.childReportName;
const childManagerAccountID = reportAction.childManagerAccountID;
const childMoneyRequestCount = reportAction.childMoneyRequestCount;
const childOwnerAccountID = reportAction.childOwnerAccountID;

const originalMessage = useMemo(() => getOriginalMessage(reportAction), [reportAction]);

return useMemo(
() =>
({
reportActionID,
message,
pendingAction,
actionName,
errors,
originalMessage,
childCommenterCount,
linkMetadata,
childReportID,
childLastVisibleActionCreated,
error,
created,
actorAccountID,
adminAccountID,
childVisibleActionCount,
childOldestFourAccountIDs,
childType,
person,
isOptimisticAction,
delegateAccountID,
previousMessage,
isAttachmentWithText,
isOriginalReportDeleted,
childStateNum,
childStatusNum,
childReportName,
childManagerAccountID,
childMoneyRequestCount,
childOwnerAccountID,
}) as ReportAction,
[
reportActionID,
message,
pendingAction,
actionName,
errors,
originalMessage,
childCommenterCount,
linkMetadata,
childReportID,
childLastVisibleActionCreated,
error,
created,
actorAccountID,
adminAccountID,
childVisibleActionCount,
childOldestFourAccountIDs,
childType,
person,
isOptimisticAction,
delegateAccountID,
previousMessage,
isAttachmentWithText,
isOriginalReportDeleted,
childStateNum,
childStatusNum,
childReportName,
childManagerAccountID,
childMoneyRequestCount,
childOwnerAccountID,
],
);
}

export default useStableReportActionForReportActionItem;
139 changes: 139 additions & 0 deletions src/hooks/useStableReportForReportActionItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {deepEqual} from 'fast-equals';
import {useMemo, useState} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import type {Report} from '@src/types/onyx';

/**
* Stable `Report` projection for the `ReportActionItem` subtree.
* Per-field `useMemo` deps so the ref holds across Onyx pushes when consumed fields are unchanged.
*
* Never add — heartbeat-style fields that update on routine activity and defeat the memo:
* - lastReadTime, lastVisibleActionCreated, lastVisibleActionLastModified
* - lastMessageText, lastMessageHtml
* - lastActorAccountID, lastActionType
*
*/
function useStableReportForReportActionItem(report: OnyxEntry<Report>): OnyxEntry<Report> {
const reportID = report?.reportID;
const chatReportID = report?.chatReportID;
const isWaitingOnBankAccount = report?.isWaitingOnBankAccount;

// Stabilize `permissions` by content
const permissionsRaw = report?.permissions;
const [permissions, setPermissions] = useState(permissionsRaw);
if (!deepEqual(permissions, permissionsRaw)) {
setPermissions(permissionsRaw);
}

const policyID = report?.policyID;
const ownerAccountID = report?.ownerAccountID;
const parentReportID = report?.parentReportID;
const parentReportActionID = report?.parentReportActionID;
const type = report?.type;
const chatType = report?.chatType;
const stateNum = report?.stateNum;
const statusNum = report?.statusNum;
const isDeletedParentAction = report?.isDeletedParentAction;
const pendingFields = report?.pendingFields;
const participants = report?.participants;
const errorFields = report?.errorFields;
const reportName = report?.reportName;
const description = report?.description;
// Coerce sentinel `0` to `undefined`. The backend ships `managerID: 0` on chat reports without a manager, and a later push removes the key entirely; treating both as `undefined` keeps the memo dep stable through that reconciliation.
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const managerID = report?.managerID || undefined;
const total = report?.total;
const nonReimbursableTotal = report?.nonReimbursableTotal;
const policyAvatar = report?.policyAvatar;
const fieldList = report?.fieldList;
const iouReportID = report?.iouReportID;
const isCancelledIOU = report?.isCancelledIOU;
const isOwnPolicyExpenseChat = report?.isOwnPolicyExpenseChat;
const writeCapability = report?.writeCapability;
const currency = report?.currency;
const visibility = report?.visibility;
const avatarUrl = report?.avatarUrl;
const policyName = report?.policyName;
const transactionCount = report?.transactionCount;
const unheldTotal = report?.unheldTotal;
const created = report?.created;

return useMemo(() => {
if (reportID === undefined) {
return undefined;
}
return {
reportID,
chatReportID,
isWaitingOnBankAccount,
permissions,
policyID,
ownerAccountID,
parentReportID,
parentReportActionID,
type,
chatType,
stateNum,
statusNum,
isDeletedParentAction,
pendingFields,
participants,
errorFields,
reportName,
description,
managerID,
total,
nonReimbursableTotal,
policyAvatar,
fieldList,
iouReportID,
isCancelledIOU,
isOwnPolicyExpenseChat,
writeCapability,
currency,
visibility,
avatarUrl,
policyName,
transactionCount,
unheldTotal,
created,
} as Report;
}, [
reportID,
chatReportID,
isWaitingOnBankAccount,
permissions,
policyID,
ownerAccountID,
parentReportID,
parentReportActionID,
type,
chatType,
stateNum,
statusNum,
isDeletedParentAction,
pendingFields,
participants,
errorFields,
reportName,
description,
managerID,
total,
nonReimbursableTotal,
policyAvatar,
fieldList,
iouReportID,
isCancelledIOU,
isOwnPolicyExpenseChat,
writeCapability,
currency,
visibility,
avatarUrl,
policyName,
transactionCount,
unheldTotal,
created,
]);
}

export default useStableReportForReportActionItem;
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ function DebugReportActionCreatePage({
parentReportAction={undefined}
displayAsGroup={false}
shouldDisplayNewMarker={false}
index={0}
isFirstVisibleReportAction={false}
shouldDisplayContextMenu={false}
personalDetails={personalDetailsList}
Expand Down
1 change: 0 additions & 1 deletion src/pages/Debug/ReportAction/DebugReportActionPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ function DebugReportActionPreview({reportAction, reportID}: DebugReportActionPre
parentReportAction={undefined}
displayAsGroup={false}
shouldDisplayNewMarker={false}
index={0}
isFirstVisibleReportAction={false}
shouldDisplayContextMenu={false}
personalDetails={personalDetails}
Expand Down
28 changes: 15 additions & 13 deletions src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {getOriginalReportID} from '@libs/ReportUtils';
import ReportActionIndexContext from '@pages/inbox/report/ReportActionIndexContext';
import ReportActionItem from '@pages/inbox/report/ReportActionItem';
import {ReportActionItemActionsContext, ReportActionItemStateContext} from '@pages/inbox/report/ReportActionItemContext';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -58,19 +59,20 @@ function DuplicateTransactionItem({transaction, index, onPreviewPressed}: Duplic
<View style={styles.pb2}>
<ReportActionItemStateContext.Provider value={stateValue}>
<ReportActionItemActionsContext.Provider value={actionsValue}>
<ReportActionItem
action={action}
report={report}
parentReportAction={getReportAction(report?.parentReportID, report?.parentReportActionID)}
index={index}
displayAsGroup={false}
shouldDisplayNewMarker={false}
isFirstVisibleReportAction={false}
shouldDisplayContextMenu={false}
personalDetails={personalDetails}
draftMessage={matchingDraftMessage}
linkedTransactionRouteError={linkedTransactionRouteError}
/>
<ReportActionIndexContext.Provider value={index}>
<ReportActionItem
action={action}
report={report}
parentReportAction={getReportAction(report?.parentReportID, report?.parentReportActionID)}
displayAsGroup={false}
shouldDisplayNewMarker={false}
isFirstVisibleReportAction={false}
shouldDisplayContextMenu={false}
personalDetails={personalDetails}
draftMessage={matchingDraftMessage}
linkedTransactionRouteError={linkedTransactionRouteError}
/>
</ReportActionIndexContext.Provider>
</ReportActionItemActionsContext.Provider>
</ReportActionItemStateContext.Provider>
</View>
Expand Down
Loading
Loading