Skip to content

ECHOES-1357 Aggregate repeated same-text toasts of the same variety with a counter badge#693

Open
david-cho-lerat-sonarsource wants to merge 1 commit into
mainfrom
david/ECHOES-1357-aggregate-repeated-toasts
Open

ECHOES-1357 Aggregate repeated same-text toasts of the same variety with a counter badge#693
david-cho-lerat-sonarsource wants to merge 1 commit into
mainfrom
david/ECHOES-1357-aggregate-repeated-toasts

Conversation

@david-cho-lerat-sonarsource
Copy link
Copy Markdown
Contributor

@david-cho-lerat-sonarsource david-cho-lerat-sonarsource commented Jun 4, 2026


Summary by Gitar

  • Toast aggregation:
    • Implemented automatic aggregation for repeated identical text toasts lacking an explicit id.
    • Added a repetitionCount badge to toasts that occur multiple times, capping display at 99+.
  • Toast lifecycle management:
    • Introduced registerToastDismissHandler and dismissToast to manage tracking and cleanup of aggregated toasts.
    • Updated toast duration logic to refresh the auto-close timer upon repeat triggers.

This will update automatically on new commits.

@netlify
Copy link
Copy Markdown

netlify Bot commented Jun 4, 2026

Deploy Preview for echoes-react ready!

Name Link
🔨 Latest commit 406cf77
🔍 Latest deploy log https://app.netlify.com/projects/echoes-react/deploys/6a22ea9fa3ea7d00081d41b4
😎 Deploy Preview https://deploy-preview-693--echoes-react.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@hashicorp-vault-sonar-prod
Copy link
Copy Markdown

hashicorp-vault-sonar-prod Bot commented Jun 4, 2026

ECHOES-1357

Comment thread src/common/components/Toast.tsx Outdated
Comment thread src/common/components/Toast.tsx
@david-cho-lerat-sonarsource david-cho-lerat-sonarsource force-pushed the david/ECHOES-1357-aggregate-repeated-toasts branch 2 times, most recently from 29fc534 to 00d336b Compare June 4, 2026 13:07
Comment thread src/common/components/Toast.tsx
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds automatic aggregation for repeated “same text + same variety” toast notifications, showing a repetition counter badge instead of rendering duplicate toasts. It extends the toast utility to track repetition state and refresh the auto-close timer on repeats, while updating the Toast UI to display the new counter in an accessible way.

Changes:

  • Implemented automatic same-text toast aggregation in src/utils/toasts.tsx, including deterministic IDs, repetition tracking, and timer refresh behavior.
  • Updated Toast to accept and render a repetitionCount badge (capped at 99+) with a screen-reader announcement, plus added a new i18n key.
  • Added Storybook coverage and unit tests validating aggregation, variety separation, non-text exclusion, counter capping, and timer reset behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
stories/Toast-stories.tsx Adds an “AggregatedRepeatedToasts” story and aligns controls/defaults to the updated toast behavior.
src/utils/toasts.tsx Core aggregation logic: computes aggregation IDs from plain text, tracks repetition state, refreshes duration, and clears state on dismiss/auto-close.
src/utils/tests/toasts-test.tsx Adds unit tests covering aggregation behavior, counter capping, non-text exclusion, and timer refresh.
src/components/badges/BadgeCounter.tsx Broadens badge props to support standard <span> attributes while keeping value as the rendered content.
src/common/components/Toast.tsx Renders repetition counter badge + screen-reader label, and adjusts trailing layout to accommodate badge + dismiss button.
i18n/keys.json Adds toast.repetition-count message used for the repetition announcement.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@david-cho-lerat-sonarsource david-cho-lerat-sonarsource force-pushed the david/ECHOES-1357-aggregate-repeated-toasts branch from c1a873a to 46535ce Compare June 4, 2026 13:29
Comment thread src/utils/toasts.tsx Outdated
@david-cho-lerat-sonarsource david-cho-lerat-sonarsource force-pushed the david/ECHOES-1357-aggregate-repeated-toasts branch from 46535ce to 48c1d64 Compare June 4, 2026 13:53
Comment thread src/utils/toasts.tsx Outdated
Base automatically changed from david/ECHOES-1339-better-document-toast-stable-id to main June 4, 2026 13:59
@david-cho-lerat-sonarsource david-cho-lerat-sonarsource force-pushed the david/ECHOES-1357-aggregate-repeated-toasts branch from 48c1d64 to 2a8f2ea Compare June 4, 2026 14:01
@david-cho-lerat-sonarsource david-cho-lerat-sonarsource force-pushed the david/ECHOES-1357-aggregate-repeated-toasts branch from 2a8f2ea to d2dd6fe Compare June 4, 2026 14:20
Comment thread src/components/badges/BadgeCounter.tsx Outdated

