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