Skip to content

feat(menu): register CmdOrCtrl+, accelerator for Settings on all platforms#3336

Draft
jeanfbrito wants to merge 27 commits into
masterfrom
feat/preferences-accelerator-linux
Draft

feat(menu): register CmdOrCtrl+, accelerator for Settings on all platforms#3336
jeanfbrito wants to merge 27 commits into
masterfrom
feat/preferences-accelerator-linux

Conversation

@jeanfbrito
Copy link
Copy Markdown
Member

Upstream Asks: RocketChat/Rocket.Chat.Electron

Formal proposal for RC Electron maintainers — suitable as a GitHub issue or PR cover letter.


1. Context

We maintain mOSdat — an automated multi-OS desktop UI test harness for
RocketChat/Rocket.Chat.Electron. We run full end-to-end functional scenarios against
built Electron packages on Ubuntu 22.04, Ubuntu 24.04, and Fedora 40 VMs, driven by a
VLM (vision-language model) for visual verification and xdotool/xdg-open for
interaction.

The catalyst for these asks is the PR3325 telephony feature test campaign
(2026-05-16), which exposed three systemic friction points that cost us ~3.5 person-hours
of debug across 6 rerun cycles. We went 0/5 → 5/5 green only after building a
workaround layer (--inject-config, ~340 LOC + 27 tests) to bypass the Settings UI
entirely. The asks below would let us delete that layer.


2. Problem Statement

Issue Cost we paid Repro
No Ctrl+, accelerator on Linux ~2h debug + 4 failed test passes menuBar.ts Settings item has no accelerator: field; keypress silently ignored on Linux
Sidebar kebab popup transient ~1h debug VLM localize call returns coords; by the time xdotool click fires, popup has dismissed; scenario retries loop
Webview steals alt+w ~30min debug Chromium consumes alt+menu sequence when a form (login) holds focus; Settings never opens
No state-seed CLI Forced manual config.json wrangling Required us to build the entire mosdat functional --inject-config layer (I1: 5 flags, ~340 LOC, 27 tests) to write Redux-persisted values before app launch

All four issues share a root cause: the only supported path to set persisted
Electron state is through the in-app Settings UI
, which is too transient and
keyboard-inaccessible for automated testing on Linux.


3. Three Asks (Ranked by ROI)

A. Register CmdOrCtrl+, accelerator on the Settings menu item

Impact: Eliminates the Ctrl+,-has-no-binding footgun on Linux. macOS users
already expect this convention; Linux gets it as a free backfill.

Effort: 1 line in menuBar.ts (or equivalent menu registration file). No behavior
change — the click handler stays identical.

Blocks our current alt+w → Down → Return fallback nav, which we had to abandon
because alt+w is consumed by the webview when a form is focused.

B. Add data-testid attributes to key interactive elements

Impact: Eliminates VLM coordinate guessing for stable UI elements. Current
workflow: VLM localize is asked to find an element, returns pixel coordinates, then
xdotool click fires. For transient elements (kebab popup, toggle buttons) this races.
With stable data-testid selectors we can use xdotool search --name or Playwright
page.getByTestId() instead.

Effort: Approximately 1 day — add data-testid strings, no logic changes.

Elements needed (intended DOM target in parentheses):

  • sidebar-kebab-button — the / kebab icon in the sidebar header
  • settings-menu-item — the Settings entry in the kebab/app menu
  • telephony-toggle — the master enable/disable toggle in Telephony settings
  • telephony-shortcut-field — the global shortcut input field
  • telephony-server-dropdown — the preferred-server <select> or custom dropdown trigger
  • telephony-server-select-modal — the root node of the server-selection modal
  • telephony-server-option — each server row inside that modal (index or name variant)
  • remember-this-choice-checkbox — the checkbox in the server-select modal

C. Optional state-seed CLI: --load-state <json> and --export-state <path>

Impact: Would eliminate the entire mosdat --inject-config layer. Instead of
writing config.json before launch (which requires knowing the correct
migrations.version to avoid a migration wipe), we could pass Redux state directly
to the Electron binary.

