Skip to content

feat: sys tray offline icon#3342

Open
tyroneyeh wants to merge 8 commits into
RocketChat:masterfrom
tyroneyeh:master_SysTray_offline
Open

feat: sys tray offline icon#3342
tyroneyeh wants to merge 8 commits into
RocketChat:masterfrom
tyroneyeh:master_SysTray_offline

Conversation

@tyroneyeh
Copy link
Copy Markdown

@tyroneyeh tyroneyeh commented May 25, 2026

Closes #2457

image

Summary by CodeRabbit

  • New Features
    • Tray icon shows distinct visuals for authenticated vs unauthenticated states on macOS, Windows, and Linux (badge-based when logged in; offline visuals when not).
    • App detects when all servers are logged out, brings the window forward, updates the tray, and displays a critical notification prompting re-authentication.

Review Change Stack

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 25, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Tray icon asset selection now reflects login status across all platforms. Platform-specific helpers return offline icons when logged out; otherwise they return badge-based icons. A new watcher detects logged-out transitions, restores/focuses the root window, displays a critical notification, and updates the tray icon accordingly.

Changes

Logged-out tray icon state and user notification

Layer / File(s) Summary
Icon asset selection contract and platform implementations
src/ui/main/icons.ts
macOS, Windows, and Linux tray icon helpers now accept isLoggedIn and return offline icons when logged out. The exported getTrayIconPath signature is extended with optional isLoggedIn (default false) and passes it through the platform switch.
Tray icon update and logged-out detection
src/ui/main/trayIcon.ts
Electron Notification is imported. updateTrayIconImage computes login state from servers and passes it to getTrayIconPath. A new watcher detects logged-out transitions, restores/focuses the root window, shows a critical notification, and updates the icon. The watcher is cleaned up on teardown.
Root window tray icon call site
src/ui/main/rootWindow.ts
The getTrayIconPath call for tray/taskbar icon updates now passes isLoggedIn: true, ensuring the logged-in icon variant is selected.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

type: feature

🚥 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 PR title directly and concisely describes the main feature: adding an offline icon to the system tray, which is the primary objective.
Linked Issues check ✅ Passed The PR implements the core feature from #2457 by detecting offline status and updating tray icons with OS-specific offline variants, plus notifying users when logged out.
Out of Scope Changes check ✅ Passed All changes in the three modified files (icons.ts, rootWindow.ts, trayIcon.ts) are directly related to implementing the offline tray icon feature requested in #2457.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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


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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/ui/main/trayIcon.ts (1)

151-154: 💤 Low value

Minor: Simplify the regex for stripping HTML tags.

The escape \/ is unnecessary inside a character class. A cleaner pattern:

-          body: t('error.authNeeded', { auth: '' }).replace(
-            /<[\/]?strong>/g,
-            ''
-          ),
+          body: t('error.authNeeded', { auth: '' }).replace(/<\/?strong>/g, ''),
🤖 Prompt for 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.

