diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 0000000..8169e48 Binary files /dev/null and b/build/icon.png differ diff --git a/desktop/app.js b/desktop/app.js index d260cfa..8f5a122 100644 --- a/desktop/app.js +++ b/desktop/app.js @@ -1625,6 +1625,7 @@ document.addEventListener('click', resetInactivityTimer); async function init() { lucide.createIcons(); + // 1. Handle tokens in URL (from successful login redirects) try { const searchParams = new URLSearchParams(location.search); const jwtToken = searchParams.get("token"); @@ -1644,6 +1645,7 @@ async function init() { console.error("Failed to parse token from URL:", e); } + // 2. Global Button Handlers el.loginBtn.onclick = () => { const loginPath = "/auth/login"; 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.addTextBtn.onclick = () => { - if (!state.selectedGuildId) { - alert("Create or select a server first."); - return; - } + if (!state.selectedGuildId) { alert("Create or select a server first."); return; } document.querySelector('input[name="channel-kind"][value="text"]').checked = true; el.channelModal.classList.remove("hidden"); }; el.addVoiceBtn.onclick = () => { - if (!state.selectedGuildId) { - alert("Create or select a server first."); - return; - } + if (!state.selectedGuildId) { alert("Create or select a server first."); return; } document.querySelector('input[name="channel-kind"][value="voice"]').checked = true; el.channelModal.classList.remove("hidden"); }; @@ -1684,13 +1680,11 @@ async function init() { if (el.mobileMembersBtn) el.mobileMembersBtn.onclick = toggleMobileMembers; if (el.overlay) el.overlay.onclick = closeMobileMenus; + // 3. Form Submissions el.guildForm.onsubmit = async (e) => { e.preventDefault(); try { - const guild = await api("/guilds", { - method: "POST", - body: JSON.stringify({ name: el.guildName.value }), - }); + const guild = await api("/guilds", { method: "POST", body: JSON.stringify({ name: el.guildName.value }) }); el.guildName.value = ""; el.modalContainer.classList.add("hidden"); state.guilds.push(guild); @@ -1711,25 +1705,15 @@ async function init() { el.channelForm.onsubmit = async (e) => { e.preventDefault(); - if (!state.selectedGuildId) { - alert("Select a server first."); - return; - } + 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; - } + 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, - }), + body: JSON.stringify({ guild_id: state.selectedGuildId, name: channelName, kind: kind }), }); el.channelName.value = ""; el.channelModal.classList.add("hidden"); @@ -1752,6 +1736,7 @@ async function init() { renderChannels(); renderDMs(); updateHeaderLabels(); + await joinVoice(); } } catch (err) { alert(err.message); } }; @@ -1760,21 +1745,12 @@ async function init() { 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 }), - }); + 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; - } + 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`) @@ -1785,71 +1761,48 @@ async function init() { } catch (err) { alert(err.message); } }; + // 4. Voice/Soundboard Controls 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.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 url = API_BASE_URL ? `${API_BASE_URL}/guilds/${state.selectedGuildId}/sounds` : `/guilds/${state.selectedGuildId}/sounds`; - const response = await fetch(url, { - method: 'POST', - body: formData - }); - if (!response.ok) { - const err = await response.json(); - throw new Error(err.error || 'Upload failed'); - } + const response = await fetch(url, { 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 { + } 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" + // 5. GIF Picker + const TENOR_API_KEY = "exTiFGKJ0CzESIHzVQWy3pRO8I1MAdpRomg95DBSu2sg6e7YcHgThMI4giGAx8D0"; const TENOR_CLIENT_KEY = "pavel-discord"; - el.gifBtn.onclick = () => { - el.gifPicker.classList.remove("hidden"); - searchGifs(""); // Initial featured search - }; + el.gifBtn.onclick = () => { el.gifPicker.classList.remove("hidden"); searchGifs(""); }; + el.gifPickerClose.onclick = () => { el.gifPicker.classList.add("hidden"); }; - el.gifPickerClose.onclick = () => { - el.gifPicker.classList.add("hidden"); - }; - - let searchTimeout = null; + let gifSearchTimeout = null; el.gifSearchInput.oninput = () => { - clearTimeout(searchTimeout); - searchTimeout = setTimeout(() => { - searchGifs(el.gifSearchInput.value); - }, 500); + clearTimeout(gifSearchTimeout); + gifSearchTimeout = setTimeout(() => { searchGifs(el.gifSearchInput.value); }, 500); }; async function searchGifs(query) { @@ -1858,62 +1811,35 @@ async function init() { 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 = '
Error loading GIFs
'; - } + } catch (err) { el.gifResults.innerHTML = '
Error loading GIFs
'; } } function renderGifs(gifs) { el.gifResults.innerHTML = ""; - if (!gifs || gifs.length === 0) { - el.gifResults.innerHTML = '
No GIFs found
'; - return; - } - + if (!gifs || gifs.length === 0) { el.gifResults.innerHTML = '
No GIFs found
'; 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"; - item.innerHTML = ``; - item.onclick = () => { - sendGif(fullUrl); + const img = document.createElement("img"); + img.src = previewUrl; + img.loading = "lazy"; + img.onclick = () => { + el.messageBody.value = fullUrl; el.gifPicker.classList.add("hidden"); - el.gifSearchInput.value = ""; + el.messageForm.dispatchEvent(new Event('submit')); }; + item.appendChild(img); el.gifResults.appendChild(item); }); } - async function sendGif(url) { - 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); - } - } - + // 6. Initial Data Loading & App Start try { state.me = await api("/me"); if (state.me && state.me.display_name) { @@ -1923,17 +1849,14 @@ async function init() { el.authScreen.classList.add("hidden"); el.main.classList.remove("hidden"); + // Check for invite code in URL const params = new URLSearchParams(location.search); const inviteCode = params.get("invite"); if (inviteCode) { 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); - } catch (err) { - alert(`Failed to join invite: ${err.message}`); - } finally { + } catch (err) { alert(`Failed to join invite: ${err.message}`); } finally { params.delete("invite"); const nextQuery = params.toString(); const nextUrl = `${location.pathname}${nextQuery ? `?${nextQuery}` : ""}`; @@ -1941,10 +1864,39 @@ async function init() { } } - await loadGuilds(); + 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("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 = '
Searching...
'; - 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 = '
Error loading GIFs
'; - } - } - - function renderGifs(gifs) { - el.gifResults.innerHTML = ""; - if (!gifs || gifs.length === 0) { - el.gifResults.innerHTML = '
No GIFs found
'; - 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() initializeConfig(); diff --git a/package.json b/package.json index 913919e..1d831ac 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "linux": { "target": [ "rpm" - ] + ], + "category": "Chat", + "icon": "build/icon.png" }, "win": { "target": [ @@ -54,4 +56,4 @@ "dependencies": { "electron-updater": "^6.8.3" } -} +} \ No newline at end of file