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:
+59
-9
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user