From cdfa5751a8634e96b1bf9f195047dfd5f69f62d7 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Tue, 4 Aug 2026 19:54:52 +0300 Subject: [PATCH 01/11] Navigate to thread --- src/CONST/index.ts | 1 + src/libs/actions/Report/index.ts | 47 ++++++++++++++++++++++ src/pages/inbox/AgentZeroStatusContext.tsx | 9 +++-- src/pages/inbox/ConciergeDraftContext.tsx | 6 ++- src/selectors/Report.ts | 5 +++ 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index c56be545e2d9..0ab0da6dbf0d 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1007,6 +1007,7 @@ const CONST = { GLOBAL_REIMBURSEMENTS: 'globalReimbursements', GLOBAL_REIMBURSEMENT_FX: 'globalReimbursementFX', DEFAULT_LETTER_AVATARS: 'defaultLetterAvatars', + CONCIERGE_RESPOND_IN_THREAD: 'conciergeRespondInThread', }, BUTTON_STATES: { DEFAULT: 'default', diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 270a148fb2f5..aaeaae921a85 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -844,6 +844,47 @@ function buildOptimisticResolvedFollowups(reportAction: OnyxEntry) }; } +/** How long to wait for the server to report a Concierge thread before giving up on following the user into it. */ +const CONCIERGE_THREAD_NAVIGATION_TIMEOUT_MS = 15000; + +/** + * Concierge answers each question in a thread off the asking message. The server opens that thread while the + * comment is being created and the thread's ID reaches us as `childReportID` on the comment, so wait for it + * rather than guessing an ID. Write requests resolve as soon as they are queued and never carry the server's + * response, so this is the only place the thread ID surfaces. + * + * Gives up after a timeout because the server declines to thread in several cases (a human agent already + * handling the chat, pregenerated replies, the beta being off), and then `childReportID` never arrives. + */ +function followConciergeThreadWhenOpened(reportID: string, reportActionID: string) { + let timeoutID: ReturnType; + + // connectWithoutView because this is an action-layer subscription with no component behind it. + const connectionID = Onyx.connectWithoutView({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + callback: (reportActions) => { + const childReportID = reportActions?.[reportActionID]?.childReportID; + if (!childReportID) { + return; + } + clearTimeout(timeoutID); + Onyx.disconnect(connectionID); + + // The user may have moved on while the thread was being opened; only follow the reply if they + // are still sitting in the chat they asked from. + if (Navigation.getTopmostReportId() !== reportID) { + return; + } + if (isSearchTopmostFullScreenRoute()) { + Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: childReportID, backTo: Navigation.getActiveRoute()})); + } else { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID, undefined, undefined, Navigation.getActiveRoute())); + } + }, + }); + timeoutID = setTimeout(() => Onyx.disconnect(connectionID), CONCIERGE_THREAD_NAVIGATION_TIMEOUT_MS); +} + /** * Add up to two report actions to a report. This method can be called for the following situations: * @@ -1143,6 +1184,12 @@ function addActions({ successData, failureData, }); + + // Concierge answers each question in its own thread, opened server-side while the comment is created. + // Not from the side panel, which renders its own report and would navigate a surface the user isn't looking at. + if (isConciergeChat && !isInSidePanel && resolvedReportActionID) { + followConciergeThreadWhenOpened(reportID, resolvedReportActionID); + } notifyNewAction(resolvedNotifyReportID, lastAction, lastAction?.actorAccountID === currentUserAccountID); } diff --git a/src/pages/inbox/AgentZeroStatusContext.tsx b/src/pages/inbox/AgentZeroStatusContext.tsx index 8e94f995717a..6a913fb0c9be 100644 --- a/src/pages/inbox/AgentZeroStatusContext.tsx +++ b/src/pages/inbox/AgentZeroStatusContext.tsx @@ -12,7 +12,7 @@ import type Report from '@src/types/onyx/Report'; import type {OnyxEntry} from 'react-native-onyx'; import {getCustomAgentParticipantAccountID, getReportParticipantAccountIDs} from '@selectors/AgentZeroChat'; -import {getReportChatType} from '@selectors/Report'; +import {getReportChatType, getReportParentReportID} from '@selectors/Report'; import {getNewestReportActionSelector} from '@selectors/ReportAction'; import {agentZeroProcessingAgentIDsSelector} from '@selectors/ReportNameValuePairs'; import {accountIDSelector} from '@selectors/Session'; @@ -36,6 +36,7 @@ type AgentZeroStatusActions = { type ReportMeta = { chatType: Report['chatType']; isDM: boolean; + parentReportID: Report['parentReportID']; participantAccountIDs: number[]; }; @@ -43,6 +44,7 @@ function reportMetaSelector(report: OnyxEntry): ReportMeta { return { chatType: getReportChatType(report), isDM: isDM(report), + parentReportID: getReportParentReportID(report), participantAccountIDs: getReportParticipantAccountIDs(report), }; } @@ -79,7 +81,7 @@ const AgentZeroStatusActionsContext = createContext(defa */ function AgentZeroStatusProvider({reportID, children}: React.PropsWithChildren<{reportID: string | undefined}>) { const [reportMeta] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {selector: reportMetaSelector}); - const {chatType, isDM: isDMReport = false, participantAccountIDs} = reportMeta ?? {}; + const {chatType, isDM: isDMReport = false, parentReportID, participantAccountIDs} = reportMeta ?? {}; const [agentParticipantAccountID] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: getCustomAgentParticipantAccountID(participantAccountIDs)}); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [currentUserAccountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector}); @@ -89,7 +91,8 @@ function AgentZeroStatusProvider({reportID, children}: React.PropsWithChildren<{ // actively-processing agents changes. const [serverAgentIDs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`, {selector: agentZeroProcessingAgentIDsSelector}); - const isConciergeChat = reportID === conciergeReportID; + // Concierge answers each question in a thread off the Concierge DM, so those threads carry its indicator too. + const isConciergeChat = !!conciergeReportID && (reportID === conciergeReportID || parentReportID === conciergeReportID); const isAdmin = chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS; const isCustomAgentChat = agentParticipantAccountID !== undefined; const otherParticipantCount = currentUserAccountID === undefined ? 0 : (participantAccountIDs ?? []).filter((accountID) => accountID !== currentUserAccountID).length; diff --git a/src/pages/inbox/ConciergeDraftContext.tsx b/src/pages/inbox/ConciergeDraftContext.tsx index ac0d7fe2f971..07297f60c2ac 100644 --- a/src/pages/inbox/ConciergeDraftContext.tsx +++ b/src/pages/inbox/ConciergeDraftContext.tsx @@ -9,7 +9,7 @@ import {policyTypeSelector} from '@src/selectors/Policy'; import type {ReportAction} from '@src/types/onyx'; import {getCustomAgentParticipantAccountID, getReportParticipantAccountIDs} from '@selectors/AgentZeroChat'; -import {getReportChatType, getReportPolicyID} from '@selectors/Report'; +import {getReportChatType, getReportParentReportID, getReportPolicyID} from '@selectors/Report'; import React, {createContext, useContext} from 'react'; import {CONCIERGE_DRAFT_STATUS} from './conciergeDraftState'; @@ -52,8 +52,10 @@ function ConciergeDraftProvider({reportID, children}: React.PropsWithChildren<{r const [participantAccountIDs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {selector: getReportParticipantAccountIDs}); const [agentParticipantAccountID] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: getCustomAgentParticipantAccountID(participantAccountIDs)}); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); + const [parentReportID] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {selector: getReportParentReportID}); - const isConciergeChat = reportID === conciergeReportID; + // Concierge answers each question in a thread off the Concierge DM, so those threads stream drafts too. + const isConciergeChat = !!conciergeReportID && (reportID === conciergeReportID || parentReportID === conciergeReportID); const isAdmin = chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS; // See AgentZeroStatusContext for the rationale: `isCustomAgent` lives on the participant's // personalDetails, stamped by Auth in `Account::formatNewDotPersonalDetails`. diff --git a/src/selectors/Report.ts b/src/selectors/Report.ts index cfa0b41246ba..978e4f526aaa 100644 --- a/src/selectors/Report.ts +++ b/src/selectors/Report.ts @@ -43,6 +43,10 @@ function getReportOwnerAccountID(report: OnyxEntry) { return report?.ownerAccountID; } +function getReportParentReportID(report: OnyxEntry) { + return report?.parentReportID; +} + const policyIDsWithEmptyReportsSelector = (accountID: number | undefined, transactionsByReportID: Record, hasDismissedEmptyReportsConfirmation: boolean) => (reports: OnyxCollection) => { if (hasDismissedEmptyReportsConfirmation || !accountID) { @@ -213,6 +217,7 @@ export { getArchiveReason, getReportChatType, getReportOwnerAccountID, + getReportParentReportID, getReportPolicyID, policyIDsWithEmptyReportsSelector, canShowReportRecipientLocalTimeSelector, From 13999fd5364ce2cc552bb3fd30b6ced889a55f83 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Wed, 5 Aug 2026 20:22:28 +0300 Subject: [PATCH 02/11] Navigate optimistically --- .../AddCommentOrAttachmentParams.ts | 1 + src/libs/actions/Report/index.ts | 105 ++++++++++-------- .../ReportActionCompose/useComposerSubmit.ts | 8 +- 3 files changed, 67 insertions(+), 47 deletions(-) diff --git a/src/libs/API/parameters/AddCommentOrAttachmentParams.ts b/src/libs/API/parameters/AddCommentOrAttachmentParams.ts index e697ce21abaf..45e76283fee1 100644 --- a/src/libs/API/parameters/AddCommentOrAttachmentParams.ts +++ b/src/libs/API/parameters/AddCommentOrAttachmentParams.ts @@ -16,6 +16,7 @@ type AddCommentOrAttachmentParams = { optimisticConciergeCreated?: string; pregeneratedResponse?: string; sidePanelContext?: string; + conciergeThreadReportID?: string; }; export default AddCommentOrAttachmentParams; diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index aaeaae921a85..4ea0ade01bcb 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -396,6 +396,7 @@ type AddCommentParams = { reportActionID?: string; delegateAccountID: number | undefined; conciergeReportID: string | undefined; + conciergeThreadReportID?: string; }; type AddActionsParams = { @@ -412,6 +413,7 @@ type AddActionsParams = { reportActionID?: string; delegateAccountID: number | undefined; conciergeReportID: string | undefined; + conciergeThreadReportID?: string; }; type AddAttachmentWithCommentParams = { @@ -844,47 +846,6 @@ function buildOptimisticResolvedFollowups(reportAction: OnyxEntry) }; } -/** How long to wait for the server to report a Concierge thread before giving up on following the user into it. */ -const CONCIERGE_THREAD_NAVIGATION_TIMEOUT_MS = 15000; - -/** - * Concierge answers each question in a thread off the asking message. The server opens that thread while the - * comment is being created and the thread's ID reaches us as `childReportID` on the comment, so wait for it - * rather than guessing an ID. Write requests resolve as soon as they are queued and never carry the server's - * response, so this is the only place the thread ID surfaces. - * - * Gives up after a timeout because the server declines to thread in several cases (a human agent already - * handling the chat, pregenerated replies, the beta being off), and then `childReportID` never arrives. - */ -function followConciergeThreadWhenOpened(reportID: string, reportActionID: string) { - let timeoutID: ReturnType; - - // connectWithoutView because this is an action-layer subscription with no component behind it. - const connectionID = Onyx.connectWithoutView({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - callback: (reportActions) => { - const childReportID = reportActions?.[reportActionID]?.childReportID; - if (!childReportID) { - return; - } - clearTimeout(timeoutID); - Onyx.disconnect(connectionID); - - // The user may have moved on while the thread was being opened; only follow the reply if they - // are still sitting in the chat they asked from. - if (Navigation.getTopmostReportId() !== reportID) { - return; - } - if (isSearchTopmostFullScreenRoute()) { - Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: childReportID, backTo: Navigation.getActiveRoute()})); - } else { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID, undefined, undefined, Navigation.getActiveRoute())); - } - }, - }); - timeoutID = setTimeout(() => Onyx.disconnect(connectionID), CONCIERGE_THREAD_NAVIGATION_TIMEOUT_MS); -} - /** * Add up to two report actions to a report. This method can be called for the following situations: * @@ -911,6 +872,7 @@ function addActions({ reportActionID, delegateAccountID, conciergeReportID, + conciergeThreadReportID, }: AddActionsParams) { if (!report?.reportID) { return; @@ -1114,7 +1076,7 @@ function addActions({ successReportActions[actionKey] = {pendingAction: null, isOptimisticAction: null}; } - const successData: Array> = [ + const successData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, @@ -1179,16 +1141,65 @@ function addActions({ DateUtils.setTimezoneUpdated(); } + // Concierge answers each question in a thread off it, so build that thread here and hand its ID to the server. + if (conciergeThreadReportID && resolvedReportActionID) { + parameters.conciergeThreadReportID = conciergeThreadReportID; + + const optimisticThread = buildOptimisticChatReport({ + participantList: [currentUserAccountID, CONST.ACCOUNT_ID.CONCIERGE], + reportName: reportCommentText, + parentReportActionID: resolvedReportActionID, + parentReportID: reportID, + optimisticReportID: conciergeThreadReportID, + currentUserAccountID, + }); + + optimisticData.push( + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${conciergeThreadReportID}`, + value: {...optimisticThread, pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: {[resolvedReportActionID]: {childReportID: conciergeThreadReportID, childType: CONST.REPORT.TYPE.CHAT}} as ReportActions, + }, + ); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${conciergeThreadReportID}`, + value: {pendingFields: {createChat: null}, errorFields: {createChatThread: null}}, + }); + failureData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${conciergeThreadReportID}`, + value: { + pendingFields: {createChat: null}, + errorFields: {createChatThread: getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage')}, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: {[resolvedReportActionID]: {childReportID: undefined, childType: ''}} as ReportActions, + }, + ); + } + API.write(commandName, parameters, { optimisticData, successData, failureData, }); - // Concierge answers each question in its own thread, opened server-side while the comment is created. - // Not from the side panel, which renders its own report and would navigate a surface the user isn't looking at. - if (isConciergeChat && !isInSidePanel && resolvedReportActionID) { - followConciergeThreadWhenOpened(reportID, resolvedReportActionID); + if (conciergeThreadReportID && resolvedReportActionID) { + if (isSearchTopmostFullScreenRoute()) { + Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: conciergeThreadReportID, backTo: Navigation.getActiveRoute()})); + } else { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(conciergeThreadReportID, undefined, undefined, Navigation.getActiveRoute())); + } } notifyNewAction(resolvedNotifyReportID, lastAction, lastAction?.actorAccountID === currentUserAccountID); } @@ -1277,6 +1288,7 @@ function addComment({ reportActionID, delegateAccountID, conciergeReportID, + conciergeThreadReportID, }: AddCommentParams) { if (shouldPlaySound) { playSound(SOUNDS.DONE); @@ -1294,6 +1306,7 @@ function addComment({ delegateAccountID, sidePanelContext, conciergeReportID, + conciergeThreadReportID, }); } diff --git a/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts b/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts index 531d6badfe90..8e23a8d4f87f 100644 --- a/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts +++ b/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts @@ -3,6 +3,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useDelegateAccountID from '@hooks/useDelegateAccountID'; import useIsInSidePanel from '@hooks/useIsInSidePanel'; import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; import useReportIsArchived from '@hooks/useReportIsArchived'; import {addAttachmentWithComment, addComment, clearAgentZeroProcessingIndicator} from '@libs/actions/Report'; @@ -12,7 +13,7 @@ import {rand64} from '@libs/NumberUtils'; import {addDomainToShortMention} from '@libs/ParsingUtils'; import {getAllPersonalDetailLogins, getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; import {getAllReportActions} from '@libs/ReportActionsUtils'; -import {canUserPerformWriteAction, isConciergeChatReport} from '@libs/ReportUtils'; +import {canUserPerformWriteAction, generateReportID, isConciergeChatReport} from '@libs/ReportUtils'; import {startSpan} from '@libs/telemetry/activeSpans'; import getSendMessageListWeight from '@libs/telemetry/getSendMessageListWeight'; import getSendMessageSource from '@libs/telemetry/getSendMessageSource'; @@ -42,6 +43,7 @@ function useComposerSubmit(reportID: string) { const route = useRoute(); const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); + const {isBetaEnabled} = usePermissions(); const [isComposerFullSize = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`); const delegateAccountID = useDelegateAccountID(); @@ -187,6 +189,10 @@ function useComposerSubmit(reportID: string) { reportActionID: optimisticReportActionID, delegateAccountID, conciergeReportID, + + // Concierge answers each question in its own thread. The side panel renders its own pinned report, + // so it stays in the DM rather than being sent to a thread it cannot show. + conciergeThreadReportID: reportID === conciergeReportID && !isInSidePanel && isBetaEnabled(CONST.BETAS.CONCIERGE_RESPOND_IN_THREAD) ? generateReportID() : undefined, }); }; From a7e9c32d7587993bef035e6ee8477072602e958f Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Wed, 5 Aug 2026 20:49:32 +0300 Subject: [PATCH 03/11] Fix lint --- src/libs/actions/Report/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 4ea0ade01bcb..058ab597f74c 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -1163,7 +1163,7 @@ function addActions({ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: {[resolvedReportActionID]: {childReportID: conciergeThreadReportID, childType: CONST.REPORT.TYPE.CHAT}} as ReportActions, + value: {[resolvedReportActionID]: {childReportID: conciergeThreadReportID, childType: CONST.REPORT.TYPE.CHAT}}, }, ); successData.push({ @@ -1183,7 +1183,7 @@ function addActions({ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: {[resolvedReportActionID]: {childReportID: undefined, childType: ''}} as ReportActions, + value: {[resolvedReportActionID]: {childReportID: null, childType: ''}}, }, ); } From df105cb658272c1c5d7ad7612c2999e07ab48b6c Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 7 Aug 2026 21:06:46 +0300 Subject: [PATCH 04/11] Optimistic thread metadata --- .../AddCommentOrAttachmentParams.ts | 1 + src/libs/actions/Report/index.ts | 51 ++++++++++++++++--- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/libs/API/parameters/AddCommentOrAttachmentParams.ts b/src/libs/API/parameters/AddCommentOrAttachmentParams.ts index 45e76283fee1..86ad7fdd9c59 100644 --- a/src/libs/API/parameters/AddCommentOrAttachmentParams.ts +++ b/src/libs/API/parameters/AddCommentOrAttachmentParams.ts @@ -17,6 +17,7 @@ type AddCommentOrAttachmentParams = { pregeneratedResponse?: string; sidePanelContext?: string; conciergeThreadReportID?: string; + conciergeThreadCreatedReportActionID?: string; }; export default AddCommentOrAttachmentParams; diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index ba6537a197d1..739387ca3723 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -1041,7 +1041,13 @@ function addActions({ } const optimisticData: Array< - OnyxUpdate + OnyxUpdate< + | typeof ONYXKEYS.COLLECTION.REPORT + | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS + | typeof ONYXKEYS.COLLECTION.REPORT_METADATA + | typeof ONYXKEYS.PERSONAL_DETAILS_LIST + | typeof ONYXKEYS.COLLECTION.SNAPSHOT + > > = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -1074,7 +1080,7 @@ function addActions({ successReportActions[actionKey] = {pendingAction: null, isOptimisticAction: null}; } - const successData: Array> = [ + const successData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, @@ -1114,7 +1120,7 @@ function addActions({ failureReportActions[pregeneratedResponseParams.optimisticConciergeReportActionID] = null; } - const failureData: Array> = [ + const failureData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, @@ -1151,6 +1157,8 @@ function addActions({ optimisticReportID: conciergeThreadReportID, currentUserAccountID, }); + const optimisticThreadCreatedAction = buildOptimisticCreatedReportAction({emailCreatingAction: CONST.REPORT.OWNER_EMAIL_FAKE, currentUserAccountID}); + parameters.conciergeThreadCreatedReportActionID = optimisticThreadCreatedAction.reportActionID; optimisticData.push( { @@ -1158,17 +1166,39 @@ function addActions({ key: `${ONYXKEYS.COLLECTION.REPORT}${conciergeThreadReportID}`, value: {...optimisticThread, pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}}, }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${conciergeThreadReportID}`, + value: {[optimisticThreadCreatedAction.reportActionID]: optimisticThreadCreatedAction}, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${conciergeThreadReportID}`, + value: {isOptimisticReport: true}, + }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: {[resolvedReportActionID]: {childReportID: conciergeThreadReportID, childType: CONST.REPORT.TYPE.CHAT}}, }, ); - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${conciergeThreadReportID}`, - value: {pendingFields: {createChat: null}, errorFields: {createChatThread: null}}, - }); + successData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${conciergeThreadReportID}`, + value: {pendingFields: {createChat: null}, errorFields: {createChatThread: null}}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${conciergeThreadReportID}`, + value: {[optimisticThreadCreatedAction.reportActionID]: {pendingAction: null}}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${conciergeThreadReportID}`, + value: {isOptimisticReport: false}, + }, + ); failureData.push( { onyxMethod: Onyx.METHOD.MERGE, @@ -1178,6 +1208,11 @@ function addActions({ errorFields: {createChatThread: getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage')}, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${conciergeThreadReportID}`, + value: {[optimisticThreadCreatedAction.reportActionID]: {pendingAction: null}}, + }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, From add805687efe52c2ccd791d154cf33ff0b271e80 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 7 Aug 2026 22:56:46 +0300 Subject: [PATCH 05/11] Thread in chat snapshot --- src/libs/actions/Report/index.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 739387ca3723..42de05f0d756 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -1065,12 +1065,6 @@ function addActions({ const snapshotDataToStore: NullishDeep = {}; snapshotDataToStore[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] = optimisticReport; snapshotDataToStore[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] = optimisticReportActions; - const optimisticSnapshotUpdate = buildOptimisticSnapshotData(CONST.SEARCH.DATA_TYPES.CHAT, snapshotDataToStore); - - // We are pushing the optimistic report and report actions into the chat snapshot so that the newly sent message appears immediately in "Reports > Chats" while offline. - if (optimisticSnapshotUpdate) { - optimisticData.push(optimisticSnapshotUpdate); - } optimisticData.push(...getOptimisticDataForAncestors(ancestors, currentTime, CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)); @@ -1182,6 +1176,11 @@ function addActions({ value: {[resolvedReportActionID]: {childReportID: conciergeThreadReportID, childType: CONST.REPORT.TYPE.CHAT}}, }, ); + snapshotDataToStore[`${ONYXKEYS.COLLECTION.REPORT}${conciergeThreadReportID}`] = optimisticThread; + snapshotDataToStore[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] = { + ...optimisticReportActions, + [resolvedReportActionID]: {...optimisticReportActions[resolvedReportActionID], childReportID: conciergeThreadReportID, childType: CONST.REPORT.TYPE.CHAT}, + }; successData.push( { onyxMethod: Onyx.METHOD.MERGE, @@ -1221,6 +1220,11 @@ function addActions({ ); } + const optimisticSnapshotUpdate = buildOptimisticSnapshotData(CONST.SEARCH.DATA_TYPES.CHAT, snapshotDataToStore); + if (optimisticSnapshotUpdate) { + optimisticData.push(optimisticSnapshotUpdate); + } + API.write(commandName, parameters, { optimisticData, successData, From 40eb520c94163401697d9a673fe9352b51b048fc Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 7 Aug 2026 23:19:54 +0300 Subject: [PATCH 06/11] Reuse route helper --- src/libs/actions/Report/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 42de05f0d756..3b0cbeeddf79 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -1232,11 +1232,7 @@ function addActions({ }); if (conciergeThreadReportID && resolvedReportActionID) { - if (isSearchTopmostFullScreenRoute()) { - Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: conciergeThreadReportID, backTo: Navigation.getActiveRoute()})); - } else { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(conciergeThreadReportID, undefined, undefined, Navigation.getActiveRoute())); - } + Navigation.navigate(getReportRouteForCurrentContext({reportID: conciergeThreadReportID})); } notifyNewAction(resolvedNotifyReportID, lastAction, lastAction?.actorAccountID === currentUserAccountID); } From 1e9dcc74185241a7003685582cdb0bb5a114c944 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Sun, 9 Aug 2026 05:56:41 +0300 Subject: [PATCH 07/11] Show thread indicator --- src/libs/actions/Report/index.ts | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 5032b9a1e151..12cf60b63395 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -1157,6 +1157,17 @@ function addActions({ const optimisticThreadCreatedAction = buildOptimisticCreatedReportAction({emailCreatingAction: CONST.REPORT.OWNER_EMAIL_FAKE, currentUserAccountID}); parameters.conciergeThreadCreatedReportActionID = optimisticThreadCreatedAction.reportActionID; + const optimisticThreadDetails = { + childReportID: conciergeThreadReportID, + childType: CONST.REPORT.TYPE.CHAT, + + // Concierge starts thinking in the thread right away, so count that as its first reply. + childVisibleActionCount: 1, + childCommenterCount: 1, + childOldestFourAccountIDs: String(CONST.ACCOUNT_ID.CONCIERGE), + childLastVisibleActionCreated: currentTime, + }; + optimisticData.push( { onyxMethod: Onyx.METHOD.SET, @@ -1176,13 +1187,13 @@ function addActions({ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: {[resolvedReportActionID]: {childReportID: conciergeThreadReportID, childType: CONST.REPORT.TYPE.CHAT}}, + value: {[resolvedReportActionID]: optimisticThreadDetails}, }, ); snapshotDataToStore[`${ONYXKEYS.COLLECTION.REPORT}${conciergeThreadReportID}`] = optimisticThread; snapshotDataToStore[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] = { ...optimisticReportActions, - [resolvedReportActionID]: {...optimisticReportActions[resolvedReportActionID], childReportID: conciergeThreadReportID, childType: CONST.REPORT.TYPE.CHAT}, + [resolvedReportActionID]: {...optimisticReportActions[resolvedReportActionID], ...optimisticThreadDetails}, }; successData.push( { @@ -1218,7 +1229,16 @@ function addActions({ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: {[resolvedReportActionID]: {childReportID: null, childType: ''}}, + value: { + [resolvedReportActionID]: { + childReportID: null, + childType: '', + childVisibleActionCount: 0, + childCommenterCount: 0, + childOldestFourAccountIDs: '', + childLastVisibleActionCreated: '', + }, + }, }, ); } From f4d914efb165fa8ce476a249bab2688c9254c73d Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Tue, 11 Aug 2026 03:57:02 +0300 Subject: [PATCH 08/11] fix thread avatar --- src/libs/actions/Report/index.ts | 18 +++---- tests/actions/ConciergeThreadTest.ts | 71 ++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 tests/actions/ConciergeThreadTest.ts diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 53adaf52e150..5e55581684ba 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -1154,6 +1154,7 @@ function addActions({ } // Concierge answers each question in a thread off it, so build that thread here and hand its ID to the server. + let conciergeThreadOnyxData: Array> = []; if (conciergeThreadReportID && resolvedReportActionID) { parameters.conciergeThreadReportID = conciergeThreadReportID; @@ -1179,7 +1180,9 @@ function addActions({ childLastVisibleActionCreated: currentTime, }; - optimisticData.push( + const optimisticParentReportAction = {...optimisticReportActions[resolvedReportActionID], ...optimisticThreadDetails}; + + conciergeThreadOnyxData = [ { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${conciergeThreadReportID}`, @@ -1198,14 +1201,12 @@ function addActions({ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: {[resolvedReportActionID]: optimisticThreadDetails}, + value: {[resolvedReportActionID]: optimisticParentReportAction}, }, - ); + ]; + optimisticData.push(...conciergeThreadOnyxData); snapshotDataToStore[`${ONYXKEYS.COLLECTION.REPORT}${conciergeThreadReportID}`] = optimisticThread; - snapshotDataToStore[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] = { - ...optimisticReportActions, - [resolvedReportActionID]: {...optimisticReportActions[resolvedReportActionID], ...optimisticThreadDetails}, - }; + snapshotDataToStore[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] = {...optimisticReportActions, [resolvedReportActionID]: optimisticParentReportAction}; successData.push( { onyxMethod: Onyx.METHOD.MERGE, @@ -1266,7 +1267,8 @@ function addActions({ }); if (conciergeThreadReportID && resolvedReportActionID) { - Navigation.navigate(getReportRouteForCurrentContext({reportID: conciergeThreadReportID})); + // The thread header takes its avatar from the question it hangs off, so open the thread once Onyx has stored both. + Onyx.update(conciergeThreadOnyxData).then(() => Navigation.navigate(getReportRouteForCurrentContext({reportID: conciergeThreadReportID}))); } notifyNewAction(resolvedNotifyReportID, lastAction, lastAction?.actorAccountID === currentUserAccountID); } diff --git a/tests/actions/ConciergeThreadTest.ts b/tests/actions/ConciergeThreadTest.ts new file mode 100644 index 000000000000..b36a72754d95 --- /dev/null +++ b/tests/actions/ConciergeThreadTest.ts @@ -0,0 +1,71 @@ +import {beforeEach, describe, expect, it, jest} from '@jest/globals'; + +import Navigation from '@libs/Navigation/Navigation'; +import {getAllReportActions} from '@libs/ReportActionsUtils'; + +import CONST from '@src/CONST'; +import * as Report from '@src/libs/actions/Report'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; + +import Onyx from 'react-native-onyx'; + +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +jest.mock('@libs/Navigation/Navigation', () => ({ + navigate: jest.fn(), + getActiveRoute: jest.fn(() => ''), +})); + +const USER_ACCOUNT_ID = 1; +const USER_EMAIL = 'user@test.com'; +const CONCIERGE_DM_ID = '1'; +const THREAD_ID = '2'; + +describe('Concierge thread', () => { + beforeEach(() => { + jest.clearAllMocks(); + Onyx.clear(); + return waitForBatchedUpdates(); + }); + + it('opens the thread only once the question it hangs off is in Onyx', async () => { + await TestHelper.signInWithTestUser(USER_ACCOUNT_ID, USER_EMAIL); + const conciergeDM: OnyxTypes.Report = { + reportID: CONCIERGE_DM_ID, + type: CONST.REPORT.TYPE.CHAT, + participants: { + [USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + [CONST.ACCOUNT_ID.CONCIERGE]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, + }, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${CONCIERGE_DM_ID}`, conciergeDM); + await Onyx.merge(ONYXKEYS.CONCIERGE_REPORT_ID, CONCIERGE_DM_ID); + await waitForBatchedUpdates(); + + // The thread header reads the question to build its avatar, so record what Onyx holds when the thread opens. + let questionWhenThreadOpened: OnyxTypes.ReportAction | undefined; + (Navigation.navigate as jest.Mock).mockImplementation(() => { + const threadReportID = Object.values(getAllReportActions(CONCIERGE_DM_ID)).at(0)?.childReportID; + questionWhenThreadOpened = Object.values(getAllReportActions(CONCIERGE_DM_ID)).find((action) => action.childReportID === threadReportID); + }); + + Report.addComment({ + report: conciergeDM, + notifyReportID: CONCIERGE_DM_ID, + ancestors: [], + text: 'How do I submit an expense?', + timezoneParam: CONST.DEFAULT_TIME_ZONE, + currentUserAccountID: USER_ACCOUNT_ID, + delegateAccountID: undefined, + conciergeReportID: CONCIERGE_DM_ID, + conciergeThreadReportID: THREAD_ID, + }); + await waitForBatchedUpdates(); + + expect(Navigation.navigate).toHaveBeenCalledTimes(1); + expect(questionWhenThreadOpened?.actorAccountID).toBe(USER_ACCOUNT_ID); + expect(questionWhenThreadOpened?.childReportID).toBe(THREAD_ID); + }); +}); From 516026eff5b22689d19394d16c44b9ba9c3f02bc Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Tue, 11 Aug 2026 03:59:45 +0300 Subject: [PATCH 09/11] rename test wording --- src/libs/actions/Report/index.ts | 1 - tests/actions/ConciergeThreadTest.ts | 13 ++++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 5e55581684ba..5ec24440cedf 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -1267,7 +1267,6 @@ function addActions({ }); if (conciergeThreadReportID && resolvedReportActionID) { - // The thread header takes its avatar from the question it hangs off, so open the thread once Onyx has stored both. Onyx.update(conciergeThreadOnyxData).then(() => Navigation.navigate(getReportRouteForCurrentContext({reportID: conciergeThreadReportID}))); } notifyNewAction(resolvedNotifyReportID, lastAction, lastAction?.actorAccountID === currentUserAccountID); diff --git a/tests/actions/ConciergeThreadTest.ts b/tests/actions/ConciergeThreadTest.ts index b36a72754d95..02073f0c375b 100644 --- a/tests/actions/ConciergeThreadTest.ts +++ b/tests/actions/ConciergeThreadTest.ts @@ -30,7 +30,7 @@ describe('Concierge thread', () => { return waitForBatchedUpdates(); }); - it('opens the thread only once the question it hangs off is in Onyx', async () => { + it('opens the thread only once its parent report action is in Onyx', async () => { await TestHelper.signInWithTestUser(USER_ACCOUNT_ID, USER_EMAIL); const conciergeDM: OnyxTypes.Report = { reportID: CONCIERGE_DM_ID, @@ -44,11 +44,10 @@ describe('Concierge thread', () => { await Onyx.merge(ONYXKEYS.CONCIERGE_REPORT_ID, CONCIERGE_DM_ID); await waitForBatchedUpdates(); - // The thread header reads the question to build its avatar, so record what Onyx holds when the thread opens. - let questionWhenThreadOpened: OnyxTypes.ReportAction | undefined; + // The thread header builds its avatar from the parent report action, so record what Onyx holds when the thread opens. + let parentReportActionWhenThreadOpened: OnyxTypes.ReportAction | undefined; (Navigation.navigate as jest.Mock).mockImplementation(() => { - const threadReportID = Object.values(getAllReportActions(CONCIERGE_DM_ID)).at(0)?.childReportID; - questionWhenThreadOpened = Object.values(getAllReportActions(CONCIERGE_DM_ID)).find((action) => action.childReportID === threadReportID); + parentReportActionWhenThreadOpened = Object.values(getAllReportActions(CONCIERGE_DM_ID)).find((action) => action.childReportID === THREAD_ID); }); Report.addComment({ @@ -65,7 +64,7 @@ describe('Concierge thread', () => { await waitForBatchedUpdates(); expect(Navigation.navigate).toHaveBeenCalledTimes(1); - expect(questionWhenThreadOpened?.actorAccountID).toBe(USER_ACCOUNT_ID); - expect(questionWhenThreadOpened?.childReportID).toBe(THREAD_ID); + expect(parentReportActionWhenThreadOpened?.actorAccountID).toBe(USER_ACCOUNT_ID); + expect(parentReportActionWhenThreadOpened?.childReportID).toBe(THREAD_ID); }); }); From 4dc44eab150e78503ebac527098f2a4a26cd04db Mon Sep 17 00:00:00 2001 From: "Rushat Gabhane (via MelvinBot)" Date: Tue, 11 Aug 2026 10:04:57 +0000 Subject: [PATCH 10/11] Fix ESLint no-unsafe-type-assertion error in ConciergeThreadTest Co-authored-by: Rushat Gabhane --- tests/actions/ConciergeThreadTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/ConciergeThreadTest.ts b/tests/actions/ConciergeThreadTest.ts index 02073f0c375b..173ed5f5853c 100644 --- a/tests/actions/ConciergeThreadTest.ts +++ b/tests/actions/ConciergeThreadTest.ts @@ -46,7 +46,7 @@ describe('Concierge thread', () => { // The thread header builds its avatar from the parent report action, so record what Onyx holds when the thread opens. let parentReportActionWhenThreadOpened: OnyxTypes.ReportAction | undefined; - (Navigation.navigate as jest.Mock).mockImplementation(() => { + jest.mocked(Navigation.navigate).mockImplementation(() => { parentReportActionWhenThreadOpened = Object.values(getAllReportActions(CONCIERGE_DM_ID)).find((action) => action.childReportID === THREAD_ID); }); From a7ec8a88beca584f6f95e0e605671c7ea7d187d9 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Tue, 11 Aug 2026 21:34:06 +0530 Subject: [PATCH 11/11] Delete tests/actions/ConciergeThreadTest.ts --- tests/actions/ConciergeThreadTest.ts | 70 ---------------------------- 1 file changed, 70 deletions(-) delete mode 100644 tests/actions/ConciergeThreadTest.ts diff --git a/tests/actions/ConciergeThreadTest.ts b/tests/actions/ConciergeThreadTest.ts deleted file mode 100644 index 173ed5f5853c..000000000000 --- a/tests/actions/ConciergeThreadTest.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {beforeEach, describe, expect, it, jest} from '@jest/globals'; - -import Navigation from '@libs/Navigation/Navigation'; -import {getAllReportActions} from '@libs/ReportActionsUtils'; - -import CONST from '@src/CONST'; -import * as Report from '@src/libs/actions/Report'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type * as OnyxTypes from '@src/types/onyx'; - -import Onyx from 'react-native-onyx'; - -import * as TestHelper from '../utils/TestHelper'; -import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; - -jest.mock('@libs/Navigation/Navigation', () => ({ - navigate: jest.fn(), - getActiveRoute: jest.fn(() => ''), -})); - -const USER_ACCOUNT_ID = 1; -const USER_EMAIL = 'user@test.com'; -const CONCIERGE_DM_ID = '1'; -const THREAD_ID = '2'; - -describe('Concierge thread', () => { - beforeEach(() => { - jest.clearAllMocks(); - Onyx.clear(); - return waitForBatchedUpdates(); - }); - - it('opens the thread only once its parent report action is in Onyx', async () => { - await TestHelper.signInWithTestUser(USER_ACCOUNT_ID, USER_EMAIL); - const conciergeDM: OnyxTypes.Report = { - reportID: CONCIERGE_DM_ID, - type: CONST.REPORT.TYPE.CHAT, - participants: { - [USER_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - [CONST.ACCOUNT_ID.CONCIERGE]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS}, - }, - }; - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${CONCIERGE_DM_ID}`, conciergeDM); - await Onyx.merge(ONYXKEYS.CONCIERGE_REPORT_ID, CONCIERGE_DM_ID); - await waitForBatchedUpdates(); - - // The thread header builds its avatar from the parent report action, so record what Onyx holds when the thread opens. - let parentReportActionWhenThreadOpened: OnyxTypes.ReportAction | undefined; - jest.mocked(Navigation.navigate).mockImplementation(() => { - parentReportActionWhenThreadOpened = Object.values(getAllReportActions(CONCIERGE_DM_ID)).find((action) => action.childReportID === THREAD_ID); - }); - - Report.addComment({ - report: conciergeDM, - notifyReportID: CONCIERGE_DM_ID, - ancestors: [], - text: 'How do I submit an expense?', - timezoneParam: CONST.DEFAULT_TIME_ZONE, - currentUserAccountID: USER_ACCOUNT_ID, - delegateAccountID: undefined, - conciergeReportID: CONCIERGE_DM_ID, - conciergeThreadReportID: THREAD_ID, - }); - await waitForBatchedUpdates(); - - expect(Navigation.navigate).toHaveBeenCalledTimes(1); - expect(parentReportActionWhenThreadOpened?.actorAccountID).toBe(USER_ACCOUNT_ID); - expect(parentReportActionWhenThreadOpened?.childReportID).toBe(THREAD_ID); - }); -});