feat: close-to-tray, meeting lobby, speaker select, link expiry, mobile RC touch (0.1.14/batch72)

General #1/#2 (closed-app notifications): the desktop app now CLOSES TO TRAY instead
of quitting, keeping its chat WebSocket alive so calls/messages still notify. Tray
icon + menu (Open / Quit), single-instance lock, first-close hint.

Guest #4 (lobby/admit): meetings can require the host to admit guests joining by link.
Setting on the schedule form ("Guests must be admitted by the host", default on) +
ad-hoc default. Guests wait on a "waiting to be let in" screen; the host gets an
Admit/Deny prompt; auto-cleanup on leave. Logged-in members always join directly.

Guest #5 (speaker): headphones/speaker output picker in the meeting (setSinkId),
remembered and applied to every tile.

Guest #3 (link expiry): guest link/token dies ~2h after a scheduled meeting's end
(HTTP 410) with a clear message; live-room links expire when the room empties.

RC #4 (mobile): touch→mouse mapping so a phone/tablet viewer can control (tap=click,
drag=move). Uses the same letterbox-correct coordinate mapping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 14:44:58 +05:30
parent bd488b3286
commit d0f9355553
9 changed files with 208 additions and 32 deletions
+51 -3
View File
@@ -8,7 +8,7 @@
// - external links open in the user's browser, not inside the app
//
// Server origin is configurable so the same build works against prod or a dev server.
const { app, BrowserWindow, session, desktopCapturer, shell, Menu, MenuItem, ipcMain, nativeImage, Notification } = require('electron');
const { app, BrowserWindow, session, desktopCapturer, shell, Menu, MenuItem, Tray, ipcMain, nativeImage, Notification } = require('electron');
const path = require('path');
const fs = require('fs');
const os = require('os');
@@ -190,6 +190,30 @@ const SERVER_URL = (process.env.SERVER_URL || (app.isPackaged ? 'https://remote.
let win;
let splash;
let tray = null;
let isQuitting = false; // true only during a real Quit (tray menu / before-quit) — otherwise close = hide to tray
// Close-to-tray: closing the window HIDES it instead of quitting, so the app keeps running in the
// background with its chat WebSocket alive. That's what lets call/message notifications still fire when
// the window is "closed" (General #1/#2) — a fully-quit Electron app gets no push. The tray icon + menu
// bring it back or quit for real.
function createTray() {
if (tray) return;
try {
let img = nativeImage.createFromPath(path.join(__dirname, 'tray.ico'));
if (img.isEmpty()) img = nativeImage.createFromPath(path.join(process.resourcesPath || __dirname, 'tray.ico'));
tray = new Tray(img.isEmpty() ? nativeImage.createEmpty() : img);
tray.setToolTip('Biz Connect');
const showApp = () => { if (!win) return createWindow(); if (win.isMinimized()) win.restore(); win.show(); win.focus(); };
tray.setContextMenu(Menu.buildFromTemplate([
{ label: 'Open Biz Connect', click: showApp },
{ type: 'separator' },
{ label: 'Quit', click: () => { isQuitting = true; app.quit(); } },
]));
tray.on('click', showApp); // single-click (Windows)
tray.on('double-click', showApp);
} catch (_) { tray = null; }
}
// A tiny brand-blue splash (splash.html) shown while the web UI loads, so launch feels instant
// and on-brand instead of a blank window. Closed as soon as the main window is ready to show.
@@ -233,6 +257,18 @@ function createWindow() {
win.once('ready-to-show', reveal);
setTimeout(reveal, 12000);
// Close = hide to tray (keep running for notifications). First time, tell the user where it went.
let toldTray = false;
win.on('close', (e) => {
if (isQuitting) return; // real quit → let it close
e.preventDefault();
win.hide();
if (!toldTray && Notification.isSupported()) {
toldTray = true;
try { const n = new Notification({ title: 'Biz Connect is still running', body: 'It stays in the system tray so you keep getting calls & messages. Quit from the tray icon.' }); n.show(); } catch (_) {}
}
});
// Open the landing page (same entry as the website): the "before login" screen with the
// no-login "Share my screen" option + sign-in. It redirects logged-in users straight to /home.
win.loadURL(SERVER_URL + '/');
@@ -376,12 +412,22 @@ function configureSession() {
} catch (_) {}
}
// Single-instance: a tray app must not spawn a second copy. If another launch happens, focus the
// existing window (restoring it from the tray) instead.
if (!app.requestSingleInstanceLock()) {
app.quit();
} else {
app.on('second-instance', () => { if (win) { if (win.isMinimized()) win.restore(); win.show(); win.focus(); } });
}
app.on('before-quit', () => { isQuitting = true; });
app.whenReady().then(() => {
configureSession();
createSplash();
createWindow();
createTray(); // keep the app reachable while its window is hidden to tray
Menu.setApplicationMenu(null); // hide the default menu bar; the web UI is the chrome
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); });
app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); else if (win) { win.show(); win.focus(); } });
// Check for shell updates on launch, then every 6 hours. Only in packaged builds.
if (app.isPackaged && autoUpdater) {
// #3: surface update progress to the web UI so the user can SEE an update is downloading /
@@ -405,4 +451,6 @@ app.whenReady().then(() => {
}
});
app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
// With close-to-tray the window is HIDDEN, not destroyed, so this normally won't fire while the app is
// meant to keep running. Only quit here if we're actually quitting (belt-and-braces).
app.on('window-all-closed', () => { if (isQuitting && process.platform !== 'darwin') app.quit(); });
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "biz-connect-desktop",
"version": "0.1.13",
"version": "0.1.14",
"description": "Biz Connect technician desktop client — loads the Connect web UI with native screen capture",
"author": {
"name": "BizGaze",
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB