Use the KeyBindingsManager for the SendMessageComposer

This commit is contained in:
Clemens Zeidler 2021-02-14 15:56:55 +13:00
parent c7f9defd12
commit b4c5dec4e5
2 changed files with 107 additions and 57 deletions

View file

@ -1,17 +1,23 @@
import { isMac } from "./Keyboard"; import { isMac, Key } from './Keyboard';
import SettingsStore from './settings/SettingsStore';
export enum KeyBindingContext { export enum KeyBindingContext {
SendMessageComposer = 'SendMessageComposer',
} }
export enum KeyAction { export enum KeyAction {
None = 'None', None = 'None',
// SendMessageComposer actions:
Send = 'Send',
SelectPrevSendHistory = 'SelectPrevSendHistory',
SelectNextSendHistory = 'SelectNextSendHistory',
EditLastMessage = 'EditLastMessage',
} }
/** /**
* Represent a key combination. * 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 = { export type KeyCombo = {
/** Currently only one `normal` key is supported */ /** Currently only one `normal` key is supported */
@ -27,8 +33,53 @@ export type KeyCombo = {
} }
export type KeyBinding = { export type KeyBinding = {
keyCombo: KeyCombo;
action: KeyAction; 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; return true;
} }
export type KeyBindingsGetter = () => KeyBinding[];
export class KeyBindingsManager { export class KeyBindingsManager {
contextBindings: Record<KeyBindingContext, KeyBinding[]> = {}; /**
* 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, KeyBindingsGetter> = {
[KeyBindingContext.SendMessageComposer]: messageComposerBindings,
};
/** /**
* Finds a matching KeyAction for a given KeyboardEvent * Finds a matching KeyAction for a given KeyboardEvent
*/ */
getAction(context: KeyBindingContext, ev: KeyboardEvent): KeyAction { getAction(context: KeyBindingContext, ev: KeyboardEvent): KeyAction {
const bindings = this.contextBindings[context]; const bindings = this.contextBindings[context]?.();
if (!bindings) { if (!bindings) {
return KeyAction.None; return KeyAction.None;
} }

View file

@ -48,6 +48,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import EMOJI_REGEX from 'emojibase-regex'; import EMOJI_REGEX from 'emojibase-regex';
import { getKeyBindingsManager, KeyAction, KeyBindingContext } from '../../../KeyBindingsManager';
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
@ -144,60 +145,48 @@ export default class SendMessageComposer extends React.Component {
if (this._editorRef.isComposing(event)) { if (this._editorRef.isComposing(event)) {
return; return;
} }
const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; const action = getKeyBindingsManager().getAction(KeyBindingContext.MessageComposer, event);
const ctrlEnterToSend = !!SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend'); switch (action) {
const send = ctrlEnterToSend case KeyAction.Send:
? event.key === Key.ENTER && isOnlyCtrlOrCmdKeyEvent(event) this._sendMessage();
: event.key === Key.ENTER && !hasModifier; event.preventDefault();
if (send) { break;
this._sendMessage(); case KeyAction.SelectPrevSendHistory:
event.preventDefault(); case KeyAction.SelectNextSendHistory:
} else if (event.key === Key.ARROW_UP) { // Try select composer history
this.onVerticalArrow(event, true); const selected = this.selectSendHistory(action === KeyAction.SelectPrevSendHistory);
} else if (event.key === Key.ARROW_DOWN) { if (selected) {
this.onVerticalArrow(event, false); // We're selecting history, so prevent the key event from doing anything else
} else if (event.key === Key.ESCAPE) { event.preventDefault();
dis.dispatch({ }
action: 'reply_to_event', break;
event: null, case KeyAction.EditLastMessage:
}); // selection must be collapsed and caret at start
} else if (this._prepareToEncrypt) { if (this._editorRef.isSelectionCollapsed() && this._editorRef.isCaretAtStart()) {
// This needs to be last! const editEvent = findEditableEvent(this.props.room, false);
this._prepareToEncrypt(); 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) // we keep sent messages/commands in a separate history (separate from undo history)
// so you can alt+up/down in them // so you can alt+up/down in them
selectSendHistory(up) { selectSendHistory(up) {