feat(composer): Ctrl+Enter / Cmd+Enter sends the open draft
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.
This commit is contained in:
@@ -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<void>>();
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1882,7 +1882,8 @@
|
||||
"expand_collapse": "Expand/collapse thread"
|
||||
},
|
||||
"composer": {
|
||||
"template_picker": "Open template picker"
|
||||
"template_picker": "Open template picker",
|
||||
"send": "Send email"
|
||||
}
|
||||
},
|
||||
"threads": {
|
||||
|
||||
Reference in New Issue
Block a user