fix: keep the text typed in quick reply when opening the full composer

This commit is contained in:
Linus Rath
2026-10-15 02:35:09 +02:00
parent 250c0a9c26
commit 053fde8d56
3 changed files with 39 additions and 5 deletions
+29
View File
@@ -0,0 +1,29 @@
import { describe, it, expect } from 'vitest';
import type { Email } from '../../api/types';
import { replyComposeParams } from '../reply-compose';
const source = {
id: 'm1',
threadId: 't1',
mailboxIds: {},
keywords: {},
size: 1,
receivedAt: '2026-10-14T10:00:00Z',
subject: 'Hello',
from: [{ name: 'Petra', email: 'petra@example.net' }],
textBody: [{ partId: '1', type: 'text/plain' }],
bodyValues: { 1: { value: 'Please confirm.' } },
} as unknown as Email;
describe('replyComposeParams', () => {
it('carries the text typed in quick reply into the composer', () => {
const params = replyComposeParams('reply', source, undefined, 'Got it, thanks!');
expect(params?.prefillBody).toBe('Got it, thanks!');
expect(params?.replyTo?.from.email).toBe('petra@example.net');
});
it('adds nothing when nothing was typed', () => {
expect(replyComposeParams('reply', source)?.prefillBody).toBeUndefined();
expect(replyComposeParams('reply', source, undefined, ' ')?.prefillBody).toBeUndefined();
});
});
+5 -1
View File
@@ -5,12 +5,15 @@ import { pickEmailBody, plainTextBody } from './email-body';
/**
* Composer params for a reply / reply-all / forward of `source`. Shared by
* the thread viewer and the `mail/message/<id>?action=reply` deep link (the
* triage widget). Null when there is nobody to reply to.
* triage widget). Null when there is nobody to reply to. `typed` is text
* the user already wrote (quick reply's "More options"); it goes above the
* quote.
*/
export function replyComposeParams(
mode: 'reply' | 'replyAll' | 'forward',
source: Email,
ownerAccountId?: string,
typed?: string,
): NonNullable<RootStackParamList['Compose']> | null {
const from = source.from?.[0];
if (!from && mode !== 'forward') return null;
@@ -24,6 +27,7 @@ export function replyComposeParams(
const body = !quoteHtml || picked.text ? plainTextBody(source) : undefined;
return {
mode,
...(typed?.trim() ? { prefillBody: typed } : null),
replyTo: {
from: from ?? { email: '' },
to: source.to,
+5 -4
View File
@@ -557,10 +557,10 @@ function EmailViewer({ route, navigation, embedded = false, onActiveChange }: Pr
};
// Reply / forward the given message (a thread card's own, or the active one).
const navigateCompose = React.useCallback((mode: 'reply' | 'replyAll' | 'forward', target?: Email) => {
const navigateCompose = React.useCallback((mode: 'reply' | 'replyAll' | 'forward', target?: Email, typed?: string) => {
const source = target ?? email;
if (!source) return;
const params = replyComposeParams(mode, source, ownerAccountId);
const params = replyComposeParams(mode, source, ownerAccountId, typed);
if (params) navigation.navigate('Compose', params);
}, [email, navigation, ownerAccountId]);
@@ -961,7 +961,7 @@ interface EmailPaneProps {
onToggleStar: (email: Email) => void;
onAddressPress: (address: EmailAddress) => void;
onEmailPatched: (email: Email) => void;
onReply: (mode: 'reply' | 'replyAll' | 'forward', email: Email) => void;
onReply: (mode: 'reply' | 'replyAll' | 'forward', email: Email, typed?: string) => void;
onSwipe: (direction: 'prev' | 'next') => void;
onZoomChange: (zoom: { pinching: boolean; zoomed: boolean }) => void;
}
@@ -1182,7 +1182,8 @@ function EmailPane({
<QuickReplyBox
email={newest}
jmapAccountId={jmapAccountId}
onMoreOptions={() => onReply('reply', newest)}
// The text typed so far moves into the full composer.
onMoreOptions={(draft) => onReply('reply', newest, draft)}
onSent={onEmailPatched}
onFocus={handleQuickReplyFocus}
onBlur={handleQuickReplyBlur}