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:
+19
-26
@@ -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,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",
|
||||
|
||||
Reference in New Issue
Block a user