diff --git a/main.js b/main.js index 030c413..1baa6e2 100644 --- a/main.js +++ b/main.js @@ -1,11 +1,9 @@ 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 + status: 'idle', // idle | checking | available | downloading | downloaded | not-available | error info: null, error: null, }; @@ -25,37 +23,8 @@ function isNewerVersionAvailable(info) { } 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; @@ -85,62 +54,10 @@ async function runUpdateCheck(reason = 'manual') { } } -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 backendUrl = (process.env.CHATTZ_URL || process.env.APP_BASE_URL || 'http://localhost:3000').replace(/\/$/, ''); const backendOrigin = new URL(backendUrl).origin; - console.log(`Desktop backend URL: ${backendUrl}`); const win = new BrowserWindow({ width: 1200, @@ -158,13 +75,13 @@ function createWindow() { win.setMenuBarVisibility(true); sess.setPermissionCheckHandler((webContents, permission) => { - const origin = webContentsOrigin(webContents); + const origin = safeOrigin(webContents.getURL()); if (origin !== backendOrigin) return false; return permission === 'media' || permission === 'clipboard-write'; }); sess.setPermissionRequestHandler((webContents, permission, callback) => { - const origin = webContentsOrigin(webContents); + const origin = safeOrigin(webContents.getURL()); if (origin === backendOrigin && (permission === 'media' || permission === 'clipboard-write')) { callback(true); } else { @@ -267,7 +184,6 @@ app.whenReady().then(() => { }); ipcMain.on('download-update', () => { - if (installInProgress) return; updateState.status = 'downloading'; updateState.error = null; broadcastUpdateState(); @@ -275,14 +191,13 @@ app.whenReady().then(() => { }); ipcMain.on('quit-and-install', () => { - installDownloadedUpdate(); + autoUpdater.quitAndInstall(); }); // Check once on startup setTimeout(() => { void runUpdateCheck('startup'); }, 5000); - startPeriodicUpdateChecks(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { @@ -297,13 +212,6 @@ app.on('window-all-closed', () => { } }); -app.on('before-quit', () => { - if (periodicUpdateTimer) { - clearInterval(periodicUpdateTimer); - periodicUpdateTimer = null; - } -}); - function safeOrigin(value) { try { return new URL(value).origin; diff --git a/static/shared/app-core.js b/static/shared/app-core.js index 8531b48..aaaa0b4 100644 --- a/static/shared/app-core.js +++ b/static/shared/app-core.js @@ -29,7 +29,6 @@ const state = { viewMode: 'chat', // 'chat' or 'video' iceServers: [{ urls: "stun:stun.l.google.com:19302" }], peerGainNodes: new Map(), // userId -> GainNode - peerAudioNodes: new Map(), // userId -> { sourceNode, compressorNode, gainNode } visibleVolumeSliders: new Set(), // userIds whose sliders are visible }, voicePresencePollId: null, @@ -577,103 +576,50 @@ function renderChannels() { pList.style.paddingLeft = "24px"; for (const p of participants) { const pRow = document.createElement("div"); - pRow.className = `channel-row voice-participant-row ${p.is_speaking ? 'voice-speaking' : ''}`; - pRow.style.display = "block"; + pRow.className = `channel-row ${p.is_speaking ? 'voice-speaking' : ''}`; pRow.style.padding = "2px 8px"; pRow.style.flexWrap = "wrap"; - const topRow = document.createElement("div"); - topRow.style.display = "flex"; - topRow.style.alignItems = "center"; - topRow.style.gap = "8px"; - topRow.style.width = "100%"; - - const avatar = document.createElement("div"); - avatar.className = "avatar"; - avatar.style.width = "20px"; - avatar.style.height = "20px"; - avatar.style.fontSize = "10px"; - avatar.textContent = shortName(p.display_name); - topRow.appendChild(avatar); - - const name = document.createElement("span"); - name.style.flex = "1"; - name.style.overflow = "hidden"; - name.style.textOverflow = "ellipsis"; - name.textContent = p.display_name; - topRow.appendChild(name); - - const isRemoteParticipant = p.user_id !== state.me.id; - if (isRemoteParticipant) { - const volumeToggle = document.createElement("button"); - volumeToggle.type = "button"; - volumeToggle.title = "Toggle volume slider"; - volumeToggle.style.display = "grid"; - volumeToggle.style.placeItems = "center"; - volumeToggle.style.width = "20px"; - volumeToggle.style.height = "20px"; - volumeToggle.style.color = "var(--text-muted)"; - volumeToggle.innerHTML = ''; - topRow.appendChild(volumeToggle); - - volumeToggle.addEventListener('click', (e) => { - toggleVolumeSlider(e); - }); + let sliderHtml = ''; + if (p.user_id !== state.me.id) { + const vol = state.userVolumes.get(p.user_id) ?? 1.0; + const isVisible = state.voice.visibleVolumeSliders.has(p.user_id); + sliderHtml = ` +
+ + + ${Math.round(vol * 100)}% +
+ `; } - if (p.is_muted) { - const muteIcon = document.createElement("i"); - muteIcon.setAttribute("data-lucide", "mic-off"); - muteIcon.className = "voice-muted-icon"; - topRow.appendChild(muteIcon); - } - - pRow.appendChild(topRow); - - const volumeControl = document.createElement("div"); - volumeControl.className = `user-volume-control ${state.voice.visibleVolumeSliders.has(p.user_id) ? 'show-volume' : ''}`; - volumeControl.dataset.userId = p.user_id; - - if (isRemoteParticipant) { - const volumeIcon = document.createElement("i"); - volumeIcon.setAttribute("data-lucide", "volume-2"); - volumeIcon.style.width = "12px"; - volumeIcon.style.height = "12px"; - volumeIcon.style.opacity = "0.6"; - volumeControl.appendChild(volumeIcon); - - const slider = document.createElement("input"); - slider.type = "range"; - slider.min = "0"; - slider.max = "2"; - slider.step = "0.1"; - slider.value = String(state.userVolumes.get(p.user_id) ?? 1.0); - slider.className = "volume-slider"; - volumeControl.appendChild(slider); - - const volumePct = document.createElement("span"); - volumePct.className = "vol-pct"; - volumePct.textContent = `${Math.round((state.userVolumes.get(p.user_id) ?? 1.0) * 100)}%`; - volumeControl.appendChild(volumePct); + pRow.innerHTML = ` +
+
${shortName(p.display_name)}
+ ${escapeHtml(p.display_name)} + ${p.is_muted ? '' : ''} +
+ ${sliderHtml} + `; + const slider = pRow.querySelector('.volume-slider'); + if (slider) { slider.addEventListener('input', (e) => { const val = parseFloat(e.target.value); state.userVolumes.set(p.user_id, val); - volumePct.textContent = `${Math.round(val * 100)}%`; + pRow.querySelector('.vol-pct').textContent = `${Math.round(val * 100)}%`; const gainNode = state.voice.peerGainNodes.get(p.user_id); - if (gainNode && state.voice.audioContext) { + if (gainNode) { gainNode.gain.setTargetAtTime(val, state.voice.audioContext.currentTime, 0.05); } }); - - volumeControl.addEventListener('click', (e) => e.stopPropagation()); - volumeControl.addEventListener('mousedown', (e) => e.stopPropagation()); - pRow.appendChild(volumeControl); + // Stop propagation to prevent joining channel again when clicking slider + slider.addEventListener('click', (e) => e.stopPropagation()); } const toggleVolumeSlider = (e) => { - if (!isRemoteParticipant) return; + if (p.user_id === state.me.id) return; e.preventDefault(); e.stopPropagation(); if (state.voice.visibleVolumeSliders.has(p.user_id)) { @@ -687,9 +633,6 @@ function renderChannels() { // Electron can swallow contextmenu events on some platforms; use // right-button mousedown as a reliable fallback for slider toggle. pRow.addEventListener('contextmenu', toggleVolumeSlider); - pRow.addEventListener('auxclick', (e) => { - if (e.button === 2) toggleVolumeSlider(e); - }); pRow.addEventListener('mousedown', (e) => { if (e.button === 2) toggleVolumeSlider(e); }); @@ -1040,24 +983,7 @@ function shouldInitiateOffer(peerId) { return state.me.id > peerId; } -function disconnectPeerAudioNodes(peerId) { - const nodes = state.voice.peerAudioNodes.get(peerId); - if (!nodes) return; - - for (const node of [nodes.sourceNode, nodes.compressorNode, nodes.gainNode]) { - try { - node?.disconnect(); - } catch { } - } - - state.voice.peerAudioNodes.delete(peerId); - state.voice.peerGainNodes.delete(peerId); -} - function stopAndClearAudioPipeline() { - for (const peerId of state.voice.peerAudioNodes.keys()) { - disconnectPeerAudioNodes(peerId); - } if (state.voice.deepFilterProcessor) { state.voice.deepFilterProcessor.destroy(); state.voice.deepFilterProcessor = null; @@ -1283,7 +1209,7 @@ async function createLocalVoiceStream() { sampleRate: 48000, echoCancellation: true, noiseSuppression: false, - autoGainControl: true, + autoGainControl: false, }, video: false, }; @@ -1406,27 +1332,16 @@ function ensurePeerConnection(peerId) { const ctx = state.voice.audioContext; if (ctx.state === 'suspended') ctx.resume(); - disconnectPeerAudioNodes(peerId); - const sourceNode = ctx.createMediaStreamSource(audio.srcObject); - const compressorNode = ctx.createDynamicsCompressor(); const gainNode = ctx.createGain(); - compressorNode.threshold.setValueAtTime(-24, ctx.currentTime); - compressorNode.knee.setValueAtTime(18, ctx.currentTime); - compressorNode.ratio.setValueAtTime(4, ctx.currentTime); - compressorNode.attack.setValueAtTime(0.003, ctx.currentTime); - compressorNode.release.setValueAtTime(0.25, ctx.currentTime); - const currentVolume = state.userVolumes.get(peerId) ?? 1.0; gainNode.gain.setValueAtTime(currentVolume, ctx.currentTime); - sourceNode.connect(compressorNode); - compressorNode.connect(gainNode); + sourceNode.connect(gainNode); gainNode.connect(ctx.destination); state.voice.peerGainNodes.set(peerId, gainNode); - state.voice.peerAudioNodes.set(peerId, { sourceNode, compressorNode, gainNode }); // Mute the original element as we play through Web Audio destination audio.volume = 0; @@ -1611,7 +1526,6 @@ async function joinVoice() { pc.close(); state.voice.peerConnections.delete(msg.user_id); } - disconnectPeerAudioNodes(msg.user_id); document.getElementById(`audio-${msg.user_id}`)?.remove(); } else if (msg.type === "signal") { await handleSignal(msg.from_user_id, msg.kind, msg.data);