Effort: Medium. Requires hooking into the Redux store boot sequence before the
first persistReducer rehydration. Main risk: ordering — state must be seeded before
the store hydrates, not after.

--load-state <json>: Merge the given JSON object into the persisted Redux state
at startup, before the store rehydrates. Allows tests to declare initial state without
touching the filesystem.

--export-state <path>: After the app reaches ready state, write the current
Redux-persisted slice to <path> as JSON. Allows snapshot-based assertions.

This is the biggest ask and we understand it may not fit near-term roadmap. A and B
alone would meaningfully reduce our test friction.


4. Concrete Code Suggestion for Ask A

In menuBar.ts (or wherever the Settings MenuItem is constructed), add the
accelerator field:

{
  id: 'preferences',
  label: t('preferences'),
  accelerator: 'CmdOrCtrl+,',
  click: () => dispatch({ type: SIDE_BAR_SETTINGS_BUTTON_CLICKED }),
}

CmdOrCtrl maps to Command on macOS and Control on Linux/Windows.
No platform guard needed — Electron handles the mapping.

If a global shortcut conflict exists on any platform, CommandOrControl+, is the
standard Electron convention for Preferences (used by VS Code, Slack, Discord on macOS
and increasingly on Linux).


5. Concrete data-testid Targets for Ask B

Attribute value Intended DOM target
sidebar-kebab-button <button> that opens the sidebar overflow / kebab menu
settings-menu-item <li> or <button> for the Settings entry in that menu
telephony-toggle <input type="checkbox"> or toggle root for the Enable Telephony setting
telephony-shortcut-field <input type="text"> for the keyboard shortcut accelerator string
telephony-server-dropdown Trigger element of the preferred-server dropdown
telephony-server-select-modal Root <div> of the TelephonyServerSelectModal
telephony-server-option Each server row <li> or <button> inside the modal
remember-this-choice-checkbox <input type="checkbox"> for the "remember this choice" option

We are not prescribing attribute format — data-testid, data-qa-id, or any
consistent naming convention your codebase already uses would work.


6. What We'll Do If Accepted

If asks A and B are merged upstream we will open a follow-up mOSdat PR that:

  1. Removes the alt+w menu-navigation fallback from all PR3325-class scenarios
    (currently present in 3325-master-toggle.yaml, 3325-global-shortcut.yaml, and
    3325-cold-start.yaml).
  2. Replaces VLM localize calls for kebab + Settings item with deterministic
    xdotool search --classname or Playwright getByTestId steps.

If ask C is accepted we will open a further PR that replaces the entire
automation/setup/inject_config.py layer (I1, ~340 LOC) with a thin wrapper that
emits --load-state instead of writing config.json directly, removing the
migrations.version detection complexity entirely.


7. Contact

We are happy to assist with any of these PRs if the direction is approved.

jeanfbrito added 27 commits May 8, 2026 18:43
Register Rocket.Chat as OS handler for callto: and tel: URL schemes
on Windows, macOS, and Linux. When a telephony link is clicked in any
app, RC launches or focuses and dispatches a typed IPC event to the
server webview with the parsed phone number.

- Register callto/tel schemes in electron-builder.json (all platforms)
- Add parseTelephonyLink() with number normalization and callto:// support
- Add performTelephonyCall() with multi-server dialog + remember choice
- Expose onTelephonyCallRequested callback on RocketChatDesktop API
- Persist telephonyPreferredServer via selectPersistableValues
- IPC listener registered before onReady to avoid cold-start race
Tests cover parseTelephonyLink (tel:/callto: protocols, number
normalization, callto:// double-slash format, extension syntax,
edge cases) and performTelephonyCall (0/1/2+ servers, preferred
server persistence, dialog remember checkbox).
…duplicate IPC listener

- Pass getRootWindow() as first argument to dialog.showMessageBox so
  the server selection prompt appears as a modal sheet attached to the
  main window (consistent with all other dialogs in the codebase)
- Add idempotency guard to listenToTelephonyRequests to prevent
  duplicate IPC handler registration during hot-reload dev cycles