export interface BadgeCounterProps {
className?: string;
export interface BadgeCounterProps extends Omit<ComponentProps<'span'>, 'children'> {
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.

the type was not reflecting what the component was actually accepting. Now we're using it with aria-hidden explicitly, this type is more faithful to the truth.

@david-cho-lerat-sonarsource david-cho-lerat-sonarsource force-pushed the david/ECHOES-1357-aggregate-repeated-toasts branch from d2dd6fe to a707905 Compare June 4, 2026 15:01
Comment thread src/utils/toasts.tsx Outdated
Comment thread src/utils/toasts.tsx Outdated
Comment thread src/utils/toasts.tsx Outdated
@david-cho-lerat-sonarsource david-cho-lerat-sonarsource force-pushed the david/ECHOES-1357-aggregate-repeated-toasts branch from 7a983ca to 3ab6cbe Compare June 5, 2026 07:53
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 5, 2026

Agentic Analysis: Early Results

Agentic Analysis and Context Augmentation are available on your project. Here are some issues that could have been prevented. Follow the links to learn how to put them into action.

2 issue(s) found across 2 file(s):

Rule File Line Message
typescript:S2966 src/utils/toast-internals/__tests__/repeated-toast-tracking-test.tsx 163 Forbidden non-null assertion.
typescript:S4622 src/utils/toast-internals/repeated-toast-tracking.ts 32 Refactor this union type to have less than 3 elements.

Analyzed by SonarQube Agentic Analysis in 3.2 s

@david-cho-lerat-sonarsource david-cho-lerat-sonarsource force-pushed the david/ECHOES-1357-aggregate-repeated-toasts branch from 3ab6cbe to 5a63a0b Compare June 5, 2026 08:03
Comment thread src/utils/toasts.tsx Outdated
@david-cho-lerat-sonarsource david-cho-lerat-sonarsource force-pushed the david/ECHOES-1357-aggregate-repeated-toasts branch from 5a63a0b to 2b049c6 Compare June 5, 2026 08:09
Comment thread src/utils/toasts.tsx Outdated
@sonarqube-next
Copy link
Copy Markdown

sonarqube-next Bot commented Jun 5, 2026

@david-cho-lerat-sonarsource david-cho-lerat-sonarsource force-pushed the david/ECHOES-1357-aggregate-repeated-toasts branch from 16619bd to 64896cb Compare June 5, 2026 14:21
@gitar-bot
Copy link
Copy Markdown

gitar-bot Bot commented Jun 5, 2026

CI failed: The test 'toast utility - automatic aggregation' failed due to a synchronization issue where the toast component fails to update the DOM correctly before the assertion. This is likely a race condition in the test's state management that needs to be resolved with proper act() wrappers.

Overview

One functional test failure was identified in the toast utility suite. The CI build is failing because the should reset the aggregation count after a UI dismiss test cannot locate the expected UI element, indicating a state synchronization issue during the toast lifecycle.

Failures

Toast aggregation test failure (confidence: high)

  • Type: test
  • Affected jobs: 79758691212
  • Related to change: yes
  • Root cause: The test attempts to assert the state of a toast notification immediately after an interaction, likely before the React component has finished its re-render or state update cycle, causing a mismatch in the DOM.
  • Suggested fix: Wrap the toast dismissal interaction and the subsequent assertion in await act(async () => ...) to ensure the DOM is fully updated before query methods attempt to locate the 'Shown 2 times' badge text.

Summary

