diff --git a/components/email/__tests__/composer-plugin-surfaces.test.tsx b/components/email/__tests__/composer-plugin-surfaces.test.tsx
index 817a277ad..66357fe00 100644
--- a/components/email/__tests__/composer-plugin-surfaces.test.tsx
+++ b/components/email/__tests__/composer-plugin-surfaces.test.tsx
@@ -1,4 +1,4 @@
-import { act, createEvent, fireEvent, render, screen } from '@testing-library/react';
+import { act, createEvent, fireEvent, render, screen, within } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import React from 'react';
import { EmailComposer } from '../email-composer';
@@ -332,7 +332,7 @@ describe('composer-send-menu', () => {
});
// compose-23: a menu with "Schedule send" as its only item.
- it('opens the schedule dialog straight from the arrow when it is the only entry', () => {
+ it('opens the schedule dialog straight from the arrow when it is the only entry', async () => {
useAuthStore.setState({
client: { hasDelayedSend: () => true, getMaxDelayedSend: () => 30 * 86400, createDraft: vi.fn() } as never,
});
@@ -343,7 +343,24 @@ describe('composer-send-menu', () => {
fireEvent.click(arrow);
expect(screen.queryByRole('menuitem', { name: /schedule_send/ })).toBeNull();
- expect(screen.getByRole('dialog')).toHaveTextContent('schedule_send_description');
+ // After the send checks, which found nothing here (compose-p2-14).
+ expect(await screen.findByRole('dialog')).toHaveTextContent('schedule_send_description');
+ });
+
+ // compose-p2-14: the checks opened on top of the picked time and said
+ // "Send anyway" though nothing was sent then.
+ it('runs the send checks before the schedule dialog, and says "Schedule anyway"', async () => {
+ useAuthStore.setState({
+ client: { hasDelayedSend: () => true, getMaxDelayedSend: () => 30 * 86400, createDraft: vi.fn() } as never,
+ });
+ render();
+
+ fireEvent.click(screen.getAllByRole('button', { name: 'schedule_send' })[0]);
+ const warnings = await screen.findByRole('alertdialog');
+ expect(screen.queryByText('schedule_send_description')).toBeNull();
+ fireEvent.click(within(warnings).getByRole('button', { name: 'schedule_anyway' }));
+
+ expect(await screen.findByText('schedule_send_description')).toBeInTheDocument();
});
it('keeps a plain Send button without plugin entries or scheduled send', () => {
diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx
index fc292094a..9ba0a6d50 100644
--- a/components/email/email-composer.tsx
+++ b/components/email/email-composer.tsx
@@ -1259,7 +1259,9 @@ export function EmailComposer({
const [previewAttachment, setPreviewAttachment] = useState(null);
// What the checks before sending found (no subject, no text, placeholders,
// a missing attachment, open proofreading issues), asked about in one dialog.
- const [sendWarnings, setSendWarnings] = useState<{ items: SendWarning[]; delayedUntil?: string } | null>(null);
+ // `schedule`: asked before the schedule dialog, which opens on "Schedule anyway".
+ const [sendWarnings, setSendWarnings] = useState<{ items: SendWarning[]; delayedUntil?: string; schedule?: boolean } | null>(null);
+ const checkThenScheduleRef = useRef<() => void>(() => {});
// Open proofreading issues, once a checker asked to warn about them.
const proofreadingConfirmedRef = useRef(false);
const [showScheduleDialog, setShowScheduleDialog] = useState(false);
@@ -1545,9 +1547,11 @@ export function EmailComposer({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [signatureIdentity?.id, signatureContent, signatureSeparatorEnabled, signaturePosition, mode, plainTextMode, pluginSignatureRevision, autoSignatureEnabled]);
+ // The send checks run first, so they don't open on top of the picked time
+ // saying "Send anyway" when nothing is sent now (compose-p2-14).
const openScheduleDialog = useCallback(() => {
- setShowScheduleDialog(true);
closeSendMenu();
+ checkThenScheduleRef.current();
}, [closeSendMenu]);
// `autoSelectReplyIdentity` fused two behaviours: matching one of the user's
@@ -3836,10 +3840,12 @@ export function EmailComposer({
else editor.commands.focus();
};
- const handleSend = async ({ warningsConfirmed = false, delayedUntil }: {
+ const handleSend = async ({ warningsConfirmed = false, delayedUntil, checkOnly = false }: {
/** "Send anyway" in the warnings dialog: don't check those again. */
warningsConfirmed?: boolean;
delayedUntil?: string;
+ /** Only the checks, then the schedule dialog instead of a send. */
+ checkOnly?: boolean;
} = {}) => {
if (isSendingRef.current) return;
@@ -3986,10 +3992,14 @@ export function EmailComposer({
}
if (warnings.length > 0) {
- setSendWarnings({ items: warnings, delayedUntil });
+ setSendWarnings({ items: warnings, delayedUntil, ...(checkOnly ? { schedule: true } : {}) });
return;
}
}
+ if (checkOnly) {
+ setShowScheduleDialog(true);
+ return;
+ }
// Checks other features register (lib/outgoing/send-pipeline.ts), such
// as the external-recipient confirm. They may ask the user, so a second
@@ -4474,6 +4484,8 @@ export function EmailComposer({
};
+ checkThenScheduleRef.current = () => void handleSend({ checkOnly: true });
+
// A host can hand a follow-up to requestCloseRef - "close this session, then
// do that". It runs only once the composer really closes, never when the
// guard dialog is cancelled, and never before onClose has had its say.
@@ -6143,7 +6155,8 @@ export function EmailComposer({
setShowScheduleDialog(false)}
- onSchedule={(delayedUntil) => handleSend({ delayedUntil })}
+ // Checked before the dialog opened (openScheduleDialog).
+ onSchedule={(delayedUntil) => handleSend({ delayedUntil, warningsConfirmed: true })}
supported={!!composerClient?.hasDelayedSend()}
maxDelayedSend={composerClient?.getMaxDelayedSend() ?? 0}
disabled={!canSend || isSending || isSavingAndClosing}
@@ -6154,6 +6167,7 @@ export function EmailComposer({
{
const first = sendWarnings?.items[0];
setSendWarnings(null);
@@ -6171,7 +6185,7 @@ export function EmailComposer({
}}
onSendAnyway={(dontAskAgain) => {
if (!sendWarnings) return;
- const { items, delayedUntil } = sendWarnings;
+ const { items, delayedUntil, schedule } = sendWarnings;
for (const warning of items) {
if (warning.kind === 'subject') {
emptySubjectConfirmedRef.current = true;
@@ -6186,7 +6200,8 @@ export function EmailComposer({
}
}
setSendWarnings(null);
- handleSend({ warningsConfirmed: true, delayedUntil });
+ if (schedule) setShowScheduleDialog(true);
+ else handleSend({ warningsConfirmed: true, delayedUntil });
}}
/>
diff --git a/components/email/send-warnings-dialog.tsx b/components/email/send-warnings-dialog.tsx
index a29709903..0ea8a44eb 100644
--- a/components/email/send-warnings-dialog.tsx
+++ b/components/email/send-warnings-dialog.tsx
@@ -20,6 +20,8 @@ interface SendWarningsDialogProps {
onBack: () => void;
/** "Send anyway". `dontAskAgain`: the box under a single warning that has a setting. */
onSendAnyway: (dontAskAgain: boolean) => void;
+ /** Asked before Schedule send: the button says "Schedule anyway". */
+ scheduling?: boolean;
}
/** The warnings that can be switched off in Settings (subject and placeholders). */
@@ -30,7 +32,7 @@ const HAS_SETTING = new Set(["subject", "placeholders"]);
* own title and text, several are listed in one dialog with one "Send anyway"
* (two dialogs in a row asked about each in turn).
*/
-export function SendWarningsDialog({ warnings, onBack, onSendAnyway }: SendWarningsDialogProps) {
+export function SendWarningsDialog({ warnings, onBack, onSendAnyway, scheduling = false }: SendWarningsDialogProps) {
const t = useTranslations("email_composer");
const tSend = useTranslations("compose_send");
const tProofreading = useTranslations("proofreading");
@@ -89,7 +91,7 @@ export function SendWarningsDialog({ warnings, onBack, onSendAnyway }: SendWarni
backLabel = tProofreading("send_warning.review");
sendLabel = tProofreading("send_warning.send");
} else {
- title = tSend("warnings_title");
+ title = tSend(scheduling ? "schedule_warnings_title" : "warnings_title");
body = (
{items.map((w) => (
@@ -113,6 +115,9 @@ export function SendWarningsDialog({ warnings, onBack, onSendAnyway }: SendWarni
);
}
+ // Nothing goes out now: the message is scheduled (compose-p2-14).
+ if (scheduling) sendLabel = tSend("schedule_anyway");
+
return (