checkpoint
This commit is contained in:
parent
3735162f41
commit
208ab8042e
3 changed files with 296 additions and 26 deletions
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue