diff --git a/.forgejo/workflows/pipeline.yaml b/.forgejo/workflows/pipeline.yaml index 9e241e8..089f873 100644 --- a/.forgejo/workflows/pipeline.yaml +++ b/.forgejo/workflows/pipeline.yaml @@ -50,11 +50,20 @@ jobs: cp "$file" "static/installers/$out" fi } + # Copy versioned artifacts for electron-updater + cp dist/*.AppImage static/installers/ || true + cp dist/*.exe static/installers/ || true + cp dist/*.rpm static/installers/ || true + cp dist/*.deb static/installers/ || true + cp dist/*.msi static/installers/ || true + + # Maintain generic names for stable website links copy_first '*.rpm' 'chattz-linux.rpm' copy_first '*.deb' 'chattz-linux.deb' copy_first '*.AppImage' 'chattz-linux.AppImage' copy_first '*.exe' 'chattz-windows.exe' copy_first '*.msi' 'chattz-windows.msi' + # Metadata files for electron-updater copy_first 'latest-linux.yml' 'latest-linux.yml' copy_first 'latest.yml' 'latest.yml' diff --git a/desktop/index.html b/desktop/index.html index 07f8f7d..ed754ec 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -268,4 +268,4 @@ - + \ No newline at end of file diff --git a/main.js b/main.js index 009d449..3a32c94 100644 --- a/main.js +++ b/main.js @@ -51,10 +51,16 @@ function isNewerVersionAvailable(info) { 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; - broadcastUpdateState(); try { const result = await autoUpdater.checkForUpdates(); const info = result && result.updateInfo ? result.updateInfo : null; @@ -62,18 +68,18 @@ async function runUpdateCheck(reason = 'manual') { updateState.status = 'available'; updateState.info = info; updateState.error = null; - broadcastUpdateState(); } else { updateState.status = 'not-available'; updateState.info = info; updateState.error = null; - broadcastUpdateState(); } } catch (err) { updateState.status = 'error'; updateState.error = err && err.message ? err.message : String(err); - broadcastUpdateState(); console.error(`Update check failed (${reason})`, err); + } finally { + updateCheckInProgress = false; + broadcastUpdateState(); } } @@ -95,46 +101,7 @@ function createWindow() { }); win.setAutoHideMenuBar(true); - win.setMenuBarVisibility(false); - - ipcMain.handle('get-config', () => { - return { - backendUrl: (process.env.CHATTZ_URL || 'https://discord.flegr.me').replace(/\/$/, '') - }; - }); - - ipcMain.handle('clipboard-write', (event, text) => { - clipboard.writeText(text); - return true; - }); - - ipcMain.handle('storage-get', (event, key) => { - if (typeof key !== 'string' || key.length === 0) return null; - const store = readPersistentStore(); - return Object.prototype.hasOwnProperty.call(store, key) ? store[key] : null; - }); - - ipcMain.handle('storage-set', (event, key, value) => { - if (typeof key !== 'string' || key.length === 0) return false; - const store = readPersistentStore(); - store[key] = value; - writePersistentStore(store); - return true; - }); - - ipcMain.handle('storage-remove', (event, key) => { - if (typeof key !== 'string' || key.length === 0) return false; - const store = readPersistentStore(); - delete store[key]; - writePersistentStore(store); - return true; - }); - - ipcMain.handle('get-update-state', () => ({ ...updateState })); - ipcMain.handle('check-for-updates-now', async () => { - await runUpdateCheck('renderer-direct'); - return { ...updateState }; - }); + win.setMenuBarVisibility(true); // Auto-approve media permissions (camera, microphone) sess.setPermissionCheckHandler((webContents, permission) => { @@ -194,12 +161,14 @@ function createWindow() { }); }; - // Use a navigation listener to detect when the remote login is complete - win.webContents.on('will-navigate', (event, navigatedUrl) => { + // Intercept navigations/redirects that return to the backend with a token + // after the OAuth login flow. Two handlers are needed because: + // - will-navigate fires for client-side navigations (location.href, link clicks) + // - will-redirect fires for server-side 302 redirects (OAuth callback chain) + const interceptLoginRedirect = (event, navigatedUrl) => { try { const urlObj = new URL(navigatedUrl); const backendObj = new URL(backendUrl); - // Detect the redirect back to the home page with a token if (urlObj.origin === backendObj.origin && urlObj.pathname === '/') { if (urlObj.searchParams.has('token')) { console.log("Detected login success redirect, returning to desktop UI..."); @@ -210,16 +179,20 @@ function createWindow() { } catch (e) { console.error(e); } - }); + }; + + win.webContents.on('will-navigate', interceptLoginRedirect); + win.webContents.on('will-redirect', interceptLoginRedirect); loadDesktopApp(); // 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', () => { - broadcastUpdateState(); - // Login flow swaps pages; force a check after desktop UI reloads so - // renderer always gets fresh updater state. - void runUpdateCheck('did-finish-load'); + setTimeout(() => { + if (!win.isDestroyed()) broadcastUpdateState(); + }, 300); }); // Uncomment to debug @@ -230,6 +203,46 @@ app.commandLine.appendSwitch('disable-webrtc-hw-encoding'); // Sometime helps re app.commandLine.appendSwitch('log-level', '3'); // Silences standard Chromium warning logs (like SRTP transport unprotect logs which are benign timing issues) app.whenReady().then(() => { + // Register IPC handlers once (before creating any windows) + ipcMain.handle('get-config', () => { + return { + backendUrl: (process.env.CHATTZ_URL || 'https://discord.flegr.me').replace(/\/$/, '') + }; + }); + + ipcMain.handle('clipboard-write', (event, text) => { + clipboard.writeText(text); + return true; + }); + + ipcMain.handle('storage-get', (event, key) => { + if (typeof key !== 'string' || key.length === 0) return null; + const store = readPersistentStore(); + return Object.prototype.hasOwnProperty.call(store, key) ? store[key] : null; + }); + + ipcMain.handle('storage-set', (event, key, value) => { + if (typeof key !== 'string' || key.length === 0) return false; + const store = readPersistentStore(); + store[key] = value; + writePersistentStore(store); + return true; + }); + + ipcMain.handle('storage-remove', (event, key) => { + if (typeof key !== 'string' || key.length === 0) return false; + const store = readPersistentStore(); + delete store[key]; + writePersistentStore(store); + 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 diff --git a/static/shared/app-core.js b/static/shared/app-core.js index 97d405d..f14b88d 100644 --- a/static/shared/app-core.js +++ b/static/shared/app-core.js @@ -95,19 +95,18 @@ async function storageRemoveCritical(key) { async function hydrateDesktopStorage() { if (!window.electronAPI?.storageGet) return; for (const key of PERSISTED_STORAGE_KEYS) { - const localVal = localStorage.getItem(key); - if (localVal !== null) { - try { - await window.electronAPI.storageSet(key, localVal); - } catch (err) { - console.warn("Failed to backfill storage key", key, err); - } - continue; - } try { + // IPC file store (renderer-storage.json) is the source of truth — + // it always survives restarts, unlike localStorage on file:// URLs. const val = await window.electronAPI.storageGet(key); if (typeof val === "string") { localStorage.setItem(key, val); + } else { + // IPC store is empty; backfill from localStorage if available + const localVal = localStorage.getItem(key); + if (localVal !== null) { + await window.electronAPI.storageSet(key, localVal); + } } } catch (err) { console.warn("Failed to hydrate storage key", key, err);