diff --git a/src/lib/__tests__/reply-compose.test.ts b/src/lib/__tests__/reply-compose.test.ts new file mode 100644 index 00000000..0a5f4f68 --- /dev/null +++ b/src/lib/__tests__/reply-compose.test.ts @@ -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(); + }); +}); diff --git a/src/lib/reply-compose.ts b/src/lib/reply-compose.ts index dc762589..20fe3a3b 100644 --- a/src/lib/reply-compose.ts +++ b/src/lib/reply-compose.ts @@ -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/?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 | 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, diff --git a/src/screens/EmailThreadScreen.tsx b/src/screens/EmailThreadScreen.tsx index e965d1c8..2ef49a6a 100644 --- a/src/screens/EmailThreadScreen.tsx +++ b/src/screens/EmailThreadScreen.tsx @@ -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({ 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}