test
All checks were successful
/ upload (release) Successful in 3m21s

This commit is contained in:
pavel 2026-02-27 00:31:22 +01:00
commit 51a18472eb
3 changed files with 74 additions and 431 deletions

BIN
build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

View file

@ -1625,6 +1625,7 @@ document.addEventListener('click', resetInactivityTimer);
async function init() { async function init() {
lucide.createIcons(); lucide.createIcons();
// 1. Handle tokens in URL (from successful login redirects)
try { try {
const searchParams = new URLSearchParams(location.search); const searchParams = new URLSearchParams(location.search);
const jwtToken = searchParams.get("token"); const jwtToken = searchParams.get("token");
@ -1644,6 +1645,7 @@ async function init() {
console.error("Failed to parse token from URL:", e); console.error("Failed to parse token from URL:", e);
} }
// 2. Global Button Handlers
el.loginBtn.onclick = () => { el.loginBtn.onclick = () => {
const loginPath = "/auth/login"; const loginPath = "/auth/login";
location.href = API_BASE_URL ? `${API_BASE_URL}${loginPath}` : loginPath; location.href = API_BASE_URL ? `${API_BASE_URL}${loginPath}` : loginPath;
@ -1662,18 +1664,12 @@ async function init() {
el.modalCancel.onclick = () => { el.modalContainer.classList.add("hidden"); }; el.modalCancel.onclick = () => { el.modalContainer.classList.add("hidden"); };
el.addTextBtn.onclick = () => { el.addTextBtn.onclick = () => {
if (!state.selectedGuildId) { if (!state.selectedGuildId) { alert("Create or select a server first."); return; }
alert("Create or select a server first.");
return;
}
document.querySelector('input[name="channel-kind"][value="text"]').checked = true; document.querySelector('input[name="channel-kind"][value="text"]').checked = true;
el.channelModal.classList.remove("hidden"); el.channelModal.classList.remove("hidden");
}; };
el.addVoiceBtn.onclick = () => { el.addVoiceBtn.onclick = () => {
if (!state.selectedGuildId) { if (!state.selectedGuildId) { alert("Create or select a server first."); return; }
alert("Create or select a server first.");
return;
}
document.querySelector('input[name="channel-kind"][value="voice"]').checked = true; document.querySelector('input[name="channel-kind"][value="voice"]').checked = true;
el.channelModal.classList.remove("hidden"); el.channelModal.classList.remove("hidden");
}; };
@ -1684,13 +1680,11 @@ async function init() {
if (el.mobileMembersBtn) el.mobileMembersBtn.onclick = toggleMobileMembers; if (el.mobileMembersBtn) el.mobileMembersBtn.onclick = toggleMobileMembers;
if (el.overlay) el.overlay.onclick = closeMobileMenus; if (el.overlay) el.overlay.onclick = closeMobileMenus;
// 3. Form Submissions
el.guildForm.onsubmit = async (e) => { el.guildForm.onsubmit = async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
const guild = await api("/guilds", { const guild = await api("/guilds", { method: "POST", body: JSON.stringify({ name: el.guildName.value }) });
method: "POST",
body: JSON.stringify({ name: el.guildName.value }),
});
el.guildName.value = ""; el.guildName.value = "";
el.modalContainer.classList.add("hidden"); el.modalContainer.classList.add("hidden");
state.guilds.push(guild); state.guilds.push(guild);
@ -1711,25 +1705,15 @@ async function init() {
el.channelForm.onsubmit = async (e) => { el.channelForm.onsubmit = async (e) => {
e.preventDefault(); e.preventDefault();
if (!state.selectedGuildId) { if (!state.selectedGuildId) { alert("Select a server first."); return; }
alert("Select a server first.");
return;
}
const kindInput = document.querySelector('input[name="channel-kind"]:checked'); const kindInput = document.querySelector('input[name="channel-kind"]:checked');
const kind = kindInput ? kindInput.value : "text"; const kind = kindInput ? kindInput.value : "text";
const channelName = el.channelName.value.trim(); const channelName = el.channelName.value.trim();
if (!channelName) { if (!channelName) { alert("Channel name is required."); return; }
alert("Channel name is required.");
return;
}
try { try {
const created = await api("/channels", { const created = await api("/channels", {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({ guild_id: state.selectedGuildId, name: channelName, kind: kind }),
guild_id: state.selectedGuildId,
name: channelName,
kind: kind,
}),
}); });
el.channelName.value = ""; el.channelName.value = "";
el.channelModal.classList.add("hidden"); el.channelModal.classList.add("hidden");
@ -1752,6 +1736,7 @@ async function init() {
renderChannels(); renderChannels();
renderDMs(); renderDMs();
updateHeaderLabels(); updateHeaderLabels();
await joinVoice();
} }
} catch (err) { alert(err.message); } } catch (err) { alert(err.message); }
}; };
@ -1760,21 +1745,12 @@ async function init() {
e.preventDefault(); e.preventDefault();
const body = el.messageBody.value.trim(); const body = el.messageBody.value.trim();
if (!body) return; if (!body) return;
try { try {
if (state.selectedTextChannelId) { if (state.selectedTextChannelId) {
await api(`/channels/${state.selectedTextChannelId}/messages`, { await api(`/channels/${state.selectedTextChannelId}/messages`, { method: "POST", body: JSON.stringify({ body }) });
method: "POST",
body: JSON.stringify({ body }),
});
} else if (state.selectedDmUserId) { } else if (state.selectedDmUserId) {
await api(`/dms/${state.selectedDmUserId}/messages`, { await api(`/dms/${state.selectedDmUserId}/messages`, { method: "POST", body: JSON.stringify({ body }) });
method: "POST", } else return;
body: JSON.stringify({ body }),
});
} else {
return;
}
el.messageBody.value = ""; el.messageBody.value = "";
const messages = state.selectedTextChannelId const messages = state.selectedTextChannelId
? await api(`/channels/${state.selectedTextChannelId}/messages?limit=100`) ? await api(`/channels/${state.selectedTextChannelId}/messages?limit=100`)
@ -1785,71 +1761,48 @@ async function init() {
} catch (err) { alert(err.message); } } catch (err) { alert(err.message); }
}; };
// 4. Voice/Soundboard Controls
el.voiceVideoBtn.onclick = toggleVideo; el.voiceVideoBtn.onclick = toggleVideo;
el.voiceScreenBtn.onclick = toggleScreenShare; el.voiceScreenBtn.onclick = toggleScreenShare;
el.voiceMuteBtn.onclick = toggleMute; el.voiceMuteBtn.onclick = toggleMute;
el.voiceLeaveBtn.onclick = leaveVoice; el.voiceLeaveBtn.onclick = leaveVoice;
el.addSoundBtn.onclick = () => { el.addSoundBtn.onclick = () => { el.soundModal.classList.remove("hidden"); };
el.soundModal.classList.remove("hidden"); el.soundModalCancel.onclick = () => { el.soundModal.classList.add("hidden"); };
};
el.soundModalCancel.onclick = () => {
el.soundModal.classList.add("hidden");
};
el.soundForm.onsubmit = async (e) => { el.soundForm.onsubmit = async (e) => {
e.preventDefault(); e.preventDefault();
if (!state.selectedGuildId) return; if (!state.selectedGuildId) return;
const formData = new FormData(); const formData = new FormData();
formData.append('name', el.soundName.value); formData.append('name', el.soundName.value);
formData.append('icon', el.soundIcon.value); formData.append('icon', el.soundIcon.value);
formData.append('file', el.soundFile.files[0]); formData.append('file', el.soundFile.files[0]);
el.soundSubmitBtn.disabled = true; el.soundSubmitBtn.disabled = true;
el.soundSubmitBtn.textContent = 'Uploading...'; el.soundSubmitBtn.textContent = 'Uploading...';
try { try {
// Need to use fetch directly because api() might not handle FormData correctly depending on implementation
const url = API_BASE_URL ? `${API_BASE_URL}/guilds/${state.selectedGuildId}/sounds` : `/guilds/${state.selectedGuildId}/sounds`; const url = API_BASE_URL ? `${API_BASE_URL}/guilds/${state.selectedGuildId}/sounds` : `/guilds/${state.selectedGuildId}/sounds`;
const response = await fetch(url, { const response = await fetch(url, { method: 'POST', body: formData });
method: 'POST', if (!response.ok) { const err = await response.json(); throw new Error(err.error || 'Upload failed'); }
body: formData
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.error || 'Upload failed');
}
el.soundModal.classList.add("hidden"); el.soundModal.classList.add("hidden");
el.soundForm.reset(); el.soundForm.reset();
await loadSounds(); await loadSounds();
} catch (err) { } catch (err) { alert(err.message); } finally {
alert(err.message);
} finally {
el.soundSubmitBtn.disabled = false; el.soundSubmitBtn.disabled = false;
el.soundSubmitBtn.textContent = 'Add Sound'; el.soundSubmitBtn.textContent = 'Add Sound';
} }
}; };
// --- GIF Picker Logic --- // 5. GIF Picker
const TENOR_API_KEY = "exTiFGKJ0CzESIHzVQWy3pRO8I1MAdpRomg95DBSu2sg6e7YcHgThMI4giGAx8D0"; // Placeholder / Public key if available, otherwise "YOUR_API_KEY" const TENOR_API_KEY = "exTiFGKJ0CzESIHzVQWy3pRO8I1MAdpRomg95DBSu2sg6e7YcHgThMI4giGAx8D0";
const TENOR_CLIENT_KEY = "pavel-discord"; const TENOR_CLIENT_KEY = "pavel-discord";
el.gifBtn.onclick = () => { el.gifBtn.onclick = () => { el.gifPicker.classList.remove("hidden"); searchGifs(""); };
el.gifPicker.classList.remove("hidden"); el.gifPickerClose.onclick = () => { el.gifPicker.classList.add("hidden"); };
searchGifs(""); // Initial featured search
};
el.gifPickerClose.onclick = () => { let gifSearchTimeout = null;
el.gifPicker.classList.add("hidden");
};
let searchTimeout = null;
el.gifSearchInput.oninput = () => { el.gifSearchInput.oninput = () => {
clearTimeout(searchTimeout); clearTimeout(gifSearchTimeout);
searchTimeout = setTimeout(() => { gifSearchTimeout = setTimeout(() => { searchGifs(el.gifSearchInput.value); }, 500);
searchGifs(el.gifSearchInput.value);
}, 500);
}; };
async function searchGifs(query) { async function searchGifs(query) {
@ -1858,62 +1811,35 @@ async function init() {
const endpoint = query const endpoint = query
? `${baseUrl}/search?q=${encodeURIComponent(query)}&key=${TENOR_API_KEY}&client_key=${TENOR_CLIENT_KEY}&limit=20` ? `${baseUrl}/search?q=${encodeURIComponent(query)}&key=${TENOR_API_KEY}&client_key=${TENOR_CLIENT_KEY}&limit=20`
: `${baseUrl}/featured?key=${TENOR_API_KEY}&client_key=${TENOR_CLIENT_KEY}&limit=20`; : `${baseUrl}/featured?key=${TENOR_API_KEY}&client_key=${TENOR_CLIENT_KEY}&limit=20`;
try { try {
const res = await fetch(endpoint); const res = await fetch(endpoint);
const data = await res.json(); const data = await res.json();
renderGifs(data.results); renderGifs(data.results);
} catch (err) { } catch (err) { el.gifResults.innerHTML = '<div class="gif-loading">Error loading GIFs</div>'; }
el.gifResults.innerHTML = '<div class="gif-loading">Error loading GIFs</div>';
}
} }
function renderGifs(gifs) { function renderGifs(gifs) {
el.gifResults.innerHTML = ""; el.gifResults.innerHTML = "";
if (!gifs || gifs.length === 0) { if (!gifs || gifs.length === 0) { el.gifResults.innerHTML = '<div class="gif-loading">No GIFs found</div>'; return; }
el.gifResults.innerHTML = '<div class="gif-loading">No GIFs found</div>';
return;
}
gifs.forEach(gif => { gifs.forEach(gif => {
// Use tinygif for preview, standard gif for sending
const previewUrl = gif.media_formats.tinygif.url; const previewUrl = gif.media_formats.tinygif.url;
const fullUrl = gif.media_formats.gif.url; const fullUrl = gif.media_formats.gif.url;
const item = document.createElement("div"); const item = document.createElement("div");
item.className = "gif-item"; item.className = "gif-item";
item.innerHTML = `<img src="${previewUrl}" loading="lazy" />`; const img = document.createElement("img");
item.onclick = () => { img.src = previewUrl;
sendGif(fullUrl); img.loading = "lazy";
img.onclick = () => {
el.messageBody.value = fullUrl;
el.gifPicker.classList.add("hidden"); el.gifPicker.classList.add("hidden");
el.gifSearchInput.value = ""; el.messageForm.dispatchEvent(new Event('submit'));
}; };
item.appendChild(img);
el.gifResults.appendChild(item); el.gifResults.appendChild(item);
}); });
} }
async function sendGif(url) { // 6. Initial Data Loading & App Start
try {
if (state.selectedTextChannelId) {
await api(`/channels/${state.selectedTextChannelId}/messages`, {
method: "POST",
body: JSON.stringify({ body: url }),
});
} else if (state.selectedDmUserId) {
await api(`/dms/${state.selectedDmUserId}/messages`, {
method: "POST",
body: JSON.stringify({ body: url }),
});
}
const messages = state.selectedTextChannelId
? await api(`/channels/${state.selectedTextChannelId}/messages?limit=100`)
: await api(`/dms/${state.selectedDmUserId}/messages?limit=100`);
renderMessages(messages);
} catch (err) {
console.error("Failed to send GIF", err);
}
}
try { try {
state.me = await api("/me"); state.me = await api("/me");
if (state.me && state.me.display_name) { if (state.me && state.me.display_name) {
@ -1923,17 +1849,14 @@ async function init() {
el.authScreen.classList.add("hidden"); el.authScreen.classList.add("hidden");
el.main.classList.remove("hidden"); el.main.classList.remove("hidden");
// Check for invite code in URL
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const inviteCode = params.get("invite"); const inviteCode = params.get("invite");
if (inviteCode) { if (inviteCode) {
try { try {
const joinedGuild = await api(`/invites/${encodeURIComponent(inviteCode)}/join`, { const joinedGuild = await api(`/invites/${encodeURIComponent(inviteCode)}/join`, { method: "POST" });
method: "POST",
});
localStorage.setItem(LAST_GUILD_STORAGE_KEY, joinedGuild.id); localStorage.setItem(LAST_GUILD_STORAGE_KEY, joinedGuild.id);
} catch (err) { } catch (err) { alert(`Failed to join invite: ${err.message}`); } finally {
alert(`Failed to join invite: ${err.message}`);
} finally {
params.delete("invite"); params.delete("invite");
const nextQuery = params.toString(); const nextQuery = params.toString();
const nextUrl = `${location.pathname}${nextQuery ? `?${nextQuery}` : ""}`; const nextUrl = `${location.pathname}${nextQuery ? `?${nextQuery}` : ""}`;
@ -1941,10 +1864,39 @@ async function init() {
} }
} }
await loadGuilds(); state.guilds = await api("/guilds");
renderGuilds();
await loadDMConversations(); await loadDMConversations();
try {
const online = await api("/presence");
state.onlineUsers = new Set(online);
} catch (err) { console.warn("presence sync failed", err); }
if (state.guilds.length > 0) {
const lastGuildId = localStorage.getItem(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);
renderGuilds();
await loadChannels();
await loadGuildMembers();
await refreshVoicePresence();
updateHeaderLabels();
} else {
state.members = [];
renderMembers();
updateHeaderLabels();
}
initChatWs();
startVoicePresencePolling();
initUpdater();
lucide.createIcons();
} catch (err) { } catch (err) {
console.error("presence sync failed", err); console.error("init failed", err);
el.authScreen.classList.remove("hidden");
el.main.classList.add("hidden");
} }
} }
@ -1983,316 +1935,5 @@ function initUpdater() {
}; };
} }
async function init() {
lucide.createIcons();
try {
const searchParams = new URLSearchParams(location.search);
const jwtToken = searchParams.get("token");
const refreshToken = searchParams.get("refresh_token");
if (jwtToken || refreshToken) {
if (jwtToken) localStorage.setItem("chattz_token", jwtToken);
if (refreshToken) localStorage.setItem("chattz_refresh_token", refreshToken);
searchParams.delete("token");
searchParams.delete("refresh_token");
const nextQuery = searchParams.toString();
const nextUrl = `${location.pathname}${nextQuery ? `?${nextQuery}` : ""}`;
history.replaceState(null, "", nextUrl);
}
} catch (e) {
console.error("Failed to parse token from URL:", e);
}
el.loginBtn.onclick = () => { location.href = "/auth/login"; };
el.logoutBtn.onclick = async () => {
await leaveVoice();
localStorage.removeItem("chattz_token");
localStorage.removeItem("chattz_refresh_token");
try { await api("/auth/logout", { method: "POST" }); } catch (e) { }
location.reload();
};
el.addGuildBtn.onclick = () => { el.modalContainer.classList.remove("hidden"); };
el.createInviteBtn.onclick = createInviteLink;
el.modalCancel.onclick = () => { el.modalContainer.classList.add("hidden"); };
el.addTextBtn.onclick = () => {
if (!state.selectedGuildId) {
alert("Create or select a server first.");
return;
}
el.modalTitle.textContent = "Create Text Channel";
el.channelModal.classList.remove("hidden");
};
el.addVoiceBtn.onclick = () => {
if (!state.selectedGuildId) {
alert("Create or select a server first.");
return;
}
el.modalTitle.textContent = "Create Voice Channel";
el.channelModal.classList.remove("hidden");
};
el.channelModalCancel.onclick = () => { el.channelModal.classList.add("hidden"); };
el.guildForm.onsubmit = async (e) => {
e.preventDefault();
const name = el.guildName.value.trim();
if (!name) return;
try {
const guild = await api("/guilds", {
method: "POST",
body: JSON.stringify({ name }),
});
el.guildName.value = "";
el.modalContainer.classList.add("hidden");
state.guilds.push(guild);
state.selectedGuildId = guild.id;
state.selectedTextChannelId = null;
state.selectedVoiceChannelId = null;
state.selectedDmUserId = null;
state.selectedDmDisplayName = null;
localStorage.setItem(LAST_GUILD_STORAGE_KEY, guild.id);
renderGuilds();
await loadChannels();
await loadGuildMembers();
await refreshVoicePresence();
renderMessages([]);
updateHeaderLabels();
} catch (err) { alert(err.message); }
};
el.channelForm.onsubmit = async (e) => {
e.preventDefault();
if (!state.selectedGuildId) {
alert("Select a server first.");
return;
}
const kindInput = document.querySelector('input[name="channel-kind"]:checked');
const kind = kindInput ? kindInput.value : "text";
const channelName = el.channelName.value.trim();
if (!channelName) {
alert("Channel name is required.");
return;
}
try {
const created = await api("/channels", {
method: "POST",
body: JSON.stringify({
guild_id: state.selectedGuildId,
name: channelName,
kind: kind,
}),
});
el.channelName.value = "";
el.channelModal.classList.add("hidden");
await loadChannels();
if (created.kind === "text") {
state.selectedTextChannelId = created.id;
state.selectedVoiceChannelId = null;
state.selectedDmUserId = null;
state.selectedDmDisplayName = null;
renderChannels();
renderDMs();
updateHeaderLabels();
await api(`/channels/${created.id}/messages?limit=100`).then(renderMessages);
} else {
state.selectedVoiceChannelId = created.id;
state.selectedTextChannelId = null;
state.selectedDmUserId = null;
state.selectedDmDisplayName = null;
renderChannels();
renderDMs();
updateHeaderLabels();
await joinVoice();
}
} catch (err) { alert(err.message); }
};
el.messageForm.onsubmit = async (e) => {
e.preventDefault();
const body = el.messageBody.value.trim();
if (!body) return;
try {
if (state.selectedTextChannelId) {
await api(`/channels/${state.selectedTextChannelId}/messages`, {
method: "POST",
body: JSON.stringify({ body }),
});
} else if (state.selectedDmUserId) {
await api(`/dms/${state.selectedDmUserId}/messages`, {
method: "POST",
body: JSON.stringify({ body }),
});
} else {
return;
}
el.messageBody.value = "";
const messages = state.selectedTextChannelId
? await api(`/channels/${state.selectedTextChannelId}/messages?limit=100`)
: await api(`/dms/${state.selectedDmUserId}/messages?limit=100`);
renderMessages(messages);
await loadDMConversations();
renderDMs();
} catch (err) { alert(err.message); }
};
el.voiceVideoBtn.onclick = toggleVideo;
el.voiceScreenBtn.onclick = toggleScreenShare;
el.voiceMuteBtn.onclick = toggleMute;
el.voiceLeaveBtn.onclick = leaveVoice;
el.addSoundBtn.onclick = () => {
el.soundModal.classList.remove("hidden");
};
el.soundModalCancel.onclick = () => {
el.soundModal.classList.add("hidden");
};
el.soundForm.onsubmit = async (e) => {
e.preventDefault();
if (!state.selectedGuildId) return;
const formData = new FormData();
formData.append('name', el.soundName.value);
formData.append('icon', el.soundIcon.value);
formData.append('file', el.soundFile.files[0]);
el.soundSubmitBtn.disabled = true;
el.soundSubmitBtn.textContent = 'Uploading...';
try {
// Need to use fetch directly because api() might not handle FormData correctly depending on implementation
const response = await fetch(`/guilds/${state.selectedGuildId}/sounds`, {
method: 'POST',
body: formData
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.error || 'Upload failed');
}
el.soundModal.classList.add("hidden");
el.soundForm.reset();
await loadSounds();
} catch (err) {
alert(err.message);
} finally {
el.soundSubmitBtn.disabled = false;
el.soundSubmitBtn.textContent = 'Add Sound';
}
};
// --- GIF Picker Logic ---
const TENOR_API_KEY = "exTiFGKJ0CzESIHzVQWy3pRO8I1MAdpRomg95DBSu2sg6e7YcHgThMI4giGAx8D0"; // Placeholder / Public key if available, otherwise "YOUR_API_KEY"
const TENOR_CLIENT_KEY = "pavel-discord";
el.gifBtn.onclick = () => {
el.gifPicker.classList.remove("hidden");
searchGifs(""); // Initial featured search
};
el.gifPickerClose.onclick = () => {
el.gifPicker.classList.add("hidden");
};
let searchTimeout = null;
el.gifSearchInput.oninput = () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
searchGifs(el.gifSearchInput.value);
}, 500);
};
async function searchGifs(query) {
el.gifResults.innerHTML = '<div class="gif-loading">Searching...</div>';
const baseUrl = "https://api.klipy.com/v2";
const endpoint = query
? `${baseUrl}/search?q=${encodeURIComponent(query)}&key=${TENOR_API_KEY}&client_key=${TENOR_CLIENT_KEY}&limit=20`
: `${baseUrl}/featured?key=${TENOR_API_KEY}&client_key=${TENOR_CLIENT_KEY}&limit=20`;
try {
const res = await fetch(endpoint);
const data = await res.json();
renderGifs(data.results);
} catch (err) {
el.gifResults.innerHTML = '<div class="gif-loading">Error loading GIFs</div>';
}
}
function renderGifs(gifs) {
el.gifResults.innerHTML = "";
if (!gifs || gifs.length === 0) {
el.gifResults.innerHTML = '<div class="gif-loading">No GIFs found</div>';
return;
}
gifs.forEach(gif => {
// Use tinygif for preview, standard gif for sending
const previewUrl = gif.media_formats.tinygif.url;
const fullUrl = gif.media_formats.gif.url;
const item = document.createElement("div");
item.className = "gif-item";
const img = document.createElement("img");
img.src = previewUrl;
img.loading = "lazy";
img.onclick = () => {
el.messageBody.value = fullUrl;
el.gifPicker.classList.add("hidden");
el.messageForm.dispatchEvent(new Event('submit'));
};
item.appendChild(img);
el.gifResults.appendChild(item);
});
}
try {
state.me = await api("/me");
el.userName.textContent = state.me.display_name;
el.userAvatar.textContent = state.me.display_name[0].toUpperCase();
state.guilds = await api("/guilds");
renderGuilds();
await loadDMConversations();
try {
const online = await api("/presence");
state.onlineUsers = new Set(online);
} catch (err) { console.warn("presence sync failed", err); }
if (state.guilds.length > 0) {
const lastGuildId = localStorage.getItem(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);
renderGuilds();
await loadChannels();
await loadGuildMembers();
await refreshVoicePresence();
updateHeaderLabels();
} else {
state.members = [];
renderMembers();
updateHeaderLabels();
}
initChatWs();
startVoicePresencePolling();
initUpdater();
lucide.createIcons();
} catch (err) {
console.error("init failed", err);
el.authScreen.classList.remove("hidden");
el.main.classList.add("hidden");
}
}
// Config fetcher will trigger init() // Config fetcher will trigger init()
initializeConfig(); initializeConfig();

View file

@ -22,7 +22,9 @@
"linux": { "linux": {
"target": [ "target": [
"rpm" "rpm"
] ],
"category": "Chat",
"icon": "build/icon.png"
}, },
"win": { "win": {
"target": [ "target": [
@ -54,4 +56,4 @@
"dependencies": { "dependencies": {
"electron-updater": "^6.8.3" "electron-updater": "^6.8.3"
} }
} }