From 8bb7b60728bd39286910233f25f68ea6865b44ec Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 03:48:53 +0100 Subject: [PATCH] fix: open a reply with the caret in the body and keep system autofill out of the address fields --- .../__tests__/compose-screen-wiring.test.ts | 17 ++++++++++++ src/screens/ComposeScreen.tsx | 27 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/lib/__tests__/compose-screen-wiring.test.ts b/src/lib/__tests__/compose-screen-wiring.test.ts index f992a9da..ecb59ed5 100644 --- a/src/lib/__tests__/compose-screen-wiring.test.ts +++ b/src/lib/__tests__/compose-screen-wiring.test.ts @@ -41,6 +41,23 @@ describe('ComposeScreen wiring', () => { expect(source).toMatch(/onPress=\{onDiscard\}/); }); + it('puts the caret in the body of a reply, also after the signature goes in', () => { + const fn = source.slice(source.indexOf('const focusReplyBody'), source.indexOf('const signatureEmbeddedRef')); + expect(fn).toMatch(/if \(!isReplyLike \|\| \(mode !== 'reply' && mode !== 'replyAll'\)\) return;/); + expect(fn).toMatch(/editorRef\.current\?\.focusEnd\(\);/); + expect(fn).toMatch(/plainBodyRef\.current\?\.setSelection\(0, 0\);/); + expect(fn).toMatch(/React\.useEffect\(\(\) => \{\s+focusReplyBody\(\);/); + const embed = source.slice(source.indexOf('const signatureEmbeddedRef'), source.indexOf('// Identity switch: swap the signature block')); + expect(embed).toMatch(/bumpBaseline\(\);\s+focusReplyBody\(\);/); + expect(embed).toMatch(/setTimeout\(focusReplyBody, 0\);/); + }); + + it('offers no system autofill in the address fields', () => { + const field = source.slice(source.indexOf('const renderRecipientField'), source.indexOf('const composer = (')); + expect(field).toMatch(/autoComplete="off"/); + expect(field).toMatch(/importantForAutofill="no"/); + }); + it('writes the text part of a rich message with mail markup', () => { const build = source.slice(source.indexOf('const buildOutgoing'), source.indexOf('// Save one draft version')); expect(build).toMatch(/textBody = htmlToPlainText\(safeHtml, \{ mailMarkup: true \}\);/); diff --git a/src/screens/ComposeScreen.tsx b/src/screens/ComposeScreen.tsx index 09f4bb98..e4482a3e 100644 --- a/src/screens/ComposeScreen.tsx +++ b/src/screens/ComposeScreen.tsx @@ -1190,6 +1190,25 @@ export default function ComposeScreen({ route, navigation }: Props) { return appendPlainTextSignature(body, identity, { separator: signatureSeparatorEnabled }); }, [isReplyLike, signaturePosition, signatureSeparatorEnabled]); + // A reply opens with the caret in the body, where the user's text goes + // (webmail email-composer.tsx handleEditorReady): in front of the + // signature and the quote. Again once the signature is in, since putting + // it in replaces the editor's content and drops the caret. + const focusReplyBody = React.useCallback(() => { + if (!isReplyLike || (mode !== 'reply' && mode !== 'replyAll')) return; + if (plainTextMode) { + plainBodyRef.current?.focus(); + plainBodyRef.current?.setSelection(0, 0); + } else { + editorRef.current?.focusEnd(); + } + }, [isReplyLike, mode, plainTextMode]); + React.useEffect(() => { + focusReplyBody(); + // On open only. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + // Embed the signature once the identities are known. A re-opened draft // keeps whatever it already carries (#848). const signatureEmbeddedRef = React.useRef(false); @@ -1206,6 +1225,8 @@ export default function ComposeScreen({ route, navigation }: Props) { baselineRef.current = snapshotOf({ to: initialTo, cc: initialCc, bcc: initialBcc, subject: initialSubject, body: next, attachments: initialAttachments, }); + // After the new text has rendered. + setTimeout(focusReplyBody, 0); } else { const sigHtml = buildEmbeddedSignatureHtml(signatureIdentity, { separator: signatureSeparatorEnabled }); void (async () => { @@ -1232,6 +1253,7 @@ export default function ComposeScreen({ route, navigation }: Props) { }); // The baseline is a ref: re-render so "edited" is worked out again. bumpBaseline(); + focusReplyBody(); })(); } return; @@ -2620,6 +2642,11 @@ export default function ComposeScreen({ route, navigation }: Props) { keyboardType="email-address" autoCapitalize="none" autoCorrect={false} + // The contact suggestions below are the autocomplete here, not + // the system's autofill (webmail: autoComplete off). + autoComplete="off" + importantForAutofill="no" + textContentType="none" /> {activeField === field && suggestions.length > 0 && (