This commit is contained in:
parent
24975c2e0d
commit
3acd082fb0
28 changed files with 3454 additions and 836 deletions
|
|
@ -1,4 +1,5 @@
|
|||
const state = {
|
||||
sessionActive: false,
|
||||
me: null,
|
||||
guilds: [],
|
||||
channels: [],
|
||||
|
|
@ -38,15 +39,8 @@ const state = {
|
|||
userVolumes: new Map(), // userId -> volume (0.0 to 2.0)
|
||||
};
|
||||
|
||||
// --- Desktop Backend Configuration ---
|
||||
let API_BASE_URL = ''; // Will be initialized via IPC
|
||||
// --- Storage ---
|
||||
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);
|
||||
|
|
@ -54,103 +48,15 @@ function storageGet(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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = storageGet("chattz_token");
|
||||
if (token) {
|
||||
urlStr += (urlStr.includes('?') ? '&' : '?') + `token=${token}`;
|
||||
}
|
||||
return urlStr;
|
||||
const proto = location.protocol === "https:" ? "wss" : "ws";
|
||||
return `${proto}://${location.host}${path}`;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -248,57 +154,83 @@ const el = {
|
|||
gifResults: document.getElementById("gif-results"),
|
||||
};
|
||||
|
||||
function stopVoicePresencePolling() {
|
||||
if (!state.voicePresencePollId) return;
|
||||
clearInterval(state.voicePresencePollId);
|
||||
state.voicePresencePollId = null;
|
||||
}
|
||||
|
||||
function closeChatWs() {
|
||||
if (!state.chatWs) return;
|
||||
const ws = state.chatWs;
|
||||
state.chatWs = null;
|
||||
ws.onclose = null;
|
||||
ws.onmessage = null;
|
||||
ws.onerror = null;
|
||||
try {
|
||||
ws.close();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
async function showLoggedOutState(message = "Session expired, please log in again") {
|
||||
state.sessionActive = false;
|
||||
stopVoicePresencePolling();
|
||||
closeChatWs();
|
||||
await leaveVoice();
|
||||
|
||||
state.me = null;
|
||||
state.guilds = [];
|
||||
state.channels = [];
|
||||
state.dmConversations = [];
|
||||
state.members = [];
|
||||
state.voicePresence.clear();
|
||||
state.selectedGuildId = null;
|
||||
state.selectedTextChannelId = null;
|
||||
state.selectedDmUserId = null;
|
||||
state.selectedDmDisplayName = null;
|
||||
state.selectedVoiceChannelId = null;
|
||||
state.lastMessageId = null;
|
||||
state.onlineUsers.clear();
|
||||
state.idleUsers.clear();
|
||||
state.userVolumes.clear();
|
||||
state.voice.viewMode = 'chat';
|
||||
|
||||
el.userName.textContent = "Username";
|
||||
el.userAvatar.textContent = "U";
|
||||
el.status.textContent = message;
|
||||
el.soundboard.classList.add("hidden");
|
||||
el.voiceConnection.classList.add("hidden");
|
||||
renderGuilds();
|
||||
renderChannels();
|
||||
renderDMs();
|
||||
renderMembers();
|
||||
renderMessages([]);
|
||||
updateHeaderLabels();
|
||||
el.main.classList.add("hidden");
|
||||
el.authScreen.classList.remove("hidden");
|
||||
}
|
||||
|
||||
// --- 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 = storageGet("chattz_token");
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
if (!headers["content-type"] && options.body && !(options.body instanceof FormData)) {
|
||||
headers["content-type"] = "application/json";
|
||||
}
|
||||
|
||||
let res = await fetch(fullUrl, {
|
||||
const res = await fetch(path, {
|
||||
...options,
|
||||
headers,
|
||||
credentials: "same-origin",
|
||||
});
|
||||
|
||||
// Handle 401 Unauthorized via Refresh Token
|
||||
if (res.status === 401 && !path.includes('/auth/refresh')) {
|
||||
const refreshToken = storageGet("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();
|
||||
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}`;
|
||||
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) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
if (res.status === 401) {
|
||||
await showLoggedOutState("Session expired, please log in again");
|
||||
const err = new Error("Session expired, please log in again");
|
||||
err.isAuthError = true;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
|
|
@ -336,16 +268,18 @@ function escapeHtml(s) {
|
|||
.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 `<div class="msg-gif"><img src="${url}" loading="lazy" /></div>`;
|
||||
});
|
||||
const GIF_URL_REGEX = /(https?:\/\/[^\s]+(?:\.gif|\/giphy\.gif)[^\s]*)/gi;
|
||||
|
||||
function normalizeMediaUrl(raw) {
|
||||
try {
|
||||
const url = new URL(raw, location.origin);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||
return null;
|
||||
}
|
||||
return url.href;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return escaped;
|
||||
}
|
||||
|
||||
function formatBytes(size) {
|
||||
|
|
@ -362,29 +296,125 @@ function formatBytes(size) {
|
|||
|
||||
const MAX_MEDIA_UPLOAD_BYTES = 50 * 1024 * 1024;
|
||||
|
||||
function renderAttachments(attachments = []) {
|
||||
if (!attachments.length) return "";
|
||||
function buildMessageBody(body) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
const text = String(body || "");
|
||||
let cursor = 0;
|
||||
|
||||
for (const match of text.matchAll(new RegExp(GIF_URL_REGEX))) {
|
||||
const [rawUrl] = match;
|
||||
const start = match.index ?? 0;
|
||||
if (start > cursor) {
|
||||
fragment.appendChild(document.createTextNode(text.slice(cursor, start)));
|
||||
}
|
||||
|
||||
const safeUrl = normalizeMediaUrl(rawUrl);
|
||||
if (safeUrl) {
|
||||
const gifWrap = document.createElement("div");
|
||||
gifWrap.className = "msg-gif";
|
||||
|
||||
const image = document.createElement("img");
|
||||
image.loading = "lazy";
|
||||
image.src = safeUrl;
|
||||
gifWrap.appendChild(image);
|
||||
fragment.appendChild(gifWrap);
|
||||
} else {
|
||||
fragment.appendChild(document.createTextNode(rawUrl));
|
||||
}
|
||||
|
||||
cursor = start + rawUrl.length;
|
||||
}
|
||||
|
||||
if (cursor < text.length) {
|
||||
fragment.appendChild(document.createTextNode(text.slice(cursor)));
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
function buildAttachments(attachments = []) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
for (const attachment of attachments) {
|
||||
const safeUrl = normalizeMediaUrl(attachment.media_url);
|
||||
if (!safeUrl) continue;
|
||||
|
||||
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 fileName = attachment.original_filename || "file";
|
||||
const size = formatBytes(attachment.size_bytes);
|
||||
|
||||
if (mime.startsWith("image/")) {
|
||||
return `<a class="msg-attachment msg-attachment-image" href="${url}" target="_blank" rel="noopener noreferrer"><img src="${url}" alt="${fileName}" loading="lazy" /></a>`;
|
||||
const link = document.createElement("a");
|
||||
link.className = "msg-attachment msg-attachment-image";
|
||||
link.href = safeUrl;
|
||||
link.target = "_blank";
|
||||
link.rel = "noopener noreferrer";
|
||||
|
||||
const image = document.createElement("img");
|
||||
image.src = safeUrl;
|
||||
image.alt = fileName;
|
||||
image.loading = "lazy";
|
||||
link.appendChild(image);
|
||||
fragment.appendChild(link);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mime.startsWith("video/")) {
|
||||
return `<div class="msg-attachment msg-attachment-video"><video controls preload="metadata" src="${url}"></video></div>`;
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "msg-attachment msg-attachment-video";
|
||||
|
||||
const video = document.createElement("video");
|
||||
video.controls = true;
|
||||
video.preload = "metadata";
|
||||
video.src = safeUrl;
|
||||
wrapper.appendChild(video);
|
||||
fragment.appendChild(wrapper);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mime.startsWith("audio/")) {
|
||||
return `<div class="msg-attachment msg-attachment-audio"><audio controls preload="metadata" src="${url}"></audio><a href="${url}" target="_blank" rel="noopener noreferrer">${fileName}</a></div>`;
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "msg-attachment msg-attachment-audio";
|
||||
|
||||
const audio = document.createElement("audio");
|
||||
audio.controls = true;
|
||||
audio.preload = "metadata";
|
||||
audio.src = safeUrl;
|
||||
wrapper.appendChild(audio);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = safeUrl;
|
||||
link.target = "_blank";
|
||||
link.rel = "noopener noreferrer";
|
||||
link.textContent = fileName;
|
||||
wrapper.appendChild(link);
|
||||
|
||||
fragment.appendChild(wrapper);
|
||||
continue;
|
||||
}
|
||||
|
||||
return `<a class="msg-attachment msg-attachment-file" href="${url}" target="_blank" rel="noopener noreferrer"><i data-lucide="file"></i><span>${fileName}</span><small>${size}</small></a>`;
|
||||
}).join("");
|
||||
const link = document.createElement("a");
|
||||
link.className = "msg-attachment msg-attachment-file";
|
||||
link.href = safeUrl;
|
||||
link.target = "_blank";
|
||||
link.rel = "noopener noreferrer";
|
||||
|
||||
const icon = document.createElement("i");
|
||||
icon.setAttribute("data-lucide", "file");
|
||||
link.appendChild(icon);
|
||||
|
||||
const name = document.createElement("span");
|
||||
name.textContent = fileName;
|
||||
link.appendChild(name);
|
||||
|
||||
const meta = document.createElement("small");
|
||||
meta.textContent = size;
|
||||
link.appendChild(meta);
|
||||
|
||||
fragment.appendChild(link);
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
function showMediaUploadLimitError(file) {
|
||||
|
|
@ -663,25 +693,40 @@ 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 ? `<div class="msg-body">${formattedBody}</div>` : "";
|
||||
const contentHtml = `${bodyHtml}${attachmentsHtml}`;
|
||||
const content = document.createElement("div");
|
||||
content.className = "msg-content";
|
||||
|
||||
if (isGrouped) {
|
||||
row.innerHTML = `<div class="msg-content">${contentHtml}</div>`;
|
||||
} else {
|
||||
row.innerHTML = `
|
||||
<div class="msg-avatar">${shortName(displayName)}</div>
|
||||
<div class="msg-content">
|
||||
<div class="msg-header">
|
||||
<span class="msg-author">${escapeHtml(displayName)}</span>
|
||||
<span class="msg-time">${formatDate(m.created_at)}</span>
|
||||
</div>
|
||||
${contentHtml}
|
||||
</div>
|
||||
`;
|
||||
if (!isGrouped) {
|
||||
const avatar = document.createElement("div");
|
||||
avatar.className = "msg-avatar";
|
||||
avatar.textContent = shortName(displayName);
|
||||
row.appendChild(avatar);
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.className = "msg-header";
|
||||
|
||||
const author = document.createElement("span");
|
||||
author.className = "msg-author";
|
||||
author.textContent = displayName;
|
||||
header.appendChild(author);
|
||||
|
||||
const time = document.createElement("span");
|
||||
time.className = "msg-time";
|
||||
time.textContent = formatDate(m.created_at);
|
||||
header.appendChild(time);
|
||||
|
||||
content.appendChild(header);
|
||||
}
|
||||
|
||||
if (m.body) {
|
||||
const body = document.createElement("div");
|
||||
body.className = "msg-body";
|
||||
body.appendChild(buildMessageBody(m.body));
|
||||
content.appendChild(body);
|
||||
}
|
||||
|
||||
content.appendChild(buildAttachments(m.attachments));
|
||||
row.appendChild(content);
|
||||
el.messageList.appendChild(row);
|
||||
lastAuthorId = m.author_user_id;
|
||||
lastTime = mDate;
|
||||
|
|
@ -784,8 +829,7 @@ async function createInviteLink() {
|
|||
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)}`;
|
||||
const link = `${location.origin}/?invite=${encodeURIComponent(invite.code)}`;
|
||||
|
||||
// Use Electron's native clipboard API if available (defined in preload.js)
|
||||
if (window.electronAPI && window.electronAPI.copyToClipboard) {
|
||||
|
|
@ -871,13 +915,15 @@ async function refreshVoicePresence() {
|
|||
}
|
||||
|
||||
function startVoicePresencePolling() {
|
||||
if (state.voicePresencePollId) clearInterval(state.voicePresencePollId);
|
||||
stopVoicePresencePolling();
|
||||
state.voicePresencePollId = setInterval(() => {
|
||||
if (!state.sessionActive) return;
|
||||
refreshVoicePresence().catch(() => { });
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function initChatWs() {
|
||||
if (!state.sessionActive) return;
|
||||
if (state.chatWs) state.chatWs.close();
|
||||
const wsUrl = getWsUrl('/ws');
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
|
@ -920,6 +966,7 @@ function initChatWs() {
|
|||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (!state.sessionActive) return;
|
||||
console.log("Chat WS closed, reconnecting...");
|
||||
setTimeout(initChatWs, 3000);
|
||||
};
|
||||
|
|
@ -1505,7 +1552,7 @@ async function joinVoice() {
|
|||
});
|
||||
renderChannels();
|
||||
} else if (msg.type === "play_sound") {
|
||||
const audio = new Audio(msg.sound_url);
|
||||
const audio = new Audio(msg.media_url);
|
||||
audio.play().catch(console.error);
|
||||
}
|
||||
refreshVoicePresence().catch(() => { });
|
||||
|
|
@ -1680,11 +1727,22 @@ function renderSounds(sounds) {
|
|||
// 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 = `
|
||||
<div class="sound-icon">${sound.icon}</div>
|
||||
<div class="sound-name">${sound.name}</div>
|
||||
${canDelete ? `<button class="sound-delete" title="Delete sound"><i data-lucide="x"></i></button>` : ''}
|
||||
`;
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'sound-icon';
|
||||
icon.textContent = sound.icon;
|
||||
const name = document.createElement('div');
|
||||
name.className = 'sound-name';
|
||||
name.textContent = sound.name;
|
||||
item.appendChild(icon);
|
||||
item.appendChild(name);
|
||||
|
||||
if (canDelete) {
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'sound-delete';
|
||||
deleteBtn.title = 'Delete sound';
|
||||
deleteBtn.innerHTML = '<i data-lucide="x"></i>';
|
||||
item.appendChild(deleteBtn);
|
||||
}
|
||||
|
||||
item.onclick = (e) => {
|
||||
if (e.target.closest('.sound-delete')) {
|
||||
|
|
@ -1694,7 +1752,7 @@ function renderSounds(sounds) {
|
|||
}
|
||||
return;
|
||||
}
|
||||
playRemoteSound(sound.file_path);
|
||||
playRemoteSound(sound.id, sound.media_url);
|
||||
};
|
||||
|
||||
el.soundboardGrid.appendChild(item);
|
||||
|
|
@ -1705,27 +1763,19 @@ function renderSounds(sounds) {
|
|||
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 api(`/guilds/${state.selectedGuildId}/sounds/${soundId}`, { method: 'DELETE' });
|
||||
await loadSounds();
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function playRemoteSound(url) {
|
||||
const fullUrl = API_BASE_URL && !url.startsWith('http') ? `${API_BASE_URL}${url}` : url;
|
||||
function playRemoteSound(soundId, mediaUrl) {
|
||||
if (state.voice.ws && state.voice.ws.readyState === WebSocket.OPEN) {
|
||||
state.voice.ws.send(JSON.stringify({ type: 'play_sound', sound_url: fullUrl }));
|
||||
state.voice.ws.send(JSON.stringify({ type: 'play_sound', sound_id: soundId }));
|
||||
}
|
||||
// Also play locally immediately
|
||||
const audio = new Audio(fullUrl);
|
||||
const audio = new Audio(mediaUrl);
|
||||
audio.play().catch(console.error);
|
||||
}
|
||||
|
||||
|
|
@ -1790,41 +1840,23 @@ document.addEventListener('click', resetInactivityTimer);
|
|||
|
||||
async function init() {
|
||||
lucide.createIcons();
|
||||
await hydrateDesktopStorage();
|
||||
initUpdater();
|
||||
|
||||
// 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) await storageSetCritical("chattz_token", jwtToken);
|
||||
if (refreshToken) await storageSetCritical("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.status.textContent = "";
|
||||
location.href = "/auth/login";
|
||||
};
|
||||
|
||||
el.logoutBtn.onclick = async () => {
|
||||
await leaveVoice();
|
||||
await storageRemoveCritical("chattz_token");
|
||||
await storageRemoveCritical("chattz_refresh_token");
|
||||
try { await api("/auth/logout", { method: "POST" }); } catch (e) { }
|
||||
location.reload();
|
||||
try {
|
||||
await api("/auth/logout", { method: "POST" });
|
||||
} catch (e) {
|
||||
if (!e?.isAuthError) {
|
||||
console.warn("logout request failed", e);
|
||||
}
|
||||
}
|
||||
await showLoggedOutState("Signed out.");
|
||||
};
|
||||
|
||||
el.addGuildBtn.onclick = () => { el.modalContainer.classList.remove("hidden"); };
|
||||
|
|
@ -1970,8 +2002,11 @@ async function init() {
|
|||
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 });
|
||||
const response = await fetch(`/guilds/${state.selectedGuildId}/sounds`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (!response.ok) { const err = await response.json(); throw new Error(err.error || 'Upload failed'); }
|
||||
el.soundModal.classList.add("hidden");
|
||||
el.soundForm.reset();
|
||||
|
|
@ -2063,8 +2098,6 @@ 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;
|
||||
|
|
@ -2080,16 +2113,15 @@ async function init() {
|
|||
el.mediaUploadBtn.disabled = true;
|
||||
el.mediaUploadBtn.title = "Uploading...";
|
||||
|
||||
const fullUrl = API_BASE_URL ? `${API_BASE_URL}${path}` : path;
|
||||
try {
|
||||
const response = await fetch(fullUrl, {
|
||||
const response = await fetch(path, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": file.type || "application/octet-stream",
|
||||
"X-File-Name": file.name,
|
||||
},
|
||||
body: file,
|
||||
credentials: "same-origin",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
@ -2116,10 +2148,12 @@ async function init() {
|
|||
// 6. Initial Data Loading & App Start
|
||||
try {
|
||||
state.me = await api("/me");
|
||||
state.sessionActive = true;
|
||||
if (state.me && state.me.display_name) {
|
||||
el.userName.textContent = state.me.display_name;
|
||||
el.userAvatar.textContent = shortName(state.me.display_name);
|
||||
}
|
||||
el.status.textContent = "";
|
||||
el.authScreen.classList.add("hidden");
|
||||
el.main.classList.remove("hidden");
|
||||
|
||||
|
|
@ -2144,7 +2178,8 @@ async function init() {
|
|||
|
||||
try {
|
||||
const online = await api("/presence");
|
||||
state.onlineUsers = new Set(online);
|
||||
state.onlineUsers = new Set(online.filter((entry) => entry.online).map((entry) => entry.user_id));
|
||||
state.idleUsers = new Set(online.filter((entry) => entry.idle).map((entry) => entry.user_id));
|
||||
} catch (err) { console.warn("presence sync failed", err); }
|
||||
|
||||
if (state.guilds.length > 0) {
|
||||
|
|
@ -2178,9 +2213,10 @@ async function init() {
|
|||
|
||||
lucide.createIcons();
|
||||
} catch (err) {
|
||||
console.error("init failed", err);
|
||||
el.authScreen.classList.remove("hidden");
|
||||
el.main.classList.add("hidden");
|
||||
if (!err?.isAuthError) {
|
||||
console.error("init failed", err);
|
||||
await showLoggedOutState("Unable to load the app.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2280,5 +2316,4 @@ function initUpdater() {
|
|||
});
|
||||
}
|
||||
|
||||
// Config fetcher will trigger init()
|
||||
initializeConfig();
|
||||
init();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue