From 2818a16f06d3a569877ef97fe185e2588d416d35 Mon Sep 17 00:00:00 2001 From: Shuki Vaknin Date: Tue, 26 May 2026 20:01:48 +0300 Subject: [PATCH] feat(composer): Ctrl+Enter / Cmd+Enter sends the open draft MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the universal "send with the platform modifier" shortcut every mainstream mail client (Gmail, Outlook, Apple Mail, Proton, Tutanota, Fastmail, Thunderbird) supports. Closes #343. Behaviour: * Window-level keydown listener registered while the composer is mounted. Fires when focus is anywhere inside the composer — chip inputs, subject, body textarea, or the rich-text contentEditable. * Plain Enter is untouched; only Enter + Ctrl (Win/Linux) or Cmd (macOS) triggers send. Shift/Alt modifiers are ignored so existing autocomplete-confirm / chip-commit Enters are not hijacked. * Routes through a ref so handleSend's per-render rebind doesn't re-register the listener every render. * All existing send-time validation, attachment-warning, draft-save and undo-send flows still apply — the shortcut just calls the same handleSend() as the toolbar button. * Listed in the Keyboard Shortcuts dialog under the existing Composer section. Tested: * Compose -> type body -> Ctrl+Enter -> Outbox. * Cc/Bcc autocomplete suggestion + Enter still selects (alt-free Enter without Ctrl, so the new listener bails). * Subject input -> Ctrl+Enter -> sends. * Body Enter without modifier -> newline. --- components/email/email-composer.tsx | 23 +++++++++++++++++++++++ hooks/use-keyboard-shortcuts.ts | 1 + locales/en/common.json | 3 ++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index 90704e69..2401ca5c 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -854,6 +854,7 @@ export function EmailComposer({ return () => window.removeEventListener('keydown', handleTemplateKey); }, []); + const addFiles = useCallback(async (files: File[]) => { if (!client || files.length === 0) return; @@ -1574,6 +1575,28 @@ export function EmailComposer({ } }; + // Ctrl+Enter (Windows/Linux) / Cmd+Enter (macOS) sends the open + // compose draft — same as every other major mail client. Fires + // even when focus is inside the To/Cc/Bcc chips, subject input, + // body textarea, or the rich-text editor's contentEditable region. + // Plain Enter in the body still inserts a newline; only Enter + + // the platform modifier sends. handleSend is rebound every render, + // so we route through a ref to keep the window listener stable. + const handleSendRef = useRef<(skipAttachmentCheck?: boolean) => Promise>(); + handleSendRef.current = handleSend; + useEffect(() => { + const handleSendShortcut = (e: KeyboardEvent) => { + if (e.key !== 'Enter') return; + if (!(e.ctrlKey || e.metaKey)) return; + // Don't hijack autocomplete-confirm or chip-commit Enters. + if (e.altKey || e.shiftKey) return; + e.preventDefault(); + void handleSendRef.current?.(); + }; + window.addEventListener('keydown', handleSendShortcut); + return () => window.removeEventListener('keydown', handleSendShortcut); + }, []); + const cleanClose = () => { if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); diff --git a/hooks/use-keyboard-shortcuts.ts b/hooks/use-keyboard-shortcuts.ts index 0a27942c..0dd8a841 100644 --- a/hooks/use-keyboard-shortcuts.ts +++ b/hooks/use-keyboard-shortcuts.ts @@ -290,5 +290,6 @@ export const KEYBOARD_SHORTCUTS = { ], composer: [ { key: "t", description: "shortcuts.composer.template_picker" }, + { key: "Ctrl/Cmd + Enter", description: "shortcuts.composer.send" }, ], } as const; diff --git a/locales/en/common.json b/locales/en/common.json index 072169df..0468b27e 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -1882,7 +1882,8 @@ "expand_collapse": "Expand/collapse thread" }, "composer": { - "template_picker": "Open template picker" + "template_picker": "Open template picker", + "send": "Send email" } }, "threads": {