v0.0.5
RustBox CI / rustbox (push) Canceled after 0s

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-30 12:06:44 +01:00
1 parent 96a32ea8fb
commit a643e7e3ab
106 files changed
+2223 -4669

No files matched your search

+36 -25
View File
@@ -1,4 +1,4 @@
/* RustBox UI module: 50-compose.js */
function parseRecipients(raw) {
return raw.split(",").map((value) => value.trim()).filter(Boolean);
}
@@ -98,7 +98,7 @@ function estimatedMimeAttachmentBytes(rawBytes) {
function estimateComposeMessageBytes(extraFiles = []) {
const editor = $("#compose-editor");
let bytes = 4096; // envelope headers, MIME boundaries and normal metadata
let bytes = 4096;
for (const selector of ["#compose-to", "#compose-cc", "#compose-bcc", "#compose-subject"]) bytes += utf8Bytes($(selector)?.value);
if (editor) {
const plainBytes = utf8Bytes(editor.innerText);
@@ -404,27 +404,37 @@ function setComposeUploadProgress(visible, percent = 0, label = trText("Uploadin
}
async function uploadComposeFiles(files, options = {}) {
const list = [...(files || [])];
const announce = options.announce !== false;
const incoming = [...(files || [])];
if (!incoming.length) return [];
if (state.composeUploading) return [];
const existing = new Set(state.composeAttachments.map((attachment) => `${String(attachment.filename || "")}\u0000${Number(attachment.size || 0)}`));
const selected = new Set();
const list = incoming.filter((file) => {
const key = `${String(file.name || "")}\u0000${Number(file.size || 0)}`;
if (existing.has(key) || selected.has(key)) return false;
selected.add(key);
return true;
});
if (!list.length) return [];
if (state.composeUploading) { if (announce) toast(trText("Another upload is already in progress"), "warning"); return []; }
const maxBytes = Math.max(1, Number(root.dataset.maxAttachmentBytes || 10 * 1024 * 1024));
const maxMiB = Math.max(1, Number(root.dataset.maxAttachmentMb || Math.round(maxBytes / 1024 / 1024)));
const oversized = list.find((file) => Number(file.size || 0) > maxBytes);
if (oversized) {
toast(`${oversized.name}: ${trText("maximum attachment size is")} ${maxMiB} MiB`, "error");
setComposeUploadProgress(true, 0, `${oversized.name}: ${trText("maximum attachment size is")} ${maxMiB} MiB`);
setTimeout(() => { if (!state.composeUploading) setComposeUploadProgress(false); }, 4200);
return [];
}
const maxMessageBytes = Math.max(1, Number(root.dataset.maxMessageBytes || 50 * 1024 * 1024));
const maxMessageMiB = Math.max(1, Number(root.dataset.maxMessageMb || Math.round(maxMessageBytes / 1024 / 1024)));
const estimatedAfterUpload = updateComposeMessageSize(list);
if (estimatedAfterUpload > maxMessageBytes) {
toast(`${trText("Estimated message size exceeds the configured limit")} (${formatComposeBytes(estimatedAfterUpload)} / ${maxMessageMiB} MiB). ${trText("The exact size will be checked before sending.")}`, "warning");
setComposeUploadProgress(true, 0, `${trText("Estimated message size exceeds the configured limit")} (${formatComposeBytes(estimatedAfterUpload)} / ${maxMessageMiB} MiB)`);
}
state.composeUploading = true;
const controller = new AbortController();
state.composeUploadController = controller;
const uploadLabel = list.length === 1 ? trText("Uploading attachment…") : trText("Uploading files…");
setComposeUploadProgress(true, 0, uploadLabel);
const progress = progressToast(uploadLabel);
await saveCurrentDraft(true, true);
const id = ensureDraftId();
const added = [];
@@ -435,27 +445,28 @@ async function uploadComposeFiles(files, options = {}) {
form.append("file", file, file.name);
const data = await uploadRequest(`/drafts/${encodeURIComponent(id)}/attachments`, form, (percent) => {
const overall = Math.round(((index + (percent / 100)) / list.length) * 100);
const label = `${trText("Uploading attachment…")} ${file.name}`;
setComposeUploadProgress(true, overall, `${label} · ${overall}%`);
progress.update(overall, label);
});
added.push(...(data.attachments || []));
setComposeUploadProgress(true, overall, `${trText("Uploading attachment…")} ${file.name} · ${overall}%`);
}, controller.signal);
const uploaded = data.attachments || [];
added.push(...uploaded);
state.composeAttachments.push(...uploaded);
renderComposeAttachments();
state.composeDirty = true;
state.composeNeedsRemoteSync = true;
storeLocalDraftSnapshot();
}
state.composeAttachments.push(...added);
renderComposeAttachments();
state.composeDirty = true;
state.composeNeedsRemoteSync = true;
storeLocalDraftSnapshot();
setComposeUploadProgress(true, 100, trText("Upload complete"));
progress.complete(added.length === 1 ? trText("Attachment uploaded") : `${added.length} ${trText("attachments uploaded")}`);
if (announce) toast(`${added.length} attachment${added.length === 1 ? "" : "s"} added`, "success");
setTimeout(() => setComposeUploadProgress(false), 450);
setTimeout(() => { if (!state.composeUploading) setComposeUploadProgress(false); }, 450);
return added;
} catch (error) {
setComposeUploadProgress(false);
progress.fail(error.message);
return [];
if (error?.name === "AbortError") setComposeUploadProgress(false);
else {
setComposeUploadProgress(true, 0, error?.message || trText("Upload failed"));
setTimeout(() => { if (!state.composeUploading) setComposeUploadProgress(false); }, 4800);
}
return added;
} finally {
if (state.composeUploadController === controller) state.composeUploadController = null;
state.composeUploading = false;
}
}
@@ -978,8 +989,8 @@ async function recoverDraft() {
async function closeComposer() {
clearTimeout(state.draftSaveTimer);
clearTimeout(state.inlineImageCleanupTimer);
if (state.composeUploadController) state.composeUploadController.abort();
await cleanupUnusedInlineAttachments();
if (state.composeUploading) return toast("Wait for the attachment upload to finish", "warning");
if (state.composeNeedsRemoteSync && draftHasContent()) {
const saved = await saveCurrentDraft(true, true, true);
if (!saved) toast("Draft kept in this browser for recovery", "warning", 5200);