checkpoint

This commit is contained in:
pavel 2026-02-27 10:57:13 +01:00
commit 208ab8042e
3 changed files with 296 additions and 26 deletions

View file

@ -40,6 +40,80 @@ const state = {
// --- Desktop Backend Configuration ---
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() {
try {
@ -73,7 +147,7 @@ function getWsUrl(path) {
urlStr = `${proto}://${url.host}${normalizedPath}`;
}
const token = localStorage.getItem("chattz_token");
const token = storageGet("chattz_token");
if (token) {
urlStr += (urlStr.includes('?') ? '&' : '?') + `token=${token}`;
}
@ -180,7 +254,7 @@ async function api(path, options = {}) {
...(options.headers || {}),
};
const token = localStorage.getItem("chattz_token");
const token = storageGet("chattz_token");
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
@ -192,7 +266,7 @@ async function api(path, options = {}) {
// Handle 401 Unauthorized via Refresh Token
if (res.status === 401 && !path.includes('/auth/refresh')) {
const refreshToken = localStorage.getItem("chattz_refresh_token");
const refreshToken = storageGet("chattz_refresh_token");
if (refreshToken) {
try {
const refreshUrl = API_BASE_URL ? `${API_BASE_URL}/auth/refresh` : '/auth/refresh';
@ -204,8 +278,8 @@ async function api(path, options = {}) {
if (refreshRes.ok) {
const newTokens = await refreshRes.json();
localStorage.setItem("chattz_token", newTokens.access_token);
localStorage.setItem("chattz_refresh_token", newTokens.refresh_token);
await storageSetCritical("chattz_token", newTokens.access_token);
await storageSetCritical("chattz_refresh_token", newTokens.refresh_token);
// Retry original request with new token
headers["Authorization"] = `Bearer ${newTokens.access_token}`;
@ -215,8 +289,8 @@ async function api(path, options = {}) {
throw new Error("Refresh token expired or invalid");
}
} catch (err) {
localStorage.removeItem("chattz_token");
localStorage.removeItem("chattz_refresh_token");
await storageRemoveCritical("chattz_token");
await storageRemoveCritical("chattz_refresh_token");
location.href = API_BASE_URL ? `${API_BASE_URL}/auth/login` : "/auth/login";
throw new Error("Session expired, please log in again");
}
@ -330,8 +404,6 @@ function playSound(type) {
console.warn("playSound failed", err);
}
}
const LAST_GUILD_STORAGE_KEY = "chattz:lastGuildId";
// --- Renderers ---
function renderGuilds() {
@ -348,7 +420,7 @@ function renderGuilds() {
state.selectedVoiceChannelId = null;
state.selectedDmUserId = null;
state.selectedDmDisplayName = null;
localStorage.setItem(LAST_GUILD_STORAGE_KEY, guild.id);
storageSet(LAST_GUILD_STORAGE_KEY, guild.id);
renderGuilds();
renderDMs();
await loadChannels();
@ -1258,7 +1330,7 @@ async function joinVoice() {
state.voice.ws = ws;
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.
createLocalVoiceStream()
@ -1390,7 +1462,7 @@ async function joinVoice() {
}
async function leaveVoice() {
localStorage.removeItem("active_voice_channel");
storageRemove("active_voice_channel");
if (state.voice.ws) state.voice.ws.close();
}
@ -1612,6 +1684,7 @@ function closeMobileMenus() {
let inactivityTimer = null;
let isCurrentlyIdle = false;
let updaterInitialized = false;
function resetInactivityTimer() {
if (isCurrentlyIdle) {
@ -1636,6 +1709,8 @@ document.addEventListener('click', resetInactivityTimer);
async function init() {
lucide.createIcons();
await hydrateDesktopStorage();
initUpdater();
// 1. Handle tokens in URL (from successful login redirects)
try {
@ -1644,8 +1719,8 @@ async function init() {
const refreshToken = searchParams.get("refresh_token");
if (jwtToken || refreshToken) {
if (jwtToken) localStorage.setItem("chattz_token", jwtToken);
if (refreshToken) localStorage.setItem("chattz_refresh_token", refreshToken);
if (jwtToken) await storageSetCritical("chattz_token", jwtToken);
if (refreshToken) await storageSetCritical("chattz_refresh_token", refreshToken);
searchParams.delete("token");
searchParams.delete("refresh_token");
@ -1665,8 +1740,8 @@ async function init() {
el.logoutBtn.onclick = async () => {
await leaveVoice();
localStorage.removeItem("chattz_token");
localStorage.removeItem("chattz_refresh_token");
await storageRemoveCritical("chattz_token");
await storageRemoveCritical("chattz_refresh_token");
try { await api("/auth/logout", { method: "POST" }); } catch (e) { }
location.reload();
};
@ -1705,7 +1780,7 @@ async function init() {
state.selectedVoiceChannelId = null;
state.selectedDmUserId = null;
state.selectedDmDisplayName = null;
localStorage.setItem(LAST_GUILD_STORAGE_KEY, guild.id);
storageSet(LAST_GUILD_STORAGE_KEY, guild.id);
renderGuilds();
await loadChannels();
await loadGuildMembers();
@ -1889,7 +1964,7 @@ async function init() {
if (inviteCode) {
try {
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 {
params.delete("invite");
const nextQuery = params.toString();
@ -1908,10 +1983,10 @@ async function init() {
} catch (err) { console.warn("presence sync failed", err); }
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];
state.selectedGuildId = guild.id;
localStorage.setItem(LAST_GUILD_STORAGE_KEY, guild.id);
storageSet(LAST_GUILD_STORAGE_KEY, guild.id);
renderGuilds();
await loadChannels();
await loadGuildMembers();
@ -1927,7 +2002,7 @@ async function init() {
startVoicePresencePolling();
// Auto-reconnect to voice if previously joined
const savedVoiceChannel = localStorage.getItem("active_voice_channel");
const savedVoiceChannel = storageGet("active_voice_channel");
if (savedVoiceChannel) {
console.log("Auto-reconnecting to voice channel:", savedVoiceChannel);
state.selectedVoiceChannelId = savedVoiceChannel;
@ -1936,7 +2011,6 @@ async function init() {
}, 800);
}
initUpdater();
lucide.createIcons();
} catch (err) {
console.error("init failed", err);
@ -1947,6 +2021,53 @@ async function init() {
function initUpdater() {
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) => {
console.log("Update available:", info.version);
@ -1978,6 +2099,20 @@ function initUpdater() {
el.updateInstallBtn.onclick = () => {
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()