diff --git a/components/email/__tests__/compose-format.test.ts b/components/email/__tests__/compose-format.test.ts index 939c856c7..27123eef2 100644 --- a/components/email/__tests__/compose-format.test.ts +++ b/components/email/__tests__/compose-format.test.ts @@ -10,6 +10,8 @@ import { } from '../compose-format'; import { SIGNATURE_BLOCK_MARKER, SIGNATURE_RANGE_MARKER, containsEmbeddedSignature } from '../signature-block'; import { plainTextBodyHasSignature } from '@/lib/signature-utils'; +import { extractUserAuthoredText } from '@/lib/email-composer-utils'; +import { buildQuotedHtmlBlock } from '../quoted-html'; const identity = { htmlSignature: 'Alice
Acme', textSignature: 'Alice\nAcme' }; const count = (haystack: string, needle: string) => haystack.split(needle).length - 1; @@ -46,10 +48,24 @@ describe('compose-format (#1022): HTML <-> plain text switch', () => { it('does not invent a signature for a body that had none (below-quote reply)', () => { const text = htmlComposeBodyToPlainText('

Thanks

original
', identity, { separator: true }); - expect(text).toBe('Thanks\n\noriginal'); + expect(text).toBe('Thanks\n\n> original'); expect(plainTextBodyHasSignature(text, identity)).toBe(false); }); + it('keeps the quoted original of a rich reply marked with "> "', () => { + const html = `

Thanks

On Monday Bob wrote:

${buildQuotedHtmlBlock('

Line one
Line two

Para two

')}`; + const text = htmlComposeBodyToPlainText(html, identity, { separator: true }); + expect(text).toBe('Thanks\n\nOn Monday Bob wrote:\n\n> Line one\n> Line two\n>\n> Para two'); + // The attachment reminder and plugin edits only see the user's text. + expect(extractUserAuthoredText(text, { plainTextMode: true })).not.toContain('Line one'); + }); + + it('keeps list markers and image alt text', () => { + const html = '
  1. first
  2. second

a bike

'; + const text = htmlComposeBodyToPlainText(html, identity, { separator: true, imageAlt: (alt) => `[Image: ${alt}]` }); + expect(text).toBe('- apples\n- pears\n\n1. first\n2. second\n\n[Image: a bike]'); + }); + it('honours the separator setting', () => { const html = `

Hi

${buildEmbeddedSignatureHtml(identity, { embed: true, separator: false })}`; expect(htmlComposeBodyToPlainText(html, identity, { separator: false })).toBe('Hi\n\nAlice\nAcme'); diff --git a/components/email/compose-format.ts b/components/email/compose-format.ts index 2d3b83cf7..b8f262804 100644 --- a/components/email/compose-format.ts +++ b/components/email/compose-format.ts @@ -202,14 +202,17 @@ export function replacePlainTextSignatureAboveQuote( * Convert the rich-text editor's body to a plain-text body. Formatting is * dropped (that is the point of plain text); an embedded signature comes back * as the identity's plain-text signature so the body still "carries" it. + * The quote keeps its "> " lines and lists their markers, as in the text part + * the send path builds: the attachment reminder and plugin edits find the + * quote by them. */ export function htmlComposeBodyToPlainText( html: string, identity: SignatureIdentityLike, - options: { separator: boolean }, + options: { separator: boolean; imageAlt?: (alt: string) => string }, ): string { const stripped = stripEmbeddedSignature(html); - const text = htmlToPlainText(stripped.html, { paragraphSpacing: true }); + const text = htmlToPlainText(stripped.html, { paragraphSpacing: true, mailMarkup: true, imageAlt: options.imageAlt }); if (!stripped.hadSignature) return text; return appendPlainTextSignature(text, identity, { separator: options.separator }); } diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index e2aaabb1b..9c1bf9184 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -1951,12 +1951,12 @@ export function EmailComposer({ const editor = editorRef.current; const html = editor && !editor.isDestroyed ? serializeEditorContent(editor) : body; editorRef.current = null; - setBody(htmlComposeBodyToPlainText(html, signatureIdentity, { separator })); + setBody(htmlComposeBodyToPlainText(html, signatureIdentity, { separator, imageAlt: imageAltText })); setPlainTextMode(true); // Keyed on the signature fields, not the identity object (see the // signature effects above for the same rationale). // eslint-disable-next-line react-hooks/exhaustive-deps - }, [plainTextMode, body, signatureIdentity?.htmlSignature, signatureIdentity?.textSignature, signatureSeparatorEnabled]); + }, [plainTextMode, body, signatureIdentity?.htmlSignature, signatureIdentity?.textSignature, signatureSeparatorEnabled, imageAltText]); // Move a chip from one recipient field to another. `toIndex`, when given, // inserts at that position in the destination (drag-and-drop reordering,