This commit is contained in:
parent
8847f3fcf6
commit
832de038ca
8 changed files with 526 additions and 9 deletions
322
desktop/app.js
322
desktop/app.js
|
|
@ -112,6 +112,9 @@ const el = {
|
|||
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"),
|
||||
|
|
@ -1940,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 = '<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");
|
||||
|
|
@ -1964,6 +2285,7 @@ async function init() {
|
|||
|
||||
initChatWs();
|
||||
startVoicePresencePolling();
|
||||
initUpdater();
|
||||
lucide.createIcons();
|
||||
} catch (err) {
|
||||
console.error("init failed", err);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue