Skip to content

fix: restore unread badge on Rocket.Chat 7.8.0+ servers - #3369

Merged
jeanfbrito merged 3 commits into
masterfrom
fix/unread-badge-after-server-7x
Jun 25, 2026
Merged

fix: restore unread badge on Rocket.Chat 7.8.0+ servers#3369
jeanfbrito merged 3 commits into
masterfrom
fix/unread-badge-after-server-7x

Conversation

@jeanfbrito

@jeanfbrito jeanfbrito commented Jun 22, 2026

Copy link
Copy Markdown
Member

Proposed changes

Restores the unread badge (dock, tray, and sidebar server avatar) on servers running Rocket.Chat 7.8.0 or newer, where it had stopped appearing.

Root cause

The desktop app reads the unread badge exclusively from Meteor's reactive Session dict, via injected.ts:

Tracker.autorun(() => {
  const unread = Session.get('unread');
  window.RocketChatDesktop.setBadge(unread);
});

Server PR RocketChat/Rocket.Chat#36001"refactor: remove unread from Meteor" (ARCH-1606), first shipped in 7.8.0 — deleted apps/meteor/client/startup/unread.ts, the only code that wrote Session.set('unread', …). Unread state moved to a React-only store (useUnread hook). On those servers, Session.get('unread') resolves to undefined, so setBadge(undefined) clears the badge across every sink. The desktop code itself was unchanged, so nothing in the desktop changelog pointed at the cause; the break surfaces only as each server independently upgrades past 7.8.0.

Fix

Those servers still broadcast unread state through two global CustomEvents on window (dispatched unconditionally by fireGlobalEventBase), mirroring the server's own useUnread hook:

Event Payload Use here
unread-changed aggregate numeric count recompute trigger / numeric total
unread-changed-by-subscription per-sub { rid, unread, alert, unreadAlert } accumulated to rebuild the alert-only indicator

injected.ts now listens to both and resolves the badge with the same logic useUnread uses: a positive count wins, otherwise an alert-only , otherwise no badge. The unreadAlert user preference is read from Meteor.user() when available and defaults to true (the server's shipped default).

The legacy Session.get('unread') autorun is kept as a fallback for servers older than 7.8.0, so the badge works across both old and new servers.

Known limitation

unread-changed-by-subscription is incremental (fires only when a subscription changes). An alert-only room (zero unread count) that settled before the listener attached may not show its dot until it next changes. Any real unread count always shows immediately. A future server-side change adding the alert state to a global event would remove this edge.

Steps to test or reproduce

  1. Connect the desktop app to a Rocket.Chat 7.8.0+ server and log in.
  2. Receive an unread message → dock/tray/sidebar badge shows the count; clears when read.
  3. (Optional, in the server view DevTools console) verify wiring directly:
    window.dispatchEvent(new CustomEvent('unread-changed', { detail: 3 }));            // shows 3
    window.dispatchEvent(new CustomEvent('unread-changed-by-subscription',
      { detail: { rid: 'x', unread: 0, alert: true, unreadAlert: 'all' } }));
    window.dispatchEvent(new CustomEvent('unread-changed', { detail: 0 }));            // shows •
  4. Connect to a pre-7.8.0 server and confirm the badge still works via the legacy Meteor path.

Summary by CodeRabbit

  • Bug Fixes
    • Improved unread badge behavior by tracking unread counts per subscription and rebuilding the badge state from server updates, ensuring accurate totals across sessions.
    • Refined badge logic to display an alert-style indicator only when appropriate (respecting the user’s unread alert preference) and to automatically clear the badge when there are no unread items.

The dock/tray/sidebar unread badge stopped appearing on servers running
Rocket.Chat 7.8.0 or newer.

Server PR RocketChat/Rocket.Chat#36001 ("refactor: remove `unread` from
Meteor", first shipped in 7.8.0) deleted `client/startup/unread.ts`, which
was the only code writing `Session.set('unread', ...)`. Unread state moved
to a React-only store. The desktop app reads the badge exclusively through
`Tracker.autorun(() => setBadge(Session.get('unread')))` in injected.ts, so
on those servers that Meteor source now resolves to `undefined` forever and
the badge clears across every sink.

Those servers still broadcast unread state through two global CustomEvents
on `window`, mirroring the server's own `useUnread` hook:
- `unread-changed`: the aggregate numeric count (recompute trigger).
- `unread-changed-by-subscription`: per-subscription { rid, unread, alert,
  unreadAlert }, accumulated here to rebuild the alert-only "•" indicator.

injected.ts now listens to both and resolves the badge with the same logic
useUnread uses: positive count wins, otherwise alert-only "•", otherwise no
badge. The legacy Session autorun is kept as a fallback for servers older
than 7.8.0.

Known limitation: `unread-changed-by-subscription` is incremental, so an
alert-only room (zero count) that settled before the listener attached may
not show its dot until it next changes; any real unread count always shows
immediately.
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ebca8373-e336-4119-bf33-f74aa7424b63

📥 Commits

Reviewing files that changed from the base of the PR and between 114da0f and ba87aae.

📒 Files selected for processing (1)
  • src/injected.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/injected.ts
📜 Recent review details
⏰ Context from checks skipped due to timeout. (8)
  • GitHub Check: build (windows-latest, windows)
  • GitHub Check: build (ubuntu-latest, linux)
  • GitHub Check: build (macos-latest, mac)
  • GitHub Check: check (ubuntu-latest)
  • GitHub Check: check (macos-latest)
  • GitHub Check: check (windows-latest)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Analyze (actions)

Walkthrough

src/injected.ts keeps the unread badge on a Session-based path for older servers and uses window events plus per-subscription state on newer servers. It also adds a one-time listener guard and recomputes the badge from accumulated unread data and unreadAlert settings.

Changes

Unread Badge Event-Based Tracking

Layer / File(s) Summary
Setup flag and per-subscription state map
src/injected.ts
Adds setupFlags.unreadChangedEvent and introduces unreadSubscriptions, a Map keyed by subscription id storing unread count plus optional alert and unreadAlert fields.
Version-gated Session badge path
src/injected.ts
Wraps the existing Tracker.autorun badge update in a server version check so Session.get('unread') is only used on pre-7.8.0 servers.
Event listeners and resolveBadge routine
src/injected.ts
Conditionally registers unread-changed-by-subscription and unread-changed window event listeners that update unreadSubscriptions then call resolveBadge. That routine sums unread counts across the map, reads unreadAlert preference (defaulting to true), and calls setBadge with a numeric total, the alert-only string, or null to clear.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: restoring unread badges on Rocket.Chat 7.8.0+ servers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • ARCH-1606: Request failed with status code 401

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/injected.ts`:
- Around line 423-440: The Tracker.autorun block that handles the legacy Meteor
Session reactive dict for unread badges is not gated on server version, allowing
it to run on 7.8.0+ servers and call setBadge(undefined), clearing the badge
value restored by the event-based approach. Wrap the Tracker.autorun block with
a version check to ensure it only executes on pre-7.8.0 servers, preventing the
legacy path from interfering with the newer event-based badge mechanism on
7.8.0+ servers.
- Around line 441-495: The unread badge count is being calculated by summing
only the subscriptions in the unreadSubscriptions map, which misses any rooms
that had unread counts before the event listeners were attached. The
'unread-changed' event listener on line 492 receives the complete aggregate
count but discards it and just calls resolveBadge() which recalculates from the
incomplete map. Modify the 'unread-changed' event listener to extract the
aggregate unread count from the event detail and use that directly instead of
calculating it from the map entries, either by passing the count to
resolveBadge() or by setting the badge directly with the received aggregate
value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 541de790-d119-4173-a13d-1409d17e16b7

📥 Commits

Reviewing files that changed from the base of the PR and between 41b0d7e and 65c8958.

📒 Files selected for processing (1)
  • src/injected.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (4)
  • GitHub Check: check (ubuntu-latest)
  • GitHub Check: check (windows-latest)
  • GitHub Check: build (windows-latest, windows)
  • GitHub Check: build (ubuntu-latest, linux)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: Use TypeScript for all new code unless explicitly told otherwise
Use optional chaining with fallbacks for platform-specific APIs instead of mocking when possible. Example: const uid = process.getuid?.() ?? 1000;

Files:

  • src/injected.ts
**/*.{tsx,ts}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{tsx,ts}: MANDATORY: Use Fuselage components for all UI work. Only create custom components when Fuselage doesn't provide what's needed
Import UI components from @rocket.chat/fuselage and check Theme.d.ts for valid color tokens
Use React functional components with hooks
Use PascalCase for component file names

Files:

  • src/injected.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Redux actions must follow FSA (Flux Standard Action) pattern
Avoid unnecessary comments — write self-documenting code through clear naming
Always verify libraries by checking official docs and .d.ts files in node_modules/. Never assume props, tokens, or APIs work without verification
Avoid subjective descriptors ('smart', 'excellent', 'dumb') in documentation and comments
Use measurable descriptions in code documentation: 'reduced memory usage', 'improved by X%' instead of subjective claims
NEVER invent metrics — don't include estimated time spent or speculated user counts. Only include numbers from actual logs, error messages, or documented sources

Files:

  • src/injected.ts
🔇 Additional comments (1)
src/injected.ts (1)

386-386: LGTM!

Also applies to: 398-406

Comment thread src/injected.ts
Comment thread src/injected.ts Outdated
@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

macOS installer download

Address CodeRabbit review on PR #3369:

- Gate the legacy Session.get('unread') autorun behind a pre-7.8.0 version
  check so it no longer fires setBadge(undefined) on 7.8.0+ servers and
  clobbers the event-based badge.
- Use the authoritative aggregate count carried by the 'unread-changed'
  event instead of the incremental per-subscription map, which could
  undercount rooms unread before the listeners attached.
Satisfy prefer-destructuring lint rule.
@jeanfbrito
jeanfbrito merged commit 5f50b89 into master Jun 25, 2026
12 checks passed
@jeanfbrito
jeanfbrito deleted the fix/unread-badge-after-server-7x branch June 25, 2026 15:16
jeanfbrito added a commit that referenced this pull request Jul 1, 2026
The per-server sidebar tile rendered a literal "0" overlay whenever a
server's unread badge was the number 0.

PR #3369 (restore unread badge on 7.8.0+ servers) made injected.ts emit
setBadge(0) explicitly via `setBadge(alertIndicator ?? 0)`, so servers
now carry `badge: 0` where they previously carried `undefined`.
SideBar/index.tsx maps that to `mentionCount={0}`, and ServerButton
guarded the Badge with `{mentionCount && <Badge>...}`. Because
`0 && <Badge>` evaluates to `0`, React renders the number as a text
node — the stray "0" on the tile.

Coerce the guard to a boolean (`!!mentionCount`) so a zero count renders
nothing. Add a regression test asserting mentionCount={0} produces no
"0" text.

The aggregate badge sinks (selectGlobalBadge, tray title, dock) already
collapse an all-zero state to undefined, so only the per-server tile was
affected and they are left unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant