diff --git a/components/email/__tests__/composer-reply-signature.test.tsx b/components/email/__tests__/composer-reply-signature.test.tsx
index 9205d4c35..fa15f205c 100644
--- a/components/email/__tests__/composer-reply-signature.test.tsx
+++ b/components/email/__tests__/composer-reply-signature.test.tsx
@@ -304,6 +304,33 @@ describe('reply signature above the quote (#842)', () => {
expect(html).toContain('Sales team');
expect(html.indexOf('Sales team')).toBeLessThan(html.indexOf('reply_line'));
});
+
+ // #1503: with the delimiter on, a reply started with the "-- " line, so
+ // there was no line above the signature to type the reply into.
+ it('leaves an empty line above the delimiter', () => {
+ useSettingsStore.setState({ signaturePosition: 'above_quote' });
+ setIdentities([ME]);
+ render();
+
+ const html = screen.getByTestId('rich-text-editor').getAttribute('data-html')!;
+ expect(html.startsWith('
--
')).toBe(true);
+ });
+
+ it('leaves an empty line above a delimiter that arrives with late identities', async () => {
+ useSettingsStore.setState({ signaturePosition: 'above_quote' });
+ setIdentities([]);
+ const props = { mode: 'reply' as const, replyTo: TO_ME };
+ const { rerender } = render();
+ setIdentities([ME]);
+ rerender();
+
+ await waitFor(() => expect(editorState.html).toContain('Alice'));
+ const lines = new DOMParser().parseFromString(editorState.html, 'text/html').body.children;
+ expect(lines[0].tagName).toBe('P');
+ expect(lines[0].textContent).toBe('');
+ expect(lines[0].hasAttribute('data-signature-block')).toBe(false);
+ expect(lines[1].getAttribute('data-signature-block')).toBe('separator');
+ });
});
it('still signs a reply below the quote at send time by default', async () => {
diff --git a/components/email/compose-format.ts b/components/email/compose-format.ts
index 82e9e9081..2d3b83cf7 100644
--- a/components/email/compose-format.ts
+++ b/components/email/compose-format.ts
@@ -139,7 +139,19 @@ export function replaceEmbeddedSignature(
? children.find((node) => node.textContent?.includes(options.quoteHeader!))
: undefined;
const quote = header ?? children.find(isQuoteBoundary) ?? null;
- removed = { parent: doc.body, insertBefore: quote };
+ // With no line above it, the signature would open the body and leave no
+ // place to type (#1503). Put an empty one first, as getInitialBody does,
+ // and keep the bare line breaks for the gap down to the quote.
+ const above = quote ? children.slice(0, children.indexOf(quote)) : children;
+ const hasLineAbove = above.some((node) => node.nodeType === 1
+ ? (node as Element).tagName !== 'BR'
+ : !!node.textContent?.trim());
+ if (!hasLineAbove) {
+ removed = { parent: doc.body, insertBefore: above[0] ?? quote };
+ signatureHtml = `${signatureHtml}`;
+ } else {
+ removed = { parent: doc.body, insertBefore: quote };
+ }
}
const host = doc.createElement('div');
host.innerHTML = signatureHtml;
diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx
index dbab1a316..1f0ad42ae 100644
--- a/components/email/email-composer.tsx
+++ b/components/email/email-composer.tsx
@@ -771,10 +771,13 @@ export function EmailComposer({
: (from.email || from.name || tCommon('unknown')))
: tCommon('unknown');
+ // Like a new mail, an empty line above the signature gives the cursor a
+ // place for the reply. Without it the reply started on the "-- " line (#1503).
const signatureBlock = buildEmbeddedSignatureHtml(initialSignatureIdentity, {
embed: shouldEmbedSignatureAboveQuote,
separator: signatureSeparatorEnabled,
});
+ const head = `${prefix || (signatureBlock ? '' : '')}${signatureBlock}`;
// Whether the quoted original may show its remote images in the editor:
// the viewer's rule, minus the per-message "load" click it doesn't know.
@@ -798,7 +801,7 @@ export function EmailComposer({
rewriteCidImagesForEditor(sanitizeEmailHtml(replyTo.htmlBody)),
{ remoteAllowed: quoteRemoteAllowed, showQuoteBorder: replyTo.quoteWrapInBlockquote !== false },
);
- return `${prefix}${signatureBlock}
${replyHeader(replyTo.quoteHeaderHtml)}${island}`;
+ return `${head}
${replyHeader(replyTo.quoteHeaderHtml)}${island}`;
}
const escaped = replyTo.body
? replyTo.body.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
')
@@ -807,7 +810,7 @@ export function EmailComposer({
const bodyHtml = wrap
? `${escaped}
`
: escaped;
- return `${prefix}${signatureBlock}
${replyHeader(replyTo.quoteHeaderHtml)}${bodyHtml}`;
+ return `${head}
${replyHeader(replyTo.quoteHeaderHtml)}${bodyHtml}`;
}
// Build quoted content as HTML
@@ -825,15 +828,15 @@ export function EmailComposer({
rewriteCidImagesForEditor(sanitizeEmailHtml(replyTo.htmlBody)),
{ remoteAllowed: quoteRemoteAllowed },
);
- return `${prefix}${signatureBlock}
${replyHeader(`${quoteHeader}
`)}${island}`;
+ return `${head}
${replyHeader(`${quoteHeader}
`)}${island}`;
}
if (replyTo.body) {
const escapedOriginal = replyTo.body.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
');
if (mode === 'forward') {
- return `${prefix}${signatureBlock}
${tQuote('forwarded_separator')}
${tQuote('from_label')}: ${escapeHtml(fromStrFull)}
${tQuote('date_label')}: ${escapeHtml(date)}
${tQuote('subject_label')}: ${escapeHtml(replyTo.subject || '')}
${escapedOriginal}`;
+ return `${head}
${tQuote('forwarded_separator')}
${tQuote('from_label')}: ${escapeHtml(fromStrFull)}
${tQuote('date_label')}: ${escapeHtml(date)}
${tQuote('subject_label')}: ${escapeHtml(replyTo.subject || '')}
${escapedOriginal}`;
} else if (mode === 'reply' || mode === 'replyAll') {
- return `${prefix}${signatureBlock}
${replyHeader(`${tQuote('reply_line', { date: escapeHtml(date), from: escapeHtml(fromStrFull) })}
`)}${escapedOriginal}
`;
+ return `${head}
${replyHeader(`${tQuote('reply_line', { date: escapeHtml(date), from: escapeHtml(fromStrFull) })}
`)}${escapedOriginal}
`;
}
}
return prefix;