Skip to content

feat: add 👍/👎 feedback prompt under the latest Concierge response - #100454

Open
oqildev wants to merge 11 commits into
Expensify:mainfrom
oqildev:feat/96775-concierge-feedback-prompt
Open

feat: add 👍/👎 feedback prompt under the latest Concierge response#100454
oqildev wants to merge 11 commits into
Expensify:mainfrom
oqildev:feat/96775-concierge-feedback-prompt

Conversation

@oqildev

@oqildev oqildev commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

Renders Was that response useful? 👍 👎 beneath the newest Concierge-authored comment. Each thumb writes a real emoji reaction and nothing else — the backend reads the reaction and, on a thumbs down, opens the feedback thread itself. Because the prompt is gated on that same reaction it resolves optimistically, stays resolved across reloads, and becomes eligible again if the user retracts the reaction from the pill row.

Eligibility is decided by getLatestConciergeFeedbackActionID, which requires the action to exist in Onyx rather than merely look like a Concierge comment. Two client-built actions are shaped identically to a finished answer — the greeting from buildConciergeGreetingReportAction and the paced reply from buildConciergeDraftReportAction — but never reach Onyx, and toggleEmojiReaction bails on both, so a prompt on either would render buttons that do nothing. The list also stands down while a synthetic draft is on screen, so the prompt never lands on a half-written answer.

Two open points from the design discussion, neither blocking:

  1. Tooltip. The design comment asked whether these need one and it was never settled. Implemented with tooltips (Useful response / Not useful response — only the thumbs-up label was mocked, so the other is the symmetric counterpart). Removing them is a one-liner.
  2. Hover contrast. Sampling the mock gives hoverComponentBG, and that is what I shipped first. Measured in the running app it is #F2EDE7 on #FCFBF91.13:1, which is not perceivable on a 28px target at 100% zoom (the mock reads clearly because it is a ~2.2× enlargement). I moved to buttonDefaultBG (1.26:1), the fill the add-reaction bubble beside this prompt already uses, so the state is actually visible. No neutral fill in the light palette reaches 3:1 — the strongest is buttonPressedBG at 1.76:1 — so if a more prominent state is wanted, a 1px border would read far better than any fill. Happy to switch back to hoverComponentBG if mock fidelity is preferred.

Fixed Issues

$ #96775
PROPOSAL: #96775 (comment)

Tests

  1. Open any chat with Concierge and ask a question, e.g. How do I submit an expense?
  2. Verify no prompt appears under the greeting or under your own message
  3. Wait for the reply — verify Was that response useful? 👍 👎 appears beneath it
  4. Tap 👍 — verify a real 👍 1 reaction pill appears, the prompt is replaced by Thanks for the feedback!, and the thanks fades after a few seconds
  5. Ask another question — verify the prompt moves to the new reply and the older one keeps only its reaction pill
  6. Tap 👎 — verify a real 👎 1 pill appears and no thread is created by the client (the N Replies indicator arrives from the backend)
  7. Reload — verify the prompt stays hidden on both messages
  8. Remove the reaction from the pill — verify the prompt becomes eligible again
  9. Web only: hover a thumb and verify the tooltip and the background fill; repeat in dark mode
  • Verify that no errors appear in the JS console

Offline tests

  1. Go offline
  2. Tap 👍 on the latest Concierge reply
  3. Verify the reaction pill appears and the prompt hides immediately — toggleEmojiReaction writes optimistically

QA Steps

Same as Tests.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
android-native.mp4
Android: mWeb Chrome
android-mweb.mp4
iOS: Native
ios-native.mp4
iOS: mWeb Safari
ios-mweb.MOV
MacOS: Chrome / Safari
chrome-web.mp4
safari-web.mp4

Renders "Was that response useful?" with two thumbs beneath the newest
Concierge-authored comment. Each thumb writes a real emoji reaction and
nothing else -- the backend reads the reaction and, on a thumbs down,
opens the feedback thread itself. Because the prompt is gated on that
same reaction it resolves optimistically, stays resolved across reloads,
and becomes eligible again if the user retracts the reaction.

