diff --git a/apps/web/src/features/email-compose/primitives/compose-persistence.test.ts b/apps/web/src/features/email-compose/primitives/compose-persistence.test.ts index 7319932daa8..8ea87fa8324 100644 --- a/apps/web/src/features/email-compose/primitives/compose-persistence.test.ts +++ b/apps/web/src/features/email-compose/primitives/compose-persistence.test.ts @@ -131,6 +131,26 @@ it('discards a reply draft at its server generation', async () => { ); }); +it('sends a reloaded reply without appending its quoted thread again', async () => { + const composeContext = createComposeContext(); + // The server keeps the quote's class but strips the editor's markers. + const root = mountReplyComposer(composeContext, () => message('parent'), { + draft: message('draft', { + is_draft: true, + replying_to_id: 'parent', + thread_db_id: 'thread', + body_html_sanitized: + '

Reply

Earlier
', + }), + }); + await root.sendEmail(); + root.dispose(); + const sent = vi.mocked(composeContext.delivery.sendMessage).mock.calls[0][0]; + expect(decodeBase64Utf8(sent.message.body_html ?? '')).not.toContain( + 'macro_quote' + ); +}); + it('keeps the draft editable after failed deletion and saves later edits', async () => { const composeContext = createComposeContext(); const goBack = vi.fn(); diff --git a/apps/web/src/features/email-compose/primitives/reply-composer.ts b/apps/web/src/features/email-compose/primitives/reply-composer.ts index aab9540ecc0..fdf38de6869 100644 --- a/apps/web/src/features/email-compose/primitives/reply-composer.ts +++ b/apps/web/src/features/email-compose/primitives/reply-composer.ts @@ -565,11 +565,15 @@ export function createReplyComposer( let sendOperationVersion = 0; const [pendingDeletion, setPendingDeletion] = createSignal(false); + // A reloaded draft keeps its quoted thread as plain blocks (the server + // strips the editor's markers), so only the saved body knows it is there. + const appendsQuotedThread = () => !!replyTarget && !form.replyAppended(); + function collectDraft(forDelivery = false) { if (!forDelivery) $removeAllWatermarkNodes(editor()); const prepared = prepareEmailBody( editor(), - forDelivery && replyTarget + forDelivery && replyTarget && appendsQuotedThread() ? { replyType: form.replyType(), replyingTo: replyTarget, @@ -1130,12 +1134,8 @@ export function createReplyComposer( if (result === 'scheduled') { const scheduledDraftId = savedDraftId(); rememberCurrentReply(); - try { - resetState(); - clearDraftState(); - } catch (error) { - props.notices.reportError(error); - } + // The scheduled draft stays in the thread with its "Scheduled for" + // bar, as a reload shows it; clearing it hid it until a refresh. void Promise.resolve( props.sideEffectOnSend?.(scheduledDraftId ?? null) ).catch(props.notices.reportError); @@ -1222,7 +1222,7 @@ export function createReplyComposer( const prepared = prepareEmailBody( currentEditor, - replyingTo + replyingTo && appendsQuotedThread() ? { replyType: form.replyType(), replyingTo, diff --git a/apps/web/src/features/email-compose/primitives/send-schedule.test.ts b/apps/web/src/features/email-compose/primitives/send-schedule.test.ts index 5279615d031..871c8acbf48 100644 --- a/apps/web/src/features/email-compose/primitives/send-schedule.test.ts +++ b/apps/web/src/features/email-compose/primitives/send-schedule.test.ts @@ -438,14 +438,10 @@ describe('send and schedule ordering', () => { expect(state.selectedTime()).toEqual(new Date(scheduled.sendTime)); pending.resolve(); await scheduling; - if (kind === 'standalone') { - expect(state.confirmedTime()).toEqual(new Date(scheduled.sendTime)); - expect(state.disabled()).toBe(true); - } else { - // Inline reply follows its normal successful-submit convention and - // collapses; a stale editing observation must not surface a cancel. - expect(state.scheduleState().type).toBe('editing'); - } + // Both keep showing the scheduled draft; a stale editing observation + // must not surface a cancel. + expect(state.confirmedTime()).toEqual(new Date(scheduled.sendTime)); + expect(state.disabled()).toBe(true); expect(context.notices.feedback.success).not.toHaveBeenCalledWith( 'Schedule cancelled. This email is editable again.' );