diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-04 18:10:39 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-05 13:32:26 +0200 |
| commit | 89ed51e9b7ea38bd4475ce3a234f8c783b3d9b09 (patch) | |
| tree | a6982fe43a43d31771beb124befac61627afb654 /packages/meshbay-client | |
| parent | f808cf0ecaa3c6db023b1496f4b09cca5ca934f9 (diff) | |
| download | meshbay-89ed51e9b7ea38bd4475ce3a234f8c783b3d9b09.tar.gz | |
feat(client): minimise to a system tray indicator (GNOME)
A dedicated monochrome button in the nav, immediately left of the notification
bell, hides the window to a tray indicator. Linux only for now; Windows is
being done on that OS, and the capability is declared per platform so the
button never appears where the desktop shows no indicator -- there it would
hide the window for good.
Hides, never closes: `window-all-closed` quits the app, so closing here would
make "minimise" mean "exit" and drop the session, the transfers and the node
connection. `second-instance` now calls the same restore path, since focusing
a hidden window does nothing visible.
The context menu is not decoration. Under libappindicator -- how GNOME shows a
tray at all, via the AppIndicator extension -- `tray.on('click')` never fires;
the indicator only opens its menu. A tray whose sole affordance was a click
would be inert on the one desktop this targets. The click handler is kept for
desktops that do send it.
Menu labels come from the renderer with the IPC call: the locale files are the
interface's, the main process has no i18n, and a second string table is how two
of them start disagreeing. English fallbacks if none arrive.
The icon lives in src/, not build/: package.json `files` packages only `src/**`
and `ui/**`, so an icon under build/ is present in a dev run and missing from
every installed one. Monochrome, stroked, matching the nav glyph.
Verified on this host: Ubuntu GNOME with ubuntu-appindicators@ubuntu.com and
libayatana-appindicator3 present, so the indicator has somewhere to appear.
Not yet run end to end.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtfG7z6wHWj8RKHCvxQtY1
Diffstat (limited to 'packages/meshbay-client')
| -rw-r--r-- | packages/meshbay-client/src/main.js | 68 | ||||
| -rw-r--r-- | packages/meshbay-client/src/preload.js | 7 | ||||
| -rw-r--r-- | packages/meshbay-client/src/tray-icon.png | bin | 0 -> 415 bytes | |||
| -rw-r--r-- | packages/meshbay-client/src/tray-icon@2x.png | bin | 0 -> 755 bytes |
4 files changed, 70 insertions, 5 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index e996829..71b56be 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -20,7 +20,8 @@ 'use strict'; -const { app, BrowserWindow, dialog, ipcMain, protocol, safeStorage, shell } = +const { app, BrowserWindow, dialog, ipcMain, Menu, protocol, safeStorage, + shell, Tray } = require('electron'); const { execFile, spawn } = require('node:child_process'); const crypto = require('node:crypto'); @@ -326,6 +327,56 @@ const HUB_FETCH_TIMEOUT_MS = 30000; let mainWindow = null; +// ── System tray ────────────────────────────────────────────────────────────── +// Linux only for now; Windows is being done on that OS. +// +// The context menu is not optional. Under libappindicator -- which is how GNOME +// shows a tray at all, via the AppIndicator extension -- `tray.on('click')` +// never fires: the indicator only opens its menu. A tray whose only affordance +// was a click would be inert on the one desktop this targets. +// +// Labels arrive from the renderer rather than being translated here. The locale +// files are the interface's, the main process has no i18n, and a second string +// table is how two of them start disagreeing. +let tray = null; + +function showFromTray() { + if (!mainWindow) return; + if (!mainWindow.isVisible()) mainWindow.show(); + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); +} + +function ensureTray(labels) { + const text = { + show: (labels && labels.show) || 'Show MeshBay', + quit: (labels && labels.quit) || 'Quit', + }; + if (tray) { + // Relabel in place: the person may have changed language since it was made. + tray.setContextMenu(Menu.buildFromTemplate([ + { label: text.show, click: showFromTray }, + { type: 'separator' }, + { label: text.quit, click: () => app.quit() }, + ])); + return tray; + } + // In src/, not build/: electron-builder packages only `src/**` and `ui/**` + // (package.json `files`), so an icon under build/ exists in a dev run and is + // missing from every installed one. + tray = new Tray(path.join(__dirname, 'tray-icon.png')); + tray.setToolTip('MeshBay'); + tray.setContextMenu(Menu.buildFromTemplate([ + { label: text.show, click: showFromTray }, + { type: 'separator' }, + { label: text.quit, click: () => app.quit() }, + ])); + // Harmless where it does nothing (GNOME), useful on desktops that do send it. + tray.on('click', showFromTray); + return tray; +} + + function createWindow() { const bounds = (config.window && config.window.width) ? config.window : { width: 1200, height: 800, @@ -541,6 +592,16 @@ function registerBridge() { } catch { return null; } }); + // Hide, never close: `window-all-closed` quits the app, and closing here would + // make "minimise to tray" mean "exit". A hidden window keeps the session, the + // transfers and the node connection exactly as they were. + ipcMain.handle('window:minimize-to-tray', (_e, labels) => { + if (process.platform !== 'linux' || !mainWindow) return false; + ensureTray(labels); + mainWindow.hide(); + return true; + }); + ipcMain.handle('device:ensure', () => ensureDeviceKey()); ipcMain.handle('device:public', () => { const key = deviceKey(); @@ -1477,10 +1538,7 @@ if (!app.requestSingleInstanceLock()) { app.quit(); } else { app.on('second-instance', () => { - if (mainWindow) { - if (mainWindow.isMinimized()) mainWindow.restore(); - mainWindow.focus(); - } + showFromTray(); }); app.whenReady().then(() => { diff --git a/packages/meshbay-client/src/preload.js b/packages/meshbay-client/src/preload.js index 97a5063..1aa16e5 100644 --- a/packages/meshbay-client/src/preload.js +++ b/packages/meshbay-client/src/preload.js @@ -41,8 +41,15 @@ contextBridge.exposeInMainWorld('meshbay', { localFolders: true, nativeSave: true, lanCast: true, + // Linux only for now -- Windows is being done on that OS. Declared per + // platform rather than always: a button that hides the window somewhere the + // desktop shows no indicator would hide it for good. + tray: process.platform === 'linux', }, + // Labels are passed in because the main process has no i18n; see main.js. + minimizeToTray: (labels) => ipcRenderer.invoke('window:minimize-to-tray', labels), + // Ask the main process to call the hub. The renderer has an `app://` origin, // which CORS refuses and which is not a credential anyway. fetch: (url, init) => ipcRenderer.invoke('hub:fetch', url, init), diff --git a/packages/meshbay-client/src/tray-icon.png b/packages/meshbay-client/src/tray-icon.png Binary files differnew file mode 100644 index 0000000..394604d --- /dev/null +++ b/packages/meshbay-client/src/tray-icon.png diff --git a/packages/meshbay-client/src/tray-icon@2x.png b/packages/meshbay-client/src/tray-icon@2x.png Binary files differnew file mode 100644 index 0000000..3e533fb --- /dev/null +++ b/packages/meshbay-client/src/tray-icon@2x.png |