Add GitNexus section with impact analysis, query, and context tools.
Gitignore .gitnexus index directory.
Dispatch WEBVIEW_GIT_COMMIT_HASH_CHANGED from server info response.
Match supportedVersions exceptions using sha:<hash> prefix against
the server's git commit hash for per-build version overrides.
Add TelephonyServer component to Settings > General tab with a
Select dropdown to choose which server handles tel:/callto: links.
Hidden when only one server exists. "Auto (ask each time)" option
clears the preference and reverts to dialog behavior.
electron-builder v26 rejects MimeType as a direct child of
linux.desktop — only desktopActions and entry are valid properties.
Move it inside desktop.entry where it belongs.
Replace hardcoded English strings in the telephony dialog with i18n
t() calls and add telephonySelectServer translation keys to all 22
locale files.
…selection

Replace dialog.showMessageBox with an in-app modal that shows server
favicons, names and hostnames — matching the sidebar appearance. Scales
to many servers via a scrollable list and includes a "remember this
choice" checkbox.

Also hardens the telephony flow:
- Mutex prevents concurrent tel: links from opening duplicate modals
- 120s timeout on modal promise prevents hanging if renderer crashes
- 10s timeout on webContents polling prevents infinite loop if server
  is removed between selection and view creation
Drop Margins wrapper from the modal and the Tile container from rows.
Title→message margin x8→x4, message→list x16→x12, rows now use
paddingBlock x6 / paddingInline x8 instead of Tile padding x12.
…ver modal

Adds 20 tests across three new spec files covering the telephony deep-link
runtime path that was previously only validated by deepLinks/main.spec.ts.

- src/telephony/renderer/preload.spec.ts (6 tests):
  IPC bridge state machine — listenToTelephonyRequests guard, pendingPayload
  buffer/replay, callback replacement, ipcRenderer.on registration.

- src/ui/components/SettingsView/features/TelephonyServer.spec.tsx (8 tests):
  Settings dropdown — hide when servers.length <= 1, option generation
  (auto + per-server), value binding to telephonyPreferredServer, dispatch
  of TELEPHONY_PREFERRED_SERVER_SET (null for auto, URL string otherwise),
  hostname fallback when server title missing.

- src/ui/components/TelephonyServerSelectModal/index.spec.tsx (6 tests):
  Modal flow — visibility gating on dialogs.telephonyServerSelect.isOpen,
  ServerItem rendering per server, dispatch payload shape on click with
  rememberChoice on/off, close dispatch with null payload, rememberChoice
  reset after close.

Adds @testing-library/react, @testing-library/jest-dom, and
@testing-library/dom (peer) as devDependencies. Fuselage Select and Dialog
are mocked at module level since they rely on React-Aria and native
<dialog>.showModal() respectively, which don't drive cleanly in
@kayahr/jest-electron-runner's renderer environment.

Spec paths follow the existing renderer testMatch convention:
src/<module>/<subdir>/<file>.spec.tsx — a flat src/telephony/preload.spec.ts
would be silently dropped by jest's testMatch globs.
tel:%2B15551234 left %2B encoded, producing phoneNumber '%2B15551234'
instead of '+15551234'. decodeURIComponent runs before strip pass;
malformed escapes return null (treated same as other invalid input).
…tive

Git commit hashes are conventionally case-insensitive. SHA-bb83777
should match same as sha-bb83777.
- TelephonyServer: extract hostname via safeHostname helper to prevent
  settings page crash on malformed server URLs (new URL() throws).
- TelephonyServerSelectModal: associate 'Remember this choice' label
  with checkbox via htmlFor/id for assistive tech.
- ServerItem: render Tile as native button (is='button' type='button')
  so keyboard users get Tab focus and Enter/Space activation.
* feat(telephony): add global shortcut to dial clipboard number

* fix(telephony): harden global shortcut handling

* refactor(telephony): share dialpad opener

