1 parent
12e0c99850
commit
edda5fc9e4
42 files changed
+733
-122
No files matched your search
@@ -214,6 +214,8 @@
|
||||
}
|
||||
|
||||
.message-pane {
|
||||
container-name: message-pane;
|
||||
container-type: inline-size;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -309,6 +311,10 @@
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.pane-resizer[data-temporarily-disabled="true"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pane-resizer::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
||||
+91
-1
@@ -1354,4 +1354,94 @@ body.reader-fullscreen-open {
|
||||
.reader-toolbar .toolbar-menu>summary {
|
||||
min-width: max-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Optional wide-pane single-line message rows. The preference is on by default,
|
||||
but the layout only activates once the message pane has enough horizontal room. */
|
||||
@container message-pane (min-width: 520px) {
|
||||
html[data-compact-wide-rows="true"] .message-row {
|
||||
min-height: min(var(--row-height), 36px);
|
||||
grid-template-columns: 19px minmax(110px, .34fr) minmax(0, 1fr) auto auto;
|
||||
grid-template-rows: minmax(24px, auto);
|
||||
column-gap: 8px;
|
||||
row-gap: 0;
|
||||
align-items: center;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
html[data-compact-wide-rows="true"] .message-row .message-state {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
html[data-compact-wide-rows="true"] .message-row .message-main {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
html[data-compact-wide-rows="true"] .message-row .message-from {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
html[data-compact-wide-rows="true"] .message-row .message-subject-line {
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
html[data-compact-wide-rows="true"] .message-row .message-subject {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
html[data-compact-wide-rows="true"] .message-row .message-snippet {
|
||||
flex: 1 1 120px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
html[data-compact-wide-rows="true"] .message-row .message-snippet:not(:empty)::before {
|
||||
content: "· ";
|
||||
color: var(--subtle);
|
||||
}
|
||||
|
||||
html[data-compact-wide-rows="true"] .message-row .message-date {
|
||||
grid-column: 4;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
html[data-compact-wide-rows="true"] .message-row .message-icons {
|
||||
grid-column: 5;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
html[data-compact-wide-rows="true"] .message-list.selection-mode .message-row {
|
||||
grid-template-columns: 18px 19px minmax(110px, .34fr) minmax(0, 1fr) auto auto;
|
||||
}
|
||||
|
||||
html[data-compact-wide-rows="true"] .message-list.selection-mode .message-row .message-select {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
html[data-compact-wide-rows="true"] .message-list.selection-mode .message-row .message-state {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
html[data-compact-wide-rows="true"] .message-list.selection-mode .message-row .message-from {
|
||||
grid-column: 3;
|
||||
}
|
||||
|
||||
html[data-compact-wide-rows="true"] .message-list.selection-mode .message-row .message-subject-line {
|
||||
grid-column: 4;
|
||||
}
|
||||
|
||||
html[data-compact-wide-rows="true"] .message-list.selection-mode .message-row .message-date {
|
||||
grid-column: 5;
|
||||
}
|
||||
|
||||
html[data-compact-wide-rows="true"] .message-list.selection-mode .message-row .message-icons {
|
||||
grid-column: 6;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -906,6 +906,12 @@
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.compose-signature {
|
||||
margin-top: 10px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--line-strong);
|
||||
}
|
||||
|
||||
.compose-signature img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
|
||||
@@ -639,7 +639,11 @@
|
||||
"Hide fields": "Ukryj pola",
|
||||
"Show fields": "Pokaż pola",
|
||||
"Hide addressing fields": "Ukryj pola adresowe",
|
||||
"Show addressing fields": "Pokaż pola adresowe"
|
||||
"Show addressing fields": "Pokaż pola adresowe",
|
||||
"Use single-line rows when the message list is wide": "Używaj jednowierszowej listy, gdy panel wiadomości jest szeroki",
|
||||
"Signature insertion": "Wstawianie podpisu",
|
||||
"Quietly when sending": "Cicho przy wysyłce",
|
||||
"Show in composer": "Pokaż w edytorze"
|
||||
},
|
||||
"fixed_interface_style_note": "gotowy styl interfejsu. Włącz edycję presetu poniżej, aby przy pierwszej zmianie utworzyć jego edytowalną kopię.",
|
||||
"custom_interface_style_note": "RustBox można w pełni dostosować: kolory, odstępy, panele, typografię i zaawansowane ustawienia interfejsu.",
|
||||
|
||||
+8
-2
@@ -708,10 +708,10 @@
|
||||
value="58"></label>
|
||||
<label><span><span data-i18n="folder_width">Folder pane width</span> <output
|
||||
id="pref-folder-width-value">228 px</output></span><input id="pref-folder-width" type="range"
|
||||
min="180" max="360" step="4" value="228"></label>
|
||||
min="180" max="420" step="4" value="228"></label>
|
||||
<label><span><span data-i18n="message_width">Message pane width</span> <output
|
||||
id="pref-message-width-value">420 px</output></span><input id="pref-message-width" type="range"
|
||||
min="300" max="720" step="10" value="420"></label>
|
||||
min="300" max="760" step="10" value="420"></label>
|
||||
<label><span><span data-i18n="corner_radius">Corner radius</span> <output id="pref-radius-value">4
|
||||
px</output></span><input id="pref-radius" type="range" min="0" max="12" step="1" value="4"></label>
|
||||
<label><span data-i18n="contrast">Contrast</span><select id="pref-contrast">
|
||||
@@ -782,6 +782,8 @@
|
||||
<div class="appearance-toggles">
|
||||
<label class="checkbox-row"><input id="pref-compact-headers" type="checkbox"><span>Compact message
|
||||
headers</span></label>
|
||||
<label class="checkbox-row"><input id="pref-compact-wide-rows" type="checkbox"><span>Use single-line
|
||||
rows when the message list is wide</span></label>
|
||||
<label class="checkbox-row"><input id="pref-show-borders" type="checkbox"><span>Show panel
|
||||
borders</span></label>
|
||||
<label class="checkbox-row"><input id="pref-monochrome" type="checkbox"><span>Monochrome
|
||||
@@ -1273,6 +1275,10 @@
|
||||
<option value="above">Above quoted text</option>
|
||||
<option value="below">Below quoted text</option>
|
||||
</select></label>
|
||||
<label><span>Signature insertion</span><select id="identity-signature-mode">
|
||||
<option value="silent">Quietly when sending</option>
|
||||
<option value="visible">Show in composer</option>
|
||||
</select></label>
|
||||
<label id="identity-pgp-key-row"><span>OpenPGP key</span><select id="identity-pgp-key">
|
||||
<option value="">Automatic / none</option>
|
||||
</select></label>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
savedSearches: [],
|
||||
responseTemplates: [],
|
||||
notificationPrefs: { enabled: true, sound: false, folders: ["INBOX"], dnd_enabled: false, dnd_start: "22:00", dnd_end: "07:00" },
|
||||
mailPrefs: { messages_per_page: 300, mark_read_delay_ms: 0, remote_images: "ask", preferred_body: "auto", thread_mode: "threaded", collapse_quotes: true, show_snippets: true, default_sort: "date_desc", flagged_on_top: false, sync_imap_keywords: false },
|
||||
mailPrefs: { messages_per_page: 300, mark_read_delay_ms: 0, remote_images: "ask", preferred_body: "auto", thread_mode: "threaded", collapse_quotes: true, show_snippets: true, default_sort: "date_desc", flagged_on_top: false, sync_imap_keywords: false, compact_wide_rows: true },
|
||||
currentFolderFlaggedOnTop: false,
|
||||
markReadTimer: null,
|
||||
ws: null,
|
||||
@@ -82,9 +82,10 @@
|
||||
composeUploading: false,
|
||||
composeUploadController: null,
|
||||
composeReplyMeta: null,
|
||||
composeAction: "new",
|
||||
composeSignatureGeneration: 0,
|
||||
composeInsertRange: null,
|
||||
composeSelectedImage: null,
|
||||
composeSignatureGeneration: 0,
|
||||
identitySignatureRange: null,
|
||||
inlineAttachmentIds: new Set(),
|
||||
inlineImageCleanupTimer: null,
|
||||
|
||||
@@ -20,6 +20,7 @@ let presetPromotionTimer = null;
|
||||
let presetPromotionInFlight = false;
|
||||
let presetPromotionNeedsAnotherSave = false;
|
||||
let presetPromotionActive = false;
|
||||
const temporarilyDisabledPanePreferences = new Set();
|
||||
|
||||
const interfaceStyleProfiles = {
|
||||
"roundcube-classic": {
|
||||
@@ -348,9 +349,29 @@ async function loadMailPreferences() {
|
||||
if (flaggedTop) flaggedTop.checked = Boolean(state.mailPrefs.flagged_on_top);
|
||||
const syncKeywords = $("#mail-pref-sync-keywords");
|
||||
if (syncKeywords) syncKeywords.checked = Boolean(state.mailPrefs.sync_imap_keywords);
|
||||
const compactWideRows = $("#pref-compact-wide-rows");
|
||||
if (compactWideRows) compactWideRows.checked = state.mailPrefs.compact_wide_rows !== false;
|
||||
root.dataset.compactWideRows = state.mailPrefs.compact_wide_rows === false ? "false" : "true";
|
||||
} catch (error) { console.warn("Could not load mailbox preferences", error); }
|
||||
}
|
||||
|
||||
async function saveCompactWideRowsAppearance(enabled) {
|
||||
const previous = state.mailPrefs.compact_wide_rows !== false;
|
||||
root.dataset.compactWideRows = enabled ? "true" : "false";
|
||||
try {
|
||||
state.mailPrefs = await request("/settings/mailbox", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ compact_wide_rows: Boolean(enabled) }),
|
||||
});
|
||||
root.dataset.compactWideRows = state.mailPrefs.compact_wide_rows === false ? "false" : "true";
|
||||
} catch (error) {
|
||||
const input = $("#pref-compact-wide-rows");
|
||||
if (input) input.checked = previous;
|
||||
root.dataset.compactWideRows = previous ? "true" : "false";
|
||||
toast(error.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMailPreferences() {
|
||||
const payload = {
|
||||
messages_per_page: Number($("#mail-pref-per-page").value),
|
||||
@@ -367,6 +388,7 @@ async function saveMailPreferences() {
|
||||
try {
|
||||
state.mailPrefs = await request("/settings/mailbox", { method: "PUT", body: JSON.stringify(payload) });
|
||||
state.perPage = Number(state.mailPrefs.messages_per_page || 300);
|
||||
root.dataset.compactWideRows = state.mailPrefs.compact_wide_rows === false ? "false" : "true";
|
||||
toast("Mailbox settings saved");
|
||||
if (state.currentDetail) renderMessageDetail(state.currentDetail);
|
||||
await loadMessages(true);
|
||||
@@ -410,6 +432,39 @@ const presetEditablePreferenceKeys = new Set([
|
||||
"folderBackground", "messageBackground", "readerBackground",
|
||||
]);
|
||||
|
||||
|
||||
function panePreferenceKeyFromValidationError(error) {
|
||||
const message = String(error?.message || "");
|
||||
if (/Invalid\s+message_pane_width\s*:/i.test(message)) return "messagePaneWidth";
|
||||
if (/Invalid\s+folder_pane_width\s*:/i.test(message)) return "folderPaneWidth";
|
||||
return null;
|
||||
}
|
||||
|
||||
function temporarilyDisablePanePreference(key, error = null) {
|
||||
if (!["folderPaneWidth", "messagePaneWidth"].includes(key)) return false;
|
||||
temporarilyDisabledPanePreferences.add(key);
|
||||
const handle = $(key === "folderPaneWidth" ? "#folder-resizer" : "#message-resizer");
|
||||
if (handle) {
|
||||
handle.dataset.temporarilyDisabled = "true";
|
||||
handle.setAttribute("aria-disabled", "true");
|
||||
}
|
||||
if (error) console.warn(`Pane resize temporarily disabled for ${key}`, error);
|
||||
return true;
|
||||
}
|
||||
|
||||
function restorePanePreference(key) {
|
||||
if (!temporarilyDisabledPanePreferences.delete(key)) return;
|
||||
const handle = $(key === "folderPaneWidth" ? "#folder-resizer" : key === "messagePaneWidth" ? "#message-resizer" : null);
|
||||
if (!handle) return;
|
||||
delete handle.dataset.temporarilyDisabled;
|
||||
handle.removeAttribute("aria-disabled");
|
||||
}
|
||||
|
||||
function suppressPaneWidthValidationError(error) {
|
||||
const key = panePreferenceKeyFromValidationError(error);
|
||||
return key ? temporarilyDisablePanePreference(key, error) : false;
|
||||
}
|
||||
|
||||
function presetCssHex(styles, name, fallback) {
|
||||
const value = styles.getPropertyValue(name).trim();
|
||||
if (/^#[0-9a-f]{6}$/i.test(value)) return value;
|
||||
@@ -441,8 +496,10 @@ function schedulePromotedAppearanceSave() {
|
||||
presetPromotionNeedsAnotherSave = false;
|
||||
try {
|
||||
await request("/settings/display", { method: "PUT", body: JSON.stringify(customAppearancePayload(state.prefs)) });
|
||||
restorePanePreference("folderPaneWidth");
|
||||
restorePanePreference("messagePaneWidth");
|
||||
} catch (error) {
|
||||
toast(error.message, "error");
|
||||
if (!suppressPaneWidthValidationError(error)) toast(error.message, "error");
|
||||
} finally {
|
||||
presetPromotionInFlight = false;
|
||||
if (presetPromotionNeedsAnotherSave) schedulePromotedAppearanceSave();
|
||||
@@ -498,16 +555,25 @@ function savePreference(key, value) {
|
||||
if (!field) return;
|
||||
const payload = { [field]: state.prefs[key] };
|
||||
if (["accentColor", "backgroundColor", "panelColor", "textColor"].includes(key)) payload.color_preset = "custom";
|
||||
try { await request("/settings/display", { method: "PUT", body: JSON.stringify(payload) }); }
|
||||
catch (error) { toast(error.message, "error"); }
|
||||
try {
|
||||
await request("/settings/display", { method: "PUT", body: JSON.stringify(payload) });
|
||||
restorePanePreference(key);
|
||||
} catch (error) { if (!suppressPaneWidthValidationError(error)) toast(error.message, "error"); }
|
||||
}, 180);
|
||||
preferenceTimers.set(key, timer);
|
||||
}
|
||||
|
||||
async function resetAppearance() {
|
||||
const payload = { theme: root.dataset.defaultTheme || "system", interface_skin: "default", preset_scale_unlocked: false, density: "compact", preview_pane: "right", ui_scale: "normal", color_preset: "sage", accent_color: "#6f9c86", background_color: "#121513", panel_color: "#181c19", text_color: "#edf0ec", font_family: "system", font_size: 13, row_height: 58, folder_pane_width: 228, message_pane_width: 420, border_radius: 4, contrast: "normal", show_avatars: true, show_attachment_icons: true, show_flags: true, reduce_motion: false, date_format: "locale", time_format: "locale", icon_size: 18, toolbar_density: "normal", sidebar_item_height: 29, reader_max_width: 0, composer_width: 900, selection_color: "#28352e", unread_font_weight: 730, unread_badge_style: "dot", compact_headers: false, show_borders: true, scrollbar_density: "thin", monochrome: false, folder_background: "", message_background: "", reader_background: "" };
|
||||
try { normalizeServerPreferences(await request("/settings/display", { method: "PUT", body: JSON.stringify(payload) })); applyPreferences(); toast("Appearance reset"); }
|
||||
catch (error) { toast(error.message, "error"); }
|
||||
try {
|
||||
normalizeServerPreferences(await request("/settings/display", { method: "PUT", body: JSON.stringify(payload) }));
|
||||
state.mailPrefs = await request("/settings/mailbox", { method: "PUT", body: JSON.stringify({ compact_wide_rows: true }) });
|
||||
const compactWideRows = $("#pref-compact-wide-rows");
|
||||
if (compactWideRows) compactWideRows.checked = true;
|
||||
root.dataset.compactWideRows = "true";
|
||||
applyPreferences();
|
||||
toast("Appearance reset");
|
||||
} catch (error) { toast(error.message, "error"); }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -125,6 +125,19 @@ function estimateComposeMessageBytes(extraFiles = []) {
|
||||
const mode = $("#compose-mode")?.value || "html";
|
||||
if (mode === "html") bytes += utf8Bytes(editor.innerHTML);
|
||||
else if (mode === "markdown") bytes += Math.ceil(plainBytes * 1.25);
|
||||
|
||||
const identity = typeof selectedIdentity === "function" ? selectedIdentity() : null;
|
||||
const action = state.composeAction || "new";
|
||||
const signatureEnabled = identity && (action === "reply"
|
||||
? identity.signature_reply !== false
|
||||
: action === "forward"
|
||||
? identity.signature_forward !== false
|
||||
: identity.signature_new !== false);
|
||||
if (signatureEnabled && identity.signature_insert_mode !== "visible") {
|
||||
const signatureText = identity.signature_text || "";
|
||||
bytes += utf8Bytes(signatureText) + 8;
|
||||
if (mode === "html") bytes += utf8Bytes(identity.signature_html || "") + 96;
|
||||
}
|
||||
}
|
||||
for (const attachment of state.composeAttachments) bytes += estimatedMimeAttachmentBytes(attachment.size);
|
||||
for (const file of extraFiles || []) bytes += estimatedMimeAttachmentBytes(file.size);
|
||||
@@ -551,6 +564,7 @@ function composeDraftPayload() {
|
||||
references: state.composeReplyMeta?.references || null,
|
||||
request_mdn: Boolean($("#compose-mdn")?.checked),
|
||||
priority: $("#compose-priority")?.value || "normal",
|
||||
compose_action: state.composeAction || "new",
|
||||
compose_format: $("#compose-mode")?.value || "html",
|
||||
pgp_mode: $("#compose-pgp-mode")?.value || "none",
|
||||
pgp_inline: Boolean($("#compose-pgp-inline")?.checked),
|
||||
@@ -1414,8 +1428,7 @@ async function uploadInlineImage(files, insertionRange = null) {
|
||||
|
||||
async function openImapDraftComposer(folder, uid) {
|
||||
const draft = await request(`/messages/${encodeURIComponent(folder)}/${uid}/draft-compose`, { method: "GET" });
|
||||
openCompose("new");
|
||||
state.composeSignatureGeneration += 1;
|
||||
openCompose("new", { skipSignature: true });
|
||||
state.composeDraftId = ensureDraftId();
|
||||
state.composeRemoteDraft = draft.imap_source || { folder, uid, uid_validity: 0 };
|
||||
state.composeAttachments = mergeComposeAttachments(draft.attachments);
|
||||
@@ -1425,6 +1438,7 @@ async function openImapDraftComposer(folder, uid) {
|
||||
inReplyTo: draft.in_reply_to || null,
|
||||
references: draft.references || null,
|
||||
};
|
||||
state.composeAction = draft.in_reply_to ? "reply" : (/^fwd:/i.test(draft.subject || "") ? "forward" : "new");
|
||||
$("#compose-to").value = draft.to || "";
|
||||
$("#compose-cc").value = draft.cc || "";
|
||||
$("#compose-bcc").value = draft.bcc || "";
|
||||
@@ -1446,6 +1460,7 @@ async function openImapDraftComposer(folder, uid) {
|
||||
const editor = $("#compose-editor");
|
||||
if (mode === "html") editor.innerHTML = hydrateComposeHtml(draft.html_body || "", state.composeAttachments, state.composeDraftId);
|
||||
else editor.textContent = draft.text_body || "";
|
||||
insertIdentitySignature(editor, state.composeAction, { preserveExisting: true });
|
||||
renderComposeAttachments();
|
||||
state.composeDirty = false;
|
||||
state.composeNeedsRemoteSync = false;
|
||||
@@ -1458,7 +1473,6 @@ async function openImapDraftComposer(folder, uid) {
|
||||
}
|
||||
|
||||
function applyRecoveredDraft(draft, sourceLabel = "Saved draft") {
|
||||
state.composeSignatureGeneration += 1;
|
||||
state.composeDraftId = draft.id || null;
|
||||
state.composeRemoteDraft = draft.imap_source || null;
|
||||
state.composeAttachments = mergeComposeAttachments(draft.attachments);
|
||||
@@ -1468,6 +1482,7 @@ function applyRecoveredDraft(draft, sourceLabel = "Saved draft") {
|
||||
inReplyTo: draft.in_reply_to || null,
|
||||
references: draft.references || null,
|
||||
};
|
||||
state.composeAction = draft.compose_action || (draft.in_reply_to ? "reply" : (/^fwd:/i.test(draft.subject || "") ? "forward" : "new"));
|
||||
$("#compose-to").value = draft.to || "";
|
||||
$("#compose-cc").value = draft.cc || "";
|
||||
$("#compose-bcc").value = draft.bcc || "";
|
||||
@@ -1485,6 +1500,7 @@ function applyRecoveredDraft(draft, sourceLabel = "Saved draft") {
|
||||
const editor = $("#compose-editor");
|
||||
if (mode === "html" && draft.html_body) editor.innerHTML = hydrateComposeHtml(draft.html_body, state.composeAttachments, state.composeDraftId);
|
||||
else editor.textContent = draft.text_body || "";
|
||||
insertIdentitySignature(editor, state.composeAction, { preserveExisting: true });
|
||||
renderComposeAttachments();
|
||||
state.composeDirty = sourceLabel === "Browser recovery";
|
||||
state.composeNeedsRemoteSync = sourceLabel === "Browser recovery";
|
||||
@@ -1560,7 +1576,6 @@ async function closeComposer() {
|
||||
if (!saved) toast("Draft kept in this browser for recovery", "warning", 5200);
|
||||
}
|
||||
if ($("#compose-dialog").open) $("#compose-dialog").close();
|
||||
state.composeSignatureGeneration += 1;
|
||||
}
|
||||
|
||||
function attachmentReminderNeeded() {
|
||||
@@ -1649,7 +1664,7 @@ function toggleComposeFields() {
|
||||
if (collapse) $("#compose-editor")?.focus();
|
||||
}
|
||||
|
||||
function openCompose(mode = "new") {
|
||||
function openCompose(mode = "new", options = {}) {
|
||||
if (mode === "forward" && state.prefs.forwardMode === "attachment") {
|
||||
void forwardAsAttachment();
|
||||
return;
|
||||
@@ -1662,13 +1677,13 @@ function openCompose(mode = "new") {
|
||||
state.composeDraftId = null;
|
||||
state.composeRemoteDraft = null;
|
||||
state.composeAttachments = [];
|
||||
state.composeSignatureGeneration += 1;
|
||||
state.inlineAttachmentIds = new Set();
|
||||
setSelectedInlineImage(null);
|
||||
setComposeFieldsCollapsed(false);
|
||||
state.composeDirty = false;
|
||||
state.composeNeedsRemoteSync = false;
|
||||
state.composeReplyMeta = null;
|
||||
state.composeAction = mode === "reply" || mode === "reply-all" ? "reply" : mode === "forward" ? "forward" : "new";
|
||||
setComposeUploadProgress(false);
|
||||
renderComposeAttachments();
|
||||
updateComposeDraftDeleteAction();
|
||||
@@ -1728,7 +1743,7 @@ function openCompose(mode = "new") {
|
||||
}
|
||||
editor.append(quote);
|
||||
}
|
||||
insertIdentitySignature(editor, mode);
|
||||
if (!options.skipSignature) insertIdentitySignature(editor, state.composeAction);
|
||||
let fullScreen = $("#compose-dialog").classList.contains("compose-fullscreen");
|
||||
try { fullScreen = localStorage.getItem("rustbox.composeFullscreen") === "1"; } catch (_) { }
|
||||
setComposeFullscreen(fullScreen, false);
|
||||
@@ -1791,6 +1806,7 @@ async function sendMessage(event) {
|
||||
from_identity_id: $("#compose-from").value ? Number($("#compose-from").value) : null,
|
||||
request_mdn: Boolean($("#compose-mdn").checked),
|
||||
priority: $("#compose-priority").value || "normal",
|
||||
compose_action: state.composeAction || "new",
|
||||
pgp_mode: $("#compose-pgp-mode").value || "none",
|
||||
pgp_inline: Boolean($("#compose-pgp-inline").checked),
|
||||
pgp_passphrase: $("#compose-pgp-passphrase").value || "",
|
||||
|
||||
@@ -904,6 +904,7 @@ async function forwardAsAttachment() {
|
||||
try {
|
||||
const blob = await fetchEmlBlob(detail);
|
||||
openCompose("new");
|
||||
state.composeAction = "forward";
|
||||
$("#compose-title").textContent = trText("Forward as attachment");
|
||||
$("#compose-subject").value = /^fwd:/i.test(detail.subject || "") ? detail.subject : `Fwd: ${detail.subject || ""}`;
|
||||
const safe = String(detail.subject || `message-${detail.uid}`).replace(/[\\/:*?"<>|]+/g, "_").slice(0, 120) || `message-${detail.uid}`;
|
||||
|
||||
@@ -371,6 +371,7 @@ function editIdentity(identity = null) {
|
||||
$("#identity-signature-text").value = identity?.signature_text || "";
|
||||
$("#identity-signature").innerHTML = sanitizeMessageHtml(identity?.signature_html || "");
|
||||
$("#identity-signature-position").value = identity?.signature_position || "above";
|
||||
$("#identity-signature-mode").value = identity?.signature_insert_mode || "silent";
|
||||
renderIdentityPgpOptions(identity?.pgp_fingerprint || "");
|
||||
$("#identity-pgp-sign").checked = Boolean(identity?.pgp_sign_default);
|
||||
$("#identity-pgp-encrypt").checked = Boolean(identity?.pgp_encrypt_default);
|
||||
@@ -403,6 +404,7 @@ async function saveIdentity(event) {
|
||||
signature_text: $("#identity-signature-text").value,
|
||||
signature_html: signatureHtml,
|
||||
signature_position: $("#identity-signature-position").value,
|
||||
signature_insert_mode: $("#identity-signature-mode").value,
|
||||
pgp_fingerprint: $("#identity-pgp-key").value,
|
||||
pgp_sign_default: $("#identity-pgp-sign").checked,
|
||||
pgp_encrypt_default: $("#identity-pgp-encrypt").checked,
|
||||
@@ -440,11 +442,15 @@ function selectedIdentity() {
|
||||
return state.identities.find((identity) => identity.id === id) || state.identities.find((identity) => identity.is_default) || null;
|
||||
}
|
||||
|
||||
function composeActionFromTitle() {
|
||||
const title = $("#compose-title").textContent;
|
||||
if (title === "Reply" || title === "Reply all") return "reply";
|
||||
if (title === "Forward") return "forward";
|
||||
return "new";
|
||||
function signatureEnabledForCompose(identity, action = state.composeAction || "new") {
|
||||
if (!identity) return false;
|
||||
if (action === "reply" || action === "reply-all") return identity.signature_reply !== false;
|
||||
if (action === "forward") return identity.signature_forward !== false;
|
||||
return identity.signature_new !== false;
|
||||
}
|
||||
|
||||
function identityUsesVisibleSignature(identity = selectedIdentity()) {
|
||||
return Boolean(identity && identity.signature_insert_mode === "visible");
|
||||
}
|
||||
|
||||
function dataUrlToSignatureFile(src, index = 0) {
|
||||
@@ -462,10 +468,12 @@ function dataUrlToSignatureFile(src, index = 0) {
|
||||
}
|
||||
}
|
||||
|
||||
async function materializeSignatureImages(signature, generation) {
|
||||
async function materializeVisibleSignatureImages(signature, generation) {
|
||||
const images = [...signature.querySelectorAll('img[src^="data:image/"]')];
|
||||
if (!images.length) return;
|
||||
const pairs = images.map((image, index) => ({ image, file: dataUrlToSignatureFile(image.getAttribute("src"), index) })).filter((item) => item.file);
|
||||
const pairs = images
|
||||
.map((image, index) => ({ image, file: dataUrlToSignatureFile(image.getAttribute("src"), index) }))
|
||||
.filter((item) => item.file);
|
||||
if (!pairs.length) return;
|
||||
const uploaded = await uploadComposeFiles(pairs.map((item) => item.file), { announce: false });
|
||||
if (!uploaded.length) return;
|
||||
@@ -488,29 +496,76 @@ async function materializeSignatureImages(signature, generation) {
|
||||
scheduleDraftSave();
|
||||
}
|
||||
|
||||
function insertIdentitySignature(editor, action = composeActionFromTitle()) {
|
||||
function plainIdentitySignatureSuffix(identity) {
|
||||
const text = String(identity?.signature_text || "").replace(/[\r\n]+$/g, "");
|
||||
return text.trim() ? `-- \n${text}` : "";
|
||||
}
|
||||
|
||||
function stripLegacyPlainSignature(editor) {
|
||||
if (!editor || $("#compose-mode")?.value === "html") return false;
|
||||
let text = editor.innerText || "";
|
||||
const original = text;
|
||||
for (const identity of state.identities) {
|
||||
const suffix = plainIdentitySignatureSuffix(identity);
|
||||
if (!suffix) continue;
|
||||
const trimmed = text.replace(/[\r\n]+$/g, "");
|
||||
if (!trimmed.endsWith(suffix)) continue;
|
||||
text = trimmed.slice(0, -suffix.length).replace(/[\r\n]+$/g, "");
|
||||
break;
|
||||
}
|
||||
if (text === original) return false;
|
||||
editor.textContent = text;
|
||||
return true;
|
||||
}
|
||||
|
||||
function removeVisibleComposeSignature(editor = $("#compose-editor"), options = {}) {
|
||||
if (!editor) return;
|
||||
state.composeSignatureGeneration += 1;
|
||||
const generation = state.composeSignatureGeneration;
|
||||
editor.querySelectorAll(".compose-signature").forEach((node) => node.remove());
|
||||
editor.querySelectorAll('.compose-signature, [data-rustbox-signature="true"]').forEach((node) => node.remove());
|
||||
if (options.stripPlainLegacy !== false) stripLegacyPlainSignature(editor);
|
||||
scheduleUnusedInlineAttachmentCleanup();
|
||||
}
|
||||
|
||||
function composeHasVisibleSignature(editor, identity) {
|
||||
if (!editor || !identity) return false;
|
||||
if (editor.querySelector('.compose-signature, [data-rustbox-signature="true"]')) return true;
|
||||
if ($("#compose-mode")?.value === "html") return false;
|
||||
const suffix = plainIdentitySignatureSuffix(identity);
|
||||
return Boolean(suffix && (editor.innerText || "").replace(/[\r\n]+$/g, "").endsWith(suffix));
|
||||
}
|
||||
|
||||
function insertIdentitySignature(editor = $("#compose-editor"), action = state.composeAction || "new", options = {}) {
|
||||
if (!editor) return;
|
||||
const identity = selectedIdentity();
|
||||
if (!identity) return;
|
||||
const enabled = action === "reply" ? identity.signature_reply !== false : action === "forward" ? identity.signature_forward !== false : identity.signature_new !== false;
|
||||
if (!enabled) return;
|
||||
const format = $("#compose-mode").value;
|
||||
const signature = el("div", "compose-signature");
|
||||
const visible = identityUsesVisibleSignature(identity) && signatureEnabledForCompose(identity, action);
|
||||
if (options.preserveExisting && visible && composeHasVisibleSignature(editor, identity)) return;
|
||||
|
||||
removeVisibleComposeSignature(editor);
|
||||
if (!visible) return;
|
||||
|
||||
const format = $("#compose-mode")?.value || "html";
|
||||
const signature = el("div", "compose-signature rustbox-signature");
|
||||
signature.dataset.rustboxSignature = "true";
|
||||
if (format === "html") {
|
||||
if (!identity.signature_html) return;
|
||||
signature.innerHTML = sanitizeMessageHtml(identity.signature_html);
|
||||
const html = String(identity.signature_html || "").trim();
|
||||
const text = String(identity.signature_text || "").trim();
|
||||
if (html) signature.innerHTML = sanitizeMessageHtml(html);
|
||||
else if (text) {
|
||||
signature.textContent = text;
|
||||
signature.style.whiteSpace = "pre-wrap";
|
||||
} else return;
|
||||
} else {
|
||||
const text = identity.signature_text || "";
|
||||
if (!text) return;
|
||||
const text = String(identity.signature_text || "").replace(/[\r\n]+$/g, "");
|
||||
if (!text.trim()) return;
|
||||
signature.textContent = `\n-- \n${text}\n`;
|
||||
}
|
||||
|
||||
const quote = editor.querySelector(".compose-quote");
|
||||
if (quote && identity.signature_position !== "below") editor.insertBefore(signature, quote);
|
||||
else editor.append(signature);
|
||||
if (format === "html") void materializeSignatureImages(signature, generation);
|
||||
|
||||
const generation = state.composeSignatureGeneration;
|
||||
if (format === "html") void materializeVisibleSignatureImages(signature, generation);
|
||||
}
|
||||
|
||||
function readFileAsDataUrl(file) {
|
||||
|
||||
@@ -33,6 +33,7 @@ function bindPaneResizer(selector, preferenceKey, minPx, maxPx) {
|
||||
const handle = $(selector);
|
||||
if (!handle) return;
|
||||
handle.addEventListener("pointerdown", (event) => {
|
||||
if (handle.dataset.temporarilyDisabled === "true" || temporarilyDisabledPanePreferences.has(preferenceKey)) return;
|
||||
if (innerWidth <= 820 || event.button !== 0) return;
|
||||
event.preventDefault();
|
||||
const scaleFactor = Number(getComputedStyle(root).getPropertyValue("--ui-scale-factor")) || 1;
|
||||
@@ -123,7 +124,7 @@ function bindToolbarMenus() {
|
||||
|
||||
function bindEvents() {
|
||||
bindToolbarMenus();
|
||||
bindPaneResizer("#folder-resizer", "folderPaneWidth", 170, 420);
|
||||
bindPaneResizer("#folder-resizer", "folderPaneWidth", 180, 420);
|
||||
bindPaneResizer("#message-resizer", "messagePaneWidth", 300, 760);
|
||||
bindRecipientAutocomplete("#compose-to");
|
||||
bindRecipientAutocomplete("#compose-cc");
|
||||
@@ -192,11 +193,11 @@ function bindEvents() {
|
||||
return;
|
||||
}
|
||||
setComposeMode(next, true);
|
||||
insertIdentitySignature(editor);
|
||||
insertIdentitySignature(editor, state.composeAction);
|
||||
scheduleDraftSave();
|
||||
updateComposeMessageSize();
|
||||
});
|
||||
$("#compose-from").addEventListener("change", () => { insertIdentitySignature($("#compose-editor")); updateComposePgpUi(true); updateComposeMessageSize(); });
|
||||
$("#compose-from").addEventListener("change", () => { insertIdentitySignature($("#compose-editor"), state.composeAction); updateComposePgpUi(true); updateComposeMessageSize(); scheduleDraftSave(); });
|
||||
["#compose-to", "#compose-cc", "#compose-bcc"].forEach((selector) => $(selector).addEventListener("input", () => {
|
||||
state.pgpRecipientStatus = [];
|
||||
updateComposePgpUi();
|
||||
@@ -429,6 +430,7 @@ function bindEvents() {
|
||||
$("#pref-unread-badge").addEventListener("change", (event) => savePreference("unreadBadgeStyle", event.target.value));
|
||||
$("#pref-scrollbar-density").addEventListener("change", (event) => savePreference("scrollbarDensity", event.target.value));
|
||||
$("#pref-compact-headers").addEventListener("change", (event) => savePreference("compactHeaders", event.target.checked));
|
||||
$("#pref-compact-wide-rows").addEventListener("change", (event) => saveCompactWideRowsAppearance(event.target.checked));
|
||||
$("#pref-show-borders").addEventListener("change", (event) => savePreference("showBorders", event.target.checked));
|
||||
$("#pref-monochrome").addEventListener("change", (event) => savePreference("monochrome", event.target.checked));
|
||||
[["folder", "folderBackground", "#101310"], ["message", "messageBackground", "#181c19"], ["reader", "readerBackground", "#181c19"]].forEach(([name, key, fallback]) => {
|
||||
|
||||
Reference in new issue
Block a user