From 60d0e98fc78cca22f332238f4ccff5c54afd0e23 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Sat, 31 Oct 2026 03:35:30 +0100
Subject: [PATCH] fix: keep a hand-picked sender's own signature and swap it
after typing under it
---
.../__tests__/compose-screen-wiring.test.ts | 15 ++++++++++
src/lib/__tests__/signature-utils.test.ts | 19 +++++++++++++
src/lib/signature-utils.ts | 28 +++++++++++++++++--
src/screens/ComposeScreen.tsx | 12 +++++---
4 files changed, 67 insertions(+), 7 deletions(-)
create mode 100644 src/lib/__tests__/compose-screen-wiring.test.ts
diff --git a/src/lib/__tests__/compose-screen-wiring.test.ts b/src/lib/__tests__/compose-screen-wiring.test.ts
new file mode 100644
index 00000000..ea35ff14
--- /dev/null
+++ b/src/lib/__tests__/compose-screen-wiring.test.ts
@@ -0,0 +1,15 @@
+import { describe, expect, it } from 'vitest';
+import fs from 'node:fs';
+import path from 'node:path';
+
+// Composer behaviour that lives in ComposeScreen itself, checked on its
+// source (the suite has no React Native renderer).
+const source = fs.readFileSync(path.resolve(__dirname, '../../screens/ComposeScreen.tsx'), 'utf8');
+
+describe('ComposeScreen wiring', () => {
+ it('lets a From picked by hand bring its own (possibly empty) signature', () => {
+ expect(source).toMatch(/signatureIdentityFor\(primaryIdentity, identities, \{ pickedByUser: identityPickedByUser \}\)/);
+ const sheet = source.slice(source.indexOf(' x x
hello
${typed}hello
${b}PS: call me
hello
PS: call me
]*>[\\s\\S]*?
]*>(?:
)?
]*>[\\s\\S]*?
]*>((?:(?!
)[\\s\\S])*)`, 'i', ); +/** + * Text typed into the end marker is the user's, not part of the signature: + * kept as an ordinary paragraph after it (webmail compose-format.ts + * removeSignatureRange). + */ +function keptEndText(endContent: string): string { + const text = endContent + .replace(/${endContent}
` : ''; +} + /** True when the body carries an embedded signature range. */ export function containsEmbeddedSignature(html: string): boolean { return SIGNATURE_RANGE_RE.test(html); @@ -259,7 +281,7 @@ export function containsEmbeddedSignature(html: string): boolean { */ export function spliceSignature(html: string, nextSignatureHtml: string): string { if (SIGNATURE_RANGE_RE.test(html)) { - return html.replace(SIGNATURE_RANGE_RE, () => nextSignatureHtml); + return html.replace(SIGNATURE_RANGE_RE, (_m, end: string) => `${nextSignatureHtml}${keptEndText(end)}`); } if (!nextSignatureHtml) return html; return `${html}${nextSignatureHtml}`; @@ -267,7 +289,7 @@ export function spliceSignature(html: string, nextSignatureHtml: string): string /** The body without its embedded signature range. */ export function stripEmbeddedSignature(html: string): string { - return html.replace(SIGNATURE_RANGE_RE, ''); + return html.replace(SIGNATURE_RANGE_RE, (_m, end: string) => keptEndText(end)); } /** diff --git a/src/screens/ComposeScreen.tsx b/src/screens/ComposeScreen.tsx index 5604df87..68fd5003 100644 --- a/src/screens/ComposeScreen.tsx +++ b/src/screens/ComposeScreen.tsx @@ -985,10 +985,14 @@ export default function ComposeScreen({ route, navigation }: Props) { return identities.find((i) => i.id === selectedIdentityId) ?? defaultIdentity; }, [identities, selectedIdentityId]); - // An alias without a signature falls back to the primary identity's. + // Set once the user picks From by hand: their pick then brings its own + // signature, also when it has none (webmail identityPickedByUser). + const [identityPickedByUser, setIdentityPickedByUser] = React.useState(false); + // An alias without a signature falls back to the primary identity's, + // unless the user picked it. const signatureIdentity = React.useMemo( - () => signatureIdentityFor(primaryIdentity, identities), - [primaryIdentity, identities], + () => signatureIdentityFor(primaryIdentity, identities, { pickedByUser: identityPickedByUser }), + [primaryIdentity, identities, identityPickedByUser], ); const openIdentityPicker = () => { @@ -3220,7 +3224,7 @@ export default function ComposeScreen({ route, navigation }: Props) { onClose={() => setIdentitySheetOpen(false)} identities={identities} selectedIdentityId={selectedIdentityId} - onPick={(identity) => { setSelectedIdentityId(identity.id); setFromOverride(null); }} + onPick={(identity) => { setSelectedIdentityId(identity.id); setFromOverride(null); setIdentityPickedByUser(true); }} />