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
',
+ }),
+ });
+ 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.'
);