Eligibility is decided by getLatestConciergeFeedbackActionID, which
requires the action to exist in Onyx rather than merely look like a
Concierge comment. Two client-built actions are shaped identically to a
finished answer -- the greeting from buildConciergeGreetingReportAction
and the paced reply from buildConciergeDraftReportAction -- but never
reach Onyx, and toggleEmojiReaction bails on both, so a prompt on either
would render buttons that do nothing. The list also stands down entirely
while a synthetic draft is on screen, since the draft's pacing status
flips independently of the server write.
The prompt was suppressed whenever `isSyntheticDraftVisible` was true. That flag stays true until the
paced draft's HTML equals the persisted action's HTML byte for byte, and the two are produced by
different parsers -- the draft by a client-side `getParsedComment`, the persisted one by the server.
When they never converge the flag never clears, so the prompt stayed hidden for the life of the mount
and only appeared after navigating away and back. Observed live in the Concierge DM.

Gate on the pacing status instead, which is what the neighbouring
`shouldDisableContextMenuForConciergeDraft` already uses and which always settles. The window between
pacing finishing and the server write landing is covered by the helper's Onyx-membership check.

While here, drop the helper's fallback to an older answer. If the newest Concierge comment is not in
Onyx the prompt now shows nothing at all, rather than moving back onto the previous reply and asking
the user to rate a message they were not looking at.
The previous commit swapped this gate to the pacing status on the theory that
`isSyntheticDraftVisible` could stick, based on a misread bug report. It does not stick: the effect
above re-seeds the draft from the persisted action, so the reveal target is the persisted HTML and
the two converge by construction.

The swap also cost something. The pacing status clears before the reveal finishes, so the prompt
could appear under an answer whose text was still animating in -- the case this gate exists to avoid.

Keeps the previous commit's other change, which stands on its own: when the newest Concierge comment
is absent from Onyx the helper returns nothing instead of falling back to an older answer.
Hover was being applied through the `style` callback, which only sees React Native's raw `state.hovered`.
`GenericPressable` merges that with its own tracking and applies the result to `hoverStyle`, so move the
fill there (and to `pressStyle`) instead.

Sampling the hover circle in the design comment gives #F1EDE8 on a #FCFBF9 page, which is
`theme.hoverComponentBG` on `theme.appBG`. The fill is meant to be this quiet, so the token stays.

The tooltip in the same mock reads "Useful response", so rename the strings to match. Only the thumbs up
label is mocked; the thumbs down wording is the symmetric counterpart and is worth confirming with design.
The design comment's hover fill samples as `hoverComponentBG`, but that mock sits on a plain white page.
Over the chat surface the token is indistinguishable from its background -- not visible even at 200% zoom
-- so an affordance that is meant to be seen is not. Use the default button surface instead, which is what
a ghost button takes on hover. Worth confirming with design, and a one line change to put back.

The glyph also sat high in the square. It was borrowing the reaction pill's text style, whose line height
is tuned for a container sized by its own padding. Stretching that line box to the thumb's height centres
the box but not the emoji inside it, since emoji carry a tall ascent. Dropping the line height lets the
text keep its natural size and hands centring back to the container's flex alignment.
Reverts the previous commit's switch to the default button surface. The reason given for it -- that the
chat surface is warmer than the page background and swallows `hoverComponentBG` -- was never measured:
neither `chatItem` nor `chatContentScrollView` sets a background, so both inherit `appBG`, which is the
same surface the design comment's mock sits on.

That leaves the design's own token as the right one, and the codebase agrees: `SearchFiltersClearButton`
is the same shape -- transparent, 28px, pill radius -- and hovers to `styles.hoveredComponentBG`. Use that
style directly rather than a local copy of it, so the two stay in step.

The fill is faint at 100% zoom. That is worth raising with design, not worth diverging over.

Keeps the glyph centring fix from the previous commit.
The design comment asked for a hover state on these thumbs. Matching the mock's sampled fill exactly gives
`hoverComponentBG`, which measures 1.13:1 against the chat surface (both verified from the running app:
surface rgb(252,251,249), fill rgb(242,237,231)). On a 28px target that is below what the eye resolves, so
the state Danny asked for does not read at all -- fidelity to the swatch at the cost of the feature.

`buttonDefaultBG` is 1.26:1 and is already the fill on the add-reaction bubble that sits beside this
prompt, so the thumbs match their neighbour and the hover is visible. No neutral fill in the light palette
reaches 3:1 -- the strongest is `buttonPressedBG` at 1.76:1 -- so this is as far as the token set goes.

Worth confirming with design, along with whether a 1px border would suit better: an edge reads far more
readily than a fill at the same contrast.
@oqildev
oqildev requested review from a team as code owners September 6, 2026 17:25
@melvin-bot

melvin-bot Bot commented Sep 6, 2026

Copy link
Copy Markdown

Hey, I noticed you changed src/languages/en.ts in a PR from a fork. For security reasons, translations are not generated automatically for PRs from forks.

If you want to automatically generate translations for other locales, an Expensify employee will have to:

  1. Look at the code and make sure there are no malicious changes.
  2. Run the Generate static translations GitHub workflow. If you have write access and the K2 extension, you can simply click: [this button]

Alternatively, if you are an external contributor, you can run the translation script locally with your own OpenAI API key. To learn more, try running:

npx bun ./scripts/generateTranslations.ts --help

Typically, you'd want to translate only what you changed by running npx bun ./scripts/generateTranslations.ts --compare-ref main

@melvin-bot
melvin-bot Bot requested review from DylanDylann and joekaufmanexpensify and removed request for a team and DylanDylann September 6, 2026 17:25
@melvin-bot

melvin-bot Bot commented Sep 6, 2026

Copy link
Copy Markdown

@DylanDylann Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 563aa1e016

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/pages/inbox/report/ReportActionsList.tsx Outdated
Comment thread src/pages/inbox/report/actionContents/ConciergeFeedbackPrompt.tsx Outdated
Opening a report at a deep link or an old unread anchor renders a single pagination window. The newest
Concierge reply inside that window is not the newest in the report, so the prompt attached to it and
invited a rating of an answer the user had already moved past.

Gate the derivation on `hasNewerActions` in both lists that compute it. Covered by a test that fails
without the gate: the same seeded reply is the feedback target on a fully loaded report and is not one
while newer pages are outstanding.

Reported by Codex on the PR.
`renderThumb` built a nested Tooltip/Pressable/Text tree from inside the prompt's body, which hides the
real render tree from the return statement and leaves the thumb's markup tangled with the prompt's
reaction state. Give it explicit props instead, so the prompt reads as what it renders and the thumb owns
its own styling.

Flagged by the coding-standards reviewer as CLEAN-REACT-PATTERNS-4.
@codecov

codecov Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/CONST/index.ts 91.22% <ø> (ø)
src/hooks/useReportActionsListModel.ts 100.00% <ø> (ø)
src/libs/EmojiUtils.tsx 85.64% <ø> (ø)
src/libs/ReportActionsUtils.ts 81.85% <100.00%> (+0.09%) ⬆️
src/pages/inbox/report/ReportActionItem.tsx 90.85% <100.00%> (+0.05%) ⬆️
src/pages/inbox/report/ReportActionsList.tsx 89.20% <100.00%> (+0.15%) ⬆️
...ges/inbox/report/ReportActionsListItemRenderer.tsx 83.33% <100.00%> (+0.98%) ⬆️
...nbox/report/actionContents/ActionContentRouter.tsx 88.82% <ø> (ø)
...inbox/report/actionContents/ChatMessageContent.tsx 100.00% <100.00%> (ø)
src/styles/index.ts 63.86% <ø> (-0.18%) ⬇️
... and 2 more
... and 249 files with indirect coverage changes

@dubielzyk-expensify

Copy link
Copy Markdown
Contributor

Videos look good to me. I'll run an adhoc though.

For the removal flow. I see that the Concierge message has a thread already. Is this expected? I assumed that only showed up when there was a 👎 added. Also do we even want this ability? I guess if it doesn't change mess with our data in anyways and only shows the latest rating then it's fine? cc @Expensify/design as well

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

🚧 dubielzyk-expensify has triggered a test Expensify/App build. You can view the workflow run here.

@dubielzyk-expensify

Copy link
Copy Markdown
Contributor

What's the spacing between thumbs? 12px? If so can we make it 8? If not, then just leave it.

CleanShot 2026-09-07 at 10 31 02@2x

It also doesn't create a thread on the 👎 , is that expected?

CleanShot 2026-09-07 at 10 31 59@2x

Nevermind. It did appear, but I wouldn't expect to see the 👍 and 👎 on the Please let us know what we can improve. message itself:

CleanShot 2026-09-07 at 10 33 00@2x

@shawnborton

Copy link
Copy Markdown
Contributor

Nevermind. It did appear, but I wouldn't expect to see the 👍 and 👎 on the Please let us know what we can improve. message itself:

Hmm yeah I think this was my understanding as well. Agree with the rest of your comments though.

…thumbs

A thumbs down makes the backend open a thread on the rated message and post its own request for detail
into it. That request is a Concierge comment like any other, so it became the newest one in the thread and
the prompt attached to it -- the user was asked to rate being asked for feedback. Recognise the thread by
the reaction the user left on its parent action and render nothing there.

The thumbs also sat about 10px further apart than the mock. The row's gap separates the label from the
thumbs, which it should, but it was also pushing the two thumbs apart on top of the padding each 28px
target already carries. Group them so they sit flush, as they do in the mock: glyph to glyph goes from
22px to 14px, against the mock's 12.3px.
@oqildev

oqildev commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

@dubielzyk-expensify @shawnborton — all three are addressed in b1501bf.

1 - 👍/👎 on Please let us know what we can improve.: I agreed, and I now recognise the feedback thread by the reaction on its parent action and render nothing there.

2 - Scope: I kept that narrow rather than suppressing the prompt in every thread, since Concierge also answers questions in threads off the DM, so tell me if you'd rather it never appear in one.

3 - Spacing: I measured it at 22px glyph to glyph, not 12, because the row's 8px gap was pushing the thumbs apart on top of the 7px each 28px target carries.

4 - Spacing fix: I grouped the thumbs so they sit flush like the mock, which takes them to 14px against the mock's 12.3px; a literal 8px would need the targets to overlap, so say the word if you'd rather I shrink them.

5 - Thread on the removal flow: that one is expected, an earlier 👎 opened it before I switched to 👍, and per the issue the thread stays as chat history while only the current rating shows.

@shawnborton

Copy link
Copy Markdown
Contributor

Spacing: I measured it at 22px glyph to glyph, not 12, because the row's 8px gap was pushing the thumbs apart on top of the 7px each 28px target carries.

Can we use button-to-button measurement? What is the gap for button-to-button? If it was 8px and that feels big, let's go down to 4px.

@dannymcclain

Copy link
Copy Markdown
Contributor

Agree with Jon and Shawn's comments. I also think for these ghost thumbs, if the button to button spacing of 4px still feels too airy, it would be fine to make them sit together with a 0px gap (since they can't both be hovered at the same time—you'd never see that they are right up against each other).

@joekaufmanexpensify joekaufmanexpensify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good for product. Excited for this!

@DylanDylann

Copy link
Copy Markdown
Contributor

@oqildev Please use translation from #100454 (comment)

@DylanDylann

Copy link
Copy Markdown
Contributor

@yuwenmemon When I give a dislike, it takes quite a long time for a new feedback thread to open. Could this be a BE issue?

@DylanDylann

Copy link
Copy Markdown
Contributor

Thanks for feedback should be gone immediately after the user unclick the link button, we should display the Was that response useful again

Screen.Recording.2026-09-08.at.16.13.03.mov

@DylanDylann

Copy link
Copy Markdown
Contributor

Thanks for feedback doesn't display when pressing 👎

@DylanDylann

Copy link
Copy Markdown
Contributor

Was that response useful should display below the option list

Screen.Recording.2026-09-08.at.16.16.49.mov

@DylanDylann

Copy link
Copy Markdown
Contributor

@codex review

@DylanDylann

Copy link
Copy Markdown
Contributor

@MelvinBot Please review this PR carefully

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b1501bf057

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/libs/ReportActionsUtils.ts Outdated
Comment on lines +1752 to +1754
const latestConciergeComment = sortedVisibleReportActions.find(
(action) =>
isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT) && action.actorAccountID === CONST.ACCOUNT_ID.CONCIERGE && !isDeletedAction(action) && !isWhisperAction(action),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude failed optimistic Concierge comments

When InitiateBankAccountUnlock fails, its optimistic Concierge ADD_COMMENT remains in REPORT_ACTIONS with errors, so this predicate still selects it even though the server never created the action. The normal context menu and reaction controls explicitly block actions with errors, but this prompt remains active and attempts AddEmojiReaction against the nonexistent server action; a thumbs-up also displays a misleading success acknowledgement. Exclude actions with errors from feedback eligibility.

Useful? React with 👍 / 👎.

// A thumbs down makes the backend open a thread on the rated message and post its own request for
// detail into it. That request is a Concierge comment like any other, so without this the newest
// comment in the feedback thread is the request itself and the user is asked to rate being asked.
const isFeedbackThread = hasReactedWithEmoji(thumbsUp, parentReactions, currentUserAccountID) || hasReactedWithEmoji(thumbsDown, parentReactions, currentUserAccountID);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict feedback-thread detection to negative ratings

When a user gives a Concierge parent message a 👍 and later opens that message's thread to ask a follow-up, every subsequent Concierge reply in the thread loses its feedback prompt because the parent reaction makes isFeedbackThread true. The behavior documented immediately above only creates a backend feedback-detail thread for 👎, so a positive parent reaction is not sufficient to classify an ordinary discussion thread as a feedback thread.

Useful? React with 👍 / 👎.

@MelvinBot

Copy link
Copy Markdown
Contributor

I reviewed the diff at b1501bf. The architecture is sound — gating on a real reaction (rather than new state) is the right call, keeping the "is latest" computation in the list matches the issue's guidance, and the persistedReportActionIDs guard against the client-built greeting/draft is a genuinely good catch. All CI is green except the reviewer checklist.

Three things I'd block on, one of which nobody has raised yet.

1. Thanks for the feedback! survives a retracted reaction (already reported by DylanDylann)

Root cause confirmed: ConciergeFeedbackPrompt.tsx:128 checks hasThanked before the reaction gate, and hasThanked is only cleared by the 4s timer. So after a 👍, removing the reaction from the pill row leaves the thanks on screen for up to 4 more seconds instead of restoring the prompt. The issue's spec is explicit that retracting makes the message eligible again.

The ordering comment is right that you can't just swap the two gates — you'd flash the prompt for a frame before the optimistic reaction lands. Clear hasThanked on the transition instead: once the user's reaction has been observed as present, clear hasThanked the moment it goes away.

Same code path causes a second, quieter problem: rate() sets hasThanked unconditionally at line 123, but toggleEmojiReaction bails silently when getOriginalReportID can't resolve. In that case the user sees "Thanks for the feedback!", nothing is written, and the prompt quietly returns 4s later.

2. Prompt renders above the option list (already reported by DylanDylann)

Confirmed in the JSX: the prompt is placed before ChatActionableButtons at ChatMessageContent.tsx:119-132. Swapping the two blocks fixes it.

3. The feedback-thread suppression only works for the user who reacted — new

ConciergeFeedbackPrompt.tsx:139 decides "am I in a feedback thread?" by asking whether the current user reacted to the parent action. The fix for dubielzyk-expensify's and shawnborton's report therefore fails open in two cases:

  • Group chats and workspace rooms. The issue notes the backend keeps one shared feedback thread per comment, created by the first reactor. Another member opens that thread, has no reaction on the parent, and gets 👍/👎 rendered under Please let us know what we can improve. — the exact thing that was flagged.
  • Whenever the parent action's reactions aren't in Onyx. parentReactions reads a separate collection key; open the thread from the LHN or a push notification on a fresh session before the parent chat loads and the gate reads undefined. Worth confirming that OpenReport on a thread hydrates reportActionsReactions_<parentReportActionID>.

A gate that doesn't depend on who reacted or on reaction data being loaded would be more robust — for example, suppress the prompt whenever the report's parent action is Concierge-authored. That does mean the prompt never appears in a Concierge thread; given dannymcclain, dubielzyk-expensify and shawnborton all reacted to the thread rendering as wrong, that seems like the outcome they want, but it's worth confirming rather than leaving the narrow version in.

Non-blocking

Thanks for the feedback! on 👎 is a spec question, not a bug. DylanDylann asked for it; the issue's acceptance criteria say 👍 shows the transient thanks and 👎 just hides the prompt, which is what the code does. Worth getting design to settle it rather than changing it silently.