* test(telephony): stabilize shortcut notification click (#3331)

* test(telephony): stabilize shortcut notification click (#3333)
* feat(telephony): add global shortcut to dial clipboard number

* fix(telephony): harden global shortcut handling

* refactor(telephony): share dialpad opener

* test(telephony): stabilize shortcut notification click
Updated the AvailableBrowsers, TelephonyGlobalShortcut, TelephonyServer, and ThemeAppearance components to include a marginBlock of 'x16' on the Field components for improved spacing and layout consistency.
Add `isTelephonyEnabled` setting (default off) to gate the telephony
feature end-to-end:

- New persisted `isTelephonyEnabled` boolean with action, reducer, and
  selector entry; surfaces as a master toggle in Settings > General.
- `TelephonyServer` and `TelephonyGlobalShortcut` controls remain
  visible but disabled while the master toggle is off.
- Global shortcut config selector returns the disabled config when the
  master toggle is off, so the existing watcher auto-unregisters any
  active accelerator on toggle-off.
- `tel:`/`callto:` deep links short-circuit when the master toggle is
  off.
- OS-level protocol registration for `tel`/`callto` moves out of the
  unconditional startup loop into a new reactive
  `setupTelephonyProtocolHandlers`, which calls
  `setAsDefaultProtocolClient` / `removeAsDefaultProtocolClient` in
  response to toggle changes. `rocketchat:` continues to register at
  startup unchanged.

The macOS `Info.plist` and Linux `.desktop` files declared by
electron-builder will still list the app as a candidate handler for
`tel`/`callto`, but it will never be set as default unless the user
opts in at runtime.
DMV-1 calls for a first-run prompt warning the user that Teams, Zoom,
or Skype may already own the tel:/callto: handler and that they must
confirm Rocket.Chat as default in OS settings themselves. Windows 10/11
hash-protects UserChoice so `setAsDefaultProtocolClient` only registers
the app as a candidate; without the prompt, users have no way to know
the OS silently kept the prior default.

Trigger every off->on transition of the master `isTelephonyEnabled`
toggle (not literal first run — the toggle is opt-in and is the natural
moment of user intent). Seed the transition tracker via a synchronous
`select` before subscribing, so returning users who reopen the app
with telephony already enabled are not re-prompted.

- New `TelephonyDefaultHandlerPromptModal` Fuselage modal (title, two
  body paragraphs, "Open System Settings" + "Got it" buttons), mounted
  in Shell next to the existing telephony modal.
- Three new void actions (`_OPEN`, `_CLOSE`, `_OPEN_SETTINGS_CLICKED`)
  and a `telephonyDefaultHandlerPrompt` sub-reducer in `dialogs.ts`.
- Main-process `setupTelephonyDefaultHandlerPrompt` watches the master
  toggle and dispatches OPEN on each off->on flip. Listens for the
  settings-button click and routes per platform:
  - Windows: `shell.openExternal('ms-settings:defaultapps')`
  - macOS: opens FaceTime preferences (where tel: default lives)
  - Linux: spawns `gnome-control-center default-apps` or
    `kcmshell5/6 componentchooser` based on `XDG_CURRENT_DESKTOP`;
    unknown DEs log a tip and rely on the modal's verbal instructions.
- i18n keys under `telephony.defaultHandlerPrompt`.
- 13 new tests covering transition detection, idempotency, teardown,
  and each platform branch.
The renderer Jest project's `testMatch` requires at least one
subdirectory between `src/<module>/` and the spec, so
`src/app/PersistableValues.spec.ts` was silently skipped. Moved the
file under `src/app/__tests__/`, fixed the relative import, and
expanded the migration assertion to cover `isTelephonyEnabled`.
Documented the constraint in `CLAUDE.md` so future renderer specs are
placed correctly.
…forms

Previously Settings had no accelerator field, so the macOS-convention
Preferences shortcut (Cmd+,) was unbound and Linux/Windows users had
no keyboard shortcut. Add 'CmdOrCtrl+,' so the standard shortcut works
everywhere.

Surfaced by external test tooling (mOSdat) that needed a deterministic
keyboard path to open Settings on Linux.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 17, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 147706c9-f7ed-40ab-bead-48d17f73ceab

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

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.

1 participant