const { app, BrowserWindow, session, desktopCapturer, ipcMain, clipboard } = require('electron'); const { autoUpdater } = require('electron-updater'); const path = require('path'); const updateState = { status: 'idle', // idle | checking | available | downloading | downloaded | not-available | error info: null, error: null, }; function broadcastUpdateState() { const wins = BrowserWindow.getAllWindows(); for (const win of wins) { if (!win.isDestroyed()) { win.webContents.send('update-state', updateState); } } } function isNewerVersionAvailable(info) { const next = info && typeof info.version === 'string' ? info.version : ''; return Boolean(next) && next !== app.getVersion(); } let updateCheckInProgress = false; async function runUpdateCheck(reason = 'manual') { if (updateCheckInProgress) { console.log(`Update check skipped (${reason}): another check is already in progress`); return; } updateCheckInProgress = true; updateState.status = 'checking'; updateState.error = null; try { const result = await autoUpdater.checkForUpdates(); const info = result && result.updateInfo ? result.updateInfo : null; if (isNewerVersionAvailable(info)) { updateState.status = 'available'; updateState.info = info; updateState.error = null; } else { updateState.status = 'not-available'; updateState.info = info; updateState.error = null; } } catch (err) { updateState.status = 'error'; updateState.error = err && err.message ? err.message : String(err); console.error(`Update check failed (${reason})`, err); } finally { updateCheckInProgress = false; broadcastUpdateState(); } } function createWindow() { const sess = session.fromPartition('persist:chattz'); const backendUrl = (process.env.CHATTZ_URL || process.env.APP_BASE_URL || 'http://localhost:3000').replace(/\/$/, ''); const backendOrigin = new URL(backendUrl).origin; const win = new BrowserWindow({ width: 1200, height: 800, title: "Chattz Desktop", webPreferences: { nodeIntegration: false, contextIsolation: true, session: sess, preload: path.join(__dirname, 'desktop', 'preload.js') } }); win.setAutoHideMenuBar(true); win.setMenuBarVisibility(true); sess.setPermissionCheckHandler((webContents, permission) => { const origin = safeOrigin(webContents.getURL()); if (origin !== backendOrigin) return false; return permission === 'media' || permission === 'clipboard-write'; }); sess.setPermissionRequestHandler((webContents, permission, callback) => { const origin = safeOrigin(webContents.getURL()); if (origin === backendOrigin && (permission === 'media' || permission === 'clipboard-write')) { callback(true); } else { callback(false); } }); sess.setDisplayMediaRequestHandler((request, callback) => { const origin = safeOrigin(request.frame?.url || win.webContents.getURL()); if (origin !== backendOrigin) { callback(null); return; } desktopCapturer.getSources({ types: ['screen', 'window'] }).then((sources) => { if (sources && sources.length > 0) { const screenSource = sources.find(s => s.id.startsWith('screen')) || sources[0]; callback({ video: screenSource, audio: 'loopback' }); } else { callback(null); } }).catch(err => { console.error("Failed to get desktop sources for screen share", err); callback(null); }); }); win.loadURL(backendUrl).catch((err) => { console.error(`Failed to load desktop app: ${err}`); }); // Ensure renderer receives latest updater state after any (re)load. // Delay broadcast by 300ms to give the renderer time to register its // onUpdateState IPC listener before we push state. win.webContents.on('did-finish-load', () => { setTimeout(() => { if (!win.isDestroyed()) broadcastUpdateState(); }, 300); }); // Uncomment to debug // win.webContents.openDevTools(); } app.commandLine.appendSwitch('disable-webrtc-hw-encoding'); // Sometime helps resolve codec mismatch behavior app.commandLine.appendSwitch('log-level', '3'); // Silences standard Chromium warning logs (like SRTP transport unprotect logs which are benign timing issues) app.whenReady().then(() => { ipcMain.handle('clipboard-write', (event, text) => { clipboard.writeText(text); return true; }); ipcMain.handle('get-update-state', () => ({ ...updateState })); ipcMain.handle('check-for-updates-now', async () => { await runUpdateCheck('renderer-direct'); return { ...updateState }; }); createWindow(); // Configure Auto-Updater autoUpdater.autoDownload = false; autoUpdater.logger = console; autoUpdater.on('update-available', (info) => { updateState.status = 'available'; updateState.info = info; updateState.error = null; broadcastUpdateState(); const wins = BrowserWindow.getAllWindows(); if (wins.length > 0) wins[0].webContents.send('update-available', info); }); autoUpdater.on('update-not-available', (info) => { updateState.status = 'not-available'; updateState.info = info || null; updateState.error = null; broadcastUpdateState(); }); autoUpdater.on('update-downloaded', (info) => { updateState.status = 'downloaded'; updateState.info = info; updateState.error = null; broadcastUpdateState(); const wins = BrowserWindow.getAllWindows(); if (wins.length > 0) wins[0].webContents.send('update-downloaded', info); }); autoUpdater.on('error', (err) => { updateState.status = 'error'; updateState.error = err.message; broadcastUpdateState(); const wins = BrowserWindow.getAllWindows(); if (wins.length > 0) wins[0].webContents.send('update-error', err.message); }); ipcMain.on('check-for-updates', () => { void runUpdateCheck('manual'); }); ipcMain.on('download-update', () => { updateState.status = 'downloading'; updateState.error = null; broadcastUpdateState(); autoUpdater.downloadUpdate(); }); ipcMain.on('quit-and-install', () => { autoUpdater.quitAndInstall(); }); // Check once on startup setTimeout(() => { void runUpdateCheck('startup'); }, 5000); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); function safeOrigin(value) { try { return new URL(value).origin; } catch (_) { return null; } }