Unnecessary Set over every action in the report. ReportActionsList.tsx:338 builds new Set(allReportActionIDs) to answer exactly one membership question, and rebuilds it whenever any report action changes. Finding the candidate first and then doing a single allReportActionIDs.includes(id) avoids the allocation entirely.

Test coverage is on the helper, not the component. getLatestConciergeFeedbackActionID is well covered, but there is no test for ConciergeFeedbackPrompt itself — so none of items 1-3 above would have been caught. Tests for retract-clears-thanks, the feedback-thread gate, and the render order relative to ChatActionableButtons would lock in the fixes.

Brittle positional assertion. DRAFT_PENDING_EXTRA_DATA_INDEX = 4 in tests/ui/ReportActionsListTest.tsx is correct today, but any future insertion into extraData makes the assertion silently check the wrong element instead of failing. Asserting against the value's position from the end, or matching on the array contents, is more durable.

findEmojiByName is typed as returning Emoji but is an unchecked table lookup. Safe here because '+1' / '-1' are hardcoded and exist, but exporting it widens the surface of a function whose type lies. Not worth changing in this PR.


Reply with @MelvinBot <feedback> if you want me to dig into any of these further.

Thanks for the reviews.

The acknowledgement was driven by the press rather than by the reaction. It now requires both, which fixes
two things at once: retracting the reaction from the pill row brings the prompt straight back instead of
leaving the thanks up until the timer fires, and a press that `toggleEmojiReaction` silently declined no
longer thanks the user for something it never wrote.

The prompt also sat above the option list; it now renders after it.

Eligibility now skips Concierge comments carrying errors. An optimistic comment whose write failed keeps
its place in the report, and the reaction row and context menu both refuse to act on those -- the server
has no such action to react to.
@oqildev

oqildev commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

@DylanDylann @MelvinBot — four of these are fixed in 29d0414, and two need a decision from you.

1 - Thanks for the feedback! surviving a retracted reaction: I tied the acknowledgement to the reaction rather than to the press, so removing it from the pill row brings the prompt straight back instead of waiting out the timer.

2 - Thanks shown when nothing was written: the same change covers it, since a press toggleEmojiReaction silently declines never sets the reaction and so never thanks the user.

3 - Prompt above the option list: I moved it to render after ChatActionableButtons.

4 - Failed optimistic Concierge comments: I excluded actions carrying errors from eligibility, matching what the reaction row and context menu already do.

5 - Feedback-thread gate failing open in group chats: I agree with the analysis, and I would rather move to suppressing whenever the parent action is Concierge-authored, but that removes the prompt from every Concierge thread, so I would like @dubielzyk-expensify and @shawnborton to confirm that is the outcome they want.

6 - Thanks for the feedback! on 👎: the acceptance criteria give the thanks to 👍 only and let 👎 just hide the prompt, which is what the code does, so I would rather design settle it than change it quietly.

7 - Translations: I do not have an OpenAI key, so the locale strings in this PR are hand-written; could someone with write access run the Generate static translations workflow to replace them.

8 - Thumb spacing: it is 0px button to button now, which is the flush option @dannymcclain said was fine, so tell me if you would rather have the 4px @shawnborton suggested.

@shawnborton

Copy link
Copy Markdown
Contributor

I don't think I am following - can you rephrase and explain it in simpler terms?

@oqildev

oqildev commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Sorry @shawnborton — that was written for the engineers. Here are the only three questions I need from design, in plain terms.

1 - Should 👍👎 ever appear inside a thread? I hid it in the feedback thread that opens after a 👎, which was the fix for Jon's screenshot, but that only holds for the person who left the 👎 — in a group chat, someone else opening that same thread would still see 👍👎 under Please let us know what we can improve.. The version that always holds is to never show it in a thread off a Concierge message, and I want to check that is what you want before I take it away from ordinary Concierge replies in threads too.

2 - How far apart should the thumbs sit? They are touching now, which is the option @dannymcclain said was fine, and you suggested 4px — I will use whichever you prefer.

3 - Should 👎 also say Thanks for the feedback!? Today only 👍 does and 👎 just hides the prompt, which is what the issue asks for, but @DylanDylann expected 👎 to thank the user as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants