diff --git a/src/KeyBindingsManager.ts b/src/KeyBindingsManager.ts index b9cf9749ca..c32610670d 100644 --- a/src/KeyBindingsManager.ts +++ b/src/KeyBindingsManager.ts @@ -1,17 +1,23 @@ -import { isMac } from "./Keyboard"; +import { isMac, Key } from './Keyboard'; +import SettingsStore from './settings/SettingsStore'; export enum KeyBindingContext { - + SendMessageComposer = 'SendMessageComposer', } export enum KeyAction { None = 'None', + // SendMessageComposer actions: + Send = 'Send', + SelectPrevSendHistory = 'SelectPrevSendHistory', + SelectNextSendHistory = 'SelectNextSendHistory', + EditLastMessage = 'EditLastMessage', } /** * Represent a key combination. * - * The combo is evaluated strictly, i.e. the KeyboardEvent must match the exactly what is specified in the KeyCombo. + * The combo is evaluated strictly, i.e. the KeyboardEvent must match exactly what is specified in the KeyCombo. */ export type KeyCombo = { /** Currently only one `normal` key is supported */ @@ -27,8 +33,53 @@ export type KeyCombo = { } export type KeyBinding = { - keyCombo: KeyCombo; action: KeyAction; + keyCombo: KeyCombo; +} + +const messageComposerBindings = (): KeyBinding[] => { + const bindings: KeyBinding[] = [ + { + action: KeyAction.SelectPrevSendHistory, + keyCombo: { + keys: [Key.ARROW_UP], + altKey: true, + ctrlKey: true, + }, + }, + { + action: KeyAction.SelectNextSendHistory, + keyCombo: { + keys: [Key.ARROW_DOWN], + altKey: true, + ctrlKey: true, + }, + }, + { + action: KeyAction.EditLastMessage, + keyCombo: { + keys: [Key.ARROW_UP], + } + }, + ]; + if (SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend')) { + bindings.push({ + action: KeyAction.Send, + keyCombo: { + keys: [Key.ENTER], + ctrlOrCmd: true, + }, + }); + } else { + bindings.push({ + action: KeyAction.Send, + keyCombo: { + keys: [Key.ENTER], + }, + }); + } + + return bindings; } /** @@ -75,14 +126,24 @@ export function isKeyComboMatch(ev: KeyboardEvent, combo: KeyCombo, onMac: boole return true; } +export type KeyBindingsGetter = () => KeyBinding[]; + export class KeyBindingsManager { - contextBindings: Record = {}; + /** + * Map of KeyBindingContext to a KeyBinding getter arrow function. + * + * Returning a getter function allowed to have dynamic bindings, e.g. when settings change the bindings can be + * recalculated. + */ + contextBindings: Record = { + [KeyBindingContext.SendMessageComposer]: messageComposerBindings, + }; /** * Finds a matching KeyAction for a given KeyboardEvent */ getAction(context: KeyBindingContext, ev: KeyboardEvent): KeyAction { - const bindings = this.contextBindings[context]; + const bindings = this.contextBindings[context]?.(); if (!bindings) { return KeyAction.None; } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 62c474e417..0559c71e9e 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -48,6 +48,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import CountlyAnalytics from "../../../CountlyAnalytics"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import EMOJI_REGEX from 'emojibase-regex'; +import { getKeyBindingsManager, KeyAction, KeyBindingContext } from '../../../KeyBindingsManager'; function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); @@ -144,60 +145,48 @@ export default class SendMessageComposer extends React.Component { if (this._editorRef.isComposing(event)) { return; } - const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; - const ctrlEnterToSend = !!SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend'); - const send = ctrlEnterToSend - ? event.key === Key.ENTER && isOnlyCtrlOrCmdKeyEvent(event) - : event.key === Key.ENTER && !hasModifier; - if (send) { - this._sendMessage(); - event.preventDefault(); - } else if (event.key === Key.ARROW_UP) { - this.onVerticalArrow(event, true); - } else if (event.key === Key.ARROW_DOWN) { - this.onVerticalArrow(event, false); - } else if (event.key === Key.ESCAPE) { - dis.dispatch({ - action: 'reply_to_event', - event: null, - }); - } else if (this._prepareToEncrypt) { - // This needs to be last! - this._prepareToEncrypt(); + const action = getKeyBindingsManager().getAction(KeyBindingContext.MessageComposer, event); + switch (action) { + case KeyAction.Send: + this._sendMessage(); + event.preventDefault(); + break; + case KeyAction.SelectPrevSendHistory: + case KeyAction.SelectNextSendHistory: + // Try select composer history + const selected = this.selectSendHistory(action === KeyAction.SelectPrevSendHistory); + if (selected) { + // We're selecting history, so prevent the key event from doing anything else + event.preventDefault(); + } + break; + case KeyAction.EditLastMessage: + // selection must be collapsed and caret at start + if (this._editorRef.isSelectionCollapsed() && this._editorRef.isCaretAtStart()) { + const editEvent = findEditableEvent(this.props.room, false); + if (editEvent) { + // We're selecting history, so prevent the key event from doing anything else + event.preventDefault(); + dis.dispatch({ + action: 'edit_event', + event: editEvent, + }); + } + } + break; + default: + if (event.key === Key.ESCAPE) { + dis.dispatch({ + action: 'reply_to_event', + event: null, + }); + } else if (this._prepareToEncrypt) { + // This needs to be last! + this._prepareToEncrypt(); + } } }; - onVerticalArrow(e, up) { - // arrows from an initial-caret composer navigates recent messages to edit - // ctrl-alt-arrows navigate send history - if (e.shiftKey || e.metaKey) return; - - const shouldSelectHistory = e.altKey && e.ctrlKey; - const shouldEditLastMessage = !e.altKey && !e.ctrlKey && up && !this.props.replyToEvent; - - if (shouldSelectHistory) { - // Try select composer history - const selected = this.selectSendHistory(up); - if (selected) { - // We're selecting history, so prevent the key event from doing anything else - e.preventDefault(); - } - } else if (shouldEditLastMessage) { - // selection must be collapsed and caret at start - if (this._editorRef.isSelectionCollapsed() && this._editorRef.isCaretAtStart()) { - const editEvent = findEditableEvent(this.props.room, false); - if (editEvent) { - // We're selecting history, so prevent the key event from doing anything else - e.preventDefault(); - dis.dispatch({ - action: 'edit_event', - event: editEvent, - }); - } - } - } - } - // we keep sent messages/commands in a separate history (separate from undo history) // so you can alt+up/down in them selectSendHistory(up) {