  • Change-related failures: 1 test failure in the toast utility module.
  • Infrastructure/flaky failures: 0.
  • Recommended action: Update the toast utility test file to properly wrap state-triggering interactions in act to ensure consistent DOM synchronization.
Code Review ✅ Approved 10 resolved / 10 findings

Aggregates repeated text toasts with a capped counter badge and refines lifecycle management to ensure timer consistency. All identified concerns regarding i18n, accessibility, and state management have been successfully resolved.

✅ 10 resolved
Edge Case: Repetition counter badge has no upper bound

📄 src/common/components/Toast.tsx:159-160 📄 src/common/components/Toast.tsx:200 📄 src/components/badges/BadgeCounter.tsx:30 📄 src/components/badges/BadgeCounter.tsx:37
ToastRepetitionCounter is rendered with value={visibleRepetitionCount}, where visibleRepetitionCount is the raw aggregation count (a number). BadgeCounter renders value verbatim and performs no capping. While BadgeCounter supports overflow strings like '23+', the toast path always passes a plain number, so a toast aggregated a large number of times (e.g. hundreds/thousands of rapid repeats) renders the full number, which can stretch the badge and the toast's trailing content layout. Consider capping the displayed value (e.g. show '99+' when visibleRepetitionCount > 99) before passing it to the badge.

Quality: Screen-reader "Repeated {count} times" is off-by-one vs occurrences

📄 src/common/components/Toast.tsx:188-202
visibleRepetitionCount equals the total number of times the toast was shown (count starts at 1 on first display, becomes 2 on the first repeat, etc.). The badge showing the total occurrence count is intuitive, but the screen-reader message Repeated {count} times. announces the same number, which reads as one more than the number of actual repeats (a toast shown 3 times has only been repeated twice). This is a minor accessibility wording nuance; if the intent is to convey total occurrences, consider wording like "Shown {count} times" to avoid ambiguity, or subtract one for the "repeated" phrasing.

Quality: New i18n key 'toast.repetition-count' missing from i18n/keys.json

📄 src/common/components/Toast.tsx:191-199 📄 src/common/components/Toast.tsx:191-200
The new screen-reader message toast.repetition-count is used via intl.formatMessage in Toast.tsx, but it has not been added to i18n/keys.json, the extracted message catalog used by the translation pipeline. Every other toast message (e.g. toast.dismiss, toast.prefix.*) is registered there. Until the key is extracted (via npm run build-intl-keys) and committed, this string cannot be translated and the consistency check in CI may fail. Run the extraction script and commit the regenerated i18n/keys.json.

Quality: hasActiveToast relies on undocumented sonner internals

📄 src/utils/toasts.tsx:387-401
hasActiveToast (src/utils/toasts.tsx:393-401) inspects sonner's internal toast objects via 'dismiss' in toast ? !toast.dismiss : !toast.delete. These dismiss/delete flags are not part of sonner's public, typed API (sonner 2.0.7) — the ternary itself hints that the field varies across versions. This check is load-bearing for the aggregation feature: it is the only mechanism that resets the counter once a toast disappears. If a future sonner upgrade changes this internal shape, the function could silently always-return-false (counter never increments, badge never shown) or always-return-true (stale counts grow across unrelated occurrences). Consider pinning/asserting the sonner version, adding a regression test against this behavior, or wrapping the access defensively with an explicit fallback so a shape change degrades gracefully rather than silently breaking aggregation.

Quality: Timer refresh relies on undocumented Sonner duration behavior

📄 src/utils/toasts.tsx:365-375
getEffectiveDurationValue deliberately adds a toggling durationOffset (0/1 ms) to the numeric duration so that Sonner restarts the auto-close timer when an aggregated toast is updated. The accompanying comment ("Sonner only restarts the auto-close timer when the numeric duration changes") documents the trick, but it depends on internal/undocumented Sonner behaviour rather than a public API contract. A future Sonner upgrade that changes how duration changes are detected (e.g. comparing object identity, debouncing, or restarting on every update) would silently break timer refresh: aggregated toasts could auto-close based on the first occurrence's timer instead of being extended on each repeat. The existing test (should refresh the auto-close timer when an aggregated toast is updated) is the only guard. Consider pinning the Sonner version range and/or adding a comment referencing the specific Sonner version this behaviour was verified against, so the fragility is visible during dependency bumps.

...and 5 more resolved from earlier reviews

Tip

Comment Gitar fix CI or enable auto-apply: gitar auto-apply:on

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment on lines +87 to +89
if (!isDefined(titleText) || !isDefined(descriptionText)) {
return undefined;
}
Comment thread src/utils/toasts.tsx
Comment on lines +216 to +220
* **Aggregating repeated toasts**
*
* When no explicit `id` is provided, repeated calls with the same plain-text `title`, the same
* plain-text `description`, and the same `variety` reuse the existing toast instead of creating
* duplicates. The toast keeps its original text and shows a counter badge on the right.
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.

2 participants