In `@src/ui/main/trayIcon.ts` around lines 151 - 154, In the trayIcon.ts
assignment where body is built using t('error.authNeeded', { auth: ''
}).replace(...), simplify the regex used to strip <strong> tags by removing the
unnecessary escape and character class; update the argument to replace to use
the cleaner pattern (e.g., replace the current /<[\/]?strong>/g with a simpler
/<\/?strong>/g) so the call in that .replace(...) is clearer and equivalent.
🤖 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/ui/main/icons.ts`:
- Line 31: Add the missing semicolon after the function declaration that ends
with the final closing brace shown in the diff (the function in icons.ts that
closes with “}”); update the file by appending a semicolon immediately after
that closing brace so the function declaration ends with “};” to satisfy ESLint.

In `@src/ui/main/trayIcon.ts`:
- Around line 66-72: The current isLoggedIn uses servers.every(...) which
returns true for an empty array, causing a false "logged in" state; change the
computation to consider empty servers as not logged in and align with the
watcher logic — e.g. set isLoggedIn to servers.length > 0 &&
servers.every((server) => server.userLoggedIn) (or, if you prefer the "offline
if any logged out" behavior, compute offline = servers.length === 0 ||
servers.some((s) => !s.userLoggedIn) and pass isLoggedIn = !offline) and update
the code paths that use servers and getTrayIconPath so both the tray
initialization and the watcher use the same predicate.
- Line 159: The call to updateTrayIconImage(trayIcon, undefined) is executed
unconditionally on watcher triggers and sends badge: undefined, which can
erroneously clear the badge; move this call inside the isLoggedOut conditional
so it only runs when isLoggedOut is true (or remove it entirely and rely on
unwatchGlobalBadge to update the tray), ensuring updateTrayIconImage is invoked
with the real badge value when logging in/out and that unwatchGlobalBadge
continues to handle badge-driven updates.

---

Nitpick comments:
In `@src/ui/main/trayIcon.ts`:
- Around line 151-154: In the trayIcon.ts assignment where body is built using
t('error.authNeeded', { auth: '' }).replace(...), simplify the regex used to
strip <strong> tags by removing the unnecessary escape and character class;
update the argument to replace to use the cleaner pattern (e.g., replace the
current /<[\/]?strong>/g with a simpler /<\/?strong>/g) so the call in that
.replace(...) is clearer and equivalent.
🪄 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: c644bd8f-3ad5-40db-a10a-bb147513bc44

📥 Commits

Reviewing files that changed from the base of the PR and between c2b584b and a983e3d.

⛔ Files ignored due to path filters (5)
  • src/public/images/tray/darwin/offline.png is excluded by !**/*.png
  • src/public/images/tray/darwin/offline@2x.png is excluded by !**/*.png
  • src/public/images/tray/linux/offline.png is excluded by !**/*.png
  • src/public/images/tray/linux/offline@2x.png is excluded by !**/*.png
  • src/public/images/tray/win32/offline.ico is excluded by !**/*.ico
📒 Files selected for processing (3)
  • src/ui/main/icons.ts
  • src/ui/main/rootWindow.ts
  • src/ui/main/trayIcon.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript for all new code in this codebase unless explicitly told otherwise
Use Fuselage components from @rocket.chat/fuselage for all UI work — only create custom components when Fuselage doesn't provide the needed functionality
Check Theme.d.ts for valid color tokens when working with Fuselage components
Use optional chaining with fallbacks for platform-specific APIs instead of mocks (e.g., process.getuid?.() ?? 1000) to ensure code works across all platforms without requiring mocks
TypeScript code must use strict mode
Use React functional components with hooks instead of class components
Redux actions must follow the FSA (Flux Standard Action) pattern
Use camelCase for file naming
Use PascalCase for component file names (React components)
Write self-documenting code through clear naming — avoid unnecessary comments

Files:

  • src/ui/main/rootWindow.ts
  • src/ui/main/icons.ts
  • src/ui/main/trayIcon.ts
🪛 ESLint
src/ui/main/icons.ts

[error] 31-31: Insert ;

(prettier/prettier)

🔇 Additional comments (3)
src/ui/main/icons.ts (1)

19-30: LGTM!

Also applies to: 33-47, 49-63, 65-87

src/ui/main/trayIcon.ts (1)

1-1: LGTM!

Also applies to: 134-161, 208-213

src/ui/main/rootWindow.ts (1)

436-445: LGTM!

Comment thread src/ui/main/icons.ts Outdated
Comment thread src/ui/main/trayIcon.ts
Comment thread src/ui/main/trayIcon.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/ui/main/trayIcon.ts (1)

141-159: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Refresh tray icon on both login-state transitions, not only logged-out.

Line 158 updates the image only when isLoggedOut is true. After re-authentication, if the badge value does not change, the icon may remain offline until another badge update occurs.

Proposed fix
   const unwatchUserLoggedIn = watch(
@@
-    async (isLoggedOut) => {
+    async (isLoggedOut) => {
+      const globalBadge = select(selectGlobalBadge);
+      updateTrayIconImage(trayIcon, globalBadge);
+
       if (isLoggedOut) {
         const rootWindow = await getRootWindow();
         if (rootWindow) {
@@
         new Notification({
@@
         }).show();
-        updateTrayIconImage(trayIcon, undefined);
       }
     }
   );
🤖 Prompt for 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.

In `@src/ui/main/trayIcon.ts` around lines 141 - 159, The tray icon refresh is
only triggered when isLoggedOut is true, so move the call to
updateTrayIconImage(trayIcon, undefined) out of the if block (or add an else
that calls it) so it runs for both logout and login transitions; update the
async (isLoggedOut) => handler that calls getRootWindow() and creates the
Notification to ensure updateTrayIconImage is invoked regardless of isLoggedOut
to prevent the icon staying stale after re-authentication.
🤖 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.

Outside diff comments:
In `@src/ui/main/trayIcon.ts`:
- Around line 141-159: The tray icon refresh is only triggered when isLoggedOut
is true, so move the call to updateTrayIconImage(trayIcon, undefined) out of the
if block (or add an else that calls it) so it runs for both logout and login
transitions; update the async (isLoggedOut) => handler that calls
getRootWindow() and creates the Notification to ensure updateTrayIconImage is
invoked regardless of isLoggedOut to prevent the icon staying stale after
re-authentication.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d2e9b0d8-76bc-49e2-a939-a4eb4b3e7745

📥 Commits

Reviewing files that changed from the base of the PR and between a983e3d and 49f4ab5.

📒 Files selected for processing (2)
  • src/ui/main/icons.ts
  • src/ui/main/trayIcon.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript for all new code in this codebase unless explicitly told otherwise
Use Fuselage components from @rocket.chat/fuselage for all UI work — only create custom components when Fuselage doesn't provide the needed functionality
Check Theme.d.ts for valid color tokens when working with Fuselage components
Use optional chaining with fallbacks for platform-specific APIs instead of mocks (e.g., process.getuid?.() ?? 1000) to ensure code works across all platforms without requiring mocks
TypeScript code must use strict mode
Use React functional components with hooks instead of class components
Redux actions must follow the FSA (Flux Standard Action) pattern
Use camelCase for file naming
Use PascalCase for component file names (React components)
Write self-documenting code through clear naming — avoid unnecessary comments

Files:

  • src/ui/main/icons.ts
  • src/ui/main/trayIcon.ts
🔇 Additional comments (1)
src/ui/main/icons.ts (1)

31-31: LGTM!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/ui/main/trayIcon.ts (1)

141-160: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve badge state when refreshing tray icon on auth transitions.

At Line 159, refreshing with undefined can clear badge-based tray variants even when unread state exists. Use the current global badge when forcing this refresh.

Suggested fix
-      updateTrayIconImage(trayIcon, undefined);
+      const globalBadge = select(selectGlobalBadge);
+      updateTrayIconImage(trayIcon, globalBadge);
🤖 Prompt for 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.

In `@src/ui/main/trayIcon.ts` around lines 141 - 160, When handling the
isLoggedOut flow in the async callback, calling updateTrayIconImage(trayIcon,
undefined) clears any badge-based tray variant; change the call to pass the
current global badge state instead of undefined (e.g., replace undefined with
the module/global badge variable used to track unread/badge state) so
updateTrayIconImage(trayIcon, <currentBadgeVariable>) preserves badge visuals
during auth transitions.
🤖 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.

Duplicate comments:
In `@src/ui/main/trayIcon.ts`:
- Around line 141-160: When handling the isLoggedOut flow in the async callback,
calling updateTrayIconImage(trayIcon, undefined) clears any badge-based tray
variant; change the call to pass the current global badge state instead of
undefined (e.g., replace undefined with the module/global badge variable used to
track unread/badge state) so updateTrayIconImage(trayIcon,
<currentBadgeVariable>) preserves badge visuals during auth transitions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 556473b7-5c21-4532-82b8-d02774afb309

📥 Commits

Reviewing files that changed from the base of the PR and between 49f4ab5 and 889dd26.

📒 Files selected for processing (1)
  • src/ui/main/trayIcon.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript for all new code in this codebase unless explicitly told otherwise
Use Fuselage components from @rocket.chat/fuselage for all UI work — only create custom components when Fuselage doesn't provide the needed functionality
Check Theme.d.ts for valid color tokens when working with Fuselage components
Use optional chaining with fallbacks for platform-specific APIs instead of mocks (e.g., process.getuid?.() ?? 1000) to ensure code works across all platforms without requiring mocks
TypeScript code must use strict mode
Use React functional components with hooks instead of class components
Redux actions must follow the FSA (Flux Standard Action) pattern
Use camelCase for file naming
Use PascalCase for component file names (React components)
Write self-documenting code through clear naming — avoid unnecessary comments

Files:

  • src/ui/main/trayIcon.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/ui/main/trayIcon.ts (1)

142-148: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle getRootWindow() rejection in the logged-out watcher.

At Line 143, a rejected getRootWindow() can break this watcher callback and prevent the notification/icon refresh path from completing. Wrap it in try/catch (or .catch) and continue safely.

Proposed fix
     async (isLoggedOut) => {
       if (isLoggedOut) {
-        const rootWindow = await getRootWindow();
-        if (rootWindow) {
-          if (rootWindow.isMinimized()) rootWindow.restore();
-          rootWindow.show();
-          rootWindow.focus();
-        }
+        try {
+          const rootWindow = await getRootWindow();
+          if (rootWindow.isMinimized()) rootWindow.restore();
+          rootWindow.show();
+          rootWindow.focus();
+        } catch {
+          // Root window may not be ready/destroyed; continue with notification/icon update.
+        }
         new Notification({
🤖 Prompt for 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.

In `@src/ui/main/trayIcon.ts` around lines 142 - 148, The watcher callback that
runs when isLoggedOut can throw if getRootWindow() rejects, so wrap the await
getRootWindow() call in a try/catch (or attach .catch) inside the logged-out
branch to swallow/log the error and continue execution so the rest of the
notification/icon refresh path still runs; update the code around the
isLoggedOut block that calls getRootWindow() to handle rejection safely (use
getRootWindow, rootWindow.restore(), rootWindow.show(), rootWindow.focus()
references) and ensure the catch logs the error and does not rethrow.
🤖 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.

Outside diff comments:
In `@src/ui/main/trayIcon.ts`:
- Around line 142-148: The watcher callback that runs when isLoggedOut can throw
if getRootWindow() rejects, so wrap the await getRootWindow() call in a
try/catch (or attach .catch) inside the logged-out branch to swallow/log the
error and continue execution so the rest of the notification/icon refresh path
still runs; update the code around the isLoggedOut block that calls
getRootWindow() to handle rejection safely (use getRootWindow,
rootWindow.restore(), rootWindow.show(), rootWindow.focus() references) and
ensure the catch logs the error and does not rethrow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c3be9576-e66d-452e-bb76-2a7160c86abe

📥 Commits

Reviewing files that changed from the base of the PR and between 0e77c04 and 3e6dda2.

📒 Files selected for processing (1)
  • src/ui/main/trayIcon.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript for all new code in this codebase unless explicitly told otherwise
Use Fuselage components from @rocket.chat/fuselage for all UI work — only create custom components when Fuselage doesn't provide the needed functionality
Check Theme.d.ts for valid color tokens when working with Fuselage components
Use optional chaining with fallbacks for platform-specific APIs instead of mocks (e.g., process.getuid?.() ?? 1000) to ensure code works across all platforms without requiring mocks
TypeScript code must use strict mode
Use React functional components with hooks instead of class components
Redux actions must follow the FSA (Flux Standard Action) pattern
Use camelCase for file naming
Use PascalCase for component file names (React components)
Write self-documenting code through clear naming — avoid unnecessary comments

Files:

  • src/ui/main/trayIcon.ts
🔇 Additional comments (1)
src/ui/main/trayIcon.ts (1)

151-151: LGTM!

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.

[Feature request] Indicate that there is no connection to server in tray icon

2 participants