${Math.round(vol * 100)}%
@@ -454,10 +459,12 @@ function renderChannels() {
pRow.addEventListener('contextmenu', (e) => {
if (p.user_id === state.me.id) return;
e.preventDefault();
- const control = pRow.querySelector('.user-volume-control');
- if (control) {
- control.classList.toggle('show-volume');
+ if (state.voice.visibleVolumeSliders.has(p.user_id)) {
+ state.voice.visibleVolumeSliders.delete(p.user_id);
+ } else {
+ state.voice.visibleVolumeSliders.add(p.user_id);
}
+ renderChannels();
});
pList.appendChild(pRow);
@@ -1936,6 +1943,324 @@ async function init() {
await loadGuilds();
await loadDMConversations();
+ } catch (err) {
+ console.error("presence sync failed", err);
+ }
+}
+
+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();
+ };
+}
+
+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");
@@ -1960,6 +2285,7 @@ async function init() {
initChatWs();
startVoicePresencePolling();
+ initUpdater();
lucide.createIcons();
} catch (err) {
console.error("init failed", err);
diff --git a/desktop/index.html b/desktop/index.html
index 28521c9..ac9fa8d 100644
--- a/desktop/index.html
+++ b/desktop/index.html
@@ -114,6 +114,11 @@
Online