1 parent
edda5fc9e4
commit
fb9eab87eb
21 files changed
+187
-70
No files matched your search
+17
-22
@@ -96,7 +96,9 @@ fn body_already_has_signature(
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
let signature = identity.signature_text.trim_matches(|ch| matches!(ch, '\r' | '\n'));
|
||||
let signature = identity
|
||||
.signature_text
|
||||
.trim_matches(|ch| matches!(ch, '\r' | '\n'));
|
||||
if signature.trim().is_empty() {
|
||||
return false;
|
||||
}
|
||||
@@ -138,10 +140,9 @@ fn signature_html_to_text(html: &str) -> String {
|
||||
}
|
||||
|
||||
fn materialize_signature_images(html: &str, attachments: &mut Vec<AttachmentData>) -> String {
|
||||
let data_image = Regex::new(
|
||||
r"(?is)data:(image/(?:png|jpeg|gif|webp));base64,([A-Za-z0-9+/=\r\n]+)",
|
||||
)
|
||||
.expect("valid signature data-image regex");
|
||||
let data_image =
|
||||
Regex::new(r"(?is)data:(image/(?:png|jpeg|gif|webp));base64,([A-Za-z0-9+/=\r\n]+)")
|
||||
.expect("valid signature data-image regex");
|
||||
let mut image_index = 0usize;
|
||||
data_image
|
||||
.replace_all(html, |captures: ®ex::Captures<'_>| {
|
||||
@@ -413,14 +414,15 @@ pub async fn send_message_handler(
|
||||
let append_signature = selected_identity.as_ref().is_some_and(|identity| {
|
||||
identity.signature_insert_mode != "visible"
|
||||
&& signature_enabled(
|
||||
identity,
|
||||
req.compose_action.as_deref(),
|
||||
req.in_reply_to.as_deref(),
|
||||
) && !body_already_has_signature(
|
||||
identity,
|
||||
req.text_body.as_deref(),
|
||||
req.html_body.as_deref(),
|
||||
)
|
||||
identity,
|
||||
req.compose_action.as_deref(),
|
||||
req.in_reply_to.as_deref(),
|
||||
)
|
||||
&& !body_already_has_signature(
|
||||
identity,
|
||||
req.text_body.as_deref(),
|
||||
req.html_body.as_deref(),
|
||||
)
|
||||
});
|
||||
drop(conn);
|
||||
|
||||
@@ -498,15 +500,8 @@ pub async fn send_message_handler(
|
||||
|
||||
let mut text_body = req.text_body.unwrap_or_default();
|
||||
let mut html_body = req.html_body;
|
||||
if append_signature
|
||||
&& let Some(identity) = selected_identity.as_ref()
|
||||
{
|
||||
append_identity_signature(
|
||||
identity,
|
||||
&mut text_body,
|
||||
&mut html_body,
|
||||
&mut attachments,
|
||||
);
|
||||
if append_signature && let Some(identity) = selected_identity.as_ref() {
|
||||
append_identity_signature(identity, &mut text_body, &mut html_body, &mut attachments);
|
||||
}
|
||||
|
||||
if pgp_inline && pgp_mode != PgpMode::None && !attachments.is_empty() {
|
||||
|
||||
@@ -906,10 +906,18 @@
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.compose-body-entry {
|
||||
min-height: 1.6em;
|
||||
}
|
||||
|
||||
.compose-signature {
|
||||
margin-top: 10px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--line-strong);
|
||||
padding-top: 9px;
|
||||
border-top: 1px solid color-mix(in srgb, var(--line) 58%, transparent);
|
||||
}
|
||||
|
||||
.compose-signature.compose-signature-new {
|
||||
margin-top: 2.4em;
|
||||
}
|
||||
|
||||
.compose-signature img {
|
||||
|
||||
@@ -555,7 +555,8 @@
|
||||
min-width: 0
|
||||
}
|
||||
|
||||
.identity-signature-grid>label {
|
||||
.identity-signature-grid>label,
|
||||
.identity-signature-grid>.identity-signature-field {
|
||||
min-width: 0
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -1246,7 +1246,7 @@
|
||||
<div class="identity-signature-grid">
|
||||
<label><span>Plain-text signature / footer</span><textarea id="identity-signature-text" rows="8"
|
||||
placeholder="Text-only signature used in plain-text messages"></textarea></label>
|
||||
<label class="identity-signature-field"><span>HTML signature / footer</span>
|
||||
<div class="identity-signature-field"><span id="identity-signature-label">HTML signature / footer</span>
|
||||
<div class="signature-editor-shell">
|
||||
<div class="signature-editor-toolbar" aria-label="Signature formatting"><button type="button"
|
||||
data-signature-command="bold" title="Bold"><svg>
|
||||
@@ -1264,11 +1264,11 @@
|
||||
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-remove-format"></use>
|
||||
</svg></button></div>
|
||||
<div id="identity-signature" class="signature-editor" contenteditable="true" role="textbox"
|
||||
aria-multiline="true"></div>
|
||||
aria-multiline="true" aria-labelledby="identity-signature-label"></div>
|
||||
</div><input id="identity-signature-image" type="file" accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
hidden><small class="muted signature-help">You can also paste an image directly into this
|
||||
editor.</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="identity-options-grid">
|
||||
<label><span>Signature position</span><select id="identity-signature-position">
|
||||
|
||||
@@ -484,6 +484,11 @@ function serializeComposeHtml(editor = $("#compose-editor")) {
|
||||
if (mode !== "html") return null;
|
||||
const clone = editor.cloneNode(true);
|
||||
clone.querySelectorAll(".rustbox-compose-font").forEach((wrapper) => wrapper.replaceWith(...wrapper.childNodes));
|
||||
clone.querySelectorAll('[data-rustbox-signature-entry="true"]').forEach((entry) => {
|
||||
entry.classList.remove("compose-body-entry");
|
||||
entry.removeAttribute("data-rustbox-signature-entry");
|
||||
if (!entry.className) entry.removeAttribute("class");
|
||||
});
|
||||
clone.querySelectorAll("img[data-rustbox-cid]").forEach((image) => {
|
||||
const cid = image.dataset.rustboxCid || "";
|
||||
if (cid) image.setAttribute("src", `cid:${cid}`);
|
||||
|
||||
@@ -362,6 +362,7 @@ function renderIdentities() {
|
||||
}
|
||||
|
||||
function editIdentity(identity = null) {
|
||||
state.identitySignatureRange = null;
|
||||
$("#identity-id").value = identity?.id || "";
|
||||
$("#identity-name").value = identity?.display_name || "";
|
||||
$("#identity-email").value = identity?.email || "";
|
||||
@@ -534,17 +535,60 @@ function composeHasVisibleSignature(editor, identity) {
|
||||
return Boolean(suffix && (editor.innerText || "").replace(/[\r\n]+$/g, "").endsWith(suffix));
|
||||
}
|
||||
|
||||
function composeEntryHasMeaningfulContent(entry) {
|
||||
if (!entry) return false;
|
||||
if ((entry.textContent || "").trim()) return true;
|
||||
return Boolean(entry.querySelector("img,video,a[href],table,ul,ol,blockquote"));
|
||||
}
|
||||
|
||||
function ensureComposeEntryBeforeSignature(editor, signature, action) {
|
||||
if (!editor || !signature || action !== "new") return null;
|
||||
let entry = editor.querySelector('[data-rustbox-signature-entry="true"]');
|
||||
if (entry) return entry;
|
||||
|
||||
let node = signature.previousSibling;
|
||||
while (node && node.nodeType === Node.TEXT_NODE && !(node.textContent || "").trim()) node = node.previousSibling;
|
||||
if (node) return null;
|
||||
|
||||
entry = el("div", "compose-body-entry");
|
||||
entry.dataset.rustboxSignatureEntry = "true";
|
||||
entry.innerHTML = "<br>";
|
||||
editor.insertBefore(entry, signature);
|
||||
return entry;
|
||||
}
|
||||
|
||||
function cleanupComposeSignatureEntry(editor) {
|
||||
const entry = editor?.querySelector('[data-rustbox-signature-entry="true"]');
|
||||
if (!entry) return;
|
||||
if (!composeEntryHasMeaningfulContent(entry)) {
|
||||
entry.remove();
|
||||
return;
|
||||
}
|
||||
entry.classList.remove("compose-body-entry");
|
||||
entry.removeAttribute("data-rustbox-signature-entry");
|
||||
if (!entry.className) entry.removeAttribute("class");
|
||||
}
|
||||
|
||||
function insertIdentitySignature(editor = $("#compose-editor"), action = state.composeAction || "new", options = {}) {
|
||||
if (!editor) return;
|
||||
const identity = selectedIdentity();
|
||||
const visible = identityUsesVisibleSignature(identity) && signatureEnabledForCompose(identity, action);
|
||||
if (options.preserveExisting && visible && composeHasVisibleSignature(editor, identity)) return;
|
||||
if (options.preserveExisting && visible && composeHasVisibleSignature(editor, identity)) {
|
||||
const existing = editor.querySelector('.compose-signature, [data-rustbox-signature="true"]');
|
||||
existing?.classList.toggle("compose-signature-new", action === "new");
|
||||
ensureComposeEntryBeforeSignature(editor, existing, action);
|
||||
return;
|
||||
}
|
||||
|
||||
removeVisibleComposeSignature(editor);
|
||||
if (!visible) return;
|
||||
if (!visible) {
|
||||
cleanupComposeSignatureEntry(editor);
|
||||
return;
|
||||
}
|
||||
|
||||
const format = $("#compose-mode")?.value || "html";
|
||||
const signature = el("div", "compose-signature rustbox-signature");
|
||||
signature.classList.toggle("compose-signature-new", action === "new");
|
||||
signature.dataset.rustboxSignature = "true";
|
||||
if (format === "html") {
|
||||
const html = String(identity.signature_html || "").trim();
|
||||
@@ -553,16 +597,23 @@ function insertIdentitySignature(editor = $("#compose-editor"), action = state.c
|
||||
else if (text) {
|
||||
signature.textContent = text;
|
||||
signature.style.whiteSpace = "pre-wrap";
|
||||
} else return;
|
||||
} else {
|
||||
cleanupComposeSignatureEntry(editor);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const text = String(identity.signature_text || "").replace(/[\r\n]+$/g, "");
|
||||
if (!text.trim()) return;
|
||||
if (!text.trim()) {
|
||||
cleanupComposeSignatureEntry(editor);
|
||||
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);
|
||||
ensureComposeEntryBeforeSignature(editor, signature, action);
|
||||
|
||||
const generation = state.composeSignatureGeneration;
|
||||
if (format === "html") void materializeVisibleSignatureImages(signature, generation);
|
||||
|
||||
@@ -458,21 +458,54 @@ function bindEvents() {
|
||||
$("#identity-form").addEventListener("submit", saveIdentity);
|
||||
$("#identity-new").addEventListener("click", () => editIdentity(null));
|
||||
$("#identity-delete").addEventListener("click", deleteIdentity);
|
||||
const signatureEditor = $("#identity-signature");
|
||||
const rememberSignatureRange = () => {
|
||||
const range = captureEditorRange(signatureEditor);
|
||||
if (range) state.identitySignatureRange = range;
|
||||
return range;
|
||||
};
|
||||
const selectedSignatureRange = () => {
|
||||
const range = state.identitySignatureRange || captureEditorRange(signatureEditor);
|
||||
if (!range || range.collapsed) return null;
|
||||
return range;
|
||||
};
|
||||
["keyup", "mouseup", "input"].forEach((eventName) => signatureEditor.addEventListener(eventName, rememberSignatureRange));
|
||||
signatureEditor.addEventListener("pointerdown", () => {
|
||||
// A plain click starts a new caret/selection. Never let a stale formatted
|
||||
// selection become the target of the next toolbar action.
|
||||
state.identitySignatureRange = null;
|
||||
});
|
||||
$$(".signature-editor-toolbar button").forEach((button) => button.addEventListener("mousedown", (event) => {
|
||||
if (event.button !== 0) return;
|
||||
rememberSignatureRange();
|
||||
event.preventDefault();
|
||||
}));
|
||||
$$("[data-signature-command]").forEach((button) => button.addEventListener("click", () => {
|
||||
const editor = $("#identity-signature");
|
||||
editor.focus();
|
||||
const range = selectedSignatureRange();
|
||||
if (!range) {
|
||||
// execCommand on a collapsed caret enables a sticky typing style (for
|
||||
// example every subsequently typed character becomes bold). Signature
|
||||
// formatting is intentionally selection-only.
|
||||
signatureEditor.focus();
|
||||
return;
|
||||
}
|
||||
restoreEditorRange(range, signatureEditor);
|
||||
document.execCommand(button.dataset.signatureCommand, false, null);
|
||||
rememberSignatureRange();
|
||||
}));
|
||||
$("#identity-signature-link").addEventListener("click", async () => {
|
||||
const editor = $("#identity-signature");
|
||||
const range = captureEditorRange(editor);
|
||||
const range = selectedSignatureRange();
|
||||
if (!range) {
|
||||
signatureEditor.focus();
|
||||
return toast("Select signature text before inserting a link", "warning");
|
||||
}
|
||||
const url = await appPrompt("Insert signature link", { placeholder: "https://example.com", type: "url", confirmLabel: "Insert" });
|
||||
if (!url) return;
|
||||
editor.focus();
|
||||
restoreEditorRange(range, editor);
|
||||
restoreEditorRange(range, signatureEditor);
|
||||
document.execCommand("createLink", false, url);
|
||||
rememberSignatureRange();
|
||||
});
|
||||
$("#identity-signature-image-btn").addEventListener("click", () => { state.identitySignatureRange = captureEditorRange($("#identity-signature")); $("#identity-signature-image").click(); });
|
||||
$("#identity-signature-image-btn").addEventListener("click", () => { rememberSignatureRange(); $("#identity-signature-image").click(); });
|
||||
$("#identity-signature-image").addEventListener("change", async (event) => { const file = event.target.files?.[0]; await insertIdentitySignatureImage(file); event.target.value = ""; });
|
||||
$("#identity-signature").addEventListener("paste", async (event) => {
|
||||
const file = [...(event.clipboardData?.items || [])].find((item) => item.kind === "file" && /^image\/(png|jpeg|gif|webp)$/i.test(item.type))?.getAsFile();
|
||||
|
||||
Reference in new issue
Block a user