Use the KeyBindingsManager for the SendMessageComposer
This commit is contained in:
parent
c7f9defd12
commit
b4c5dec4e5
2 changed files with 107 additions and 57 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,19 +145,37 @@ 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)
|
|
||||||
: event.key === Key.ENTER && !hasModifier;
|
|
||||||
if (send) {
|
|
||||||
this._sendMessage();
|
this._sendMessage();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (event.key === Key.ARROW_UP) {
|
break;
|
||||||
this.onVerticalArrow(event, true);
|
case KeyAction.SelectPrevSendHistory:
|
||||||
} else if (event.key === Key.ARROW_DOWN) {
|
case KeyAction.SelectNextSendHistory:
|
||||||
this.onVerticalArrow(event, false);
|
// Try select composer history
|
||||||
} else if (event.key === Key.ESCAPE) {
|
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({
|
dis.dispatch({
|
||||||
action: 'reply_to_event',
|
action: 'reply_to_event',
|
||||||
event: null,
|
event: null,
|
||||||
|
@ -165,39 +184,9 @@ export default class SendMessageComposer extends React.Component {
|
||||||
// This needs to be last!
|
// This needs to be last!
|
||||||
this._prepareToEncrypt();
|
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) {
|
||||||
|
|
Loading…
Reference in a new issue