From 1762368e2d4df0b97c4414e9975f0b994ed72fb3 Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Thu, 26 Feb 2026 22:58:46 +0000 Subject: [PATCH 01/24] chore: bump version to 0.0.46 [skip ci] --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7c39f92..945d706 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "chattz-electron", - "version": "0.1.0", + "version": "0.0.46", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chattz-electron", - "version": "0.1.0", + "version": "0.0.46", "dependencies": { "electron-updater": "^6.8.3" }, diff --git a/package.json b/package.json index ef4716e..913919e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chattz-electron", - "version": "0.1.0", + "version": "0.0.46", "description": "Electron frontend for Chattz", "author": "Pavel Flegr ", "homepage": "https://discord.flegr.me", From 51a18472eb92282baa370b54167a7fdab2632c58 Mon Sep 17 00:00:00 2001 From: pavel Date: Fri, 27 Feb 2026 00:31:22 +0100 Subject: [PATCH 02/24] test --- build/icon.png | Bin 0 -> 440879 bytes desktop/app.js | 499 +++++++------------------------------------------ package.json | 6 +- 3 files changed, 74 insertions(+), 431 deletions(-) create mode 100644 build/icon.png diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8169e48c850c3572cc7e7657923123a36fde5e4b GIT binary patch literal 440879 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%0A?|$g1PRx$q!>1|{U8`V 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

$;1VDJCnM0I`TN{ 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%0D<>u+BF;po@83ub_& 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 z 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 Hu0mjflJF%4 literal 0 HcmV?d00001 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 From 59e4ed98685890213580e91b60ee18588a79f3e3 Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Thu, 26 Feb 2026 23:34:19 +0000 Subject: [PATCH 03/24] chore: bump version to 0.0.47 [skip ci] --- package-lock.json | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 945d706..c2c2a47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "chattz-electron", - "version": "0.0.46", + "version": "0.0.47", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chattz-electron", - "version": "0.0.46", + "version": "0.0.47", "dependencies": { "electron-updater": "^6.8.3" }, diff --git a/package.json b/package.json index 1d831ac..cbc3726 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chattz-electron", - "version": "0.0.46", + "version": "0.0.47", "description": "Electron frontend for Chattz", "author": "Pavel Flegr ", "homepage": "https://discord.flegr.me", @@ -56,4 +56,4 @@ "dependencies": { "electron-updater": "^6.8.3" } -} \ No newline at end of file +} From 88e0a0bbfae1ea20595f9ea4cc44ae1e4b8eb1e6 Mon Sep 17 00:00:00 2001 From: pavel Date: Fri, 27 Feb 2026 01:01:53 +0100 Subject: [PATCH 04/24] fix --- desktop/app.js | 11 +++++++++-- package.json | 4 ++-- static/app.js | 9 +++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/desktop/app.js b/desktop/app.js index 8f5a122..bff5420 100644 --- a/desktop/app.js +++ b/desktop/app.js @@ -455,16 +455,23 @@ function renderChannels() { slider.addEventListener('click', (e) => e.stopPropagation()); } - // Show slider on right click - pRow.addEventListener('contextmenu', (e) => { + const toggleVolumeSlider = (e) => { if (p.user_id === state.me.id) return; e.preventDefault(); + e.stopPropagation(); if (state.voice.visibleVolumeSliders.has(p.user_id)) { state.voice.visibleVolumeSliders.delete(p.user_id); } else { state.voice.visibleVolumeSliders.add(p.user_id); } renderChannels(); + }; + + // Electron can swallow contextmenu events on some platforms; use + // right-button mousedown as a reliable fallback for slider toggle. + pRow.addEventListener('contextmenu', toggleVolumeSlider); + pRow.addEventListener('mousedown', (e) => { + if (e.button === 2) toggleVolumeSlider(e); }); pList.appendChild(pRow); diff --git a/package.json b/package.json index cbc3726..4f274d3 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "productName": "Chattz", "linux": { "target": [ - "rpm" + "AppImage" ], "category": "Chat", "icon": "build/icon.png" @@ -56,4 +56,4 @@ "dependencies": { "electron-updater": "^6.8.3" } -} +} \ No newline at end of file diff --git a/static/app.js b/static/app.js index 89614a0..d6b264f 100644 --- a/static/app.js +++ b/static/app.js @@ -407,16 +407,21 @@ function renderChannels() { slider.addEventListener('click', (e) => e.stopPropagation()); } - // Show slider on right click - pRow.addEventListener('contextmenu', (e) => { + const toggleVolumeSlider = (e) => { if (p.user_id === state.me.id) return; e.preventDefault(); + e.stopPropagation(); if (state.voice.visibleVolumeSliders.has(p.user_id)) { state.voice.visibleVolumeSliders.delete(p.user_id); } else { state.voice.visibleVolumeSliders.add(p.user_id); } renderChannels(); + }; + + pRow.addEventListener('contextmenu', toggleVolumeSlider); + pRow.addEventListener('mousedown', (e) => { + if (e.button === 2) toggleVolumeSlider(e); }); pList.appendChild(pRow); From 0b5fc9914c6d384eef3a54fffe691d233b9b3275 Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Fri, 27 Feb 2026 00:02:53 +0000 Subject: [PATCH 05/24] chore: bump version to 0.0.48 [skip ci] --- package-lock.json | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index c2c2a47..f657203 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "chattz-electron", - "version": "0.0.47", + "version": "0.0.48", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chattz-electron", - "version": "0.0.47", + "version": "0.0.48", "dependencies": { "electron-updater": "^6.8.3" }, diff --git a/package.json b/package.json index 4f274d3..9949c75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chattz-electron", - "version": "0.0.47", + "version": "0.0.48", "description": "Electron frontend for Chattz", "author": "Pavel Flegr ", "homepage": "https://discord.flegr.me", @@ -56,4 +56,4 @@ "dependencies": { "electron-updater": "^6.8.3" } -} \ No newline at end of file +} From 3735162f41a98c5a721082c63051a78d31426618 Mon Sep 17 00:00:00 2001 From: pavel Date: Fri, 27 Feb 2026 09:55:48 +0100 Subject: [PATCH 06/24] refactor --- desktop/app.js | 1947 +----------------------------------- desktop/index.html | 4 +- static/app.js | 1932 +----------------------------------- static/index.html | 2 +- static/shared/app-core.js | 1984 +++++++++++++++++++++++++++++++++++++ 5 files changed, 1989 insertions(+), 3880 deletions(-) create mode 100644 static/shared/app-core.js diff --git a/desktop/app.js b/desktop/app.js index bff5420..e0b9fa7 100644 --- a/desktop/app.js +++ b/desktop/app.js @@ -1,1946 +1 @@ -const state = { - me: null, - guilds: [], - channels: [], - dmConversations: [], - members: [], - voicePresence: new Map(), - selectedGuildId: null, - selectedTextChannelId: null, - selectedDmUserId: null, - selectedDmDisplayName: null, - selectedVoiceChannelId: null, - voice: { - ws: null, - joinedChannelId: null, - localStream: null, - rawStream: null, - videoStream: null, - screenStream: null, - peerConnections: new Map(), - videoSenders: new Map(), - screenSenders: new Map(), - muted: false, - sharingVideo: false, - sharingScreen: false, - deepFilterProcessor: null, - deepFilterModule: null, - viewMode: 'chat', // 'chat' or 'video' - iceServers: [{ urls: "stun:stun.l.google.com:19302" }], - peerGainNodes: new Map(), // userId -> GainNode - visibleVolumeSliders: new Set(), // userIds whose sliders are visible - }, - voicePresencePollId: null, - onlineUsers: new Set(), - idleUsers: new Set(), - userVolumes: new Map(), // userId -> volume (0.0 to 2.0) -}; - -// --- Desktop Backend Configuration --- -let API_BASE_URL = ''; // Will be initialized via IPC - -async function initializeConfig() { - try { - const config = await window.electronAPI.getConfig(); - API_BASE_URL = config.backendUrl; - console.log(`Backend initialized: ${API_BASE_URL}`); - - // Start the app now that config is ready - init(); - } catch (err) { - console.error("Failed to fetch config via IPC", err); - // Fallback to URL search params if IPC fails (unlikely in desktop app) - const urlParams = new URLSearchParams(window.location.search); - API_BASE_URL = urlParams.get('backend') || ''; - } -} - -// Start config fetch immediately -initializeConfig(); - -function getWsUrl(path) { - let urlStr; - if (!API_BASE_URL) { - const proto = location.protocol === "https:" ? "wss" : "ws"; - urlStr = `${proto}://${location.host}${path}`; - } else { - const url = new URL(API_BASE_URL); - const proto = url.protocol === "https:" ? "wss" : "ws"; - const normalizedPath = path.startsWith('/') ? path : `/${path}`; - urlStr = `${proto}://${url.host}${normalizedPath}`; - } - - const token = localStorage.getItem("chattz_token"); - if (token) { - urlStr += (urlStr.includes('?') ? '&' : '?') + `token=${token}`; - } - return urlStr; -} - - - - - - -const el = { - authScreen: document.getElementById("auth-screen"), - loginBtn: document.getElementById("login-btn"), - main: document.getElementById("main"), - status: document.getElementById("status"), - - // Guilds - guildList: document.getElementById("guild-list"), - addGuildBtn: document.getElementById("add-guild-btn"), - guildTitle: document.getElementById("guild-title"), - createInviteBtn: document.getElementById("create-invite-btn"), - inviteCopiedBadge: document.getElementById("invite-copied-badge"), - - // Channels - channelList: document.getElementById("channel-list"), - voiceChannelList: document.getElementById("voice-channel-list"), - addTextBtn: document.getElementById("add-text-btn"), - addVoiceBtn: document.getElementById("add-voice-btn"), - dmList: document.getElementById("dm-list"), - channelTitle: document.getElementById("channel-title"), - - // Messages - messageList: document.getElementById("message-list"), - messageForm: document.getElementById("message-form"), - messageBody: document.getElementById("message-body"), - - // User Panel - userName: document.getElementById("user-name"), - userAvatar: document.getElementById("user-avatar"), - logoutBtn: document.getElementById("logout-btn"), - updateNotifier: document.getElementById("update-notifier"), - updateDownloadBtn: document.getElementById("update-download-btn"), - updateInstallBtn: document.getElementById("update-install-btn"), - - // Voice Connection - voiceConnection: document.getElementById("voice-connection"), - vcChannelName: document.getElementById("vc-channel-name"), - voiceVideoBtn: document.getElementById("voice-video-btn"), - voiceScreenBtn: document.getElementById("voice-screen-btn"), - voiceMuteBtn: document.getElementById("voice-mute-btn"), - voiceLeaveBtn: document.getElementById("voice-leave-btn"), - videoGrid: document.getElementById("video-grid"), - messageForm: document.getElementById("message-form"), - messageInputWrapper: document.querySelector(".chat-input-wrapper"), - - // Members - memberList: document.getElementById("member-list"), - - // Modals - modalContainer: document.getElementById("modal-container"), - guildForm: document.getElementById("guild-form"), - guildName: document.getElementById("guild-name"), - modalCancel: document.getElementById("modal-cancel"), - - channelModal: document.getElementById("channel-modal"), - channelForm: document.getElementById("channel-form"), - channelName: document.getElementById("channel-name"), - channelModalCancel: document.getElementById("channel-modal-cancel"), - - // Sound Board - soundboard: document.getElementById('soundboard'), - soundboardGrid: document.getElementById('soundboard-grid'), - addSoundBtn: document.getElementById('add-sound-btn'), - soundModal: document.getElementById('sound-modal'), - soundForm: document.getElementById('sound-form'), - soundName: document.getElementById('sound-name'), - soundIcon: document.getElementById('sound-icon'), - soundFile: document.getElementById('sound-file'), - soundModalCancel: document.getElementById('sound-modal-cancel'), - soundSubmitBtn: document.getElementById('sound-submit-btn'), - - // Mobile - mobileMenuBtn: document.getElementById("mobile-menu-btn"), - mobileMembersBtn: document.getElementById("mobile-members-btn"), - overlay: document.getElementById("overlay"), - utilitySidebar: document.getElementById("utility-sidebar"), - - // GIF Picker - gifBtn: document.getElementById("gif-btn"), - gifPicker: document.getElementById("gif-picker"), - gifPickerClose: document.getElementById("gif-picker-close"), - gifSearchInput: document.getElementById("gif-search-input"), - gifResults: document.getElementById("gif-results"), -}; - -// --- API Helpers --- - -async function api(path, options = {}) { - const fullUrl = API_BASE_URL ? (path.startsWith('http') ? path : `${API_BASE_URL}${path}`) : path; - - const headers = { - "content-type": "application/json", - ...(options.headers || {}), - }; - - const token = localStorage.getItem("chattz_token"); - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } - - let res = await fetch(fullUrl, { - ...options, - headers, - }); - - // Handle 401 Unauthorized via Refresh Token - if (res.status === 401 && !path.includes('/auth/refresh')) { - const refreshToken = localStorage.getItem("chattz_refresh_token"); - if (refreshToken) { - try { - const refreshUrl = API_BASE_URL ? `${API_BASE_URL}/auth/refresh` : '/auth/refresh'; - const refreshRes = await fetch(refreshUrl, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ refresh_token: refreshToken }) - }); - - if (refreshRes.ok) { - const newTokens = await refreshRes.json(); - localStorage.setItem("chattz_token", newTokens.access_token); - localStorage.setItem("chattz_refresh_token", newTokens.refresh_token); - - // Retry original request with new token - headers["Authorization"] = `Bearer ${newTokens.access_token}`; - res = await fetch(fullUrl, { ...options, headers }); - } else { - // Both tokens invalid/expired, wipe out to force clear state - throw new Error("Refresh token expired or invalid"); - } - } catch (err) { - localStorage.removeItem("chattz_token"); - localStorage.removeItem("chattz_refresh_token"); - location.href = API_BASE_URL ? `${API_BASE_URL}/auth/login` : "/auth/login"; - throw new Error("Session expired, please log in again"); - } - } - } - - if (!res.ok) { - let detail = "request failed"; - try { - const body = await res.json(); - detail = body.error || detail; - } catch { } - throw new Error(`${res.status}: ${detail}`); - } - - if (res.status === 204) return null; - return res.json(); -} - -// --- Utils --- - -function shortName(name) { - if (!name || typeof name !== 'string') return "?"; - const trimmed = name.trim(); - if (!trimmed) return "?"; - const words = trimmed.split(/\s+/).filter(w => w.length > 0).slice(0, 2); - if (words.length === 0) return "?"; - if (words.length === 1) return words[0].substring(0, 2).toUpperCase(); - return words.map((w) => w[0]?.toUpperCase() || "").join(""); -} - -function escapeHtml(s) { - if (s === null || s === undefined) return ""; - return String(s) - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """) - .replaceAll("'", "'"); -} - -function formatMessageBody(body) { - const escaped = escapeHtml(body); - // Detect GIF URLs (simple regex for demo) - const urlRegex = /(https?:\/\/[^\s]+(?:\.gif|\/giphy\.gif)[^\s]*)/gi; - if (urlRegex.test(body)) { - return escaped.replace(urlRegex, (url) => { - return `
`; - }); - } - return escaped; -} - -function formatDate(isoString) { - const d = new Date(isoString); - const now = new Date(); - const isToday = d.toDateString() === now.toDateString(); - - if (isToday) { - return `Today at ${d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; - } - return d.toLocaleDateString(); -} - -let audioCtx = null; - -// Global interaction listener to unlock AudioContext (autoplay policy) -window.addEventListener('click', () => { - if (audioCtx && audioCtx.state === 'suspended') { - audioCtx.resume(); - } -}, { once: true }); - -function playSound(type) { - try { - if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - if (audioCtx.state === 'suspended') audioCtx.resume(); - - const sounds = { - message: { freq: 880, type: 'sine', duration: 0.1, volume: 0.1 }, - dm: { freq: [660, 880], type: 'sine', duration: 0.15, volume: 0.1 }, - join: { freq: [440, 880], type: 'sine', duration: 0.2, volume: 0.1 }, - leave: { freq: [880, 440], type: 'sine', duration: 0.2, volume: 0.1 }, - 'peer-join': { freq: [660, 990], type: 'sine', duration: 0.15, volume: 0.05 }, - 'peer-leave': { freq: [990, 660], type: 'sine', duration: 0.15, volume: 0.05 }, - }; - - const s = sounds[type]; - if (!s) return; - - const osc = audioCtx.createOscillator(); - const gain = audioCtx.createGain(); - - osc.type = s.type; - if (Array.isArray(s.freq)) { - osc.frequency.setValueAtTime(s.freq[0], audioCtx.currentTime); - osc.frequency.exponentialRampToValueAtTime(s.freq[1], audioCtx.currentTime + s.duration); - } else { - osc.frequency.setValueAtTime(s.freq, audioCtx.currentTime); - } - - gain.gain.setValueAtTime(s.volume, audioCtx.currentTime); - gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + s.duration); - - osc.connect(gain); - gain.connect(audioCtx.destination); - - osc.start(); - osc.stop(audioCtx.currentTime + s.duration); - } catch (err) { - console.warn("playSound failed", err); - } -} -const LAST_GUILD_STORAGE_KEY = "chattz:lastGuildId"; - -// --- Renderers --- - -function renderGuilds() { - el.guildList.innerHTML = ""; - - for (const guild of state.guilds) { - const btn = document.createElement("button"); - btn.className = `guild-pill ${state.selectedGuildId === guild.id ? "active" : ""}`; - btn.title = guild.name; - btn.textContent = shortName(guild.name); - btn.onclick = async () => { - 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(); - renderDMs(); - await loadChannels(); - await loadGuildMembers(); - await refreshVoicePresence(); - startVoicePresencePolling(); - renderMessages([]); - updateHeaderLabels(); - }; - el.guildList.appendChild(btn); - } -} - -function renderChannels() { - el.channelList.innerHTML = ""; - el.voiceChannelList.innerHTML = ""; - - for (const channel of state.channels) { - const row = document.createElement("button"); - row.className = `channel-row ${(channel.kind === 'text' && state.selectedTextChannelId === channel.id) || - (channel.kind === 'voice' && state.selectedVoiceChannelId === channel.id) ? "active" : "" - }`; - - const iconName = channel.kind === 'text' ? 'hash' : 'volume-2'; - row.innerHTML = ` ${escapeHtml(channel.name)}`; - - row.onclick = async () => { - if (channel.kind === 'text') { - state.selectedDmUserId = null; - state.selectedDmDisplayName = null; - state.selectedTextChannelId = channel.id; - state.voice.viewMode = 'chat'; - renderChannels(); - renderDMs(); - updateView(); - updateHeaderLabels(); - const messages = await api(`/channels/${channel.id}/messages?limit=100`); - renderMessages(messages); - } else { - state.selectedVoiceChannelId = channel.id; - state.voice.viewMode = 'video'; - renderChannels(); - renderDMs(); - updateView(); - updateHeaderLabels(); - try { - await joinVoice(); - } catch (err) { - console.error("joinVoice failed from channel click", err); - alert(`Voice join failed: ${err?.message || err}`); - } - } - if (window.innerWidth <= 768) closeMobileMenus(); - }; - - if (channel.kind === 'text') { - el.channelList.appendChild(row); - } else { - el.voiceChannelList.appendChild(row); - - const participants = state.voicePresence.get(channel.id) || []; - if (participants.length > 0) { - const pList = document.createElement("div"); - pList.className = "voice-row-members channel-list"; - pList.style.paddingLeft = "24px"; - for (const p of participants) { - const pRow = document.createElement("div"); - pRow.className = `channel-row ${p.is_speaking ? 'voice-speaking' : ''}`; - pRow.style.padding = "2px 8px"; - pRow.style.flexWrap = "wrap"; - - let sliderHtml = ''; - if (p.user_id !== state.me.id) { - const vol = state.userVolumes.get(p.user_id) ?? 1.0; - const isVisible = state.voice.visibleVolumeSliders.has(p.user_id); - sliderHtml = ` -
- - - ${Math.round(vol * 100)}% -
- `; - } - - pRow.innerHTML = ` -
-
${shortName(p.display_name)}
- ${escapeHtml(p.display_name)} - ${p.is_muted ? '' : ''} -
- ${sliderHtml} - `; - - const slider = pRow.querySelector('.volume-slider'); - if (slider) { - slider.addEventListener('input', (e) => { - const val = parseFloat(e.target.value); - state.userVolumes.set(p.user_id, val); - pRow.querySelector('.vol-pct').textContent = `${Math.round(val * 100)}%`; - - const gainNode = state.voice.peerGainNodes.get(p.user_id); - if (gainNode) { - gainNode.gain.setTargetAtTime(val, state.voice.audioContext.currentTime, 0.05); - } - }); - // Stop propagation to prevent joining channel again when clicking slider - slider.addEventListener('click', (e) => e.stopPropagation()); - } - - const toggleVolumeSlider = (e) => { - if (p.user_id === state.me.id) return; - e.preventDefault(); - e.stopPropagation(); - if (state.voice.visibleVolumeSliders.has(p.user_id)) { - state.voice.visibleVolumeSliders.delete(p.user_id); - } else { - state.voice.visibleVolumeSliders.add(p.user_id); - } - renderChannels(); - }; - - // Electron can swallow contextmenu events on some platforms; use - // right-button mousedown as a reliable fallback for slider toggle. - pRow.addEventListener('contextmenu', toggleVolumeSlider); - pRow.addEventListener('mousedown', (e) => { - if (e.button === 2) toggleVolumeSlider(e); - }); - - pList.appendChild(pRow); - } - el.voiceChannelList.appendChild(pList); - } - } - } - lucide.createIcons(); -} - -function renderDMs() { - el.dmList.innerHTML = ""; - for (const dm of state.dmConversations) { - const isOnline = state.onlineUsers.has(dm.user_id); - const isIdle = state.idleUsers.has(dm.user_id); - const row = document.createElement("button"); - row.className = `channel-row ${state.selectedDmUserId === dm.user_id ? "active" : ""}`; - row.innerHTML = ` -
- -
-
- ${escapeHtml(dm.display_name)} - `; - row.onclick = async () => { - state.selectedDmUserId = dm.user_id; - state.selectedDmDisplayName = dm.display_name; - state.selectedTextChannelId = null; - state.selectedVoiceChannelId = null; - state.voice.viewMode = 'chat'; - renderChannels(); - renderDMs(); - updateView(); - updateHeaderLabels(); - const messages = await api(`/dms/${dm.user_id}/messages?limit=100`); - renderMessages(messages); - }; - el.dmList.appendChild(row); - } - - lucide.createIcons(); -} - -function renderMessages(messages) { - el.messageList.innerHTML = ""; - let lastAuthorId = null; - let lastTime = null; - - for (const m of messages.slice().reverse()) { - const row = document.createElement("div"); - const mDate = new Date(m.created_at); - const isGrouped = lastAuthorId === m.author_user_id && - lastTime && (mDate - lastTime < 300000); // 5 minutes - - row.className = `msg ${isGrouped ? "msg-grouped" : ""}`; - - const displayName = m.author_display_name || "Unknown User"; - - if (isGrouped) { - row.innerHTML = `
${formatMessageBody(m.body)}
`; - } else { - row.innerHTML = ` -
${shortName(displayName)}
-
-
- ${escapeHtml(displayName)} - ${formatDate(m.created_at)} -
-
${formatMessageBody(m.body)}
-
- `; - } - el.messageList.appendChild(row); - lastAuthorId = m.author_user_id; - lastTime = mDate; - } - - if (messages.length > 0) { - state.lastMessageId = messages[0].id; - } - - el.messageList.scrollTop = el.messageList.scrollHeight; -} - -function renderMembers() { - el.memberList.innerHTML = ""; - for (const m of state.members) { - const isOnline = state.onlineUsers.has(m.id); - const isIdle = state.idleUsers.has(m.id); - const row = document.createElement("div"); - row.className = "member-row"; - row.innerHTML = ` -
- ${shortName(m.display_name)} -
-
-
${escapeHtml(m.display_name)}
- `; - row.style.cursor = "pointer"; - row.onclick = async () => { - if (state.me && m.id === state.me.id) return; - state.selectedDmUserId = m.id; - state.selectedDmDisplayName = m.display_name; - state.selectedTextChannelId = null; - state.selectedVoiceChannelId = null; - state.voice.viewMode = 'chat'; - renderChannels(); - renderDMs(); - updateView(); - updateHeaderLabels(); - const messages = await api(`/dms/${m.id}/messages?limit=100`); - renderMessages(messages); - }; - el.memberList.appendChild(row); - } -} - -function updateHeaderLabels() { - const guild = state.guilds.find((g) => g.id === state.selectedGuildId); - el.guildTitle.textContent = guild ? guild.name : "No server selected"; - - if (state.selectedDmUserId) { - const dmFromConversations = state.dmConversations.find((u) => u.user_id === state.selectedDmUserId); - const dmFromMembers = state.members.find((m) => m.id === state.selectedDmUserId); - const displayName = dmFromConversations?.display_name || dmFromMembers?.display_name || state.selectedDmDisplayName; - const dmName = displayName ? `@${displayName}` : "Direct Message"; - el.channelTitle.textContent = dmName; - el.messageBody.placeholder = displayName - ? `Message @${displayName}` - : "Message"; - return; - } - - if (state.voice.viewMode === 'video' && state.selectedVoiceChannelId) { - const channel = state.channels.find((c) => c.id === state.selectedVoiceChannelId); - el.channelTitle.textContent = channel ? channel.name : "Voice Arena"; - return; - } - - const channel = state.channels.find((c) => c.id === state.selectedTextChannelId); - el.channelTitle.textContent = channel ? channel.name : "Select a channel"; - el.messageBody.placeholder = channel ? `Message #${channel.name}` : "Select a channel"; -} - -async function createInviteLink() { - if (!state.selectedGuildId) { - alert("Select a server first."); - return; - } - try { - const invite = await api(`/guilds/${state.selectedGuildId}/invites`, { - method: "POST", - body: JSON.stringify({ max_uses: 50, expires_in_hours: 24 }), - }); - // Use the backend URL if available, otherwise fallback to current origin - const base = API_BASE_URL ? new URL(API_BASE_URL).origin : location.origin; - const link = `${base}/?invite=${encodeURIComponent(invite.code)}`; - - // Use Electron's native clipboard API if available (defined in preload.js) - if (window.electronAPI && window.electronAPI.copyToClipboard) { - try { - await window.electronAPI.copyToClipboard(link); - showCopiedBadge(); - return; - } catch (e) { - console.error("Electron clipboard write failed:", e); - } - } - - // Fallback to navigator.clipboard - try { - await navigator.clipboard.writeText(link); - showCopiedBadge(); - } catch (err) { - console.error("Navigator clipboard write failed:", err); - prompt("Copy invite link:", link); - } - } catch (err) { - alert(err.message); - } -} - -function showCopiedBadge() { - el.inviteCopiedBadge.classList.remove("hidden"); - setTimeout(() => { - el.inviteCopiedBadge.classList.add("hidden"); - }, 2000); -} - -// --- Logic --- - -async function loadGuilds() { - state.guilds = await api("/guilds"); - renderGuilds(); -} - -async function loadDMConversations() { - state.dmConversations = await api("/dms"); - renderDMs(); -} - -async function loadGuildMembers() { - if (!state.selectedGuildId) { - state.members = []; - renderMembers(); - return; - } - state.members = await api(`/guilds/${state.selectedGuildId}/members`); - renderMembers(); -} - -async function loadChannels() { - if (!state.selectedGuildId) { - state.channels = []; - renderChannels(); - return; - } - - state.channels = await api(`/guilds/${state.selectedGuildId}/channels`); - renderChannels(); -} - -async function refreshVoicePresence() { - if (!state.selectedGuildId) { - state.voicePresence.clear(); - renderChannels(); - return; - } - - try { - const res = await api(`/guilds/${state.selectedGuildId}/voice-presence`); - state.voicePresence.clear(); - for (const entry of res.channels || []) { - state.voicePresence.set(entry.channel_id, entry.participants || []); - } - renderChannels(); - } catch (err) { - console.warn("voice presence failed", err); - } -} - -function startVoicePresencePolling() { - if (state.voicePresencePollId) clearInterval(state.voicePresencePollId); - state.voicePresencePollId = setInterval(() => { - refreshVoicePresence().catch(() => { }); - }, 3000); -} - -function initChatWs() { - if (state.chatWs) state.chatWs.close(); - const wsUrl = getWsUrl('/ws'); - const ws = new WebSocket(wsUrl); - state.chatWs = ws; - - ws.onmessage = (event) => { - const msg = JSON.parse(event.data); - const authorId = msg.message?.author_user_id; - const isFromMe = authorId && state.me && authorId === state.me.id; - - if (msg.type === "message_created") { - if (!isFromMe) playSound('message'); - if (state.selectedTextChannelId === msg.channel_id) { - api(`/channels/${state.selectedTextChannelId}/messages?limit=100`).then(renderMessages); - } - } else if (msg.type === "dm_created") { - if (!isFromMe) playSound('dm'); - if (state.selectedDmUserId === msg.other_user_id) { - api(`/dms/${state.selectedDmUserId}/messages?limit=100`).then(renderMessages); - playSound('dm'); - loadDMConversations().then(renderDMs); - } else { - loadDMConversations().then(renderDMs); - } - } else if (msg.type === "user_presence") { - if (msg.online) { - state.onlineUsers.add(msg.user_id); - if (msg.idle) { - state.idleUsers.add(msg.user_id); - } else { - state.idleUsers.delete(msg.user_id); - } - } else { - state.onlineUsers.delete(msg.user_id); - state.idleUsers.delete(msg.user_id); - } - renderMembers(); - renderDMs(); - } - }; - - ws.onclose = () => { - console.log("Chat WS closed, reconnecting..."); - setTimeout(initChatWs, 3000); - }; -} - -// --- Voice --- - -function getVoiceWsUrl(channelId) { - return getWsUrl(`/channels/${channelId}/voice/ws`); -} - -function shouldInitiateOffer(peerId) { - if (!state.me || !state.me.id) return false; - return state.me.id > peerId; -} - -function stopAndClearAudioPipeline() { - if (state.voice.deepFilterProcessor) { - state.voice.deepFilterProcessor.destroy(); - state.voice.deepFilterProcessor = null; - } - if (state.voice.localStream) { - for (const track of state.voice.localStream.getTracks()) track.stop(); - } - if (state.voice.rawStream && state.voice.rawStream !== state.voice.localStream) { - for (const track of state.voice.rawStream.getTracks()) track.stop(); - } - if (state.voice.audioContext) { - state.voice.audioContext.close().catch(() => { }); - } - state.voice.localStream = null; - state.voice.rawStream = null; - state.voice.audioContext = null; -} - -function stopAndClearVideoPipeline() { - if (state.voice.videoStream) { - for (const track of state.voice.videoStream.getTracks()) track.stop(); - } - state.voice.videoStream = null; - state.voice.sharingVideo = false; - document.getElementById(`video-${state.me.id}-camera`)?.parentElement?.remove(); -} - -function stopAndClearScreenPipeline() { - if (state.voice.screenStream) { - for (const track of state.voice.screenStream.getTracks()) track.stop(); - } - state.voice.screenStream = null; - state.voice.sharingScreen = false; - document.getElementById(`video-${state.me.id}-screen`)?.parentElement?.remove(); -} - -function updateVideoGridVisibility() { - // Persistence logic: we always update the grid visibility based on participants - // however the actual container visibility in the UI is managed by updateView() -} - -function updateView() { - if (state.voice.viewMode === 'video') { - el.videoGrid.classList.remove("hidden"); - el.messageList.classList.add("hidden"); - el.messageInputWrapper.classList.add("hidden"); - } else { - el.videoGrid.classList.add("hidden"); - el.messageList.classList.remove("hidden"); - el.messageInputWrapper.classList.remove("hidden"); - } -} - -function renderVideo(peerId, displayName, stream, source) { - const videoId = `video-${peerId}-${source}`; - let videoEl = document.getElementById(videoId); - if (!videoEl) { - const container = document.createElement("div"); - container.className = "video-item"; - const label = source === 'screen' ? `${escapeHtml(displayName)}'s Screen` : escapeHtml(displayName); - container.innerHTML = ` - -
${label}
- `; - container.onclick = () => { - const isFullscreen = container.classList.contains('fullscreen'); - // Reset all - document.querySelectorAll('.video-item').forEach(el => el.classList.remove('fullscreen')); - - if (isFullscreen) { - el.videoGrid.classList.remove('has-fullscreen'); - } else { - container.classList.add('fullscreen'); - el.videoGrid.classList.add('has-fullscreen'); - } - }; - - el.videoGrid.appendChild(container); - videoEl = container.querySelector("video"); - } - videoEl.srcObject = stream; -} - -async function buildAudioPipeline(rawStream) { - const ctx = new AudioContext(); - state.voice.audioContext = ctx; - const source = ctx.createMediaStreamSource(rawStream); - let processedSource = source; - - try { - const deepFilter = await getDeepFilterModule(); - const processor = new deepFilter.DeepFilterNet3Core({ - sampleRate: 48000, - noiseReductionLevel: 70, - assetConfig: { - cdnUrl: resolveDeepFilterAssetsBaseUrl(), - }, - }); - await processor.initialize(); - const workletNode = await processor.createAudioWorkletNode(ctx); - processor.setNoiseSuppressionEnabled(true); - source.connect(workletNode); - processedSource = workletNode; - state.voice.deepFilterProcessor = processor; - console.info('DeepFilterNet3 enabled'); - } catch (err) { - console.warn('DeepFilterNet3 unavailable, falling back to raw mic audio', err); - state.voice.deepFilterProcessor = null; - } - - // Metering/Speaking detection with adaptive threshold and hysteresis. - const analyser = ctx.createAnalyser(); - analyser.fftSize = 1024; - processedSource.connect(analyser); - - const supportsFloatTimeDomain = typeof analyser.getFloatTimeDomainData === 'function'; - const timeData = supportsFloatTimeDomain ? new Float32Array(analyser.fftSize) : new Uint8Array(analyser.fftSize); - let localIsSpeaking = false; - let speechFrames = 0; - let silenceFrames = 0; - let noiseFloor = 0.004; - let levelEma = 0; - - const checkVolume = () => { - if (!state.voice.audioContext || state.voice.audioContext.state === 'closed') return; - - try { - if (supportsFloatTimeDomain) { - analyser.getFloatTimeDomainData(timeData); - } else { - analyser.getByteTimeDomainData(timeData); - } - } catch (err) { - setTimeout(checkVolume, 60); - return; - } - - let sumSquares = 0; - let peak = 0; - for (let i = 0; i < timeData.length; i++) { - const v = supportsFloatTimeDomain - ? timeData[i] - : (timeData[i] - 128) / 128; - sumSquares += v * v; - const abs = Math.abs(v); - if (abs > peak) peak = abs; - } - - const rms = Math.sqrt(sumSquares / timeData.length); - const level = Math.max(rms, peak * 0.5); - levelEma = levelEma * 0.75 + level * 0.25; - - if (!localIsSpeaking) { - // Learn room noise slowly while idle. - noiseFloor = noiseFloor * 0.98 + levelEma * 0.02; - } else { - // Do not let noise floor jump up while speaking. - noiseFloor = Math.min(noiseFloor, levelEma); - } - - const startThreshold = Math.max(noiseFloor * 2.2, 0.010); - const stopThreshold = Math.max(noiseFloor * 1.5, 0.006); - - if (levelEma > startThreshold) { - speechFrames = Math.min(speechFrames + 1, 8); - silenceFrames = 0; - } else if (levelEma < stopThreshold) { - silenceFrames = Math.min(silenceFrames + 1, 8); - speechFrames = Math.max(speechFrames - 1, 0); - } - - const newSpeakingState = localIsSpeaking ? silenceFrames < 3 : speechFrames >= 2; - - if (newSpeakingState !== localIsSpeaking) { - localIsSpeaking = newSpeakingState; - if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) { - state.voice.ws.send(JSON.stringify({ type: 'set_speaking_status', is_speaking: localIsSpeaking })); - } - // Local UI update - state.voicePresence.get(state.selectedVoiceChannelId)?.forEach(p => { - if (p.user_id === state.me.id) p.is_speaking = localIsSpeaking; - }); - renderChannels(); - const myVideo = document.getElementById(`video-${state.me.id}-camera`) || document.getElementById(`video-${state.me.id}-screen`); - if (myVideo) { - myVideo.parentElement.classList.toggle('speaking', localIsSpeaking); - } - } - setTimeout(checkVolume, 60); - }; - checkVolume(); - - const destination = ctx.createMediaStreamDestination(); - processedSource.connect(destination); - return destination.stream; -} - -function resolveDeepFilterModuleUrl() { - if (location.protocol === 'file:') { - return new URL('./vendor/deepfilternet3-noise-filter.esm.js', location.href).toString(); - } - return '/static/vendor/deepfilternet3-noise-filter.esm.js'; -} - -function resolveDeepFilterAssetsBaseUrl() { - if (location.protocol === 'file:') { - return new URL('./vendor/deepfilternet3', location.href).toString(); - } - return '/static/vendor/deepfilternet3'; -} - -async function getDeepFilterModule() { - if (state.voice.deepFilterModule) return state.voice.deepFilterModule; - const moduleUrl = resolveDeepFilterModuleUrl(); - state.voice.deepFilterModule = await import(moduleUrl); - return state.voice.deepFilterModule; -} - -async function createLocalVoiceStream() { - const constraints = { - audio: { - channelCount: 1, - sampleRate: 48000, - echoCancellation: true, - noiseSuppression: false, - autoGainControl: false, - }, - video: false, - }; - const rawStream = await navigator.mediaDevices.getUserMedia(constraints); - let localStream = rawStream; - try { - localStream = await buildAudioPipeline(rawStream); - } catch (err) { - console.warn('voice audio pipeline failed, using raw mic stream', err); - } - state.voice.rawStream = rawStream; - state.voice.localStream = localStream; - if (state.voice.muted) { - state.voice.localStream.getAudioTracks().forEach((t) => { - t.enabled = false; - }); - } -} - -function ensurePeerConnection(peerId) { - if (state.voice.peerConnections.has(peerId)) { - return state.voice.peerConnections.get(peerId); - } - - const pc = new RTCPeerConnection({ - iceServers: state.voice.iceServers - }); - - pc.makingOffer = false; - pc.ignoreOffer = false; - pc.polite = !shouldInitiateOffer(peerId); - - // 1. Audio - if (state.voice.localStream) { - state.voice.localStream.getAudioTracks().forEach(track => { - pc.addTrack(track, state.voice.localStream); - }); - } else { - pc.addTransceiver('audio', { direction: 'recvonly' }); - } - - // 2. Camera Video - if (state.voice.videoStream) { - state.voice.videoStream.getVideoTracks().forEach(track => { - const sender = pc.addTrack(track, state.voice.videoStream); - state.voice.videoSenders.set(peerId, sender); - }); - } - - // 3. Screen Video - if (state.voice.screenStream) { - state.voice.screenStream.getVideoTracks().forEach(track => { - const sender = pc.addTrack(track, state.voice.screenStream); - state.voice.screenSenders.set(peerId, sender); - }); - } - - pc.onicecandidate = (event) => { - if (!event.candidate || !state.voice.ws) return; - state.voice.ws.send(JSON.stringify({ - type: "signal", - to_user_id: peerId, - kind: "ice", - data: event.candidate, - })); - }; - - pc.oniceconnectionstatechange = async () => { - const iceState = pc.iceConnectionState; - console.debug(`voice ice state (${peerId}):`, iceState); - if (iceState !== "failed" || pc.restartingIce) return; - pc.restartingIce = true; - try { - const offer = await pc.createOffer({ iceRestart: true }); - await pc.setLocalDescription(offer); - if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) { - state.voice.ws.send(JSON.stringify({ - type: "signal", - to_user_id: peerId, - kind: "offer", - data: pc.localDescription, - })); - } - } catch (err) { - console.warn("ICE restart failed", err); - } finally { - pc.restartingIce = false; - } - }; - - pc.ontrack = (event) => { - const stream = event.streams[0]; - if (event.track.kind === "audio") { - let audio = document.getElementById(`audio-${peerId}`); - if (!audio) { - audio = document.createElement("audio"); - audio.id = `audio-${peerId}`; - audio.autoplay = true; - audio.playsInline = true; - document.body.appendChild(audio); - } - audio.muted = false; - audio.srcObject = new MediaStream([event.track]); - const tryPlay = () => { - const playPromise = audio.play(); - if (playPromise && typeof playPromise.catch === "function") { - playPromise.catch((err) => { - console.warn(`remote audio playback blocked for ${peerId}`, err); - }); - } - }; - audio.onloadedmetadata = tryPlay; - audio.oncanplay = tryPlay; - tryPlay(); - - // Web Audio Pipeline for Volume Boosting - if (!state.voice.audioContext) { - state.voice.audioContext = new (window.AudioContext || window.webkitAudioContext)(); - } - const ctx = state.voice.audioContext; - if (ctx.state === 'suspended') ctx.resume(); - - const sourceNode = ctx.createMediaStreamSource(audio.srcObject); - const gainNode = ctx.createGain(); - - const currentVolume = state.userVolumes.get(peerId) ?? 1.0; - gainNode.gain.setValueAtTime(currentVolume, ctx.currentTime); - - sourceNode.connect(gainNode); - gainNode.connect(ctx.destination); - - state.voice.peerGainNodes.set(peerId, gainNode); - - // Mute the original element as we play through Web Audio destination - audio.volume = 0; - audio.muted = true; - } else if (event.track.kind === "video") { - const peer = state.members.find(m => m.id === peerId) || { display_name: "Unknown" }; - renderVideo(peerId, peer.display_name, stream, event.track.id); - - event.track.onmute = () => { - document.getElementById(`video-${peerId}-${event.track.id}`)?.parentElement?.remove(); - }; - event.track.onended = () => { - document.getElementById(`video-${peerId}-${event.track.id}`)?.parentElement?.remove(); - }; - } - }; - - pc.onnegotiationneeded = async () => { - try { - pc.makingOffer = true; - const offer = await pc.createOffer(); - await pc.setLocalDescription(offer); - state.voice.ws.send(JSON.stringify({ - type: "signal", - to_user_id: peerId, - kind: "offer", - data: pc.localDescription, - })); - } catch (err) { - console.error("negotiation failed", err); - } finally { - pc.makingOffer = false; - } - }; - - state.voice.peerConnections.set(peerId, pc); - return pc; -} - -async function attachLocalAudioToPeerConnections() { - if (!state.voice.localStream) return; - const localTrack = state.voice.localStream.getAudioTracks()[0]; - if (!localTrack) return; - - await Promise.all( - Array.from(state.voice.peerConnections.values()).map(async (pc) => { - const audioTransceiver = pc.getTransceivers().find((t) => t.receiver?.track?.kind === 'audio'); - if (audioTransceiver?.sender) { - await audioTransceiver.sender.replaceTrack(localTrack); - if (audioTransceiver.direction === 'recvonly') { - audioTransceiver.direction = 'sendrecv'; - } - } else { - pc.addTrack(localTrack, state.voice.localStream); - } - }) - ); -} - -async function handleSignal(fromPeerId, kind, data) { - const pc = ensurePeerConnection(fromPeerId); - - try { - if (kind === "offer") { - const offerCollision = pc.makingOffer || pc.signalingState !== "stable"; - pc.ignoreOffer = !pc.polite && offerCollision; - if (pc.ignoreOffer) return; - - await pc.setRemoteDescription(new RTCSessionDescription(data)); - const answer = await pc.createAnswer(); - await pc.setLocalDescription(answer); - state.voice.ws.send(JSON.stringify({ - type: "signal", - to_user_id: fromPeerId, - kind: "answer", - data: pc.localDescription, - })); - } else if (kind === "answer") { - await pc.setRemoteDescription(new RTCSessionDescription(data)); - } else if (kind === "ice") { - try { - await pc.addIceCandidate(data ? new RTCIceCandidate(data) : null); - } catch (err) { - if (!pc.ignoreOffer) { - console.warn("failed to add ice candidate", err); - } - } - } - } catch (err) { - console.error("handleSignal failed", err); - } -} - -async function joinVoice() { - if (!state.selectedVoiceChannelId) { - alert('No voice channel selected.'); - return; - } - if (state.voice.joinedChannelId === state.selectedVoiceChannelId && - state.voice.ws && - state.voice.ws.readyState === WebSocket.OPEN) { - return; - } - - await leaveVoice(); - - el.voiceConnection.classList.remove('hidden'); - el.vcChannelName.textContent = 'Connecting...'; - - const ws = new WebSocket(getVoiceWsUrl(state.selectedVoiceChannelId)); - const currentWs = ws; - let wsOpened = false; - let micUnavailable = false; - - state.voice.ws = ws; - state.voice.joinedChannelId = state.selectedVoiceChannelId; - - // Start mic setup in parallel so channel join is not blocked by device init. - createLocalVoiceStream() - .then(async () => { - if (state.voice.ws !== currentWs) { - stopAndClearAudioPipeline(); - return; - } - try { - await attachLocalAudioToPeerConnections(); - } catch (err) { - console.warn('failed to attach local audio to existing peer connections', err); - } - }) - .catch((err) => { - micUnavailable = true; - console.warn('failed to initialize local voice stream, joining as listen-only', err); - state.voice.localStream = null; - state.voice.rawStream = null; - if (state.voice.audioContext) { - state.voice.audioContext.close().catch(() => { }); - state.voice.audioContext = null; - } - }); - - const connectTimer = setTimeout(() => { - if (!wsOpened && state.voice.ws === currentWs) { - console.error('voice websocket connect timeout'); - try { currentWs.close(); } catch (_) { } - el.vcChannelName.textContent = 'Connection timeout'; - alert('Could not connect to voice channel (timeout). Please try again.'); - } - }, 5000); - - ws.onopen = () => { - if (state.voice.ws !== currentWs) return; - wsOpened = true; - clearTimeout(connectTimer); - const channel = state.channels.find(c => c.id === state.selectedVoiceChannelId); - el.vcChannelName.textContent = channel ? channel.name : "Voice"; - if (micUnavailable) { - console.warn('Voice joined in listen-only mode because microphone is unavailable.'); - } - el.voiceConnection.classList.remove("hidden"); - el.soundboard.classList.remove("hidden"); - playSound('join'); - refreshVoicePresence().catch(() => { }); - loadSounds().catch(() => { }); - }; - - ws.onmessage = async (event) => { - if (state.voice.ws !== currentWs) return; - const msg = JSON.parse(event.data); - if (msg.type === "peers") { - for (const peer of msg.peers) { - ensurePeerConnection(peer.user_id); - } - } else if (msg.type === "peer_joined") { - playSound('peer-join'); - ensurePeerConnection(msg.user_id); - } else if (msg.type === "peer_left") { - playSound('peer-leave'); - const pc = state.voice.peerConnections.get(msg.user_id); - if (pc) { - pc.close(); - state.voice.peerConnections.delete(msg.user_id); - } - document.getElementById(`audio-${msg.user_id}`)?.remove(); - } else if (msg.type === "signal") { - await handleSignal(msg.from_user_id, msg.kind, msg.data); - } else if (msg.type === "video_status_changed") { - if (!msg.is_sharing_video) { - document.getElementById(`video-${msg.user_id}-camera`)?.parentElement?.remove(); - } - } else if (msg.type === "screen_status_changed") { - if (!msg.is_sharing_screen) { - document.getElementById(`video-${msg.user_id}-screen`)?.parentElement?.remove(); - } - } else if (msg.type === "speaking_status_changed") { - state.voicePresence.get(state.selectedVoiceChannelId)?.forEach(p => { - if (p.user_id === msg.user_id) p.is_speaking = msg.is_speaking; - }); - renderChannels(); - const videoEl = document.getElementById(`video-${msg.user_id}-camera`) || document.getElementById(`video-${msg.user_id}-screen`); - if (videoEl) { - videoEl.parentElement.classList.toggle('speaking', msg.is_speaking); - } - } else if (msg.type === "mute_status_changed") { - state.voicePresence.get(state.selectedVoiceChannelId)?.forEach(p => { - if (p.user_id === msg.user_id) p.is_muted = msg.is_muted; - }); - renderChannels(); - } else if (msg.type === "play_sound") { - const audio = new Audio(msg.sound_url); - audio.play().catch(console.error); - } - refreshVoicePresence().catch(() => { }); - }; - - ws.onerror = (event) => { - if (state.voice.ws !== currentWs) return; - console.error('voice websocket error', event); - el.vcChannelName.textContent = 'Connection failed'; - }; - - ws.onclose = (event) => { - if (state.voice.ws !== currentWs) return; - clearTimeout(connectTimer); - if (!wsOpened) { - console.error('voice websocket closed before open', event.code, event.reason); - const code = event && typeof event.code === 'number' ? event.code : 'unknown'; - el.vcChannelName.textContent = `Connection failed (${code})`; - alert(`Voice connection failed (code ${code}).`); - } - if (wsOpened) { - el.voiceConnection.classList.add("hidden"); - } - el.soundboard.classList.add("hidden"); - for (const pc of state.voice.peerConnections.values()) pc.close(); - state.voice.peerConnections.clear(); - state.voice.videoSenders.clear(); - state.voice.screenSenders.clear(); - stopAndClearAudioPipeline(); - state.voice.joinedChannelId = null; - state.voice.ws = null; - playSound('leave'); - refreshVoicePresence().catch(() => { }); - }; -} - -async function leaveVoice() { - if (state.voice.ws) state.voice.ws.close(); -} - -function toggleMute() { - if (!state.voice.localStream) return; - state.voice.muted = !state.voice.muted; - state.voice.localStream.getAudioTracks().forEach((t) => { - t.enabled = !state.voice.muted; - }); - if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) { - state.voice.ws.send(JSON.stringify({ type: "set_mute_status", is_muted: state.voice.muted })); - } - el.voiceMuteBtn.innerHTML = state.voice.muted ? '' : ''; - el.voiceMuteBtn.style.color = state.voice.muted ? 'var(--danger)' : 'var(--text-muted)'; - lucide.createIcons(); -} - -async function toggleVideo() { - if (state.voice.sharingVideo) { - stopAndClearVideoPipeline(); - if (state.voice.ws) { - state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: false })); - } - for (const [peerId, pc] of state.voice.peerConnections) { - const sender = state.voice.videoSenders.get(peerId); - if (sender) { - pc.removeTrack(sender); - state.voice.videoSenders.delete(peerId); - } - } - } else { - try { - const stream = await navigator.mediaDevices.getUserMedia({ video: true }); - state.voice.videoStream = stream; - state.voice.sharingVideo = true; - renderVideo(state.me.id, state.me.display_name, stream, 'camera'); - - if (state.voice.ws) { - state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: true })); - } - - for (const [peerId, pc] of state.voice.peerConnections) { - stream.getVideoTracks().forEach(track => { - const sender = pc.addTrack(track, stream); - state.voice.videoSenders.set(peerId, sender); - }); - } - } catch (err) { - console.error("camera denied", err); - alert("Could not access camera."); - return; - } - } - - el.voiceVideoBtn.innerHTML = state.voice.sharingVideo ? '' : ''; - el.voiceVideoBtn.style.color = state.voice.sharingVideo ? 'var(--green)' : 'var(--text-muted)'; - lucide.createIcons(); -} - -async function toggleScreenShare() { - if (state.voice.sharingScreen) { - stopAndClearScreenPipeline(); - if (state.voice.ws) { - state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: false })); - } - for (const [peerId, pc] of state.voice.peerConnections) { - const sender = state.voice.screenSenders.get(peerId); - if (sender) { - pc.removeTrack(sender); - state.voice.screenSenders.delete(peerId); - } - } - } else { - try { - const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }); - state.voice.screenStream = stream; - state.voice.sharingScreen = true; - renderVideo(state.me.id, state.me.display_name, stream, 'screen'); - - // Stop sharing if user clicks "Stop sharing" in browser UI - stream.getVideoTracks()[0].onended = () => { - if (state.voice.sharingScreen) toggleScreenShare(); - }; - - if (state.voice.ws) { - state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: true })); - } - - for (const [peerId, pc] of state.voice.peerConnections) { - stream.getVideoTracks().forEach(track => { - const sender = pc.addTrack(track, stream); - state.voice.screenSenders.set(peerId, sender); - }); - } - } catch (err) { - console.error("screen share denied", err); - return; - } - } - - el.voiceScreenBtn.innerHTML = state.voice.sharingScreen ? '' : ''; - el.voiceScreenBtn.style.color = state.voice.sharingScreen ? 'var(--green)' : 'var(--text-muted)'; - lucide.createIcons(); -} - -function toggleWatchVideo() { - state.voice.watchingVideo = !state.voice.watchingVideo; - updateVideoGridVisibility(); - - el.voiceWatchBtn.innerHTML = state.voice.watchingVideo ? '' : ''; - el.voiceWatchBtn.style.color = state.voice.watchingVideo ? 'var(--green)' : 'var(--danger)'; - el.voiceWatchBtn.title = state.voice.watchingVideo ? 'Stop Watching' : 'Start Watching'; - lucide.createIcons(); -} - -// --- Sound Board --- - -async function loadSounds() { - if (!state.selectedGuildId) return; - try { - const sounds = await api(`/guilds/${state.selectedGuildId}/sounds`); - renderSounds(sounds); - } catch (err) { - console.error("failed to load sounds", err); - } -} - -function renderSounds(sounds) { - el.soundboardGrid.innerHTML = ''; - sounds.forEach(sound => { - const item = document.createElement('div'); - item.className = 'sound-item'; - - // Only show delete button if user is creator or owner - const canDelete = state.me && (state.me.id === sound.created_by_user_id || (state.guilds.find(g => g.id === state.selectedGuildId)?.owner_user_id === state.me.id)); - - item.innerHTML = ` -
${sound.icon}
-
${sound.name}
- ${canDelete ? `` : ''} - `; - - item.onclick = (e) => { - if (e.target.closest('.sound-delete')) { - e.stopPropagation(); - if (confirm(`Are you sure you want to delete "${sound.name}"?`)) { - deleteSound(sound.id); - } - return; - } - playRemoteSound(sound.file_path); - }; - - el.soundboardGrid.appendChild(item); - }); - lucide.createIcons(); -} - -async function deleteSound(soundId) { - if (!state.selectedGuildId) return; - try { - const url = API_BASE_URL ? `${API_BASE_URL}/guilds/${state.selectedGuildId}/sounds/${soundId}` : `/guilds/${state.selectedGuildId}/sounds/${soundId}`; - const response = await fetch(url, { - method: 'DELETE' - }); - if (!response.ok) { - const err = await response.json(); - throw new Error(err.error || 'Delete failed'); - } - await loadSounds(); - } catch (err) { - alert(err.message); - } -} - -function playRemoteSound(url) { - const fullUrl = API_BASE_URL && !url.startsWith('http') ? `${API_BASE_URL}${url}` : url; - if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) { - state.voice.ws.send(JSON.stringify({ type: 'play_sound', sound_url: fullUrl })); - } - // Also play locally immediately - const audio = new Audio(fullUrl); - audio.play().catch(console.error); -} - -// --- Mobile Logic --- - -function toggleMobileMenu() { - const isOpen = el.main.classList.contains("menu-open"); - if (isOpen) { - el.main.classList.remove("menu-open"); - el.overlay.classList.add("hidden"); - } else { - el.main.classList.add("menu-open"); - el.utilitySidebar.classList.remove("active"); // close members if open - el.overlay.classList.remove("hidden"); - } -} - -function toggleMobileMembers() { - const isOpen = el.utilitySidebar.classList.contains("active"); - if (isOpen) { - el.utilitySidebar.classList.remove("active"); - el.overlay.classList.add("hidden"); - } else { - el.utilitySidebar.classList.add("active"); - el.main.classList.remove("menu-open"); // close menu if open - el.overlay.classList.remove("hidden"); - } -} - -function closeMobileMenus() { - el.main.classList.remove("menu-open"); - el.utilitySidebar.classList.remove("active"); - el.overlay.classList.add("hidden"); -} - -// --- Initialization --- - -let inactivityTimer = null; -let isCurrentlyIdle = false; - -function resetInactivityTimer() { - if (isCurrentlyIdle) { - isCurrentlyIdle = false; - if (state.chatWs && state.chatWs.readyState === WebSocket.OPEN) { - state.chatWs.send(JSON.stringify({ type: "set_idle_status", is_idle: false })); - } - } - - if (inactivityTimer) clearTimeout(inactivityTimer); - inactivityTimer = setTimeout(() => { - isCurrentlyIdle = true; - if (state.chatWs && state.chatWs.readyState === WebSocket.OPEN) { - state.chatWs.send(JSON.stringify({ type: "set_idle_status", is_idle: true })); - } - }, 5 * 60 * 1000); // 5 minutes -} - -document.addEventListener('mousemove', resetInactivityTimer); -document.addEventListener('keydown', resetInactivityTimer); -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"); - 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); - } - - // 2. Global Button Handlers - el.loginBtn.onclick = () => { - const loginPath = "/auth/login"; - location.href = API_BASE_URL ? `${API_BASE_URL}${loginPath}` : loginPath; - }; - - 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; } - 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; } - document.querySelector('input[name="channel-kind"][value="voice"]').checked = true; - el.channelModal.classList.remove("hidden"); - }; - el.channelModalCancel.onclick = () => { el.channelModal.classList.add("hidden"); }; - - // Mobile Listeners - if (el.mobileMenuBtn) el.mobileMenuBtn.onclick = toggleMobileMenu; - 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 }) }); - 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(); - const messages = await api(`/channels/${created.id}/messages?limit=100`); - renderMessages(messages); - } 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); } - }; - - // 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.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 { - 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'); } - 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'; - } - }; - - // 5. GIF Picker - const TENOR_API_KEY = "exTiFGKJ0CzESIHzVQWy3pRO8I1MAdpRomg95DBSu2sg6e7YcHgThMI4giGAx8D0"; - const TENOR_CLIENT_KEY = "pavel-discord"; - - el.gifBtn.onclick = () => { el.gifPicker.classList.remove("hidden"); searchGifs(""); }; - el.gifPickerClose.onclick = () => { el.gifPicker.classList.add("hidden"); }; - - let gifSearchTimeout = null; - el.gifSearchInput.oninput = () => { - clearTimeout(gifSearchTimeout); - gifSearchTimeout = 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 => { - 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); - }); - } - - // 6. Initial Data Loading & App Start - try { - state.me = await api("/me"); - if (state.me && state.me.display_name) { - el.userName.textContent = state.me.display_name; - el.userAvatar.textContent = shortName(state.me.display_name); - } - 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" }); - localStorage.setItem(LAST_GUILD_STORAGE_KEY, joinedGuild.id); - } catch (err) { alert(`Failed to join invite: ${err.message}`); } finally { - params.delete("invite"); - const nextQuery = params.toString(); - const nextUrl = `${location.pathname}${nextQuery ? `?${nextQuery}` : ""}`; - history.replaceState(null, "", nextUrl); - } - } - - 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"); - } -} - -function initUpdater() { - if (!window.electronAPI) return; - - window.electronAPI.onUpdateAvailable((info) => { - console.log("Update available:", info.version); - el.updateNotifier.classList.remove("hidden"); - el.updateDownloadBtn.classList.remove("hidden"); - el.updateInstallBtn.classList.add("hidden"); - }); - - window.electronAPI.onUpdateDownloaded((info) => { - console.log("Update downloaded:", info.version); - el.updateNotifier.classList.remove("hidden"); - el.updateDownloadBtn.classList.add("hidden"); - el.updateInstallBtn.classList.remove("hidden"); - el.updateInstallBtn.title = `Install Update ${info.version}`; - }); - - window.electronAPI.onUpdateError((err) => { - console.error("Update error:", err); - // Optionally hide indicator on error - // el.updateNotifier.classList.add("hidden"); - }); - - el.updateDownloadBtn.onclick = () => { - el.updateDownloadBtn.disabled = true; - el.updateDownloadBtn.style.opacity = "0.5"; - window.electronAPI.downloadUpdate(); - }; - - el.updateInstallBtn.onclick = () => { - window.electronAPI.quitAndInstall(); - }; -} - -// Config fetcher will trigger init() -initializeConfig(); +import "../static/shared/app-core.js"; diff --git a/desktop/index.html b/desktop/index.html index ac9fa8d..07f8f7d 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -265,7 +265,7 @@ - + - \ No newline at end of file + diff --git a/static/app.js b/static/app.js index d6b264f..2794dc8 100644 --- a/static/app.js +++ b/static/app.js @@ -1,1931 +1 @@ -const state = { - me: null, - guilds: [], - channels: [], - dmConversations: [], - members: [], - voicePresence: new Map(), - selectedGuildId: null, - selectedTextChannelId: null, - selectedDmUserId: null, - selectedDmDisplayName: null, - selectedVoiceChannelId: null, - voice: { - ws: null, - joinedChannelId: null, - localStream: null, - rawStream: null, - videoStream: null, - screenStream: null, - peerConnections: new Map(), - videoSenders: new Map(), - screenSenders: new Map(), - muted: false, - sharingVideo: false, - sharingScreen: false, - deepFilterProcessor: null, - deepFilterModule: null, - viewMode: 'chat', // 'chat' or 'video' - iceServers: [{ urls: "stun:stun.l.google.com:19302" }], - peerGainNodes: new Map(), // userId -> GainNode - visibleVolumeSliders: new Set(), // userIds whose sliders are currently visible - }, - voicePresencePollId: null, - chatWs: null, - lastMessageId: null, - onlineUsers: new Set(), - idleUsers: new Set(), - userVolumes: new Map(), // userId -> volume (0.0 to 2.0) -}; - - - -const el = { - authScreen: document.getElementById("auth-screen"), - loginBtn: document.getElementById("login-btn"), - main: document.getElementById("main"), - status: document.getElementById("status"), - - // Guilds - guildList: document.getElementById("guild-list"), - addGuildBtn: document.getElementById("add-guild-btn"), - guildTitle: document.getElementById("guild-title"), - createInviteBtn: document.getElementById("create-invite-btn"), - inviteCopiedBadge: document.getElementById("invite-copied-badge"), - - // Channels - channelList: document.getElementById("channel-list"), - voiceChannelList: document.getElementById("voice-channel-list"), - addTextBtn: document.getElementById("add-text-btn"), - addVoiceBtn: document.getElementById("add-voice-btn"), - dmList: document.getElementById("dm-list"), - channelTitle: document.getElementById("channel-title"), - - // Messages - messageList: document.getElementById("message-list"), - messageForm: document.getElementById("message-form"), - messageBody: document.getElementById("message-body"), - - // User Panel - userName: document.getElementById("user-name"), - userAvatar: document.getElementById("user-avatar"), - logoutBtn: document.getElementById("logout-btn"), - - // Voice Connection - voiceConnection: document.getElementById("voice-connection"), - vcChannelName: document.getElementById("vc-channel-name"), - voiceVideoBtn: document.getElementById("voice-video-btn"), - voiceScreenBtn: document.getElementById("voice-screen-btn"), - voiceMuteBtn: document.getElementById("voice-mute-btn"), - voiceLeaveBtn: document.getElementById("voice-leave-btn"), - videoGrid: document.getElementById("video-grid"), - messageForm: document.getElementById("message-form"), - messageInputWrapper: document.querySelector(".chat-input-wrapper"), - - // Members - memberList: document.getElementById("member-list"), - - // Modals - modalContainer: document.getElementById("modal-container"), - guildForm: document.getElementById("guild-form"), - guildName: document.getElementById("guild-name"), - modalCancel: document.getElementById("modal-cancel"), - - channelModal: document.getElementById("channel-modal"), - channelForm: document.getElementById("channel-form"), - channelName: document.getElementById("channel-name"), - channelModalCancel: document.getElementById("channel-modal-cancel"), - - // Sound Board - soundboard: document.getElementById('soundboard'), - soundboardGrid: document.getElementById('soundboard-grid'), - addSoundBtn: document.getElementById('add-sound-btn'), - soundModal: document.getElementById('sound-modal'), - soundForm: document.getElementById('sound-form'), - soundName: document.getElementById('sound-name'), - soundIcon: document.getElementById('sound-icon'), - soundFile: document.getElementById('sound-file'), - soundModalCancel: document.getElementById('sound-modal-cancel'), - soundSubmitBtn: document.getElementById('sound-submit-btn'), - - // Mobile - mobileMenuBtn: document.getElementById("mobile-menu-btn"), - mobileMembersBtn: document.getElementById("mobile-members-btn"), - overlay: document.getElementById("overlay"), - utilitySidebar: document.getElementById("utility-sidebar"), - - // GIF Picker - gifBtn: document.getElementById("gif-btn"), - gifPicker: document.getElementById("gif-picker"), - gifPickerClose: document.getElementById("gif-picker-close"), - gifSearchInput: document.getElementById("gif-search-input"), - gifResults: document.getElementById("gif-results"), -}; - -// --- API Helpers --- - -async function api(path, options = {}) { - const headers = { - "content-type": "application/json", - ...(options.headers || {}), - }; - - const token = localStorage.getItem("chattz_token"); - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } - - let res = await fetch(path, { - ...options, - headers, - }); - - // Handle 401 Unauthorized via Refresh Token - if (res.status === 401 && !path.includes('/auth/refresh')) { - const refreshToken = localStorage.getItem("chattz_refresh_token"); - if (refreshToken) { - try { - const refreshRes = await fetch('/auth/refresh', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ refresh_token: refreshToken }) - }); - - if (refreshRes.ok) { - const newTokens = await refreshRes.json(); - localStorage.setItem("chattz_token", newTokens.access_token); - localStorage.setItem("chattz_refresh_token", newTokens.refresh_token); - - // Retry original request with new token - headers["Authorization"] = `Bearer ${newTokens.access_token}`; - res = await fetch(path, { ...options, headers }); - } else { - // Both tokens invalid/expired - throw new Error("Refresh token expired or invalid"); - } - } catch (err) { - localStorage.removeItem("chattz_token"); - localStorage.removeItem("chattz_refresh_token"); - location.href = "/auth/login"; - throw new Error("Session expired, please log in again"); - } - } - } - - if (!res.ok) { - let detail = "request failed"; - try { - const body = await res.json(); - detail = body.error || detail; - } catch { } - throw new Error(`${res.status}: ${detail}`); - } - - if (res.status === 204) return null; - return res.json(); -} - -// --- Utils --- - -function shortName(name) { - if (!name || typeof name !== 'string') return "?"; - const trimmed = name.trim(); - if (!trimmed) return "?"; - const words = trimmed.split(/\s+/).filter(w => w.length > 0).slice(0, 2); - if (words.length === 0) return "?"; - if (words.length === 1) return words[0].substring(0, 2).toUpperCase(); - return words.map((w) => w[0]?.toUpperCase() || "").join(""); -} - -function escapeHtml(s) { - if (s === null || s === undefined) return ""; - return String(s) - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """) - .replaceAll("'", "'"); -} - -function formatMessageBody(body) { - const escaped = escapeHtml(body); - // Detect GIF URLs (simple regex for demo) - const urlRegex = /(https?:\/\/[^\s]+(?:\.gif|\/giphy\.gif)[^\s]*)/gi; - if (urlRegex.test(body)) { - return escaped.replace(urlRegex, (url) => { - return `
`; - }); - } - return escaped; -} - -function formatDate(isoString) { - const d = new Date(isoString); - const now = new Date(); - const isToday = d.toDateString() === now.toDateString(); - - if (isToday) { - return `Today at ${d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; - } - return d.toLocaleDateString(); -} - -let audioCtx = null; - -// Global interaction listener to unlock AudioContext (autoplay policy) -window.addEventListener('click', () => { - if (audioCtx && audioCtx.state === 'suspended') { - audioCtx.resume(); - } -}, { once: true }); - -function playSound(type) { - try { - if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - if (audioCtx.state === 'suspended') audioCtx.resume(); - - const sounds = { - message: { freq: 880, type: 'sine', duration: 0.1, volume: 0.1 }, - dm: { freq: [660, 880], type: 'sine', duration: 0.15, volume: 0.1 }, - join: { freq: [440, 880], type: 'sine', duration: 0.2, volume: 0.1 }, - leave: { freq: [880, 440], type: 'sine', duration: 0.2, volume: 0.1 }, - 'peer-join': { freq: [660, 990], type: 'sine', duration: 0.15, volume: 0.05 }, - 'peer-leave': { freq: [990, 660], type: 'sine', duration: 0.15, volume: 0.05 }, - }; - - const s = sounds[type]; - if (!s) return; - - const osc = audioCtx.createOscillator(); - const gain = audioCtx.createGain(); - - osc.type = s.type; - if (Array.isArray(s.freq)) { - osc.frequency.setValueAtTime(s.freq[0], audioCtx.currentTime); - osc.frequency.exponentialRampToValueAtTime(s.freq[1], audioCtx.currentTime + s.duration); - } else { - osc.frequency.setValueAtTime(s.freq, audioCtx.currentTime); - } - - gain.gain.setValueAtTime(s.volume, audioCtx.currentTime); - gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + s.duration); - - osc.connect(gain); - gain.connect(audioCtx.destination); - - osc.start(); - osc.stop(audioCtx.currentTime + s.duration); - } catch (err) { - console.warn("playSound failed", err); - } -} -const LAST_GUILD_STORAGE_KEY = "chattz:lastGuildId"; - -// --- Renderers --- - -function renderGuilds() { - el.guildList.innerHTML = ""; - - for (const guild of state.guilds) { - const btn = document.createElement("button"); - btn.className = `guild-pill ${state.selectedGuildId === guild.id ? "active" : ""}`; - btn.title = guild.name; - btn.textContent = shortName(guild.name); - btn.onclick = async () => { - 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(); - renderDMs(); - await loadChannels(); - await loadGuildMembers(); - await refreshVoicePresence(); - startVoicePresencePolling(); - renderMessages([]); - updateHeaderLabels(); - }; - el.guildList.appendChild(btn); - } -} - -function renderChannels() { - el.channelList.innerHTML = ""; - el.voiceChannelList.innerHTML = ""; - - for (const channel of state.channels) { - const row = document.createElement("button"); - row.className = `channel-row ${(channel.kind === 'text' && state.selectedTextChannelId === channel.id) || - (channel.kind === 'voice' && state.selectedVoiceChannelId === channel.id) ? "active" : "" - }`; - - const iconName = channel.kind === 'text' ? 'hash' : 'volume-2'; - row.innerHTML = ` ${escapeHtml(channel.name)}`; - - row.onclick = async () => { - if (channel.kind === 'text') { - state.selectedDmUserId = null; - state.selectedDmDisplayName = null; - state.selectedTextChannelId = channel.id; - state.voice.viewMode = 'chat'; - renderChannels(); - renderDMs(); - updateView(); - updateHeaderLabels(); - const messages = await api(`/channels/${channel.id}/messages?limit=100`); - renderMessages(messages); - } else { - state.selectedVoiceChannelId = channel.id; - state.voice.viewMode = 'video'; - renderChannels(); - renderDMs(); - updateView(); - updateHeaderLabels(); - try { - await joinVoice(); - } catch (err) { - console.error("joinVoice failed from channel click", err); - alert(`Voice join failed: ${err?.message || err}`); - } - } - if (window.innerWidth <= 768) closeMobileMenus(); - }; - - if (channel.kind === 'text') { - el.channelList.appendChild(row); - } else { - el.voiceChannelList.appendChild(row); - - const participants = state.voicePresence.get(channel.id) || []; - if (participants.length > 0) { - const pList = document.createElement("div"); - pList.className = "voice-row-members channel-list"; - pList.style.paddingLeft = "24px"; - for (const p of participants) { - const pRow = document.createElement("div"); - pRow.className = `channel-row ${p.is_speaking ? 'voice-speaking' : ''}`; - pRow.style.padding = "2px 8px"; - pRow.style.flexWrap = "wrap"; - - let sliderHtml = ''; - if (p.user_id !== state.me.id) { - const vol = state.userVolumes.get(p.user_id) ?? 1.0; - const isVisible = state.voice.visibleVolumeSliders.has(p.user_id); - sliderHtml = ` -
- - - ${Math.round(vol * 100)}% -
- `; - } - - pRow.innerHTML = ` -
-
${shortName(p.display_name)}
- ${escapeHtml(p.display_name)} - ${p.is_muted ? '' : ''} -
- ${sliderHtml} - `; - - const slider = pRow.querySelector('.volume-slider'); - if (slider) { - slider.addEventListener('input', (e) => { - const val = parseFloat(e.target.value); - state.userVolumes.set(p.user_id, val); - pRow.querySelector('.vol-pct').textContent = `${Math.round(val * 100)}%`; - - const gainNode = state.voice.peerGainNodes.get(p.user_id); - if (gainNode) { - gainNode.gain.setTargetAtTime(val, state.voice.audioContext.currentTime, 0.05); - } - }); - // Stop propagation to prevent joining channel again when clicking slider - slider.addEventListener('click', (e) => e.stopPropagation()); - } - - const toggleVolumeSlider = (e) => { - if (p.user_id === state.me.id) return; - e.preventDefault(); - e.stopPropagation(); - if (state.voice.visibleVolumeSliders.has(p.user_id)) { - state.voice.visibleVolumeSliders.delete(p.user_id); - } else { - state.voice.visibleVolumeSliders.add(p.user_id); - } - renderChannels(); - }; - - pRow.addEventListener('contextmenu', toggleVolumeSlider); - pRow.addEventListener('mousedown', (e) => { - if (e.button === 2) toggleVolumeSlider(e); - }); - - pList.appendChild(pRow); - } - el.voiceChannelList.appendChild(pList); - } - } - } - lucide.createIcons(); -} - -function renderDMs() { - el.dmList.innerHTML = ""; - for (const dm of state.dmConversations) { - const isOnline = state.onlineUsers.has(dm.user_id); - const isIdle = state.idleUsers.has(dm.user_id); - const row = document.createElement("button"); - row.className = `channel-row ${state.selectedDmUserId === dm.user_id ? "active" : ""}`; - row.innerHTML = ` -
- -
-
- ${escapeHtml(dm.display_name)} - `; - row.onclick = async () => { - state.selectedDmUserId = dm.user_id; - state.selectedDmDisplayName = dm.display_name; - state.selectedTextChannelId = null; - state.selectedVoiceChannelId = null; - state.voice.viewMode = 'chat'; - renderChannels(); - renderDMs(); - updateView(); - updateHeaderLabels(); - const messages = await api(`/dms/${dm.user_id}/messages?limit=100`); - renderMessages(messages); - }; - el.dmList.appendChild(row); - } - - lucide.createIcons(); -} - -function renderMessages(messages) { - el.messageList.innerHTML = ""; - let lastAuthorId = null; - let lastTime = null; - - for (const m of messages.slice().reverse()) { - const row = document.createElement("div"); - const mDate = new Date(m.created_at); - const isGrouped = lastAuthorId === m.author_user_id && - lastTime && (mDate - lastTime < 300000); // 5 minutes - - row.className = `msg ${isGrouped ? "msg-grouped" : ""}`; - - const displayName = m.author_display_name || "Unknown User"; - - if (isGrouped) { - row.innerHTML = `
${formatMessageBody(m.body)}
`; - } else { - row.innerHTML = ` -
${shortName(displayName)}
-
-
- ${escapeHtml(displayName)} - ${formatDate(m.created_at)} -
-
${formatMessageBody(m.body)}
-
- `; - } - el.messageList.appendChild(row); - lastAuthorId = m.author_user_id; - lastTime = mDate; - } - - if (messages.length > 0) { - state.lastMessageId = messages[0].id; - } - - el.messageList.scrollTop = el.messageList.scrollHeight; -} - -function renderMembers() { - el.memberList.innerHTML = ""; - for (const m of state.members) { - const isOnline = state.onlineUsers.has(m.id); - const isIdle = state.idleUsers.has(m.id); - const row = document.createElement("div"); - row.className = "member-row"; - row.innerHTML = ` -
- ${shortName(m.display_name)} -
-
-
${escapeHtml(m.display_name)}
- `; - row.style.cursor = "pointer"; - row.onclick = async () => { - if (state.me && m.id === state.me.id) return; - state.selectedDmUserId = m.id; - state.selectedDmDisplayName = m.display_name; - state.selectedTextChannelId = null; - state.selectedVoiceChannelId = null; - state.voice.viewMode = 'chat'; - renderChannels(); - renderDMs(); - updateView(); - updateHeaderLabels(); - const messages = await api(`/dms/${m.id}/messages?limit=100`); - renderMessages(messages); - }; - el.memberList.appendChild(row); - } -} - -function updateHeaderLabels() { - const guild = state.guilds.find((g) => g.id === state.selectedGuildId); - el.guildTitle.textContent = guild ? guild.name : "No server selected"; - - if (state.selectedDmUserId) { - const dmFromConversations = state.dmConversations.find((u) => u.user_id === state.selectedDmUserId); - const dmFromMembers = state.members.find((m) => m.id === state.selectedDmUserId); - const displayName = dmFromConversations?.display_name || dmFromMembers?.display_name || state.selectedDmDisplayName; - const dmName = displayName ? `@${displayName}` : "Direct Message"; - el.channelTitle.textContent = dmName; - el.messageBody.placeholder = displayName - ? `Message @${displayName}` - : "Message"; - return; - } - - if (state.voice.viewMode === 'video' && state.selectedVoiceChannelId) { - const channel = state.channels.find((c) => c.id === state.selectedVoiceChannelId); - el.channelTitle.textContent = channel ? channel.name : "Voice Arena"; - return; - } - - const channel = state.channels.find((c) => c.id === state.selectedTextChannelId); - el.channelTitle.textContent = channel ? channel.name : "Select a channel"; - el.messageBody.placeholder = channel ? `Message #${channel.name}` : "Select a channel"; -} - -async function createInviteLink() { - if (!state.selectedGuildId) { - alert("Select a server first."); - return; - } - try { - const invite = await api(`/guilds/${state.selectedGuildId}/invites`, { - method: "POST", - body: JSON.stringify({ max_uses: 50, expires_in_hours: 24 }), - }); - const link = `${location.origin}/?invite=${encodeURIComponent(invite.code)}`; - try { - await navigator.clipboard.writeText(link); - showCopiedBadge(); - } catch { - prompt("Copy invite link:", link); - } - } catch (err) { - alert(err.message); - } -} - -function showCopiedBadge() { - el.inviteCopiedBadge.classList.remove("hidden"); - setTimeout(() => { - el.inviteCopiedBadge.classList.add("hidden"); - }, 2000); -} - -// --- Logic --- - -async function loadGuilds() { - state.guilds = await api("/guilds"); - renderGuilds(); -} - -async function loadDMConversations() { - state.dmConversations = await api("/dms"); - renderDMs(); -} - -async function loadGuildMembers() { - if (!state.selectedGuildId) { - state.members = []; - renderMembers(); - return; - } - state.members = await api(`/guilds/${state.selectedGuildId}/members`); - renderMembers(); -} - -async function loadChannels() { - if (!state.selectedGuildId) { - state.channels = []; - renderChannels(); - return; - } - - state.channels = await api(`/guilds/${state.selectedGuildId}/channels`); - renderChannels(); -} - -async function refreshVoicePresence() { - if (!state.selectedGuildId) { - state.voicePresence.clear(); - renderChannels(); - return; - } - - try { - const res = await api(`/guilds/${state.selectedGuildId}/voice-presence`); - state.voicePresence.clear(); - for (const entry of res.channels || []) { - state.voicePresence.set(entry.channel_id, entry.participants || []); - } - renderChannels(); - } catch (err) { - console.warn("voice presence failed", err); - } -} - -function startVoicePresencePolling() { - if (state.voicePresencePollId) clearInterval(state.voicePresencePollId); - state.voicePresencePollId = setInterval(() => { - refreshVoicePresence().catch(() => { }); - }, 3000); -} - -function initChatWs() { - if (state.chatWs) state.chatWs.close(); - const protocol = location.protocol === "https:" ? "wss:" : "ws:"; - let wsUrl = `${protocol}//${location.host}/ws`; - const token = localStorage.getItem("chattz_token"); - if (token) { - wsUrl += (wsUrl.includes('?') ? '&' : '?') + `token=${token}`; - } - const ws = new WebSocket(wsUrl); - state.chatWs = ws; - - ws.onmessage = (event) => { - const msg = JSON.parse(event.data); - const authorId = msg.message?.author_user_id; - const isFromMe = authorId && state.me && authorId === state.me.id; - - if (msg.type === "message_created") { - if (!isFromMe) playSound('message'); - if (state.selectedTextChannelId === msg.channel_id) { - api(`/channels/${state.selectedTextChannelId}/messages?limit=100`).then(renderMessages); - } - } else if (msg.type === "dm_created") { - if (!isFromMe) playSound('dm'); - if (state.selectedDmUserId === msg.other_user_id) { - api(`/dms/${state.selectedDmUserId}/messages?limit=100`).then(renderMessages); - playSound('dm'); - loadDMConversations().then(renderDMs); - } else { - loadDMConversations().then(renderDMs); - } - } else if (msg.type === "user_presence") { - if (msg.online) { - state.onlineUsers.add(msg.user_id); - if (msg.idle) { - state.idleUsers.add(msg.user_id); - } else { - state.idleUsers.delete(msg.user_id); - } - } else { - state.onlineUsers.delete(msg.user_id); - state.idleUsers.delete(msg.user_id); - } - renderMembers(); - renderDMs(); - } - }; - - ws.onclose = () => { - console.log("Chat WS closed, refreshing page for resilience..."); - // Automatically refresh on connection loss instead of just background reconnecting - location.reload(); - }; -} - -// --- Voice --- - -function getVoiceWsUrl(channelId) { - const proto = location.protocol === "https:" ? "wss" : "ws"; - let urlStr = `${proto}://${location.host}/channels/${channelId}/voice/ws`; - const token = localStorage.getItem("chattz_token"); - if (token) { - urlStr += (urlStr.includes('?') ? '&' : '?') + `token=${token}`; - } - return urlStr; -} - -function shouldInitiateOffer(peerId) { - if (!state.me || !state.me.id) return false; - return state.me.id > peerId; -} - -function stopAndClearAudioPipeline() { - if (state.voice.deepFilterProcessor) { - state.voice.deepFilterProcessor.destroy(); - state.voice.deepFilterProcessor = null; - } - if (state.voice.localStream) { - for (const track of state.voice.localStream.getTracks()) track.stop(); - } - if (state.voice.rawStream && state.voice.rawStream !== state.voice.localStream) { - for (const track of state.voice.rawStream.getTracks()) track.stop(); - } - if (state.voice.audioContext) { - state.voice.audioContext.close().catch(() => { }); - } - state.voice.localStream = null; - state.voice.rawStream = null; - state.voice.audioContext = null; -} - -function stopAndClearVideoPipeline() { - if (state.voice.videoStream) { - for (const track of state.voice.videoStream.getTracks()) track.stop(); - } - state.voice.videoStream = null; - state.voice.sharingVideo = false; - document.getElementById(`video-${state.me.id}-camera`)?.parentElement?.remove(); -} - -function stopAndClearScreenPipeline() { - if (state.voice.screenStream) { - for (const track of state.voice.screenStream.getTracks()) track.stop(); - } - state.voice.screenStream = null; - state.voice.sharingScreen = false; - document.getElementById(`video-${state.me.id}-screen`)?.parentElement?.remove(); -} - -function updateVideoGridVisibility() { - // Persistence logic: we always update the grid visibility based on participants - // however the actual container visibility in the UI is managed by updateView() -} - -function updateView() { - if (state.voice.viewMode === 'video') { - el.videoGrid.classList.remove("hidden"); - el.messageList.classList.add("hidden"); - el.messageInputWrapper.classList.add("hidden"); - } else { - el.videoGrid.classList.add("hidden"); - el.messageList.classList.remove("hidden"); - el.messageInputWrapper.classList.remove("hidden"); - } -} - -function renderVideo(peerId, displayName, stream, source) { - const videoId = `video-${peerId}-${source}`; - let videoEl = document.getElementById(videoId); - if (!videoEl) { - const container = document.createElement("div"); - container.className = "video-item"; - const label = source === 'screen' ? `${escapeHtml(displayName)}'s Screen` : escapeHtml(displayName); - container.innerHTML = ` - -
${label}
- `; - container.onclick = () => { - const isFullscreen = container.classList.contains('fullscreen'); - // Reset all - document.querySelectorAll('.video-item').forEach(el => el.classList.remove('fullscreen')); - - if (isFullscreen) { - el.videoGrid.classList.remove('has-fullscreen'); - } else { - container.classList.add('fullscreen'); - el.videoGrid.classList.add('has-fullscreen'); - } - }; - - el.videoGrid.appendChild(container); - videoEl = container.querySelector("video"); - } - videoEl.srcObject = stream; -} - -async function buildAudioPipeline(rawStream) { - const ctx = new AudioContext(); - state.voice.audioContext = ctx; - const source = ctx.createMediaStreamSource(rawStream); - let processedSource = source; - - try { - const deepFilter = await getDeepFilterModule(); - const processor = new deepFilter.DeepFilterNet3Core({ - sampleRate: 48000, - noiseReductionLevel: 70, - assetConfig: { - cdnUrl: resolveDeepFilterAssetsBaseUrl(), - }, - }); - await processor.initialize(); - const workletNode = await processor.createAudioWorkletNode(ctx); - processor.setNoiseSuppressionEnabled(true); - source.connect(workletNode); - processedSource = workletNode; - state.voice.deepFilterProcessor = processor; - console.info('DeepFilterNet3 enabled'); - } catch (err) { - console.warn('DeepFilterNet3 unavailable, falling back to raw mic audio', err); - state.voice.deepFilterProcessor = null; - } - - // Metering/Speaking detection with adaptive threshold and hysteresis. - const analyser = ctx.createAnalyser(); - analyser.fftSize = 1024; - processedSource.connect(analyser); - - const supportsFloatTimeDomain = typeof analyser.getFloatTimeDomainData === 'function'; - const timeData = supportsFloatTimeDomain ? new Float32Array(analyser.fftSize) : new Uint8Array(analyser.fftSize); - let localIsSpeaking = false; - let speechFrames = 0; - let silenceFrames = 0; - let noiseFloor = 0.004; - let levelEma = 0; - - const checkVolume = () => { - if (!state.voice.audioContext || state.voice.audioContext.state === 'closed') return; - - try { - if (supportsFloatTimeDomain) { - analyser.getFloatTimeDomainData(timeData); - } else { - analyser.getByteTimeDomainData(timeData); - } - } catch (err) { - setTimeout(checkVolume, 60); - return; - } - - let sumSquares = 0; - let peak = 0; - for (let i = 0; i < timeData.length; i++) { - const v = supportsFloatTimeDomain ? timeData[i] : (timeData[i] - 128) / 128; - sumSquares += v * v; - const abs = Math.abs(v); - if (abs > peak) peak = abs; - } - - const rms = Math.sqrt(sumSquares / timeData.length); - const level = Math.max(rms, peak * 0.5); - levelEma = levelEma * 0.75 + level * 0.25; - - if (!localIsSpeaking) { - noiseFloor = noiseFloor * 0.98 + levelEma * 0.02; - } else { - noiseFloor = Math.min(noiseFloor, levelEma); - } - - const startThreshold = Math.max(noiseFloor * 2.2, 0.010); - const stopThreshold = Math.max(noiseFloor * 1.5, 0.006); - - if (levelEma > startThreshold) { - speechFrames = Math.min(speechFrames + 1, 8); - silenceFrames = 0; - } else if (levelEma < stopThreshold) { - silenceFrames = Math.min(silenceFrames + 1, 8); - speechFrames = Math.max(speechFrames - 1, 0); - } - - const newSpeakingState = localIsSpeaking ? silenceFrames < 3 : speechFrames >= 2; - if (newSpeakingState !== localIsSpeaking) { - localIsSpeaking = newSpeakingState; - if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) { - state.voice.ws.send(JSON.stringify({ type: 'set_speaking_status', is_speaking: localIsSpeaking })); - } - state.voicePresence.get(state.selectedVoiceChannelId)?.forEach(p => { - if (p.user_id === state.me.id) p.is_speaking = localIsSpeaking; - }); - renderChannels(); - const myVideo = document.getElementById(`video-${state.me.id}-camera`) || document.getElementById(`video-${state.me.id}-screen`); - if (myVideo) myVideo.parentElement.classList.toggle('speaking', localIsSpeaking); - } - setTimeout(checkVolume, 60); - }; - checkVolume(); - - const destination = ctx.createMediaStreamDestination(); - processedSource.connect(destination); - return destination.stream; -} - -function resolveDeepFilterModuleUrl() { - if (location.protocol === 'file:') { - return new URL('./vendor/deepfilternet3-noise-filter.esm.js', location.href).toString(); - } - return '/static/vendor/deepfilternet3-noise-filter.esm.js'; -} - -function resolveDeepFilterAssetsBaseUrl() { - if (location.protocol === 'file:') { - return new URL('./vendor/deepfilternet3', location.href).toString(); - } - return '/static/vendor/deepfilternet3'; -} - -async function getDeepFilterModule() { - if (state.voice.deepFilterModule) return state.voice.deepFilterModule; - const moduleUrl = resolveDeepFilterModuleUrl(); - state.voice.deepFilterModule = await import(moduleUrl); - return state.voice.deepFilterModule; -} - -async function createLocalVoiceStream() { - const constraints = { - audio: { - channelCount: 1, - sampleRate: 48000, - echoCancellation: true, - noiseSuppression: false, - autoGainControl: false, - }, - video: false, - }; - const rawStream = await navigator.mediaDevices.getUserMedia(constraints); - let localStream = rawStream; - try { - localStream = await buildAudioPipeline(rawStream); - } catch (err) { - console.warn('voice audio pipeline failed, using raw mic stream', err); - } - state.voice.rawStream = rawStream; - state.voice.localStream = localStream; - if (state.voice.muted) { - state.voice.localStream.getAudioTracks().forEach((t) => { - t.enabled = false; - }); - } -} - -function ensurePeerConnection(peerId) { - if (state.voice.peerConnections.has(peerId)) { - return state.voice.peerConnections.get(peerId); - } - - const pc = new RTCPeerConnection({ - iceServers: state.voice.iceServers - }); - - pc.makingOffer = false; - pc.ignoreOffer = false; - pc.polite = !shouldInitiateOffer(peerId); - - // 1. Audio - if (state.voice.localStream) { - state.voice.localStream.getAudioTracks().forEach(track => { - pc.addTrack(track, state.voice.localStream); - }); - } else { - pc.addTransceiver('audio', { direction: 'recvonly' }); - } - - // 2. Camera Video - if (state.voice.videoStream) { - state.voice.videoStream.getVideoTracks().forEach(track => { - const sender = pc.addTrack(track, state.voice.videoStream); - state.voice.videoSenders.set(peerId, sender); - }); - } - - // 3. Screen Video - if (state.voice.screenStream) { - state.voice.screenStream.getVideoTracks().forEach(track => { - const sender = pc.addTrack(track, state.voice.screenStream); - state.voice.screenSenders.set(peerId, sender); - }); - } - - pc.onicecandidate = (event) => { - if (!event.candidate || !state.voice.ws) return; - state.voice.ws.send(JSON.stringify({ - type: "signal", - to_user_id: peerId, - kind: "ice", - data: event.candidate, - })); - }; - - pc.oniceconnectionstatechange = async () => { - const iceState = pc.iceConnectionState; - console.debug(`voice ice state (${peerId}):`, iceState); - if (iceState !== "failed" || pc.restartingIce) return; - pc.restartingIce = true; - try { - const offer = await pc.createOffer({ iceRestart: true }); - await pc.setLocalDescription(offer); - if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) { - state.voice.ws.send(JSON.stringify({ - type: "signal", - to_user_id: peerId, - kind: "offer", - data: pc.localDescription, - })); - } - } catch (err) { - console.warn("ICE restart failed", err); - } finally { - pc.restartingIce = false; - } - }; - - pc.ontrack = (event) => { - const stream = event.streams[0]; - if (event.track.kind === "audio") { - let audio = document.getElementById(`audio-${peerId}`); - if (!audio) { - audio = document.createElement("audio"); - audio.id = `audio-${peerId}`; - audio.autoplay = true; - audio.playsInline = true; - document.body.appendChild(audio); - } - audio.muted = false; - audio.srcObject = new MediaStream([event.track]); - const tryPlay = () => { - const playPromise = audio.play(); - if (playPromise && typeof playPromise.catch === "function") { - playPromise.catch((err) => { - console.warn(`remote audio playback blocked for ${peerId}`, err); - }); - } - }; - audio.onloadedmetadata = tryPlay; - audio.oncanplay = tryPlay; - tryPlay(); - - // Web Audio Pipeline for Volume Boosting - if (!state.voice.audioContext) { - state.voice.audioContext = new (window.AudioContext || window.webkitAudioContext)(); - } - const ctx = state.voice.audioContext; - if (ctx.state === 'suspended') ctx.resume(); - - const sourceNode = ctx.createMediaStreamSource(audio.srcObject); - const gainNode = ctx.createGain(); - - const currentVolume = state.userVolumes.get(peerId) ?? 1.0; - gainNode.gain.setValueAtTime(currentVolume, ctx.currentTime); - - sourceNode.connect(gainNode); - gainNode.connect(ctx.destination); - - state.voice.peerGainNodes.set(peerId, gainNode); - - // Mute the original element as we play through Web Audio destination - audio.volume = 0; - audio.muted = true; - } else if (event.track.kind === "video") { - const peer = state.members.find(m => m.id === peerId) || { display_name: "Unknown" }; - renderVideo(peerId, peer.display_name, stream, event.track.id); - - event.track.onmute = () => { - document.getElementById(`video-${peerId}-${event.track.id}`)?.parentElement?.remove(); - }; - event.track.onended = () => { - document.getElementById(`video-${peerId}-${event.track.id}`)?.parentElement?.remove(); - }; - } - }; - - pc.onnegotiationneeded = async () => { - try { - pc.makingOffer = true; - const offer = await pc.createOffer(); - await pc.setLocalDescription(offer); - state.voice.ws.send(JSON.stringify({ - type: "signal", - to_user_id: peerId, - kind: "offer", - data: pc.localDescription, - })); - } catch (err) { - console.error("negotiation failed", err); - } finally { - pc.makingOffer = false; - } - }; - - state.voice.peerConnections.set(peerId, pc); - return pc; -} - -async function attachLocalAudioToPeerConnections() { - if (!state.voice.localStream) return; - const localTrack = state.voice.localStream.getAudioTracks()[0]; - if (!localTrack) return; - - await Promise.all( - Array.from(state.voice.peerConnections.values()).map(async (pc) => { - const audioTransceiver = pc.getTransceivers().find((t) => t.receiver?.track?.kind === 'audio'); - if (audioTransceiver?.sender) { - await audioTransceiver.sender.replaceTrack(localTrack); - if (audioTransceiver.direction === 'recvonly') { - audioTransceiver.direction = 'sendrecv'; - } - } else { - pc.addTrack(localTrack, state.voice.localStream); - } - }) - ); -} - -async function handleSignal(fromPeerId, kind, data) { - const pc = ensurePeerConnection(fromPeerId); - - try { - if (kind === "offer") { - const offerCollision = pc.makingOffer || pc.signalingState !== "stable"; - pc.ignoreOffer = !pc.polite && offerCollision; - if (pc.ignoreOffer) return; - - await pc.setRemoteDescription(new RTCSessionDescription(data)); - const answer = await pc.createAnswer(); - await pc.setLocalDescription(answer); - state.voice.ws.send(JSON.stringify({ - type: "signal", - to_user_id: fromPeerId, - kind: "answer", - data: pc.localDescription, - })); - } else if (kind === "answer") { - await pc.setRemoteDescription(new RTCSessionDescription(data)); - } else if (kind === "ice") { - try { - await pc.addIceCandidate(data ? new RTCIceCandidate(data) : null); - } catch (err) { - if (!pc.ignoreOffer) { - console.warn("failed to add ice candidate", err); - } - } - } - } catch (err) { - console.error("handleSignal failed", err); - } -} - -async function joinVoice() { - if (!state.selectedVoiceChannelId) { - alert('No voice channel selected.'); - return; - } - if (state.voice.joinedChannelId === state.selectedVoiceChannelId && - state.voice.ws && - state.voice.ws.readyState === WebSocket.OPEN) { - return; - } - - await leaveVoice(); - - el.voiceConnection.classList.remove('hidden'); - el.vcChannelName.textContent = 'Connecting...'; - - const ws = new WebSocket(getVoiceWsUrl(state.selectedVoiceChannelId)); - const currentWs = ws; - let wsOpened = false; - let micUnavailable = false; - - state.voice.ws = ws; - state.voice.joinedChannelId = state.selectedVoiceChannelId; - localStorage.setItem("active_voice_channel", state.selectedVoiceChannelId); - - // Start mic setup in parallel so channel join is not blocked by device init. - createLocalVoiceStream() - .then(async () => { - if (state.voice.ws !== currentWs) { - stopAndClearAudioPipeline(); - return; - } - try { - await attachLocalAudioToPeerConnections(); - } catch (err) { - console.warn('failed to attach local audio to existing peer connections', err); - } - }) - .catch((err) => { - micUnavailable = true; - console.warn('failed to initialize local voice stream, joining as listen-only', err); - state.voice.localStream = null; - state.voice.rawStream = null; - if (state.voice.audioContext) { - state.voice.audioContext.close().catch(() => { }); - state.voice.audioContext = null; - } - }); - - const connectTimer = setTimeout(() => { - if (!wsOpened && state.voice.ws === currentWs) { - console.error('voice websocket connect timeout'); - try { currentWs.close(); } catch (_) { } - el.vcChannelName.textContent = 'Connection timeout'; - alert('Could not connect to voice channel (timeout). Please try again.'); - } - }, 5000); - - ws.onopen = () => { - if (state.voice.ws !== currentWs) return; - wsOpened = true; - clearTimeout(connectTimer); - const channel = state.channels.find(c => c.id === state.selectedVoiceChannelId); - el.vcChannelName.textContent = channel ? channel.name : "Voice"; - if (micUnavailable) { - console.warn('Voice joined in listen-only mode because microphone is unavailable.'); - } - el.voiceConnection.classList.remove("hidden"); - el.soundboard.classList.remove("hidden"); - playSound('join'); - refreshVoicePresence().catch(() => { }); - loadSounds().catch(() => { }); - }; - - ws.onmessage = async (event) => { - if (state.voice.ws !== currentWs) return; - const msg = JSON.parse(event.data); - if (msg.type === "peers") { - for (const peer of msg.peers) { - ensurePeerConnection(peer.user_id); - } - } else if (msg.type === "peer_joined") { - playSound('peer-join'); - ensurePeerConnection(msg.user_id); - } else if (msg.type === "peer_left") { - playSound('peer-leave'); - const pc = state.voice.peerConnections.get(msg.user_id); - if (pc) { - pc.close(); - state.voice.peerConnections.delete(msg.user_id); - } - document.getElementById(`audio-${msg.user_id}`)?.remove(); - } else if (msg.type === "signal") { - await handleSignal(msg.from_user_id, msg.kind, msg.data); - } else if (msg.type === "video_status_changed") { - if (!msg.is_sharing_video) { - document.getElementById(`video-${msg.user_id}-camera`)?.parentElement?.remove(); - } - } else if (msg.type === "screen_status_changed") { - if (!msg.is_sharing_screen) { - document.getElementById(`video-${msg.user_id}-screen`)?.parentElement?.remove(); - } - } else if (msg.type === "speaking_status_changed") { - state.voicePresence.get(state.selectedVoiceChannelId)?.forEach(p => { - if (p.user_id === msg.user_id) p.is_speaking = msg.is_speaking; - }); - renderChannels(); - const videoEl = document.getElementById(`video-${msg.user_id}-camera`) || document.getElementById(`video-${msg.user_id}-screen`); - if (videoEl) { - videoEl.parentElement.classList.toggle('speaking', msg.is_speaking); - } - } else if (msg.type === "mute_status_changed") { - state.voicePresence.get(state.selectedVoiceChannelId)?.forEach(p => { - if (p.user_id === msg.user_id) p.is_muted = msg.is_muted; - }); - renderChannels(); - } else if (msg.type === "play_sound") { - const audio = new Audio(msg.sound_url); - audio.play().catch(console.error); - } - refreshVoicePresence().catch(() => { }); - }; - - ws.onerror = (event) => { - if (state.voice.ws !== currentWs) return; - console.error('voice websocket error', event); - el.vcChannelName.textContent = 'Connection failed'; - }; - - ws.onclose = (event) => { - if (state.voice.ws !== currentWs) return; - clearTimeout(connectTimer); - if (!wsOpened) { - console.error('voice websocket closed before open', event.code, event.reason); - const code = event && typeof event.code === 'number' ? event.code : 'unknown'; - el.vcChannelName.textContent = `Connection failed (${code})`; - alert(`Voice connection failed (code ${code}).`); - } - if (wsOpened) { - el.voiceConnection.classList.add("hidden"); - } - el.soundboard.classList.add("hidden"); - for (const pc of state.voice.peerConnections.values()) pc.close(); - state.voice.peerConnections.clear(); - state.voice.videoSenders.clear(); - state.voice.screenSenders.clear(); - stopAndClearAudioPipeline(); - state.voice.joinedChannelId = null; - state.voice.ws = null; - playSound('leave'); - refreshVoicePresence().catch(() => { }); - }; -} - -async function leaveVoice() { - localStorage.removeItem("active_voice_channel"); - if (state.voice.ws) state.voice.ws.close(); -} - -function toggleMute() { - if (!state.voice.localStream) return; - state.voice.muted = !state.voice.muted; - state.voice.localStream.getAudioTracks().forEach((t) => { - t.enabled = !state.voice.muted; - }); - if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) { - state.voice.ws.send(JSON.stringify({ type: "set_mute_status", is_muted: state.voice.muted })); - } - el.voiceMuteBtn.innerHTML = state.voice.muted ? '' : ''; - el.voiceMuteBtn.style.color = state.voice.muted ? 'var(--danger)' : 'var(--text-muted)'; - lucide.createIcons(); -} - -async function toggleVideo() { - if (state.voice.sharingVideo) { - stopAndClearVideoPipeline(); - if (state.voice.ws) { - state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: false })); - } - for (const [peerId, pc] of state.voice.peerConnections) { - const sender = state.voice.videoSenders.get(peerId); - if (sender) { - pc.removeTrack(sender); - state.voice.videoSenders.delete(peerId); - } - } - } else { - try { - const stream = await navigator.mediaDevices.getUserMedia({ video: true }); - state.voice.videoStream = stream; - state.voice.sharingVideo = true; - renderVideo(state.me.id, state.me.display_name, stream, 'camera'); - - if (state.voice.ws) { - state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: true })); - } - - for (const [peerId, pc] of state.voice.peerConnections) { - stream.getVideoTracks().forEach(track => { - const sender = pc.addTrack(track, stream); - state.voice.videoSenders.set(peerId, sender); - }); - } - } catch (err) { - console.error("camera denied", err); - alert("Could not access camera."); - return; - } - } - - el.voiceVideoBtn.innerHTML = state.voice.sharingVideo ? '' : ''; - el.voiceVideoBtn.style.color = state.voice.sharingVideo ? 'var(--green)' : 'var(--text-muted)'; - lucide.createIcons(); -} - -async function toggleScreenShare() { - if (state.voice.sharingScreen) { - stopAndClearScreenPipeline(); - if (state.voice.ws) { - state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: false })); - } - for (const [peerId, pc] of state.voice.peerConnections) { - const sender = state.voice.screenSenders.get(peerId); - if (sender) { - pc.removeTrack(sender); - state.voice.screenSenders.delete(peerId); - } - } - } else { - try { - const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }); - state.voice.screenStream = stream; - state.voice.sharingScreen = true; - renderVideo(state.me.id, state.me.display_name, stream, 'screen'); - - // Stop sharing if user clicks "Stop sharing" in browser UI - stream.getVideoTracks()[0].onended = () => { - if (state.voice.sharingScreen) toggleScreenShare(); - }; - - if (state.voice.ws) { - state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: true })); - } - - for (const [peerId, pc] of state.voice.peerConnections) { - stream.getVideoTracks().forEach(track => { - const sender = pc.addTrack(track, stream); - state.voice.screenSenders.set(peerId, sender); - }); - } - } catch (err) { - console.error("screen share denied", err); - return; - } - } - - el.voiceScreenBtn.innerHTML = state.voice.sharingScreen ? '' : ''; - el.voiceScreenBtn.style.color = state.voice.sharingScreen ? 'var(--green)' : 'var(--text-muted)'; - lucide.createIcons(); -} - -function toggleWatchVideo() { - state.voice.watchingVideo = !state.voice.watchingVideo; - updateVideoGridVisibility(); - - el.voiceWatchBtn.innerHTML = state.voice.watchingVideo ? '' : ''; - el.voiceWatchBtn.style.color = state.voice.watchingVideo ? 'var(--green)' : 'var(--danger)'; - el.voiceWatchBtn.title = state.voice.watchingVideo ? 'Stop Watching' : 'Start Watching'; - lucide.createIcons(); -} - -// --- Sound Board --- - -async function loadSounds() { - if (!state.selectedGuildId) return; - try { - const sounds = await api(`/guilds/${state.selectedGuildId}/sounds`); - renderSounds(sounds); - } catch (err) { - console.error("failed to load sounds", err); - } -} - -function renderSounds(sounds) { - el.soundboardGrid.innerHTML = ''; - sounds.forEach(sound => { - const item = document.createElement('div'); - item.className = 'sound-item'; - - // Only show delete button if user is creator or owner - const canDelete = state.me && (state.me.id === sound.created_by_user_id || (state.guilds.find(g => g.id === state.selectedGuildId)?.owner_user_id === state.me.id)); - - item.innerHTML = ` -
${sound.icon}
-
${sound.name}
- ${canDelete ? `` : ''} - `; - - item.onclick = (e) => { - if (e.target.closest('.sound-delete')) { - e.stopPropagation(); - if (confirm(`Are you sure you want to delete "${sound.name}"?`)) { - deleteSound(sound.id); - } - return; - } - playRemoteSound(sound.file_path); - }; - - el.soundboardGrid.appendChild(item); - }); - lucide.createIcons(); -} - -async function deleteSound(soundId) { - if (!state.selectedGuildId) return; - try { - const response = await fetch(`/guilds/${state.selectedGuildId}/sounds/${soundId}`, { - method: 'DELETE' - }); - if (!response.ok) { - const err = await response.json(); - throw new Error(err.error || 'Delete failed'); - } - await loadSounds(); - } catch (err) { - alert(err.message); - } -} - -function playRemoteSound(url) { - if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) { - state.voice.ws.send(JSON.stringify({ type: 'play_sound', sound_url: url })); - } - // Also play locally immediately - const audio = new Audio(url); - audio.play().catch(console.error); -} - -// --- Mobile Logic --- - -function toggleMobileMenu() { - const isOpen = el.main.classList.contains("menu-open"); - if (isOpen) { - el.main.classList.remove("menu-open"); - el.overlay.classList.add("hidden"); - } else { - el.main.classList.add("menu-open"); - el.utilitySidebar.classList.remove("active"); // close members if open - el.overlay.classList.remove("hidden"); - } -} - -function toggleMobileMembers() { - const isOpen = el.utilitySidebar.classList.contains("active"); - if (isOpen) { - el.utilitySidebar.classList.remove("active"); - el.overlay.classList.add("hidden"); - } else { - el.utilitySidebar.classList.add("active"); - el.main.classList.remove("menu-open"); // close menu if open - el.overlay.classList.remove("hidden"); - } -} - -function closeMobileMenus() { - el.main.classList.remove("menu-open"); - el.utilitySidebar.classList.remove("active"); - el.overlay.classList.add("hidden"); -} - -// --- Initialization --- - -let inactivityTimer = null; -let isCurrentlyIdle = false; - -function resetInactivityTimer() { - if (isCurrentlyIdle) { - isCurrentlyIdle = false; - if (state.chatWs && state.chatWs.readyState === WebSocket.OPEN) { - state.chatWs.send(JSON.stringify({ type: "set_idle_status", is_idle: false })); - } - } - - if (inactivityTimer) clearTimeout(inactivityTimer); - inactivityTimer = setTimeout(() => { - isCurrentlyIdle = true; - if (state.chatWs && state.chatWs.readyState === WebSocket.OPEN) { - state.chatWs.send(JSON.stringify({ type: "set_idle_status", is_idle: true })); - } - }, 5 * 60 * 1000); // 5 minutes -} - -document.addEventListener('mousemove', resetInactivityTimer); -document.addEventListener('keydown', resetInactivityTimer); -document.addEventListener('click', resetInactivityTimer); - -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; - } - 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; - } - document.querySelector('input[name="channel-kind"][value="voice"]').checked = true; - el.channelModal.classList.remove("hidden"); - }; - el.channelModalCancel.onclick = () => { el.channelModal.classList.add("hidden"); }; - - // Mobile Listeners - if (el.mobileMenuBtn) el.mobileMenuBtn.onclick = toggleMobileMenu; - if (el.mobileMembersBtn) el.mobileMembersBtn.onclick = toggleMobileMembers; - if (el.overlay) el.overlay.onclick = closeMobileMenus; - - el.guildForm.onsubmit = async (e) => { - e.preventDefault(); - try { - 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); - 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(); - const messages = await api(`/channels/${created.id}/messages?limit=100`); - renderMessages(messages); - } else { - state.selectedVoiceChannelId = created.id; - state.selectedTextChannelId = null; - state.selectedDmUserId = null; - state.selectedDmDisplayName = null; - renderChannels(); - renderDMs(); - updateHeaderLabels(); - } - } 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"; - item.innerHTML = ``; - item.onclick = () => { - sendGif(fullUrl); - el.gifPicker.classList.add("hidden"); - el.gifSearchInput.value = ""; - }; - 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); - } - } - - try { - state.me = await api("/me"); - if (state.me && state.me.display_name) { - el.userName.textContent = state.me.display_name; - el.userAvatar.textContent = shortName(state.me.display_name); - } - el.authScreen.classList.add("hidden"); - el.main.classList.remove("hidden"); - - const params = new URLSearchParams(location.search); - const inviteCode = params.get("invite"); - if (inviteCode) { - try { - 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 { - params.delete("invite"); - const nextQuery = params.toString(); - const nextUrl = `${location.pathname}${nextQuery ? `?${nextQuery}` : ""}`; - history.replaceState(null, "", nextUrl); - } - } - - await loadGuilds(); - 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(); - - // Auto-reconnect to voice if previously joined - const savedVoiceChannel = localStorage.getItem("active_voice_channel"); - if (savedVoiceChannel) { - console.log("Auto-reconnecting to voice channel:", savedVoiceChannel); - state.selectedVoiceChannelId = savedVoiceChannel; - // Wait for components to be ready - setTimeout(() => { - joinVoice().catch(err => console.error("Auto-reconnect failed", err)); - }, 800); - } - - lucide.createIcons(); - } catch (err) { - console.error("init failed", err); - el.authScreen.classList.remove("hidden"); - el.main.classList.add("hidden"); - } -} - -init(); +import "./shared/app-core.js"; diff --git a/static/index.html b/static/index.html index fb34084..5aef5f7 100644 --- a/static/index.html +++ b/static/index.html @@ -265,7 +265,7 @@ - + diff --git a/static/shared/app-core.js b/static/shared/app-core.js new file mode 100644 index 0000000..2b5fa49 --- /dev/null +++ b/static/shared/app-core.js @@ -0,0 +1,1984 @@ +const state = { + me: null, + guilds: [], + channels: [], + dmConversations: [], + members: [], + voicePresence: new Map(), + selectedGuildId: null, + selectedTextChannelId: null, + selectedDmUserId: null, + selectedDmDisplayName: null, + selectedVoiceChannelId: null, + voice: { + ws: null, + joinedChannelId: null, + localStream: null, + rawStream: null, + videoStream: null, + screenStream: null, + peerConnections: new Map(), + videoSenders: new Map(), + screenSenders: new Map(), + muted: false, + sharingVideo: false, + sharingScreen: false, + deepFilterProcessor: null, + deepFilterModule: null, + viewMode: 'chat', // 'chat' or 'video' + iceServers: [{ urls: "stun:stun.l.google.com:19302" }], + peerGainNodes: new Map(), // userId -> GainNode + visibleVolumeSliders: new Set(), // userIds whose sliders are visible + }, + voicePresencePollId: null, + chatWs: null, + lastMessageId: null, + onlineUsers: new Set(), + idleUsers: new Set(), + userVolumes: new Map(), // userId -> volume (0.0 to 2.0) +}; + +// --- Desktop Backend Configuration --- +let API_BASE_URL = ''; // Will be initialized via IPC + +async function initializeConfig() { + try { + if (window.electronAPI?.getConfig) { + const config = await window.electronAPI.getConfig(); + API_BASE_URL = config.backendUrl; + console.log(`Backend initialized: ${API_BASE_URL}`); + } else { + API_BASE_URL = ''; + } + } catch (err) { + console.error("Failed to fetch config via IPC", err); + // Fallback to URL search params if IPC fails (unlikely in desktop app) + const urlParams = new URLSearchParams(window.location.search); + API_BASE_URL = urlParams.get('backend') || ''; + } finally { + // Start the app once config resolution has completed. + init(); + } +} + +function getWsUrl(path) { + let urlStr; + if (!API_BASE_URL) { + const proto = location.protocol === "https:" ? "wss" : "ws"; + urlStr = `${proto}://${location.host}${path}`; + } else { + const url = new URL(API_BASE_URL); + const proto = url.protocol === "https:" ? "wss" : "ws"; + const normalizedPath = path.startsWith('/') ? path : `/${path}`; + urlStr = `${proto}://${url.host}${normalizedPath}`; + } + + const token = localStorage.getItem("chattz_token"); + if (token) { + urlStr += (urlStr.includes('?') ? '&' : '?') + `token=${token}`; + } + return urlStr; +} + + + + + + +const el = { + authScreen: document.getElementById("auth-screen"), + loginBtn: document.getElementById("login-btn"), + main: document.getElementById("main"), + status: document.getElementById("status"), + + // Guilds + guildList: document.getElementById("guild-list"), + addGuildBtn: document.getElementById("add-guild-btn"), + guildTitle: document.getElementById("guild-title"), + createInviteBtn: document.getElementById("create-invite-btn"), + inviteCopiedBadge: document.getElementById("invite-copied-badge"), + + // Channels + channelList: document.getElementById("channel-list"), + voiceChannelList: document.getElementById("voice-channel-list"), + addTextBtn: document.getElementById("add-text-btn"), + addVoiceBtn: document.getElementById("add-voice-btn"), + dmList: document.getElementById("dm-list"), + channelTitle: document.getElementById("channel-title"), + + // Messages + messageList: document.getElementById("message-list"), + messageForm: document.getElementById("message-form"), + messageBody: document.getElementById("message-body"), + + // User Panel + userName: document.getElementById("user-name"), + userAvatar: document.getElementById("user-avatar"), + logoutBtn: document.getElementById("logout-btn"), + updateNotifier: document.getElementById("update-notifier"), + updateDownloadBtn: document.getElementById("update-download-btn"), + updateInstallBtn: document.getElementById("update-install-btn"), + + // Voice Connection + voiceConnection: document.getElementById("voice-connection"), + vcChannelName: document.getElementById("vc-channel-name"), + voiceVideoBtn: document.getElementById("voice-video-btn"), + voiceScreenBtn: document.getElementById("voice-screen-btn"), + voiceMuteBtn: document.getElementById("voice-mute-btn"), + voiceLeaveBtn: document.getElementById("voice-leave-btn"), + videoGrid: document.getElementById("video-grid"), + messageForm: document.getElementById("message-form"), + messageInputWrapper: document.querySelector(".chat-input-wrapper"), + + // Members + memberList: document.getElementById("member-list"), + + // Modals + modalContainer: document.getElementById("modal-container"), + guildForm: document.getElementById("guild-form"), + guildName: document.getElementById("guild-name"), + modalCancel: document.getElementById("modal-cancel"), + + channelModal: document.getElementById("channel-modal"), + channelForm: document.getElementById("channel-form"), + channelName: document.getElementById("channel-name"), + channelModalCancel: document.getElementById("channel-modal-cancel"), + + // Sound Board + soundboard: document.getElementById('soundboard'), + soundboardGrid: document.getElementById('soundboard-grid'), + addSoundBtn: document.getElementById('add-sound-btn'), + soundModal: document.getElementById('sound-modal'), + soundForm: document.getElementById('sound-form'), + soundName: document.getElementById('sound-name'), + soundIcon: document.getElementById('sound-icon'), + soundFile: document.getElementById('sound-file'), + soundModalCancel: document.getElementById('sound-modal-cancel'), + soundSubmitBtn: document.getElementById('sound-submit-btn'), + + // Mobile + mobileMenuBtn: document.getElementById("mobile-menu-btn"), + mobileMembersBtn: document.getElementById("mobile-members-btn"), + overlay: document.getElementById("overlay"), + utilitySidebar: document.getElementById("utility-sidebar"), + + // GIF Picker + gifBtn: document.getElementById("gif-btn"), + gifPicker: document.getElementById("gif-picker"), + gifPickerClose: document.getElementById("gif-picker-close"), + gifSearchInput: document.getElementById("gif-search-input"), + gifResults: document.getElementById("gif-results"), +}; + +// --- API Helpers --- + +async function api(path, options = {}) { + const fullUrl = API_BASE_URL ? (path.startsWith('http') ? path : `${API_BASE_URL}${path}`) : path; + + const headers = { + "content-type": "application/json", + ...(options.headers || {}), + }; + + const token = localStorage.getItem("chattz_token"); + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } + + let res = await fetch(fullUrl, { + ...options, + headers, + }); + + // Handle 401 Unauthorized via Refresh Token + if (res.status === 401 && !path.includes('/auth/refresh')) { + const refreshToken = localStorage.getItem("chattz_refresh_token"); + if (refreshToken) { + try { + const refreshUrl = API_BASE_URL ? `${API_BASE_URL}/auth/refresh` : '/auth/refresh'; + const refreshRes = await fetch(refreshUrl, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ refresh_token: refreshToken }) + }); + + if (refreshRes.ok) { + const newTokens = await refreshRes.json(); + localStorage.setItem("chattz_token", newTokens.access_token); + localStorage.setItem("chattz_refresh_token", newTokens.refresh_token); + + // Retry original request with new token + headers["Authorization"] = `Bearer ${newTokens.access_token}`; + res = await fetch(fullUrl, { ...options, headers }); + } else { + // Both tokens invalid/expired, wipe out to force clear state + throw new Error("Refresh token expired or invalid"); + } + } catch (err) { + localStorage.removeItem("chattz_token"); + localStorage.removeItem("chattz_refresh_token"); + location.href = API_BASE_URL ? `${API_BASE_URL}/auth/login` : "/auth/login"; + throw new Error("Session expired, please log in again"); + } + } + } + + if (!res.ok) { + let detail = "request failed"; + try { + const body = await res.json(); + detail = body.error || detail; + } catch { } + throw new Error(`${res.status}: ${detail}`); + } + + if (res.status === 204) return null; + return res.json(); +} + +// --- Utils --- + +function shortName(name) { + if (!name || typeof name !== 'string') return "?"; + const trimmed = name.trim(); + if (!trimmed) return "?"; + const words = trimmed.split(/\s+/).filter(w => w.length > 0).slice(0, 2); + if (words.length === 0) return "?"; + if (words.length === 1) return words[0].substring(0, 2).toUpperCase(); + return words.map((w) => w[0]?.toUpperCase() || "").join(""); +} + +function escapeHtml(s) { + if (s === null || s === undefined) return ""; + return String(s) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function formatMessageBody(body) { + const escaped = escapeHtml(body); + // Detect GIF URLs (simple regex for demo) + const urlRegex = /(https?:\/\/[^\s]+(?:\.gif|\/giphy\.gif)[^\s]*)/gi; + if (urlRegex.test(body)) { + return escaped.replace(urlRegex, (url) => { + return `
`; + }); + } + return escaped; +} + +function formatDate(isoString) { + const d = new Date(isoString); + const now = new Date(); + const isToday = d.toDateString() === now.toDateString(); + + if (isToday) { + return `Today at ${d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; + } + return d.toLocaleDateString(); +} + +let audioCtx = null; + +// Global interaction listener to unlock AudioContext (autoplay policy) +window.addEventListener('click', () => { + if (audioCtx && audioCtx.state === 'suspended') { + audioCtx.resume(); + } +}, { once: true }); + +function playSound(type) { + try { + if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + if (audioCtx.state === 'suspended') audioCtx.resume(); + + const sounds = { + message: { freq: 880, type: 'sine', duration: 0.1, volume: 0.1 }, + dm: { freq: [660, 880], type: 'sine', duration: 0.15, volume: 0.1 }, + join: { freq: [440, 880], type: 'sine', duration: 0.2, volume: 0.1 }, + leave: { freq: [880, 440], type: 'sine', duration: 0.2, volume: 0.1 }, + 'peer-join': { freq: [660, 990], type: 'sine', duration: 0.15, volume: 0.05 }, + 'peer-leave': { freq: [990, 660], type: 'sine', duration: 0.15, volume: 0.05 }, + }; + + const s = sounds[type]; + if (!s) return; + + const osc = audioCtx.createOscillator(); + const gain = audioCtx.createGain(); + + osc.type = s.type; + if (Array.isArray(s.freq)) { + osc.frequency.setValueAtTime(s.freq[0], audioCtx.currentTime); + osc.frequency.exponentialRampToValueAtTime(s.freq[1], audioCtx.currentTime + s.duration); + } else { + osc.frequency.setValueAtTime(s.freq, audioCtx.currentTime); + } + + gain.gain.setValueAtTime(s.volume, audioCtx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + s.duration); + + osc.connect(gain); + gain.connect(audioCtx.destination); + + osc.start(); + osc.stop(audioCtx.currentTime + s.duration); + } catch (err) { + console.warn("playSound failed", err); + } +} +const LAST_GUILD_STORAGE_KEY = "chattz:lastGuildId"; + +// --- Renderers --- + +function renderGuilds() { + el.guildList.innerHTML = ""; + + for (const guild of state.guilds) { + const btn = document.createElement("button"); + btn.className = `guild-pill ${state.selectedGuildId === guild.id ? "active" : ""}`; + btn.title = guild.name; + btn.textContent = shortName(guild.name); + btn.onclick = async () => { + 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(); + renderDMs(); + await loadChannels(); + await loadGuildMembers(); + await refreshVoicePresence(); + startVoicePresencePolling(); + renderMessages([]); + updateHeaderLabels(); + }; + el.guildList.appendChild(btn); + } +} + +function renderChannels() { + el.channelList.innerHTML = ""; + el.voiceChannelList.innerHTML = ""; + + for (const channel of state.channels) { + const row = document.createElement("button"); + row.className = `channel-row ${(channel.kind === 'text' && state.selectedTextChannelId === channel.id) || + (channel.kind === 'voice' && state.selectedVoiceChannelId === channel.id) ? "active" : "" + }`; + + const iconName = channel.kind === 'text' ? 'hash' : 'volume-2'; + row.innerHTML = ` ${escapeHtml(channel.name)}`; + + row.onclick = async () => { + if (channel.kind === 'text') { + state.selectedDmUserId = null; + state.selectedDmDisplayName = null; + state.selectedTextChannelId = channel.id; + state.voice.viewMode = 'chat'; + renderChannels(); + renderDMs(); + updateView(); + updateHeaderLabels(); + const messages = await api(`/channels/${channel.id}/messages?limit=100`); + renderMessages(messages); + } else { + state.selectedVoiceChannelId = channel.id; + state.voice.viewMode = 'video'; + renderChannels(); + renderDMs(); + updateView(); + updateHeaderLabels(); + try { + await joinVoice(); + } catch (err) { + console.error("joinVoice failed from channel click", err); + alert(`Voice join failed: ${err?.message || err}`); + } + } + if (window.innerWidth <= 768) closeMobileMenus(); + }; + + if (channel.kind === 'text') { + el.channelList.appendChild(row); + } else { + el.voiceChannelList.appendChild(row); + + const participants = state.voicePresence.get(channel.id) || []; + if (participants.length > 0) { + const pList = document.createElement("div"); + pList.className = "voice-row-members channel-list"; + pList.style.paddingLeft = "24px"; + for (const p of participants) { + const pRow = document.createElement("div"); + pRow.className = `channel-row ${p.is_speaking ? 'voice-speaking' : ''}`; + pRow.style.padding = "2px 8px"; + pRow.style.flexWrap = "wrap"; + + let sliderHtml = ''; + if (p.user_id !== state.me.id) { + const vol = state.userVolumes.get(p.user_id) ?? 1.0; + const isVisible = state.voice.visibleVolumeSliders.has(p.user_id); + sliderHtml = ` +
+ + + ${Math.round(vol * 100)}% +
+ `; + } + + pRow.innerHTML = ` +
+
${shortName(p.display_name)}
+ ${escapeHtml(p.display_name)} + ${p.is_muted ? '' : ''} +
+ ${sliderHtml} + `; + + const slider = pRow.querySelector('.volume-slider'); + if (slider) { + slider.addEventListener('input', (e) => { + const val = parseFloat(e.target.value); + state.userVolumes.set(p.user_id, val); + pRow.querySelector('.vol-pct').textContent = `${Math.round(val * 100)}%`; + + const gainNode = state.voice.peerGainNodes.get(p.user_id); + if (gainNode) { + gainNode.gain.setTargetAtTime(val, state.voice.audioContext.currentTime, 0.05); + } + }); + // Stop propagation to prevent joining channel again when clicking slider + slider.addEventListener('click', (e) => e.stopPropagation()); + } + + const toggleVolumeSlider = (e) => { + if (p.user_id === state.me.id) return; + e.preventDefault(); + e.stopPropagation(); + if (state.voice.visibleVolumeSliders.has(p.user_id)) { + state.voice.visibleVolumeSliders.delete(p.user_id); + } else { + state.voice.visibleVolumeSliders.add(p.user_id); + } + renderChannels(); + }; + + // Electron can swallow contextmenu events on some platforms; use + // right-button mousedown as a reliable fallback for slider toggle. + pRow.addEventListener('contextmenu', toggleVolumeSlider); + pRow.addEventListener('mousedown', (e) => { + if (e.button === 2) toggleVolumeSlider(e); + }); + + pList.appendChild(pRow); + } + el.voiceChannelList.appendChild(pList); + } + } + } + lucide.createIcons(); +} + +function renderDMs() { + el.dmList.innerHTML = ""; + for (const dm of state.dmConversations) { + const isOnline = state.onlineUsers.has(dm.user_id); + const isIdle = state.idleUsers.has(dm.user_id); + const row = document.createElement("button"); + row.className = `channel-row ${state.selectedDmUserId === dm.user_id ? "active" : ""}`; + row.innerHTML = ` +
+ +
+
+ ${escapeHtml(dm.display_name)} + `; + row.onclick = async () => { + state.selectedDmUserId = dm.user_id; + state.selectedDmDisplayName = dm.display_name; + state.selectedTextChannelId = null; + state.selectedVoiceChannelId = null; + state.voice.viewMode = 'chat'; + renderChannels(); + renderDMs(); + updateView(); + updateHeaderLabels(); + const messages = await api(`/dms/${dm.user_id}/messages?limit=100`); + renderMessages(messages); + }; + el.dmList.appendChild(row); + } + + lucide.createIcons(); +} + +function renderMessages(messages) { + el.messageList.innerHTML = ""; + let lastAuthorId = null; + let lastTime = null; + + for (const m of messages.slice().reverse()) { + const row = document.createElement("div"); + const mDate = new Date(m.created_at); + const isGrouped = lastAuthorId === m.author_user_id && + lastTime && (mDate - lastTime < 300000); // 5 minutes + + row.className = `msg ${isGrouped ? "msg-grouped" : ""}`; + + const displayName = m.author_display_name || "Unknown User"; + + if (isGrouped) { + row.innerHTML = `
${formatMessageBody(m.body)}
`; + } else { + row.innerHTML = ` +
${shortName(displayName)}
+
+
+ ${escapeHtml(displayName)} + ${formatDate(m.created_at)} +
+
${formatMessageBody(m.body)}
+
+ `; + } + el.messageList.appendChild(row); + lastAuthorId = m.author_user_id; + lastTime = mDate; + } + + if (messages.length > 0) { + state.lastMessageId = messages[0].id; + } + + el.messageList.scrollTop = el.messageList.scrollHeight; +} + +function renderMembers() { + el.memberList.innerHTML = ""; + for (const m of state.members) { + const isOnline = state.onlineUsers.has(m.id); + const isIdle = state.idleUsers.has(m.id); + const row = document.createElement("div"); + row.className = "member-row"; + row.innerHTML = ` +
+ ${shortName(m.display_name)} +
+
+
${escapeHtml(m.display_name)}
+ `; + row.style.cursor = "pointer"; + row.onclick = async () => { + if (state.me && m.id === state.me.id) return; + state.selectedDmUserId = m.id; + state.selectedDmDisplayName = m.display_name; + state.selectedTextChannelId = null; + state.selectedVoiceChannelId = null; + state.voice.viewMode = 'chat'; + renderChannels(); + renderDMs(); + updateView(); + updateHeaderLabels(); + const messages = await api(`/dms/${m.id}/messages?limit=100`); + renderMessages(messages); + }; + el.memberList.appendChild(row); + } +} + +function updateHeaderLabels() { + const guild = state.guilds.find((g) => g.id === state.selectedGuildId); + el.guildTitle.textContent = guild ? guild.name : "No server selected"; + + if (state.selectedDmUserId) { + const dmFromConversations = state.dmConversations.find((u) => u.user_id === state.selectedDmUserId); + const dmFromMembers = state.members.find((m) => m.id === state.selectedDmUserId); + const displayName = dmFromConversations?.display_name || dmFromMembers?.display_name || state.selectedDmDisplayName; + const dmName = displayName ? `@${displayName}` : "Direct Message"; + el.channelTitle.textContent = dmName; + el.messageBody.placeholder = displayName + ? `Message @${displayName}` + : "Message"; + return; + } + + if (state.voice.viewMode === 'video' && state.selectedVoiceChannelId) { + const channel = state.channels.find((c) => c.id === state.selectedVoiceChannelId); + el.channelTitle.textContent = channel ? channel.name : "Voice Arena"; + return; + } + + const channel = state.channels.find((c) => c.id === state.selectedTextChannelId); + el.channelTitle.textContent = channel ? channel.name : "Select a channel"; + el.messageBody.placeholder = channel ? `Message #${channel.name}` : "Select a channel"; +} + +async function createInviteLink() { + if (!state.selectedGuildId) { + alert("Select a server first."); + return; + } + try { + const invite = await api(`/guilds/${state.selectedGuildId}/invites`, { + method: "POST", + body: JSON.stringify({ max_uses: 50, expires_in_hours: 24 }), + }); + // Use the backend URL if available, otherwise fallback to current origin + const base = API_BASE_URL ? new URL(API_BASE_URL).origin : location.origin; + const link = `${base}/?invite=${encodeURIComponent(invite.code)}`; + + // Use Electron's native clipboard API if available (defined in preload.js) + if (window.electronAPI && window.electronAPI.copyToClipboard) { + try { + await window.electronAPI.copyToClipboard(link); + showCopiedBadge(); + return; + } catch (e) { + console.error("Electron clipboard write failed:", e); + } + } + + // Fallback to navigator.clipboard + try { + await navigator.clipboard.writeText(link); + showCopiedBadge(); + } catch (err) { + console.error("Navigator clipboard write failed:", err); + prompt("Copy invite link:", link); + } + } catch (err) { + alert(err.message); + } +} + +function showCopiedBadge() { + el.inviteCopiedBadge.classList.remove("hidden"); + setTimeout(() => { + el.inviteCopiedBadge.classList.add("hidden"); + }, 2000); +} + +// --- Logic --- + +async function loadGuilds() { + state.guilds = await api("/guilds"); + renderGuilds(); +} + +async function loadDMConversations() { + state.dmConversations = await api("/dms"); + renderDMs(); +} + +async function loadGuildMembers() { + if (!state.selectedGuildId) { + state.members = []; + renderMembers(); + return; + } + state.members = await api(`/guilds/${state.selectedGuildId}/members`); + renderMembers(); +} + +async function loadChannels() { + if (!state.selectedGuildId) { + state.channels = []; + renderChannels(); + return; + } + + state.channels = await api(`/guilds/${state.selectedGuildId}/channels`); + renderChannels(); +} + +async function refreshVoicePresence() { + if (!state.selectedGuildId) { + state.voicePresence.clear(); + renderChannels(); + return; + } + + try { + const res = await api(`/guilds/${state.selectedGuildId}/voice-presence`); + state.voicePresence.clear(); + for (const entry of res.channels || []) { + state.voicePresence.set(entry.channel_id, entry.participants || []); + } + renderChannels(); + } catch (err) { + console.warn("voice presence failed", err); + } +} + +function startVoicePresencePolling() { + if (state.voicePresencePollId) clearInterval(state.voicePresencePollId); + state.voicePresencePollId = setInterval(() => { + refreshVoicePresence().catch(() => { }); + }, 3000); +} + +function initChatWs() { + if (state.chatWs) state.chatWs.close(); + const wsUrl = getWsUrl('/ws'); + const ws = new WebSocket(wsUrl); + state.chatWs = ws; + + ws.onmessage = (event) => { + const msg = JSON.parse(event.data); + const authorId = msg.message?.author_user_id; + const isFromMe = authorId && state.me && authorId === state.me.id; + + if (msg.type === "message_created") { + if (!isFromMe) playSound('message'); + if (state.selectedTextChannelId === msg.channel_id) { + api(`/channels/${state.selectedTextChannelId}/messages?limit=100`).then(renderMessages); + } + } else if (msg.type === "dm_created") { + if (!isFromMe) playSound('dm'); + if (state.selectedDmUserId === msg.other_user_id) { + api(`/dms/${state.selectedDmUserId}/messages?limit=100`).then(renderMessages); + playSound('dm'); + loadDMConversations().then(renderDMs); + } else { + loadDMConversations().then(renderDMs); + } + } else if (msg.type === "user_presence") { + if (msg.online) { + state.onlineUsers.add(msg.user_id); + if (msg.idle) { + state.idleUsers.add(msg.user_id); + } else { + state.idleUsers.delete(msg.user_id); + } + } else { + state.onlineUsers.delete(msg.user_id); + state.idleUsers.delete(msg.user_id); + } + renderMembers(); + renderDMs(); + } + }; + + ws.onclose = () => { + console.log("Chat WS closed, reconnecting..."); + setTimeout(initChatWs, 3000); + }; +} + +// --- Voice --- + +function getVoiceWsUrl(channelId) { + return getWsUrl(`/channels/${channelId}/voice/ws`); +} + +function shouldInitiateOffer(peerId) { + if (!state.me || !state.me.id) return false; + return state.me.id > peerId; +} + +function stopAndClearAudioPipeline() { + if (state.voice.deepFilterProcessor) { + state.voice.deepFilterProcessor.destroy(); + state.voice.deepFilterProcessor = null; + } + if (state.voice.localStream) { + for (const track of state.voice.localStream.getTracks()) track.stop(); + } + if (state.voice.rawStream && state.voice.rawStream !== state.voice.localStream) { + for (const track of state.voice.rawStream.getTracks()) track.stop(); + } + if (state.voice.audioContext) { + state.voice.audioContext.close().catch(() => { }); + } + state.voice.localStream = null; + state.voice.rawStream = null; + state.voice.audioContext = null; +} + +function stopAndClearVideoPipeline() { + if (state.voice.videoStream) { + for (const track of state.voice.videoStream.getTracks()) track.stop(); + } + state.voice.videoStream = null; + state.voice.sharingVideo = false; + document.getElementById(`video-${state.me.id}-camera`)?.parentElement?.remove(); +} + +function stopAndClearScreenPipeline() { + if (state.voice.screenStream) { + for (const track of state.voice.screenStream.getTracks()) track.stop(); + } + state.voice.screenStream = null; + state.voice.sharingScreen = false; + document.getElementById(`video-${state.me.id}-screen`)?.parentElement?.remove(); +} + +function updateVideoGridVisibility() { + // Persistence logic: we always update the grid visibility based on participants + // however the actual container visibility in the UI is managed by updateView() +} + +function updateView() { + if (state.voice.viewMode === 'video') { + el.videoGrid.classList.remove("hidden"); + el.messageList.classList.add("hidden"); + el.messageInputWrapper.classList.add("hidden"); + } else { + el.videoGrid.classList.add("hidden"); + el.messageList.classList.remove("hidden"); + el.messageInputWrapper.classList.remove("hidden"); + } +} + +function renderVideo(peerId, displayName, stream, source) { + const videoId = `video-${peerId}-${source}`; + let videoEl = document.getElementById(videoId); + if (!videoEl) { + const container = document.createElement("div"); + container.className = "video-item"; + const label = source === 'screen' ? `${escapeHtml(displayName)}'s Screen` : escapeHtml(displayName); + container.innerHTML = ` + +
${label}
+ `; + container.onclick = () => { + const isFullscreen = container.classList.contains('fullscreen'); + // Reset all + document.querySelectorAll('.video-item').forEach(el => el.classList.remove('fullscreen')); + + if (isFullscreen) { + el.videoGrid.classList.remove('has-fullscreen'); + } else { + container.classList.add('fullscreen'); + el.videoGrid.classList.add('has-fullscreen'); + } + }; + + el.videoGrid.appendChild(container); + videoEl = container.querySelector("video"); + } + videoEl.srcObject = stream; +} + +async function buildAudioPipeline(rawStream) { + const ctx = new AudioContext(); + state.voice.audioContext = ctx; + const source = ctx.createMediaStreamSource(rawStream); + let processedSource = source; + + try { + const deepFilter = await getDeepFilterModule(); + const processor = new deepFilter.DeepFilterNet3Core({ + sampleRate: 48000, + noiseReductionLevel: 70, + assetConfig: { + cdnUrl: resolveDeepFilterAssetsBaseUrl(), + }, + }); + await processor.initialize(); + const workletNode = await processor.createAudioWorkletNode(ctx); + processor.setNoiseSuppressionEnabled(true); + source.connect(workletNode); + processedSource = workletNode; + state.voice.deepFilterProcessor = processor; + console.info('DeepFilterNet3 enabled'); + } catch (err) { + console.warn('DeepFilterNet3 unavailable, falling back to raw mic audio', err); + state.voice.deepFilterProcessor = null; + } + + // Metering/Speaking detection with adaptive threshold and hysteresis. + const analyser = ctx.createAnalyser(); + analyser.fftSize = 1024; + processedSource.connect(analyser); + + const supportsFloatTimeDomain = typeof analyser.getFloatTimeDomainData === 'function'; + const timeData = supportsFloatTimeDomain ? new Float32Array(analyser.fftSize) : new Uint8Array(analyser.fftSize); + let localIsSpeaking = false; + let speechFrames = 0; + let silenceFrames = 0; + let noiseFloor = 0.004; + let levelEma = 0; + + const checkVolume = () => { + if (!state.voice.audioContext || state.voice.audioContext.state === 'closed') return; + + try { + if (supportsFloatTimeDomain) { + analyser.getFloatTimeDomainData(timeData); + } else { + analyser.getByteTimeDomainData(timeData); + } + } catch (err) { + setTimeout(checkVolume, 60); + return; + } + + let sumSquares = 0; + let peak = 0; + for (let i = 0; i < timeData.length; i++) { + const v = supportsFloatTimeDomain + ? timeData[i] + : (timeData[i] - 128) / 128; + sumSquares += v * v; + const abs = Math.abs(v); + if (abs > peak) peak = abs; + } + + const rms = Math.sqrt(sumSquares / timeData.length); + const level = Math.max(rms, peak * 0.5); + levelEma = levelEma * 0.75 + level * 0.25; + + if (!localIsSpeaking) { + // Learn room noise slowly while idle. + noiseFloor = noiseFloor * 0.98 + levelEma * 0.02; + } else { + // Do not let noise floor jump up while speaking. + noiseFloor = Math.min(noiseFloor, levelEma); + } + + const startThreshold = Math.max(noiseFloor * 2.2, 0.010); + const stopThreshold = Math.max(noiseFloor * 1.5, 0.006); + + if (levelEma > startThreshold) { + speechFrames = Math.min(speechFrames + 1, 8); + silenceFrames = 0; + } else if (levelEma < stopThreshold) { + silenceFrames = Math.min(silenceFrames + 1, 8); + speechFrames = Math.max(speechFrames - 1, 0); + } + + const newSpeakingState = localIsSpeaking ? silenceFrames < 3 : speechFrames >= 2; + + if (newSpeakingState !== localIsSpeaking) { + localIsSpeaking = newSpeakingState; + if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) { + state.voice.ws.send(JSON.stringify({ type: 'set_speaking_status', is_speaking: localIsSpeaking })); + } + // Local UI update + state.voicePresence.get(state.selectedVoiceChannelId)?.forEach(p => { + if (p.user_id === state.me.id) p.is_speaking = localIsSpeaking; + }); + renderChannels(); + const myVideo = document.getElementById(`video-${state.me.id}-camera`) || document.getElementById(`video-${state.me.id}-screen`); + if (myVideo) { + myVideo.parentElement.classList.toggle('speaking', localIsSpeaking); + } + } + setTimeout(checkVolume, 60); + }; + checkVolume(); + + const destination = ctx.createMediaStreamDestination(); + processedSource.connect(destination); + return destination.stream; +} + +function resolveDeepFilterModuleUrl() { + if (location.protocol === 'file:') { + return new URL('./vendor/deepfilternet3-noise-filter.esm.js', location.href).toString(); + } + return '/static/vendor/deepfilternet3-noise-filter.esm.js'; +} + +function resolveDeepFilterAssetsBaseUrl() { + if (location.protocol === 'file:') { + return new URL('./vendor/deepfilternet3', location.href).toString(); + } + return '/static/vendor/deepfilternet3'; +} + +async function getDeepFilterModule() { + if (state.voice.deepFilterModule) return state.voice.deepFilterModule; + const moduleUrl = resolveDeepFilterModuleUrl(); + state.voice.deepFilterModule = await import(moduleUrl); + return state.voice.deepFilterModule; +} + +async function createLocalVoiceStream() { + const constraints = { + audio: { + channelCount: 1, + sampleRate: 48000, + echoCancellation: true, + noiseSuppression: false, + autoGainControl: false, + }, + video: false, + }; + const rawStream = await navigator.mediaDevices.getUserMedia(constraints); + let localStream = rawStream; + try { + localStream = await buildAudioPipeline(rawStream); + } catch (err) { + console.warn('voice audio pipeline failed, using raw mic stream', err); + } + state.voice.rawStream = rawStream; + state.voice.localStream = localStream; + if (state.voice.muted) { + state.voice.localStream.getAudioTracks().forEach((t) => { + t.enabled = false; + }); + } +} + +function ensurePeerConnection(peerId) { + if (state.voice.peerConnections.has(peerId)) { + return state.voice.peerConnections.get(peerId); + } + + const pc = new RTCPeerConnection({ + iceServers: state.voice.iceServers + }); + + pc.makingOffer = false; + pc.ignoreOffer = false; + pc.polite = !shouldInitiateOffer(peerId); + + // 1. Audio + if (state.voice.localStream) { + state.voice.localStream.getAudioTracks().forEach(track => { + pc.addTrack(track, state.voice.localStream); + }); + } else { + pc.addTransceiver('audio', { direction: 'recvonly' }); + } + + // 2. Camera Video + if (state.voice.videoStream) { + state.voice.videoStream.getVideoTracks().forEach(track => { + const sender = pc.addTrack(track, state.voice.videoStream); + state.voice.videoSenders.set(peerId, sender); + }); + } + + // 3. Screen Video + if (state.voice.screenStream) { + state.voice.screenStream.getVideoTracks().forEach(track => { + const sender = pc.addTrack(track, state.voice.screenStream); + state.voice.screenSenders.set(peerId, sender); + }); + } + + pc.onicecandidate = (event) => { + if (!event.candidate || !state.voice.ws) return; + state.voice.ws.send(JSON.stringify({ + type: "signal", + to_user_id: peerId, + kind: "ice", + data: event.candidate, + })); + }; + + pc.oniceconnectionstatechange = async () => { + const iceState = pc.iceConnectionState; + console.debug(`voice ice state (${peerId}):`, iceState); + if (iceState !== "failed" || pc.restartingIce) return; + pc.restartingIce = true; + try { + const offer = await pc.createOffer({ iceRestart: true }); + await pc.setLocalDescription(offer); + if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) { + state.voice.ws.send(JSON.stringify({ + type: "signal", + to_user_id: peerId, + kind: "offer", + data: pc.localDescription, + })); + } + } catch (err) { + console.warn("ICE restart failed", err); + } finally { + pc.restartingIce = false; + } + }; + + pc.ontrack = (event) => { + const stream = event.streams[0]; + if (event.track.kind === "audio") { + let audio = document.getElementById(`audio-${peerId}`); + if (!audio) { + audio = document.createElement("audio"); + audio.id = `audio-${peerId}`; + audio.autoplay = true; + audio.playsInline = true; + document.body.appendChild(audio); + } + audio.muted = false; + audio.srcObject = new MediaStream([event.track]); + const tryPlay = () => { + const playPromise = audio.play(); + if (playPromise && typeof playPromise.catch === "function") { + playPromise.catch((err) => { + console.warn(`remote audio playback blocked for ${peerId}`, err); + }); + } + }; + audio.onloadedmetadata = tryPlay; + audio.oncanplay = tryPlay; + tryPlay(); + + // Web Audio Pipeline for Volume Boosting + if (!state.voice.audioContext) { + state.voice.audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } + const ctx = state.voice.audioContext; + if (ctx.state === 'suspended') ctx.resume(); + + const sourceNode = ctx.createMediaStreamSource(audio.srcObject); + const gainNode = ctx.createGain(); + + const currentVolume = state.userVolumes.get(peerId) ?? 1.0; + gainNode.gain.setValueAtTime(currentVolume, ctx.currentTime); + + sourceNode.connect(gainNode); + gainNode.connect(ctx.destination); + + state.voice.peerGainNodes.set(peerId, gainNode); + + // Mute the original element as we play through Web Audio destination + audio.volume = 0; + audio.muted = true; + } else if (event.track.kind === "video") { + const peer = state.members.find(m => m.id === peerId) || { display_name: "Unknown" }; + renderVideo(peerId, peer.display_name, stream, event.track.id); + + event.track.onmute = () => { + document.getElementById(`video-${peerId}-${event.track.id}`)?.parentElement?.remove(); + }; + event.track.onended = () => { + document.getElementById(`video-${peerId}-${event.track.id}`)?.parentElement?.remove(); + }; + } + }; + + pc.onnegotiationneeded = async () => { + try { + pc.makingOffer = true; + const offer = await pc.createOffer(); + await pc.setLocalDescription(offer); + state.voice.ws.send(JSON.stringify({ + type: "signal", + to_user_id: peerId, + kind: "offer", + data: pc.localDescription, + })); + } catch (err) { + console.error("negotiation failed", err); + } finally { + pc.makingOffer = false; + } + }; + + state.voice.peerConnections.set(peerId, pc); + return pc; +} + +async function attachLocalAudioToPeerConnections() { + if (!state.voice.localStream) return; + const localTrack = state.voice.localStream.getAudioTracks()[0]; + if (!localTrack) return; + + await Promise.all( + Array.from(state.voice.peerConnections.values()).map(async (pc) => { + const audioTransceiver = pc.getTransceivers().find((t) => t.receiver?.track?.kind === 'audio'); + if (audioTransceiver?.sender) { + await audioTransceiver.sender.replaceTrack(localTrack); + if (audioTransceiver.direction === 'recvonly') { + audioTransceiver.direction = 'sendrecv'; + } + } else { + pc.addTrack(localTrack, state.voice.localStream); + } + }) + ); +} + +async function handleSignal(fromPeerId, kind, data) { + const pc = ensurePeerConnection(fromPeerId); + + try { + if (kind === "offer") { + const offerCollision = pc.makingOffer || pc.signalingState !== "stable"; + pc.ignoreOffer = !pc.polite && offerCollision; + if (pc.ignoreOffer) return; + + await pc.setRemoteDescription(new RTCSessionDescription(data)); + const answer = await pc.createAnswer(); + await pc.setLocalDescription(answer); + state.voice.ws.send(JSON.stringify({ + type: "signal", + to_user_id: fromPeerId, + kind: "answer", + data: pc.localDescription, + })); + } else if (kind === "answer") { + await pc.setRemoteDescription(new RTCSessionDescription(data)); + } else if (kind === "ice") { + try { + await pc.addIceCandidate(data ? new RTCIceCandidate(data) : null); + } catch (err) { + if (!pc.ignoreOffer) { + console.warn("failed to add ice candidate", err); + } + } + } + } catch (err) { + console.error("handleSignal failed", err); + } +} + +async function joinVoice() { + if (!state.selectedVoiceChannelId) { + alert('No voice channel selected.'); + return; + } + if (state.voice.joinedChannelId === state.selectedVoiceChannelId && + state.voice.ws && + state.voice.ws.readyState === WebSocket.OPEN) { + return; + } + + await leaveVoice(); + + el.voiceConnection.classList.remove('hidden'); + el.vcChannelName.textContent = 'Connecting...'; + + const ws = new WebSocket(getVoiceWsUrl(state.selectedVoiceChannelId)); + const currentWs = ws; + let wsOpened = false; + let micUnavailable = false; + + state.voice.ws = ws; + state.voice.joinedChannelId = state.selectedVoiceChannelId; + localStorage.setItem("active_voice_channel", state.selectedVoiceChannelId); + + // Start mic setup in parallel so channel join is not blocked by device init. + createLocalVoiceStream() + .then(async () => { + if (state.voice.ws !== currentWs) { + stopAndClearAudioPipeline(); + return; + } + try { + await attachLocalAudioToPeerConnections(); + } catch (err) { + console.warn('failed to attach local audio to existing peer connections', err); + } + }) + .catch((err) => { + micUnavailable = true; + console.warn('failed to initialize local voice stream, joining as listen-only', err); + state.voice.localStream = null; + state.voice.rawStream = null; + if (state.voice.audioContext) { + state.voice.audioContext.close().catch(() => { }); + state.voice.audioContext = null; + } + }); + + const connectTimer = setTimeout(() => { + if (!wsOpened && state.voice.ws === currentWs) { + console.error('voice websocket connect timeout'); + try { currentWs.close(); } catch (_) { } + el.vcChannelName.textContent = 'Connection timeout'; + alert('Could not connect to voice channel (timeout). Please try again.'); + } + }, 5000); + + ws.onopen = () => { + if (state.voice.ws !== currentWs) return; + wsOpened = true; + clearTimeout(connectTimer); + const channel = state.channels.find(c => c.id === state.selectedVoiceChannelId); + el.vcChannelName.textContent = channel ? channel.name : "Voice"; + if (micUnavailable) { + console.warn('Voice joined in listen-only mode because microphone is unavailable.'); + } + el.voiceConnection.classList.remove("hidden"); + el.soundboard.classList.remove("hidden"); + playSound('join'); + refreshVoicePresence().catch(() => { }); + loadSounds().catch(() => { }); + }; + + ws.onmessage = async (event) => { + if (state.voice.ws !== currentWs) return; + const msg = JSON.parse(event.data); + if (msg.type === "peers") { + for (const peer of msg.peers) { + ensurePeerConnection(peer.user_id); + } + } else if (msg.type === "peer_joined") { + playSound('peer-join'); + ensurePeerConnection(msg.user_id); + } else if (msg.type === "peer_left") { + playSound('peer-leave'); + const pc = state.voice.peerConnections.get(msg.user_id); + if (pc) { + pc.close(); + state.voice.peerConnections.delete(msg.user_id); + } + document.getElementById(`audio-${msg.user_id}`)?.remove(); + } else if (msg.type === "signal") { + await handleSignal(msg.from_user_id, msg.kind, msg.data); + } else if (msg.type === "video_status_changed") { + if (!msg.is_sharing_video) { + document.getElementById(`video-${msg.user_id}-camera`)?.parentElement?.remove(); + } + } else if (msg.type === "screen_status_changed") { + if (!msg.is_sharing_screen) { + document.getElementById(`video-${msg.user_id}-screen`)?.parentElement?.remove(); + } + } else if (msg.type === "speaking_status_changed") { + state.voicePresence.get(state.selectedVoiceChannelId)?.forEach(p => { + if (p.user_id === msg.user_id) p.is_speaking = msg.is_speaking; + }); + renderChannels(); + const videoEl = document.getElementById(`video-${msg.user_id}-camera`) || document.getElementById(`video-${msg.user_id}-screen`); + if (videoEl) { + videoEl.parentElement.classList.toggle('speaking', msg.is_speaking); + } + } else if (msg.type === "mute_status_changed") { + state.voicePresence.get(state.selectedVoiceChannelId)?.forEach(p => { + if (p.user_id === msg.user_id) p.is_muted = msg.is_muted; + }); + renderChannels(); + } else if (msg.type === "play_sound") { + const audio = new Audio(msg.sound_url); + audio.play().catch(console.error); + } + refreshVoicePresence().catch(() => { }); + }; + + ws.onerror = (event) => { + if (state.voice.ws !== currentWs) return; + console.error('voice websocket error', event); + el.vcChannelName.textContent = 'Connection failed'; + }; + + ws.onclose = (event) => { + if (state.voice.ws !== currentWs) return; + clearTimeout(connectTimer); + if (!wsOpened) { + console.error('voice websocket closed before open', event.code, event.reason); + const code = event && typeof event.code === 'number' ? event.code : 'unknown'; + el.vcChannelName.textContent = `Connection failed (${code})`; + alert(`Voice connection failed (code ${code}).`); + } + if (wsOpened) { + el.voiceConnection.classList.add("hidden"); + } + el.soundboard.classList.add("hidden"); + for (const pc of state.voice.peerConnections.values()) pc.close(); + state.voice.peerConnections.clear(); + state.voice.videoSenders.clear(); + state.voice.screenSenders.clear(); + stopAndClearAudioPipeline(); + state.voice.joinedChannelId = null; + state.voice.ws = null; + playSound('leave'); + refreshVoicePresence().catch(() => { }); + }; +} + +async function leaveVoice() { + localStorage.removeItem("active_voice_channel"); + if (state.voice.ws) state.voice.ws.close(); +} + +function toggleMute() { + if (!state.voice.localStream) return; + state.voice.muted = !state.voice.muted; + state.voice.localStream.getAudioTracks().forEach((t) => { + t.enabled = !state.voice.muted; + }); + if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) { + state.voice.ws.send(JSON.stringify({ type: "set_mute_status", is_muted: state.voice.muted })); + } + el.voiceMuteBtn.innerHTML = state.voice.muted ? '' : ''; + el.voiceMuteBtn.style.color = state.voice.muted ? 'var(--danger)' : 'var(--text-muted)'; + lucide.createIcons(); +} + +async function toggleVideo() { + if (state.voice.sharingVideo) { + stopAndClearVideoPipeline(); + if (state.voice.ws) { + state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: false })); + } + for (const [peerId, pc] of state.voice.peerConnections) { + const sender = state.voice.videoSenders.get(peerId); + if (sender) { + pc.removeTrack(sender); + state.voice.videoSenders.delete(peerId); + } + } + } else { + try { + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + state.voice.videoStream = stream; + state.voice.sharingVideo = true; + renderVideo(state.me.id, state.me.display_name, stream, 'camera'); + + if (state.voice.ws) { + state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: true })); + } + + for (const [peerId, pc] of state.voice.peerConnections) { + stream.getVideoTracks().forEach(track => { + const sender = pc.addTrack(track, stream); + state.voice.videoSenders.set(peerId, sender); + }); + } + } catch (err) { + console.error("camera denied", err); + alert("Could not access camera."); + return; + } + } + + el.voiceVideoBtn.innerHTML = state.voice.sharingVideo ? '' : ''; + el.voiceVideoBtn.style.color = state.voice.sharingVideo ? 'var(--green)' : 'var(--text-muted)'; + lucide.createIcons(); +} + +async function toggleScreenShare() { + if (state.voice.sharingScreen) { + stopAndClearScreenPipeline(); + if (state.voice.ws) { + state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: false })); + } + for (const [peerId, pc] of state.voice.peerConnections) { + const sender = state.voice.screenSenders.get(peerId); + if (sender) { + pc.removeTrack(sender); + state.voice.screenSenders.delete(peerId); + } + } + } else { + try { + const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }); + state.voice.screenStream = stream; + state.voice.sharingScreen = true; + renderVideo(state.me.id, state.me.display_name, stream, 'screen'); + + // Stop sharing if user clicks "Stop sharing" in browser UI + stream.getVideoTracks()[0].onended = () => { + if (state.voice.sharingScreen) toggleScreenShare(); + }; + + if (state.voice.ws) { + state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: true })); + } + + for (const [peerId, pc] of state.voice.peerConnections) { + stream.getVideoTracks().forEach(track => { + const sender = pc.addTrack(track, stream); + state.voice.screenSenders.set(peerId, sender); + }); + } + } catch (err) { + console.error("screen share denied", err); + return; + } + } + + el.voiceScreenBtn.innerHTML = state.voice.sharingScreen ? '' : ''; + el.voiceScreenBtn.style.color = state.voice.sharingScreen ? 'var(--green)' : 'var(--text-muted)'; + lucide.createIcons(); +} + +function toggleWatchVideo() { + state.voice.watchingVideo = !state.voice.watchingVideo; + updateVideoGridVisibility(); + + el.voiceWatchBtn.innerHTML = state.voice.watchingVideo ? '' : ''; + el.voiceWatchBtn.style.color = state.voice.watchingVideo ? 'var(--green)' : 'var(--danger)'; + el.voiceWatchBtn.title = state.voice.watchingVideo ? 'Stop Watching' : 'Start Watching'; + lucide.createIcons(); +} + +// --- Sound Board --- + +async function loadSounds() { + if (!state.selectedGuildId) return; + try { + const sounds = await api(`/guilds/${state.selectedGuildId}/sounds`); + renderSounds(sounds); + } catch (err) { + console.error("failed to load sounds", err); + } +} + +function renderSounds(sounds) { + el.soundboardGrid.innerHTML = ''; + sounds.forEach(sound => { + const item = document.createElement('div'); + item.className = 'sound-item'; + + // Only show delete button if user is creator or owner + const canDelete = state.me && (state.me.id === sound.created_by_user_id || (state.guilds.find(g => g.id === state.selectedGuildId)?.owner_user_id === state.me.id)); + + item.innerHTML = ` +
${sound.icon}
+
${sound.name}
+ ${canDelete ? `` : ''} + `; + + item.onclick = (e) => { + if (e.target.closest('.sound-delete')) { + e.stopPropagation(); + if (confirm(`Are you sure you want to delete "${sound.name}"?`)) { + deleteSound(sound.id); + } + return; + } + playRemoteSound(sound.file_path); + }; + + el.soundboardGrid.appendChild(item); + }); + lucide.createIcons(); +} + +async function deleteSound(soundId) { + if (!state.selectedGuildId) return; + try { + const url = API_BASE_URL ? `${API_BASE_URL}/guilds/${state.selectedGuildId}/sounds/${soundId}` : `/guilds/${state.selectedGuildId}/sounds/${soundId}`; + const response = await fetch(url, { + method: 'DELETE' + }); + if (!response.ok) { + const err = await response.json(); + throw new Error(err.error || 'Delete failed'); + } + await loadSounds(); + } catch (err) { + alert(err.message); + } +} + +function playRemoteSound(url) { + const fullUrl = API_BASE_URL && !url.startsWith('http') ? `${API_BASE_URL}${url}` : url; + if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) { + state.voice.ws.send(JSON.stringify({ type: 'play_sound', sound_url: fullUrl })); + } + // Also play locally immediately + const audio = new Audio(fullUrl); + audio.play().catch(console.error); +} + +// --- Mobile Logic --- + +function toggleMobileMenu() { + const isOpen = el.main.classList.contains("menu-open"); + if (isOpen) { + el.main.classList.remove("menu-open"); + el.overlay.classList.add("hidden"); + } else { + el.main.classList.add("menu-open"); + el.utilitySidebar.classList.remove("active"); // close members if open + el.overlay.classList.remove("hidden"); + } +} + +function toggleMobileMembers() { + const isOpen = el.utilitySidebar.classList.contains("active"); + if (isOpen) { + el.utilitySidebar.classList.remove("active"); + el.overlay.classList.add("hidden"); + } else { + el.utilitySidebar.classList.add("active"); + el.main.classList.remove("menu-open"); // close menu if open + el.overlay.classList.remove("hidden"); + } +} + +function closeMobileMenus() { + el.main.classList.remove("menu-open"); + el.utilitySidebar.classList.remove("active"); + el.overlay.classList.add("hidden"); +} + +// --- Initialization --- + +let inactivityTimer = null; +let isCurrentlyIdle = false; + +function resetInactivityTimer() { + if (isCurrentlyIdle) { + isCurrentlyIdle = false; + if (state.chatWs && state.chatWs.readyState === WebSocket.OPEN) { + state.chatWs.send(JSON.stringify({ type: "set_idle_status", is_idle: false })); + } + } + + if (inactivityTimer) clearTimeout(inactivityTimer); + inactivityTimer = setTimeout(() => { + isCurrentlyIdle = true; + if (state.chatWs && state.chatWs.readyState === WebSocket.OPEN) { + state.chatWs.send(JSON.stringify({ type: "set_idle_status", is_idle: true })); + } + }, 5 * 60 * 1000); // 5 minutes +} + +document.addEventListener('mousemove', resetInactivityTimer); +document.addEventListener('keydown', resetInactivityTimer); +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"); + 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); + } + + // 2. Global Button Handlers + el.loginBtn.onclick = () => { + const loginPath = "/auth/login"; + location.href = API_BASE_URL ? `${API_BASE_URL}${loginPath}` : loginPath; + }; + + 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; } + 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; } + document.querySelector('input[name="channel-kind"][value="voice"]').checked = true; + el.channelModal.classList.remove("hidden"); + }; + el.channelModalCancel.onclick = () => { el.channelModal.classList.add("hidden"); }; + + // Mobile Listeners + if (el.mobileMenuBtn) el.mobileMenuBtn.onclick = toggleMobileMenu; + 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 }) }); + 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(); + const messages = await api(`/channels/${created.id}/messages?limit=100`); + renderMessages(messages); + } 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); } + }; + + // 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.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 { + 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'); } + 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'; + } + }; + + // 5. GIF Picker + const TENOR_API_KEY = "exTiFGKJ0CzESIHzVQWy3pRO8I1MAdpRomg95DBSu2sg6e7YcHgThMI4giGAx8D0"; + const TENOR_CLIENT_KEY = "pavel-discord"; + + el.gifBtn.onclick = () => { el.gifPicker.classList.remove("hidden"); searchGifs(""); }; + el.gifPickerClose.onclick = () => { el.gifPicker.classList.add("hidden"); }; + + let gifSearchTimeout = null; + el.gifSearchInput.oninput = () => { + clearTimeout(gifSearchTimeout); + gifSearchTimeout = 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 => { + 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.gifPicker.classList.add("hidden"); + el.gifSearchInput.value = ""; + sendGif(fullUrl); + }; + 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) { + el.userName.textContent = state.me.display_name; + el.userAvatar.textContent = shortName(state.me.display_name); + } + 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" }); + localStorage.setItem(LAST_GUILD_STORAGE_KEY, joinedGuild.id); + } catch (err) { alert(`Failed to join invite: ${err.message}`); } finally { + params.delete("invite"); + const nextQuery = params.toString(); + const nextUrl = `${location.pathname}${nextQuery ? `?${nextQuery}` : ""}`; + history.replaceState(null, "", nextUrl); + } + } + + 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(); + + // Auto-reconnect to voice if previously joined + const savedVoiceChannel = localStorage.getItem("active_voice_channel"); + if (savedVoiceChannel) { + console.log("Auto-reconnecting to voice channel:", savedVoiceChannel); + state.selectedVoiceChannelId = savedVoiceChannel; + setTimeout(() => { + joinVoice().catch(err => console.error("Auto-reconnect failed", err)); + }, 800); + } + + initUpdater(); + lucide.createIcons(); + } catch (err) { + console.error("init failed", err); + el.authScreen.classList.remove("hidden"); + el.main.classList.add("hidden"); + } +} + +function initUpdater() { + if (!window.electronAPI || !el.updateNotifier || !el.updateDownloadBtn || !el.updateInstallBtn) return; + + window.electronAPI.onUpdateAvailable((info) => { + console.log("Update available:", info.version); + el.updateNotifier.classList.remove("hidden"); + el.updateDownloadBtn.classList.remove("hidden"); + el.updateInstallBtn.classList.add("hidden"); + }); + + window.electronAPI.onUpdateDownloaded((info) => { + console.log("Update downloaded:", info.version); + el.updateNotifier.classList.remove("hidden"); + el.updateDownloadBtn.classList.add("hidden"); + el.updateInstallBtn.classList.remove("hidden"); + el.updateInstallBtn.title = `Install Update ${info.version}`; + }); + + window.electronAPI.onUpdateError((err) => { + console.error("Update error:", err); + // Optionally hide indicator on error + // el.updateNotifier.classList.add("hidden"); + }); + + el.updateDownloadBtn.onclick = () => { + el.updateDownloadBtn.disabled = true; + el.updateDownloadBtn.style.opacity = "0.5"; + window.electronAPI.downloadUpdate(); + }; + + el.updateInstallBtn.onclick = () => { + window.electronAPI.quitAndInstall(); + }; +} + +// Config fetcher will trigger init() +initializeConfig(); From 208ab8042ef6bb9b098a10da227586f89a55f4dd Mon Sep 17 00:00:00 2001 From: pavel Date: Fri, 27 Feb 2026 10:57:13 +0100 Subject: [PATCH 07/24] checkpoint --- desktop/preload.js | 6 ++ main.js | 137 ++++++++++++++++++++++++++++- static/shared/app-core.js | 179 +++++++++++++++++++++++++++++++++----- 3 files changed, 296 insertions(+), 26 deletions(-) diff --git a/desktop/preload.js b/desktop/preload.js index ae9d90e..dfcc665 100644 --- a/desktop/preload.js +++ b/desktop/preload.js @@ -3,9 +3,15 @@ const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('electronAPI', { copyToClipboard: (text) => ipcRenderer.invoke('clipboard-write', text), 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'), downloadUpdate: () => ipcRenderer.send('download-update'), 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)), onUpdateDownloaded: (callback) => ipcRenderer.on('update-downloaded', (event, info) => callback(info)), onUpdateError: (callback) => ipcRenderer.on('update-error', (event, error) => callback(error)) diff --git a/main.js b/main.js index 0f0878d..009d449 100644 --- a/main.js +++ b/main.js @@ -1,8 +1,82 @@ const { app, BrowserWindow, session, desktopCapturer, ipcMain, clipboard } = require('electron'); const { autoUpdater } = require('electron-updater'); const path = require('path'); +const fs = require('fs'); 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() { // Create a persistent session for chattz to keep the user logged in const sess = session.fromPartition('persist:chattz'); @@ -34,6 +108,34 @@ function createWindow() { 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) sess.setPermissionCheckHandler((webContents, permission) => { if (permission === 'media' || permission === 'clipboard-read' || permission === 'clipboard-write') { @@ -112,6 +214,14 @@ function createWindow() { 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 // win.webContents.openDevTools(); } @@ -127,27 +237,46 @@ app.whenReady().then(() => { autoUpdater.logger = console; autoUpdater.on('update-available', (info) => { + updateState.status = 'available'; + updateState.info = info; + updateState.error = null; + broadcastUpdateState(); const wins = BrowserWindow.getAllWindows(); 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) => { + updateState.status = 'downloaded'; + updateState.info = info; + updateState.error = null; + broadcastUpdateState(); const wins = BrowserWindow.getAllWindows(); if (wins.length > 0) wins[0].webContents.send('update-downloaded', info); }); autoUpdater.on('error', (err) => { + updateState.status = 'error'; + updateState.error = err.message; + broadcastUpdateState(); const wins = BrowserWindow.getAllWindows(); if (wins.length > 0) wins[0].webContents.send('update-error', err.message); }); ipcMain.on('check-for-updates', () => { - autoUpdater.checkForUpdatesAndNotify().catch(err => { - console.error("Manual update check failed", err); - }); + void runUpdateCheck('manual'); }); ipcMain.on('download-update', () => { + updateState.status = 'downloading'; + updateState.error = null; + broadcastUpdateState(); autoUpdater.downloadUpdate(); }); @@ -157,7 +286,7 @@ app.whenReady().then(() => { // Check once on startup setTimeout(() => { - autoUpdater.checkForUpdatesAndNotify().catch(() => { }); + void runUpdateCheck('startup'); }, 5000); app.on('activate', () => { diff --git a/static/shared/app-core.js b/static/shared/app-core.js index 2b5fa49..97d405d 100644 --- a/static/shared/app-core.js +++ b/static/shared/app-core.js @@ -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() From ad6eaad5d28a262eb878994b17d2372ccd1e528a Mon Sep 17 00:00:00 2001 From: pavel Date: Fri, 27 Feb 2026 12:35:56 +0100 Subject: [PATCH 08/24] updater stuff --- .forgejo/workflows/pipeline.yaml | 9 +++ desktop/index.html | 2 +- main.js | 117 +++++++++++++++++-------------- static/shared/app-core.js | 17 +++-- 4 files changed, 83 insertions(+), 62 deletions(-) diff --git a/.forgejo/workflows/pipeline.yaml b/.forgejo/workflows/pipeline.yaml index 9e241e8..089f873 100644 --- a/.forgejo/workflows/pipeline.yaml +++ b/.forgejo/workflows/pipeline.yaml @@ -50,11 +50,20 @@ jobs: cp "$file" "static/installers/$out" fi } + # Copy versioned artifacts for electron-updater + cp dist/*.AppImage static/installers/ || true + cp dist/*.exe static/installers/ || true + cp dist/*.rpm static/installers/ || true + cp dist/*.deb static/installers/ || true + cp dist/*.msi static/installers/ || true + + # Maintain generic names for stable website links copy_first '*.rpm' 'chattz-linux.rpm' copy_first '*.deb' 'chattz-linux.deb' copy_first '*.AppImage' 'chattz-linux.AppImage' copy_first '*.exe' 'chattz-windows.exe' copy_first '*.msi' 'chattz-windows.msi' + # Metadata files for electron-updater copy_first 'latest-linux.yml' 'latest-linux.yml' copy_first 'latest.yml' 'latest.yml' diff --git a/desktop/index.html b/desktop/index.html index 07f8f7d..ed754ec 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -268,4 +268,4 @@ - + \ No newline at end of file diff --git a/main.js b/main.js index 009d449..3a32c94 100644 --- a/main.js +++ b/main.js @@ -51,10 +51,16 @@ function isNewerVersionAvailable(info) { return Boolean(next) && next !== app.getVersion(); } +let updateCheckInProgress = false; + async function runUpdateCheck(reason = 'manual') { + if (updateCheckInProgress) { + console.log(`Update check skipped (${reason}): another check is already in progress`); + return; + } + updateCheckInProgress = true; updateState.status = 'checking'; updateState.error = null; - broadcastUpdateState(); try { const result = await autoUpdater.checkForUpdates(); const info = result && result.updateInfo ? result.updateInfo : null; @@ -62,18 +68,18 @@ async function runUpdateCheck(reason = 'manual') { 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); + } finally { + updateCheckInProgress = false; + broadcastUpdateState(); } } @@ -95,46 +101,7 @@ function createWindow() { }); win.setAutoHideMenuBar(true); - win.setMenuBarVisibility(false); - - ipcMain.handle('get-config', () => { - return { - backendUrl: (process.env.CHATTZ_URL || 'https://discord.flegr.me').replace(/\/$/, '') - }; - }); - - ipcMain.handle('clipboard-write', (event, text) => { - clipboard.writeText(text); - 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 }; - }); + win.setMenuBarVisibility(true); // Auto-approve media permissions (camera, microphone) sess.setPermissionCheckHandler((webContents, permission) => { @@ -194,12 +161,14 @@ function createWindow() { }); }; - // Use a navigation listener to detect when the remote login is complete - win.webContents.on('will-navigate', (event, navigatedUrl) => { + // Intercept navigations/redirects that return to the backend with a token + // after the OAuth login flow. Two handlers are needed because: + // - will-navigate fires for client-side navigations (location.href, link clicks) + // - will-redirect fires for server-side 302 redirects (OAuth callback chain) + const interceptLoginRedirect = (event, navigatedUrl) => { try { const urlObj = new URL(navigatedUrl); const backendObj = new URL(backendUrl); - // Detect the redirect back to the home page with a token if (urlObj.origin === backendObj.origin && urlObj.pathname === '/') { if (urlObj.searchParams.has('token')) { console.log("Detected login success redirect, returning to desktop UI..."); @@ -210,16 +179,20 @@ function createWindow() { } catch (e) { console.error(e); } - }); + }; + + win.webContents.on('will-navigate', interceptLoginRedirect); + win.webContents.on('will-redirect', interceptLoginRedirect); loadDesktopApp(); // Ensure renderer receives latest updater state after any (re)load. + // Delay broadcast by 300ms to give the renderer time to register its + // onUpdateState IPC listener before we push state. 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'); + setTimeout(() => { + if (!win.isDestroyed()) broadcastUpdateState(); + }, 300); }); // Uncomment to debug @@ -230,6 +203,46 @@ app.commandLine.appendSwitch('disable-webrtc-hw-encoding'); // Sometime helps re app.commandLine.appendSwitch('log-level', '3'); // Silences standard Chromium warning logs (like SRTP transport unprotect logs which are benign timing issues) app.whenReady().then(() => { + // Register IPC handlers once (before creating any windows) + ipcMain.handle('get-config', () => { + return { + backendUrl: (process.env.CHATTZ_URL || 'https://discord.flegr.me').replace(/\/$/, '') + }; + }); + + ipcMain.handle('clipboard-write', (event, text) => { + clipboard.writeText(text); + 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 }; + }); + createWindow(); // Configure Auto-Updater diff --git a/static/shared/app-core.js b/static/shared/app-core.js index 97d405d..f14b88d 100644 --- a/static/shared/app-core.js +++ b/static/shared/app-core.js @@ -95,19 +95,18 @@ async function storageRemoveCritical(key) { 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 { + // IPC file store (renderer-storage.json) is the source of truth — + // it always survives restarts, unlike localStorage on file:// URLs. const val = await window.electronAPI.storageGet(key); if (typeof val === "string") { localStorage.setItem(key, val); + } else { + // IPC store is empty; backfill from localStorage if available + const localVal = localStorage.getItem(key); + if (localVal !== null) { + await window.electronAPI.storageSet(key, localVal); + } } } catch (err) { console.warn("Failed to hydrate storage key", key, err); From 86a5c4b7c30da80e48ed63283d99b945e07bcedf Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Fri, 27 Feb 2026 11:37:00 +0000 Subject: [PATCH 09/24] chore: bump version to 0.0.49 [skip ci] --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f657203..7f023f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "chattz-electron", - "version": "0.0.48", + "version": "0.0.49", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chattz-electron", - "version": "0.0.48", + "version": "0.0.49", "dependencies": { "electron-updater": "^6.8.3" }, diff --git a/package.json b/package.json index 9949c75..8484627 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chattz-electron", - "version": "0.0.48", + "version": "0.0.49", "description": "Electron frontend for Chattz", "author": "Pavel Flegr ", "homepage": "https://discord.flegr.me", From ec3b29f4d92147a17202805a955f63410a3923f9 Mon Sep 17 00:00:00 2001 From: pavel Date: Fri, 27 Feb 2026 14:35:10 +0100 Subject: [PATCH 10/24] missing rpm --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8484627..0a59d4f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "productName": "Chattz", "linux": { "target": [ - "AppImage" + "AppImage", + "rpm" ], "category": "Chat", "icon": "build/icon.png" From 80be74782c762ccc0c3b012a52d43359e5ee481a Mon Sep 17 00:00:00 2001 From: Forgejo Actions Date: Fri, 27 Feb 2026 13:37:20 +0000 Subject: [PATCH 11/24] chore: bump version to 0.0.50 [skip ci] --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7f023f4..686ca4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "chattz-electron", - "version": "0.0.49", + "version": "0.0.50", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chattz-electron", - "version": "0.0.49", + "version": "0.0.50", "dependencies": { "electron-updater": "^6.8.3" }, diff --git a/package.json b/package.json index 0a59d4f..6967283 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chattz-electron", - "version": "0.0.49", + "version": "0.0.50", "description": "Electron frontend for Chattz", "author": "Pavel Flegr ", "homepage": "https://discord.flegr.me", From 003391e60e01f06e8e9b595cf203868b769e53c4 Mon Sep 17 00:00:00 2001 From: pavel Date: Fri, 27 Feb 2026 14:59:10 +0100 Subject: [PATCH 12/24] chat scroll --- static/shared/app-core.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/static/shared/app-core.js b/static/shared/app-core.js index f14b88d..a7ad4e1 100644 --- a/static/shared/app-core.js +++ b/static/shared/app-core.js @@ -354,6 +354,11 @@ function formatDate(isoString) { return d.toLocaleDateString(); } +function scrollMessagesToBottom() { + if (!el.messageList) return; + el.messageList.scrollTop = el.messageList.scrollHeight; +} + let audioCtx = null; // Global interaction listener to unlock AudioContext (autoplay policy) @@ -628,7 +633,17 @@ function renderMessages(messages) { state.lastMessageId = messages[0].id; } - el.messageList.scrollTop = el.messageList.scrollHeight; + scrollMessagesToBottom(); + requestAnimationFrame(() => { + scrollMessagesToBottom(); + }); + + // GIF/image height resolves after initial paint; keep chat pinned to latest. + for (const gifImage of el.messageList.querySelectorAll(".msg-gif img")) { + if (gifImage.complete) continue; + gifImage.addEventListener("load", scrollMessagesToBottom, { once: true }); + gifImage.addEventListener("error", scrollMessagesToBottom, { once: true }); + } } function renderMembers() { From cfef57d8a187e37f949b999e5b6c0bffeae5af92 Mon Sep 17 00:00:00 2001 From: pavel Date: Fri, 27 Feb 2026 15:00:17 +0100 Subject: [PATCH 13/24] skip version commit --- .forgejo/workflows/pipeline.yaml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.forgejo/workflows/pipeline.yaml b/.forgejo/workflows/pipeline.yaml index 089f873..c76ae86 100644 --- a/.forgejo/workflows/pipeline.yaml +++ b/.forgejo/workflows/pipeline.yaml @@ -19,17 +19,6 @@ jobs: VERSION=${GITHUB_REF_NAME#v} echo "Bumping version to $VERSION" npm version $VERSION --no-git-tag-version - - # Configure Git - git config user.name "Forgejo Actions" - git config user.email "actions@noreply.flegr.me" - - # Commit and push back to main if version changed - if [ -n "$(git status --porcelain package.json)" ]; then - git add package.json package-lock.json - git commit -m "chore: bump version to $VERSION [skip ci]" - git push origin HEAD:main - fi - run: npm ci - run: npm run dist:linux - name: Build Windows installer with 32-bit Wine prefix From e1dd679c47b7fc890c126d69fb11e9177405d57c Mon Sep 17 00:00:00 2001 From: pavel Date: Fri, 27 Feb 2026 15:38:46 +0100 Subject: [PATCH 14/24] feat: media uploads --- .env.example | 7 + Cargo.lock | 815 +++++++++++++++++- Cargo.toml | 2 + src/config.rs | 45 +- src/db.rs | 193 ++++- src/entity/attachments.rs | 66 ++ src/entity/mod.rs | 1 + src/handlers.rs | 261 +++++- src/main.rs | 12 +- src/media.rs | 74 ++ src/migration/m20260213_000001_init.rs | 29 +- src/migration/m20260213_000002_invites.rs | 7 +- .../m20260213_000004_direct_messages.rs | 12 +- src/migration/m20260227_000006_attachments.rs | 122 +++ src/migration/mod.rs | 2 + src/models.rs | 13 +- static/index.html | 4 + static/shared/app-core.js | 124 ++- static/styles.css | 91 +- 19 files changed, 1807 insertions(+), 73 deletions(-) create mode 100644 src/entity/attachments.rs create mode 100644 src/media.rs create mode 100644 src/migration/m20260227_000006_attachments.rs diff --git a/.env.example b/.env.example index 04a1dd8..c062b55 100644 --- a/.env.example +++ b/.env.example @@ -19,3 +19,10 @@ TURN_PASSWORD=replace-me # 32+ random chars; used to sign session cookies SESSION_SECRET=replace-with-long-random-secret COOKIE_SECURE=false + +# Cloudflare R2 media uploads +R2_ACCOUNT_ID=replace-me +R2_ACCESS_KEY_ID=replace-me +R2_SECRET_ACCESS_KEY=replace-me +R2_BUCKET=chattz-media +R2_PUBLIC_BASE_URL=https://media.example.com diff --git a/Cargo.lock b/Cargo.lock index 190bd84..b496651 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,6 +92,418 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-config" +version = "1.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a8fc176d53d6fe85017f230405e3255cedb4a02221cb55ed6d76dccbbb099b2" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http 0.63.5", + "aws-smithy-json 0.62.4", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 1.4.0", + "time", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d203b0bf2626dcba8665f5cd0871d7c2c0930223d6b6be9097592fea21242d0" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-runtime" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede2ddc593e6c8acc6ce3358c28d6677a6dc49b65ba4b37a2befe14a11297e75" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http 0.63.5", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.119.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d65fddc3844f902dfe1864acb8494db5f9342015ee3ab7890270d36fbd2e01c" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http 0.62.6", + "aws-smithy-json 0.61.9", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "lru", + "percent-encoding", + "regex-lite", + "sha2", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.99.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9acba7c62f3d4e2408fa998a3a8caacd8b9a5b5549cf36e2372fbdae329d5449" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http 0.63.5", + "aws-smithy-json 0.62.4", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37411f8e0f4bea0c3ca0958ce7f18f6439db24d555dbd809787262cd00926aa9" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http 0.63.5", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.4.0", + "percent-encoding", + "sha2", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc50d0f63e714784b84223abd7abbc8577de8c35d699e0edd19f0a88a08ae13" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.63.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87294a084b43d649d967efe58aa1f9e0adc260e13a6938eb904c0ae9b45824ae" +dependencies = [ + "aws-smithy-http 0.62.6", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 0.2.12", + "http-body 0.4.6", + "md-5", + "pin-project-lite", + "sha1", + "sha2", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c0b3e587fbaa5d7f7e870544508af8ce82ea47cd30376e69e1e37c4ac746f79" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.62.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http" +version = "0.63.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d619373d490ad70966994801bc126846afaa0d1ee920697a031f0cf63f2568e7" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00ccbb08c10f6bcf912f398188e42ee2eab5f1767ce215a02a73bc5df1bbdd95" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.13", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.8.1", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.7", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.36", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.61.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-json" +version = "0.62.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b3a779093e18cad88bbae08dc4261e1d95018c4c5b9356a52bcae7c0b6e9bb" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3f39d5bb871aaf461d59144557f16d5927a5248a983a40654d9cf3b9ba183b" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f76a580e3d8f8961e5d48763214025a2af65c2fa4cd1fb7f270a0e107a71b0" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ccf7f6eba8b2dcf8ce9b74806c6c185659c311665c4bf8d6e71ebd454db6bf" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http 0.63.5", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4af6e5def28be846479bbeac55aa4603d6f7986fc5da4601ba324dd5d377516" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca2734c16913a45343b37313605d84e7d8b34a4611598ce1d25b35860a2bed3" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b53543b4b86ed43f051644f704a98c7291b3618b67adf057ee77a366fa52fcaa" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0470cc047657c6e286346bdf10a8719d26efd6a91626992e0e64481e44323e96" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + [[package]] name = "axum" version = "0.8.8" @@ -104,10 +516,10 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.8.1", "hyper-util", "itoa", "matchit", @@ -138,8 +550,8 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", @@ -166,6 +578,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.8.3" @@ -208,6 +630,16 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + [[package]] name = "cc" version = "1.2.56" @@ -215,6 +647,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -235,6 +669,8 @@ name = "chattz" version = "0.1.0" dependencies = [ "anyhow", + "aws-config", + "aws-sdk-s3", "axum", "chrono", "dotenvy", @@ -267,6 +703,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -282,6 +727,16 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -312,6 +767,28 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc-fast" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3" +dependencies = [ + "crc", + "digest", + "rand 0.9.2", + "regex", + "rustversion", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -448,6 +925,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -494,6 +977,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -532,6 +1021,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.31" @@ -673,6 +1168,44 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -744,6 +1277,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.4.0" @@ -754,6 +1298,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -761,7 +1316,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.4.0", ] [[package]] @@ -772,8 +1327,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -795,6 +1350,30 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.8.1" @@ -805,8 +1384,9 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "http", - "http-body", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -817,19 +1397,35 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + [[package]] name = "hyper-rustls" version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http", - "hyper", + "http 1.4.0", + "hyper 1.8.1", "hyper-util", - "rustls", + "rustls 0.23.36", + "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tower-service", "webpki-roots 1.0.6", ] @@ -844,14 +1440,14 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", - "http-body", - "hyper", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.8.1", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.2", "tokio", "tower-service", "tracing", @@ -1032,6 +1628,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -1120,6 +1726,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -1193,7 +1808,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http", + "http 1.4.0", "httparse", "memchr", "mime", @@ -1278,6 +1893,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "ordered-float" version = "4.6.0" @@ -1311,6 +1932,12 @@ dependencies = [ "syn", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "parking" version = "2.2.1" @@ -1484,8 +2111,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls", - "socket2", + "rustls 0.23.36", + "socket2 0.6.2", "thiserror", "tokio", "tracing", @@ -1504,7 +2131,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls", + "rustls 0.23.36", "rustls-pki-types", "slab", "thiserror", @@ -1522,7 +2149,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.6.2", "tracing", "windows-sys 0.60.2", ] @@ -1642,6 +2269,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + [[package]] name = "regex-syntax" version = "0.8.9" @@ -1657,25 +2290,25 @@ dependencies = [ "base64", "bytes", "futures-core", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", - "hyper", - "hyper-rustls", + "hyper 1.8.1", + "hyper-rustls 0.27.7", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", "quinn", - "rustls", + "rustls 0.23.36", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.4", "tower", "tower-http", "tower-service", @@ -1735,20 +2368,45 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ + "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.9", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -1759,12 +2417,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1782,12 +2451,31 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "sea-bae" version = "0.2.1" @@ -1935,6 +2623,29 @@ dependencies = [ "syn", ] +[[package]] +name = "security-framework" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -2081,6 +2792,16 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.2" @@ -2147,7 +2868,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rustls", + "rustls 0.23.36", "serde", "serde_json", "sha2", @@ -2469,7 +3190,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", - "socket2", + "socket2 0.6.2", "tokio-macros", "windows-sys 0.61.2", ] @@ -2485,13 +3206,23 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls", + "rustls 0.23.36", "tokio", ] @@ -2557,8 +3288,8 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", - "http-body", + "http 1.4.0", + "http-body 1.0.1", "http-body-util", "http-range-header", "httpdate", @@ -2663,7 +3394,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http", + "http 1.4.0", "httparse", "log", "rand 0.9.2", @@ -2783,6 +3514,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "want" version = "0.3.1" @@ -3213,6 +3950,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 5ab0c14..d7ece96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,8 @@ edition = "2024" [dependencies] anyhow = "1" +aws-config = { version = "1", default-features = false, features = ["behavior-version-latest", "rt-tokio", "rustls"] } +aws-sdk-s3 = { version = "1", default-features = false, features = ["rt-tokio", "rustls"] } axum = { version = "0.8", features = ["macros", "ws", "multipart"] } chrono = { version = "0.4", features = ["serde"] } dotenvy = "0.15" diff --git a/src/config.rs b/src/config.rs index 0696d55..8f55aa0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,6 +17,16 @@ pub struct Settings { pub turn_urls: Vec, pub turn_username: Option, pub turn_password: Option, + pub media: Option, +} + +#[derive(Clone, Debug)] +pub struct MediaSettings { + pub account_id: String, + pub access_key_id: String, + pub secret_access_key: String, + pub bucket: String, + pub public_base_url: String, } impl Settings { @@ -33,7 +43,8 @@ impl Settings { oidc_token_url: required("OIDC_TOKEN_URL")?, oidc_userinfo_url: required("OIDC_USERINFO_URL")?, oidc_redirect_url: required("OIDC_REDIRECT_URL")?, - oidc_scopes: std::env::var("OIDC_SCOPES").unwrap_or_else(|_| "openid profile email".to_string()), + oidc_scopes: std::env::var("OIDC_SCOPES") + .unwrap_or_else(|_| "openid profile email".to_string()), session_secret: required("SESSION_SECRET")?, cookie_secure: std::env::var("COOKIE_SECURE") .unwrap_or_else(|_| "false".into()) @@ -43,10 +54,42 @@ impl Settings { turn_urls: parse_csv_env("TURN_URLS", ""), turn_username: optional("TURN_USERNAME"), turn_password: optional("TURN_PASSWORD"), + media: MediaSettings::from_env()?, }) } } +impl MediaSettings { + fn from_env() -> Result> { + let account_id = optional("R2_ACCOUNT_ID"); + let access_key_id = optional("R2_ACCESS_KEY_ID"); + let secret_access_key = optional("R2_SECRET_ACCESS_KEY"); + let bucket = optional("R2_BUCKET"); + let public_base_url = optional("R2_PUBLIC_BASE_URL"); + + if account_id.is_none() + && access_key_id.is_none() + && secret_access_key.is_none() + && bucket.is_none() + && public_base_url.is_none() + { + return Ok(None); + } + + Ok(Some(Self { + account_id: account_id.context("missing env var R2_ACCOUNT_ID")?, + access_key_id: access_key_id.context("missing env var R2_ACCESS_KEY_ID")?, + secret_access_key: secret_access_key.context("missing env var R2_SECRET_ACCESS_KEY")?, + bucket: bucket.context("missing env var R2_BUCKET")?, + public_base_url: public_base_url.context("missing env var R2_PUBLIC_BASE_URL")?, + })) + } + + pub fn endpoint_url(&self) -> String { + format!("https://{}.r2.cloudflarestorage.com", self.account_id) + } +} + fn required(name: &str) -> Result { std::env::var(name).with_context(|| format!("missing env var {name}")) } diff --git a/src/db.rs b/src/db.rs index d4364bc..42d980b 100644 --- a/src/db.rs +++ b/src/db.rs @@ -9,12 +9,12 @@ use uuid::Uuid; use crate::{ entity::{ - channels, direct_messages, guild_members, guilds, invites, messages, soundboard_sounds, - users, + attachments, channels, direct_messages, guild_members, guilds, invites, messages, + soundboard_sounds, users, }, models::{ - BasicUser, Channel, DmConversation, DmMessageWithAuthor, Guild, Invite, MessageWithAuthor, - SoundboardSound, User, + Attachment, BasicUser, Channel, DmConversation, DmMessageWithAuthor, Guild, Invite, + MessageWithAuthor, SoundboardSound, User, }, }; @@ -350,6 +350,63 @@ pub async fn create_message( author_user_id: model.author_user_id, author_display_name: user.display_name, body: model.body, + attachments: Vec::new(), + created_at: model.created_at, + }) +} + +pub async fn create_message_with_attachment( + db: &DatabaseConnection, + channel_id: Uuid, + author_user_id: Uuid, + body: &str, + object_key: &str, + media_url: &str, + mime_type: &str, + size_bytes: i64, + original_filename: &str, +) -> Result { + let txn = db.begin().await?; + + let model = messages::Entity::insert(messages::ActiveModel { + id: Set(Uuid::new_v4()), + channel_id: Set(channel_id), + author_user_id: Set(author_user_id), + body: Set(body.to_string()), + ..Default::default() + }) + .exec_with_returning(&txn) + .await?; + + let attachment = attachments::Entity::insert(attachments::ActiveModel { + id: Set(Uuid::new_v4()), + channel_message_id: Set(Some(model.id)), + direct_message_id: Set(None), + uploader_user_id: Set(author_user_id), + object_key: Set(object_key.to_string()), + media_url: Set(media_url.to_string()), + mime_type: Set(mime_type.to_string()), + size_bytes: Set(size_bytes), + original_filename: Set(original_filename.to_string()), + ..Default::default() + }) + .exec_with_returning(&txn) + .await?; + + let user = users::Entity::find_by_id(author_user_id) + .one(&txn) + .await? + .ok_or_else(|| anyhow!("author not found"))?; + + txn.commit().await?; + + Ok(MessageWithAuthor { + id: model.id, + channel_id: model.channel_id, + author_user_id: model.author_user_id, + author_display_name: user.display_name, + body: model.body, + attachments: vec![map_attachment(attachment)], created_at: model.created_at, }) } @@ -367,6 +424,10 @@ pub async fn list_messages( .all(db) .await?; + let attachment_map = + list_attachments_for_channel_messages(db, rows.iter().map(|(msg, _)| msg.id).collect()) + .await?; + Ok(rows .into_iter() .map(|(msg, user)| { @@ -379,6 +440,7 @@ pub async fn list_messages( author_user_id: msg.author_user_id, author_display_name, body: msg.body, + attachments: attachment_map.get(&msg.id).cloned().unwrap_or_default(), created_at: msg.created_at, } }) @@ -412,6 +474,63 @@ pub async fn create_direct_message( recipient_user_id: model.recipient_user_id, author_display_name: user.display_name, body: model.body, + attachments: Vec::new(), + created_at: model.created_at, + }) +} + +pub async fn create_direct_message_with_attachment( + db: &DatabaseConnection, + sender_user_id: Uuid, + recipient_user_id: Uuid, + body: &str, + object_key: &str, + media_url: &str, + mime_type: &str, + size_bytes: i64, + original_filename: &str, +) -> Result { + let txn = db.begin().await?; + + let model = direct_messages::Entity::insert(direct_messages::ActiveModel { + id: Set(Uuid::new_v4()), + sender_user_id: Set(sender_user_id), + recipient_user_id: Set(recipient_user_id), + body: Set(body.to_string()), + ..Default::default() + }) + .exec_with_returning(&txn) + .await?; + + let attachment = attachments::Entity::insert(attachments::ActiveModel { + id: Set(Uuid::new_v4()), + channel_message_id: Set(None), + direct_message_id: Set(Some(model.id)), + uploader_user_id: Set(sender_user_id), + object_key: Set(object_key.to_string()), + media_url: Set(media_url.to_string()), + mime_type: Set(mime_type.to_string()), + size_bytes: Set(size_bytes), + original_filename: Set(original_filename.to_string()), + ..Default::default() + }) + .exec_with_returning(&txn) + .await?; + + let user = users::Entity::find_by_id(sender_user_id) + .one(&txn) + .await? + .ok_or_else(|| anyhow!("sender not found"))?; + + txn.commit().await?; + + Ok(DmMessageWithAuthor { + id: model.id, + author_user_id: model.sender_user_id, + recipient_user_id: model.recipient_user_id, + author_display_name: user.display_name, + body: model.body, + attachments: vec![map_attachment(attachment)], created_at: model.created_at, }) } @@ -454,6 +573,9 @@ pub async fn list_direct_messages( .map(|u| (u.id, u.display_name)) .collect(); + let attachment_map = + list_attachments_for_direct_messages(db, rows.iter().map(|msg| msg.id).collect()).await?; + Ok(rows .into_iter() .map(|msg| DmMessageWithAuthor { @@ -465,11 +587,64 @@ pub async fn list_direct_messages( .cloned() .unwrap_or_else(|| "Unknown User".to_string()), body: msg.body, + attachments: attachment_map.get(&msg.id).cloned().unwrap_or_default(), created_at: msg.created_at, }) .collect()) } +async fn list_attachments_for_channel_messages( + db: &DatabaseConnection, + message_ids: Vec, +) -> Result>> { + if message_ids.is_empty() { + return Ok(std::collections::HashMap::new()); + } + + let rows = attachments::Entity::find() + .filter(attachments::Column::ChannelMessageId.is_in(message_ids)) + .order_by_asc(attachments::Column::CreatedAt) + .all(db) + .await?; + + let mut grouped = std::collections::HashMap::>::new(); + for row in rows { + if let Some(message_id) = row.channel_message_id { + grouped + .entry(message_id) + .or_default() + .push(map_attachment(row)); + } + } + Ok(grouped) +} + +async fn list_attachments_for_direct_messages( + db: &DatabaseConnection, + message_ids: Vec, +) -> Result>> { + if message_ids.is_empty() { + return Ok(std::collections::HashMap::new()); + } + + let rows = attachments::Entity::find() + .filter(attachments::Column::DirectMessageId.is_in(message_ids)) + .order_by_asc(attachments::Column::CreatedAt) + .all(db) + .await?; + + let mut grouped = std::collections::HashMap::>::new(); + for row in rows { + if let Some(message_id) = row.direct_message_id { + grouped + .entry(message_id) + .or_default() + .push(map_attachment(row)); + } + } + Ok(grouped) +} + pub async fn list_dm_conversations( db: &DatabaseConnection, current_user_id: Uuid, @@ -543,6 +718,16 @@ fn map_guild(model: guilds::Model) -> Guild { } } +fn map_attachment(model: attachments::Model) -> Attachment { + Attachment { + id: model.id, + media_url: model.media_url, + mime_type: model.mime_type, + size_bytes: model.size_bytes, + original_filename: model.original_filename, + } +} + fn map_channel(model: channels::Model) -> Channel { Channel { id: model.id, diff --git a/src/entity/attachments.rs b/src/entity/attachments.rs new file mode 100644 index 0000000..f5581d1 --- /dev/null +++ b/src/entity/attachments.rs @@ -0,0 +1,66 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "attachments")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub channel_message_id: Option, + pub direct_message_id: Option, + pub uploader_user_id: Uuid, + pub object_key: String, + pub media_url: String, + pub mime_type: String, + pub size_bytes: i64, + pub original_filename: String, + pub created_at: DateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::messages::Entity", + from = "Column::ChannelMessageId", + to = "super::messages::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Messages, + #[sea_orm( + belongs_to = "super::direct_messages::Entity", + from = "Column::DirectMessageId", + to = "super::direct_messages::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + DirectMessages, + #[sea_orm( + belongs_to = "super::users::Entity", + from = "Column::UploaderUserId", + to = "super::users::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Users, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Messages.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::DirectMessages.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Users.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/entity/mod.rs b/src/entity/mod.rs index d1e64f7..d5dda3d 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -1,3 +1,4 @@ +pub mod attachments; pub mod channels; pub mod direct_messages; pub mod guild_members; diff --git a/src/handlers.rs b/src/handlers.rs index 9337339..5c2f27b 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1,10 +1,11 @@ use axum::{ Json, Router, - extract::{Path, Query, State, WebSocketUpgrade}, + extract::{Multipart, Path, Query, State, WebSocketUpgrade}, http::{HeaderMap, StatusCode, header}, response::{Html, IntoResponse, Redirect}, routing::{get, post}, }; +use chrono::Datelike; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -12,7 +13,7 @@ use crate::{ AppState, auth::{self, ApiError, AuthUser}, chat, db, - models::{Guild, SoundboardSound}, + models::{DmMessageWithAuthor, Guild, MessageWithAuthor, SoundboardSound}, voice, }; use tracing::info; @@ -30,6 +31,10 @@ pub fn routes() -> Router { "/dms/{other_user_id}/messages", get(list_dm_messages).post(send_dm_message), ) + .route( + "/dms/{other_user_id}/attachments", + post(upload_dm_attachment), + ) .route("/presence", get(presence_list)) .route("/rtc-config", get(rtc_config)) .route("/guilds", get(list_guilds).post(create_guild)) @@ -54,6 +59,10 @@ pub fn routes() -> Router { "/channels/{channel_id}/messages", get(list_messages).post(send_message), ) + .route( + "/channels/{channel_id}/attachments", + post(upload_channel_attachment), + ) .route("/channels/{channel_id}/voice/ws", get(voice_ws)) .route("/ws", get(chat_ws)) } @@ -680,6 +689,89 @@ async fn send_dm_message( Ok((StatusCode::CREATED, Json(SendMessageResponse { ok: true }))) } +async fn upload_channel_attachment( + State(state): State, + user: AuthUser, + Path(channel_id): Path, + multipart: Multipart, +) -> Result { + ensure_channel_member(&state, channel_id, user.id).await?; + let uploaded = + upload_media_from_multipart(&state, multipart, channel_object_key_prefix(channel_id)) + .await?; + + let message = db::create_message_with_attachment( + &state.db, + channel_id, + user.id, + "", + &uploaded.object_key, + &uploaded.media_url, + &uploaded.mime_type, + uploaded.size_bytes, + &uploaded.original_filename, + ) + .await + .map_err(|e| ApiError::internal(&format!("failed to create message attachment: {e}")))?; + + let guild_id = db::guild_id_for_channel(&state.db, channel_id) + .await + .map_err(|e| ApiError::internal(&format!("channel lookup failed: {e}")))? + .ok_or_else(|| ApiError::internal("channel not found after attachment upload"))?; + + let members = db::list_guild_member_ids(&state.db, guild_id) + .await + .map_err(|e| ApiError::internal(&format!("failed to list guild members: {e}")))?; + + broadcast_channel_message(&state, members, channel_id, &message).await; + Ok((StatusCode::CREATED, Json(message))) +} + +async fn upload_dm_attachment( + State(state): State, + user: AuthUser, + Path(other_user_id): Path, + multipart: Multipart, +) -> Result { + if other_user_id == user.id { + return Err(ApiError::bad_request("cannot send dm to yourself")); + } + + let other_user_exists = db::user_exists(&state.db, other_user_id) + .await + .map_err(|e| ApiError::internal(&format!("user check failed: {e}")))?; + if !other_user_exists { + return Err(ApiError { + status: StatusCode::NOT_FOUND, + message: "user not found".to_string(), + }); + } + + let uploaded = upload_media_from_multipart( + &state, + multipart, + dm_object_key_prefix(user.id, other_user_id), + ) + .await?; + + let message = db::create_direct_message_with_attachment( + &state.db, + user.id, + other_user_id, + "", + &uploaded.object_key, + &uploaded.media_url, + &uploaded.mime_type, + uploaded.size_bytes, + &uploaded.original_filename, + ) + .await + .map_err(|e| ApiError::internal(&format!("failed to create dm attachment: {e}")))?; + + broadcast_dm_message(&state, user.id, other_user_id, &message).await; + Ok((StatusCode::CREATED, Json(message))) +} + async fn voice_ws( ws: WebSocketUpgrade, State(state): State, @@ -794,6 +886,171 @@ async fn upload_sound( Ok(Json(sound)) } +const MAX_MEDIA_UPLOAD_BYTES: usize = 25 * 1024 * 1024; + +struct UploadedMedia { + object_key: String, + media_url: String, + mime_type: String, + size_bytes: i64, + original_filename: String, +} + +async fn upload_media_from_multipart( + state: &AppState, + mut multipart: Multipart, + object_key_prefix: String, +) -> Result { + let storage = state + .media + .as_ref() + .ok_or_else(|| ApiError::internal("media storage is not configured"))?; + + let mut file_name = None; + let mut file_data = None; + let mut mime_type = None; + + while let Some(field) = multipart + .next_field() + .await + .map_err(|e| ApiError::bad_request(&e.to_string()))? + { + if field.name().unwrap_or_default() != "file" { + continue; + } + + file_name = Some(field.file_name().unwrap_or("upload.bin").to_string()); + mime_type = Some( + field + .content_type() + .unwrap_or("application/octet-stream") + .to_string(), + ); + let bytes = field + .bytes() + .await + .map_err(|e| ApiError::bad_request(&e.to_string()))?; + if bytes.len() > MAX_MEDIA_UPLOAD_BYTES { + return Err(ApiError::bad_request("file exceeds 25MB upload limit")); + } + file_data = Some(bytes.to_vec()); + break; + } + + let (original_filename, mime_type, file_data) = match (file_name, mime_type, file_data) { + (Some(name), Some(mime), Some(data)) => (name, mime, data), + _ => return Err(ApiError::bad_request("missing file upload")), + }; + + let safe_name = sanitize_file_name(&original_filename); + let object_key = format!("{}/{}-{}", object_key_prefix, Uuid::new_v4(), safe_name); + let size_bytes = file_data.len() as i64; + let media_url = storage + .upload_object(&object_key, file_data, &mime_type, &original_filename) + .await + .map_err(|e| ApiError::internal(&e.to_string()))?; + + Ok(UploadedMedia { + object_key, + media_url, + mime_type, + size_bytes, + original_filename, + }) +} + +async fn broadcast_channel_message( + state: &AppState, + members: Vec, + channel_id: Uuid, + message: &MessageWithAuthor, +) { + state + .chat + .broadcast_to_many( + members, + chat::ServerEvent::MessageCreated { + channel_id, + message: serde_json::to_value(message).unwrap_or_default(), + }, + ) + .await; +} + +async fn broadcast_dm_message( + state: &AppState, + current_user_id: Uuid, + other_user_id: Uuid, + message: &DmMessageWithAuthor, +) { + state + .chat + .broadcast_to_user( + current_user_id, + chat::ServerEvent::DmCreated { + other_user_id, + message: serde_json::to_value(message).unwrap_or_default(), + }, + ) + .await; + state + .chat + .broadcast_to_user( + other_user_id, + chat::ServerEvent::DmCreated { + other_user_id: current_user_id, + message: serde_json::to_value(message).unwrap_or_default(), + }, + ) + .await; +} + +fn sanitize_file_name(file_name: &str) -> String { + let sanitized: String = file_name + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '_') { + ch + } else { + '_' + } + }) + .collect(); + + let trimmed = sanitized.trim_matches('_').trim(); + if trimmed.is_empty() { + "upload.bin".to_string() + } else { + trimmed.to_string() + } +} + +fn channel_object_key_prefix(channel_id: Uuid) -> String { + let now = chrono::Utc::now(); + format!( + "channels/{}/{:04}/{:02}", + channel_id, + now.year(), + now.month() + ) +} + +fn dm_object_key_prefix(user_a: Uuid, user_b: Uuid) -> String { + let now = chrono::Utc::now(); + let (left, right) = if user_a <= user_b { + (user_a, user_b) + } else { + (user_b, user_a) + }; + format!( + "dms/{}-{}/{:04}/{:02}", + left, + right, + now.year(), + now.month() + ) +} + async fn delete_sound_post( state: State, user: AuthUser, diff --git a/src/main.rs b/src/main.rs index 53ea6f2..303636f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod config; mod db; mod entity; mod handlers; +mod media; mod migration; mod models; mod voice; @@ -17,7 +18,9 @@ use sea_orm_migration::MigratorTrait; use tower_http::{services::ServeDir, trace::TraceLayer}; use tracing::info; -use crate::{config::Settings, handlers::routes, migration::Migrator, voice::VoiceHub}; +use crate::{ + config::Settings, handlers::routes, media::MediaStorage, migration::Migrator, voice::VoiceHub, +}; #[derive(Clone)] pub struct AppState { @@ -26,6 +29,7 @@ pub struct AppState { pub http: reqwest::Client, pub voice: Arc, pub chat: Arc, + pub media: Option>, } #[tokio::main] @@ -43,12 +47,18 @@ async fn main() -> anyhow::Result<()> { .await .with_context(|| "failed to run migrations")?; + let media = match settings.media.as_ref() { + Some(media_settings) => Some(Arc::new(MediaStorage::new(media_settings).await?)), + None => None, + }; + let state = AppState { db, settings, http: reqwest::Client::new(), voice: Arc::new(VoiceHub::default()), chat: Arc::new(chat::ChatHub::default()), + media, }; let port = state.settings.port; diff --git a/src/media.rs b/src/media.rs new file mode 100644 index 0000000..ee7bd23 --- /dev/null +++ b/src/media.rs @@ -0,0 +1,74 @@ +use anyhow::{Result, anyhow}; +use aws_config::BehaviorVersion; +use aws_sdk_s3::{ + Client, + config::{Credentials, Region}, + primitives::ByteStream, +}; + +use crate::config::MediaSettings; + +#[derive(Clone)] +pub struct MediaStorage { + client: Client, + bucket: String, + public_base_url: String, +} + +impl MediaStorage { + pub async fn new(settings: &MediaSettings) -> Result { + let shared_config = aws_config::defaults(BehaviorVersion::latest()) + .region(Region::new("auto")) + .credentials_provider(Credentials::new( + settings.access_key_id.clone(), + settings.secret_access_key.clone(), + None, + None, + "chattz-r2", + )) + .load() + .await; + + let config = aws_sdk_s3::config::Builder::from(&shared_config) + .endpoint_url(settings.endpoint_url()) + .force_path_style(true) + .build(); + + Ok(Self { + client: Client::from_conf(config), + bucket: settings.bucket.clone(), + public_base_url: settings.public_base_url.trim_end_matches('/').to_string(), + }) + } + + pub async fn upload_object( + &self, + object_key: &str, + bytes: Vec, + content_type: &str, + original_filename: &str, + ) -> Result { + self.client + .put_object() + .bucket(&self.bucket) + .key(object_key) + .body(ByteStream::from(bytes)) + .content_type(content_type) + .content_disposition(format!( + "inline; filename=\"{}\"", + sanitize_header_value(original_filename) + )) + .send() + .await + .map_err(|e| anyhow!("failed to upload object to R2: {e}"))?; + + Ok(format!("{}/{}", self.public_base_url, object_key)) + } +} + +fn sanitize_header_value(value: &str) -> String { + value + .chars() + .filter(|c| *c != '\\' && *c != '"' && !c.is_control()) + .collect() +} diff --git a/src/migration/m20260213_000001_init.rs b/src/migration/m20260213_000001_init.rs index 9c671d4..3c1d1ff 100644 --- a/src/migration/m20260213_000001_init.rs +++ b/src/migration/m20260213_000001_init.rs @@ -11,13 +11,13 @@ impl MigrationTrait for Migration { Table::create() .table(Users::Table) .if_not_exists() + .col(ColumnDef::new(Users::Id).uuid().not_null().primary_key()) .col( - ColumnDef::new(Users::Id) - .uuid() + ColumnDef::new(Users::OidcSub) + .string() .not_null() - .primary_key(), + .unique_key(), ) - .col(ColumnDef::new(Users::OidcSub).string().not_null().unique_key()) .col(ColumnDef::new(Users::Email).string()) .col(ColumnDef::new(Users::DisplayName).string().not_null()) .col(ColumnDef::new(Users::AvatarUrl).string()) @@ -42,12 +42,7 @@ impl MigrationTrait for Migration { Table::create() .table(Guilds::Table) .if_not_exists() - .col( - ColumnDef::new(Guilds::Id) - .uuid() - .not_null() - .primary_key(), - ) + .col(ColumnDef::new(Guilds::Id).uuid().not_null().primary_key()) .col(ColumnDef::new(Guilds::Name).string().not_null()) .col(ColumnDef::new(Guilds::OwnerUserId).uuid().not_null()) .col( @@ -108,12 +103,7 @@ impl MigrationTrait for Migration { Table::create() .table(Channels::Table) .if_not_exists() - .col( - ColumnDef::new(Channels::Id) - .uuid() - .not_null() - .primary_key(), - ) + .col(ColumnDef::new(Channels::Id).uuid().not_null().primary_key()) .col(ColumnDef::new(Channels::GuildId).uuid().not_null()) .col(ColumnDef::new(Channels::Name).string().not_null()) .col( @@ -138,12 +128,7 @@ impl MigrationTrait for Migration { Table::create() .table(Messages::Table) .if_not_exists() - .col( - ColumnDef::new(Messages::Id) - .uuid() - .not_null() - .primary_key(), - ) + .col(ColumnDef::new(Messages::Id).uuid().not_null().primary_key()) .col(ColumnDef::new(Messages::ChannelId).uuid().not_null()) .col(ColumnDef::new(Messages::AuthorUserId).uuid().not_null()) .col(ColumnDef::new(Messages::Body).text().not_null()) diff --git a/src/migration/m20260213_000002_invites.rs b/src/migration/m20260213_000002_invites.rs index e99c28d..3dbcd5d 100644 --- a/src/migration/m20260213_000002_invites.rs +++ b/src/migration/m20260213_000002_invites.rs @@ -11,7 +11,12 @@ impl MigrationTrait for Migration { Table::create() .table(Invites::Table) .if_not_exists() - .col(ColumnDef::new(Invites::Code).string().not_null().primary_key()) + .col( + ColumnDef::new(Invites::Code) + .string() + .not_null() + .primary_key(), + ) .col(ColumnDef::new(Invites::GuildId).uuid().not_null()) .col(ColumnDef::new(Invites::CreatedByUserId).uuid().not_null()) .col( diff --git a/src/migration/m20260213_000004_direct_messages.rs b/src/migration/m20260213_000004_direct_messages.rs index 4b723ab..63ba7e2 100644 --- a/src/migration/m20260213_000004_direct_messages.rs +++ b/src/migration/m20260213_000004_direct_messages.rs @@ -17,8 +17,16 @@ impl MigrationTrait for Migration { .not_null() .primary_key(), ) - .col(ColumnDef::new(DirectMessages::SenderUserId).uuid().not_null()) - .col(ColumnDef::new(DirectMessages::RecipientUserId).uuid().not_null()) + .col( + ColumnDef::new(DirectMessages::SenderUserId) + .uuid() + .not_null(), + ) + .col( + ColumnDef::new(DirectMessages::RecipientUserId) + .uuid() + .not_null(), + ) .col(ColumnDef::new(DirectMessages::Body).text().not_null()) .col( ColumnDef::new(DirectMessages::CreatedAt) diff --git a/src/migration/m20260227_000006_attachments.rs b/src/migration/m20260227_000006_attachments.rs new file mode 100644 index 0000000..96f6da6 --- /dev/null +++ b/src/migration/m20260227_000006_attachments.rs @@ -0,0 +1,122 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Attachments::Table) + .if_not_exists() + .col( + ColumnDef::new(Attachments::Id) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(Attachments::ChannelMessageId).uuid().null()) + .col(ColumnDef::new(Attachments::DirectMessageId).uuid().null()) + .col(ColumnDef::new(Attachments::UploaderUserId).uuid().not_null()) + .col(ColumnDef::new(Attachments::ObjectKey).string().not_null()) + .col(ColumnDef::new(Attachments::MediaUrl).string().not_null()) + .col(ColumnDef::new(Attachments::MimeType).string().not_null()) + .col(ColumnDef::new(Attachments::SizeBytes).big_integer().not_null()) + .col(ColumnDef::new(Attachments::OriginalFilename).string().not_null()) + .col( + ColumnDef::new(Attachments::CreatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .foreign_key( + ForeignKey::create() + .name("fk_attachments_channel_message") + .from(Attachments::Table, Attachments::ChannelMessageId) + .to(Messages::Table, Messages::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_attachments_direct_message") + .from(Attachments::Table, Attachments::DirectMessageId) + .to(DirectMessages::Table, DirectMessages::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk_attachments_uploader") + .from(Attachments::Table, Attachments::UploaderUserId) + .to(Users::Table, Users::Id), + ) + .check( + Expr::cust( + "(channel_message_id IS NOT NULL AND direct_message_id IS NULL) OR (channel_message_id IS NULL AND direct_message_id IS NOT NULL)", + ), + ) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx_attachments_channel_message") + .table(Attachments::Table) + .col(Attachments::ChannelMessageId) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx_attachments_direct_message") + .table(Attachments::Table) + .col(Attachments::DirectMessageId) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Attachments::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +enum Attachments { + Table, + Id, + ChannelMessageId, + DirectMessageId, + UploaderUserId, + ObjectKey, + MediaUrl, + MimeType, + SizeBytes, + OriginalFilename, + CreatedAt, +} + +#[derive(DeriveIden)] +enum Messages { + Table, + Id, +} + +#[derive(DeriveIden)] +enum DirectMessages { + Table, + Id, +} + +#[derive(DeriveIden)] +enum Users { + Table, + Id, +} diff --git a/src/migration/mod.rs b/src/migration/mod.rs index 4db4f51..1ac95c9 100644 --- a/src/migration/mod.rs +++ b/src/migration/mod.rs @@ -5,6 +5,7 @@ mod m20260213_000002_invites; mod m20260213_000003_channel_kind; mod m20260213_000004_direct_messages; mod m20260224_000005_soundboard; +mod m20260227_000006_attachments; pub struct Migrator; @@ -17,6 +18,7 @@ impl MigratorTrait for Migrator { Box::new(m20260213_000003_channel_kind::Migration), Box::new(m20260213_000004_direct_messages::Migration), Box::new(m20260224_000005_soundboard::Migration), + Box::new(m20260227_000006_attachments::Migration), ] } } diff --git a/src/models.rs b/src/models.rs index 74ac74a..aabf16d 100644 --- a/src/models.rs +++ b/src/models.rs @@ -39,6 +39,15 @@ pub struct Message { pub created_at: DateTimeWithTimeZone, } +#[derive(Debug, Clone, Serialize)] +pub struct Attachment { + pub id: Uuid, + pub media_url: String, + pub mime_type: String, + pub size_bytes: i64, + pub original_filename: String, +} + #[derive(Debug, Clone, Serialize)] pub struct BasicUser { pub id: Uuid, @@ -46,13 +55,14 @@ pub struct BasicUser { pub avatar_url: Option, } -#[derive(Debug, Clone, Serialize, sea_orm::FromQueryResult)] +#[derive(Debug, Clone, Serialize)] pub struct MessageWithAuthor { pub id: Uuid, pub channel_id: Uuid, pub author_user_id: Uuid, pub author_display_name: String, pub body: String, + pub attachments: Vec, pub created_at: DateTimeWithTimeZone, } @@ -71,6 +81,7 @@ pub struct DmMessageWithAuthor { pub recipient_user_id: Uuid, pub author_display_name: String, pub body: String, + pub attachments: Vec, pub created_at: DateTimeWithTimeZone, } diff --git a/static/index.html b/static/index.html index 5aef5f7..6386f77 100644 --- a/static/index.html +++ b/static/index.html @@ -144,6 +144,10 @@
-
${formatMessageBody(m.body)}
+ ${contentHtml} `; } @@ -644,6 +687,15 @@ function renderMessages(messages) { gifImage.addEventListener("load", scrollMessagesToBottom, { once: true }); gifImage.addEventListener("error", scrollMessagesToBottom, { once: true }); } + + for (const media of el.messageList.querySelectorAll(".msg-attachment img, .msg-attachment video, .msg-attachment audio")) { + if ("complete" in media && media.complete) continue; + media.addEventListener("loadeddata", scrollMessagesToBottom, { once: true }); + media.addEventListener("load", scrollMessagesToBottom, { once: true }); + media.addEventListener("error", scrollMessagesToBottom, { once: true }); + } + + lucide.createIcons(); } function renderMembers() { @@ -1862,6 +1914,26 @@ async function init() { } catch (err) { alert(err.message); } }; + el.mediaUploadBtn.onclick = () => { + if (!state.selectedTextChannelId && !state.selectedDmUserId) { + alert("Select a chat first."); + return; + } + el.mediaFileInput.click(); + }; + + el.mediaFileInput.onchange = async () => { + const file = el.mediaFileInput.files?.[0]; + if (!file) return; + try { + await uploadMediaFile(file); + } catch (err) { + alert(err.message); + } finally { + el.mediaFileInput.value = ""; + } + }; + // 4. Voice/Soundboard Controls el.voiceVideoBtn.onclick = toggleVideo; el.voiceScreenBtn.onclick = toggleScreenShare; @@ -1962,6 +2034,54 @@ async function init() { } } + async function uploadMediaFile(file) { + const token = storageGet("chattz_token"); + if (!token) throw new Error("You need to log in again."); + + const path = state.selectedTextChannelId + ? `/channels/${state.selectedTextChannelId}/attachments` + : state.selectedDmUserId + ? `/dms/${state.selectedDmUserId}/attachments` + : null; + if (!path) throw new Error("Select a chat first."); + + const formData = new FormData(); + formData.append("file", file); + + el.mediaUploadBtn.disabled = true; + el.mediaUploadBtn.title = "Uploading..."; + + const fullUrl = API_BASE_URL ? `${API_BASE_URL}${path}` : path; + try { + const response = await fetch(fullUrl, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + }, + body: formData, + }); + + if (!response.ok) { + let detail = "upload failed"; + try { + const payload = await response.json(); + detail = payload.error || detail; + } catch { } + throw new Error(`${response.status}: ${detail}`); + } + + 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(); + } finally { + el.mediaUploadBtn.disabled = false; + el.mediaUploadBtn.title = "Upload File"; + } + } + // 6. Initial Data Loading & App Start try { state.me = await api("/me"); diff --git a/static/styles.css b/static/styles.css index 9147a37..aac3dd9 100644 --- a/static/styles.css +++ b/static/styles.css @@ -885,6 +885,10 @@ select { color: var(--text-normal); } +.hidden-file-input { + display: none; +} + /* ═══════════════════════════════════════════════════════════ */ /* GIF Picker Modal */ /* ═══════════════════════════════════════════════════════════ */ @@ -996,6 +1000,91 @@ select { object-fit: contain; } +.msg-attachment { + display: block; + margin-top: 8px; +} + +.msg-attachment-image { + max-width: min(440px, 100%); + border-radius: var(--radius-md); + overflow: hidden; +} + +.msg-attachment-image img { + width: 100%; + max-height: 360px; + object-fit: contain; + display: block; + background: rgba(0, 0, 0, 0.18); +} + +.msg-attachment-video { + max-width: min(520px, 100%); +} + +.msg-attachment-video video { + width: 100%; + max-height: 420px; + border-radius: var(--radius-md); + background: #000; +} + +.msg-attachment-audio { + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px 14px; + max-width: min(420px, 100%); + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.msg-attachment-audio audio { + width: 100%; +} + +.msg-attachment-audio a { + color: var(--text-link); + font-size: 13px; +} + +.msg-attachment-file { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + max-width: min(420px, 100%); + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.06); + color: var(--text-normal); +} + +.msg-attachment-file:hover { + background: rgba(255, 255, 255, 0.07); +} + +.msg-attachment-file i { + width: 18px; + height: 18px; + color: var(--text-link); + flex-shrink: 0; +} + +.msg-attachment-file span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.msg-attachment-file small { + color: var(--text-muted); + margin-left: auto; + white-space: nowrap; +} + /* ═══════════════════════════════════════════════════════════ */ /* Sound Board */ /* ═══════════════════════════════════════════════════════════ */ @@ -1618,4 +1707,4 @@ select { .chat-header { padding: 0 8px; } -} \ No newline at end of file +} From 5c97cc515fc77c9faf58354e585eaf596d91a5d2 Mon Sep 17 00:00:00 2001 From: pavel Date: Fri, 27 Feb 2026 15:51:03 +0100 Subject: [PATCH 15/24] fix: uploads --- desktop/index.html | 6 +++++- static/shared/app-core.js | 38 ++++++++++++++++++++------------------ 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/desktop/index.html b/desktop/index.html index ed754ec..fac5aa8 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -144,6 +144,10 @@
+ + @@ -268,4 +272,4 @@ - \ No newline at end of file + diff --git a/static/shared/app-core.js b/static/shared/app-core.js index 0aa2e4f..f7013a9 100644 --- a/static/shared/app-core.js +++ b/static/shared/app-core.js @@ -1914,25 +1914,27 @@ async function init() { } catch (err) { alert(err.message); } }; - el.mediaUploadBtn.onclick = () => { - if (!state.selectedTextChannelId && !state.selectedDmUserId) { - alert("Select a chat first."); - return; - } - el.mediaFileInput.click(); - }; + if (el.mediaUploadBtn && el.mediaFileInput) { + el.mediaUploadBtn.onclick = () => { + if (!state.selectedTextChannelId && !state.selectedDmUserId) { + alert("Select a chat first."); + return; + } + el.mediaFileInput.click(); + }; - el.mediaFileInput.onchange = async () => { - const file = el.mediaFileInput.files?.[0]; - if (!file) return; - try { - await uploadMediaFile(file); - } catch (err) { - alert(err.message); - } finally { - el.mediaFileInput.value = ""; - } - }; + el.mediaFileInput.onchange = async () => { + const file = el.mediaFileInput.files?.[0]; + if (!file) return; + try { + await uploadMediaFile(file); + } catch (err) { + alert(err.message); + } finally { + el.mediaFileInput.value = ""; + } + }; + } // 4. Voice/Soundboard Controls el.voiceVideoBtn.onclick = toggleVideo; From 6882b49336324285d2e9af0483c4feaf36eb0ba5 Mon Sep 17 00:00:00 2001 From: pavel Date: Fri, 27 Feb 2026 15:57:39 +0100 Subject: [PATCH 16/24] refactor --- desktop/index.html | 6 +- desktop/styles.css | 1629 ------------------------------- package.json | 11 +- scripts/generate-html.js | 53 + shared-html/index.template.html | 272 ++++++ static/index.html | 1 + 6 files changed, 335 insertions(+), 1637 deletions(-) delete mode 100644 desktop/styles.css create mode 100644 scripts/generate-html.js create mode 100644 shared-html/index.template.html diff --git a/desktop/index.html b/desktop/index.html index fac5aa8..cb1de11 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -1,3 +1,4 @@ + @@ -8,7 +9,7 @@ - + @@ -116,8 +117,7 @@
+ + diff --git a/shared-html/index.template.html b/shared-html/index.template.html index 9780068..bbf97e8 100644 --- a/shared-html/index.template.html +++ b/shared-html/index.template.html @@ -266,6 +266,16 @@ + + diff --git a/src/handlers.rs b/src/handlers.rs index 5c2f27b..6982750 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1,6 +1,7 @@ use axum::{ Json, Router, - extract::{Multipart, Path, Query, State, WebSocketUpgrade}, + body::Bytes, + extract::{DefaultBodyLimit, Path, Query, State, WebSocketUpgrade}, http::{HeaderMap, StatusCode, header}, response::{Html, IntoResponse, Redirect}, routing::{get, post}, @@ -35,6 +36,7 @@ pub fn routes() -> Router { "/dms/{other_user_id}/attachments", post(upload_dm_attachment), ) + .layer(DefaultBodyLimit::max(MAX_MEDIA_UPLOAD_BYTES)) .route("/presence", get(presence_list)) .route("/rtc-config", get(rtc_config)) .route("/guilds", get(list_guilds).post(create_guild)) @@ -50,6 +52,7 @@ pub fn routes() -> Router { "/guilds/{guild_id}/sounds", get(list_sounds).post(upload_sound), ) + .layer(DefaultBodyLimit::max(MAX_MEDIA_UPLOAD_BYTES)) .route( "/guilds/{guild_id}/sounds/{sound_id}", post(delete_sound_post).delete(delete_sound), @@ -63,6 +66,7 @@ pub fn routes() -> Router { "/channels/{channel_id}/attachments", post(upload_channel_attachment), ) + .layer(DefaultBodyLimit::max(MAX_MEDIA_UPLOAD_BYTES)) .route("/channels/{channel_id}/voice/ws", get(voice_ws)) .route("/ws", get(chat_ws)) } @@ -693,12 +697,17 @@ async fn upload_channel_attachment( State(state): State, user: AuthUser, Path(channel_id): Path, - multipart: Multipart, + headers: HeaderMap, + body: Bytes, ) -> Result { ensure_channel_member(&state, channel_id, user.id).await?; - let uploaded = - upload_media_from_multipart(&state, multipart, channel_object_key_prefix(channel_id)) - .await?; + let uploaded = upload_media_from_request( + &state, + &headers, + body, + channel_object_key_prefix(channel_id), + ) + .await?; let message = db::create_message_with_attachment( &state.db, @@ -731,7 +740,8 @@ async fn upload_dm_attachment( State(state): State, user: AuthUser, Path(other_user_id): Path, - multipart: Multipart, + headers: HeaderMap, + body: Bytes, ) -> Result { if other_user_id == user.id { return Err(ApiError::bad_request("cannot send dm to yourself")); @@ -747,9 +757,10 @@ async fn upload_dm_attachment( }); } - let uploaded = upload_media_from_multipart( + let uploaded = upload_media_from_request( &state, - multipart, + &headers, + body, dm_object_key_prefix(user.id, other_user_id), ) .await?; @@ -886,7 +897,7 @@ async fn upload_sound( Ok(Json(sound)) } -const MAX_MEDIA_UPLOAD_BYTES: usize = 25 * 1024 * 1024; +const MAX_MEDIA_UPLOAD_BYTES: usize = 50 * 1024 * 1024; struct UploadedMedia { object_key: String, @@ -896,9 +907,10 @@ struct UploadedMedia { original_filename: String, } -async fn upload_media_from_multipart( +async fn upload_media_from_request( state: &AppState, - mut multipart: Multipart, + headers: &HeaderMap, + body: Bytes, object_key_prefix: String, ) -> Result { let storage = state @@ -906,47 +918,34 @@ async fn upload_media_from_multipart( .as_ref() .ok_or_else(|| ApiError::internal("media storage is not configured"))?; - let mut file_name = None; - let mut file_data = None; - let mut mime_type = None; + let original_filename = headers + .get("x-file-name") + .and_then(|v| v.to_str().ok()) + .map(str::trim) + .filter(|v| !v.is_empty()) + .map(ToString::to_string) + .ok_or_else(|| ApiError::bad_request("missing x-file-name header"))?; - while let Some(field) = multipart - .next_field() - .await - .map_err(|e| ApiError::bad_request(&e.to_string()))? - { - if field.name().unwrap_or_default() != "file" { - continue; - } + let mime_type = headers + .get(header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .map(str::trim) + .filter(|v| !v.is_empty()) + .unwrap_or("application/octet-stream") + .to_string(); - file_name = Some(field.file_name().unwrap_or("upload.bin").to_string()); - mime_type = Some( - field - .content_type() - .unwrap_or("application/octet-stream") - .to_string(), - ); - let bytes = field - .bytes() - .await - .map_err(|e| ApiError::bad_request(&e.to_string()))?; - if bytes.len() > MAX_MEDIA_UPLOAD_BYTES { - return Err(ApiError::bad_request("file exceeds 25MB upload limit")); - } - file_data = Some(bytes.to_vec()); - break; + if body.is_empty() { + return Err(ApiError::bad_request("missing file upload")); + } + if body.len() > MAX_MEDIA_UPLOAD_BYTES { + return Err(ApiError::bad_request("file exceeds 50MB upload limit")); } - - let (original_filename, mime_type, file_data) = match (file_name, mime_type, file_data) { - (Some(name), Some(mime), Some(data)) => (name, mime, data), - _ => return Err(ApiError::bad_request("missing file upload")), - }; let safe_name = sanitize_file_name(&original_filename); let object_key = format!("{}/{}-{}", object_key_prefix, Uuid::new_v4(), safe_name); - let size_bytes = file_data.len() as i64; + let size_bytes = body.len() as i64; let media_url = storage - .upload_object(&object_key, file_data, &mime_type, &original_filename) + .upload_object(&object_key, body.to_vec(), &mime_type, &original_filename) .await .map_err(|e| ApiError::internal(&e.to_string()))?; diff --git a/static/index.html b/static/index.html index 65093cb..96baa20 100644 --- a/static/index.html +++ b/static/index.html @@ -270,6 +270,16 @@ + + diff --git a/static/shared/app-core.js b/static/shared/app-core.js index f7013a9..4779042 100644 --- a/static/shared/app-core.js +++ b/static/shared/app-core.js @@ -230,6 +230,9 @@ const el = { soundFile: document.getElementById('sound-file'), soundModalCancel: document.getElementById('sound-modal-cancel'), soundSubmitBtn: document.getElementById('sound-submit-btn'), + uploadLimitModal: document.getElementById('upload-limit-modal'), + uploadLimitMessage: document.getElementById('upload-limit-message'), + uploadLimitOk: document.getElementById('upload-limit-ok'), // Mobile mobileMenuBtn: document.getElementById("mobile-menu-btn"), @@ -357,6 +360,8 @@ function formatBytes(size) { return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`; } +const MAX_MEDIA_UPLOAD_BYTES = 50 * 1024 * 1024; + function renderAttachments(attachments = []) { if (!attachments.length) return ""; @@ -382,6 +387,16 @@ function renderAttachments(attachments = []) { }).join(""); } +function showMediaUploadLimitError(file) { + const selectedSize = formatBytes(file?.size || 0); + if (el.uploadLimitModal && el.uploadLimitMessage) { + el.uploadLimitMessage.textContent = `Uploads are limited to 50 MB per file. Selected file size: ${selectedSize}.`; + el.uploadLimitModal.classList.remove("hidden"); + return; + } + alert(`Uploads are limited to 50 MB per file.\nSelected file size: ${selectedSize}.`); +} + function formatDate(isoString) { const d = new Date(isoString); const now = new Date(); @@ -1967,6 +1982,17 @@ async function init() { } }; + if (el.uploadLimitOk && el.uploadLimitModal) { + el.uploadLimitOk.onclick = () => { + el.uploadLimitModal.classList.add("hidden"); + }; + el.uploadLimitModal.onclick = (e) => { + if (e.target === el.uploadLimitModal) { + el.uploadLimitModal.classList.add("hidden"); + } + }; + } + // 5. GIF Picker const TENOR_API_KEY = "exTiFGKJ0CzESIHzVQWy3pRO8I1MAdpRomg95DBSu2sg6e7YcHgThMI4giGAx8D0"; const TENOR_CLIENT_KEY = "pavel-discord"; @@ -2039,6 +2065,10 @@ async function init() { async function uploadMediaFile(file) { const token = storageGet("chattz_token"); if (!token) throw new Error("You need to log in again."); + if (file.size > MAX_MEDIA_UPLOAD_BYTES) { + showMediaUploadLimitError(file); + return; + } const path = state.selectedTextChannelId ? `/channels/${state.selectedTextChannelId}/attachments` @@ -2047,9 +2077,6 @@ async function init() { : null; if (!path) throw new Error("Select a chat first."); - const formData = new FormData(); - formData.append("file", file); - el.mediaUploadBtn.disabled = true; el.mediaUploadBtn.title = "Uploading..."; @@ -2059,8 +2086,10 @@ async function init() { method: "POST", headers: { Authorization: `Bearer ${token}`, + "Content-Type": file.type || "application/octet-stream", + "X-File-Name": file.name, }, - body: formData, + body: file, }); if (!response.ok) { diff --git a/static/styles.css b/static/styles.css index aac3dd9..cc5a4ad 100644 --- a/static/styles.css +++ b/static/styles.css @@ -1478,6 +1478,13 @@ select { letter-spacing: -0.3px; } +.modal-copy { + margin: 0; + text-align: center; + color: var(--text-normal); + line-height: 1.5; +} + .form-item { margin-bottom: 20px; } From 3acd082fb0bfadcbc89740ea9e5791c4ed5a7747 Mon Sep 17 00:00:00 2001 From: pavel Date: Fri, 27 Feb 2026 20:34:33 +0100 Subject: [PATCH 18/24] crazy refactor --- .env.example | 7 +- Cargo.lock | 51 +- Cargo.toml | 5 +- README.md | 11 +- desktop/index.html | 5 +- desktop/preload.js | 18 +- main.js | 143 +-- scripts/generate-html.js | 14 +- shared-html/index.template.html | 5 +- src/auth.rs | 272 ++-- src/chat.rs | 317 ++++- src/config.rs | 128 +- src/db.rs | 478 ++++++- src/entity/mod.rs | 1 + src/entity/sessions.rs | 35 + src/entity/soundboard_sounds.rs | 6 +- src/handlers.rs | 419 +++++-- src/main.rs | 1116 ++++++++++++++++- src/media.rs | 15 +- src/migration/m20260227_000007_sessions.rs | 107 ++ .../m20260227_000008_soundboard_media.rs | 72 ++ ...27_000009_soundboard_file_path_nullable.rs | 50 + src/migration/mod.rs | 6 + src/models.rs | 13 +- src/voice.rs | 494 ++++++-- static/index.html | 9 +- static/shared/app-core.js | 507 ++++---- static/vendor/lucide.min.js | 12 + 28 files changed, 3467 insertions(+), 849 deletions(-) create mode 100644 src/entity/sessions.rs create mode 100644 src/migration/m20260227_000007_sessions.rs create mode 100644 src/migration/m20260227_000008_soundboard_media.rs create mode 100644 src/migration/m20260227_000009_soundboard_file_path_nullable.rs create mode 100644 static/vendor/lucide.min.js diff --git a/.env.example b/.env.example index c062b55..1390efc 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5432/chattz PORT=3000 +APP_BASE_URL=http://localhost:3000 # Authentik OIDC app values OIDC_CLIENT_ID=replace-me @@ -16,13 +17,9 @@ TURN_URLS=turn:turn.example.com:3478?transport=udp,turn:turn.example.com:3478?tr TURN_USERNAME=replace-me TURN_PASSWORD=replace-me -# 32+ random chars; used to sign session cookies -SESSION_SECRET=replace-with-long-random-secret -COOKIE_SECURE=false - # Cloudflare R2 media uploads R2_ACCOUNT_ID=replace-me R2_ACCESS_KEY_ID=replace-me R2_SECRET_ACCESS_KEY=replace-me R2_BUCKET=chattz-media -R2_PUBLIC_BASE_URL=https://media.example.com +MEDIA_BASE_URL=https://media.example.com diff --git a/Cargo.lock b/Cargo.lock index b496651..c44764d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -675,13 +675,14 @@ dependencies = [ "chrono", "dotenvy", "futures-util", - "jsonwebtoken", "reqwest", "sea-orm", "sea-orm-migration", "serde", "serde_json", + "sha2", "tokio", + "tower", "tower-http", "tracing", "tracing-subscriber", @@ -1648,21 +1649,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonwebtoken" -version = "9.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" -dependencies = [ - "base64", - "js-sys", - "pem", - "ring", - "serde", - "serde_json", - "simple_asn1", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -1825,16 +1811,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -1967,16 +1943,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "pem" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" -dependencies = [ - "base64", - "serde_core", -] - [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2765,18 +2731,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "simple_asn1" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror", - "time", -] - [[package]] name = "slab" version = "0.4.12" @@ -3131,7 +3085,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", diff --git a/Cargo.toml b/Cargo.toml index b6a6c27..6dbd768 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,14 +10,15 @@ aws-sdk-s3 = { version = "1", default-features = false, features = ["rt-tokio", axum = { version = "0.8", features = ["macros", "ws", "multipart"] } chrono = { version = "0.4", features = ["serde"] } dotenvy = "0.15" -jsonwebtoken = "9" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } -sea-orm = { version = "1.1", default-features = false, features = ["sqlx-postgres", "runtime-tokio-rustls", "macros", "with-chrono", "with-uuid"] } +sea-orm = { version = "1.1", default-features = false, features = ["sqlx-postgres", "runtime-tokio-rustls", "macros", "with-chrono", "with-uuid", "mock"] } sea-orm-migration = { version = "1.1", default-features = false, features = ["sqlx-postgres", "runtime-tokio-rustls"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +sha2 = "0.10" futures-util = "0.3" tokio = { version = "1", features = ["fs", "io-util", "macros", "rt-multi-thread"] } +tower = { version = "0.5", features = ["util"] } tower-http = { version = "0.6", features = ["trace", "fs"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } diff --git a/README.md b/README.md index af5e8f0..2708a60 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A simple single-instance Discord-style monolith in Rust using: ## What this includes - OIDC login flow (`/auth/login`, `/auth/callback`, `/auth/logout`) -- Signed session cookie auth +- HttpOnly session cookie auth - Channel voice chat over WebRTC (P2P mesh) with server WebSocket signaling - Guild invite codes (create + join) - Direct messages (DM) between users @@ -41,6 +41,11 @@ For voice reliability on restrictive networks, configure TURN in `.env`: - `TURN_USERNAME` - `TURN_PASSWORD` +For production deployments: +- `APP_BASE_URL` must be your public app origin and should use `https` +- `MEDIA_BASE_URL` should be a separate media origin for user uploads +- uploads and soundboard require R2/object storage to be configured + 3. Run app: ```bash @@ -55,7 +60,7 @@ Web UI is available at `http://localhost:${PORT}/`. ## Authentik setup notes Create an Authentik OAuth2/OIDC provider + application and set: -- Redirect URI: `http://localhost:3000/auth/callback` +- Redirect URI: `${APP_BASE_URL}/auth/callback` - Scopes including at least: `openid profile email` If you change `PORT`, update `OIDC_REDIRECT_URL` and this redirect URI to match. @@ -91,6 +96,7 @@ For Authentik these are commonly under `/application/o/...` for the app slug. - `GET /channels/:channel_id/voice/ws` (WebSocket signaling) All endpoints except health and auth flow require the session cookie from successful login. +Authenticated WebSocket connections (`/ws`, `/channels/:channel_id/voice/ws`) also use the same cookie session. ## Notes @@ -98,6 +104,7 @@ This is intentionally minimal and monolithic (single process, single Postgres in Voice is implemented as browser-to-browser WebRTC audio with signaling in this server. For two users behind strict NAT/firewall, you may need TURN for reliable connectivity. The web UI remembers the last selected guild in browser local storage and auto-selects it on reload. +User uploads are served from the configured media origin, not from `/static`. Mic filter modes in the UI: - `NSNet2 (Compat)`: always-on denoising mode (implemented using DeepFilterNet3 with lighter suppression preset) diff --git a/desktop/index.html b/desktop/index.html index e04011c..1684077 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -6,12 +6,9 @@ Chattz - - - - + diff --git a/desktop/preload.js b/desktop/preload.js index dfcc665..f43670e 100644 --- a/desktop/preload.js +++ b/desktop/preload.js @@ -1,18 +1,20 @@ const { contextBridge, ipcRenderer } = require('electron'); +function onIpc(channel, callback) { + const listener = (_event, payload) => callback(payload); + ipcRenderer.on(channel, listener); + return () => ipcRenderer.removeListener(channel, listener); +} + contextBridge.exposeInMainWorld('electronAPI', { copyToClipboard: (text) => ipcRenderer.invoke('clipboard-write', text), - 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'), downloadUpdate: () => ipcRenderer.send('download-update'), 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)), - onUpdateDownloaded: (callback) => ipcRenderer.on('update-downloaded', (event, info) => callback(info)), - onUpdateError: (callback) => ipcRenderer.on('update-error', (event, error) => callback(error)) + onUpdateState: (callback) => onIpc('update-state', callback), + onUpdateAvailable: (callback) => onIpc('update-available', callback), + onUpdateDownloaded: (callback) => onIpc('update-downloaded', callback), + onUpdateError: (callback) => onIpc('update-error', callback) }); diff --git a/main.js b/main.js index 3a32c94..1baa6e2 100644 --- a/main.js +++ b/main.js @@ -1,8 +1,6 @@ const { app, BrowserWindow, session, desktopCapturer, ipcMain, clipboard } = require('electron'); const { autoUpdater } = require('electron-updater'); const path = require('path'); -const fs = require('fs'); -const url = require('url'); const updateState = { status: 'idle', // idle | checking | available | downloading | downloaded | not-available | error @@ -19,33 +17,6 @@ function broadcastUpdateState() { } } -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(); @@ -84,8 +55,9 @@ async function runUpdateCheck(reason = 'manual') { } function createWindow() { - // Create a persistent session for chattz to keep the user logged in const sess = session.fromPartition('persist:chattz'); + const backendUrl = (process.env.CHATTZ_URL || process.env.APP_BASE_URL || 'http://localhost:3000').replace(/\/$/, ''); + const backendOrigin = new URL(backendUrl).origin; const win = new BrowserWindow({ width: 1200, @@ -94,7 +66,6 @@ function createWindow() { webPreferences: { nodeIntegration: false, contextIsolation: true, - webSecurity: false, // Required for cross-origin fetch/ws from file:// with cookies session: sess, preload: path.join(__dirname, 'desktop', 'preload.js') } @@ -103,32 +74,33 @@ function createWindow() { win.setAutoHideMenuBar(true); win.setMenuBarVisibility(true); - // Auto-approve media permissions (camera, microphone) sess.setPermissionCheckHandler((webContents, permission) => { - if (permission === 'media' || permission === 'clipboard-read' || permission === 'clipboard-write') { - return true; - } - return false; + const origin = safeOrigin(webContents.getURL()); + if (origin !== backendOrigin) return false; + return permission === 'media' || permission === 'clipboard-write'; }); sess.setPermissionRequestHandler((webContents, permission, callback) => { - if (permission === 'media' || permission === 'clipboard-read' || permission === 'clipboard-write') { + const origin = safeOrigin(webContents.getURL()); + if (origin === backendOrigin && (permission === 'media' || permission === 'clipboard-write')) { callback(true); } else { callback(false); } }); - // Handle screen share requests sess.setDisplayMediaRequestHandler((request, callback) => { + const origin = safeOrigin(request.frame?.url || win.webContents.getURL()); + if (origin !== backendOrigin) { + callback(null); + return; + } desktopCapturer.getSources({ types: ['screen', 'window'] }).then((sources) => { - // Provide the first screen source by default, or implement a picker window here if (sources && sources.length > 0) { - // We prefer a screen over a window if available, simple heuristic const screenSource = sources.find(s => s.id.startsWith('screen')) || sources[0]; callback({ video: screenSource, audio: 'loopback' }); } else { - callback(null); // Reject safely + callback(null); } }).catch(err => { console.error("Failed to get desktop sources for screen share", err); @@ -136,55 +108,9 @@ function createWindow() { }); }); - const backendUrl = (process.env.CHATTZ_URL || 'https://discord.flegr.me').replace(/\/$/, ''); - const indexPath = path.join(__dirname, 'desktop', 'index.html'); - - const loadDesktopApp = (queryParams = '') => { - const options = {}; - if (queryParams) { - try { - options.query = Object.fromEntries(new URLSearchParams(queryParams)); - } catch (e) { - console.error("Failed to parse query params", e); - } - } - - // Use setImmediate to ensure the current navigation tick is cleared, - // which prevents ERR_ABORTED (-3) on some platforms when interrupting a redirect. - setImmediate(() => { - if (win.isDestroyed()) return; - win.loadFile(indexPath, options).catch((err) => { - // Ignore aborted errors as they often happen during fast redirects - if (err.toString().includes('-3') || err.code === -3) return; - console.error(`Failed to load desktop file: ${err}`); - }); - }); - }; - - // Intercept navigations/redirects that return to the backend with a token - // after the OAuth login flow. Two handlers are needed because: - // - will-navigate fires for client-side navigations (location.href, link clicks) - // - will-redirect fires for server-side 302 redirects (OAuth callback chain) - const interceptLoginRedirect = (event, navigatedUrl) => { - try { - const urlObj = new URL(navigatedUrl); - const backendObj = new URL(backendUrl); - if (urlObj.origin === backendObj.origin && urlObj.pathname === '/') { - if (urlObj.searchParams.has('token')) { - console.log("Detected login success redirect, returning to desktop UI..."); - event.preventDefault(); - loadDesktopApp(urlObj.search.slice(1)); - } - } - } catch (e) { - console.error(e); - } - }; - - win.webContents.on('will-navigate', interceptLoginRedirect); - win.webContents.on('will-redirect', interceptLoginRedirect); - - loadDesktopApp(); + win.loadURL(backendUrl).catch((err) => { + console.error(`Failed to load desktop app: ${err}`); + }); // Ensure renderer receives latest updater state after any (re)load. // Delay broadcast by 300ms to give the renderer time to register its @@ -203,40 +129,11 @@ app.commandLine.appendSwitch('disable-webrtc-hw-encoding'); // Sometime helps re app.commandLine.appendSwitch('log-level', '3'); // Silences standard Chromium warning logs (like SRTP transport unprotect logs which are benign timing issues) app.whenReady().then(() => { - // Register IPC handlers once (before creating any windows) - ipcMain.handle('get-config', () => { - return { - backendUrl: (process.env.CHATTZ_URL || 'https://discord.flegr.me').replace(/\/$/, '') - }; - }); - ipcMain.handle('clipboard-write', (event, text) => { clipboard.writeText(text); 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'); @@ -314,3 +211,11 @@ app.on('window-all-closed', () => { app.quit(); } }); + +function safeOrigin(value) { + try { + return new URL(value).origin; + } catch (_) { + return null; + } +} diff --git a/scripts/generate-html.js b/scripts/generate-html.js index 1b3497b..a773454 100644 --- a/scripts/generate-html.js +++ b/scripts/generate-html.js @@ -5,26 +5,30 @@ const root = path.resolve(__dirname, '..'); const templatePath = path.join(root, 'shared-html', 'index.template.html'); const template = fs.readFileSync(templatePath, 'utf8'); +const updaterMarkup = ``; + const variants = { web: { styles_href: '/static/styles.css', + lucide_src: '/static/vendor/lucide.min.js', app_src: '/static/app.js?v=20260227-shared-core-1', web_downloads: `
Desktop downloads: Windows Linux (RPM)
`, - desktop_updater: '', + desktop_updater: updaterMarkup, outPath: path.join(root, 'static', 'index.html'), }, desktop: { styles_href: '../static/styles.css', + lucide_src: '../static/vendor/lucide.min.js', app_src: 'app.js?v=20260227-shared-core-1', web_downloads: '', - desktop_updater: ``, + desktop_updater: updaterMarkup, outPath: path.join(root, 'desktop', 'index.html'), }, }; diff --git a/shared-html/index.template.html b/shared-html/index.template.html index bbf97e8..3425611 100644 --- a/shared-html/index.template.html +++ b/shared-html/index.template.html @@ -5,12 +5,9 @@ Chattz - - - - + diff --git a/src/auth.rs b/src/auth.rs index 667c64d..535953e 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,33 +1,25 @@ -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -use anyhow::{Context, Result, anyhow}; +use anyhow::{Result, anyhow}; use axum::{ Json, extract::{FromRef, FromRequestParts}, - http::{StatusCode, request::Parts}, + http::{HeaderMap, StatusCode, request::Parts}, response::{IntoResponse, Response}, }; -use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode}; -use serde::{Deserialize, Serialize}; +use chrono::{Duration, Utc}; +use serde::Serialize; +use sha2::{Digest, Sha256}; use uuid::Uuid; use crate::{AppState, db}; -const OAUTH_STATE_COOKIE: &str = "chattz_oauth_state"; -const SESSION_TTL_SECS: u64 = 60 * 15; // 15 minutes -const REFRESH_TTL_SECS: u64 = 60 * 60 * 24 * 30; // 30 days - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct SessionClaims { - pub sub: String, - pub kind: String, // "access" or "refresh" - pub exp: usize, - pub iat: usize, -} +pub const OAUTH_STATE_COOKIE: &str = "chattz_oauth_state"; +pub const SESSION_COOKIE: &str = "chattz_session"; +const SESSION_TTL_DAYS: i64 = 30; #[derive(Debug, Clone)] pub struct AuthUser { pub id: Uuid, + pub session_id: String, } #[derive(Debug)] @@ -44,6 +36,13 @@ impl ApiError { } } + pub fn forbidden(msg: &str) -> Self { + Self { + status: StatusCode::FORBIDDEN, + message: msg.to_string(), + } + } + pub fn bad_request(msg: &str) -> Self { Self { status: StatusCode::BAD_REQUEST, @@ -57,6 +56,13 @@ impl ApiError { message: msg.to_string(), } } + + pub fn service_unavailable(msg: &str) -> Self { + Self { + status: StatusCode::SERVICE_UNAVAILABLE, + message: msg.to_string(), + } + } } #[derive(Serialize)] @@ -84,29 +90,25 @@ impl From for ApiError { impl FromRequestParts for AuthUser where - AppState: axum::extract::FromRef, + AppState: FromRef, S: Send + Sync, { type Rejection = ApiError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let app = AppState::from_ref(state); + let session_id = read_cookie_from_headers(&parts.headers, SESSION_COOKIE) + .ok_or_else(|| ApiError::unauthorized("missing session cookie"))?; - let token = read_bearer_token(parts) - .or_else(|| read_query_token(parts)) - .ok_or_else(|| ApiError::unauthorized("missing jwt token"))?; - - let user_id = verify_session(&token, &app.settings.session_secret, "access") - .map_err(|_| ApiError::unauthorized("invalid or expired token"))?; - - let exists = db::user_exists(&app.db, user_id) + let user_id = db::touch_active_session(&app.db, &session_id) .await - .map_err(|_| ApiError::unauthorized("session user not found"))?; - if !exists { - return Err(ApiError::unauthorized("session user not found")); - } + .map_err(|_| ApiError::unauthorized("invalid or expired session"))? + .ok_or_else(|| ApiError::unauthorized("invalid or expired session"))?; - Ok(Self { id: user_id }) + Ok(Self { + id: user_id, + session_id, + }) } } @@ -114,6 +116,14 @@ pub fn new_oauth_state() -> String { Uuid::new_v4().to_string() } +pub fn new_session_id() -> String { + Uuid::new_v4().simple().to_string() +} + +pub fn session_expiry() -> chrono::DateTime { + (Utc::now() + Duration::days(SESSION_TTL_DAYS)).fixed_offset() +} + pub fn make_oauth_state_cookie(value: &str, secure: bool) -> String { format!( "{name}={value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600{secure_flag}", @@ -123,76 +133,32 @@ pub fn make_oauth_state_cookie(value: &str, secure: bool) -> String { } pub fn clear_oauth_state_cookie(secure: bool) -> String { + clear_cookie(OAUTH_STATE_COOKIE, secure, "Lax") +} + +pub fn make_session_cookie(value: &str, secure: bool) -> String { format!( - "{name}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0{secure_flag}", - name = OAUTH_STATE_COOKIE, + "{name}={value}; Path=/; HttpOnly; SameSite=Strict; Max-Age={ttl}{secure_flag}", + name = SESSION_COOKIE, + ttl = Duration::days(SESSION_TTL_DAYS).num_seconds(), secure_flag = if secure { "; Secure" } else { "" } ) } -pub fn read_oauth_state_from_headers(headers: &axum::http::HeaderMap) -> Option { +pub fn clear_session_cookie(secure: bool) -> String { + clear_cookie(SESSION_COOKIE, secure, "Strict") +} + +pub fn read_cookie_from_headers(headers: &HeaderMap, cookie_name: &str) -> Option { let raw = headers.get(axum::http::header::COOKIE)?.to_str().ok()?; raw.split(';').find_map(|pair| { let mut kv = pair.trim().splitn(2, '='); let key = kv.next()?; let value = kv.next()?; - (key == OAUTH_STATE_COOKIE).then(|| value.to_string()) + (key == cookie_name).then(|| value.to_string()) }) } -pub fn new_jwt_tokens(user_id: Uuid, secret: &str) -> Result<(String, String)> { - let now = now_ts(); - - let access_claims = SessionClaims { - sub: user_id.to_string(), - kind: "access".to_string(), - iat: now as usize, - exp: (now + SESSION_TTL_SECS) as usize, - }; - - let refresh_claims = SessionClaims { - sub: user_id.to_string(), - kind: "refresh".to_string(), - iat: now as usize, - exp: (now + REFRESH_TTL_SECS) as usize, - }; - - let access_token = encode( - &Header::default(), - &access_claims, - &EncodingKey::from_secret(secret.as_bytes()), - ) - .context("failed to encode access token")?; - - let refresh_token = encode( - &Header::default(), - &refresh_claims, - &EncodingKey::from_secret(secret.as_bytes()), - ) - .context("failed to encode refresh token")?; - - Ok((access_token, refresh_token)) -} - -pub fn verify_session(token: &str, secret: &str, expected_kind: &str) -> Result { - let mut validation = Validation::default(); - validation.validate_exp = true; - - let data = decode::( - token, - &DecodingKey::from_secret(secret.as_bytes()), - &validation, - ) - .context("failed to decode session token")?; - - if data.claims.kind != expected_kind { - return Err(anyhow!("invalid token kind")); - } - - let user_id = Uuid::parse_str(&data.claims.sub).context("invalid sub in session token")?; - Ok(user_id) -} - pub fn validate_oauth_state(expected_cookie: Option, query_state: &str) -> Result<()> { let expected = expected_cookie.ok_or_else(|| anyhow!("missing oauth state cookie"))?; if expected != query_state { @@ -201,37 +167,113 @@ pub fn validate_oauth_state(expected_cookie: Option, query_state: &str) Ok(()) } -fn read_bearer_token(parts: &Parts) -> Option { - let raw = parts - .headers - .get(axum::http::header::AUTHORIZATION)? - .to_str() - .ok()?; - if raw.starts_with("Bearer ") { - Some(raw["Bearer ".len()..].trim().to_string()) - } else { - None +pub fn user_agent_hash(headers: &HeaderMap) -> Option { + header_hash(headers, axum::http::header::USER_AGENT.as_str()) +} + +pub fn ip_hash(headers: &HeaderMap) -> Option { + headers + .get("x-forwarded-for") + .and_then(|v| v.to_str().ok()) + .and_then(|raw| raw.split(',').next().map(str::trim)) + .filter(|value| !value.is_empty()) + .map(hash_string) + .or_else(|| header_hash(headers, "x-real-ip")) +} + +pub fn origin_matches(headers: &HeaderMap, expected_origin: &str) -> bool { + headers + .get(axum::http::header::ORIGIN) + .and_then(|value| value.to_str().ok()) + .map(|origin| origin == expected_origin) + .unwrap_or(false) +} + +fn header_hash(headers: &HeaderMap, header_name: &str) -> Option { + headers + .get(header_name) + .and_then(|v| v.to_str().ok()) + .filter(|value| !value.trim().is_empty()) + .map(hash_string) +} + +fn hash_string(value: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(value.as_bytes()); + let digest = hasher.finalize(); + let mut out = String::with_capacity(digest.len() * 2); + for byte in digest { + out.push(nibble_to_hex(byte >> 4)); + out.push(nibble_to_hex(byte & 0x0f)); + } + out +} + +fn nibble_to_hex(value: u8) -> char { + match value { + 0..=9 => (b'0' + value) as char, + 10..=15 => (b'a' + (value - 10)) as char, + _ => unreachable!(), } } -fn read_query_token(parts: &Parts) -> Option { - let query = parts.uri.query()?; +fn clear_cookie(name: &str, secure: bool, same_site: &str) -> String { + format!( + "{name}=; Path=/; HttpOnly; SameSite={same_site}; Max-Age=0{secure_flag}", + secure_flag = if secure { "; Secure" } else { "" } + ) +} - // Simple query param parsing without pulling in url::Url overhead - for pair in query.split('&') { - let mut kv = pair.splitn(2, '='); - let key = kv.next()?; - let value = kv.next()?; - if key == "token" { - return Some(value.to_string()); - } +#[cfg(test)] +mod tests { + use super::{ + SESSION_COOKIE, clear_session_cookie, hash_string, make_session_cookie, origin_matches, + read_cookie_from_headers, + }; + use axum::http::{HeaderMap, header}; + + #[test] + fn hash_string_is_stable() { + assert_eq!( + hash_string("example"), + "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c" + ); } - None -} -fn now_ts() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_else(|_| Duration::from_secs(0)) - .as_secs() + #[test] + fn reads_named_cookie_from_header() { + let mut headers = HeaderMap::new(); + headers.insert( + header::COOKIE, + "other=value; chattz_session=session-123; another=ok" + .parse() + .unwrap(), + ); + + assert_eq!( + read_cookie_from_headers(&headers, SESSION_COOKIE), + Some("session-123".to_string()) + ); + } + + #[test] + fn origin_match_requires_exact_origin() { + let mut headers = HeaderMap::new(); + headers.insert(header::ORIGIN, "http://localhost:3000".parse().unwrap()); + + assert!(origin_matches(&headers, "http://localhost:3000")); + assert!(!origin_matches(&headers, "https://localhost:3000")); + } + + #[test] + fn session_cookie_has_strict_policy_and_clear_cookie_expires() { + let session_cookie = make_session_cookie("session-123", false); + let cleared_cookie = clear_session_cookie(false); + + assert!(session_cookie.contains("HttpOnly")); + assert!(session_cookie.contains("SameSite=Strict")); + assert!(session_cookie.contains("Max-Age=")); + assert!(cleared_cookie.contains("SameSite=Strict")); + assert!(cleared_cookie.contains("Max-Age=0")); + } } diff --git a/src/chat.rs b/src/chat.rs index 8b59bf6..565f08b 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1,27 +1,30 @@ -use crate::AppState; +use std::collections::HashMap; + use axum::extract::ws::{Message, WebSocket}; use futures_util::{SinkExt, StreamExt}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use tokio::sync::{RwLock, mpsc}; use uuid::Uuid; +use crate::{AppState, db}; + #[derive(Clone)] pub struct ChatClient { tx: mpsc::UnboundedSender, is_idle: bool, } -#[derive(Serialize, Clone)] +#[derive(Serialize, Clone, Debug, PartialEq, Eq)] pub struct OnlineUser { pub user_id: Uuid, + pub online: bool, pub idle: bool, } #[derive(Default)] pub struct ChatHub { - // user_id -> client - clients: RwLock>, + // user_id -> connection_id -> client + clients: RwLock>>, } #[derive(Serialize, Clone)] @@ -45,85 +48,113 @@ pub enum ServerEvent { #[derive(Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] enum ClientEvent { - // Currently no interactive client events for the general chat WS Ping, SetIdleStatus { is_idle: bool }, } impl ChatHub { - pub async fn add_client(&self, user_id: Uuid, tx: mpsc::UnboundedSender) { + pub async fn add_client( + &self, + user_id: Uuid, + connection_id: Uuid, + tx: mpsc::UnboundedSender, + ) -> Option { let mut clients = self.clients.write().await; - clients.insert(user_id, ChatClient { tx, is_idle: false }); - } + let previous = aggregate_presence(clients.get(&user_id), user_id); - pub async fn remove_client(&self, user_id: Uuid) { - let mut clients = self.clients.write().await; - clients.remove(&user_id); - } - - pub async fn get_online_users(&self) -> Vec { - let clients = self.clients.read().await; clients + .entry(user_id) + .or_default() + .insert(connection_id, ChatClient { tx, is_idle: false }); + + let current = aggregate_presence(clients.get(&user_id), user_id); + presence_delta(previous, current) + } + + pub async fn remove_client(&self, user_id: Uuid, connection_id: Uuid) -> Option { + let mut clients = self.clients.write().await; + let previous = aggregate_presence(clients.get(&user_id), user_id); + + if let Some(connections) = clients.get_mut(&user_id) { + connections.remove(&connection_id); + if connections.is_empty() { + clients.remove(&user_id); + } + } + + let current = aggregate_presence(clients.get(&user_id), user_id); + presence_delta(previous, current) + } + + pub async fn get_online_users_for(&self, visible_user_ids: &[Uuid]) -> Vec { + let clients = self.clients.read().await; + visible_user_ids .iter() - .map(|(id, client)| OnlineUser { - user_id: *id, - idle: client.is_idle, - }) + .filter_map(|user_id| aggregate_presence(clients.get(user_id), *user_id)) + .filter(|presence| presence.online) .collect() } - pub async fn broadcast_all(&self, event: ServerEvent) { - let clients = self.clients.read().await; - for client in clients.values() { - let _ = client.tx.send(event.clone()); - } - } - - pub async fn broadcast_to_user(&self, user_id: Uuid, event: ServerEvent) { - let clients = self.clients.read().await; - if let Some(client) = clients.get(&user_id) { - let _ = client.tx.send(event); - } - } - pub async fn broadcast_to_many(&self, user_ids: Vec, event: ServerEvent) { let clients = self.clients.read().await; for user_id in user_ids { - if let Some(client) = clients.get(&user_id) { + if let Some(connections) = clients.get(&user_id) { + for client in connections.values() { + let _ = client.tx.send(event.clone()); + } + } + } + } + + pub async fn broadcast_to_user(&self, user_id: Uuid, event: ServerEvent) { + let clients = self.clients.read().await; + if let Some(connections) = clients.get(&user_id) { + for client in connections.values() { let _ = client.tx.send(event.clone()); } } } - pub async fn set_idle_status(&self, user_id: Uuid, is_idle: bool) { + pub async fn set_idle_status( + &self, + user_id: Uuid, + connection_id: Uuid, + is_idle: bool, + ) -> Option { + let mut clients = self.clients.write().await; + let previous = aggregate_presence(clients.get(&user_id), user_id); + + if let Some(connections) = clients.get_mut(&user_id) + && let Some(client) = connections.get_mut(&connection_id) { - let mut clients = self.clients.write().await; - if let Some(client) = clients.get_mut(&user_id) { - client.is_idle = is_idle; - } + client.is_idle = is_idle; } - self.broadcast_all(ServerEvent::UserPresence { - user_id, - online: true, - idle: is_idle, - }) - .await; + + let current = aggregate_presence(clients.get(&user_id), user_id); + presence_delta(previous, current) } } pub async fn handle_socket(state: AppState, socket: WebSocket, user_id: Uuid) { let (mut ws_sender, mut ws_receiver) = socket.split(); let (tx, mut rx) = mpsc::unbounded_channel::(); + let connection_id = Uuid::new_v4(); - state.chat.add_client(user_id, tx).await; - state - .chat - .broadcast_all(ServerEvent::UserPresence { - user_id, - online: true, - idle: false, - }) - .await; + if let Some(presence) = state.chat.add_client(user_id, connection_id, tx).await { + if let Ok(visible_user_ids) = db::list_visible_user_ids(&state.db, user_id).await { + state + .chat + .broadcast_to_many( + visible_user_ids, + ServerEvent::UserPresence { + user_id: presence.user_id, + online: presence.online, + idle: presence.idle, + }, + ) + .await; + } + } let send_task = tokio::spawn(async move { while let Some(event) = rx.recv().await { @@ -142,8 +173,24 @@ pub async fn handle_socket(state: AppState, socket: WebSocket, user_id: Uuid) { Message::Text(text) => { if let Ok(ClientEvent::SetIdleStatus { is_idle }) = serde_json::from_str::(&text) + && let Some(presence) = state + .chat + .set_idle_status(user_id, connection_id, is_idle) + .await + && let Ok(visible_user_ids) = + db::list_visible_user_ids(&state.db, user_id).await { - state.chat.set_idle_status(user_id, is_idle).await; + state + .chat + .broadcast_to_many( + visible_user_ids, + ServerEvent::UserPresence { + user_id: presence.user_id, + online: presence.online, + idle: presence.idle, + }, + ) + .await; } } _ => {} @@ -151,13 +198,157 @@ pub async fn handle_socket(state: AppState, socket: WebSocket, user_id: Uuid) { } send_task.abort(); - state.chat.remove_client(user_id).await; - state - .chat - .broadcast_all(ServerEvent::UserPresence { - user_id, + if let Some(presence) = state.chat.remove_client(user_id, connection_id).await + && let Ok(visible_user_ids) = db::list_visible_user_ids(&state.db, user_id).await + { + state + .chat + .broadcast_to_many( + visible_user_ids, + ServerEvent::UserPresence { + user_id: presence.user_id, + online: presence.online, + idle: presence.idle, + }, + ) + .await; + } +} + +fn aggregate_presence( + connections: Option<&HashMap>, + user_id: Uuid, +) -> Option { + let connections = connections?; + if connections.is_empty() { + return None; + } + + let idle = connections.values().all(|client| client.is_idle); + Some(OnlineUser { + user_id, + online: true, + idle, + }) +} + +fn presence_delta(previous: Option, current: Option) -> Option { + match (previous, current) { + (None, None) => None, + (Some(prev), Some(curr)) if prev == curr => None, + (Some(prev), None) => Some(OnlineUser { + user_id: prev.user_id, online: false, idle: false, - }) - .await; + }), + (_, Some(curr)) => Some(curr), + } +} + +#[cfg(test)] +mod tests { + use super::{ChatHub, OnlineUser, ServerEvent}; + use serde_json::json; + use tokio::sync::mpsc; + use uuid::Uuid; + + #[tokio::test] + async fn multiple_connections_keep_user_online_until_last_disconnect() { + let hub = ChatHub::default(); + let user_id = Uuid::new_v4(); + let first_connection = Uuid::new_v4(); + let second_connection = Uuid::new_v4(); + let (tx1, _rx1) = mpsc::unbounded_channel(); + let (tx2, _rx2) = mpsc::unbounded_channel(); + + let first_presence = hub.add_client(user_id, first_connection, tx1).await; + let second_presence = hub.add_client(user_id, second_connection, tx2).await; + let after_first_disconnect = hub.remove_client(user_id, first_connection).await; + let after_last_disconnect = hub.remove_client(user_id, second_connection).await; + + assert_eq!( + first_presence, + Some(OnlineUser { + user_id, + online: true, + idle: false, + }) + ); + assert_eq!(second_presence, None); + assert_eq!(after_first_disconnect, None); + assert_eq!( + after_last_disconnect, + Some(OnlineUser { + user_id, + online: false, + idle: false, + }) + ); + } + + #[tokio::test] + async fn idle_status_only_flips_when_all_connections_are_idle() { + let hub = ChatHub::default(); + let user_id = Uuid::new_v4(); + let first_connection = Uuid::new_v4(); + let second_connection = Uuid::new_v4(); + let (tx1, _rx1) = mpsc::unbounded_channel(); + let (tx2, _rx2) = mpsc::unbounded_channel(); + + hub.add_client(user_id, first_connection, tx1).await; + hub.add_client(user_id, second_connection, tx2).await; + + let first_idle = hub.set_idle_status(user_id, first_connection, true).await; + let second_idle = hub.set_idle_status(user_id, second_connection, true).await; + let active_again = hub.set_idle_status(user_id, first_connection, false).await; + + assert_eq!(first_idle, None); + assert_eq!( + second_idle, + Some(OnlineUser { + user_id, + online: true, + idle: true, + }) + ); + assert_eq!( + active_again, + Some(OnlineUser { + user_id, + online: true, + idle: false, + }) + ); + } + + #[tokio::test] + async fn broadcast_to_user_reaches_all_active_connections() { + let hub = ChatHub::default(); + let user_id = Uuid::new_v4(); + let first_connection = Uuid::new_v4(); + let second_connection = Uuid::new_v4(); + let (tx1, mut rx1) = mpsc::unbounded_channel(); + let (tx2, mut rx2) = mpsc::unbounded_channel(); + + hub.add_client(user_id, first_connection, tx1).await; + hub.add_client(user_id, second_connection, tx2).await; + + hub.broadcast_to_user( + user_id, + ServerEvent::DmCreated { + other_user_id: Uuid::new_v4(), + message: json!({ "body": "hello" }), + }, + ) + .await; + + assert!(matches!( + rx1.try_recv(), + Ok(ServerEvent::DmCreated { message, .. }) if message["body"] == "hello" + )); + assert!(matches!( + rx2.try_recv(), + Ok(ServerEvent::DmCreated { message, .. }) if message["body"] == "hello" + )); + } } diff --git a/src/config.rs b/src/config.rs index 8f55aa0..fbe7ecb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,11 @@ -use anyhow::{Context, Result}; +use anyhow::{Context, Result, anyhow}; +use reqwest::Url; #[derive(Clone, Debug)] pub struct Settings { pub port: u16, + pub app_base_url: String, + pub app_origin: String, pub database_url: String, pub oidc_client_id: String, pub oidc_client_secret: String, @@ -11,8 +14,7 @@ pub struct Settings { pub oidc_userinfo_url: String, pub oidc_redirect_url: String, pub oidc_scopes: String, - pub session_secret: String, - pub cookie_secure: bool, + pub session_cookie_secure: bool, pub stun_urls: Vec, pub turn_urls: Vec, pub turn_username: Option, @@ -31,11 +33,17 @@ pub struct MediaSettings { impl Settings { pub fn from_env() -> Result { + let app_base_url = required("APP_BASE_URL")?; + let app_url = Url::parse(&app_base_url).context("APP_BASE_URL must be a valid URL")?; + let app_origin = origin_from_url(&app_url)?; + Ok(Self { port: std::env::var("PORT") .unwrap_or_else(|_| "3000".into()) .parse() .context("PORT must be a valid u16")?, + app_base_url: trim_trailing_slash(&app_base_url), + app_origin, database_url: required("DATABASE_URL")?, oidc_client_id: required("OIDC_CLIENT_ID")?, oidc_client_secret: required("OIDC_CLIENT_SECRET")?, @@ -45,11 +53,7 @@ impl Settings { oidc_redirect_url: required("OIDC_REDIRECT_URL")?, oidc_scopes: std::env::var("OIDC_SCOPES") .unwrap_or_else(|_| "openid profile email".to_string()), - session_secret: required("SESSION_SECRET")?, - cookie_secure: std::env::var("COOKIE_SECURE") - .unwrap_or_else(|_| "false".into()) - .parse() - .context("COOKIE_SECURE must be true/false")?, + session_cookie_secure: requires_secure_cookie(&app_url)?, stun_urls: parse_csv_env("STUN_URLS", "stun:stun.l.google.com:19302"), turn_urls: parse_csv_env("TURN_URLS", ""), turn_username: optional("TURN_USERNAME"), @@ -65,7 +69,7 @@ impl MediaSettings { let access_key_id = optional("R2_ACCESS_KEY_ID"); let secret_access_key = optional("R2_SECRET_ACCESS_KEY"); let bucket = optional("R2_BUCKET"); - let public_base_url = optional("R2_PUBLIC_BASE_URL"); + let public_base_url = optional("MEDIA_BASE_URL").or_else(|| optional("R2_PUBLIC_BASE_URL")); if account_id.is_none() && access_key_id.is_none() @@ -81,7 +85,9 @@ impl MediaSettings { access_key_id: access_key_id.context("missing env var R2_ACCESS_KEY_ID")?, secret_access_key: secret_access_key.context("missing env var R2_SECRET_ACCESS_KEY")?, bucket: bucket.context("missing env var R2_BUCKET")?, - public_base_url: public_base_url.context("missing env var R2_PUBLIC_BASE_URL")?, + public_base_url: trim_trailing_slash( + &public_base_url.context("missing env var MEDIA_BASE_URL")?, + ), })) } @@ -109,3 +115,105 @@ fn parse_csv_env(name: &str, default_value: &str) -> Vec { .map(ToString::to_string) .collect() } + +fn origin_from_url(url: &Url) -> Result { + let host = url + .host_str() + .ok_or_else(|| anyhow!("APP_BASE_URL must include a host"))?; + let mut origin = format!("{}://{}", url.scheme(), host); + if let Some(port) = url.port() { + origin.push(':'); + origin.push_str(&port.to_string()); + } + Ok(origin) +} + +fn requires_secure_cookie(url: &Url) -> Result { + match url.scheme() { + "https" => Ok(true), + "http" => { + let host = url + .host_str() + .ok_or_else(|| anyhow!("APP_BASE_URL must include a host"))?; + if matches!(host, "localhost" | "127.0.0.1" | "::1") { + Ok(false) + } else { + Err(anyhow!( + "APP_BASE_URL must use https outside localhost when session cookies are enabled" + )) + } + } + other => Err(anyhow!("APP_BASE_URL scheme {other} is not supported")), + } +} + +fn trim_trailing_slash(value: &str) -> String { + value.trim_end_matches('/').to_string() +} + +#[cfg(test)] +mod tests { + use super::{MediaSettings, origin_from_url, requires_secure_cookie, trim_trailing_slash}; + use reqwest::Url; + + #[test] + fn origin_from_url_preserves_explicit_port() { + let url = Url::parse("https://chat.example.com:8443/app").unwrap(); + let origin = origin_from_url(&url).unwrap(); + + assert_eq!(origin, "https://chat.example.com:8443"); + } + + #[test] + fn secure_cookie_is_required_for_https_origins() { + let url = Url::parse("https://chat.example.com").unwrap(); + + assert_eq!(requires_secure_cookie(&url).unwrap(), true); + } + + #[test] + fn localhost_http_is_allowed_without_secure_cookie() { + let url = Url::parse("http://localhost:3000").unwrap(); + + assert_eq!(requires_secure_cookie(&url).unwrap(), false); + } + + #[test] + fn non_localhost_http_is_rejected() { + let url = Url::parse("http://chat.example.com").unwrap(); + let err = requires_secure_cookie(&url).unwrap_err(); + + assert!( + err.to_string() + .contains("APP_BASE_URL must use https outside localhost") + ); + } + + #[test] + fn trim_trailing_slash_removes_only_suffix_slashes() { + assert_eq!( + trim_trailing_slash("https://chat.example.com///"), + "https://chat.example.com" + ); + assert_eq!( + trim_trailing_slash("https://chat.example.com/app"), + "https://chat.example.com/app" + ); + } + + #[test] + fn media_endpoint_url_uses_account_id() { + let media = MediaSettings { + account_id: "acct123".to_string(), + access_key_id: "key".to_string(), + secret_access_key: "secret".to_string(), + bucket: "bucket".to_string(), + public_base_url: "https://media.example.com".to_string(), + }; + + assert_eq!( + media.endpoint_url(), + "https://acct123.r2.cloudflarestorage.com" + ); + } +} diff --git a/src/db.rs b/src/db.rs index 42d980b..c86dd8c 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,15 +1,17 @@ +use std::collections::HashSet; + use anyhow::{Result, anyhow}; use chrono::{Duration, Utc}; use sea_orm::{ ActiveModelTrait, ActiveValue::Set, ColumnTrait, Condition, DatabaseConnection, DatabaseTransaction, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, - TransactionTrait, sea_query::OnConflict, + Statement, TransactionTrait, sea_query::OnConflict, }; use uuid::Uuid; use crate::{ entity::{ - attachments, channels, direct_messages, guild_members, guilds, invites, messages, + attachments, channels, direct_messages, guild_members, guilds, invites, messages, sessions, soundboard_sounds, users, }, models::{ @@ -26,6 +28,88 @@ pub async fn user_exists(db: &DatabaseConnection, user_id: Uuid) -> Result Ok(count > 0) } +pub async fn create_session( + db: &DatabaseConnection, + session_id: &str, + user_id: Uuid, + expires_at: chrono::DateTime, + user_agent_hash: Option, + ip_hash: Option, +) -> Result<()> { + sessions::Entity::insert(sessions::ActiveModel { + id: Set(session_id.to_string()), + user_id: Set(user_id), + expires_at: Set(expires_at), + created_at: Set(Utc::now().fixed_offset()), + last_seen_at: Set(Utc::now().fixed_offset()), + revoked_at: Set(None), + user_agent_hash: Set(user_agent_hash), + ip_hash: Set(ip_hash), + }) + .exec(db) + .await?; + + Ok(()) +} + +pub async fn touch_active_session( + db: &DatabaseConnection, + session_id: &str, +) -> Result> { + let session = sessions::Entity::find_by_id(session_id.to_string()) + .one(db) + .await?; + + let Some(session) = session else { + return Ok(None); + }; + + if session.revoked_at.is_some() || session.expires_at <= Utc::now().fixed_offset() { + return Ok(None); + } + + let user_id = session.user_id; + sessions::Entity::update_many() + .col_expr( + sessions::Column::LastSeenAt, + sea_orm::sea_query::Expr::value(Utc::now().fixed_offset()), + ) + .filter(sessions::Column::Id.eq(session_id.to_string())) + .exec(db) + .await?; + Ok(Some(user_id)) +} + +pub async fn revoke_session(db: &DatabaseConnection, session_id: &str) -> Result<()> { + if let Some(session) = sessions::Entity::find_by_id(session_id.to_string()) + .one(db) + .await? + { + sessions::Entity::update_many() + .col_expr( + sessions::Column::RevokedAt, + sea_orm::sea_query::Expr::value(Some(Utc::now().fixed_offset())), + ) + .filter(sessions::Column::Id.eq(session.id)) + .exec(db) + .await?; + } + + Ok(()) +} + +pub async fn cleanup_sessions(db: &DatabaseConnection) -> Result<()> { + sessions::Entity::delete_many() + .filter( + Condition::any() + .add(sessions::Column::ExpiresAt.lte(Utc::now().fixed_offset())) + .add(sessions::Column::RevokedAt.is_not_null()), + ) + .exec(db) + .await?; + Ok(()) +} + pub async fn upsert_user_from_oidc( db: &DatabaseConnection, oidc_sub: &str, @@ -106,18 +190,58 @@ pub async fn list_guilds_for_user(db: &DatabaseConnection, user_id: Uuid) -> Res .collect()) } +pub async fn list_visible_user_ids(db: &DatabaseConnection, user_id: Uuid) -> Result> { + let guild_ids: Vec = guild_members::Entity::find() + .filter(guild_members::Column::UserId.eq(user_id)) + .select_only() + .column(guild_members::Column::GuildId) + .into_tuple() + .all(db) + .await?; + + let mut visible = HashSet::from([user_id]); + + if !guild_ids.is_empty() { + let guild_users = guild_members::Entity::find() + .filter(guild_members::Column::GuildId.is_in(guild_ids)) + .all(db) + .await?; + visible.extend(guild_users.into_iter().map(|membership| membership.user_id)); + } + + let dm_rows = direct_messages::Entity::find() + .filter( + Condition::any() + .add(direct_messages::Column::SenderUserId.eq(user_id)) + .add(direct_messages::Column::RecipientUserId.eq(user_id)), + ) + .all(db) + .await?; + + for row in dm_rows { + if row.sender_user_id == user_id { + visible.insert(row.recipient_user_id); + } else { + visible.insert(row.sender_user_id); + } + } + + Ok(visible.into_iter().collect()) +} + pub async fn create_guild( db: &DatabaseConnection, owner_user_id: Uuid, name: &str, ) -> Result { + let txn = db.begin().await?; let guild = guilds::Entity::insert(guilds::ActiveModel { id: Set(Uuid::new_v4()), name: Set(name.to_string()), owner_user_id: Set(owner_user_id), ..Default::default() }) - .exec_with_returning(db) + .exec_with_returning(&txn) .await?; guild_members::Entity::insert(guild_members::ActiveModel { @@ -133,17 +257,13 @@ pub async fn create_guild( .do_nothing() .to_owned(), ) - .exec(db) + .exec(&txn) .await?; + txn.commit().await?; Ok(map_guild(guild)) } -pub async fn get_guild_by_id(db: &DatabaseConnection, guild_id: Uuid) -> Result> { - let row = guilds::Entity::find_by_id(guild_id).one(db).await?; - Ok(row.map(map_guild)) -} - pub async fn is_guild_owner( db: &DatabaseConnection, guild_id: Uuid, @@ -197,7 +317,12 @@ pub async fn create_invite( pub async fn join_invite(db: &DatabaseConnection, code: &str, user_id: Uuid) -> Result { let txn = db.begin().await?; - let invite = invites::Entity::find_by_id(code.to_string()) + let invite = invites::Entity::find() + .from_raw_sql(Statement::from_sql_and_values( + sea_orm::DatabaseBackend::Postgres, + r#"SELECT * FROM invites WHERE code = $1 FOR UPDATE"#, + [code.into()], + )) .one(&txn) .await? .ok_or_else(|| anyhow!("invite not found"))?; @@ -797,7 +922,10 @@ pub async fn create_sound( created_by_user_id: Uuid, name: &str, icon: &str, - file_path: &str, + object_key: &str, + media_url: &str, + mime_type: &str, + size_bytes: i64, ) -> Result { let model = soundboard_sounds::Entity::insert(soundboard_sounds::ActiveModel { id: Set(Uuid::new_v4()), @@ -805,7 +933,13 @@ pub async fn create_sound( created_by_user_id: Set(created_by_user_id), name: Set(name.to_string()), icon: Set(icon.to_string()), - file_path: Set(file_path.to_string()), + object_key: Set(Some(object_key.to_string())), + media_url: Set(media_url.to_string()), + mime_type: Set(Some(mime_type.to_string())), + size_bytes: Set(Some(size_bytes)), + // Keep the legacy column populated until every deployment has applied + // the nullable migration and old fallback paths are fully removed. + file_path: Set(Some(media_url.to_string())), ..Default::default() }) .exec_with_returning(db) @@ -819,6 +953,11 @@ pub async fn get_sound_by_id(db: &DatabaseConnection, id: Uuid) -> Result
+ + diff --git a/static/shared/app-core.js b/static/shared/app-core.js index a7ad4e1..0aa2e4f 100644 --- a/static/shared/app-core.js +++ b/static/shared/app-core.js @@ -183,6 +183,8 @@ const el = { messageList: document.getElementById("message-list"), messageForm: document.getElementById("message-form"), messageBody: document.getElementById("message-body"), + mediaUploadBtn: document.getElementById("media-upload-btn"), + mediaFileInput: document.getElementById("media-file-input"), // User Panel userName: document.getElementById("user-name"), @@ -343,6 +345,43 @@ function formatMessageBody(body) { return escaped; } +function formatBytes(size) { + if (!Number.isFinite(size) || size < 1024) return `${size || 0} B`; + const units = ["KB", "MB", "GB"]; + let value = size; + let unitIndex = -1; + while (value >= 1024 && unitIndex < units.length - 1) { + value /= 1024; + unitIndex += 1; + } + return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`; +} + +function renderAttachments(attachments = []) { + if (!attachments.length) return ""; + + return attachments.map((attachment) => { + const url = escapeHtml(attachment.media_url); + const mime = attachment.mime_type || "application/octet-stream"; + const fileName = escapeHtml(attachment.original_filename || "file"); + const size = formatBytes(attachment.size_bytes); + + if (mime.startsWith("image/")) { + return `${fileName}`; + } + + if (mime.startsWith("video/")) { + return `
`; + } + + if (mime.startsWith("audio/")) { + return ``; + } + + return `${fileName}${size}`; + }).join(""); +} + function formatDate(isoString) { const d = new Date(isoString); const now = new Date(); @@ -609,9 +648,13 @@ function renderMessages(messages) { row.className = `msg ${isGrouped ? "msg-grouped" : ""}`; const displayName = m.author_display_name || "Unknown User"; + const formattedBody = formatMessageBody(m.body || ""); + const attachmentsHtml = renderAttachments(m.attachments); + const bodyHtml = formattedBody ? `
${formattedBody}
` : ""; + const contentHtml = `${bodyHtml}${attachmentsHtml}`; if (isGrouped) { - row.innerHTML = `
${formatMessageBody(m.body)}
`; + row.innerHTML = `
${contentHtml}
`; } else { row.innerHTML = `
${shortName(displayName)}
@@ -620,7 +663,7 @@ function renderMessages(messages) { ${escapeHtml(displayName)} ${formatDate(m.created_at)}