feat: media uploads
All checks were successful
/ upload (release) Successful in 5m37s

This commit is contained in:
pavel 2026-02-27 15:38:46 +01:00
commit e1dd679c47
19 changed files with 1807 additions and 73 deletions

View file

@ -183,6 +183,8 @@ const el = {
messageList: document.getElementById("message-list"),
messageForm: document.getElementById("message-form"),
messageBody: document.getElementById("message-body"),
mediaUploadBtn: document.getElementById("media-upload-btn"),
mediaFileInput: document.getElementById("media-file-input"),
// User Panel
userName: document.getElementById("user-name"),
@ -343,6 +345,43 @@ function formatMessageBody(body) {
return escaped;
}
function formatBytes(size) {
if (!Number.isFinite(size) || size < 1024) return `${size || 0} B`;
const units = ["KB", "MB", "GB"];
let value = size;
let unitIndex = -1;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex += 1;
}
return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
}
function renderAttachments(attachments = []) {
if (!attachments.length) return "";
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 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>`;
}
if (mime.startsWith("video/")) {
return `<div class="msg-attachment msg-attachment-video"><video controls preload="metadata" src="${url}"></video></div>`;
}
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>`;
}
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("");
}
function formatDate(isoString) {
const d = new Date(isoString);
const now = new Date();
@ -609,9 +648,13 @@ 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}`;
if (isGrouped) {
row.innerHTML = `<div class="msg-content"><div class="msg-body">${formatMessageBody(m.body)}</div></div>`;
row.innerHTML = `<div class="msg-content">${contentHtml}</div>`;
} else {
row.innerHTML = `
<div class="msg-avatar">${shortName(displayName)}</div>
@ -620,7 +663,7 @@ function renderMessages(messages) {
<span class="msg-author">${escapeHtml(displayName)}</span>
<span class="msg-time">${formatDate(m.created_at)}</span>
</div>
<div class="msg-body">${formatMessageBody(m.body)}</div>
${contentHtml}
</div>
`;
}
@ -644,6 +687,15 @@ function renderMessages(messages) {
gifImage.addEventListener("load", scrollMessagesToBottom, { once: true });
gifImage.addEventListener("error", scrollMessagesToBottom, { once: true });
}
for (const media of el.messageList.querySelectorAll(".msg-attachment img, .msg-attachment video, .msg-attachment audio")) {
if ("complete" in media && media.complete) continue;
media.addEventListener("loadeddata", scrollMessagesToBottom, { once: true });
media.addEventListener("load", scrollMessagesToBottom, { once: true });
media.addEventListener("error", scrollMessagesToBottom, { once: true });
}
lucide.createIcons();
}
function renderMembers() {
@ -1862,6 +1914,26 @@ async function init() {
} catch (err) { alert(err.message); }
};
el.mediaUploadBtn.onclick = () => {
if (!state.selectedTextChannelId && !state.selectedDmUserId) {
alert("Select a chat first.");
return;
}
el.mediaFileInput.click();
};
el.mediaFileInput.onchange = async () => {
const file = el.mediaFileInput.files?.[0];
if (!file) return;
try {
await uploadMediaFile(file);
} catch (err) {
alert(err.message);
} finally {
el.mediaFileInput.value = "";
}
};
// 4. Voice/Soundboard Controls
el.voiceVideoBtn.onclick = toggleVideo;
el.voiceScreenBtn.onclick = toggleScreenShare;
@ -1962,6 +2034,54 @@ async function init() {
}
}
async function uploadMediaFile(file) {
const token = storageGet("chattz_token");
if (!token) throw new Error("You need to log in again.");
const path = state.selectedTextChannelId
? `/channels/${state.selectedTextChannelId}/attachments`
: state.selectedDmUserId
? `/dms/${state.selectedDmUserId}/attachments`
: null;
if (!path) throw new Error("Select a chat first.");
const formData = new FormData();
formData.append("file", file);
el.mediaUploadBtn.disabled = true;
el.mediaUploadBtn.title = "Uploading...";
const fullUrl = API_BASE_URL ? `${API_BASE_URL}${path}` : path;
try {
const response = await fetch(fullUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
});
if (!response.ok) {
let detail = "upload failed";
try {
const payload = await response.json();
detail = payload.error || detail;
} catch { }
throw new Error(`${response.status}: ${detail}`);
}
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();
} finally {
el.mediaUploadBtn.disabled = false;
el.mediaUploadBtn.title = "Upload File";
}
}
// 6. Initial Data Loading & App Start
try {
state.me = await api("/me");