fix: seen-by popup (#2), info-view DP preview (#5), notification DP cache + no Close btn, branded update flow (0.1.7)

- #2: 'Seen by' opens an on-screen popup listing readers (was a flash toast).
- #5: the DM contact-info view now shows the DP; clicking it previews full-size.
- Desktop notifications: cache DPs per-sender (fast AND with photo after the first);
  removed timeoutType:'never' which added an unwanted 'Close' button.
- Update flow: dropped the unbranded native restart dialog — the branded web banner
  handles Restart. Banner text clearer ('Downloading update…'), plus an update
  indicator that cascades profile 'i' badge → Settings → version line, with the
  Settings button becoming 'Restart now' when the update is downloaded. desktop 0.1.7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 16:56:39 +05:30
parent 49718e5d37
commit 4e2ccb5d60
3 changed files with 60 additions and 34 deletions
+19 -26
View File
@@ -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);
+1 -1
View File
@@ -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",