fix(desktop): custom screen picker, single restart prompt, reliable call Join (0.1.8/batch61)

- Custom branded screen/window picker (picker.html) — useSystemPicker was
  silently no-op on Win11 and auto-shared the primary display with no choice.
- Use checkForUpdates (not ...AndNotify): drops electron-updater's own native
  "Update ready" toast that duplicated our in-app banner (two restart prompts).
- Call notification DP: wait up to 2.5s for the caller photo on persistent
  (call) toasts instead of 600ms so the DP actually shows.
- Re-surface Join/Decline invite when opening a chat with an active incoming
  call, so a call-notification click always lands on a joinable call.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 17:38:50 +05:30
parent 4e2ccb5d60
commit 6ed0fa0ea0
4 changed files with 156 additions and 15 deletions
+59 -9
View File
@@ -91,10 +91,12 @@ const activeNotifs = new Set();
ipcMain.handle('reply-notification', async (_e, payload = {}) => {
if (!Notification.isSupported()) return null;
// Do NOT block the toast on the avatar download (that made desktop notifications lag ~15s vs the
// browser's instant one). Race it against a short cap: use the DP only if it's ready fast.
// browser's instant one). Race it against a cap: use the DP only if it's ready in time. Chat toasts
// stay snappy (700ms). CALL toasts (persistent) ring for ~40s, so we can afford to wait longer
// (2.5s) to actually show the caller's photo — the whole point of a call notification.
const img = await Promise.race([
avatarToTempPng(payload.avatar),
new Promise((r) => setTimeout(() => r(null), 600)),
new Promise((r) => setTimeout(() => r(null), payload.persistent ? 2500 : 700)),
]);
return await new Promise((resolve) => {
let done = false;
@@ -219,16 +221,61 @@ const GRANTED = new Set([
'clipboard-read', 'clipboard-sanitized-write', 'fullscreen', 'pointerLock',
]);
// Custom "Share your screen" picker. Enumerates screens + windows, shows a branded modal grid with
// live thumbnails, and resolves to the chosen desktopCapturer source (or null if cancelled). Replaces
// the unreliable OS system picker. Only one picker at a time.
let pickerWin = null;
function pickShareSource() {
return new Promise((resolve) => {
let settled = false;
const finish = (v) => { if (settled) return; settled = true; ipcMain.removeListener('picker-choose', onChoose); if (pickerWin && !pickerWin.isDestroyed()) { try { pickerWin.close(); } catch (_) {} } pickerWin = null; resolve(v); };
let allSources = [];
const onChoose = (_e, id) => {
if (!id) return finish(null);
finish(allSources.find((s) => s.id === id) || null);
};
desktopCapturer.getSources({ types: ['screen', 'window'], thumbnailSize: { width: 320, height: 200 }, fetchWindowIcons: true })
.then((sources) => {
allSources = sources;
const payload = { screen: [], window: [] };
for (const s of sources) {
const bucket = s.id.startsWith('screen:') ? 'screen' : 'window';
payload[bucket].push({
id: s.id,
name: s.name || (bucket === 'screen' ? 'Screen' : 'Window'),
thumb: s.thumbnail ? s.thumbnail.toDataURL() : '',
appIcon: s.appIcon && !s.appIcon.isEmpty() ? s.appIcon.toDataURL() : null,
});
}
if (pickerWin && !pickerWin.isDestroyed()) { try { pickerWin.close(); } catch (_) {} }
pickerWin = new BrowserWindow({
width: 760, height: 560, parent: win || undefined, modal: !!win, resizable: true,
minimizable: false, maximizable: false, title: 'Share your screen', backgroundColor: '#f4f6fb',
show: false, autoHideMenuBar: true,
webPreferences: { preload: undefined, nodeIntegration: true, contextIsolation: false },
});
pickerWin.setMenu(null);
pickerWin.loadFile(path.join(__dirname, 'picker.html'));
pickerWin.once('ready-to-show', () => { pickerWin.show(); pickerWin.webContents.send('picker-sources', payload); });
pickerWin.on('closed', () => { if (!settled) finish(null); }); // closed via the X → cancel
ipcMain.on('picker-choose', onChoose);
})
.catch(() => finish(null));
});
}
function configureSession() {
const ses = session.fromPartition('persist:bizconnect');
// getDisplayMedia: prefer the OS's native screen/window PICKER (Windows 11 / macOS) so the user
// chooses what to share (and can pick a single window, avoiding the whole-screen mirror). If the
// system picker isn't available, this handler falls back to auto-selecting the primary display.
// getDisplayMedia: show OUR OWN branded screen/window picker. The Electron `useSystemPicker`
// option silently no-ops on many Windows 11 builds (it needs a specific WebRTC feature) and then
// auto-shares the primary display with no choice — which is exactly the "no picker appears" bug.
// So we enumerate sources ourselves and pop a picker window (pickShareSource) to let the user
// pick a specific screen or window.
ses.setDisplayMediaRequestHandler((request, callback) => {
desktopCapturer.getSources({ types: ['screen', 'window'] }).then((sources) => {
callback(sources.length ? { video: sources[0], audio: 'loopback' } : {});
pickShareSource().then((source) => {
callback(source ? { video: source, audio: 'loopback' } : {}); // {} = user cancelled → no share
}).catch(() => callback({}));
}, { useSystemPicker: true });
}, { useSystemPicker: false });
// Async grant (getUserMedia, notifications, …)
ses.setPermissionRequestHandler((_wc, permission, callback) => callback(GRANTED.has(permission)));
// Sync check (some getUserMedia paths query this before requesting)
@@ -255,7 +302,10 @@ app.whenReady().then(() => {
// 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(() => {});
// checkForUpdates (NOT ...AndNotify): ...AndNotify pops electron-updater's OWN native "Update ready
// — Restart/Later" toast on download, which duplicated our branded in-app banner (the user saw TWO
// restart prompts). Plain checkForUpdates still auto-downloads and fires 'update-downloaded'.
const check = () => autoUpdater.checkForUpdates().catch(() => {});
check();
setInterval(check, 6 * 60 * 60 * 1000);
}