diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index 336674915180..b95e336cdc00 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -50,6 +50,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'; @@ -59,6 +60,7 @@ import {getOlderActions, openReport, readNewestAction, subscribeToNewActionEvent import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; +import {getStableReportSelector} from '@src/selectors/Report'; import type * as OnyxTypes from '@src/types/onyx'; import MoneyRequestReportTransactionList from './MoneyRequestReportTransactionList'; import MoneyRequestViewReportFields from './MoneyRequestViewReportFields'; @@ -98,6 +100,7 @@ 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]; + const [reportStable] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`, {selector: getStableReportSelector}); 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}`); @@ -554,32 +557,33 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) hasNextActionMadeBySameActor(visibleReportActions, index, isOffline); return ( - 1} - isFirstVisibleReportAction={firstVisibleReportActionID === reportAction.reportActionID} - shouldHideThreadDividerLine - linkedReportActionID={linkedReportActionID} - personalDetails={personalDetails} - userBillingFundID={userBillingFundID} - isReportArchived={isReportArchived} - isTryNewDotNVPDismissed={isTryNewDotNVPDismissed} - reportNameValuePairsOrigin={reportNameValuePairs?.origin} - reportNameValuePairsOriginalID={reportNameValuePairs?.originalID} - /> + + 1} + isFirstVisibleReportAction={firstVisibleReportActionID === reportAction.reportActionID} + shouldHideThreadDividerLine + linkedReportActionID={linkedReportActionID} + personalDetails={personalDetails} + userBillingFundID={userBillingFundID} + isReportArchived={isReportArchived} + isTryNewDotNVPDismissed={isTryNewDotNVPDismissed} + reportNameValuePairsOrigin={reportNameValuePairs?.origin} + reportNameValuePairsOriginalID={reportNameValuePairs?.originalID} + /> + ); }, [ visibleReportActions, parentReportAction, - report, + reportStable, isOffline, transactionThreadReport, unreadMarkerReportActionID, diff --git a/src/components/Search/SearchList/ListItem/ChatListItem.tsx b/src/components/Search/SearchList/ListItem/ChatListItem.tsx index e0413ccfd04a..2df64d2556fa 100644 --- a/src/components/Search/SearchList/ListItem/ChatListItem.tsx +++ b/src/components/Search/SearchList/ListItem/ChatListItem.tsx @@ -10,6 +10,7 @@ import FS from '@libs/Fullstory'; import ReportActionItem from '@pages/inbox/report/ReportActionItem'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; +import {getStableReportSelector} from '@src/selectors/Report'; import type {ChatListItemProps, ReportActionListItemType} from './types'; /** @@ -28,7 +29,7 @@ function ChatListItem({ shouldSyncFocus, }: ChatListItemProps) { const reportActionItem = item as unknown as ReportActionListItemType; - const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportActionItem?.reportID}`); + const [reportStable] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportActionItem?.reportID}`, {selector: getStableReportSelector}); const personalDetails = usePersonalDetails(); const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID); const styles = useThemeStyles(); @@ -51,7 +52,9 @@ function ChatListItem({ item.cursorStyle, ]; - const fsClass = FS.getChatFSClass(report); + const fsClass = FS.getChatFSClass(reportStable); + + const handlePress = () => onSelectRow(item); return ( ({ > onSelectRow(item)} + report={reportStable} + onPress={handlePress} parentReportAction={undefined} displayAsGroup={false} shouldDisplayNewMarker={false} - index={item.index ?? 0} isFirstVisibleReportAction={false} shouldDisplayContextMenu={false} shouldShowBorder diff --git a/src/pages/Debug/ReportAction/DebugReportActionCreatePage.tsx b/src/pages/Debug/ReportAction/DebugReportActionCreatePage.tsx index cc68738eef12..541cbf35aa5c 100644 --- a/src/pages/Debug/ReportAction/DebugReportActionCreatePage.tsx +++ b/src/pages/Debug/ReportAction/DebugReportActionCreatePage.tsx @@ -115,7 +115,6 @@ function DebugReportActionCreatePage({ parentReportAction={undefined} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} shouldDisplayContextMenu={false} personalDetails={personalDetailsList} diff --git a/src/pages/Debug/ReportAction/DebugReportActionPreview.tsx b/src/pages/Debug/ReportAction/DebugReportActionPreview.tsx index e93df5acead1..99805c1ea517 100644 --- a/src/pages/Debug/ReportAction/DebugReportActionPreview.tsx +++ b/src/pages/Debug/ReportAction/DebugReportActionPreview.tsx @@ -30,7 +30,6 @@ function DebugReportActionPreview({reportAction, reportID}: DebugReportActionPre parentReportAction={undefined} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} shouldDisplayContextMenu={false} personalDetails={personalDetails} diff --git a/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx b/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx index 0561a026a1a7..eb91dff785fa 100644 --- a/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx +++ b/src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx @@ -11,23 +11,23 @@ import ReportActionItem from '@pages/inbox/report/ReportActionItem'; import {ReportActionItemActionsContext, ReportActionItemStateContext} from '@pages/inbox/report/ReportActionItemContext'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {getStableReportSelector} from '@src/selectors/Report'; import type {Transaction} from '@src/types/onyx'; type DuplicateTransactionItemProps = { transaction: OnyxEntry; - index: number; onPreviewPressed: (reportID: string) => void; }; const linkedTransactionRouteErrorSelector = (transaction: OnyxEntry) => transaction?.errorFields?.route ?? null; -function DuplicateTransactionItem({transaction, index, onPreviewPressed}: DuplicateTransactionItemProps) { +function DuplicateTransactionItem({transaction, onPreviewPressed}: DuplicateTransactionItemProps) { const styles = useThemeStyles(); const personalDetails = usePersonalDetails(); const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID); - const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`); - const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`); + const [reportStable] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`, {selector: getStableReportSelector}); + const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportStable?.reportID}`); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT); const isTryNewDotNVPDismissed = !!tryNewDot?.classicRedirect?.dismissed; @@ -36,7 +36,7 @@ function DuplicateTransactionItem({transaction, index, onPreviewPressed}: Duplic return IOUTransactionID === transaction?.transactionID; }); - const originalReportID = getOriginalReportID(report?.reportID, action, reportActions); + const originalReportID = getOriginalReportID(reportStable?.reportID, action, reportActions); const [draftMessage] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`); @@ -50,7 +50,7 @@ function DuplicateTransactionItem({transaction, index, onPreviewPressed}: Duplic const stateValue = useMemo(() => ({shouldOpenReportInRHP: true}), []); const actionsValue = useMemo(() => ({onPreviewPressed}), [onPreviewPressed]); - if (!action || !report) { + if (!action || !reportStable) { return null; } @@ -63,9 +63,8 @@ function DuplicateTransactionItem({transaction, index, onPreviewPressed}: Duplic >) => ( + ({item}: ListRenderItemInfo>) => ( ), diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index f67cb244e544..046bf71d75db 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -109,9 +109,6 @@ type PureReportActionItemProps = { /** Should we display the new marker on top of the comment? */ shouldDisplayNewMarker: boolean; - /** Position index of the report action in the overall report FlatList view */ - index: number; - /** Flag to show, hide the thread divider line */ shouldHideThreadDividerLine?: boolean; @@ -185,7 +182,6 @@ function PureReportActionItem({ transactionThreadReport, linkedReportActionID, displayAsGroup, - index, parentReportAction, shouldDisplayNewMarker, shouldHideThreadDividerLine = false, @@ -631,7 +627,6 @@ function PureReportActionItem({ shouldShowBorder={shouldShowBorder} isOnSearch={isOnSearch} userBillingFundID={userBillingFundID} - index={index} setIsPaymentMethodPopoverActive={setIsPaymentMethodPopoverActive} /> {Permissions.canUseLinkPreviews() && !isHidden && (action.linkMetadata?.length ?? 0) > 0 && ( @@ -701,7 +696,6 @@ export default memo(PureReportActionItem, (prevProps, nextProps) => { prevProps.report?.description === nextProps.report?.description && isCompletedTaskReport(prevProps.report) === isCompletedTaskReport(nextProps.report) && prevProps.report?.managerID === nextProps.report?.managerID && - prevProps.index === nextProps.index && prevProps.shouldHideThreadDividerLine === nextProps.shouldHideThreadDividerLine && prevProps.report?.total === nextProps.report?.total && prevProps.report?.nonReimbursableTotal === nextProps.report?.nonReimbursableTotal && diff --git a/src/pages/inbox/report/ReportActionIndexContext.tsx b/src/pages/inbox/report/ReportActionIndexContext.tsx new file mode 100644 index 000000000000..1abe9823fcf3 --- /dev/null +++ b/src/pages/inbox/report/ReportActionIndexContext.tsx @@ -0,0 +1,13 @@ +import {createContext} from 'react'; + +/** + * Carries an action item's position index from the list renderer down to the rare consumers that + * actually need it (e.g. `ReportActionItemMessageEdit` for scroll-to-index during edit mode). + * + * Using context keeps `index` out of the prop signatures of every intermediate component, so a + * position shift caused by a new message arriving doesn't cascade re-renders through items that + * never read it. Only components that `useContext(ReportActionIndexContext)` re-render on change. + */ +const ReportActionIndexContext = createContext(0); + +export default ReportActionIndexContext; diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index 839c78a909bb..51139fa966d8 100644 --- a/src/pages/inbox/report/ReportActionItem.tsx +++ b/src/pages/inbox/report/ReportActionItem.tsx @@ -7,6 +7,7 @@ import useReportTransactions from '@hooks/useReportTransactions'; import {getIOUReportIDFromReportActionPreview, getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {isArchivedNonExpenseReport, isClosedExpenseReportWithNoExpenses} from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; +import {getStableReportSelector} from '@src/selectors/Report'; import type {PersonalDetailsList, Transaction} from '@src/types/onyx'; import type {PureReportActionItemProps} from './PureReportActionItem'; import PureReportActionItem from './PureReportActionItem'; @@ -39,7 +40,7 @@ function ReportActionItem({ const reportID = report?.reportID; const originalReportID = useOriginalReportID(reportID, action); const isOriginalReportArchived = useReportIsArchived(originalReportID); - const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`); + const [stableOriginalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`, {selector: getStableReportSelector}); const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getIOUReportIDFromReportActionPreview(action)}`); const transactionsOnIOUReport = useReportTransactions(iouReport?.reportID); @@ -68,8 +69,8 @@ function ReportActionItem({ linkedTransactionRouteError={linkedTransactionRouteError} personalDetails={personalDetails} originalReportID={originalReportID} - originalReport={originalReport} - isArchivedRoom={isArchivedNonExpenseReport(originalReport, isOriginalReportArchived)} + originalReport={stableOriginalReport} + isArchivedRoom={isArchivedNonExpenseReport(stableOriginalReport, isOriginalReportArchived)} isClosedExpenseReportWithNoExpenses={isClosedExpenseReportWithNoExpenses(iouReport, transactionsOnIOUReport)} userBillingFundID={userBillingFundID} isTryNewDotNVPDismissed={isTryNewDotNVPDismissed} diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx index 864b9bc4b4b7..a4598150c736 100644 --- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; // eslint-disable-next-line no-restricted-imports import {InteractionManager, View} from 'react-native'; import type {MeasureInWindowOnSuccessCallback, TextInputKeyPressEvent, TextInputScrollEvent} from 'react-native'; @@ -47,6 +47,7 @@ import Suggestions from './ReportActionCompose/Suggestions'; import useDebouncedCommentMaxLengthValidation from './ReportActionCompose/useDebouncedCommentMaxLengthValidation'; import useEditMessage from './ReportActionCompose/useEditMessage'; import {useReportActionActiveEdit, useReportActionActiveEditActions} from './ReportActionEditMessageContext'; +import ReportActionIndexContext from './ReportActionIndexContext'; import shouldUseEmojiPickerSelection from './shouldUseEmojiPickerSelection'; import useDebouncedSaveDraft from './useDebouncedSaveDraft'; import useDraftMessageVideoAttributeCache from './useDraftMessageVideoAttributeCache'; @@ -64,9 +65,6 @@ type ReportActionItemMessageEditProps = { /** PolicyID of the policy the report belongs to */ policyID?: string; - /** Position index of the report action in the overall report FlatList view */ - index: number; - /** Whether or not the emoji picker is disabled */ shouldDisableEmojiPicker?: boolean; @@ -84,7 +82,8 @@ const DEFAULT_MODAL_VALUE = { isVisible: false, }; -function ReportActionItemMessageEdit({action, reportID, originalReportID, policyID, index, isGroupPolicyReport, shouldDisableEmojiPicker = false, ref}: ReportActionItemMessageEditProps) { +function ReportActionItemMessageEdit({action, reportID, originalReportID, policyID, isGroupPolicyReport, shouldDisableEmojiPicker = false, ref}: ReportActionItemMessageEditProps) { + const index = useContext(ReportActionIndexContext); const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); diff --git a/src/pages/inbox/report/ReportActionItemParentAction.tsx b/src/pages/inbox/report/ReportActionItemParentAction.tsx index 991cd6485895..6dcdecc48d7c 100644 --- a/src/pages/inbox/report/ReportActionItemParentAction.tsx +++ b/src/pages/inbox/report/ReportActionItemParentAction.tsx @@ -34,9 +34,6 @@ type ReportActionItemParentActionProps = { /** Flag to show, hide the thread divider line */ shouldHideThreadDividerLine?: boolean; - /** Position index of the report parent action in the overall report FlatList view */ - index: number; - /** The id of the report */ reportID: string; @@ -77,7 +74,6 @@ function ReportActionItemParentAction({ action, transactionThreadReport, parentReportAction, - index = 0, shouldHideThreadDividerLine = false, shouldDisplayReplyDivider, isFirstVisibleReportAction = false, @@ -202,7 +198,6 @@ function ReportActionItemParentAction({ action={ancestorReportAction} displayAsGroup={false} shouldDisplayNewMarker={ancestor.shouldDisplayNewMarker} - index={index} isFirstVisibleReportAction={isFirstVisibleReportAction} shouldUseThreadDividerLine={shouldUseThreadDividerLine} isThreadReportParentAction diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx index 6b96286c2f10..7d8d16d3764d 100644 --- a/src/pages/inbox/report/ReportActionsList.tsx +++ b/src/pages/inbox/report/ReportActionsList.tsx @@ -66,8 +66,10 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; +import {getStableReportSelector} from '@src/selectors/Report'; import type * as OnyxTypes from '@src/types/onyx'; import FloatingMessageCounter from './FloatingMessageCounter'; +import ReportActionIndexContext from './ReportActionIndexContext'; import ReportActionsListHeader from './ReportActionsListHeader'; import ReportActionsListItemRenderer from './ReportActionsListItemRenderer'; import {getUnreadMarkerReportAction} from './shouldDisplayNewMarkerOnReportAction'; @@ -212,6 +214,8 @@ function ReportActionsList({ const prevIsLoadingInitialReportActions = usePrevious(reportLoadingState?.isLoadingInitialReportActions); const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`); + const [reportStable] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, {selector: getStableReportSelector}); + const backTo = route?.params?.backTo as string; const linkedReportActionID = route?.params?.reportActionID; @@ -739,13 +743,12 @@ function ReportActionsList({ const safeIndex = actionIndexMap.get(reportAction.reportActionID) ?? index; return ( - <> + - - + {!!reportStable?.reportID && ( + + )} + ); }, [ @@ -787,7 +792,7 @@ function ReportActionsList({ parentReportActionForTransactionThread, personalDetailsList, renderedVisibleReportActions, - report, + reportStable, reportNameValuePairs?.origin, reportNameValuePairs?.originalID, shouldHideThreadDividerLine, diff --git a/src/pages/inbox/report/ReportActionsListItemRenderer.tsx b/src/pages/inbox/report/ReportActionsListItemRenderer.tsx index 253315e0d75e..31fd9036fd7c 100644 --- a/src/pages/inbox/report/ReportActionsListItemRenderer.tsx +++ b/src/pages/inbox/report/ReportActionsListItemRenderer.tsx @@ -17,9 +17,6 @@ type ReportActionsListItemRendererProps = { /** The transaction thread report's parentReportAction */ parentReportActionForTransactionThread: OnyxEntry; - /** Position index of the report action in the overall report FlatList view */ - index: number; - /** Report for this action */ report: OnyxEntry; @@ -71,7 +68,6 @@ type ReportActionsListItemRendererProps = { function ReportActionsListItemRenderer({ reportAction, parentReportAction, - index, report, transactionThreadReport, displayAsGroup, @@ -175,7 +171,6 @@ function ReportActionsListItemRenderer({ report={report} action={action} transactionThreadReport={transactionThreadReport} - index={index} isFirstVisibleReportAction={isFirstVisibleReportAction} shouldUseThreadDividerLine={shouldUseThreadDividerLine} personalDetails={personalDetails} @@ -197,7 +192,6 @@ function ReportActionsListItemRenderer({ linkedReportActionID={linkedReportActionID} displayAsGroup={displayAsGroup} shouldDisplayNewMarker={shouldDisplayNewMarker} - index={index} isFirstVisibleReportAction={isFirstVisibleReportAction} shouldUseThreadDividerLine={shouldUseThreadDividerLine} shouldHighlight={shouldHighlight} diff --git a/src/pages/inbox/report/actionContents/ActionContentRouter.tsx b/src/pages/inbox/report/actionContents/ActionContentRouter.tsx index 8667431d0d38..bcf38ea7e238 100644 --- a/src/pages/inbox/report/actionContents/ActionContentRouter.tsx +++ b/src/pages/inbox/report/actionContents/ActionContentRouter.tsx @@ -127,9 +127,6 @@ type ActionContentRouterProps = { /** User payment card ID */ userBillingFundID?: number; - /** Position index of the report action in the overall report FlatList view */ - index: number; - /** Toggle whether the payment method popover is active */ setIsPaymentMethodPopoverActive: (value: boolean) => void; }; @@ -156,7 +153,6 @@ function ActionContentRouter({ shouldShowBorder, isOnSearch, userBillingFundID, - index, setIsPaymentMethodPopoverActive, }: ActionContentRouterProps): React.JSX.Element | null { const {translate, formatTravelDate} = useLocalize(); @@ -487,7 +483,6 @@ function ActionContentRouter({ originalReportID={originalReportID} displayAsGroup={displayAsGroup} draftMessage={draftMessage} - index={index} isHidden={isHidden} updateHiddenState={updateHiddenState} isArchivedRoom={isArchivedRoom} diff --git a/src/pages/inbox/report/actionContents/ChatMessageContent.tsx b/src/pages/inbox/report/actionContents/ChatMessageContent.tsx index 1b4755788cc8..ca01c501c523 100644 --- a/src/pages/inbox/report/actionContents/ChatMessageContent.tsx +++ b/src/pages/inbox/report/actionContents/ChatMessageContent.tsx @@ -34,7 +34,6 @@ type ChatMessageContentProps = { originalReportID: string; displayAsGroup: boolean; draftMessage: string | undefined; - index: number; isHidden: boolean; updateHiddenState: (isHiddenValue: boolean) => void; isArchivedRoom?: boolean; @@ -50,7 +49,6 @@ function ChatMessageContent({ originalReportID, displayAsGroup, draftMessage, - index, isHidden, updateHiddenState, isArchivedRoom, @@ -87,7 +85,6 @@ function ChatMessageContent({ reportID={reportID} originalReportID={originalReportID} policyID={report?.policyID} - index={index} shouldDisableEmojiPicker={(chatIncludesConcierge(report) && isBlockedFromConcierge(blockedFromConcierge)) || isArchivedNonExpenseReport(report, isArchivedRoom)} isGroupPolicyReport={!!report?.policyID && report.policyID !== CONST.POLICY.ID_FAKE} /> diff --git a/src/pages/inbox/report/actionContents/ConfirmWhisperContent.tsx b/src/pages/inbox/report/actionContents/ConfirmWhisperContent.tsx index 7a2c9d9c4e5b..68b2f8a9ddfe 100644 --- a/src/pages/inbox/report/actionContents/ConfirmWhisperContent.tsx +++ b/src/pages/inbox/report/actionContents/ConfirmWhisperContent.tsx @@ -4,10 +4,12 @@ import type {OnyxEntry} from 'react-native-onyx'; import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext'; import type {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons'; import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons'; +import useOnyx from '@hooks/useOnyx'; import useReportIsArchived from '@hooks/useReportIsArchived'; import {resolveActionableMentionConfirmWhisper} from '@libs/actions/Report'; import ReportActionItemMessage from '@pages/inbox/report/ReportActionItemMessage'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {Report, ReportAction} from '@src/types/onyx'; type ConfirmWhisperContentProps = { @@ -19,16 +21,19 @@ type ConfirmWhisperContentProps = { }; function ConfirmWhisperContent({action, reportID, originalReportID, report, originalReport}: ConfirmWhisperContentProps) { - const reportActionReport = originalReport ?? report; + const actionReportStable = originalReport ?? report; const isOriginalReportArchived = useReportIsArchived(originalReportID); const mentionReportContextValue = {currentReportID: report?.reportID, exactlyMatch: true}; + // Subscribe to the full report here — the resolve action needs heartbeat fields for its failure-revert payload. + const [actionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${actionReportStable?.reportID}`); + const buttons: ActionableItem[] = [ { text: 'common.buttonConfirm', key: `${action.reportActionID}-actionableReportMentionConfirmWhisper-${CONST.REPORT.ACTIONABLE_MENTION_INVITE_TO_SUBMIT_EXPENSE_CONFIRM_WHISPER.DONE}`, onPress: () => - resolveActionableMentionConfirmWhisper(reportActionReport, action, CONST.REPORT.ACTIONABLE_MENTION_INVITE_TO_SUBMIT_EXPENSE_CONFIRM_WHISPER.DONE, isOriginalReportArchived), + resolveActionableMentionConfirmWhisper(actionReport, action, CONST.REPORT.ACTIONABLE_MENTION_INVITE_TO_SUBMIT_EXPENSE_CONFIRM_WHISPER.DONE, isOriginalReportArchived), isPrimary: true, }, ]; diff --git a/src/pages/inbox/report/actionContents/MentionWhisperContent.tsx b/src/pages/inbox/report/actionContents/MentionWhisperContent.tsx index 78586ae77809..882d7fa20134 100644 --- a/src/pages/inbox/report/actionContents/MentionWhisperContent.tsx +++ b/src/pages/inbox/report/actionContents/MentionWhisperContent.tsx @@ -28,10 +28,14 @@ function MentionWhisperContent({action, report, originalReport, originalReportID const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID); - const reportActionReport = originalReport ?? report; + const reportActionReportStable = originalReport ?? report; const reportPolicyID = report?.policyID; const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${reportPolicyID}`); + // `actionReportStable` is a stable projection (heartbeat fields stripped). The resolve action reads + // those fields for its failure-revert payload, so subscribe to the full report here. + const [actionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportActionReportStable?.reportID}`); + const isReportInPolicy = !!reportPolicyID && reportPolicyID !== CONST.POLICY.ID_FAKE && personalPolicyID !== reportPolicyID; const hasMentionedPolicyMembers = getOriginalMessage(action)?.inviteeEmails?.every((login) => isPolicyMember(policy, login)); @@ -42,7 +46,7 @@ function MentionWhisperContent({action, report, originalReport, originalReportID key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE}`, onPress: () => resolveActionableMentionWhisper( - reportActionReport, + actionReport, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE, isOriginalReportArchived, @@ -56,7 +60,7 @@ function MentionWhisperContent({action, report, originalReport, originalReportID key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE}`, onPress: () => resolveActionableMentionWhisper( - reportActionReport, + actionReport, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE, isOriginalReportArchived, @@ -68,7 +72,7 @@ function MentionWhisperContent({action, report, originalReport, originalReportID key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING}`, onPress: () => resolveActionableMentionWhisper( - reportActionReport, + actionReport, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING, isOriginalReportArchived, diff --git a/src/pages/inbox/report/actionContents/ReportMentionWhisperContent.tsx b/src/pages/inbox/report/actionContents/ReportMentionWhisperContent.tsx index 3087174fd4ef..8ddacb008402 100644 --- a/src/pages/inbox/report/actionContents/ReportMentionWhisperContent.tsx +++ b/src/pages/inbox/report/actionContents/ReportMentionWhisperContent.tsx @@ -4,11 +4,13 @@ import type {OnyxEntry} from 'react-native-onyx'; import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext'; import type {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons'; import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons'; +import useOnyx from '@hooks/useOnyx'; import useReportIsArchived from '@hooks/useReportIsArchived'; import {getOriginalMessage} from '@libs/ReportActionsUtils'; import ReportActionItemMessage from '@pages/inbox/report/ReportActionItemMessage'; import {resolveActionableReportMentionWhisper} from '@userActions/Report'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {Report, ReportAction} from '@src/types/onyx'; type ReportMentionWhisperContentProps = { @@ -20,7 +22,11 @@ type ReportMentionWhisperContentProps = { function ReportMentionWhisperContent({action, reportID, report, originalReport}: ReportMentionWhisperContentProps) { const isReportArchived = useReportIsArchived(reportID); - const reportActionReport = originalReport ?? report; + const reportActionReportStable = originalReport ?? report; + + // Subscribe to the full report here — the resolve action needs heartbeat fields for its failure-revert payload. + const [actionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportActionReportStable?.reportID}`); + const resolution = getOriginalMessage(action)?.resolution; const mentionReportContextValue = {currentReportID: report?.reportID, exactlyMatch: true}; @@ -30,13 +36,13 @@ function ReportMentionWhisperContent({action, reportID, report, originalReport}: { text: 'common.yes', key: `${action.reportActionID}-actionableReportMentionWhisper-${CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.CREATE}`, - onPress: () => resolveActionableReportMentionWhisper(reportActionReport, action, CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.CREATE, isReportArchived), + onPress: () => resolveActionableReportMentionWhisper(actionReport, action, CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.CREATE, isReportArchived), isPrimary: true, }, { text: 'common.no', key: `${action.reportActionID}-actionableReportMentionWhisper-${CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.NOTHING}`, - onPress: () => resolveActionableReportMentionWhisper(reportActionReport, action, CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.NOTHING, isReportArchived), + onPress: () => resolveActionableReportMentionWhisper(actionReport, action, CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.NOTHING, isReportArchived), }, ]; diff --git a/src/selectors/Report.ts b/src/selectors/Report.ts index f14b53d63a6f..977e1ca594f9 100644 --- a/src/selectors/Report.ts +++ b/src/selectors/Report.ts @@ -1,5 +1,5 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; +import type {TupleToUnion, ValueOf} from 'type-fest'; import {getOriginalMessage, isClosedAction} from '@libs/ReportActionsUtils'; import {getPolicyIDsWithEmptyReportsForAccount, isOpenExpenseReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; @@ -55,4 +55,103 @@ function openExpenseReportIDsSelector(reports: OnyxCollection): OpenExpe return openExpenseReportIDMap; } -export {getArchiveReason, getReportChatType, getReportOwnerAccountID, getReportPolicyID, policyIDsWithEmptyReportsSelector, openExpenseReportIDsSelector}; +type ValidReportKeys> = T; + +/** + * Fields deliberately stripped from the projection. They update on routine activity + * (incoming/outgoing messages, read receipts) and would invalidate the projection on every + * chat heartbeat even though no item-subtree consumer reads them. + */ +type ExcludedFields = ValidReportKeys< + [ + 'lastMessageText', + 'lastVisibleActionCreated', + 'lastReadTime', + 'lastReadSequenceNumber', + 'lastMentionedTime', + 'lastVisibleActionLastModified', + 'lastMessageHtml', + 'lastActorAccountID', + 'lastActionType', + ] +>; + +type StableReport = Omit>; + +/** + * Stable `Report` projection for components that must not re-render on chat heartbeat + * fields (`last*` on `Report`). Intended as a bridge until rows subscribe to derived per-row facts. + * + * If a consumer needs excluded fields (e.g. ConfirmWhisperContent), subscribe separately to the + * full report — do not add those fields back into this projection. + * + * When adding a new `Report` field: include it in the return object below; only add to + * `ExcludedFields` if it updates on every message/read and the subtree does not read it. + */ +function getStableReportSelector(report: OnyxEntry) { + if (!report?.reportID) { + return undefined; + } + return { + reportID: report.reportID, + avatarUrl: report.avatarUrl, + created: report.created, + submitted: report.submitted, + approved: report.approved, + chatType: report.chatType, + hasOutstandingChildRequest: report.hasOutstandingChildRequest, + hasOutstandingChildTask: report.hasOutstandingChildTask, + isOwnPolicyExpenseChat: report.isOwnPolicyExpenseChat, + isPinned: report.isPinned, + policyAvatar: report.policyAvatar, + policyName: report.policyName, + oldPolicyName: report.oldPolicyName, + hasParentAccess: report.hasParentAccess, + description: report.description, + isDeletedParentAction: report.isDeletedParentAction, + policyID: report.policyID, + reportName: report.reportName, + chatReportID: report.chatReportID, + stateNum: report.stateNum, + statusNum: report.statusNum, + writeCapability: report.writeCapability, + type: report.type, + visibility: report.visibility, + invoiceReceiver: report.invoiceReceiver, + transactionCount: report.transactionCount, + parentReportID: report.parentReportID, + parentReportActionID: report.parentReportActionID, + // 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 projection stable through that reconciliation. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + managerID: report.managerID || undefined, + ownerAccountID: report.ownerAccountID, + participants: report.participants, + total: report.total, + unheldTotal: report.unheldTotal, + unheldNonReimbursableTotal: report.unheldNonReimbursableTotal, + currency: report.currency, + errorFields: report.errorFields, + errors: report.errors, + isWaitingOnBankAccount: report.isWaitingOnBankAccount, + isCancelledIOU: report.isCancelledIOU, + hasReportBeenRetracted: report.hasReportBeenRetracted, + hasReportBeenReopened: report.hasReportBeenReopened, + isExportedToIntegration: report.isExportedToIntegration, + hasExportError: report.hasExportError, + iouReportID: report.iouReportID, + preexistingReportID: report.preexistingReportID, + nonReimbursableTotal: report.nonReimbursableTotal, + privateNotes: report.privateNotes, + fieldList: report.fieldList, + permissions: report.permissions, + tripData: report.tripData, + welcomeMessage: report.welcomeMessage, + nextStep: report.nextStep, + pendingAction: report.pendingAction, + pendingFields: report.pendingFields, + } satisfies Record & StableReport; +} + +export {getArchiveReason, getReportChatType, getReportOwnerAccountID, getReportPolicyID, policyIDsWithEmptyReportsSelector, openExpenseReportIDsSelector, getStableReportSelector}; diff --git a/tests/ui/ClearReportActionErrorsUITest.tsx b/tests/ui/ClearReportActionErrorsUITest.tsx index e46b322e68db..d7f456a1765c 100644 --- a/tests/ui/ClearReportActionErrorsUITest.tsx +++ b/tests/ui/ClearReportActionErrorsUITest.tsx @@ -97,7 +97,6 @@ describe('ClearReportActionErrors UI', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} originalReportID={originalReportID} /> diff --git a/tests/ui/PureReportActionItemTest.tsx b/tests/ui/PureReportActionItemTest.tsx index 242b61379145..eaf5f3ee07f8 100644 --- a/tests/ui/PureReportActionItemTest.tsx +++ b/tests/ui/PureReportActionItemTest.tsx @@ -107,7 +107,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -393,7 +392,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -441,7 +439,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -493,7 +490,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -537,7 +533,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -606,7 +601,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -662,7 +656,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -745,7 +738,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -924,7 +916,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -971,7 +962,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -1125,7 +1115,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -1304,7 +1293,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -1395,7 +1383,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} isClosedExpenseReportWithNoExpenses /> @@ -1431,7 +1418,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -1468,7 +1454,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -1516,7 +1501,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -1552,7 +1536,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -1592,7 +1575,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -1630,7 +1612,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -1667,7 +1648,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -1707,7 +1687,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -1748,7 +1727,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -1873,7 +1851,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -1910,7 +1887,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -2489,7 +2465,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} /> @@ -2577,7 +2552,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} reportNameValuePairsOrigin="harvest" reportNameValuePairsOriginalID="origReport123" @@ -2615,7 +2589,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} reportNameValuePairsOrigin="harvest" reportNameValuePairsOriginalID="origReport123" @@ -2665,7 +2638,6 @@ describe('PureReportActionItem', () => { action={action} displayAsGroup={false} shouldDisplayNewMarker={false} - index={0} isFirstVisibleReportAction={false} isThreadReportParentAction /> diff --git a/tests/ui/ReportActionItemMessageEditTest.tsx b/tests/ui/ReportActionItemMessageEditTest.tsx index 589e6513aa1f..0194b0fd2c8b 100644 --- a/tests/ui/ReportActionItemMessageEditTest.tsx +++ b/tests/ui/ReportActionItemMessageEditTest.tsx @@ -70,7 +70,6 @@ const defaultProps: ReportActionItemMessageEditProps = { action: defaultReportAction, reportID: defaultReport.reportID, originalReportID: defaultReport.reportID, - index: 0, isGroupPolicyReport: false, }; diff --git a/tests/ui/ReportActionMessageEditLayoutTest.tsx b/tests/ui/ReportActionMessageEditLayoutTest.tsx index c3fbb0b6d199..de69ac25343a 100644 --- a/tests/ui/ReportActionMessageEditLayoutTest.tsx +++ b/tests/ui/ReportActionMessageEditLayoutTest.tsx @@ -159,7 +159,6 @@ function MessageEditLayoutHost({layout}: {layout: LayoutMode}) { action={commentAction} reportID={defaultReport.reportID} originalReportID={defaultReport.reportID} - index={0} isGroupPolicyReport={false} /> )}