From 63996bf9d5aad083519b51cb564f9f9e5d478548 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Tue, 18 Aug 2026 14:18:52 +0300 Subject: [PATCH 01/11] Thread Concierge attachments --- src/libs/actions/Report/index.ts | 3 +++ .../report/ReportActionCompose/useComposerSubmit.ts | 12 ++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index ca9180724a36..34fab1105da3 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -444,6 +444,7 @@ type AddAttachmentWithCommentParams = { delegateAccountID: number | undefined; sidePanelContext?: SidePanelContext; conciergeReportID: string | undefined; + conciergeThreadReportID?: string; }; type MergeReportsProps = { @@ -1290,6 +1291,7 @@ function addAttachmentWithComment({ delegateAccountID, sidePanelContext, conciergeReportID, + conciergeThreadReportID, }: AddAttachmentWithCommentParams) { if (!report?.reportID) { return; @@ -1316,6 +1318,7 @@ function addAttachmentWithComment({ delegateAccountID, sidePanelContext, conciergeReportID, + conciergeThreadReportID, }); handlePlaySound(); return; diff --git a/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts b/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts index b3c2c6533a5d..5c99b22a0926 100644 --- a/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts +++ b/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts @@ -88,6 +88,10 @@ function useComposerSubmit(reportID: string) { clearAgentZeroProcessingIndicator(reportID, CONST.ACCOUNT_ID.CONCIERGE); } + // 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. + const shouldRespondInThread = reportID === conciergeReportID && !isInSidePanel && isBetaEnabled(CONST.BETAS.CONCIERGE_RESPOND_IN_THREAD); + if (attachmentFileRef.current) { addAttachmentWithComment({ report: targetReport, @@ -102,6 +106,9 @@ function useComposerSubmit(reportID: string) { delegateAccountID, sidePanelContext, conciergeReportID, + + // A send with several attachments posts one message per attachment, so it stays in the DM. + conciergeThreadReportID: shouldRespondInThread && !Array.isArray(attachmentFileRef.current) ? generateReportID() : undefined, }); attachmentFileRef.current = null; return; @@ -197,10 +204,7 @@ 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, + conciergeThreadReportID: shouldRespondInThread ? generateReportID() : undefined, }); }; From b04bf60befbb10cd429528d412b3c8f7f598cbfc Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Wed, 19 Aug 2026 01:40:46 +0300 Subject: [PATCH 02/11] Count single attachment array --- src/libs/actions/Report/index.ts | 15 ++++++++++++++- .../ReportActionCompose/useComposerSubmit.ts | 3 ++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 34fab1105da3..197e32f7d60d 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -1325,7 +1325,20 @@ function addAttachmentWithComment({ } // Multiple attachments - first: combine text + first attachment as a single action - addActions({report, notifyReportID, ancestors, timezoneParam: timezone, currentUserAccountID, text, file: attachments?.at(0), isInSidePanel, delegateAccountID, conciergeReportID}); + // Only a lone attachment threads, since each of several attachments posts its own message. + addActions({ + report, + notifyReportID, + ancestors, + timezoneParam: timezone, + currentUserAccountID, + text, + file: attachments?.at(0), + isInSidePanel, + delegateAccountID, + conciergeReportID, + conciergeThreadReportID: attachments.length === 1 ? conciergeThreadReportID : undefined, + }); // Remaining: attachment-only actions (no text duplication) for (let i = 1; i < attachments?.length; i += 1) { diff --git a/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts b/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts index 5c99b22a0926..a1062184e048 100644 --- a/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts +++ b/src/pages/inbox/report/ReportActionCompose/useComposerSubmit.ts @@ -93,6 +93,7 @@ function useComposerSubmit(reportID: string) { const shouldRespondInThread = reportID === conciergeReportID && !isInSidePanel && isBetaEnabled(CONST.BETAS.CONCIERGE_RESPOND_IN_THREAD); if (attachmentFileRef.current) { + const attachmentCount = Array.isArray(attachmentFileRef.current) ? attachmentFileRef.current.length : 1; addAttachmentWithComment({ report: targetReport, notifyReportID: reportID, @@ -108,7 +109,7 @@ function useComposerSubmit(reportID: string) { conciergeReportID, // A send with several attachments posts one message per attachment, so it stays in the DM. - conciergeThreadReportID: shouldRespondInThread && !Array.isArray(attachmentFileRef.current) ? generateReportID() : undefined, + conciergeThreadReportID: shouldRespondInThread && attachmentCount === 1 ? generateReportID() : undefined, }); attachmentFileRef.current = null; return; From 1fbe82c434b68fde82fff2dd8622149901b6cb17 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Wed, 19 Aug 2026 03:22:15 +0300 Subject: [PATCH 03/11] Restore single line From d2751d5aef65287e7e609a73d3c2287727bc9dff Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Wed, 19 Aug 2026 03:23:59 +0300 Subject: [PATCH 04/11] Drop comment --- src/libs/actions/Report/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 197e32f7d60d..650129fed290 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -1325,7 +1325,6 @@ function addAttachmentWithComment({ } // Multiple attachments - first: combine text + first attachment as a single action - // Only a lone attachment threads, since each of several attachments posts its own message. addActions({ report, notifyReportID, From 130ca3b2574d1c21439dd5c37784eb69ae2bed80 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 24 Aug 2026 12:18:52 +0300 Subject: [PATCH 05/11] Show thread title --- src/libs/ReportNameUtils.ts | 11 +++++++ src/libs/actions/Report/index.ts | 1 - tests/unit/ReportNameUtilsTest.ts | 49 +++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts index a9bd8efedd28..24ccc4d83cdf 100644 --- a/src/libs/ReportNameUtils.ts +++ b/src/libs/ReportNameUtils.ts @@ -961,6 +961,7 @@ function computeChatThreadReportName( reports: OnyxCollection, currentUserLogin: string, transactions: OnyxCollection, + conciergeReportID: string | undefined, parentReportAction?: ReportAction, policyTags?: OnyxEntry, policy?: OnyxEntry, @@ -998,6 +999,15 @@ function computeChatThreadReportName( return translate('parentReportAction.deletedMessage'); } + // Concierge titles each of its threads with a summary of the question, so prefer that over the question itself. + if ( + report.reportName && + report.reportName !== CONST.REPORT.DEFAULT_REPORT_NAME && + isConciergeChatReport(reports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`], conciergeReportID) + ) { + return report.reportName; + } + const isAttachment = isReportActionAttachment(!isEmptyObject(parentReportAction) ? parentReportAction : undefined); const reportActionMessage = getReportActionText(parentReportAction).replaceAll(/(\n+|\r\n|\n|\r)/gm, ' '); if (isAttachment && reportActionMessage) { @@ -1135,6 +1145,7 @@ function computeReportName({ reports ?? {}, currentUserLogin ?? '', transactions, + conciergeReportID, parentReportAction, policyTags, reportPolicy, diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index ffecf1fb648c..a037f714f157 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -1163,7 +1163,6 @@ function addActions({ const optimisticThread = buildOptimisticChatReport({ participantList: [currentUserAccountID, CONST.ACCOUNT_ID.CONCIERGE], - reportName: reportCommentText, parentReportActionID: resolvedReportActionID, parentReportID: reportID, optimisticReportID: conciergeThreadReportID, diff --git a/tests/unit/ReportNameUtilsTest.ts b/tests/unit/ReportNameUtilsTest.ts index e61bdd146f94..6b5068926ce5 100644 --- a/tests/unit/ReportNameUtilsTest.ts +++ b/tests/unit/ReportNameUtilsTest.ts @@ -404,6 +404,55 @@ describe('ReportNameUtils', () => { }); }); + describe('computeReportName - Concierge threads', () => { + const conciergeReportID = '777'; + const question = 'How do I set up QuickBooks?'; + + const computeConciergeThreadName = (threadReportName: string) => { + const conciergeDM = {...createRegularChat(90, [currentUserAccountID, CONST.ACCOUNT_ID.CONCIERGE]), reportID: conciergeReportID}; + const thread: Report = { + ...createRegularChat(91, [currentUserAccountID, CONST.ACCOUNT_ID.CONCIERGE]), + reportName: threadReportName, + parentReportID: conciergeReportID, + parentReportActionID: '888', + }; + const parentAction = createMock({ + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + reportActionID: '888', + message: [{type: 'COMMENT', html: question, text: question}], + created: '', + lastModified: '', + actorAccountID: currentUserAccountID, + person: [], + }); + + return computeReportNameOriginal({ + dateFnsLocale: undefined, + conciergeReportID, + report: thread, + reports: {[`${ONYXKEYS.COLLECTION.REPORT}${conciergeReportID}`]: conciergeDM}, + policies: emptyCollections.policies, + transactions: undefined, + allReportNameValuePairs: undefined, + personalDetailsList: participantsPersonalDetails, + reportActions: {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${conciergeReportID}`]: {'888': parentAction}}, + currentUserAccountID, + currentUserLogin, + reportTransactions: buildTransactionsByReportID(undefined), + translate: translateLocal, + isTrackIntentUser: false, + }); + }; + + test('uses the generated title once Concierge has titled the thread', () => { + expect(computeConciergeThreadName('QuickBooks setup')).toBe('QuickBooks setup'); + }); + + test('falls back to the question while the thread still has the default name', () => { + expect(computeConciergeThreadName(CONST.REPORT.DEFAULT_REPORT_NAME)).toBe(question); + }); + }); + describe('computeReportName - Thread report action names', () => { test('Submitted parent action', () => { const thread: Report = createWorkspaceThread(50); From a312e361f481271d42b8cce277d7405ef7f91550 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Mon, 24 Aug 2026 14:16:06 +0300 Subject: [PATCH 06/11] Respect moderation --- src/libs/ReportNameUtils.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts index 24ccc4d83cdf..eff9a68b1e4d 100644 --- a/src/libs/ReportNameUtils.ts +++ b/src/libs/ReportNameUtils.ts @@ -999,15 +999,6 @@ function computeChatThreadReportName( return translate('parentReportAction.deletedMessage'); } - // Concierge titles each of its threads with a summary of the question, so prefer that over the question itself. - if ( - report.reportName && - report.reportName !== CONST.REPORT.DEFAULT_REPORT_NAME && - isConciergeChatReport(reports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`], conciergeReportID) - ) { - return report.reportName; - } - const isAttachment = isReportActionAttachment(!isEmptyObject(parentReportAction) ? parentReportAction : undefined); const reportActionMessage = getReportActionText(parentReportAction).replaceAll(/(\n+|\r\n|\n|\r)/gm, ' '); if (isAttachment && reportActionMessage) { @@ -1020,6 +1011,15 @@ function computeChatThreadReportName( ) { return translate('parentReportAction.hiddenMessage'); } + + // Concierge titles each of its threads with a summary of the question, so prefer that over the question itself. + if ( + report.reportName && + report.reportName !== CONST.REPORT.DEFAULT_REPORT_NAME && + isConciergeChatReport(reports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`], conciergeReportID) + ) { + return report.reportName; + } if (isAdminRoom(report) || isUserCreatedPolicyRoom(report)) { return reportActionMessage; } From 2087b04ea7f8d84bd030aae742334bfe18d84c66 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Tue, 25 Aug 2026 15:30:23 +0300 Subject: [PATCH 07/11] Concierge thread avatar --- src/components/AvatarWithDisplayName.tsx | 5 +++++ src/libs/ReportUtils.ts | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 16a0028d1d1d..3ff2c05b16e2 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -268,6 +268,11 @@ function AvatarWithDisplayName({ } if (isChatThread(report)) { + if (report?.parentReportID === conciergeReportID) { + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.PROFILE.getRoute(CONST.ACCOUNT_ID.CONCIERGE))); + return; + } + // In an ideal situation account ID won't be 0 if (actorAccountID.current && actorAccountID.current > 0) { Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.PROFILE.getRoute(actorAccountID.current))); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 01758446e98c..2e54f74828ae 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3884,6 +3884,9 @@ function getIconsForChatThread( if (!report?.parentReportID || !report?.parentReportActionID) { return []; } + if (report.parentReportID === conciergeReportIDOnyxConnect) { + return getIconsForParticipants([CONST.ACCOUNT_ID.CONCIERGE], personalDetails); + } const parentReportAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID]; const actorAccountID = getReportActionActorAccountID(parentReportAction, report as OnyxEntry, report as OnyxEntry); const actorDetails = actorAccountID ? personalDetails?.[actorAccountID] : undefined; From ea801bebc0a15587477a251a5d5a9669fe110783 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Wed, 26 Aug 2026 05:19:36 +0300 Subject: [PATCH 08/11] Fix lint --- tests/unit/ReportNameUtilsTest.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/ReportNameUtilsTest.ts b/tests/unit/ReportNameUtilsTest.ts index 6b5068926ce5..8fea617a33b1 100644 --- a/tests/unit/ReportNameUtilsTest.ts +++ b/tests/unit/ReportNameUtilsTest.ts @@ -406,6 +406,7 @@ describe('ReportNameUtils', () => { describe('computeReportName - Concierge threads', () => { const conciergeReportID = '777'; + const parentReportActionID = '888'; const question = 'How do I set up QuickBooks?'; const computeConciergeThreadName = (threadReportName: string) => { @@ -414,11 +415,11 @@ describe('ReportNameUtils', () => { ...createRegularChat(91, [currentUserAccountID, CONST.ACCOUNT_ID.CONCIERGE]), reportName: threadReportName, parentReportID: conciergeReportID, - parentReportActionID: '888', + parentReportActionID, }; const parentAction = createMock({ actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - reportActionID: '888', + reportActionID: parentReportActionID, message: [{type: 'COMMENT', html: question, text: question}], created: '', lastModified: '', @@ -435,7 +436,7 @@ describe('ReportNameUtils', () => { transactions: undefined, allReportNameValuePairs: undefined, personalDetailsList: participantsPersonalDetails, - reportActions: {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${conciergeReportID}`]: {'888': parentAction}}, + reportActions: {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${conciergeReportID}`]: {[parentReportActionID]: parentAction}}, currentUserAccountID, currentUserLogin, reportTransactions: buildTransactionsByReportID(undefined), From 5f5919074f7fc3f96ebed535d20be0e057d6a396 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Wed, 26 Aug 2026 05:19:37 +0300 Subject: [PATCH 09/11] Prefer title over attachment --- src/libs/ReportNameUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts index eff9a68b1e4d..d70e91bd64c8 100644 --- a/src/libs/ReportNameUtils.ts +++ b/src/libs/ReportNameUtils.ts @@ -1001,9 +1001,6 @@ function computeChatThreadReportName( const isAttachment = isReportActionAttachment(!isEmptyObject(parentReportAction) ? parentReportAction : undefined); const reportActionMessage = getReportActionText(parentReportAction).replaceAll(/(\n+|\r\n|\n|\r)/gm, ' '); - if (isAttachment && reportActionMessage) { - return `[${translate('common.attachment')}]`; - } if ( parentReportActionMessage?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || parentReportActionMessage?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN || @@ -1020,6 +1017,9 @@ function computeChatThreadReportName( ) { return report.reportName; } + if (isAttachment && reportActionMessage) { + return `[${translate('common.attachment')}]`; + } if (isAdminRoom(report) || isUserCreatedPolicyRoom(report)) { return reportActionMessage; } From 661c49ed43b33f12582b80514aec6ab91d3e5109 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Wed, 26 Aug 2026 05:48:44 +0300 Subject: [PATCH 10/11] Keep thread avatar --- src/components/AvatarWithDisplayName.tsx | 5 ----- src/libs/ReportUtils.ts | 3 --- 2 files changed, 8 deletions(-) diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 3ff2c05b16e2..16a0028d1d1d 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -268,11 +268,6 @@ function AvatarWithDisplayName({ } if (isChatThread(report)) { - if (report?.parentReportID === conciergeReportID) { - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.PROFILE.getRoute(CONST.ACCOUNT_ID.CONCIERGE))); - return; - } - // In an ideal situation account ID won't be 0 if (actorAccountID.current && actorAccountID.current > 0) { Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.PROFILE.getRoute(actorAccountID.current))); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 11b963a90c43..9e3e295af647 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3884,9 +3884,6 @@ function getIconsForChatThread( if (!report?.parentReportID || !report?.parentReportActionID) { return []; } - if (report.parentReportID === conciergeReportIDOnyxConnect) { - return getIconsForParticipants([CONST.ACCOUNT_ID.CONCIERGE], personalDetails); - } const parentReportAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID]; const actorAccountID = getReportActionActorAccountID(parentReportAction, report as OnyxEntry, report as OnyxEntry); const actorDetails = actorAccountID ? personalDetails?.[actorAccountID] : undefined; From 5321be650f84dfa20b7df5d65d8985e1a2d1e78e Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Wed, 26 Aug 2026 05:50:31 +0300 Subject: [PATCH 11/11] Concierge avatar in LHN --- src/libs/SidebarUtils.ts | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 2680cbadcdf6..b73fae4b4e89 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -169,6 +169,7 @@ import { getDisplayNameForParticipant, getDisplayNamesWithTooltips, getIcons, + getIconsForParticipants, getMovedTransactionMessage, getParticipantsAccountIDsForDisplay, getPendingDeleteMemberAccountIDs, @@ -1437,19 +1438,23 @@ function getOptionData({ result.subtitle = subtitle; result.participantsList = participantPersonalDetailList; - const reportIcons = getIcons( - report, - formatPhoneNumber, - translate, - personalDetails, - personalDetail?.avatar, - personalDetail?.login, - personalDetail?.accountID ?? CONST.DEFAULT_NUMBER_ID, - policy, - invoiceReceiverPolicy, - isReportArchived, - getPendingDeleteMemberAccountIDs(reportMetadata?.pendingChatMembers), - ); + // A Concierge thread is a conversation with Concierge, so the LHN shows Concierge rather than whoever asked. + const reportIcons = + isChatThread(report) && report.parentReportID === conciergeReportID + ? getIconsForParticipants([CONST.ACCOUNT_ID.CONCIERGE], personalDetails) + : getIcons( + report, + formatPhoneNumber, + translate, + personalDetails, + personalDetail?.avatar, + personalDetail?.login, + personalDetail?.accountID ?? CONST.DEFAULT_NUMBER_ID, + policy, + invoiceReceiverPolicy, + isReportArchived, + getPendingDeleteMemberAccountIDs(reportMetadata?.pendingChatMembers), + ); // IOU icon trimming (single vs diagonal) is handled at the component level // using useReportPreviewSenderID which has access to transaction attendee data.