fix: preserve signature styling and reactivity in above-quote mode #272
This commit is contained in:
@@ -1659,7 +1659,9 @@ export default function Home() {
|
|||||||
|
|
||||||
// Append signature from the sending identity (fall back to primary
|
// Append signature from the sending identity (fall back to primary
|
||||||
// when the reply-from lives on the same identity but a different alias).
|
// when the reply-from lives on the same identity but a different alias).
|
||||||
const finalBody = appendPlainTextSignature(body, sendingIdentity);
|
const finalBody = appendPlainTextSignature(body, sendingIdentity, {
|
||||||
|
separator: useSettingsStore.getState().signatureSeparatorEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
const originalEmailId = selectedEmail.id;
|
const originalEmailId = selectedEmail.id;
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { appendPlainTextSignature, getPlainTextSignature } from "@/lib/signature
|
|||||||
import { resolveReplyFrom } from "@/lib/reply-identity";
|
import { resolveReplyFrom } from "@/lib/reply-identity";
|
||||||
import { computeReplyThreadingHeaders } from "@/lib/email-threading";
|
import { computeReplyThreadingHeaders } from "@/lib/email-threading";
|
||||||
import { RichTextEditor } from "@/components/email/rich-text-editor";
|
import { RichTextEditor } from "@/components/email/rich-text-editor";
|
||||||
|
import type { Editor } from "@tiptap/react";
|
||||||
|
|
||||||
/** Strip HTML tags and decode entities to get a plain-text version */
|
/** Strip HTML tags and decode entities to get a plain-text version */
|
||||||
function htmlToPlainText(html: string): string {
|
function htmlToPlainText(html: string): string {
|
||||||
@@ -116,6 +117,39 @@ type ComposerAttachment = {
|
|||||||
abortController?: AbortController;
|
abortController?: AbortController;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SignatureIdentityLike = {
|
||||||
|
htmlSignature?: string;
|
||||||
|
textSignature?: string;
|
||||||
|
} | null | undefined;
|
||||||
|
|
||||||
|
// Render the embedded signature for "above quote" mode. Bracketed with
|
||||||
|
// `data-signature-block` marker paragraphs so we can swap the inner content
|
||||||
|
// when the user switches identity without losing the surrounding draft or
|
||||||
|
// quoted message. The markers are preserved through TipTap by the
|
||||||
|
// StyledParagraph extension.
|
||||||
|
function buildEmbeddedSignatureHtml(
|
||||||
|
identity: SignatureIdentityLike,
|
||||||
|
options: { embed: boolean; separator: boolean }
|
||||||
|
): string {
|
||||||
|
if (!options.embed) return '';
|
||||||
|
const startMarker = options.separator
|
||||||
|
? `<p data-signature-block="separator">-- </p>`
|
||||||
|
: `<p data-signature-block="start"></p>`;
|
||||||
|
const endMarker = `<p data-signature-block="end"></p>`;
|
||||||
|
if (identity?.htmlSignature) {
|
||||||
|
return `${startMarker}${sanitizeEmailHtml(identity.htmlSignature)}${endMarker}`;
|
||||||
|
}
|
||||||
|
if (identity?.textSignature) {
|
||||||
|
const escaped = identity.textSignature
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/\n/g, '<br>');
|
||||||
|
return `${startMarker}<p>${escaped}</p>${endMarker}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
export function EmailComposer({
|
export function EmailComposer({
|
||||||
onSend,
|
onSend,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -136,6 +170,7 @@ export function EmailComposer({
|
|||||||
const attachmentReminderEnabled = useSettingsStore((state) => state.attachmentReminderEnabled);
|
const attachmentReminderEnabled = useSettingsStore((state) => state.attachmentReminderEnabled);
|
||||||
const attachmentReminderKeywords = useSettingsStore((state) => state.attachmentReminderKeywords);
|
const attachmentReminderKeywords = useSettingsStore((state) => state.attachmentReminderKeywords);
|
||||||
const signaturePosition = useSettingsStore((state) => state.signaturePosition);
|
const signaturePosition = useSettingsStore((state) => state.signaturePosition);
|
||||||
|
const signatureSeparatorEnabled = useSettingsStore((state) => state.signatureSeparatorEnabled);
|
||||||
const identities = useIdentityStore((s) => s.identities);
|
const identities = useIdentityStore((s) => s.identities);
|
||||||
const primaryIdentity = identities[0] ?? null;
|
const primaryIdentity = identities[0] ?? null;
|
||||||
|
|
||||||
@@ -206,8 +241,9 @@ export function EmailComposer({
|
|||||||
// drafting area and the quoted content so it reads naturally as a
|
// drafting area and the quoted content so it reads naturally as a
|
||||||
// closing for the reply body. Send-time append is skipped — see
|
// closing for the reply body. Send-time append is skipped — see
|
||||||
// shouldEmbedSignatureAboveQuote.
|
// shouldEmbedSignatureAboveQuote.
|
||||||
|
const plainSep = signatureSeparatorEnabled ? '\n\n-- \n' : '\n\n';
|
||||||
const signatureBlock = shouldEmbedSignatureAboveQuote
|
const signatureBlock = shouldEmbedSignatureAboveQuote
|
||||||
? `\n\n-- \n${getPlainTextSignature(initialSignatureIdentity)}`
|
? `${plainSep}${getPlainTextSignature(initialSignatureIdentity)}`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
if (mode === 'forward') {
|
if (mode === 'forward') {
|
||||||
@@ -225,21 +261,10 @@ export function EmailComposer({
|
|||||||
const from = replyTo.from?.[0];
|
const from = replyTo.from?.[0];
|
||||||
const fromStr = from ? `${from.name || from.email}` : tCommon('unknown');
|
const fromStr = from ? `${from.name || from.email}` : tCommon('unknown');
|
||||||
|
|
||||||
// When "above quote" is configured, splice signature between the user's
|
const signatureBlock = buildEmbeddedSignatureHtml(initialSignatureIdentity, {
|
||||||
// drafting area and the quoted content so it reads naturally as a closing
|
embed: shouldEmbedSignatureAboveQuote,
|
||||||
// for the reply body. Send-time append is skipped — see
|
separator: signatureSeparatorEnabled,
|
||||||
// shouldEmbedSignatureAboveQuote.
|
});
|
||||||
const buildEmbeddedSignatureHtml = (): string => {
|
|
||||||
if (!shouldEmbedSignatureAboveQuote) return '';
|
|
||||||
if (initialSignatureIdentity?.htmlSignature) {
|
|
||||||
return `<br><br>-- <br>${sanitizeEmailHtml(initialSignatureIdentity.htmlSignature)}`;
|
|
||||||
}
|
|
||||||
if (initialSignatureIdentity?.textSignature) {
|
|
||||||
return `<br><br>-- <br>${initialSignatureIdentity.textSignature.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>')}`;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
const signatureBlock = buildEmbeddedSignatureHtml();
|
|
||||||
|
|
||||||
// Build quoted content as HTML
|
// Build quoted content as HTML
|
||||||
if (replyTo.htmlBody && (mode === 'reply' || mode === 'replyAll' || mode === 'forward')) {
|
if (replyTo.htmlBody && (mode === 'reply' || mode === 'replyAll' || mode === 'forward')) {
|
||||||
@@ -335,6 +360,69 @@ export function EmailComposer({
|
|||||||
const signatureIdentity = (currentIdentity?.htmlSignature || currentIdentity?.textSignature)
|
const signatureIdentity = (currentIdentity?.htmlSignature || currentIdentity?.textSignature)
|
||||||
? currentIdentity
|
? currentIdentity
|
||||||
: primaryIdentity;
|
: primaryIdentity;
|
||||||
|
|
||||||
|
// Hold the TipTap editor instance so we can swap the embedded signature
|
||||||
|
// when the user switches identity in "above quote" mode without rebuilding
|
||||||
|
// the whole body (which would lose user edits to the surrounding draft).
|
||||||
|
const editorRef = useRef<Editor | null>(null);
|
||||||
|
const prevSignatureIdentityIdRef = useRef<string | null | undefined>(signatureIdentity?.id);
|
||||||
|
const prevSignatureSeparatorRef = useRef<boolean>(signatureSeparatorEnabled);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const editor = editorRef.current;
|
||||||
|
const identityChanged = prevSignatureIdentityIdRef.current !== signatureIdentity?.id;
|
||||||
|
const separatorChanged = prevSignatureSeparatorRef.current !== signatureSeparatorEnabled;
|
||||||
|
prevSignatureIdentityIdRef.current = signatureIdentity?.id;
|
||||||
|
prevSignatureSeparatorRef.current = signatureSeparatorEnabled;
|
||||||
|
if (!editor) return;
|
||||||
|
if (!identityChanged && !separatorChanged) return;
|
||||||
|
if (plainTextMode) return;
|
||||||
|
if (mode !== 'reply' && mode !== 'replyAll' && mode !== 'forward') return;
|
||||||
|
if (signaturePosition !== 'above_quote') return;
|
||||||
|
|
||||||
|
const currentHtml = editor.getHTML();
|
||||||
|
const doc = new DOMParser().parseFromString(currentHtml, 'text/html');
|
||||||
|
const startEl = doc.querySelector('[data-signature-block="separator"], [data-signature-block="start"]');
|
||||||
|
if (!startEl) return;
|
||||||
|
const endEl = doc.querySelector('[data-signature-block="end"]');
|
||||||
|
|
||||||
|
const newSignature = buildEmbeddedSignatureHtml(signatureIdentity, {
|
||||||
|
embed: true,
|
||||||
|
separator: signatureSeparatorEnabled,
|
||||||
|
});
|
||||||
|
if (!newSignature) return;
|
||||||
|
|
||||||
|
// Build a temporary container holding the replacement nodes so we can
|
||||||
|
// splice them in without re-serializing/parsing twice.
|
||||||
|
const replacementHost = doc.createElement('div');
|
||||||
|
replacementHost.innerHTML = newSignature;
|
||||||
|
const replacementNodes = Array.from(replacementHost.childNodes);
|
||||||
|
|
||||||
|
const parent = startEl.parentNode;
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
// Remove the existing signature range [startEl … endEl] inclusive, or
|
||||||
|
// from startEl to the next blockquote if no end marker is present.
|
||||||
|
const removeUntil = endEl && endEl.parentNode === parent ? endEl : null;
|
||||||
|
let cursor: ChildNode | null = startEl;
|
||||||
|
const toRemove: ChildNode[] = [];
|
||||||
|
while (cursor) {
|
||||||
|
toRemove.push(cursor);
|
||||||
|
if (cursor === removeUntil) break;
|
||||||
|
const next: ChildNode | null = cursor.nextSibling;
|
||||||
|
if (!removeUntil && next && (next as Element).tagName === 'BLOCKQUOTE') break;
|
||||||
|
cursor = next;
|
||||||
|
}
|
||||||
|
const insertBefore = toRemove[toRemove.length - 1]?.nextSibling ?? null;
|
||||||
|
toRemove.forEach((node) => parent.removeChild(node));
|
||||||
|
replacementNodes.forEach((node) => parent.insertBefore(node, insertBefore));
|
||||||
|
|
||||||
|
const nextHtml = doc.body.innerHTML;
|
||||||
|
if (nextHtml !== currentHtml) {
|
||||||
|
editor.commands.setContent(nextHtml, { emitUpdate: true });
|
||||||
|
}
|
||||||
|
}, [signatureIdentity?.id, signatureIdentity?.htmlSignature, signatureIdentity?.textSignature, signatureSeparatorEnabled, signaturePosition, mode, plainTextMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!autoSelectReplyIdentity) return;
|
if (!autoSelectReplyIdentity) return;
|
||||||
if (selectedIdentityId || initialData?.selectedIdentityId) return;
|
if (selectedIdentityId || initialData?.selectedIdentityId) return;
|
||||||
@@ -1038,11 +1126,12 @@ export function EmailComposer({
|
|||||||
// Build HTML signature block (used only in rich text mode)
|
// Build HTML signature block (used only in rich text mode)
|
||||||
const buildSignatureHtml = (): string => {
|
const buildSignatureHtml = (): string => {
|
||||||
if (signatureAlreadyInBody) return '';
|
if (signatureAlreadyInBody) return '';
|
||||||
|
const sep = signatureSeparatorEnabled ? `<br><br>-- <br>` : `<br><br>`;
|
||||||
if (signatureIdentity?.htmlSignature) {
|
if (signatureIdentity?.htmlSignature) {
|
||||||
return `<br><br>-- <br>${sanitizeEmailHtml(signatureIdentity.htmlSignature)}`;
|
return `${sep}${sanitizeEmailHtml(signatureIdentity.htmlSignature)}`;
|
||||||
}
|
}
|
||||||
if (signatureIdentity?.textSignature) {
|
if (signatureIdentity?.textSignature) {
|
||||||
return `<br><br>-- <br>${signatureIdentity.textSignature.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>')}`;
|
return `${sep}${signatureIdentity.textSignature.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>')}`;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
@@ -1053,9 +1142,10 @@ export function EmailComposer({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
// In plain text mode, send text/plain only (no HTML body)
|
// In plain text mode, send text/plain only (no HTML body)
|
||||||
|
const signatureOpts = { separator: signatureSeparatorEnabled };
|
||||||
const finalBody = plainTextMode
|
const finalBody = plainTextMode
|
||||||
? (signatureAlreadyInBody ? body : appendPlainTextSignature(body, signatureIdentity))
|
? (signatureAlreadyInBody ? body : appendPlainTextSignature(body, signatureIdentity, signatureOpts))
|
||||||
: (signatureAlreadyInBody ? htmlToPlainText(body) : appendPlainTextSignature(htmlToPlainText(body), signatureIdentity));
|
: (signatureAlreadyInBody ? htmlToPlainText(body) : appendPlainTextSignature(htmlToPlainText(body), signatureIdentity, signatureOpts));
|
||||||
|
|
||||||
const rewritten = plainTextMode ? null : rewriteInlineImages(body);
|
const rewritten = plainTextMode ? null : rewriteInlineImages(body);
|
||||||
const finalHtmlBody = plainTextMode
|
const finalHtmlBody = plainTextMode
|
||||||
@@ -1628,6 +1718,7 @@ export function EmailComposer({
|
|||||||
onImageUpload={handleImageUpload}
|
onImageUpload={handleImageUpload}
|
||||||
placeholder={t('body_placeholder')}
|
placeholder={t('body_placeholder')}
|
||||||
hasError={validationErrors.body}
|
hasError={validationErrors.body}
|
||||||
|
onEditorReady={(ed) => { editorRef.current = ed; }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1638,13 +1729,13 @@ export function EmailComposer({
|
|||||||
: plainTextMode ? (
|
: plainTextMode ? (
|
||||||
getPlainTextSignature(signatureIdentity) ? (
|
getPlainTextSignature(signatureIdentity) ? (
|
||||||
<div className="px-4 pb-3 text-sm leading-6 text-muted-foreground break-words whitespace-pre-wrap font-mono">
|
<div className="px-4 pb-3 text-sm leading-6 text-muted-foreground break-words whitespace-pre-wrap font-mono">
|
||||||
{'-- \n'}{getPlainTextSignature(signatureIdentity)}
|
{signatureSeparatorEnabled ? '-- \n' : ''}{getPlainTextSignature(signatureIdentity)}
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
) : composerSignatureHtml ? (
|
) : composerSignatureHtml ? (
|
||||||
<div
|
<div
|
||||||
className="px-4 pb-3 text-sm leading-6 text-foreground break-words [&_a]:text-primary [&_a]:underline-offset-2 [&_a:hover]:underline"
|
className="px-4 pb-3 text-sm leading-6 text-foreground break-words [&_a]:text-primary [&_a]:underline-offset-2 [&_a:hover]:underline"
|
||||||
dangerouslySetInnerHTML={{ __html: `<div>-- </div>${composerSignatureHtml}` }}
|
dangerouslySetInnerHTML={{ __html: `${signatureSeparatorEnabled ? '<div>-- </div>' : ''}${composerSignatureHtml}` }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useCallback, useState, useRef } from "react";
|
import React, { useEffect, useCallback, useState, useRef } from "react";
|
||||||
import { useEditor, EditorContent } from "@tiptap/react";
|
import { useEditor, EditorContent, type Editor } from "@tiptap/react";
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
|
import Paragraph from "@tiptap/extension-paragraph";
|
||||||
|
import Heading from "@tiptap/extension-heading";
|
||||||
import Underline from "@tiptap/extension-underline";
|
import Underline from "@tiptap/extension-underline";
|
||||||
import Link from "@tiptap/extension-link";
|
import Link from "@tiptap/extension-link";
|
||||||
import TextAlign from "@tiptap/extension-text-align";
|
import TextAlign from "@tiptap/extension-text-align";
|
||||||
@@ -44,6 +46,51 @@ export interface InlineImageUpload {
|
|||||||
cid?: string;
|
cid?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pasted email content (signatures, replies, quoted text) commonly carries
|
||||||
|
// inline styles on block elements. StarterKit's default Paragraph/Heading
|
||||||
|
// drop unknown attributes; extend them to round-trip `style` and `class` so
|
||||||
|
// signature formatting survives the editor.
|
||||||
|
const styledBlockAttributes = {
|
||||||
|
style: {
|
||||||
|
default: null as string | null,
|
||||||
|
parseHTML: (el: HTMLElement) => el.getAttribute("style"),
|
||||||
|
renderHTML: (attrs: Record<string, string | null>) =>
|
||||||
|
attrs.style ? { style: attrs.style } : {},
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
default: null as string | null,
|
||||||
|
parseHTML: (el: HTMLElement) => el.getAttribute("class"),
|
||||||
|
renderHTML: (attrs: Record<string, string | null>) =>
|
||||||
|
attrs.class ? { class: attrs.class } : {},
|
||||||
|
},
|
||||||
|
"data-signature-block": {
|
||||||
|
default: null as string | null,
|
||||||
|
parseHTML: (el: HTMLElement) => el.getAttribute("data-signature-block"),
|
||||||
|
renderHTML: (attrs: Record<string, string | null>) =>
|
||||||
|
attrs["data-signature-block"]
|
||||||
|
? { "data-signature-block": attrs["data-signature-block"] }
|
||||||
|
: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledParagraph = Paragraph.extend({
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
...this.parent?.(),
|
||||||
|
...styledBlockAttributes,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledHeading = Heading.extend({
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
...this.parent?.(),
|
||||||
|
...styledBlockAttributes,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
interface RichTextEditorProps {
|
interface RichTextEditorProps {
|
||||||
content: string;
|
content: string;
|
||||||
onChange: (html: string) => void;
|
onChange: (html: string) => void;
|
||||||
@@ -51,6 +98,7 @@ interface RichTextEditorProps {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
hasError?: boolean;
|
hasError?: boolean;
|
||||||
|
onEditorReady?: (editor: Editor) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarButton({
|
function ToolbarButton({
|
||||||
@@ -131,17 +179,23 @@ export function RichTextEditor({
|
|||||||
placeholder,
|
placeholder,
|
||||||
className,
|
className,
|
||||||
hasError,
|
hasError,
|
||||||
|
onEditorReady,
|
||||||
}: RichTextEditorProps) {
|
}: RichTextEditorProps) {
|
||||||
const onImageUploadRef = React.useRef(onImageUpload);
|
const onImageUploadRef = React.useRef(onImageUpload);
|
||||||
onImageUploadRef.current = onImageUpload;
|
onImageUploadRef.current = onImageUpload;
|
||||||
|
const onEditorReadyRef = React.useRef(onEditorReady);
|
||||||
|
onEditorReadyRef.current = onEditorReady;
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit.configure({
|
StarterKit.configure({
|
||||||
heading: { levels: [1, 2] },
|
heading: false,
|
||||||
|
paragraph: false,
|
||||||
link: false,
|
link: false,
|
||||||
underline: false,
|
underline: false,
|
||||||
}),
|
}),
|
||||||
|
StyledParagraph,
|
||||||
|
StyledHeading.configure({ levels: [1, 2] }),
|
||||||
Underline,
|
Underline,
|
||||||
Link.configure({
|
Link.configure({
|
||||||
openOnClick: false,
|
openOnClick: false,
|
||||||
@@ -239,6 +293,12 @@ export function RichTextEditor({
|
|||||||
}
|
}
|
||||||
}, [content, editor]);
|
}, [content, editor]);
|
||||||
|
|
||||||
|
// Expose the editor instance once it's ready so parents can target
|
||||||
|
// specific nodes (e.g. swap the embedded signature on identity change).
|
||||||
|
useEffect(() => {
|
||||||
|
if (editor) onEditorReadyRef.current?.(editor);
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
const addLink = useCallback(() => {
|
const addLink = useCallback(() => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
const previousUrl = editor.getAttributes("link").href;
|
const previousUrl = editor.getAttributes("link").href;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export function ComposingSettings() {
|
|||||||
attachmentReminderKeywords,
|
attachmentReminderKeywords,
|
||||||
subAddressDelimiter,
|
subAddressDelimiter,
|
||||||
signaturePosition,
|
signaturePosition,
|
||||||
|
signatureSeparatorEnabled,
|
||||||
updateSetting,
|
updateSetting,
|
||||||
} = useSettingsStore();
|
} = useSettingsStore();
|
||||||
|
|
||||||
@@ -62,6 +63,13 @@ export function ComposingSettings() {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem label={t('signature_separator.label')} description={t('signature_separator.description')}>
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={signatureSeparatorEnabled}
|
||||||
|
onChange={(checked) => updateSetting('signatureSeparatorEnabled', checked)}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
<SettingItem
|
<SettingItem
|
||||||
label={t('sub_address_delimiter.label')}
|
label={t('sub_address_delimiter.label')}
|
||||||
description={t('sub_address_delimiter.description', { delimiter: subAddressDelimiter })}
|
description={t('sub_address_delimiter.description', { delimiter: subAddressDelimiter })}
|
||||||
|
|||||||
@@ -110,13 +110,18 @@ export function getPlainTextSignature(signature?: SignatureSource | null): strin
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function appendPlainTextSignature(body: string, signature?: SignatureSource | null): string {
|
export function appendPlainTextSignature(
|
||||||
|
body: string,
|
||||||
|
signature?: SignatureSource | null,
|
||||||
|
options: { separator?: boolean } = {},
|
||||||
|
): string {
|
||||||
const plainTextSignature = getPlainTextSignature(signature);
|
const plainTextSignature = getPlainTextSignature(signature);
|
||||||
if (!plainTextSignature) {
|
if (!plainTextSignature) {
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${body}\n\n-- \n${plainTextSignature}`;
|
const sep = options.separator === false ? '\n\n' : '\n\n-- \n';
|
||||||
|
return `${body}${sep}${plainTextSignature}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasMeaningfulHtmlBody(html: string): boolean {
|
export function hasMeaningfulHtmlBody(html: string): boolean {
|
||||||
|
|||||||
@@ -977,6 +977,10 @@
|
|||||||
"above_quote": "Před citovaným textem",
|
"above_quote": "Před citovaným textem",
|
||||||
"below_quote": "Za citovaným textem"
|
"below_quote": "Za citovaným textem"
|
||||||
},
|
},
|
||||||
|
"signature_separator": {
|
||||||
|
"label": "Oddělovač podpisu",
|
||||||
|
"description": "Před podpis přidat standardní oddělovací řádek \"-- \" (RFC 3676). Vypněte, pokud chcete plynule přejít z textu zprávy do podpisu."
|
||||||
|
},
|
||||||
"sub_address_delimiter": {
|
"sub_address_delimiter": {
|
||||||
"label": "Oddělovač sub-adresy",
|
"label": "Oddělovač sub-adresy",
|
||||||
"description": "Znak oddělující uživatelské jméno od sub-adresy. Zvolte oddělovač používaný vaším poštovním serverem (např. uzivatel{delimiter}stitek@domena.cz).",
|
"description": "Znak oddělující uživatelské jméno od sub-adresy. Zvolte oddělovač používaný vaším poštovním serverem (např. uzivatel{delimiter}stitek@domena.cz).",
|
||||||
|
|||||||
@@ -977,6 +977,10 @@
|
|||||||
"above_quote": "Vor zitiertem Text",
|
"above_quote": "Vor zitiertem Text",
|
||||||
"below_quote": "Nach zitiertem Text"
|
"below_quote": "Nach zitiertem Text"
|
||||||
},
|
},
|
||||||
|
"signature_separator": {
|
||||||
|
"label": "Signatur-Trenner",
|
||||||
|
"description": "Der Signatur die Standard-Trennerzeile \"-- \" voranstellen (RFC 3676). Deaktivieren, wenn der Nachrichtentext direkt in die Signatur übergehen soll."
|
||||||
|
},
|
||||||
"sub_address_delimiter": {
|
"sub_address_delimiter": {
|
||||||
"label": "Sub-Adress-Trennzeichen",
|
"label": "Sub-Adress-Trennzeichen",
|
||||||
"description": "Zeichen, das Ihren Benutzernamen vom Sub-Adress-Tag trennt. Verwenden Sie das von Ihrem Mailserver verwendete Trennzeichen (z. B. benutzer{delimiter}tag@domain.de).",
|
"description": "Zeichen, das Ihren Benutzernamen vom Sub-Adress-Tag trennt. Verwenden Sie das von Ihrem Mailserver verwendete Trennzeichen (z. B. benutzer{delimiter}tag@domain.de).",
|
||||||
|
|||||||
@@ -980,6 +980,10 @@
|
|||||||
"above_quote": "Before quoted text",
|
"above_quote": "Before quoted text",
|
||||||
"below_quote": "After quoted text"
|
"below_quote": "After quoted text"
|
||||||
},
|
},
|
||||||
|
"signature_separator": {
|
||||||
|
"label": "Signature Delimiter",
|
||||||
|
"description": "Prefix the signature with the standard \"-- \" delimiter line (RFC 3676). Turn off if you'd rather flow straight from your message into the signature."
|
||||||
|
},
|
||||||
"sub_address_delimiter": {
|
"sub_address_delimiter": {
|
||||||
"label": "Sub-Address Delimiter",
|
"label": "Sub-Address Delimiter",
|
||||||
"description": "Character separating your username from a sub-address tag. Match the delimiter your mail server uses (e.g. user{delimiter}tag@domain.com).",
|
"description": "Character separating your username from a sub-address tag. Match the delimiter your mail server uses (e.g. user{delimiter}tag@domain.com).",
|
||||||
|
|||||||
@@ -972,6 +972,10 @@
|
|||||||
"above_quote": "Antes del texto citado",
|
"above_quote": "Antes del texto citado",
|
||||||
"below_quote": "Después del texto citado"
|
"below_quote": "Después del texto citado"
|
||||||
},
|
},
|
||||||
|
"signature_separator": {
|
||||||
|
"label": "Delimitador de firma",
|
||||||
|
"description": "Anteponer a la firma la línea delimitadora estándar \"-- \" (RFC 3676). Desactiva si prefieres pasar directamente del mensaje a la firma."
|
||||||
|
},
|
||||||
"sub_address_delimiter": {
|
"sub_address_delimiter": {
|
||||||
"label": "Delimitador de sub-dirección",
|
"label": "Delimitador de sub-dirección",
|
||||||
"description": "Carácter que separa tu nombre de usuario de la etiqueta de sub-dirección. Usa el delimitador que utilice tu servidor de correo (por ejemplo, usuario{delimiter}etiqueta@dominio.com).",
|
"description": "Carácter que separa tu nombre de usuario de la etiqueta de sub-dirección. Usa el delimitador que utilice tu servidor de correo (por ejemplo, usuario{delimiter}etiqueta@dominio.com).",
|
||||||
|
|||||||
@@ -972,6 +972,10 @@
|
|||||||
"above_quote": "Avant le texte cité",
|
"above_quote": "Avant le texte cité",
|
||||||
"below_quote": "Après le texte cité"
|
"below_quote": "Après le texte cité"
|
||||||
},
|
},
|
||||||
|
"signature_separator": {
|
||||||
|
"label": "Délimiteur de signature",
|
||||||
|
"description": "Préfixer la signature par la ligne de délimitation standard \"-- \" (RFC 3676). Désactivez si vous préférez enchaîner directement du message à la signature."
|
||||||
|
},
|
||||||
"sub_address_delimiter": {
|
"sub_address_delimiter": {
|
||||||
"label": "Délimiteur de sous-adresse",
|
"label": "Délimiteur de sous-adresse",
|
||||||
"description": "Caractère séparant votre nom d'utilisateur de l'étiquette de sous-adresse. Utilisez le délimiteur configuré sur votre serveur de messagerie (par ex. utilisateur{delimiter}tag@domaine.com).",
|
"description": "Caractère séparant votre nom d'utilisateur de l'étiquette de sous-adresse. Utilisez le délimiteur configuré sur votre serveur de messagerie (par ex. utilisateur{delimiter}tag@domaine.com).",
|
||||||
|
|||||||
@@ -972,6 +972,10 @@
|
|||||||
"above_quote": "Prima del testo citato",
|
"above_quote": "Prima del testo citato",
|
||||||
"below_quote": "Dopo il testo citato"
|
"below_quote": "Dopo il testo citato"
|
||||||
},
|
},
|
||||||
|
"signature_separator": {
|
||||||
|
"label": "Delimitatore firma",
|
||||||
|
"description": "Anteporre alla firma la riga di delimitazione standard \"-- \" (RFC 3676). Disattiva se preferisci passare direttamente dal messaggio alla firma."
|
||||||
|
},
|
||||||
"sub_address_delimiter": {
|
"sub_address_delimiter": {
|
||||||
"label": "Delimitatore sub-indirizzo",
|
"label": "Delimitatore sub-indirizzo",
|
||||||
"description": "Carattere che separa il tuo nome utente dall'etichetta del sub-indirizzo. Usa il delimitatore configurato sul tuo server di posta (es. utente{delimiter}tag@dominio.com).",
|
"description": "Carattere che separa il tuo nome utente dall'etichetta del sub-indirizzo. Usa il delimitatore configurato sul tuo server di posta (es. utente{delimiter}tag@dominio.com).",
|
||||||
|
|||||||
@@ -972,6 +972,10 @@
|
|||||||
"above_quote": "引用テキストの前",
|
"above_quote": "引用テキストの前",
|
||||||
"below_quote": "引用テキストの後"
|
"below_quote": "引用テキストの後"
|
||||||
},
|
},
|
||||||
|
"signature_separator": {
|
||||||
|
"label": "署名区切り",
|
||||||
|
"description": "署名の前に標準の区切り行「-- 」(RFC 3676)を付けます。本文から署名へ直接続けたい場合はオフにしてください。"
|
||||||
|
},
|
||||||
"sub_address_delimiter": {
|
"sub_address_delimiter": {
|
||||||
"label": "サブアドレス区切り文字",
|
"label": "サブアドレス区切り文字",
|
||||||
"description": "ユーザー名とサブアドレスタグを区切る文字です。お使いのメールサーバーが使用する区切り文字に合わせてください(例: user{delimiter}tag@domain.com)。",
|
"description": "ユーザー名とサブアドレスタグを区切る文字です。お使いのメールサーバーが使用する区切り文字に合わせてください(例: user{delimiter}tag@domain.com)。",
|
||||||
|
|||||||
@@ -977,6 +977,10 @@
|
|||||||
"above_quote": "인용 텍스트 앞",
|
"above_quote": "인용 텍스트 앞",
|
||||||
"below_quote": "인용 텍스트 뒤"
|
"below_quote": "인용 텍스트 뒤"
|
||||||
},
|
},
|
||||||
|
"signature_separator": {
|
||||||
|
"label": "서명 구분선",
|
||||||
|
"description": "서명 앞에 표준 구분선 \"-- \" (RFC 3676)을 추가합니다. 본문에서 바로 서명으로 이어지길 원하면 해제하세요."
|
||||||
|
},
|
||||||
"sub_address_delimiter": {
|
"sub_address_delimiter": {
|
||||||
"label": "서브 주소 구분자",
|
"label": "서브 주소 구분자",
|
||||||
"description": "사용자 이름과 서브 주소 태그를 나누는 문자예요. 메일 서버가 사용하는 구분자에 맞춰 주세요 (예: user{delimiter}tag@domain.com).",
|
"description": "사용자 이름과 서브 주소 태그를 나누는 문자예요. 메일 서버가 사용하는 구분자에 맞춰 주세요 (예: user{delimiter}tag@domain.com).",
|
||||||
|
|||||||
@@ -972,6 +972,10 @@
|
|||||||
"above_quote": "Pirms citētā teksta",
|
"above_quote": "Pirms citētā teksta",
|
||||||
"below_quote": "Pēc citētā teksta"
|
"below_quote": "Pēc citētā teksta"
|
||||||
},
|
},
|
||||||
|
"signature_separator": {
|
||||||
|
"label": "Paraksta atdalītājs",
|
||||||
|
"description": "Pirms paraksta pievienot standarta atdalītāju \"-- \" (RFC 3676). Izslēdziet, ja vēlaties pāriet no ziņojuma tieši uz parakstu."
|
||||||
|
},
|
||||||
"sub_address_delimiter": {
|
"sub_address_delimiter": {
|
||||||
"label": "Apakšadreses atdalītājs",
|
"label": "Apakšadreses atdalītājs",
|
||||||
"description": "Zīme, kas atdala lietotājvārdu no apakšadreses tagu. Izvēlieties atdalītāju, ko lieto jūsu pasta serveris (piem. lietotajs{delimiter}tags@domens.lv).",
|
"description": "Zīme, kas atdala lietotājvārdu no apakšadreses tagu. Izvēlieties atdalītāju, ko lieto jūsu pasta serveris (piem. lietotajs{delimiter}tags@domens.lv).",
|
||||||
|
|||||||
@@ -972,6 +972,10 @@
|
|||||||
"above_quote": "Voor geciteerde tekst",
|
"above_quote": "Voor geciteerde tekst",
|
||||||
"below_quote": "Na geciteerde tekst"
|
"below_quote": "Na geciteerde tekst"
|
||||||
},
|
},
|
||||||
|
"signature_separator": {
|
||||||
|
"label": "Handtekening-scheider",
|
||||||
|
"description": "De handtekening voorafgaan met de standaard scheidingsregel \"-- \" (RFC 3676). Zet uit als je liever direct van het bericht in de handtekening overgaat."
|
||||||
|
},
|
||||||
"sub_address_delimiter": {
|
"sub_address_delimiter": {
|
||||||
"label": "Sub-adres scheidingsteken",
|
"label": "Sub-adres scheidingsteken",
|
||||||
"description": "Teken dat je gebruikersnaam scheidt van het sub-adres-label. Gebruik het scheidingsteken dat je mailserver gebruikt (bv. gebruiker{delimiter}tag@domein.nl).",
|
"description": "Teken dat je gebruikersnaam scheidt van het sub-adres-label. Gebruik het scheidingsteken dat je mailserver gebruikt (bv. gebruiker{delimiter}tag@domein.nl).",
|
||||||
|
|||||||
@@ -977,6 +977,10 @@
|
|||||||
"above_quote": "Przed cytowanym tekstem",
|
"above_quote": "Przed cytowanym tekstem",
|
||||||
"below_quote": "Po cytowanym tekście"
|
"below_quote": "Po cytowanym tekście"
|
||||||
},
|
},
|
||||||
|
"signature_separator": {
|
||||||
|
"label": "Separator podpisu",
|
||||||
|
"description": "Poprzedź podpis standardową linią separatora \"-- \" (RFC 3676). Wyłącz, jeśli chcesz przejść bezpośrednio z treści wiadomości do podpisu."
|
||||||
|
},
|
||||||
"sub_address_delimiter": {
|
"sub_address_delimiter": {
|
||||||
"label": "Separator sub-adresu",
|
"label": "Separator sub-adresu",
|
||||||
"description": "Znak oddzielający Twoją nazwę użytkownika od tagu sub-adresu. Użyj separatora zgodnego z Twoim serwerem pocztowym (np. user{delimiter}tag@domain.com).",
|
"description": "Znak oddzielający Twoją nazwę użytkownika od tagu sub-adresu. Użyj separatora zgodnego z Twoim serwerem pocztowym (np. user{delimiter}tag@domain.com).",
|
||||||
|
|||||||
@@ -972,6 +972,10 @@
|
|||||||
"above_quote": "Antes do texto citado",
|
"above_quote": "Antes do texto citado",
|
||||||
"below_quote": "Depois do texto citado"
|
"below_quote": "Depois do texto citado"
|
||||||
},
|
},
|
||||||
|
"signature_separator": {
|
||||||
|
"label": "Delimitador de assinatura",
|
||||||
|
"description": "Anteceder a assinatura com a linha delimitadora padrão \"-- \" (RFC 3676). Desative se preferir passar direto da mensagem para a assinatura."
|
||||||
|
},
|
||||||
"sub_address_delimiter": {
|
"sub_address_delimiter": {
|
||||||
"label": "Delimitador de sub-endereço",
|
"label": "Delimitador de sub-endereço",
|
||||||
"description": "Caractere que separa seu nome de usuário da tag de sub-endereço. Use o delimitador configurado no seu servidor de e-mail (ex.: usuario{delimiter}tag@dominio.com).",
|
"description": "Caractere que separa seu nome de usuário da tag de sub-endereço. Use o delimitador configurado no seu servidor de e-mail (ex.: usuario{delimiter}tag@dominio.com).",
|
||||||
|
|||||||
@@ -972,6 +972,10 @@
|
|||||||
"above_quote": "Перед цитируемым текстом",
|
"above_quote": "Перед цитируемым текстом",
|
||||||
"below_quote": "После цитируемого текста"
|
"below_quote": "После цитируемого текста"
|
||||||
},
|
},
|
||||||
|
"signature_separator": {
|
||||||
|
"label": "Разделитель подписи",
|
||||||
|
"description": "Добавлять перед подписью стандартную строку-разделитель \"-- \" (RFC 3676). Отключите, если хотите переходить от текста сразу к подписи."
|
||||||
|
},
|
||||||
"sub_address_delimiter": {
|
"sub_address_delimiter": {
|
||||||
"label": "Разделитель суб-адресов",
|
"label": "Разделитель суб-адресов",
|
||||||
"description": "Символ, отделяющий имя пользователя от тега суб-адреса. Используйте разделитель, настроенный на вашем почтовом сервере (например, user{delimiter}tag@domain.com).",
|
"description": "Символ, отделяющий имя пользователя от тега суб-адреса. Используйте разделитель, настроенный на вашем почтовом сервере (например, user{delimiter}tag@domain.com).",
|
||||||
|
|||||||
@@ -977,6 +977,10 @@
|
|||||||
"above_quote": "Alıntılanan metinden önce",
|
"above_quote": "Alıntılanan metinden önce",
|
||||||
"below_quote": "Alıntılanan metinden sonra"
|
"below_quote": "Alıntılanan metinden sonra"
|
||||||
},
|
},
|
||||||
|
"signature_separator": {
|
||||||
|
"label": "İmza ayırıcı",
|
||||||
|
"description": "İmzayı standart \"-- \" ayırıcı satırıyla başlat (RFC 3676). Mesajdan doğrudan imzaya geçmek istiyorsanız kapatın."
|
||||||
|
},
|
||||||
"sub_address_delimiter": {
|
"sub_address_delimiter": {
|
||||||
"label": "Alt Adres Ayırıcı",
|
"label": "Alt Adres Ayırıcı",
|
||||||
"description": "Kullanıcı adını alt adres etiketinden ayıran karakter. Posta sunucunuzun kullandığı ayırıcıyı seçin (ör. kullanici{delimiter}etiket@domain.com).",
|
"description": "Kullanıcı adını alt adres etiketinden ayıran karakter. Posta sunucunuzun kullandığı ayırıcıyı seçin (ör. kullanici{delimiter}etiket@domain.com).",
|
||||||
|
|||||||
@@ -977,6 +977,10 @@
|
|||||||
"above_quote": "Перед цитованим текстом",
|
"above_quote": "Перед цитованим текстом",
|
||||||
"below_quote": "Після цитованого тексту"
|
"below_quote": "Після цитованого тексту"
|
||||||
},
|
},
|
||||||
|
"signature_separator": {
|
||||||
|
"label": "Розділювач підпису",
|
||||||
|
"description": "Додавати перед підписом стандартний рядок-розділювач \"-- \" (RFC 3676). Вимкніть, якщо хочете переходити з повідомлення відразу до підпису."
|
||||||
|
},
|
||||||
"sub_address_delimiter": {
|
"sub_address_delimiter": {
|
||||||
"label": "Розділювач під-адреси",
|
"label": "Розділювач під-адреси",
|
||||||
"description": "Символ, який відокремлює ім'я користувача від мітки під-адреси. Використовуйте розділювач, налаштований на вашому поштовому сервері (напр. user{delimiter}tag@domain.com).",
|
"description": "Символ, який відокремлює ім'я користувача від мітки під-адреси. Використовуйте розділювач, налаштований на вашому поштовому сервері (напр. user{delimiter}tag@domain.com).",
|
||||||
|
|||||||
@@ -977,6 +977,10 @@
|
|||||||
"above_quote": "引用文本之前",
|
"above_quote": "引用文本之前",
|
||||||
"below_quote": "引用文本之后"
|
"below_quote": "引用文本之后"
|
||||||
},
|
},
|
||||||
|
"signature_separator": {
|
||||||
|
"label": "签名分隔符",
|
||||||
|
"description": "在签名前加入标准的 “-- ” 分隔行(RFC 3676)。如果希望正文直接连到签名,请关闭。"
|
||||||
|
},
|
||||||
"sub_address_delimiter": {
|
"sub_address_delimiter": {
|
||||||
"label": "子地址分隔符",
|
"label": "子地址分隔符",
|
||||||
"description": "用于分隔用户名和子地址标签的字符。请选择与您的邮件服务器一致的分隔符(例如 user{delimiter}tag@domain.com)。",
|
"description": "用于分隔用户名和子地址标签的字符。请选择与您的邮件服务器一致的分隔符(例如 user{delimiter}tag@domain.com)。",
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ interface SettingsState {
|
|||||||
plainTextMode: boolean; // Send plain text only (no rich text editor)
|
plainTextMode: boolean; // Send plain text only (no rich text editor)
|
||||||
subAddressDelimiter: string; // Character separating user from tag (e.g. "user+tag@")
|
subAddressDelimiter: string; // Character separating user from tag (e.g. "user+tag@")
|
||||||
signaturePosition: SignaturePosition; // Position of the signature relative to quoted text in replies/forwards
|
signaturePosition: SignaturePosition; // Position of the signature relative to quoted text in replies/forwards
|
||||||
|
signatureSeparatorEnabled: boolean; // Prefix the signature with the RFC 3676 "-- " delimiter
|
||||||
|
|
||||||
// Privacy & Security
|
// Privacy & Security
|
||||||
sessionTimeout: number; // minutes (0 = never)
|
sessionTimeout: number; // minutes (0 = never)
|
||||||
@@ -298,6 +299,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
plainTextMode: false,
|
plainTextMode: false,
|
||||||
subAddressDelimiter: DEFAULT_SUB_ADDRESS_DELIMITER,
|
subAddressDelimiter: DEFAULT_SUB_ADDRESS_DELIMITER,
|
||||||
signaturePosition: 'below_quote' as SignaturePosition,
|
signaturePosition: 'below_quote' as SignaturePosition,
|
||||||
|
signatureSeparatorEnabled: true,
|
||||||
|
|
||||||
// Privacy & Security
|
// Privacy & Security
|
||||||
sessionTimeout: 0, // Never
|
sessionTimeout: 0, // Never
|
||||||
@@ -470,6 +472,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
plainTextMode: state.plainTextMode,
|
plainTextMode: state.plainTextMode,
|
||||||
subAddressDelimiter: state.subAddressDelimiter,
|
subAddressDelimiter: state.subAddressDelimiter,
|
||||||
signaturePosition: state.signaturePosition,
|
signaturePosition: state.signaturePosition,
|
||||||
|
signatureSeparatorEnabled: state.signatureSeparatorEnabled,
|
||||||
sessionTimeout: state.sessionTimeout,
|
sessionTimeout: state.sessionTimeout,
|
||||||
emailNotificationsEnabled: state.emailNotificationsEnabled,
|
emailNotificationsEnabled: state.emailNotificationsEnabled,
|
||||||
emailNotificationSound: state.emailNotificationSound,
|
emailNotificationSound: state.emailNotificationSound,
|
||||||
|
|||||||
Reference in New Issue
Block a user