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