const { app, BrowserWindow, session, desktopCapturer, ipcMain, clipboard } = require('electron'); const { autoUpdater } = require('electron-updater'); const path = require('path'); const packageJson = require('./package.json'); const PERIODIC_UPDATE_CHECK_INTERVAL_MS = 1 * 60 * 1000; const updateState = { status: 'idle', // idle | checking | available | downloading | downloaded | installing | 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; let installInProgress = false; let periodicUpdateTimer = null; function resolveBackendUrl() { const configuredUrl = (process.env.CHATTZ_URL || process.env.APP_BASE_URL || '').trim(); if (configuredUrl) { return configuredUrl.replace(/\/$/, ''); } const packagedFallback = typeof packageJson.homepage === 'string' ? packageJson.homepage.trim() : ''; if (app.isPackaged && packagedFallback) { return packagedFallback.replace(/\/$/, ''); } return 'http://localhost:3000'; } function webContentsOrigin(webContents) { if (!webContents || typeof webContents.getURL !== 'function') { return null; } return safeOrigin(webContents.getURL()); } async function runUpdateCheck(reason = 'manual') { if (installInProgress) { console.log(`Update check skipped (${reason}): install already in progress`); return; } 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 installDownloadedUpdate() { if (installInProgress) { console.log('Update install already in progress'); return; } installInProgress = true; updateState.status = 'installing'; updateState.error = null; broadcastUpdateState(); const wins = BrowserWindow.getAllWindows(); for (const win of wins) { if (win.isDestroyed()) continue; try { win.removeAllListeners('close'); win.destroy(); } catch (err) { console.warn('Failed to destroy window before update install', err); } } setImmediate(() => { try { autoUpdater.quitAndInstall(false, true); } catch (err) { installInProgress = false; updateState.status = 'error'; updateState.error = err && err.message ? err.message : String(err); broadcastUpdateState(); console.error('quitAndInstall failed', err); } }); setTimeout(() => { if (installInProgress) { console.warn('Update install is still waiting for app shutdown'); } }, 15000); } function startPeriodicUpdateChecks() { if (periodicUpdateTimer || !app.isPackaged) { return; } periodicUpdateTimer = setInterval(() => { void runUpdateCheck('periodic'); }, PERIODIC_UPDATE_CHECK_INTERVAL_MS); } function createWindow() { const sess = session.fromPartition('persist:chattz'); const backendUrl = resolveBackendUrl(); const backendOrigin = new URL(backendUrl).origin; console.log(`Desktop backend URL: ${backendUrl}`); 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 = webContentsOrigin(webContents); if (origin !== backendOrigin) return false; return permission === 'media' || permission === 'clipboard-write'; }); sess.setPermissionRequestHandler((webContents, permission, callback) => { const origin = webContentsOrigin(webContents); 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', () => { if (installInProgress) return; updateState.status = 'downloading'; updateState.error = null; broadcastUpdateState(); autoUpdater.downloadUpdate(); }); ipcMain.on('quit-and-install', () => { installDownloadedUpdate(); }); // Check once on startup setTimeout(() => { void runUpdateCheck('startup'); }, 5000); startPeriodicUpdateChecks(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('before-quit', () => { if (periodicUpdateTimer) { clearInterval(periodicUpdateTimer); periodicUpdateTimer = null; } }); function safeOrigin(value) { try { return new URL(value).origin; } catch (_) { return null; } }