From 7cd85b632a9faec2b8cd44914246ac24a21206c1 Mon Sep 17 00:00:00 2001 From: Yuwen Memon Date: Fri, 24 Jul 2026 11:51:35 -0700 Subject: [PATCH] revert: render server-driven AZ status on expense reports --- .../MoneyRequestReportActionsList.tsx | 39 +----------- .../MoneyRequestReportView.tsx | 44 ++++++------- src/pages/inbox/AgentZeroStatusContext.tsx | 9 +-- src/pages/inbox/report/ReportActionsList.tsx | 4 +- .../inbox/report/ReportActionsListHeader.tsx | 21 +++++++ .../report/ReportActionsListTailIndicator.tsx | 21 ------- ...equestReportActionsListRejectModalTest.tsx | 27 -------- tests/ui/MoneyRequestReportViewTest.tsx | 27 -------- tests/unit/AgentZeroStatusContextTest.ts | 62 ------------------- ...st.tsx => ReportActionsListHeaderTest.tsx} | 8 +-- 10 files changed, 50 insertions(+), 212 deletions(-) create mode 100644 src/pages/inbox/report/ReportActionsListHeader.tsx delete mode 100644 src/pages/inbox/report/ReportActionsListTailIndicator.tsx rename tests/unit/{ReportActionsListTailIndicatorTest.tsx => ReportActionsListHeaderTest.tsx} (78%) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index a56672263755..9e69a9566c10 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -52,7 +52,6 @@ import {useConciergeDraft} from '@pages/inbox/ConciergeDraftContext'; import FloatingMessageCounter from '@pages/inbox/report/FloatingMessageCounter'; import ReportActionIndexContext from '@pages/inbox/report/ReportActionIndexContext'; import ReportActionsListItemRenderer from '@pages/inbox/report/ReportActionsListItemRenderer'; -import ReportActionsListTailIndicator from '@pages/inbox/report/ReportActionsListTailIndicator'; import {getUnreadMarkerReportAction} from '@pages/inbox/report/shouldDisplayNewMarkerOnReportAction'; import useReportUnreadMessageScrollTracking from '@pages/inbox/report/useReportUnreadMessageScrollTracking'; @@ -139,7 +138,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) const {reportActions: unfilteredReportActions, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID, route?.params?.reportActionID); const reportActions = useMemo(() => getFilteredReportActionsForReportView(unfilteredReportActions), [unfilteredReportActions]); - const {draftReportAction, isDraftPendingCompletion} = useConciergeDraft(); + const {draftReportAction} = useConciergeDraft(); const draftReportActionID = draftReportAction?.reportActionID; const allReportTransactions = useReportTransactionsCollection(reportIDFromRoute); @@ -224,7 +223,6 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) const listRef = useActionListRef(); const scrollingVerticalBottomOffset = useRef(0); - const tailIndicatorHeightRef = useRef(0); const readActionSkipped = useRef(false); const stickToBottomRef = useRef(false); const stickToBottomTimeoutRef = useRef(null); @@ -508,10 +506,6 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) resetKey: report?.reportID ?? reportIDFromRoute ?? '', }); - useEffect(() => { - tailIndicatorHeightRef.current = 0; - }, [report?.reportID]); - /** * Subscribe to read/unread events and update our unreadMarkerTime */ @@ -700,24 +694,6 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) } }; - const handleTailIndicatorLayout = useCallback( - (event: LayoutChangeEvent) => { - const previousHeight = tailIndicatorHeightRef.current; - const nextHeight = event.nativeEvent.layout.height; - tailIndicatorHeightRef.current = nextHeight; - - if (previousHeight > 0 || nextHeight <= 0 || linkedReportActionID || !hasNewestReportAction || scrollOffsetRef.current >= CONST.REPORT.ACTIONS.AUTOSCROLL_TO_TOP_THRESHOLD) { - return; - } - - setIsFloatingMessageCounterVisible(false); - requestAnimationFrame(() => { - scrollToBottom(); - }); - }, - [hasNewestReportAction, linkedReportActionID, scrollToBottom, scrollOffsetRef, setIsFloatingMessageCounterVisible], - ); - /** * Runs when the FlatList finishes laying out */ @@ -738,18 +714,6 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) return null; } - const listFooterComponent = ( - - - - ); - const shouldUseMarkAsDoneCopy = shouldShowMarkAsDone({ policy, report, @@ -814,7 +778,6 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) contentContainerStyle={shouldUseNarrowLayout ? styles.pt4 : styles.pt3} isLoadingInitialActions={!!showReportActionsLoadingState} skeletonReasonAttributes={skeletonReasonAttributes} - listFooterComponent={listFooterComponent} /> )} diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx index ab3017773f2a..14b16356a090 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx @@ -28,8 +28,6 @@ import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan import Navigation from '@navigation/Navigation'; -import {AgentZeroStatusProvider} from '@pages/inbox/AgentZeroStatusContext'; -import {ConciergeDraftProvider} from '@pages/inbox/ConciergeDraftContext'; import ReportActionsList from '@pages/inbox/report/ReportActionsList'; import ReportFooter from '@pages/inbox/report/ReportFooter'; import UserTypingEventListener from '@pages/inbox/report/UserTypingEventListener'; @@ -278,29 +276,25 @@ function MoneyRequestReportView({report, reportLoadingState, shouldDisplayReport )} - - - - {shouldDisplayMoneyRequestActionsList ? ( - - ) : ( - <> - - - - )} - {shouldDisplayReportFooter ? ( - <> - - - - ) : null} - - - + + {shouldDisplayMoneyRequestActionsList ? ( + + ) : ( + <> + + + + )} + {shouldDisplayReportFooter ? ( + <> + + + + ) : null} + diff --git a/src/pages/inbox/AgentZeroStatusContext.tsx b/src/pages/inbox/AgentZeroStatusContext.tsx index 0727482b7b5e..6ba2c67765fa 100644 --- a/src/pages/inbox/AgentZeroStatusContext.tsx +++ b/src/pages/inbox/AgentZeroStatusContext.tsx @@ -59,9 +59,8 @@ const AgentZeroStatusStateContext = createContext(defaultS const AgentZeroStatusActionsContext = createContext(defaultActions); /** - * Cheap outer guard — subscribes to scalar report metadata plus this report's processing-indicator - * NVP. For non-AgentZero reports with no server-driven indicator (the common case), returns - * children directly. + * Cheap outer guard — only subscribes to the scalar CONCIERGE_REPORT_ID and the report's chat + * metadata. For non-AgentZero reports (the common case), returns children directly. * * AgentZero chats include Concierge DMs, policy #admins rooms, and custom-agent chats (any * report with a participant whose personalDetails carries `isCustomAgent: true`, stamped @@ -73,15 +72,13 @@ function AgentZeroStatusProvider({reportID, children}: React.PropsWithChildren<{ const [agentParticipantAccountID] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: getCustomAgentParticipantAccountID(participantAccountIDs)}); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [currentUserAccountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector}); - const [serverAgentIDs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`, {selector: agentZeroProcessingAgentIDsSelector}); const isConciergeChat = reportID === conciergeReportID; const isAdmin = chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS; const isCustomAgentChat = agentParticipantAccountID !== undefined; - const hasServerDrivenStatus = (serverAgentIDs?.length ?? 0) > 0; const otherParticipantCount = currentUserAccountID === undefined ? 0 : (participantAccountIDs ?? []).filter((accountID) => accountID !== currentUserAccountID).length; const customAgentDMAccountID = isCustomAgentChat && isDMReport && otherParticipantCount === 1 ? agentParticipantAccountID : undefined; - const isAgentZeroChat = isConciergeChat || isAdmin || isCustomAgentChat || hasServerDrivenStatus; + const isAgentZeroChat = isConciergeChat || isAdmin || isCustomAgentChat; if (!reportID || !isAgentZeroChat) { return children; diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx index 2ba648c83921..0e1cbd5f6b88 100644 --- a/src/pages/inbox/report/ReportActionsList.tsx +++ b/src/pages/inbox/report/ReportActionsList.tsx @@ -66,9 +66,9 @@ import React, {useEffect, useRef, useState} from 'react'; import FloatingMessageCounter from './FloatingMessageCounter'; import ReportActionIndexContext from './ReportActionIndexContext'; import {useReportActionsListActions, useReportActionsListState} from './ReportActionsListContext'; +import ReportActionsListHeader from './ReportActionsListHeader'; import ReportActionsListItemRenderer from './ReportActionsListItemRenderer'; import ReportActionsListPaddingView from './ReportActionsListPaddingView'; -import ReportActionsListTailIndicator from './ReportActionsListTailIndicator'; import ReportActionsSkeletonGuard from './ReportActionsSkeletonGuard'; import ShowPreviousMessagesButton from './ShowPreviousMessagesButton'; import useFollowActionBadgeTarget from './useFollowActionBadgeTarget'; @@ -396,7 +396,7 @@ function ReportActionsListContent({reportID, onLayout}: ReportActionsListProps) const extraData = [shouldUseNarrowLayout ? unreadMarkerReportActionID : undefined, isArchivedNonExpenseReport(report, isReportArchived), draftReportActionID, draftMessageHTML]; const listHeaderComponent = ( - diff --git a/src/pages/inbox/report/ReportActionsListHeader.tsx b/src/pages/inbox/report/ReportActionsListHeader.tsx new file mode 100644 index 000000000000..9929f339e7f9 --- /dev/null +++ b/src/pages/inbox/report/ReportActionsListHeader.tsx @@ -0,0 +1,21 @@ +import ConciergeThinkingMessage from '@pages/home/report/ConciergeThinkingMessage'; + +import React from 'react'; + +type ReportActionsListHeaderProps = { + /** The ID of the report being displayed */ + reportID: string; + + /** Whether a Concierge draft is still streaming in — hides the thinking indicator only while the reply is actively revealing, not after it completes */ + isDraftPendingCompletion?: boolean; +}; + +function ReportActionsListHeader({reportID, isDraftPendingCompletion}: ReportActionsListHeaderProps) { + if (isDraftPendingCompletion) { + return null; + } + + return ; +} + +export default ReportActionsListHeader; diff --git a/src/pages/inbox/report/ReportActionsListTailIndicator.tsx b/src/pages/inbox/report/ReportActionsListTailIndicator.tsx deleted file mode 100644 index ef7b56e7e377..000000000000 --- a/src/pages/inbox/report/ReportActionsListTailIndicator.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import ConciergeThinkingMessage from '@pages/home/report/ConciergeThinkingMessage'; - -import React from 'react'; - -type ReportActionsListTailIndicatorProps = { - /** The ID of the report being displayed */ - reportID: string; - - /** Whether a Concierge draft is still streaming in - hides the thinking indicator only while the reply is actively revealing, not after it completes */ - isDraftPendingCompletion?: boolean; -}; - -function ReportActionsListTailIndicator({reportID, isDraftPendingCompletion}: ReportActionsListTailIndicatorProps) { - if (isDraftPendingCompletion) { - return null; - } - - return ; -} - -export default ReportActionsListTailIndicator; diff --git a/tests/ui/MoneyRequestReportActionsListRejectModalTest.tsx b/tests/ui/MoneyRequestReportActionsListRejectModalTest.tsx index 97c395f06e91..d25724dcfe45 100644 --- a/tests/ui/MoneyRequestReportActionsListRejectModalTest.tsx +++ b/tests/ui/MoneyRequestReportActionsListRejectModalTest.tsx @@ -69,15 +69,6 @@ jest.mock('@components/MoneyRequestReportView/MoneyRequestReportTransactionList' return ({listFooterComponent}: {listFooterComponent?: React.ReactElement}) => {listFooterComponent}; }); -jest.mock('@pages/home/report/ConciergeThinkingMessage', () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const {View} = require('react-native'); - return { - __esModule: true, - default: () => , - }; -}); - jest.mock('@components/HoldOrRejectEducationalModal', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const {View} = require('react-native'); @@ -240,24 +231,6 @@ describe('MoneyRequestReportActionsList - Reject Educational Modal', () => { }); }); - it('renders Concierge thinking indicator in the money request report table path', async () => { - await act(async () => { - await Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT}${FAKE_REPORT_ID}` as const]: mockReport, - [`${ONYXKEYS.COLLECTION.POLICY}${FAKE_POLICY_ID}` as const]: mockPolicy, - [`${ONYXKEYS.COLLECTION.TRANSACTION}${FAKE_TRANSACTION_ID}` as const]: mockTransaction, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${FAKE_REPORT_ID}` as const]: {[mockReportAction.reportActionID]: mockReportAction}, - [`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${FAKE_REPORT_ID}` as const]: {isLoadingInitialReportActions: false, hasOnceLoadedReportActions: true}, - [ONYXKEYS.SESSION]: {accountID: FAKE_ACCOUNT_ID, email: FAKE_EMAIL} as Session, - }); - }); - - renderComponent(); - await waitForBatchedUpdatesWithAct(); - - expect(screen.getByTestId('ConciergeThinkingMessage')).toBeTruthy(); - }); - it('should show reject educational modal when reject option is selected and explanation has NOT been dismissed', async () => { await act(async () => { await Onyx.multiSet({ diff --git a/tests/ui/MoneyRequestReportViewTest.tsx b/tests/ui/MoneyRequestReportViewTest.tsx index 637ca0f6e4a8..c43e56141cc7 100644 --- a/tests/ui/MoneyRequestReportViewTest.tsx +++ b/tests/ui/MoneyRequestReportViewTest.tsx @@ -13,8 +13,6 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import * as MoneyRequestReportUtils from '@libs/MoneyRequestReportUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import {AgentZeroStatusProvider} from '@pages/inbox/AgentZeroStatusContext'; -import {ConciergeDraftProvider} from '@pages/inbox/ConciergeDraftContext'; import ReportActionsList from '@pages/inbox/report/ReportActionsList'; import UserTypingEventListener from '@pages/inbox/report/UserTypingEventListener'; @@ -57,22 +55,6 @@ jest.mock('@components/MoneyRequestHeader', () => jest.fn(() => null)); jest.mock('@components/CollapsibleHeaderOnKeyboard', () => jest.fn(() => null)); jest.mock('@components/ReportActionItem/MoneyRequestReceiptView', () => jest.fn(() => null)); jest.mock('@pages/inbox/report/ReportFooter', () => jest.fn(() => null)); -jest.mock('@pages/inbox/AgentZeroStatusContext', () => { - const reactModule = jest.requireActual('react'); - return { - AgentZeroStatusProvider: jest.fn(({children}: {children: React.ReactNode}) => reactModule.createElement(reactModule.Fragment, null, children)), - useAgentZeroStatus: jest.fn(() => ({candidateAgentIDs: []})), - useAgentZeroStatusActions: jest.fn(() => ({kickoffWaitingIndicator: jest.fn()})), - }; -}); -jest.mock('@pages/inbox/ConciergeDraftContext', () => { - const reactModule = jest.requireActual('react'); - return { - ConciergeDraftProvider: jest.fn(({children}: {children: React.ReactNode}) => reactModule.createElement(reactModule.Fragment, null, children)), - useConciergeDraft: jest.fn(() => ({draftReportAction: null, hasActiveDraft: false, isDraftPendingCompletion: false})), - useConciergeDraftActions: jest.fn(() => ({clearDraft: jest.fn(), dispatchLocalDraftEvent: jest.fn(), revealDraftFromReportAction: jest.fn()})), - }; -}); jest.mock('@components/OfflineWithFeedback', () => { const reactModule = jest.requireActual('react'); return jest.fn(({children}: {children: React.ReactNode}) => reactModule.createElement(reactModule.Fragment, null, children)); @@ -86,8 +68,6 @@ const mockUseReportTransactionsCollection = useReportTransactionsCollection as j const mockMoneyRequestReportActionsList = MoneyRequestReportActionsList as jest.MockedFunction; const mockReportActionsListBody = ReportActionsList as jest.MockedFunction; const mockUserTypingEventListener = UserTypingEventListener as jest.MockedFunction; -const mockAgentZeroStatusProvider = AgentZeroStatusProvider as jest.MockedFunction; -const mockConciergeDraftProvider = ConciergeDraftProvider as jest.MockedFunction; const defaultPaginatedReportActionsResult: ReturnType = { reportActions: [], @@ -212,11 +192,4 @@ describe('MoneyRequestReportView', () => { expect(mockReportActionsListBody).not.toHaveBeenCalled(); expect(mockUserTypingEventListener).not.toHaveBeenCalled(); }); - - it('wraps the action list and footer in the AgentZero providers for the report', () => { - renderMoneyRequestReportView(jest.fn()); - - expect(mockAgentZeroStatusProvider.mock.calls.at(-1)?.at(0)).toEqual(expect.objectContaining({reportID: REPORT_ID})); - expect(mockConciergeDraftProvider.mock.calls.at(-1)?.at(0)).toEqual(expect.objectContaining({reportID: REPORT_ID})); - }); }); diff --git a/tests/unit/AgentZeroStatusContextTest.ts b/tests/unit/AgentZeroStatusContextTest.ts index 99f2a96590df..0dcc41532bd7 100644 --- a/tests/unit/AgentZeroStatusContextTest.ts +++ b/tests/unit/AgentZeroStatusContextTest.ts @@ -143,68 +143,6 @@ describe('AgentZeroStatusContext', () => { expect(mockSubscribeToReportReasoningEvents).not.toHaveBeenCalled(); }); - it('should render server-driven status for an expense report', async () => { - // Given a non-Concierge expense report with a server-written processing indicator - const serverLabel = 'Concierge is updating category...'; - await Onyx.merge(ONYXKEYS.CONCIERGE_REPORT_ID, '999'); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { - reportID, - type: CONST.REPORT.TYPE.EXPENSE, - }); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`, { - agentZeroProcessingRequestIndicator: {[CONST.ACCOUNT_ID.CONCIERGE]: serverLabel}, - }); - - // When the provider renders for that report, because the status gate must subscribe - const {result} = renderHook( - () => ({ - state: useAgentZeroStatus(), - status: useAgentZeroStatusIndicator(reportID, CONST.ACCOUNT_ID.CONCIERGE), - }), - {wrapper}, - ); - - // Then Concierge should become the only candidate and show the server label because - // App renders parent-report status from the NVP rather than local optimism. - await waitForBatchedUpdates(); - expect(result.current.state.candidateAgentIDs).toEqual([CONST.ACCOUNT_ID.CONCIERGE]); - expect(result.current.status.isProcessing).toBe(true); - expect(result.current.status.statusLabel).toBe(serverLabel); - expect(mockSubscribeToReportReasoningEvents).toHaveBeenCalledWith(reportID); - }); - - it('should not optimistically kickoff for an expense report without server status', async () => { - // Given a non-Concierge expense report with no server-written processing indicator - await Onyx.merge(ONYXKEYS.CONCIERGE_REPORT_ID, '999'); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { - reportID, - type: CONST.REPORT.TYPE.EXPENSE, - }); - - const {result} = renderHook( - () => ({ - state: useAgentZeroStatus(), - status: useAgentZeroStatusIndicator(reportID, CONST.ACCOUNT_ID.CONCIERGE), - actions: useAgentZeroStatusActions(), - }), - {wrapper}, - ); - await waitForBatchedUpdates(); - - // When the composer tries to kickoff an indicator outside an AgentZero chat - act(() => { - result.current.actions.kickoffWaitingIndicator(); - }); - - // Then no candidate or subscription should appear because server-driven expense - // reports should remain quiet until a real processing NVP arrives. - await waitForBatchedUpdates(); - expect(result.current.state.candidateAgentIDs).toEqual([]); - expect(result.current.status.isProcessing).toBe(false); - expect(result.current.status.statusLabel).toBe(''); - expect(mockSubscribeToReportReasoningEvents).not.toHaveBeenCalled(); - }); - it('should return processing state when server label is present in Concierge chat', async () => { // Given a Concierge chat with a server status label const serverLabel = 'Concierge is looking up categories...'; diff --git a/tests/unit/ReportActionsListTailIndicatorTest.tsx b/tests/unit/ReportActionsListHeaderTest.tsx similarity index 78% rename from tests/unit/ReportActionsListTailIndicatorTest.tsx rename to tests/unit/ReportActionsListHeaderTest.tsx index a99138fef1a0..4da9069ddf18 100644 --- a/tests/unit/ReportActionsListTailIndicatorTest.tsx +++ b/tests/unit/ReportActionsListHeaderTest.tsx @@ -1,6 +1,6 @@ import {render, screen} from '@testing-library/react-native'; -import ReportActionsListTailIndicator from '@pages/inbox/report/ReportActionsListTailIndicator'; +import ReportActionsListHeader from '@pages/inbox/report/ReportActionsListHeader'; import React from 'react'; @@ -15,19 +15,19 @@ jest.mock('@pages/home/report/ConciergeThinkingMessage', () => { }; }); -describe('ReportActionsListTailIndicator', () => { +describe('ReportActionsListHeader', () => { beforeEach(() => { jest.clearAllMocks(); }); it('renders ConciergeThinkingMessage when no draft is pending completion', () => { - render(); + render(); expect(screen.getByTestId('ConciergeThinkingMessage')).toBeTruthy(); }); it('renders nothing while a draft is still streaming in (pending completion)', () => { render( - ,