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);
|
return () => window.removeEventListener('keydown', handleTemplateKey);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const addFiles = useCallback(async (files: File[]) => {
|
const addFiles = useCallback(async (files: File[]) => {
|
||||||
if (!client || files.length === 0) return;
|
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 = () => {
|
const cleanClose = () => {
|
||||||
if (saveTimeoutRef.current) {
|
if (saveTimeoutRef.current) {
|
||||||
clearTimeout(saveTimeoutRef.current);
|
clearTimeout(saveTimeoutRef.current);
|
||||||
|
|||||||
@@ -290,5 +290,6 @@ export const KEYBOARD_SHORTCUTS = {
|
|||||||
],
|
],
|
||||||
composer: [
|
composer: [
|
||||||
{ key: "t", description: "shortcuts.composer.template_picker" },
|
{ key: "t", description: "shortcuts.composer.template_picker" },
|
||||||
|
{ key: "Ctrl/Cmd + Enter", description: "shortcuts.composer.send" },
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -1882,7 +1882,8 @@
|
|||||||
"expand_collapse": "Expand/collapse thread"
|
"expand_collapse": "Expand/collapse thread"
|
||||||
},
|
},
|
||||||
"composer": {
|
"composer": {
|
||||||
"template_picker": "Open template picker"
|
"template_picker": "Open template picker",
|
||||||
|
"send": "Send email"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"threads": {
|
"threads": {
|
||||||
|
|||||||
Reference in New Issue
Block a user