Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
70c6b40
feat: add 👍/👎 feedback prompt under the latest Concierge response
oqildev Sep 5, 2026
f65466f
fix: stop gating the Concierge feedback prompt on a flag that can stick
oqildev Sep 6, 2026
34b8058
Revert the feedback prompt gate to the synthetic-draft flag
oqildev Sep 6, 2026
538f241
Use the mock's hover treatment and tooltip copy for the feedback thumbs
oqildev Sep 6, 2026
05b04a9
Make the thumb hover visible and centre the glyph inside it
oqildev Sep 6, 2026
b6711ff
Use the shared ghost-button hover style for the feedback thumbs
oqildev Sep 6, 2026
563aa1e
Make the thumb hover state actually visible
oqildev Sep 6, 2026
0de855a
Do not offer feedback while newer report actions are unloaded
oqildev Sep 6, 2026
f2c9c7a
Extract the feedback thumb into its own component
oqildev Sep 6, 2026
b1501bf
Keep the prompt out of the feedback thread and close the gap between …
oqildev Sep 7, 2026
29d0414
Address the review on the acknowledgement, ordering and failed comments
oqildev Sep 8, 2026
021c65f
Merge remote-tracking branch 'upstream/main' into feat/96775-concierg…
oqildev Sep 9, 2026
e52d586
Hoist the parent report action selector out of the component
oqildev Sep 9, 2026
595c9f1
Identify the feedback thread by its own marker
oqildev Sep 13, 2026
3bb8d33
Skip the feedback scan outside the chats where Concierge answers
oqildev Sep 14, 2026
c41a2b7
Hide the feedback prompt on a message moderation has hidden
oqildev Sep 14, 2026
6cb8e90
Give the feedback prompt strings translation context
oqildev Sep 14, 2026
aff0228
Merge remote-tracking branch 'upstream/main' into feat/96775-concierg…
oqildev Sep 15, 2026
535fb8c
Restore the JavaScript action bundles rebuilt during the merge
oqildev Sep 15, 2026
5493589
Address the review on the draft gate, the chat guard and comments
oqildev Sep 15, 2026
540be7e
Merge remote-tracking branch 'upstream/main' into feat/96775-concierg…
oqildev Sep 16, 2026
cfb68aa
Decide the feedback thread case in the list
oqildev Sep 17, 2026
650a7de
Take the locale strings from the translation workflow
oqildev Sep 17, 2026
bb8b21a
Merge remote-tracking branch 'upstream/main' into feat/96775-concierg…
oqildev Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9329,6 +9329,9 @@ const CONST = {
REACTION_BUBBLE: 'EmojiReactions-ReactionBubble',
ADD_REACTION_BUBBLE: 'EmojiReactions-AddReactionBubble',
},
CONCIERGE_FEEDBACK: {
THUMB: 'ConciergeFeedback-Thumb',
},
MINI_CONTEXT_MENU: {
QUICK_REACTION: 'MiniContextMenu-QuickReaction',
EMOJI_PICKER_BUTTON: 'MiniContextMenu-EmojiPickerButton',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import REPORT_LINK_ROUTE_PARAMS from '@libs/Navigation/reportLinkRouteParams';
import TransitionTracker from '@libs/Navigation/TransitionTracker';
import type {ReportsSplitNavigatorParamList} from '@libs/Navigation/types';
import {isTrackOnboardingChoice} from '@libs/OnboardingUtils';
import {getFilteredReportActionsForReportView, getOneTransactionThreadReportID, hasNextActionMadeBySameActor} from '@libs/ReportActionsUtils';
import {getFilteredReportActionsForReportView, getLatestConciergeFeedbackActionID, getOneTransactionThreadReportID, hasNextActionMadeBySameActor} from '@libs/ReportActionsUtils';
import {canUserPerformWriteAction, chatIncludesChronosWithID, getReportLastVisibleActionCreated, isHarvestCreatedExpenseReport, shouldShowMarkAsDone} from '@libs/ReportUtils';
import markOpenReportEnd from '@libs/telemetry/markOpenReportEnd';

Expand Down Expand Up @@ -186,6 +186,21 @@ function MoneyRequestReportActionsListContent({reportIDFromRoute, onLayout}: Mon
const lastVisibleActionCreated = getReportLastVisibleActionCreated(report, transactionThreadReport);
const hasNewestReportAction = lastAction?.created === lastVisibleActionCreated;

const reportActionIDs = useMemo(() => {
return reportActions?.map((action) => action.reportActionID) ?? [];
}, [reportActions]);

const conciergeFeedbackForReportActionID = reportNameValuePairs?.conciergeFeedbackForReportActionID;

// Skip inside the thread the backend opens after a thumbs down, while a Concierge answer is still streaming, and while newer actions are not loaded because the newest reply may not be in the list yet
const latestConciergeFeedbackActionID = useMemo(
() =>
conciergeFeedbackForReportActionID || isDraftPendingCompletion || hasNewerActions
? undefined
: getLatestConciergeFeedbackActionID(visibleReportActionsNewestFirst, reportActionIDs),
[conciergeFeedbackForReportActionID, isDraftPendingCompletion, hasNewerActions, visibleReportActionsNewestFirst, reportActionIDs],
);

const {onStartReached, onEndReached} = useMoneyRequestReportPagination({
reportID,
reportActions,
Expand Down Expand Up @@ -384,6 +399,7 @@ function MoneyRequestReportActionsListContent({reportIDFromRoute, onLayout}: Mon
linkedReportActionID={linkedReportActionID}
isHarvestCreatedExpenseReport={shouldShowHarvestCreatedAction}
shouldDisableContextMenuForConciergeDraft={shouldDisableContextMenuForConciergeDraft}
isLatestConciergeFeedbackAction={!!latestConciergeFeedbackActionID && latestConciergeFeedbackActionID === reportAction.reportActionID}
/>
</ReportActionPositionContextProvider>
</ReportActionScrollToNewestContext.Provider>
Expand All @@ -402,11 +418,15 @@ function MoneyRequestReportActionsListContent({reportIDFromRoute, onLayout}: Mon
shouldShowHarvestCreatedAction,
draftReportActionID,
isDraftPendingCompletion,
latestConciergeFeedbackActionID,
scrollToBottom,
],
);

const reportActionsExtraData = useMemo(() => [draftReportActionID, isDraftPendingCompletion], [draftReportActionID, isDraftPendingCompletion]);
const reportActionsExtraData = useMemo(
() => [draftReportActionID, isDraftPendingCompletion, latestConciergeFeedbackActionID],
[draftReportActionID, isDraftPendingCompletion, latestConciergeFeedbackActionID],
);

const scrollToLatestMessages = useCallback(() => {
setIsFloatingMessageCounterVisible(false);
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useReportActionsListModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ function useReportActionsListModel(reportID: string, isReportLoadPending: boolea
isConciergeHiddenHistory,
showFullHistory,
hasPreviousMessages,
allReportActionIDs,
};

// The command handles on `ReportActionsListActionsContext`. Referentially stable, so actions-only
Expand Down
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ const translations: TranslationDeepObject<typeof en> = {
collapseReasoning: 'Begründung einklappen',
expandReasoning: 'Begründung erweitern',
enableNotifications: {prompt: 'Möchten Sie benachrichtigt werden, wenn Concierge antwortet?', cta: 'Benachrichtigen'},
feedback: {prompt: 'War diese Antwort hilfreich?', useful: 'Hilfreiche Antwort', notUseful: 'Nicht hilfreiche Antwort', thanks: 'Danke für Ihr Feedback!'},
},
supportalNoAccess: {
title: 'Nicht so schnell',
Expand Down
1 change: 1 addition & 0 deletions src/languages/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ const translations: TranslationDeepObject<typeof en> = {
prompt: 'Θέλετε να ειδοποιείστε όταν απαντά ο Concierge;',
cta: 'Ειδοποίηση',
},
feedback: {prompt: 'Ήταν χρήσιμη αυτή η απάντηση;', useful: 'Χρήσιμη απάντηση', notUseful: 'Μη χρήσιμη απάντηση', thanks: 'Ευχαριστούμε για τα σχόλιά σας!'},
},
supportalNoAccess: {
title: 'Όχι τόσο γρήγορα',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,12 @@ const translations = {
prompt: 'Want to be notified when Concierge responds?',
cta: 'Notify',
},
feedback: {
prompt: 'Was that response useful?',
useful: 'Useful response',
notUseful: 'Not useful response',
thanks: 'Thanks for the feedback!',
},
},
supportalNoAccess: {
title: 'Not so fast',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ const translations: TranslationDeepObject<typeof en> = {
prompt: '¿Quieres que te avisemos cuando Concierge responda?',
cta: 'Notificar',
},
feedback: {prompt: '¿Te ha sido útil esta respuesta?', useful: 'Respuesta útil', notUseful: 'Respuesta poco útil', thanks: '¡Gracias por tus comentarios!'},
},
supportalNoAccess: {
title: 'No tan rápido',
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ const translations: TranslationDeepObject<typeof en> = {
collapseReasoning: 'Réduire le raisonnement',
expandReasoning: 'Développer le raisonnement',
enableNotifications: {prompt: 'Vous souhaitez être averti lorsque Concierge répond ?', cta: 'Notifier'},
feedback: {prompt: 'Cette réponse était-elle utile ?', useful: 'Réponse utile', notUseful: 'Réponse inutile', thanks: 'Merci pour votre retour !'},
},
supportalNoAccess: {
title: 'Pas si vite',
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ const translations: TranslationDeepObject<typeof en> = {
collapseReasoning: 'Comprimi ragionamento',
expandReasoning: 'Espandi ragionamento',
enableNotifications: {prompt: 'Vuoi ricevere una notifica quando Concierge risponde?', cta: 'Notifica'},
feedback: {prompt: 'Questa risposta è stata utile?', useful: 'Risposta utile', notUseful: 'Risposta non utile', thanks: 'Grazie per il tuo feedback!'},
},
supportalNoAccess: {
title: 'Non così in fretta',
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@ const translations: TranslationDeepObject<typeof en> = {
prompt: 'Conciergeから返信があったときに通知を受け取りますか?',
cta: '通知',
},
feedback: {prompt: 'この回答は役に立ちましたか?', useful: '役に立つ返信', notUseful: '役に立たない返信', thanks: 'フィードバックをありがとうございます!'},
},
supportalNoAccess: {
title: 'ちょっと待ってください',
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ const translations: TranslationDeepObject<typeof en> = {
collapseReasoning: 'Redenering inklappen',
expandReasoning: 'Redenering uitklappen',
enableNotifications: {prompt: 'Wil je een melding krijgen wanneer Concierge reageert?', cta: 'Melden'},
feedback: {prompt: 'Was dit antwoord nuttig?', useful: 'Nuttig antwoord', notUseful: 'Niet nuttig antwoord', thanks: 'Bedankt voor je feedback!'},
},
supportalNoAccess: {
title: 'Niet zo snel',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ const translations: TranslationDeepObject<typeof en> = {
collapseReasoning: 'Zwiń rozumowanie',
expandReasoning: 'Rozwiń rozumowanie',
enableNotifications: {prompt: 'Chcesz otrzymywać powiadomienia, gdy Concierge odpowie?', cta: 'Powiadom'},
feedback: {prompt: 'Czy ta odpowiedź była pomocna?', useful: 'Przydatna odpowiedź', notUseful: 'Nieprzydatna odpowiedź', thanks: 'Dzięki za opinię!'},
},
supportalNoAccess: {
title: 'Nie tak szybko',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ const translations: TranslationDeepObject<typeof en> = {
collapseReasoning: 'Recolher raciocínio',
expandReasoning: 'Expandir raciocínio',
enableNotifications: {prompt: 'Quer ser avisado quando o Concierge responder?', cta: 'Notificar'},
feedback: {prompt: 'Essa resposta foi útil?', useful: 'Resposta útil', notUseful: 'Resposta inútil', thanks: 'Obrigado pelo feedback!'},
},
supportalNoAccess: {
title: 'Calma aí',
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,7 @@ const translations: TranslationDeepObject<typeof en> = {
prompt: '希望在Concierge回复时收到通知吗?',
cta: '通知',
},
feedback: {prompt: '这个回复有用吗?', useful: '有用回复', notUseful: '无用回复', thanks: '感谢你的反馈!'},
},
supportalNoAccess: {
title: '先别急',
Expand Down
2 changes: 2 additions & 0 deletions src/libs/DebugUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ function validateReportDraftProperty(key: keyof Report | keyof ReportNameValuePa
case 'welcomeMessage':
case 'origin':
case 'originalID':
case 'conciergeFeedbackForReportActionID':
case 'submitterUserID':
case 'submitterPayrollID':
case 'orderDealNumbers':
Expand Down Expand Up @@ -705,6 +706,7 @@ function validateReportDraftProperty(key: keyof Report | keyof ReportNameValuePa
transactionCount: CONST.RED_BRICK_ROAD_PENDING_ACTION,
origin: CONST.RED_BRICK_ROAD_PENDING_ACTION,
originalID: CONST.RED_BRICK_ROAD_PENDING_ACTION,
conciergeFeedbackForReportActionID: CONST.RED_BRICK_ROAD_PENDING_ACTION,
});
case 'expensify_text_title':
return validateObject<ObjectElement<ReportNameValuePairs, 'expensify_text_title'>>(value, {
Expand Down
1 change: 1 addition & 0 deletions src/libs/EmojiUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,7 @@ export {
extractEmojis,
getAddedEmojis,
isFirstLetterEmoji,
findEmojiByName,
hasAccountIDEmojiReacted,
getRemovedSkinToneEmoji,
getSpacersIndexes,
Expand Down
26 changes: 26 additions & 0 deletions src/libs/ReportActionsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1752,6 +1752,31 @@ function isOlderReportAction(a: ReportAction, b: ReportAction): boolean {
return isNewerReportAction(b, a);
}

/**
* Returns the ID of the newest Concierge comment that can show the feedback prompt.
* The comment has to be in Onyx because the Concierge greeting and the streaming draft are built on the client and cannot hold a reaction.
*
* @param sortedVisibleReportActions - visible report actions sorted newest first
* @param persistedReportActionIDs - IDs of the report actions stored in Onyx
*/
function getLatestConciergeFeedbackActionID(sortedVisibleReportActions: ReportAction[], persistedReportActionIDs: string[]): string | undefined {
const latestConciergeComment = sortedVisibleReportActions.find(
(action) =>
isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT) &&
action.actorAccountID === CONST.ACCOUNT_ID.CONCIERGE &&
!isDeletedAction(action) &&
!isWhisperAction(action) &&
// A failed comment does not exist on the server, so a reaction on it cannot be saved
isEmptyObject(action.errors),
);

if (!latestConciergeComment || !persistedReportActionIDs.includes(latestConciergeComment.reportActionID)) {
return undefined;
}

return latestConciergeComment.reportActionID;
}

/**
* The first visible action is the second last action in sortedReportActions which satisfy following conditions:
* 1. That is not pending deletion as pending deletion actions are kept in sortedReportActions in memory.
Expand Down Expand Up @@ -5116,6 +5141,7 @@ export {
getCombinedReportActions,
getDismissedViolationMessageText,
getFirstVisibleReportActionID,
getLatestConciergeFeedbackActionID,
getIOUActionForReportID,
getIOUActionForTransactionID,
getIOUReportIDFromReportActionPreview,
Expand Down
6 changes: 6 additions & 0 deletions src/pages/inbox/report/ReportActionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ type ReportActionItemProps = {

shouldUseThreadDividerLine?: boolean;
shouldDisplayContextMenu?: boolean;

/** Whether this is the newest Concierge comment eligible for the inline feedback prompt */
isLatestConciergeFeedbackAction?: boolean;

linkedTransactionRouteError?: Errors;

/** Whether to show border for MoneyRequestReportPreviewContent */
Expand Down Expand Up @@ -167,6 +171,7 @@ function ReportActionItem({
isThreadReportParentAction = false,
shouldUseThreadDividerLine = false,
shouldDisplayContextMenu = true,
isLatestConciergeFeedbackAction = false,
parentReportActionForTransactionThread,
linkedTransactionRouteError: linkedTransactionRouteErrorProp,
shouldShowBorder,
Expand Down Expand Up @@ -633,6 +638,7 @@ function ReportActionItem({
shouldShowBorder={shouldShowBorder}
isOnSearch={isOnSearch}
setIsPaymentMethodPopoverActive={setIsPaymentMethodPopoverActive}
isLatestConciergeFeedbackAction={isLatestConciergeFeedbackAction}
/>
{Permissions.canUseLinkPreviews() && !isHidden && (action.linkMetadata?.length ?? 0) > 0 && (
<View style={hasDraft ? styles.chatItemReactionsDraftRight : {}}>
Expand Down
10 changes: 10 additions & 0 deletions src/pages/inbox/report/ReportActionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigat
import TransitionTracker from '@libs/Navigation/TransitionTracker';
import {
getFirstVisibleReportActionID,
getLatestConciergeFeedbackActionID,
getReportActionHtml,
getReportActionMessage,
isConsecutiveActionMadeByPreviousActor,
Expand Down Expand Up @@ -126,6 +127,7 @@ function ReportActionsListContent({reportID, conciergeChat, onLayout}: ReportAct
isConciergeHiddenHistory,
showFullHistory,
hasPreviousMessages,
allReportActionIDs,
} = useReportActionsListState();

const {setTreatAsNoPaginationAnchor, loadOlderChats, loadNewerChats, handleShowPreviousMessages} = useReportActionsListActions();
Expand Down Expand Up @@ -329,6 +331,12 @@ function ReportActionsListContent({reportID, conciergeChat, onLayout}: ReportAct

const firstVisibleReportActionID = getFirstVisibleReportActionID(sortedReportActions, isOffline);

// Skip inside the thread the backend opens after a thumbs down, while a Concierge answer is still streaming, and while newer actions are not loaded because the newest reply may not be in the list yet
const latestConciergeFeedbackActionID =
reportNameValuePairs?.conciergeFeedbackForReportActionID || isDraftPendingCompletion || hasNewerActions
? undefined
: getLatestConciergeFeedbackActionID(renderedVisibleReportActions, allReportActionIDs);

useFollowActionBadgeTarget({
isProduction,
reportID,
Expand Down Expand Up @@ -386,6 +394,7 @@ function ReportActionsListContent({reportID, conciergeChat, onLayout}: ReportAct
shouldDisplayNewMarker={reportAction.reportActionID === unreadMarkerReportActionID}
shouldDisplayReplyDivider={renderedVisibleReportActions.length > 1}
isFirstVisibleReportAction={firstVisibleReportActionID === reportAction.reportActionID}
isLatestConciergeFeedbackAction={!!latestConciergeFeedbackActionID && latestConciergeFeedbackActionID === reportAction.reportActionID}
shouldUseThreadDividerLine={shouldUseThreadDividerLine}
isHarvestCreatedExpenseReport={isHarvestCreatedExpenseReportAction}
shouldDisableContextMenuForConciergeDraft={shouldDisableContextMenuForConciergeDraft}
Expand All @@ -411,6 +420,7 @@ function ReportActionsListContent({reportID, conciergeChat, onLayout}: ReportAct
draftReportActionID,
draftMessageHTML,
isDraftPendingCompletion,
latestConciergeFeedbackActionID,
];

const listHeaderComponent = (
Expand Down
6 changes: 6 additions & 0 deletions src/pages/inbox/report/ReportActionsListItemRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ type ReportActionsListItemRendererProps = {

shouldDisplayReplyDivider: boolean;
isFirstVisibleReportAction: boolean;

/** Whether this is the newest Concierge comment eligible for the inline feedback prompt */
isLatestConciergeFeedbackAction?: boolean;

shouldUseThreadDividerLine?: boolean;

/** Animate highlight action in few seconds */
Expand Down Expand Up @@ -67,6 +71,7 @@ function ReportActionsListItemRenderer({
parentReportActionForTransactionThread,
isHarvestCreatedExpenseReport = false,
shouldDisableContextMenuForConciergeDraft = false,
isLatestConciergeFeedbackAction = false,
}: ReportActionsListItemRendererProps) {
const originalMessage = useMemo(() => getOriginalMessage(reportAction), [reportAction]);

Expand Down Expand Up @@ -176,6 +181,7 @@ function ReportActionsListItemRenderer({
shouldHighlight={shouldHighlight}
isHarvestCreatedExpenseReport={isHarvestCreatedExpenseReport}
shouldDisplayContextMenu={!shouldDisableContextMenuForConciergeDraft}
isLatestConciergeFeedbackAction={isLatestConciergeFeedbackAction}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ type ActionContentRouterProps = {
/** Whether the search-page UI is active */
isOnSearch: boolean;

/** Whether this is the newest Concierge comment eligible for the inline feedback prompt */
isLatestConciergeFeedbackAction: boolean;

setIsPaymentMethodPopoverActive: (value: boolean) => void;
isTrackIntentUser?: boolean;
};
Expand All @@ -155,6 +158,7 @@ function ActionContentRouter({
isOnSearch,
setIsPaymentMethodPopoverActive,
isTrackIntentUser,
isLatestConciergeFeedbackAction,
}: ActionContentRouterProps): React.JSX.Element | null {
const {translate, formatTravelDate} = useLocalize();
const styles = useThemeStyles();
Expand Down Expand Up @@ -559,6 +563,7 @@ function ActionContentRouter({
isHidden={isHidden}
updateHiddenState={updateHiddenState}
isOnSearch={isOnSearch}
isLatestConciergeFeedbackAction={isLatestConciergeFeedbackAction}
/>
);
}
Expand Down
Loading
Loading