checkpoint
This commit is contained in:
parent
3735162f41
commit
208ab8042e
3 changed files with 296 additions and 26 deletions
|
|
@ -3,9 +3,15 @@ const { contextBridge, ipcRenderer } = require('electron');
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
copyToClipboard: (text) => ipcRenderer.invoke('clipboard-write', text),
|
copyToClipboard: (text) => ipcRenderer.invoke('clipboard-write', text),
|
||||||
getConfig: () => ipcRenderer.invoke('get-config'),
|
getConfig: () => ipcRenderer.invoke('get-config'),
|
||||||
|
storageGet: (key) => ipcRenderer.invoke('storage-get', key),
|
||||||
|
storageSet: (key, value) => ipcRenderer.invoke('storage-set', key, value),
|
||||||
|
storageRemove: (key) => ipcRenderer.invoke('storage-remove', key),
|
||||||
|
getUpdateState: () => ipcRenderer.invoke('get-update-state'),
|
||||||
|
checkForUpdatesNow: () => ipcRenderer.invoke('check-for-updates-now'),
|
||||||
checkForUpdates: () => ipcRenderer.send('check-for-updates'),
|
checkForUpdates: () => ipcRenderer.send('check-for-updates'),
|
||||||
downloadUpdate: () => ipcRenderer.send('download-update'),
|
downloadUpdate: () => ipcRenderer.send('download-update'),
|
||||||
quitAndInstall: () => ipcRenderer.send('quit-and-install'),
|
quitAndInstall: () => ipcRenderer.send('quit-and-install'),
|
||||||
|
onUpdateState: (callback) => ipcRenderer.on('update-state', (event, state) => callback(state)),
|
||||||
onUpdateAvailable: (callback) => ipcRenderer.on('update-available', (event, info) => callback(info)),
|
onUpdateAvailable: (callback) => ipcRenderer.on('update-available', (event, info) => callback(info)),
|
||||||
onUpdateDownloaded: (callback) => ipcRenderer.on('update-downloaded', (event, info) => callback(info)),
|
onUpdateDownloaded: (callback) => ipcRenderer.on('update-downloaded', (event, info) => callback(info)),
|
||||||
onUpdateError: (callback) => ipcRenderer.on('update-error', (event, error) => callback(error))
|
onUpdateError: (callback) => ipcRenderer.on('update-error', (event, error) => callback(error))
|
||||||
|
|
|
||||||
137
main.js
137
main.js
|
|
@ -1,8 +1,82 @@
|
||||||
const { app, BrowserWindow, session, desktopCapturer, ipcMain, clipboard } = require('electron');
|
const { app, BrowserWindow, session, desktopCapturer, ipcMain, clipboard } = require('electron');
|
||||||
const { autoUpdater } = require('electron-updater');
|
const { autoUpdater } = require('electron-updater');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
|
|
||||||
|
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 getStorageFilePath() {
|
||||||
|
return path.join(app.getPath('userData'), 'renderer-storage.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPersistentStore() {
|
||||||
|
const filePath = getStorageFilePath();
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) return {};
|
||||||
|
const raw = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return parsed && typeof parsed === 'object' ? parsed : {};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to read persistent store', err);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writePersistentStore(store) {
|
||||||
|
const filePath = getStorageFilePath();
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(store), 'utf8');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to write persistent store', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNewerVersionAvailable(info) {
|
||||||
|
const next = info && typeof info.version === 'string' ? info.version : '';
|
||||||
|
return Boolean(next) && next !== app.getVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runUpdateCheck(reason = 'manual') {
|
||||||
|
updateState.status = 'checking';
|
||||||
|
updateState.error = null;
|
||||||
|
broadcastUpdateState();
|
||||||
|
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;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
// Create a persistent session for chattz to keep the user logged in
|
// Create a persistent session for chattz to keep the user logged in
|
||||||
const sess = session.fromPartition('persist:chattz');
|
const sess = session.fromPartition('persist:chattz');
|
||||||
|
|
@ -34,6 +108,34 @@ function createWindow() {
|
||||||
return true;
|
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 };
|
||||||
|
});
|
||||||
|
|
||||||
// Auto-approve media permissions (camera, microphone)
|
// Auto-approve media permissions (camera, microphone)
|
||||||
sess.setPermissionCheckHandler((webContents, permission) => {
|
sess.setPermissionCheckHandler((webContents, permission) => {
|
||||||
if (permission === 'media' || permission === 'clipboard-read' || permission === 'clipboard-write') {
|
if (permission === 'media' || permission === 'clipboard-read' || permission === 'clipboard-write') {
|
||||||
|
|
@ -112,6 +214,14 @@ function createWindow() {
|
||||||
|
|
||||||
loadDesktopApp();
|
loadDesktopApp();
|
||||||
|
|
||||||
|
// Ensure renderer receives latest updater state after any (re)load.
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
// Uncomment to debug
|
// Uncomment to debug
|
||||||
// win.webContents.openDevTools();
|
// win.webContents.openDevTools();
|
||||||
}
|
}
|
||||||
|
|
@ -127,27 +237,46 @@ app.whenReady().then(() => {
|
||||||
autoUpdater.logger = console;
|
autoUpdater.logger = console;
|
||||||
|
|
||||||
autoUpdater.on('update-available', (info) => {
|
autoUpdater.on('update-available', (info) => {
|
||||||
|
updateState.status = 'available';
|
||||||
|
updateState.info = info;
|
||||||
|
updateState.error = null;
|
||||||
|
broadcastUpdateState();
|
||||||
const wins = BrowserWindow.getAllWindows();
|
const wins = BrowserWindow.getAllWindows();
|
||||||
if (wins.length > 0) wins[0].webContents.send('update-available', info);
|
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) => {
|
autoUpdater.on('update-downloaded', (info) => {
|
||||||
|
updateState.status = 'downloaded';
|
||||||
|
updateState.info = info;
|
||||||
|
updateState.error = null;
|
||||||
|
broadcastUpdateState();
|
||||||
const wins = BrowserWindow.getAllWindows();
|
const wins = BrowserWindow.getAllWindows();
|
||||||
if (wins.length > 0) wins[0].webContents.send('update-downloaded', info);
|
if (wins.length > 0) wins[0].webContents.send('update-downloaded', info);
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on('error', (err) => {
|
autoUpdater.on('error', (err) => {
|
||||||
|
updateState.status = 'error';
|
||||||
|
updateState.error = err.message;
|
||||||
|
broadcastUpdateState();
|
||||||
const wins = BrowserWindow.getAllWindows();
|
const wins = BrowserWindow.getAllWindows();
|
||||||
if (wins.length > 0) wins[0].webContents.send('update-error', err.message);
|
if (wins.length > 0) wins[0].webContents.send('update-error', err.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('check-for-updates', () => {
|
ipcMain.on('check-for-updates', () => {
|
||||||
autoUpdater.checkForUpdatesAndNotify().catch(err => {
|
void runUpdateCheck('manual');
|
||||||
console.error("Manual update check failed", err);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('download-update', () => {
|
ipcMain.on('download-update', () => {
|
||||||
|
updateState.status = 'downloading';
|
||||||
|
updateState.error = null;
|
||||||
|
broadcastUpdateState();
|
||||||
autoUpdater.downloadUpdate();
|
autoUpdater.downloadUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -157,7 +286,7 @@ app.whenReady().then(() => {
|
||||||
|
|
||||||
// Check once on startup
|
// Check once on startup
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
autoUpdater.checkForUpdatesAndNotify().catch(() => { });
|
void runUpdateCheck('startup');
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,80 @@ const state = {
|
||||||
|
|
||||||
// --- Desktop Backend Configuration ---
|
// --- Desktop Backend Configuration ---
|
||||||
let API_BASE_URL = ''; // Will be initialized via IPC
|
let API_BASE_URL = ''; // Will be initialized via IPC
|
||||||
|
const LAST_GUILD_STORAGE_KEY = "chattz:lastGuildId";
|
||||||
|
const PERSISTED_STORAGE_KEYS = [
|
||||||
|
"chattz_token",
|
||||||
|
"chattz_refresh_token",
|
||||||
|
LAST_GUILD_STORAGE_KEY,
|
||||||
|
"active_voice_channel",
|
||||||
|
];
|
||||||
|
|
||||||
|
function storageGet(key) {
|
||||||
|
return localStorage.getItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function storageSet(key, value) {
|
||||||
|
localStorage.setItem(key, value);
|
||||||
|
if (window.electronAPI?.storageSet) {
|
||||||
|
void window.electronAPI.storageSet(key, value).catch((err) => {
|
||||||
|
console.warn("Failed to persist storage key", key, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function storageRemove(key) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
if (window.electronAPI?.storageRemove) {
|
||||||
|
void window.electronAPI.storageRemove(key).catch((err) => {
|
||||||
|
console.warn("Failed to remove persisted storage key", key, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function storageSetCritical(key, value) {
|
||||||
|
localStorage.setItem(key, value);
|
||||||
|
if (window.electronAPI?.storageSet) {
|
||||||
|
try {
|
||||||
|
await window.electronAPI.storageSet(key, value);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Failed to persist critical storage key", key, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function storageRemoveCritical(key) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
if (window.electronAPI?.storageRemove) {
|
||||||
|
try {
|
||||||
|
await window.electronAPI.storageRemove(key);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Failed to remove critical storage key", key, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
const val = await window.electronAPI.storageGet(key);
|
||||||
|
if (typeof val === "string") {
|
||||||
|
localStorage.setItem(key, val);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Failed to hydrate storage key", key, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function initializeConfig() {
|
async function initializeConfig() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -73,7 +147,7 @@ function getWsUrl(path) {
|
||||||
urlStr = `${proto}://${url.host}${normalizedPath}`;
|
urlStr = `${proto}://${url.host}${normalizedPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem("chattz_token");
|
const token = storageGet("chattz_token");
|
||||||
if (token) {
|
if (token) {
|
||||||
urlStr += (urlStr.includes('?') ? '&' : '?') + `token=${token}`;
|
urlStr += (urlStr.includes('?') ? '&' : '?') + `token=${token}`;
|
||||||
}
|
}
|
||||||
|
|
@ -180,7 +254,7 @@ async function api(path, options = {}) {
|
||||||
...(options.headers || {}),
|
...(options.headers || {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const token = localStorage.getItem("chattz_token");
|
const token = storageGet("chattz_token");
|
||||||
if (token) {
|
if (token) {
|
||||||
headers["Authorization"] = `Bearer ${token}`;
|
headers["Authorization"] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
@ -192,7 +266,7 @@ async function api(path, options = {}) {
|
||||||
|
|
||||||
// Handle 401 Unauthorized via Refresh Token
|
// Handle 401 Unauthorized via Refresh Token
|
||||||
if (res.status === 401 && !path.includes('/auth/refresh')) {
|
if (res.status === 401 && !path.includes('/auth/refresh')) {
|
||||||
const refreshToken = localStorage.getItem("chattz_refresh_token");
|
const refreshToken = storageGet("chattz_refresh_token");
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
try {
|
try {
|
||||||
const refreshUrl = API_BASE_URL ? `${API_BASE_URL}/auth/refresh` : '/auth/refresh';
|
const refreshUrl = API_BASE_URL ? `${API_BASE_URL}/auth/refresh` : '/auth/refresh';
|
||||||
|
|
@ -204,8 +278,8 @@ async function api(path, options = {}) {
|
||||||
|
|
||||||
if (refreshRes.ok) {
|
if (refreshRes.ok) {
|
||||||
const newTokens = await refreshRes.json();
|
const newTokens = await refreshRes.json();
|
||||||
localStorage.setItem("chattz_token", newTokens.access_token);
|
await storageSetCritical("chattz_token", newTokens.access_token);
|
||||||
localStorage.setItem("chattz_refresh_token", newTokens.refresh_token);
|
await storageSetCritical("chattz_refresh_token", newTokens.refresh_token);
|
||||||
|
|
||||||
// Retry original request with new token
|
// Retry original request with new token
|
||||||
headers["Authorization"] = `Bearer ${newTokens.access_token}`;
|
headers["Authorization"] = `Bearer ${newTokens.access_token}`;
|
||||||
|
|
@ -215,8 +289,8 @@ async function api(path, options = {}) {
|
||||||
throw new Error("Refresh token expired or invalid");
|
throw new Error("Refresh token expired or invalid");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
localStorage.removeItem("chattz_token");
|
await storageRemoveCritical("chattz_token");
|
||||||
localStorage.removeItem("chattz_refresh_token");
|
await storageRemoveCritical("chattz_refresh_token");
|
||||||
location.href = API_BASE_URL ? `${API_BASE_URL}/auth/login` : "/auth/login";
|
location.href = API_BASE_URL ? `${API_BASE_URL}/auth/login` : "/auth/login";
|
||||||
throw new Error("Session expired, please log in again");
|
throw new Error("Session expired, please log in again");
|
||||||
}
|
}
|
||||||
|
|
@ -330,8 +404,6 @@ function playSound(type) {
|
||||||
console.warn("playSound failed", err);
|
console.warn("playSound failed", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const LAST_GUILD_STORAGE_KEY = "chattz:lastGuildId";
|
|
||||||
|
|
||||||
// --- Renderers ---
|
// --- Renderers ---
|
||||||
|
|
||||||
function renderGuilds() {
|
function renderGuilds() {
|
||||||
|
|
@ -348,7 +420,7 @@ function renderGuilds() {
|
||||||
state.selectedVoiceChannelId = null;
|
state.selectedVoiceChannelId = null;
|
||||||
state.selectedDmUserId = null;
|
state.selectedDmUserId = null;
|
||||||
state.selectedDmDisplayName = null;
|
state.selectedDmDisplayName = null;
|
||||||
localStorage.setItem(LAST_GUILD_STORAGE_KEY, guild.id);
|
storageSet(LAST_GUILD_STORAGE_KEY, guild.id);
|
||||||
renderGuilds();
|
renderGuilds();
|
||||||
renderDMs();
|
renderDMs();
|
||||||
await loadChannels();
|
await loadChannels();
|
||||||
|
|
@ -1258,7 +1330,7 @@ async function joinVoice() {
|
||||||
|
|
||||||
state.voice.ws = ws;
|
state.voice.ws = ws;
|
||||||
state.voice.joinedChannelId = state.selectedVoiceChannelId;
|
state.voice.joinedChannelId = state.selectedVoiceChannelId;
|
||||||
localStorage.setItem("active_voice_channel", state.selectedVoiceChannelId);
|
storageSet("active_voice_channel", state.selectedVoiceChannelId);
|
||||||
|
|
||||||
// Start mic setup in parallel so channel join is not blocked by device init.
|
// Start mic setup in parallel so channel join is not blocked by device init.
|
||||||
createLocalVoiceStream()
|
createLocalVoiceStream()
|
||||||
|
|
@ -1390,7 +1462,7 @@ async function joinVoice() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function leaveVoice() {
|
async function leaveVoice() {
|
||||||
localStorage.removeItem("active_voice_channel");
|
storageRemove("active_voice_channel");
|
||||||
if (state.voice.ws) state.voice.ws.close();
|
if (state.voice.ws) state.voice.ws.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1612,6 +1684,7 @@ function closeMobileMenus() {
|
||||||
|
|
||||||
let inactivityTimer = null;
|
let inactivityTimer = null;
|
||||||
let isCurrentlyIdle = false;
|
let isCurrentlyIdle = false;
|
||||||
|
let updaterInitialized = false;
|
||||||
|
|
||||||
function resetInactivityTimer() {
|
function resetInactivityTimer() {
|
||||||
if (isCurrentlyIdle) {
|
if (isCurrentlyIdle) {
|
||||||
|
|
@ -1636,6 +1709,8 @@ document.addEventListener('click', resetInactivityTimer);
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
|
await hydrateDesktopStorage();
|
||||||
|
initUpdater();
|
||||||
|
|
||||||
// 1. Handle tokens in URL (from successful login redirects)
|
// 1. Handle tokens in URL (from successful login redirects)
|
||||||
try {
|
try {
|
||||||
|
|
@ -1644,8 +1719,8 @@ async function init() {
|
||||||
const refreshToken = searchParams.get("refresh_token");
|
const refreshToken = searchParams.get("refresh_token");
|
||||||
|
|
||||||
if (jwtToken || refreshToken) {
|
if (jwtToken || refreshToken) {
|
||||||
if (jwtToken) localStorage.setItem("chattz_token", jwtToken);
|
if (jwtToken) await storageSetCritical("chattz_token", jwtToken);
|
||||||
if (refreshToken) localStorage.setItem("chattz_refresh_token", refreshToken);
|
if (refreshToken) await storageSetCritical("chattz_refresh_token", refreshToken);
|
||||||
|
|
||||||
searchParams.delete("token");
|
searchParams.delete("token");
|
||||||
searchParams.delete("refresh_token");
|
searchParams.delete("refresh_token");
|
||||||
|
|
@ -1665,8 +1740,8 @@ async function init() {
|
||||||
|
|
||||||
el.logoutBtn.onclick = async () => {
|
el.logoutBtn.onclick = async () => {
|
||||||
await leaveVoice();
|
await leaveVoice();
|
||||||
localStorage.removeItem("chattz_token");
|
await storageRemoveCritical("chattz_token");
|
||||||
localStorage.removeItem("chattz_refresh_token");
|
await storageRemoveCritical("chattz_refresh_token");
|
||||||
try { await api("/auth/logout", { method: "POST" }); } catch (e) { }
|
try { await api("/auth/logout", { method: "POST" }); } catch (e) { }
|
||||||
location.reload();
|
location.reload();
|
||||||
};
|
};
|
||||||
|
|
@ -1705,7 +1780,7 @@ async function init() {
|
||||||
state.selectedVoiceChannelId = null;
|
state.selectedVoiceChannelId = null;
|
||||||
state.selectedDmUserId = null;
|
state.selectedDmUserId = null;
|
||||||
state.selectedDmDisplayName = null;
|
state.selectedDmDisplayName = null;
|
||||||
localStorage.setItem(LAST_GUILD_STORAGE_KEY, guild.id);
|
storageSet(LAST_GUILD_STORAGE_KEY, guild.id);
|
||||||
renderGuilds();
|
renderGuilds();
|
||||||
await loadChannels();
|
await loadChannels();
|
||||||
await loadGuildMembers();
|
await loadGuildMembers();
|
||||||
|
|
@ -1889,7 +1964,7 @@ async function init() {
|
||||||
if (inviteCode) {
|
if (inviteCode) {
|
||||||
try {
|
try {
|
||||||
const joinedGuild = await api(`/invites/${encodeURIComponent(inviteCode)}/join`, { method: "POST" });
|
const joinedGuild = await api(`/invites/${encodeURIComponent(inviteCode)}/join`, { method: "POST" });
|
||||||
localStorage.setItem(LAST_GUILD_STORAGE_KEY, joinedGuild.id);
|
storageSet(LAST_GUILD_STORAGE_KEY, joinedGuild.id);
|
||||||
} catch (err) { alert(`Failed to join invite: ${err.message}`); } finally {
|
} catch (err) { alert(`Failed to join invite: ${err.message}`); } finally {
|
||||||
params.delete("invite");
|
params.delete("invite");
|
||||||
const nextQuery = params.toString();
|
const nextQuery = params.toString();
|
||||||
|
|
@ -1908,10 +1983,10 @@ async function init() {
|
||||||
} catch (err) { console.warn("presence sync failed", err); }
|
} catch (err) { console.warn("presence sync failed", err); }
|
||||||
|
|
||||||
if (state.guilds.length > 0) {
|
if (state.guilds.length > 0) {
|
||||||
const lastGuildId = localStorage.getItem(LAST_GUILD_STORAGE_KEY);
|
const lastGuildId = storageGet(LAST_GUILD_STORAGE_KEY);
|
||||||
const guild = state.guilds.find((g) => g.id === lastGuildId) || state.guilds[0];
|
const guild = state.guilds.find((g) => g.id === lastGuildId) || state.guilds[0];
|
||||||
state.selectedGuildId = guild.id;
|
state.selectedGuildId = guild.id;
|
||||||
localStorage.setItem(LAST_GUILD_STORAGE_KEY, guild.id);
|
storageSet(LAST_GUILD_STORAGE_KEY, guild.id);
|
||||||
renderGuilds();
|
renderGuilds();
|
||||||
await loadChannels();
|
await loadChannels();
|
||||||
await loadGuildMembers();
|
await loadGuildMembers();
|
||||||
|
|
@ -1927,7 +2002,7 @@ async function init() {
|
||||||
startVoicePresencePolling();
|
startVoicePresencePolling();
|
||||||
|
|
||||||
// Auto-reconnect to voice if previously joined
|
// Auto-reconnect to voice if previously joined
|
||||||
const savedVoiceChannel = localStorage.getItem("active_voice_channel");
|
const savedVoiceChannel = storageGet("active_voice_channel");
|
||||||
if (savedVoiceChannel) {
|
if (savedVoiceChannel) {
|
||||||
console.log("Auto-reconnecting to voice channel:", savedVoiceChannel);
|
console.log("Auto-reconnecting to voice channel:", savedVoiceChannel);
|
||||||
state.selectedVoiceChannelId = savedVoiceChannel;
|
state.selectedVoiceChannelId = savedVoiceChannel;
|
||||||
|
|
@ -1936,7 +2011,6 @@ async function init() {
|
||||||
}, 800);
|
}, 800);
|
||||||
}
|
}
|
||||||
|
|
||||||
initUpdater();
|
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("init failed", err);
|
console.error("init failed", err);
|
||||||
|
|
@ -1947,6 +2021,53 @@ async function init() {
|
||||||
|
|
||||||
function initUpdater() {
|
function initUpdater() {
|
||||||
if (!window.electronAPI || !el.updateNotifier || !el.updateDownloadBtn || !el.updateInstallBtn) return;
|
if (!window.electronAPI || !el.updateNotifier || !el.updateDownloadBtn || !el.updateInstallBtn) return;
|
||||||
|
if (updaterInitialized) return;
|
||||||
|
updaterInitialized = true;
|
||||||
|
|
||||||
|
const applyUpdateState = (state) => {
|
||||||
|
if (!state || !state.status) return;
|
||||||
|
if (state.status === 'available') {
|
||||||
|
const v = state.info?.version ? ` ${state.info.version}` : '';
|
||||||
|
el.updateNotifier.classList.remove("hidden");
|
||||||
|
el.updateDownloadBtn.classList.remove("hidden");
|
||||||
|
el.updateInstallBtn.classList.add("hidden");
|
||||||
|
el.updateDownloadBtn.disabled = false;
|
||||||
|
el.updateDownloadBtn.style.opacity = "";
|
||||||
|
el.updateDownloadBtn.title = `Download Update${v}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.status === 'downloading') {
|
||||||
|
el.updateNotifier.classList.remove("hidden");
|
||||||
|
el.updateDownloadBtn.classList.remove("hidden");
|
||||||
|
el.updateInstallBtn.classList.add("hidden");
|
||||||
|
el.updateDownloadBtn.disabled = true;
|
||||||
|
el.updateDownloadBtn.style.opacity = "0.5";
|
||||||
|
el.updateDownloadBtn.title = "Downloading update...";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.status === 'downloaded') {
|
||||||
|
const v = state.info?.version ? ` ${state.info.version}` : '';
|
||||||
|
el.updateNotifier.classList.remove("hidden");
|
||||||
|
el.updateDownloadBtn.classList.add("hidden");
|
||||||
|
el.updateInstallBtn.classList.remove("hidden");
|
||||||
|
el.updateInstallBtn.title = `Install Update${v}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.status === 'not-available') {
|
||||||
|
el.updateNotifier.classList.add("hidden");
|
||||||
|
el.updateDownloadBtn.disabled = false;
|
||||||
|
el.updateDownloadBtn.style.opacity = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.status === 'error') {
|
||||||
|
el.updateDownloadBtn.disabled = false;
|
||||||
|
el.updateDownloadBtn.style.opacity = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.electronAPI.onUpdateState((state) => {
|
||||||
|
applyUpdateState(state);
|
||||||
|
});
|
||||||
|
|
||||||
window.electronAPI.onUpdateAvailable((info) => {
|
window.electronAPI.onUpdateAvailable((info) => {
|
||||||
console.log("Update available:", info.version);
|
console.log("Update available:", info.version);
|
||||||
|
|
@ -1978,6 +2099,20 @@ function initUpdater() {
|
||||||
el.updateInstallBtn.onclick = () => {
|
el.updateInstallBtn.onclick = () => {
|
||||||
window.electronAPI.quitAndInstall();
|
window.electronAPI.quitAndInstall();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Recover missed startup events and then run a fresh check now that listeners exist.
|
||||||
|
window.electronAPI.getUpdateState?.()
|
||||||
|
.then((state) => applyUpdateState(state))
|
||||||
|
.catch((err) => console.warn("Failed to fetch initial update state", err))
|
||||||
|
.finally(() => {
|
||||||
|
if (window.electronAPI.checkForUpdatesNow) {
|
||||||
|
void window.electronAPI.checkForUpdatesNow()
|
||||||
|
.then((state) => applyUpdateState(state))
|
||||||
|
.catch((err) => console.warn("Direct update check failed", err));
|
||||||
|
} else {
|
||||||
|
window.electronAPI.checkForUpdates?.();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config fetcher will trigger init()
|
// Config fetcher will trigger init()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue