diff --git a/desktop/main.js b/desktop/main.js index 25eb6e7..fe364b2 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -56,20 +56,26 @@ const APP_ID = 'com.bizgaze.connect.desktop'; // URL (legacy) or an http(s) DP URL, which we download (external photos can't be drawn to a canvas // in the renderer without tainting it, so the renderer now passes the URL straight through). function tmpPngPath() { return path.join(app.getPath('temp'), 'bizc-toast-' + crypto.randomBytes(4).toString('hex') + '.png'); } +// Cache downloaded DPs for the session (keyed by URL) so the SAME sender's photo is instant on the +// next notification — the first one may still show without a photo if the download is slow, but after +// that it's cached. Cached files are NOT deleted after use. +const avatarCache = new Map(); function avatarToTempPng(src) { return new Promise((resolve) => { try { if (!src) return resolve(null); - if (/^data:image\/png;base64,/.test(src)) { const p = tmpPngPath(); fs.writeFileSync(p, Buffer.from(src.split(',')[1], 'base64')); return resolve(p); } + const cached = avatarCache.get(src); + if (cached) { try { if (fs.existsSync(cached)) return resolve(cached); } catch (_) {} avatarCache.delete(src); } + if (/^data:image\/png;base64,/.test(src)) { const p = tmpPngPath(); fs.writeFileSync(p, Buffer.from(src.split(',')[1], 'base64')); avatarCache.set(src, p); return resolve(p); } if (/^https?:\/\//i.test(src)) { const mod = src.startsWith('https') ? require('https') : require('http'); const p = tmpPngPath(); const file = fs.createWriteStream(p); const req = mod.get(src, (res) => { if (res.statusCode !== 200) { res.resume(); file.close(() => { try { fs.unlinkSync(p); } catch (_) {} }); return resolve(null); } - res.pipe(file); file.on('finish', () => file.close(() => resolve(p))); + res.pipe(file); file.on('finish', () => file.close(() => { avatarCache.set(src, p); resolve(p); })); }); req.on('error', () => resolve(null)); - req.setTimeout(2500, () => { try { req.destroy(); } catch (_) {} resolve(null); }); + req.setTimeout(4000, () => { try { req.destroy(); } catch (_) {} resolve(null); }); return; } resolve(null); @@ -96,27 +102,26 @@ ipcMain.handle('reply-notification', async (_e, payload = {}) => { const finish = (v) => { if (done) return; done = true; if (n) { activeNotifs.delete(n); } - if (img) { try { fs.unlinkSync(img); } catch (_) {} } - resolve(v); + resolve(v); // note: img is cached, not deleted }; try { + // No timeoutType:'never' — on Windows that added an unwanted "Close" action button. Windows' + // default toast behavior + our strong reference keep it visible long enough; the in-app call + // popup provides the persistent Join/Decline for calls. n = new Notification({ title: payload.title || 'Biz Connect', body: payload.body || '', icon: img ? nativeImage.createFromPath(img) : undefined, silent: false, - // A call invite stays on screen until clicked/ended; a chat toast uses the default timeout. - timeoutType: payload.persistent ? 'never' : 'default', }); activeNotifs.add(n); // strong ref → toast isn't collected; click stays live n.on('click', () => { if (win) { if (win.isMinimized()) win.restore(); win.show(); win.focus(); } // raise the app finish({ kind: payload.kind, id: payload.id, open: true }); }); - n.on('close', () => finish(null)); // user/system dismissed it → no action (don't force-close) + n.on('close', () => finish(null)); // user/system dismissed it → no action n.show(); - // Safety timeout so the promise never leaks. Calls get the full ring window; chats shorter. - setTimeout(() => { try { if (n) n.close(); } catch (_) {} finish(null); }, payload.persistent ? 45000 : 25000); + setTimeout(() => finish(null), payload.persistent ? 45000 : 25000); // don't leak the promise } catch (_) { finish(null); } }); }); @@ -246,22 +251,10 @@ app.whenReady().then(() => { autoUpdater.on('update-not-available', () => sendUpdate({ phase: 'current' })); autoUpdater.on('download-progress', (p) => sendUpdate({ phase: 'downloading', percent: Math.round((p && p.percent) || 0) })); autoUpdater.on('error', () => sendUpdate({ phase: 'error' })); - // When an update finishes downloading (auto or via the Settings "Check for updates"), offer a - // clear restart prompt instead of only the silent on-next-launch install. - let promptedForUpdate = false; - autoUpdater.on('update-downloaded', async (info) => { - sendUpdate({ phase: 'ready', version: info && info.version }); - if (promptedForUpdate) return; promptedForUpdate = true; - try { - const { dialog } = require('electron'); - const res = await dialog.showMessageBox(win || undefined, { - type: 'info', buttons: ['Restart now', 'Later'], defaultId: 0, cancelId: 1, - title: 'Update ready', message: 'Biz Connect ' + ((info && info.version) || '') + ' is ready.', - detail: 'Restart the app to finish updating.', - }); - if (res.response === 0) autoUpdater.quitAndInstall(); - } catch (_) { /* fall back to install-on-next-launch */ } - }); + // When an update finishes downloading, tell the web UI so it can show a BRANDED "Update ready — + // Restart now" banner (restartToUpdate IPC does the install). No native dialog — that was + // unbranded. It still installs on next launch if the user never clicks Restart. + autoUpdater.on('update-downloaded', (info) => sendUpdate({ phase: 'ready', version: info && info.version })); const check = () => autoUpdater.checkForUpdatesAndNotify().catch(() => {}); check(); setInterval(check, 6 * 60 * 60 * 1000); diff --git a/desktop/package.json b/desktop/package.json index bdee320..3c0b3d8 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "biz-connect-desktop", - "version": "0.1.6", + "version": "0.1.7", "description": "Biz Connect technician desktop client — loads the Connect web UI with native screen capture", "author": { "name": "BizGaze", diff --git a/server/public/home.html b/server/public/home.html index 5fea6bd..e37edd7 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -449,6 +449,9 @@ .upd-banner{position:fixed;left:50%;bottom:16px;transform:translateX(-50%) translateY(70px);z-index:9500;display:none;align-items:center;gap:.5rem;background:var(--blue);color:#fff;border-radius:999px;padding:.5rem .5rem .5rem .9rem;box-shadow:0 10px 30px rgba(20,30,60,.32);font-size:.85rem;font-weight:600;transition:transform .25s ease;} .upd-banner.show{display:flex;transform:translateX(-50%) translateY(0);} .upd-banner button{border:none;background:var(--brand);color:var(--blue-d);border-radius:999px;padding:.35rem .8rem;font-weight:700;cursor:pointer;} + .upd-dot{position:absolute;top:-3px;right:-3px;width:15px;height:15px;border-radius:50%;background:var(--brand);color:var(--blue-d);font-size:.66rem;font-weight:800;font-style:italic;display:grid;place-items:center;border:2px solid var(--blue);pointer-events:none;} + #setUpd.has-update{background:var(--brand);color:var(--blue-d);} + .set-ver-i{display:inline-grid;place-items:center;width:15px;height:15px;border-radius:50%;background:var(--brand);color:var(--blue-d);font-size:.66rem;font-weight:800;font-style:italic;margin-left:.35rem;vertical-align:middle;} .bubble .msg-link{color:inherit;text-decoration:underline;text-underline-offset:2px;word-break:break-word;} .bubble.them .msg-link{color:var(--blue);} .bubble.mine .msg-link{color:#dbe9ff;} .bubble .fwd-label{display:flex;align-items:center;gap:.2rem;font-size:.72rem;font-style:italic;opacity:.7;margin-bottom:.2rem;} @@ -550,6 +553,10 @@ .modal input#grpName:focus,.modal input#giName:focus{outline:none;border-color:var(--brand);} /* Branded confirm dialog (replaces the OS/Electron window.confirm). */ .bz-confirm .bzc-msg{margin:.2rem 0 1.1rem;color:var(--ink);font-size:.92rem;line-height:1.5;} + .seen-modal .seen-list{max-height:44vh;overflow-y:auto;display:flex;flex-direction:column;gap:.2rem;} + .seen-modal .seen-row{display:flex;align-items:center;gap:.6rem;padding:.35rem .3rem;} + .seen-modal .seen-row .mini-av{width:30px;height:30px;flex:0 0 auto;border-radius:50%;display:grid;place-items:center;color:#fff;font-weight:700;font-size:.72rem;} + .seen-modal .seen-row .sn{font-size:.92rem;} .bzc-actions{display:flex;justify-content:flex-end;gap:.5rem;} .bzc-actions button{border:none;border-radius:10px;padding:.55rem 1rem;font-size:.9rem;font-weight:600;cursor:pointer;font-family:inherit;} .bzc-cancel{background:var(--blue-soft);color:var(--blue);} @@ -833,7 +840,7 @@
- @@ -1010,13 +1017,15 @@ function openSettings(){ +sw('setGroup','Group message notifications', notifOn('group')) +sw('setDm','Direct message notifications', notifOn('dm')) +'' - +(window.bizConnectNative?(''):'') + +(window.bizConnectNative?(''):'') +'