From 1d98d64c0a61678b0a1f9c1428454b4a2c3b26c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Thu, 29 Jan 2026 10:28:15 -0800 Subject: [PATCH 01/11] pregenerated responses POC --- .../AddCommentOrAttachmentParams.ts | 2 + src/libs/ReportActionFollowupUtils/index.ts | 18 ++- src/libs/ReportActionsUtils.ts | 5 +- src/libs/actions/Report/SuggestedFollowup.ts | 58 +++++++++- src/libs/actions/Report/index.ts | 104 +++++++++++++++++- .../inbox/report/PureReportActionItem.tsx | 2 +- tests/actions/ReportTest.ts | 49 ++++++++- tests/unit/ReportActionsFollowupUtilsTest.ts | 38 +++++++ 8 files changed, 257 insertions(+), 19 deletions(-) diff --git a/src/libs/API/parameters/AddCommentOrAttachmentParams.ts b/src/libs/API/parameters/AddCommentOrAttachmentParams.ts index 6f87172def2d..de70f600204a 100644 --- a/src/libs/API/parameters/AddCommentOrAttachmentParams.ts +++ b/src/libs/API/parameters/AddCommentOrAttachmentParams.ts @@ -11,6 +11,8 @@ type AddCommentOrAttachmentParams = { isOldDotConciergeChat?: boolean; idempotencyKey?: string; pageHTML?: string; + optimisticConciergeReportActionID?: string; + pregeneratedResponse?: string; }; export default AddCommentOrAttachmentParams; diff --git a/src/libs/ReportActionFollowupUtils/index.ts b/src/libs/ReportActionFollowupUtils/index.ts index d1735bae5155..70ad5fa84e42 100644 --- a/src/libs/ReportActionFollowupUtils/index.ts +++ b/src/libs/ReportActionFollowupUtils/index.ts @@ -1,9 +1,14 @@ import {DomUtils, parseDocument} from 'htmlparser2'; -import type {Followup} from '@libs/ReportActionsUtils'; import {getReportActionMessage, isActionOfType} from '@libs/ReportActionsUtils'; import CONST from '@src/CONST'; import type {OnyxInputOrEntry, ReportAction} from '@src/types/onyx'; +type Followup = { + text: string; + response?: string; +}; + + /** * Checks if a report action contains actionable (unresolved) followup suggestions. * @param reportAction - The report action to check @@ -44,7 +49,14 @@ function parseFollowupsFromHtml(html: string): Followup[] | null { return []; } - const followupTextElements = DomUtils.getElementsByTagName('followup-text', followupList, true); - return followupTextElements.map((el) => ({text: DomUtils.textContent(el)})); + const followupElements = DomUtils.getElementsByTagName('followup', followupList, true); + return followupElements.map((followupEl) => { + const followupTextElement = DomUtils.getElementsByTagName('followup-text', followupEl, true).at(0); + const followupResponseElement = DomUtils.getElementsByTagName('followup-response', followupEl, true).at(0); + const text = followupTextElement ? DomUtils.textContent(followupTextElement) : ''; + const response = followupResponseElement ? DomUtils.textContent(followupResponseElement) : undefined; + return {text, response}; + }); } export {containsActionableFollowUps, parseFollowupsFromHtml}; +export type {Followup}; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index a2ff666a743e..7054040a8e14 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -68,9 +68,6 @@ type MemberChangeMessageRoomReferenceElement = { type MemberChangeMessageElement = MessageTextElement | MemberChangeMessageUserMentionElement | MemberChangeMessageRoomReferenceElement; -type Followup = { - text: string; -}; function isPolicyExpenseChat(report: OnyxInputOrEntry): boolean { return report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT || !!(report && typeof report === 'object' && 'isPolicyExpenseChat' in report && report.isPolicyExpenseChat); @@ -4005,4 +4002,4 @@ export { stripFollowupListFromHtml, }; -export type {LastVisibleMessage, Followup}; +export type {LastVisibleMessage}; diff --git a/src/libs/actions/Report/SuggestedFollowup.ts b/src/libs/actions/Report/SuggestedFollowup.ts index a71c8c9ccd64..5095245620c8 100644 --- a/src/libs/actions/Report/SuggestedFollowup.ts +++ b/src/libs/actions/Report/SuggestedFollowup.ts @@ -1,18 +1,27 @@ import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import {rand64} from '@libs/NumberUtils'; +import type {Followup} from '@libs/ReportActionFollowupUtils'; import type {Ancestor} from '@libs/ReportUtils'; +import {buildOptimisticAddCommentReportAction} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report, ReportAction} from '@src/types/onyx'; +import type {Report, ReportAction, ReportActions} from '@src/types/onyx'; import type {Timezone} from '@src/types/onyx/PersonalDetails'; import {addComment, buildOptimisticResolvedFollowups} from '.'; +/** Delay before showing pre-generated Concierge response (in milliseconds) */ +const CONCIERGE_RESPONSE_DELAY_MS = 500; + /** * Resolves a suggested followup by posting the selected question as a comment * and optimistically updating the HTML to mark the followup-list as resolved. + * If the followup has a pre-generated response, it will show a "Concierge is typing" + * indicator briefly before displaying the response. * @param report - The report where the action exists * @param notifyReportID - The report ID to notify for new actions * @param reportAction - The report action containing the followup-list - * @param selectedFollowup - The followup question selected by the user + * @param followup - The followup object containing the question text and optional pre-generated response * @param timezoneParam - The user's timezone * @param ancestors - Array of ancestor reports for proper threading */ @@ -20,7 +29,7 @@ function resolveSuggestedFollowup( report: OnyxEntry, notifyReportID: string | undefined, reportAction: OnyxEntry, - selectedFollowup: string, + followup: Followup, timezoneParam: Timezone, ancestors: Ancestor[] = [], ) { @@ -42,8 +51,47 @@ function resolveSuggestedFollowup( [reportActionID]: resolvedAction, }); - // Post the selected followup question as a comment - addComment(report, notifyReportID ?? reportID, ancestors, selectedFollowup, timezoneParam); + // If there's a pre-generated response, show typing indicator then display response after delay + if (followup.response) { + // Generate optimistic Concierge response action ID + const optimisticConciergeReportActionID = rand64(); + + // Post user's comment immediately (API call includes pregenerated params for backend reconciliation) + addComment(report, notifyReportID ?? reportID, ancestors, followup.text, timezoneParam, false, false, { + optimisticConciergeReportActionID, + pregeneratedResponse: followup.response, + }); + + // Show "Concierge is typing..." indicator + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, { + [CONST.ACCOUNT_ID.CONCIERGE]: true, + }); + + // After a brief delay, clear typing indicator and show the Concierge response + setTimeout(() => { + // Clear the typing indicator + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, { + [CONST.ACCOUNT_ID.CONCIERGE]: false, + }); + + // Create and add the optimistic Concierge response action + const optimisticConciergeAction = buildOptimisticAddCommentReportAction( + followup.response, + undefined, + CONST.ACCOUNT_ID.CONCIERGE, + 0, + reportID, + optimisticConciergeReportActionID, + ); + + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { + [optimisticConciergeReportActionID]: optimisticConciergeAction.reportAction, + } as ReportActions); + }, CONCIERGE_RESPONSE_DELAY_MS); + } else { + // Post the selected followup question as a comment + addComment(report, notifyReportID ?? reportID, ancestors, followup.text, timezoneParam); + } } export default resolveSuggestedFollowup; diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 334d32d1fc2d..4dd37202da26 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -567,6 +567,12 @@ function buildOptimisticResolvedFollowups(reportAction: OnyxEntry) }; } +type PregeneratedResponseParams = { + optimisticConciergeReportActionID: string; + pregeneratedResponse: string; + optimisticConciergeAction: OptimisticAddCommentReportAction; +}; + /** * Add up to two report actions to a report. This method can be called for the following situations: * @@ -577,8 +583,20 @@ function buildOptimisticResolvedFollowups(reportAction: OnyxEntry) * @param report - The report where the comment should be added * @param notifyReportID - The report ID we should notify for new actions. This is usually the same as reportID, except when adding a comment to an expense report with a single transaction thread, in which case we want to notify the parent expense report. * @param isInSidePanel - Whether the comment is being added from the side panel + * @param pregeneratedResponseParams - Optional params for pre-generated Concierge response handling (includes optimistic action) + * @param apiOnlyPregeneratedParams - Optional params for pre-generated response (API only, no optimistic action - used when response display is delayed) */ -function addActions(report: OnyxEntry, notifyReportID: string, ancestors: Ancestor[], timezoneParam: Timezone, text = '', file?: FileObject, isInSidePanel = false) { +function addActions( + report: OnyxEntry, + notifyReportID: string, + ancestors: Ancestor[], + timezoneParam: Timezone, + text = '', + file?: FileObject, + isInSidePanel = false, + pregeneratedResponseParams?: PregeneratedResponseParams, + apiOnlyPregeneratedParams?: ApiOnlyPregeneratedResponseParams, +) { if (!report?.reportID) { return; } @@ -652,6 +670,11 @@ function addActions(report: OnyxEntry, notifyReportID: string, ancestors optimisticReportActions[lastActionReportActionID] = resolvedAction; } + // Add the pre-generated Concierge response to optimistic actions if provided + if (pregeneratedResponseParams) { + optimisticReportActions[pregeneratedResponseParams.optimisticConciergeReportActionID] = pregeneratedResponseParams.optimisticConciergeAction; + } + const parameters: AddCommentOrAttachmentParams = { reportID, reportActionID: file ? attachmentAction?.reportActionID : reportCommentAction?.reportActionID, @@ -673,6 +696,18 @@ function addActions(report: OnyxEntry, notifyReportID: string, ancestors } } + // Add pre-generated response parameters for the backend to reconcile + if (pregeneratedResponseParams) { + parameters.optimisticConciergeReportActionID = pregeneratedResponseParams.optimisticConciergeReportActionID; + parameters.pregeneratedResponse = pregeneratedResponseParams.pregeneratedResponse; + } + + // Add API-only pregenerated params (for delayed response display - optimistic action added separately after delay) + if (apiOnlyPregeneratedParams) { + parameters.optimisticConciergeReportActionID = apiOnlyPregeneratedParams.optimisticConciergeReportActionID; + parameters.pregeneratedResponse = apiOnlyPregeneratedParams.pregeneratedResponse; + } + const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -730,6 +765,11 @@ function addActions(report: OnyxEntry, notifyReportID: string, ancestors failureReportActions[lastActionReportActionID] = lastVisibleAction; } + // In case of error, remove the optimistic Concierge response + if (pregeneratedResponseParams) { + failureReportActions[pregeneratedResponseParams.optimisticConciergeReportActionID] = null as unknown as ReportAction; + } + const failureData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -804,12 +844,67 @@ function addAttachmentWithComment( handlePlaySound(); } +type ApiOnlyPregeneratedResponseParams = { + optimisticConciergeReportActionID: string; + pregeneratedResponse: string; +}; + /** Add a single comment to a report */ -function addComment(report: OnyxEntry, notifyReportID: string, ancestors: Ancestor[], text: string, timezoneParam: Timezone, shouldPlaySound?: boolean, isInSidePanel?: boolean) { +function addComment( + report: OnyxEntry, + notifyReportID: string, + ancestors: Ancestor[], + text: string, + timezoneParam: Timezone, + shouldPlaySound?: boolean, + isInSidePanel?: boolean, + apiOnlyPregeneratedParams?: ApiOnlyPregeneratedResponseParams, +) { if (shouldPlaySound) { playSound(SOUNDS.DONE); } - addActions(report, notifyReportID, ancestors, timezoneParam, text, undefined, isInSidePanel); + addActions(report, notifyReportID, ancestors, timezoneParam, text, undefined, isInSidePanel, undefined, apiOnlyPregeneratedParams); +} + +/** + * Add a comment to a report with a pre-generated Concierge response. + * This is used when the user selects a followup question that has a cached response. + * @param report - The report where the comment should be added + * @param notifyReportID - The report ID to notify for new actions + * @param ancestors - Array of ancestor reports for proper threading + * @param text - The user's question/comment text + * @param timezoneParam - The user's timezone + * @param optimisticConciergeReportActionID - The pre-generated ID for the Concierge response action + * @param pregeneratedResponse - The pre-generated response content from Concierge + */ +function addCommentWithPregeneratedResponse( + report: OnyxEntry, + notifyReportID: string, + ancestors: Ancestor[], + text: string, + timezoneParam: Timezone, + optimisticConciergeReportActionID: string, + pregeneratedResponse: string, +) { + if (!report?.reportID) { + return; + } + + // Create the optimistic Concierge response action + const optimisticConciergeAction = buildOptimisticAddCommentReportAction( + pregeneratedResponse, + undefined, + CONST.ACCOUNT_ID.CONCIERGE, + 1, // offset to ensure it appears after the user's comment + report.reportID, + optimisticConciergeReportActionID, + ); + + addActions(report, notifyReportID, ancestors, timezoneParam, text, undefined, false, { + optimisticConciergeReportActionID, + pregeneratedResponse, + optimisticConciergeAction: optimisticConciergeAction.reportAction, + }); } function reportActionsExist(reportID: string): boolean { @@ -6577,11 +6672,12 @@ function setOptimisticTransactionThread(reportID?: string, parentReportID?: stri }); } -export type {Video, GuidedSetupData, TaskForParameters, IntroSelected}; +export type {Video, GuidedSetupData, TaskForParameters, IntroSelected, ApiOnlyPregeneratedResponseParams}; export { addAttachmentWithComment, addComment, + addCommentWithPregeneratedResponse, addPolicyReport, broadcastUserIsLeavingRoom, broadcastUserIsTyping, diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index d08f37e43d8f..3116dc97c9f0 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -888,7 +888,7 @@ function PureReportActionItem({ shouldUseLocalization: false, key: `${action.reportActionID}-followup-${followup.text}`, onPress: () => { - resolveSuggestedFollowup(reportActionReport, reportID, action, followup.text, personalDetail.timezone ?? CONST.DEFAULT_TIME_ZONE); + resolveSuggestedFollowup(reportActionReport, reportID, action, followup, personalDetail.timezone ?? CONST.DEFAULT_TIME_ZONE); }, })); } diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 342dc18ebf63..59b06c17b074 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -3702,7 +3702,7 @@ describe('actions/Report', () => { }); await waitForBatchedUpdates(); - resolveSuggestedFollowup(report, undefined, reportAction, 'test question', CONST.DEFAULT_TIME_ZONE); + resolveSuggestedFollowup(report, undefined, reportAction, {text: 'test question'}, CONST.DEFAULT_TIME_ZONE); await waitForBatchedUpdates(); // The report action should remain unchanged (no followup-list to resolve) @@ -3730,7 +3730,7 @@ describe('actions/Report', () => { }); await waitForBatchedUpdates(); - resolveSuggestedFollowup(report, undefined, reportAction, 'How do I set up QuickBooks?', CONST.DEFAULT_TIME_ZONE); + resolveSuggestedFollowup(report, undefined, reportAction, {text: 'How do I set up QuickBooks?'}, CONST.DEFAULT_TIME_ZONE); await waitForBatchedUpdates(); // Verify the followup-list was marked as selected @@ -3741,5 +3741,50 @@ describe('actions/Report', () => { // Verify addComment was called (which triggers ADD_COMMENT API call) TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.ADD_COMMENT, 1); }); + + it('should optimistically resolve followups and post comment with pre-generated response when response is provided', async () => { + const reportAction = { + reportActionID: REPORT_ACTION_ID, + actorAccountID: CONST.ACCOUNT_ID.CONCIERGE, + message: [ + { + html: '

