feat(chat/notif): single hover action-pill; sender DP in desktop+mobile notifications
- Message actions (reply/react/edit/delete) consolidated into ONE hover pill anchored to the
bubble's top-right, overlapping it so on short messages the icons no longer float off into
empty space and vanish before you can click.
- Desktop toast + mobile(FCM)/web-background push now show the sender's real DP:
* renderer passes the DP URL through; desktop shell downloads it for the toast icon
(canvas-drawing an external DP tainted it → initials). Desktop bumped to 0.1.4.
* DM push payload carries icon=sender avatar; sw.js already uses it (web background),
sendFcm sets notification.image (Android).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+29
-12
@@ -39,20 +39,36 @@ try { ({ autoUpdater } = require('electron-updater')); } catch (_) { /* not inst
|
||||
// Uses Electron's OWN Notification (native, no SnoreToast) whose 'click' event fires reliably.
|
||||
const APP_ID = 'com.bizgaze.connect.desktop';
|
||||
|
||||
// Write the avatar the renderer drew (a data: URL) to a temp PNG for the toast icon.
|
||||
function writeTempPng(dataUrl) {
|
||||
try {
|
||||
if (!dataUrl || !/^data:image\/png;base64,/.test(dataUrl)) return null;
|
||||
const p = path.join(app.getPath('temp'), 'bizc-toast-' + crypto.randomBytes(4).toString('hex') + '.png');
|
||||
fs.writeFileSync(p, Buffer.from(dataUrl.split(',')[1], 'base64'));
|
||||
return p;
|
||||
} catch (_) { return null; }
|
||||
// Resolve the sender/group avatar to a local temp PNG for the toast icon. Accepts either a data:
|
||||
// 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'); }
|
||||
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); }
|
||||
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)));
|
||||
});
|
||||
req.on('error', () => resolve(null));
|
||||
req.setTimeout(2500, () => { try { req.destroy(); } catch (_) {} resolve(null); });
|
||||
return;
|
||||
}
|
||||
resolve(null);
|
||||
} catch (_) { resolve(null); }
|
||||
});
|
||||
}
|
||||
|
||||
// Resolves {open} when the toast is clicked (renderer then opens that chat), else null.
|
||||
ipcMain.handle('reply-notification', (_e, payload = {}) => new Promise((resolve) => {
|
||||
if (!Notification.isSupported()) return resolve(null);
|
||||
const img = writeTempPng(payload.avatar);
|
||||
ipcMain.handle('reply-notification', async (_e, payload = {}) => {
|
||||
if (!Notification.isSupported()) return null;
|
||||
const img = await avatarToTempPng(payload.avatar);
|
||||
return await new Promise((resolve) => {
|
||||
let done = false;
|
||||
const finish = (v) => { if (!done) { done = true; if (img) { try { fs.unlinkSync(img); } catch (_) {} } resolve(v); } };
|
||||
try {
|
||||
@@ -70,7 +86,8 @@ ipcMain.handle('reply-notification', (_e, payload = {}) => new Promise((resolve)
|
||||
n.show();
|
||||
setTimeout(() => finish(null), 30000); // don't leave the promise pending forever
|
||||
} catch (_) { finish(null); }
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
// Windows attributes notifications to the AppUserModelID. Without setting it, toasts read
|
||||
// "electron.app.<name>"; setting it to the installer's appId makes Windows resolve the
|
||||
|
||||
Reference in New Issue
Block a user