diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 3258674cf6..55baf9cb73 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -55,6 +55,14 @@ const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.sourc const IS_MAC = navigator.platform.indexOf("Mac") !== -1; +const SURROUND_WITH_CHARACTERS = ["\"", "_", "`", "'", "*", "~", "$"]; +const SURROUND_WITH_DOUBLE_CHARACTERS = new Map([ + ["(", ")"], + ["[", "]"], + ["{", "}"], + ["<", ">"], +]); + function ctrlShortcutLabel(key: string): string { return (IS_MAC ? "⌘" : "Ctrl") + "+" + key; } @@ -99,6 +107,7 @@ interface IState { showVisualBell?: boolean; autoComplete?: AutocompleteWrapperModel; completionIndex?: number; + surroundWith: boolean; } @replaceableComponent("views.rooms.BasicMessageEditor") @@ -117,12 +126,14 @@ export default class BasicMessageEditor extends React.Component private readonly emoticonSettingHandle: string; private readonly shouldShowPillAvatarSettingHandle: string; + private readonly surroundWithHandle: string; private readonly historyManager = new HistoryManager(); constructor(props) { super(props); this.state = { showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"), + surroundWith: SettingsStore.getValue("MessageComposerInput.surroundWith"), }; this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null, @@ -130,6 +141,8 @@ export default class BasicMessageEditor extends React.Component this.configureEmoticonAutoReplace(); this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null, this.configureShouldShowPillAvatar); + this.surroundWithHandle = SettingsStore.watchSetting("MessageComposerInput.surroundWith", null, + this.surroundWithSettingChanged); } public componentDidUpdate(prevProps: IProps) { @@ -422,6 +435,28 @@ export default class BasicMessageEditor extends React.Component private onKeyDown = (event: React.KeyboardEvent): void => { const model = this.props.model; let handled = false; + + if (this.state.surroundWith && document.getSelection().type != "Caret") { + // This surrounds the selected text with a character. This is + // intentionally left out of the keybinding manager as the keybinds + // here shouldn't be changeable + + const selectionRange = getRangeForSelection( + this.editorRef.current, + this.props.model, + document.getSelection(), + ); + // trim the range as we want it to exclude leading/trailing spaces + selectionRange.trim(); + + if ([...SURROUND_WITH_DOUBLE_CHARACTERS.keys(), ...SURROUND_WITH_CHARACTERS].includes(event.key)) { + this.historyManager.ensureLastChangesPushed(this.props.model); + this.modifiedFlag = true; + toggleInlineFormat(selectionRange, event.key, SURROUND_WITH_DOUBLE_CHARACTERS.get(event.key)); + handled = true; + } + } + const action = getKeyBindingsManager().getMessageComposerAction(event); switch (action) { case MessageComposerAction.FormatBold: @@ -574,6 +609,11 @@ export default class BasicMessageEditor extends React.Component this.setState({ showPillAvatar }); }; + private surroundWithSettingChanged = () => { + const surroundWith = SettingsStore.getValue("MessageComposerInput.surroundWith"); + this.setState({ surroundWith }); + }; + componentWillUnmount() { document.removeEventListener("selectionchange", this.onSelectionChange); this.editorRef.current.removeEventListener("input", this.onInput, true); @@ -581,6 +621,7 @@ export default class BasicMessageEditor extends React.Component this.editorRef.current.removeEventListener("compositionend", this.onCompositionEnd, true); SettingsStore.unwatchSetting(this.emoticonSettingHandle); SettingsStore.unwatchSetting(this.shouldShowPillAvatarSettingHandle); + SettingsStore.unwatchSetting(this.surroundWithHandle); } componentDidMount() { diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index d3da8a7784..ca85db967a 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -61,6 +61,7 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta 'MessageComposerInput.suggestEmoji', 'sendTypingNotifications', 'MessageComposerInput.ctrlEnterToSend', + 'MessageComposerInput.surroundWith', 'MessageComposerInput.showStickersButton', ]; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1cefa19e3e..70bb1e436b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -847,6 +847,7 @@ "Use Ctrl + F to search timeline": "Use Ctrl + F to search timeline", "Use Command + Enter to send a message": "Use Command + Enter to send a message", "Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message", + "Surround selected text when typing special characters": "Surround selected text when typing special characters", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Mirror local video feed": "Mirror local video feed", "Enable Community Filter Panel": "Enable Community Filter Panel", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index cfe2c097fc..ca0253a838 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -449,6 +449,11 @@ export const SETTINGS: {[setting: string]: ISetting} = { displayName: isMac ? _td("Use Command + Enter to send a message") : _td("Use Ctrl + Enter to send a message"), default: false, }, + "MessageComposerInput.surroundWith": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td("Surround selected text when typing special characters"), + default: false, + }, "MessageComposerInput.autoReplaceEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Automatically replace plain text Emoji'),