Here is help

How do I set up QuickBooks?To set up QuickBooks, go to Settings...', + text: 'Here is help', + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, + }, + ], + } as OnyxTypes.ReportAction; + + // Set up initial Onyx state + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + [REPORT_ACTION_ID]: reportAction, + }); + await waitForBatchedUpdates(); + + resolveSuggestedFollowup( + report, + undefined, + reportAction, + {text: 'How do I set up QuickBooks?', response: 'To set up QuickBooks, go to Settings...'}, + CONST.DEFAULT_TIME_ZONE, + ); + await waitForBatchedUpdates(); + + // Verify the followup-list was marked as selected + const reportActions = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}` as const); + const updatedHtml = (reportActions?.[REPORT_ACTION_ID]?.message as Message[])?.at(0)?.html; + expect(updatedHtml).toContain(''); + + // Verify addComment was called (which triggers ADD_COMMENT API call) + // With pre-generated response, the API call should include the optimistic Concierge response params + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.ADD_COMMENT, 1); + + // Verify an optimistic Concierge report action was created + const allReportActions = Object.values(reportActions ?? {}); + const conciergeActions = allReportActions.filter((action) => action?.actorAccountID === CONST.ACCOUNT_ID.CONCIERGE); + // Should have 2 Concierge actions: the original one and the optimistic response + expect(conciergeActions.length).toBe(2); + }); }); }); diff --git a/tests/unit/ReportActionsFollowupUtilsTest.ts b/tests/unit/ReportActionsFollowupUtilsTest.ts index cc295b0df79a..0f171fbc0140 100644 --- a/tests/unit/ReportActionsFollowupUtilsTest.ts +++ b/tests/unit/ReportActionsFollowupUtilsTest.ts @@ -61,6 +61,44 @@ describe('ReportActionsFollowupUtils', () => { const html = ''; expect(parseFollowupsFromHtml(html)).toEqual([]); }); + + it('should parse followup with pre-generated response', () => { + const html = ` + + How do I set up QuickBooks? + To set up QuickBooks, go to Settings > Integrations... + +`; + expect(parseFollowupsFromHtml(html)).toEqual([ + {text: 'How do I set up QuickBooks?', response: 'To set up QuickBooks, go to Settings > Integrations...'}, + ]); + }); + + it('should parse multiple followups with mixed response availability', () => { + const html = ` + + Question without response + + + Question with response + Here is the cached response + +`; + expect(parseFollowupsFromHtml(html)).toEqual([ + {text: 'Question without response', response: undefined}, + {text: 'Question with response', response: 'Here is the cached response'}, + ]); + }); + + it('should handle empty followup-response element', () => { + const html = ` + + Question + + +`; + expect(parseFollowupsFromHtml(html)).toEqual([{text: 'Question', response: ''}]); + }); }); describe('stripFollowupListFromHtml', () => { From dd4fe909d9540c70d37985fccb0f977543d1a51a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Thu, 29 Jan 2026 10:36:11 -0800 Subject: [PATCH 02/11] prettier fix --- src/libs/ReportActionFollowupUtils/index.ts | 1 - src/libs/ReportActionsUtils.ts | 1 - src/libs/actions/Report/SuggestedFollowup.ts | 9 +-------- tests/actions/ReportTest.ts | 8 +------- tests/unit/ReportActionsFollowupUtilsTest.ts | 4 +--- 5 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/libs/ReportActionFollowupUtils/index.ts b/src/libs/ReportActionFollowupUtils/index.ts index 70ad5fa84e42..cc6e9ab99199 100644 --- a/src/libs/ReportActionFollowupUtils/index.ts +++ b/src/libs/ReportActionFollowupUtils/index.ts @@ -8,7 +8,6 @@ type Followup = { response?: string; }; - /** * Checks if a report action contains actionable (unresolved) followup suggestions. * @param reportAction - The report action to check diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 7054040a8e14..58f667c239c4 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -68,7 +68,6 @@ type MemberChangeMessageRoomReferenceElement = { type MemberChangeMessageElement = MessageTextElement | MemberChangeMessageUserMentionElement | MemberChangeMessageRoomReferenceElement; - function isPolicyExpenseChat(report: OnyxInputOrEntry): boolean { return report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT || !!(report && typeof report === 'object' && 'isPolicyExpenseChat' in report && report.isPolicyExpenseChat); } diff --git a/src/libs/actions/Report/SuggestedFollowup.ts b/src/libs/actions/Report/SuggestedFollowup.ts index 5095245620c8..2a7fb2ac1a02 100644 --- a/src/libs/actions/Report/SuggestedFollowup.ts +++ b/src/libs/actions/Report/SuggestedFollowup.ts @@ -75,14 +75,7 @@ function resolveSuggestedFollowup( }); // Create and add the optimistic Concierge response action - const optimisticConciergeAction = buildOptimisticAddCommentReportAction( - followup.response, - undefined, - CONST.ACCOUNT_ID.CONCIERGE, - 0, - reportID, - optimisticConciergeReportActionID, - ); + const optimisticConciergeAction = buildOptimisticAddCommentReportAction(followup.response, undefined, CONST.ACCOUNT_ID.CONCIERGE, 0, reportID, optimisticConciergeReportActionID); Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { [optimisticConciergeReportActionID]: optimisticConciergeAction.reportAction, diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 59b06c17b074..7b5ad45d3dd0 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -3762,13 +3762,7 @@ describe('actions/Report', () => { }); await waitForBatchedUpdates(); - resolveSuggestedFollowup( - report, - undefined, - reportAction, - {text: 'How do I set up QuickBooks?', response: 'To set up QuickBooks, go to Settings...'}, - CONST.DEFAULT_TIME_ZONE, - ); + resolveSuggestedFollowup(report, undefined, reportAction, {text: 'How do I set up QuickBooks?', response: 'To set up QuickBooks, go to Settings...'}, CONST.DEFAULT_TIME_ZONE); await waitForBatchedUpdates(); // Verify the followup-list was marked as selected diff --git a/tests/unit/ReportActionsFollowupUtilsTest.ts b/tests/unit/ReportActionsFollowupUtilsTest.ts index 0f171fbc0140..02af520d248b 100644 --- a/tests/unit/ReportActionsFollowupUtilsTest.ts +++ b/tests/unit/ReportActionsFollowupUtilsTest.ts @@ -69,9 +69,7 @@ describe('ReportActionsFollowupUtils', () => { To set up QuickBooks, go to Settings > Integrations... `; - expect(parseFollowupsFromHtml(html)).toEqual([ - {text: 'How do I set up QuickBooks?', response: 'To set up QuickBooks, go to Settings > Integrations...'}, - ]); + expect(parseFollowupsFromHtml(html)).toEqual([{text: 'How do I set up QuickBooks?', response: 'To set up QuickBooks, go to Settings > Integrations...'}]); }); it('should parse multiple followups with mixed response availability', () => { From 00db222cc171915a5429d0ac0162b235d04f2e5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Thu, 29 Jan 2026 13:04:47 -0800 Subject: [PATCH 03/11] remove unused code, refactor --- src/libs/actions/Report/SuggestedFollowup.ts | 53 +++++++------- src/libs/actions/Report/index.ts | 73 ++------------------ 2 files changed, 33 insertions(+), 93 deletions(-) diff --git a/src/libs/actions/Report/SuggestedFollowup.ts b/src/libs/actions/Report/SuggestedFollowup.ts index 2a7fb2ac1a02..37cf7a344683 100644 --- a/src/libs/actions/Report/SuggestedFollowup.ts +++ b/src/libs/actions/Report/SuggestedFollowup.ts @@ -51,40 +51,41 @@ function resolveSuggestedFollowup( [reportActionID]: resolvedAction, }); + if (!followup.response) { + addComment(report, notifyReportID ?? reportID, ancestors, followup.text, timezoneParam); + return; + } + // If there's a pre-generated response, show typing indicator then display response after delay - if (followup.response) { - // Generate optimistic Concierge response action ID - const optimisticConciergeReportActionID = rand64(); + // Generate optimistic Concierge response action ID + const optimisticConciergeReportActionID = rand64(); - // Post user's comment immediately (API call includes pregenerated params for backend reconciliation) - addComment(report, notifyReportID ?? reportID, ancestors, followup.text, timezoneParam, false, false, { - optimisticConciergeReportActionID, - pregeneratedResponse: followup.response, - }); + // Post user's comment immediately (API call includes pregenerated params for backend reconciliation) + addComment(report, notifyReportID ?? reportID, ancestors, followup.text, timezoneParam, false, false, { + optimisticConciergeReportActionID, + pregeneratedResponse: followup.response, + }); + + // Show "Concierge is typing..." indicator + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, { + [CONST.ACCOUNT_ID.CONCIERGE]: true, + }); - // Show "Concierge is typing..." indicator + // After a brief delay, clear typing indicator and show the Concierge response + setTimeout(() => { + // Clear the typing indicator Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, { - [CONST.ACCOUNT_ID.CONCIERGE]: true, + [CONST.ACCOUNT_ID.CONCIERGE]: false, }); - // After a brief delay, clear typing indicator and show the Concierge response - setTimeout(() => { - // Clear the typing indicator - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, { - [CONST.ACCOUNT_ID.CONCIERGE]: false, - }); + // Create and add the optimistic Concierge response action + const optimisticConciergeAction = buildOptimisticAddCommentReportAction(followup.response, undefined, CONST.ACCOUNT_ID.CONCIERGE, 0, reportID, optimisticConciergeReportActionID); - // Create and add the optimistic Concierge response action - const optimisticConciergeAction = buildOptimisticAddCommentReportAction(followup.response, undefined, CONST.ACCOUNT_ID.CONCIERGE, 0, reportID, optimisticConciergeReportActionID); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { + [optimisticConciergeReportActionID]: optimisticConciergeAction.reportAction, + } as ReportActions); + }, CONCIERGE_RESPONSE_DELAY_MS); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { - [optimisticConciergeReportActionID]: optimisticConciergeAction.reportAction, - } as ReportActions); - }, CONCIERGE_RESPONSE_DELAY_MS); - } else { - // Post the selected followup question as a comment - addComment(report, notifyReportID ?? reportID, ancestors, followup.text, timezoneParam); - } } export default resolveSuggestedFollowup; diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 4dd37202da26..3be9f67a1b40 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -567,12 +567,6 @@ function buildOptimisticResolvedFollowups(reportAction: OnyxEntry) }; } -type PregeneratedResponseParams = { - optimisticConciergeReportActionID: string; - pregeneratedResponse: string; - optimisticConciergeAction: OptimisticAddCommentReportAction; -}; - /** * Add up to two report actions to a report. This method can be called for the following situations: * @@ -583,8 +577,7 @@ type PregeneratedResponseParams = { * @param report - The report where the comment should be added * @param notifyReportID - The report ID we should notify for new actions. This is usually the same as reportID, except when adding a comment to an expense report with a single transaction thread, in which case we want to notify the parent expense report. * @param isInSidePanel - Whether the comment is being added from the side panel - * @param pregeneratedResponseParams - Optional params for pre-generated Concierge response handling (includes optimistic action) - * @param apiOnlyPregeneratedParams - Optional params for pre-generated response (API only, no optimistic action - used when response display is delayed) + * @param pregeneratedResponseParams - Optional params for pre-generated response (API only, no optimistic action - used when response display is delayed) */ function addActions( report: OnyxEntry, @@ -595,7 +588,6 @@ function addActions( file?: FileObject, isInSidePanel = false, pregeneratedResponseParams?: PregeneratedResponseParams, - apiOnlyPregeneratedParams?: ApiOnlyPregeneratedResponseParams, ) { if (!report?.reportID) { return; @@ -670,11 +662,6 @@ function addActions( optimisticReportActions[lastActionReportActionID] = resolvedAction; } - // Add the pre-generated Concierge response to optimistic actions if provided - if (pregeneratedResponseParams) { - optimisticReportActions[pregeneratedResponseParams.optimisticConciergeReportActionID] = pregeneratedResponseParams.optimisticConciergeAction; - } - const parameters: AddCommentOrAttachmentParams = { reportID, reportActionID: file ? attachmentAction?.reportActionID : reportCommentAction?.reportActionID, @@ -696,18 +683,12 @@ function addActions( } } - // Add pre-generated response parameters for the backend to reconcile + // Add pregenerated params if (pregeneratedResponseParams) { parameters.optimisticConciergeReportActionID = pregeneratedResponseParams.optimisticConciergeReportActionID; parameters.pregeneratedResponse = pregeneratedResponseParams.pregeneratedResponse; } - // Add API-only pregenerated params (for delayed response display - optimistic action added separately after delay) - if (apiOnlyPregeneratedParams) { - parameters.optimisticConciergeReportActionID = apiOnlyPregeneratedParams.optimisticConciergeReportActionID; - parameters.pregeneratedResponse = apiOnlyPregeneratedParams.pregeneratedResponse; - } - const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -844,7 +825,7 @@ function addAttachmentWithComment( handlePlaySound(); } -type ApiOnlyPregeneratedResponseParams = { +type PregeneratedResponseParams = { optimisticConciergeReportActionID: string; pregeneratedResponse: string; }; @@ -858,53 +839,12 @@ function addComment( timezoneParam: Timezone, shouldPlaySound?: boolean, isInSidePanel?: boolean, - apiOnlyPregeneratedParams?: ApiOnlyPregeneratedResponseParams, + pregeneratedResponseParams?: PregeneratedResponseParams, ) { if (shouldPlaySound) { playSound(SOUNDS.DONE); } - addActions(report, notifyReportID, ancestors, timezoneParam, text, undefined, isInSidePanel, undefined, apiOnlyPregeneratedParams); -} - -/** - * Add a comment to a report with a pre-generated Concierge response. - * This is used when the user selects a followup question that has a cached response. - * @param report - The report where the comment should be added - * @param notifyReportID - The report ID to notify for new actions - * @param ancestors - Array of ancestor reports for proper threading - * @param text - The user's question/comment text - * @param timezoneParam - The user's timezone - * @param optimisticConciergeReportActionID - The pre-generated ID for the Concierge response action - * @param pregeneratedResponse - The pre-generated response content from Concierge - */ -function addCommentWithPregeneratedResponse( - report: OnyxEntry, - notifyReportID: string, - ancestors: Ancestor[], - text: string, - timezoneParam: Timezone, - optimisticConciergeReportActionID: string, - pregeneratedResponse: string, -) { - if (!report?.reportID) { - return; - } - - // Create the optimistic Concierge response action - const optimisticConciergeAction = buildOptimisticAddCommentReportAction( - pregeneratedResponse, - undefined, - CONST.ACCOUNT_ID.CONCIERGE, - 1, // offset to ensure it appears after the user's comment - report.reportID, - optimisticConciergeReportActionID, - ); - - addActions(report, notifyReportID, ancestors, timezoneParam, text, undefined, false, { - optimisticConciergeReportActionID, - pregeneratedResponse, - optimisticConciergeAction: optimisticConciergeAction.reportAction, - }); + addActions(report, notifyReportID, ancestors, timezoneParam, text, undefined, isInSidePanel, pregeneratedResponseParams); } function reportActionsExist(reportID: string): boolean { @@ -6672,12 +6612,11 @@ function setOptimisticTransactionThread(reportID?: string, parentReportID?: stri }); } -export type {Video, GuidedSetupData, TaskForParameters, IntroSelected, ApiOnlyPregeneratedResponseParams}; +export type {Video, GuidedSetupData, TaskForParameters, IntroSelected, PregeneratedResponseParams as ApiOnlyPregeneratedResponseParams}; export { addAttachmentWithComment, addComment, - addCommentWithPregeneratedResponse, addPolicyReport, broadcastUserIsLeavingRoom, broadcastUserIsTyping, From a5c6aef827ae78993cad5bda39c87ce330990cfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Thu, 29 Jan 2026 13:19:04 -0800 Subject: [PATCH 04/11] cleanup v1 --- src/libs/actions/Report/SuggestedFollowup.ts | 26 ++++++++++++-------- src/libs/actions/Report/index.ts | 4 +-- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/Report/SuggestedFollowup.ts b/src/libs/actions/Report/SuggestedFollowup.ts index 37cf7a344683..fa14e9f3488f 100644 --- a/src/libs/actions/Report/SuggestedFollowup.ts +++ b/src/libs/actions/Report/SuggestedFollowup.ts @@ -6,7 +6,7 @@ import type {Ancestor} from '@libs/ReportUtils'; import {buildOptimisticAddCommentReportAction} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report, ReportAction, ReportActions} from '@src/types/onyx'; +import type {Report, ReportAction} from '@src/types/onyx'; import type {Timezone} from '@src/types/onyx/PersonalDetails'; import {addComment, buildOptimisticResolvedFollowups} from '.'; @@ -21,7 +21,7 @@ const CONCIERGE_RESPONSE_DELAY_MS = 500; * @param report - The report where the action exists * @param notifyReportID - The report ID to notify for new actions * @param reportAction - The report action containing the followup-list - * @param followup - The followup object containing the question text and optional pre-generated response + * @param selectedFollowup - The followup object containing the question text and optional pre-generated response * @param timezoneParam - The user's timezone * @param ancestors - Array of ancestor reports for proper threading */ @@ -29,7 +29,7 @@ function resolveSuggestedFollowup( report: OnyxEntry, notifyReportID: string | undefined, reportAction: OnyxEntry, - followup: Followup, + selectedFollowup: Followup, timezoneParam: Timezone, ancestors: Ancestor[] = [], ) { @@ -51,8 +51,8 @@ function resolveSuggestedFollowup( [reportActionID]: resolvedAction, }); - if (!followup.response) { - addComment(report, notifyReportID ?? reportID, ancestors, followup.text, timezoneParam); + if (!selectedFollowup.response) { + addComment(report, notifyReportID ?? reportID, ancestors, selectedFollowup.text, timezoneParam); return; } @@ -61,9 +61,9 @@ function resolveSuggestedFollowup( const optimisticConciergeReportActionID = rand64(); // Post user's comment immediately (API call includes pregenerated params for backend reconciliation) - addComment(report, notifyReportID ?? reportID, ancestors, followup.text, timezoneParam, false, false, { + addComment(report, notifyReportID ?? reportID, ancestors, selectedFollowup.text, timezoneParam, false, false, { optimisticConciergeReportActionID, - pregeneratedResponse: followup.response, + pregeneratedResponse: selectedFollowup.response, }); // Show "Concierge is typing..." indicator @@ -79,13 +79,19 @@ function resolveSuggestedFollowup( }); // Create and add the optimistic Concierge response action - const optimisticConciergeAction = buildOptimisticAddCommentReportAction(followup.response, undefined, CONST.ACCOUNT_ID.CONCIERGE, 0, reportID, optimisticConciergeReportActionID); + const optimisticConciergeAction = buildOptimisticAddCommentReportAction( + selectedFollowup.response, + undefined, + CONST.ACCOUNT_ID.CONCIERGE, + 0, + reportID, + optimisticConciergeReportActionID, + ); Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { [optimisticConciergeReportActionID]: optimisticConciergeAction.reportAction, - } as ReportActions); + }); }, CONCIERGE_RESPONSE_DELAY_MS); - } export default resolveSuggestedFollowup; diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 3be9f67a1b40..0829c52f99b3 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -732,7 +732,7 @@ function addActions( }; } - const failureReportActions: Record = {}; + const failureReportActions: Record = {}; for (const [actionKey, action] of Object.entries(optimisticReportActions)) { failureReportActions[actionKey] = { @@ -748,7 +748,7 @@ function addActions( // In case of error, remove the optimistic Concierge response if (pregeneratedResponseParams) { - failureReportActions[pregeneratedResponseParams.optimisticConciergeReportActionID] = null as unknown as ReportAction; + failureReportActions[pregeneratedResponseParams.optimisticConciergeReportActionID] = undefined; } const failureData: Array> = [ From 946b1a1cabd6f7db72e0b507608810a19317a066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Thu, 29 Jan 2026 13:23:41 -0800 Subject: [PATCH 05/11] cleanup v2 --- src/libs/actions/Report/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 0829c52f99b3..c805992d989b 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -275,6 +275,11 @@ type ReportError = { type?: string; }; +type PregeneratedResponseParams = { + optimisticConciergeReportActionID: string; + pregeneratedResponse: string; +}; + const addNewMessageWithText = new Set([WRITE_COMMANDS.ADD_COMMENT, WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT]); let conciergeReportIDOnyxConnect: string | undefined; let deprecatedCurrentUserAccountID = -1; @@ -825,11 +830,6 @@ function addAttachmentWithComment( handlePlaySound(); } -type PregeneratedResponseParams = { - optimisticConciergeReportActionID: string; - pregeneratedResponse: string; -}; - /** Add a single comment to a report */ function addComment( report: OnyxEntry, @@ -6612,7 +6612,7 @@ function setOptimisticTransactionThread(reportID?: string, parentReportID?: stri }); } -export type {Video, GuidedSetupData, TaskForParameters, IntroSelected, PregeneratedResponseParams as ApiOnlyPregeneratedResponseParams}; +export type {Video, GuidedSetupData, TaskForParameters, IntroSelected}; export { addAttachmentWithComment, From 98cc7832e63ce3756329dfabbe6230e4ebc24c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Thu, 29 Jan 2026 13:47:51 -0800 Subject: [PATCH 06/11] cleanup v3 --- src/libs/ReportUtils.ts | 1 + src/libs/actions/Report/SuggestedFollowup.ts | 34 +++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index bde4474a0294..fe4a888f756d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -13413,4 +13413,5 @@ export type { OptimisticNewReport, PrepareOnboardingOnyxDataParams, SelfDMParameters, + OptimisticReportAction, }; diff --git a/src/libs/actions/Report/SuggestedFollowup.ts b/src/libs/actions/Report/SuggestedFollowup.ts index fa14e9f3488f..d560d71f0c47 100644 --- a/src/libs/actions/Report/SuggestedFollowup.ts +++ b/src/libs/actions/Report/SuggestedFollowup.ts @@ -2,7 +2,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import {rand64} from '@libs/NumberUtils'; import type {Followup} from '@libs/ReportActionFollowupUtils'; -import type {Ancestor} from '@libs/ReportUtils'; +import type {Ancestor, OptimisticReportAction} from '@libs/ReportUtils'; import {buildOptimisticAddCommentReportAction} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -11,7 +11,7 @@ import type {Timezone} from '@src/types/onyx/PersonalDetails'; import {addComment, buildOptimisticResolvedFollowups} from '.'; /** Delay before showing pre-generated Concierge response (in milliseconds) */ -const CONCIERGE_RESPONSE_DELAY_MS = 500; +const CONCIERGE_RESPONSE_DELAY_MS = 1500; /** * Resolves a suggested followup by posting the selected question as a comment @@ -57,39 +57,41 @@ function resolveSuggestedFollowup( } // If there's a pre-generated response, show typing indicator then display response after delay - // Generate optimistic Concierge response action ID + const optimisticConciergeReportActionID = rand64(); - // Post user's comment immediately (API call includes pregenerated params for backend reconciliation) + // Post user's comment immediately addComment(report, notifyReportID ?? reportID, ancestors, selectedFollowup.text, timezoneParam, false, false, { optimisticConciergeReportActionID, pregeneratedResponse: selectedFollowup.response, }); + const optimisticConciergeAction = buildOptimisticAddCommentReportAction( + selectedFollowup.response, + undefined, + CONST.ACCOUNT_ID.CONCIERGE, + CONCIERGE_RESPONSE_DELAY_MS, + reportID, + optimisticConciergeReportActionID, + ); + + addOptimisticConciergeActionWithDelay(reportID, optimisticConciergeAction); +} + +function addOptimisticConciergeActionWithDelay(reportID: string, optimisticConciergeAction: OptimisticReportAction) { // Show "Concierge is typing..." indicator Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, { [CONST.ACCOUNT_ID.CONCIERGE]: true, }); - // After a brief delay, clear typing indicator and show the Concierge response setTimeout(() => { // Clear the typing indicator Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, { [CONST.ACCOUNT_ID.CONCIERGE]: false, }); - // Create and add the optimistic Concierge response action - const optimisticConciergeAction = buildOptimisticAddCommentReportAction( - selectedFollowup.response, - undefined, - CONST.ACCOUNT_ID.CONCIERGE, - 0, - reportID, - optimisticConciergeReportActionID, - ); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { - [optimisticConciergeReportActionID]: optimisticConciergeAction.reportAction, + [optimisticConciergeAction.reportAction.reportActionID]: optimisticConciergeAction.reportAction, }); }, CONCIERGE_RESPONSE_DELAY_MS); } From cef82a3892cbeae47de13f660ec4dcf3d30bf417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Thu, 29 Jan 2026 16:12:54 -0800 Subject: [PATCH 07/11] button changes --- src/components/ReportActionItem/ActionableItemButtons.tsx | 2 -- src/pages/inbox/report/PureReportActionItem.tsx | 8 +++++--- src/styles/index.ts | 6 ------ 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/components/ReportActionItem/ActionableItemButtons.tsx b/src/components/ReportActionItem/ActionableItemButtons.tsx index 0454480860fc..4ea02ce0fba1 100644 --- a/src/components/ReportActionItem/ActionableItemButtons.tsx +++ b/src/components/ReportActionItem/ActionableItemButtons.tsx @@ -22,7 +22,6 @@ type ActionableItemButtonsProps = { styles?: { text?: StyleProp; button?: StyleProp; - buttonHover?: StyleProp; container?: StyleProp; }; }; @@ -41,7 +40,6 @@ function ActionableItemButtons(props: ActionableItemButtonsProps) { medium success={item.isPrimary} innerStyles={props.styles?.button} - hoverStyles={props.styles?.buttonHover} primaryTextNumberOfLines={props.primaryTextNumberOfLines} textStyles={props.styles?.text} /> diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 3116dc97c9f0..32426eec007b 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -1730,10 +1730,12 @@ function PureReportActionItem({ shouldUseLocalization={!isConciergeOptions && !actionContainsFollowUps} primaryTextNumberOfLines={actionableButtonsNoLines} styles={{ - text: [isConciergeOptions || actionContainsFollowUps ? styles.textAlignLeft : undefined, actionContainsFollowUps && styles.fontWeightNormal], + text: [isConciergeOptions || actionContainsFollowUps ? styles.textAlignLeft : undefined], button: actionContainsFollowUps ? [styles.actionableItemButton, hovered && styles.actionableItemButtonBackgroundHovered] : undefined, - buttonHover: actionContainsFollowUps ? styles.actionableItemButtonHovered : undefined, - container: actionContainsFollowUps && shouldUseNarrowLayout ? [styles.alignItemsStretch] : undefined, + container: [ + actionContainsFollowUps && shouldUseNarrowLayout ? styles.alignItemsStretch : undefined, + actionContainsFollowUps ? styles.mt5 : undefined, + ], }} /> )} diff --git a/src/styles/index.ts b/src/styles/index.ts index 8b4c7cc8817b..8720fdcb98e6 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -931,9 +931,6 @@ const staticStyles = (theme: ThemeColors) => actionableItemButton: { paddingTop: 8, paddingBottom: 8, - backgroundColor: 'transparent', - borderWidth: 1, - borderColor: theme.border, alignItems: 'flex-start', borderRadius: variables.componentBorderRadiusMedium, }, @@ -942,9 +939,6 @@ const staticStyles = (theme: ThemeColors) => borderColor: theme.buttonPressedBG, }, - actionableItemButtonHovered: { - borderWidth: 1, - }, hoveredComponentBG: { backgroundColor: theme.hoverComponentBG, From c2caaeedf989450d00c23a4d1d801011977171a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Thu, 29 Jan 2026 16:16:46 -0800 Subject: [PATCH 08/11] fix test --- tests/actions/ReportTest.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 7b5ad45d3dd0..2e519e2f1fd8 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -3755,7 +3755,7 @@ describe('actions/Report', () => { ], } as OnyxTypes.ReportAction; - // Set up initial Onyx state + // Set up initial Onyx state with real timers await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { [REPORT_ACTION_ID]: reportAction, @@ -3766,7 +3766,7 @@ describe('actions/Report', () => { await waitForBatchedUpdates(); // Verify the followup-list was marked as selected - const reportActions = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}` as const); + let reportActions = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}` as const); const updatedHtml = (reportActions?.[REPORT_ACTION_ID]?.message as Message[])?.at(0)?.html; expect(updatedHtml).toContain(''); @@ -3774,7 +3774,14 @@ describe('actions/Report', () => { // With pre-generated response, the API call should include the optimistic Concierge response params TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.ADD_COMMENT, 1); + // Wait for the delayed Concierge response (1500ms delay in SuggestedFollowup.ts) + await new Promise((resolve) => { + setTimeout(resolve, 1600); + }); + await waitForBatchedUpdates(); + // Verify an optimistic Concierge report action was created + reportActions = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}` as const); const allReportActions = Object.values(reportActions ?? {}); const conciergeActions = allReportActions.filter((action) => action?.actorAccountID === CONST.ACCOUNT_ID.CONCIERGE); // Should have 2 Concierge actions: the original one and the optimistic response From 2cecb134195c1f2b6164fa104d256d983e814e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Thu, 29 Jan 2026 16:27:40 -0800 Subject: [PATCH 09/11] prettier --- src/styles/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/styles/index.ts b/src/styles/index.ts index 8720fdcb98e6..dd802d0fadc2 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -939,7 +939,6 @@ const staticStyles = (theme: ThemeColors) => borderColor: theme.buttonPressedBG, }, - hoveredComponentBG: { backgroundColor: theme.hoverComponentBG, }, From cb0b010bf39254f9fc7c7da54d3ae8586663273c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Thu, 29 Jan 2026 17:31:45 -0800 Subject: [PATCH 10/11] safeguard debug panel --- src/pages/Debug/Report/DebugReportActions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Debug/Report/DebugReportActions.tsx b/src/pages/Debug/Report/DebugReportActions.tsx index 36a947f8273d..78a2ec4b8156 100644 --- a/src/pages/Debug/Report/DebugReportActions.tsx +++ b/src/pages/Debug/Report/DebugReportActions.tsx @@ -95,7 +95,7 @@ function DebugReportActions({reportID}: DebugReportActionsProps) { return (sortedAllReportActions ?? []) .filter( (reportAction) => - reportAction.reportActionID.includes(debouncedSearchValue) || getReportActionDebugText(reportAction).toLowerCase().includes(debouncedSearchValue.toLowerCase()), + reportAction?.reportActionID?.includes(debouncedSearchValue) || getReportActionDebugText(reportAction).toLowerCase().includes(debouncedSearchValue.toLowerCase()), ) .map((reportAction) => ({ reportActionID: reportAction.reportActionID, From 615e6103fc5c0dc4bdeaf529f7635c07a5f76d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Thu, 29 Jan 2026 18:11:04 -0800 Subject: [PATCH 11/11] fix some AI comments --- src/libs/actions/Report/SuggestedFollowup.ts | 2 +- src/libs/actions/Report/index.ts | 4 ++-- src/pages/Debug/Report/DebugReportActions.tsx | 3 ++- src/pages/inbox/report/PureReportActionItem.tsx | 2 +- tests/actions/ReportTest.ts | 4 ++-- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/Report/SuggestedFollowup.ts b/src/libs/actions/Report/SuggestedFollowup.ts index d560d71f0c47..fbd83bfddf88 100644 --- a/src/libs/actions/Report/SuggestedFollowup.ts +++ b/src/libs/actions/Report/SuggestedFollowup.ts @@ -96,4 +96,4 @@ function addOptimisticConciergeActionWithDelay(reportID: string, optimisticConci }, CONCIERGE_RESPONSE_DELAY_MS); } -export default resolveSuggestedFollowup; +export {resolveSuggestedFollowup, CONCIERGE_RESPONSE_DELAY_MS}; diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 3b13d8826f51..8e8df7fe6d6f 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -737,7 +737,7 @@ function addActions( }; } - const failureReportActions: Record = {}; + const failureReportActions: Record = {}; for (const [actionKey, action] of Object.entries(optimisticReportActions)) { failureReportActions[actionKey] = { @@ -753,7 +753,7 @@ function addActions( // In case of error, remove the optimistic Concierge response if (pregeneratedResponseParams) { - failureReportActions[pregeneratedResponseParams.optimisticConciergeReportActionID] = undefined; + failureReportActions[pregeneratedResponseParams.optimisticConciergeReportActionID] = null; } const failureData: Array> = [ diff --git a/src/pages/Debug/Report/DebugReportActions.tsx b/src/pages/Debug/Report/DebugReportActions.tsx index 78a2ec4b8156..d1662bdba411 100644 --- a/src/pages/Debug/Report/DebugReportActions.tsx +++ b/src/pages/Debug/Report/DebugReportActions.tsx @@ -93,9 +93,10 @@ function DebugReportActions({reportID}: DebugReportActionsProps) { const searchedReportActions = useMemo(() => { return (sortedAllReportActions ?? []) + .filter((reportAction) => reportAction?.reportActionID) .filter( (reportAction) => - reportAction?.reportActionID?.includes(debouncedSearchValue) || getReportActionDebugText(reportAction).toLowerCase().includes(debouncedSearchValue.toLowerCase()), + reportAction.reportActionID?.includes(debouncedSearchValue) || getReportActionDebugText(reportAction).toLowerCase().includes(debouncedSearchValue.toLowerCase()), ) .map((reportAction) => ({ reportActionID: reportAction.reportActionID, diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index ebbf3edf7dc8..72a9bad07520 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -51,7 +51,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import resolveSuggestedFollowup from '@libs/actions/Report/SuggestedFollowup'; +import {resolveSuggestedFollowup} from '@libs/actions/Report/SuggestedFollowup'; import ControlSelection from '@libs/ControlSelection'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index a433a4730c43..c1a1556a688b 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -9,7 +9,7 @@ import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; import type {SearchQueryJSON} from '@components/Search/types'; import useAncestors from '@hooks/useAncestors'; -import resolveSuggestedFollowup from '@libs/actions/Report/SuggestedFollowup'; +import {CONCIERGE_RESPONSE_DELAY_MS, resolveSuggestedFollowup} from '@libs/actions/Report/SuggestedFollowup'; import {getOnboardingMessages} from '@libs/actions/Welcome/OnboardingFlow'; import {WRITE_COMMANDS} from '@libs/API/types'; import HttpUtils from '@libs/HttpUtils'; @@ -3812,7 +3812,7 @@ describe('actions/Report', () => { // Wait for the delayed Concierge response (1500ms delay in SuggestedFollowup.ts) await new Promise((resolve) => { - setTimeout(resolve, 1600); + setTimeout(resolve, CONCIERGE_RESPONSE_DELAY_MS + 100); }); await waitForBatchedUpdates();