From b5e902e1f2f8f3a00fea93adeb7985db3ba13b23 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 21 Jan 2020 15:55:21 +0000 Subject: [PATCH 1/9] Fix escaping commands using double-slash //, e.g //plain sends `/plain` --- src/components/views/rooms/SendMessageComposer.js | 9 +++++++-- src/editor/serialize.js | 12 ++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index c11d940331..c4ae2929af 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -24,6 +24,8 @@ import { containsEmote, stripEmoteCommand, unescapeMessage, + startsWith, + stripPrefix, } from '../../../editor/serialize'; import {CommandPartCreator} from '../../../editor/parts'; import BasicMessageComposer from "./BasicMessageComposer"; @@ -61,6 +63,9 @@ function createMessageContent(model, permalinkCreator) { if (isEmote) { model = stripEmoteCommand(model); } + if (startsWith(model, "//")) { + model = stripPrefix(model, "/"); + } model = unescapeMessage(model); const repliedToEvent = RoomViewStore.getQuotingEvent(); @@ -175,13 +180,13 @@ export default class SendMessageComposer extends React.Component { const parts = this.model.parts; const firstPart = parts[0]; if (firstPart) { - if (firstPart.type === "command") { + if (firstPart.type === "command" && !firstPart.text.startsWith("//")) { return true; } // be extra resilient when somehow the AutocompleteWrapperModel or // CommandPartCreator fails to insert a command part, so we don't send // a command as a message - if (firstPart.text.startsWith("/") && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { + if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//") && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { return true; } } diff --git a/src/editor/serialize.js b/src/editor/serialize.js index a55eed97da..ba380f2809 100644 --- a/src/editor/serialize.js +++ b/src/editor/serialize.js @@ -61,18 +61,26 @@ export function textSerialize(model) { } export function containsEmote(model) { + return startsWith(model, "/me "); +} + +export function startsWith(model, prefix) { const firstPart = model.parts[0]; // part type will be "plain" while editing, // and "command" while composing a message. return firstPart && (firstPart.type === "plain" || firstPart.type === "command") && - firstPart.text.startsWith("/me "); + firstPart.text.startsWith(prefix); } export function stripEmoteCommand(model) { // trim "/me " + return stripPrefix(model, "/me "); +} + +export function stripPrefix(model, prefix) { model = model.clone(); - model.removeText({index: 0, offset: 0}, 4); + model.removeText({index: 0, offset: 0}, prefix.length); return model; } From 060938379a7183f7959aa4af436eef775ce6c9d3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 21 Jan 2020 15:58:51 +0000 Subject: [PATCH 2/9] Fix changes after typing / at pos=0 allowing to cancel command --- src/components/views/rooms/SendMessageComposer.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index c4ae2929af..8de105d84d 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -180,13 +180,14 @@ export default class SendMessageComposer extends React.Component { const parts = this.model.parts; const firstPart = parts[0]; if (firstPart) { - if (firstPart.type === "command" && !firstPart.text.startsWith("//")) { + if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) { return true; } // be extra resilient when somehow the AutocompleteWrapperModel or // CommandPartCreator fails to insert a command part, so we don't send // a command as a message - if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//") && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { + if (firstPart.text.startsWith("/") && firstPart.text.startsWith("//") && !firstPart.text.startsWith("//") + && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { return true; } } From b34fe45518fbfff23f92a860a7af653fc383180b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 21 Jan 2020 16:50:04 +0000 Subject: [PATCH 3/9] First attempt. Has a lag issue due to the async-clear :( --- src/SlashCommands.js | 13 +++---- .../views/rooms/SendMessageComposer.js | 39 +++++++++++++++++-- src/i18n/strings/en_EN.json | 4 +- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 20b8ba76da..414dd60121 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -81,6 +81,8 @@ class Command { } run(roomId, args) { + // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me` + if (!this.runFn) return; return this.runFn.bind(this)(roomId, args); } @@ -918,12 +920,12 @@ export function processCommandInput(roomId, input) { input = input.replace(/\s+$/, ''); if (input[0] !== '/') return null; // not a command - const bits = input.match(/^(\S+?)( +((.|\n)*))?$/); + const bits = input.match(/^(\S+?)(?: +((.|\n)*))?$/); let cmd; let args; if (bits) { cmd = bits[1].substring(1).toLowerCase(); - args = bits[3]; + args = bits[2]; } else { cmd = input; } @@ -932,11 +934,8 @@ export function processCommandInput(roomId, input) { cmd = aliases[cmd]; } if (CommandMap[cmd]) { - // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me` - if (!CommandMap[cmd].runFn) return null; - return CommandMap[cmd].run(roomId, args); - } else { - return reject(_t('Unrecognised command:') + ' ' + input); } + return null; + // return reject(_t('Unrecognised command:') + ' ' + input); } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 8de105d84d..9f3a407402 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -43,6 +43,9 @@ import ContentMessages from '../../../ContentMessages'; import {Key} from "../../../Keyboard"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +const SEND_ANYWAY = Symbol("send-anyway"); +const UNKNOWN_CMD = Symbol("unknown-cmd"); + function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); Object.assign(content, replyContent); @@ -194,7 +197,12 @@ export default class SendMessageComposer extends React.Component { return false; } - async _runSlashCommand() { + /** + * Parses and executes current input as a Slash Command + * @returns {Promise} UNKNOWN_CMD if the command is not known, + * SEND_ANYWAY if the input should be sent as message instead + */ + async _tryRunSlashCommand() { const commandText = this.model.parts.reduce((text, part) => { // use mxid to textify user pills in a command if (part.type === "user-pill") { @@ -236,16 +244,38 @@ export default class SendMessageComposer extends React.Component { } else { console.log("Command success."); } + } else { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + // unknown command, ask the user if they meant to send it as a message + const {finished} = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, { + title: _t("Unknown Command"), + description: _t("Unrecognised command: ") + commandText, + button: _t('Send as message'), + danger: true, + }); + const [sendAnyway] = await finished; + return sendAnyway ? SEND_ANYWAY : UNKNOWN_CMD; } } - _sendMessage() { + async _sendMessage() { if (this.model.isEmpty) { return; } + + let shouldSend = true; + if (!containsEmote(this.model) && this._isSlashCommand()) { - this._runSlashCommand(); - } else { + const resp = await this._tryRunSlashCommand(); + if (resp === UNKNOWN_CMD) { + // unknown command, bail to let the user modify it + return; + } + + shouldSend = resp === SEND_ANYWAY; + } + + if (shouldSend) { const isReply = !!RoomViewStore.getQuotingEvent(); const {roomId} = this.props.room; const content = createMessageContent(this.model, this.props.permalinkCreator); @@ -259,6 +289,7 @@ export default class SendMessageComposer extends React.Component { }); } } + this.sendHistoryManager.save(this.model); // clear composer this.model.reset([]); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f0eab6b12d..314731a910 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -200,7 +200,6 @@ "Sends the given message coloured as a rainbow": "Sends the given message coloured as a rainbow", "Sends the given emote coloured as a rainbow": "Sends the given emote coloured as a rainbow", "Displays list of commands with usages and descriptions": "Displays list of commands with usages and descriptions", - "Unrecognised command:": "Unrecognised command:", "Reason": "Reason", "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.", "%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.", @@ -1077,6 +1076,9 @@ "Server error": "Server error", "Command error": "Command error", "Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.", + "Unknown Command": "Unknown Command", + "Unrecognised command: ": "Unrecognised command: ", + "Send as message": "Send as message", "Failed to connect to integration manager": "Failed to connect to integration manager", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "Add some now": "Add some now", From 9f7df33bc30acaceaa7d1f9d100fbf2de153d8d3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 21 Jan 2020 16:57:07 +0000 Subject: [PATCH 4/9] re-arrange to split the async task into two and only wait on the user-blocking one --- src/SlashCommands.js | 10 +- .../views/rooms/SendMessageComposer.js | 100 +++++++++--------- 2 files changed, 52 insertions(+), 58 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 414dd60121..2eb34576ac 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -907,14 +907,14 @@ const aliases = { /** - * Process the given text for /commands and perform them. + * Process the given text for /commands and return a bound method to perform them. * @param {string} roomId The room in which the command was performed. * @param {string} input The raw text input by the user. - * @return {Object|null} An object with the property 'error' if there was an error + * @return {null|function(): Object} Function returning an object with the property 'error' if there was an error * processing the command, or 'promise' if a request was sent out. * Returns null if the input didn't match a command. */ -export function processCommandInput(roomId, input) { +export function getCommand(roomId, input) { // trim any trailing whitespace, as it can confuse the parser for // IRC-style commands input = input.replace(/\s+$/, ''); @@ -934,8 +934,6 @@ export function processCommandInput(roomId, input) { cmd = aliases[cmd]; } if (CommandMap[cmd]) { - return CommandMap[cmd].run(roomId, args); + return () => CommandMap[cmd].run(roomId, args); } - return null; - // return reject(_t('Unrecognised command:') + ' ' + input); } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 9f3a407402..994c28f531 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -35,7 +35,7 @@ import ReplyThread from "../elements/ReplyThread"; import {parseEvent} from '../../../editor/deserialize'; import {findEditableEvent} from '../../../utils/EventUtils'; import SendHistoryManager from "../../../SendHistoryManager"; -import {processCommandInput} from '../../../SlashCommands'; +import {getCommand} from '../../../SlashCommands'; import * as sdk from '../../../index'; import Modal from '../../../Modal'; import {_t, _td} from '../../../languageHandler'; @@ -197,12 +197,7 @@ export default class SendMessageComposer extends React.Component { return false; } - /** - * Parses and executes current input as a Slash Command - * @returns {Promise} UNKNOWN_CMD if the command is not known, - * SEND_ANYWAY if the input should be sent as message instead - */ - async _tryRunSlashCommand() { + _getSlashCommand() { const commandText = this.model.parts.reduce((text, part) => { // use mxid to textify user pills in a command if (part.type === "user-pill") { @@ -210,51 +205,41 @@ export default class SendMessageComposer extends React.Component { } return text + part.text; }, ""); - const cmd = processCommandInput(this.props.room.roomId, commandText); + return [getCommand(this.props.room.roomId, commandText), commandText]; + } - if (cmd) { - let error = cmd.error; - if (cmd.promise) { - try { - await cmd.promise; - } catch (err) { - error = err; - } + async _runSlashCommand(fn) { + const cmd = fn(); + let error = cmd.error; + if (cmd.promise) { + try { + await cmd.promise; + } catch (err) { + error = err; } - if (error) { - console.error("Command failure: %s", error); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - // assume the error is a server error when the command is async - const isServerError = !!cmd.promise; - const title = isServerError ? _td("Server error") : _td("Command error"); + } + if (error) { + console.error("Command failure: %s", error); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + // assume the error is a server error when the command is async + const isServerError = !!cmd.promise; + const title = isServerError ? _td("Server error") : _td("Command error"); - let errText; - if (typeof error === 'string') { - errText = error; - } else if (error.message) { - errText = error.message; - } else { - errText = _t("Server unavailable, overloaded, or something else went wrong."); - } - - Modal.createTrackedDialog(title, '', ErrorDialog, { - title: _t(title), - description: errText, - }); + let errText; + if (typeof error === 'string') { + errText = error; + } else if (error.message) { + errText = error.message; } else { - console.log("Command success."); + errText = _t("Server unavailable, overloaded, or something else went wrong."); } - } else { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - // unknown command, ask the user if they meant to send it as a message - const {finished} = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, { - title: _t("Unknown Command"), - description: _t("Unrecognised command: ") + commandText, - button: _t('Send as message'), - danger: true, + + Modal.createTrackedDialog(title, '', ErrorDialog, { + title: _t(title), + description: errText, }); - const [sendAnyway] = await finished; - return sendAnyway ? SEND_ANYWAY : UNKNOWN_CMD; + } else { + console.log("Command success."); } } @@ -266,13 +251,24 @@ export default class SendMessageComposer extends React.Component { let shouldSend = true; if (!containsEmote(this.model) && this._isSlashCommand()) { - const resp = await this._tryRunSlashCommand(); - if (resp === UNKNOWN_CMD) { - // unknown command, bail to let the user modify it - return; + const [cmd, commandText] = this._getSlashCommand(); + if (cmd) { + shouldSend = false; + this._runSlashCommand(cmd); + } else { + // ask the user if their unknown command should be sent as a message instead + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + // unknown command, ask the user if they meant to send it as a message + const {finished} = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, { + title: _t("Unknown Command"), + description: _t("Unrecognised command: ") + commandText, + button: _t('Send as message'), + danger: true, + }); + const [sendAnyway] = await finished; + // if !sendAnyway bail to let the user edit the composer and try again + if (!sendAnyway) return; } - - shouldSend = resp === SEND_ANYWAY; } if (shouldSend) { From a8df058ea6f7e55e2b8f1bbddc171a46777febe8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 21 Jan 2020 17:54:27 +0000 Subject: [PATCH 5/9] tidy up, improve wording on modal --- .../views/rooms/SendMessageComposer.js | 23 +++++++++++++------ src/i18n/strings/en_EN.json | 4 +++- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 994c28f531..7870699fec 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -43,9 +43,6 @@ import ContentMessages from '../../../ContentMessages'; import {Key} from "../../../Keyboard"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -const SEND_ANYWAY = Symbol("send-anyway"); -const UNKNOWN_CMD = Symbol("unknown-cmd"); - function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); Object.assign(content, replyContent); @@ -256,14 +253,26 @@ export default class SendMessageComposer extends React.Component { shouldSend = false; this._runSlashCommand(cmd); } else { - // ask the user if their unknown command should be sent as a message instead + // ask the user if their unknown command should be sent as a message const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - // unknown command, ask the user if they meant to send it as a message const {finished} = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, { title: _t("Unknown Command"), - description: _t("Unrecognised command: ") + commandText, + description:
+

+ { _t("Unrecognised command: %(commandText)s", {commandText}) } +

+

+ { _t("You can use /help to list available commands. Did you mean to send this as a message?", {}, { + code: t => { t }, + }) } +

+

+ { _t("Protip: Begin your message with // to start it with a slash.", {}, { + code: t => { t }, + }) } +

+
, button: _t('Send as message'), - danger: true, }); const [sendAnyway] = await finished; // if !sendAnyway bail to let the user edit the composer and try again diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 314731a910..da4111aec8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1077,7 +1077,9 @@ "Command error": "Command error", "Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.", "Unknown Command": "Unknown Command", - "Unrecognised command: ": "Unrecognised command: ", + "Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s", + "You can use /help to list available commands. Did you mean to send this as a message?": "You can use /help to list available commands. Did you mean to send this as a message?", + "Protip: Begin your message with // to start it with a slash.": "Protip: Begin your message with // to start it with a slash.", "Send as message": "Send as message", "Failed to connect to integration manager": "Failed to connect to integration manager", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", From e455aa474d652c1f66228ea4e2e2f8b69f2a796b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 21 Jan 2020 17:58:53 +0000 Subject: [PATCH 6/9] improve copy further --- src/components/views/rooms/SendMessageComposer.js | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 7870699fec..4402a034f6 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -267,7 +267,7 @@ export default class SendMessageComposer extends React.Component { }) }

- { _t("Protip: Begin your message with // to start it with a slash.", {}, { + { _t("Hint: Begin your message with // to start it with a slash.", {}, { code: t => { t }, }) }

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index da4111aec8..a3c56e5973 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1079,7 +1079,7 @@ "Unknown Command": "Unknown Command", "Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s", "You can use /help to list available commands. Did you mean to send this as a message?": "You can use /help to list available commands. Did you mean to send this as a message?", - "Protip: Begin your message with // to start it with a slash.": "Protip: Begin your message with // to start it with a slash.", + "Hint: Begin your message with // to start it with a slash.": "Hint: Begin your message with // to start it with a slash.", "Send as message": "Send as message", "Failed to connect to integration manager": "Failed to connect to integration manager", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", From 7b26067397e5bc1870c725968778bde83c6b0b34 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 21 Jan 2020 18:03:01 +0000 Subject: [PATCH 7/9] delint --- src/components/views/rooms/SendMessageComposer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 4402a034f6..c4970c4570 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -262,7 +262,8 @@ export default class SendMessageComposer extends React.Component { { _t("Unrecognised command: %(commandText)s", {commandText}) }

- { _t("You can use /help to list available commands. Did you mean to send this as a message?", {}, { + { _t("You can use /help to list available commands. " + + "Did you mean to send this as a message?", {}, { code: t => { t }, }) }

From e1e53f567f93ab044f24bc195d40fa4046acd2fb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 22 Jan 2020 11:56:27 +0000 Subject: [PATCH 8/9] add more tests --- .../views/rooms/SendMessageComposer.js | 3 +- .../views/rooms/SendMessageComposer-test.js | 83 +++++++++++++++++++ test/editor/mock.js | 10 +++ test/editor/model-test.js | 12 +-- 4 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 test/components/views/rooms/SendMessageComposer-test.js diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index c4970c4570..a857e40f55 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -58,7 +58,8 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { } } -function createMessageContent(model, permalinkCreator) { +// exported for tests +export function createMessageContent(model, permalinkCreator) { const isEmote = containsEmote(model); if (isEmote) { model = stripEmoteCommand(model); diff --git a/test/components/views/rooms/SendMessageComposer-test.js b/test/components/views/rooms/SendMessageComposer-test.js new file mode 100644 index 0000000000..d5a143a1fb --- /dev/null +++ b/test/components/views/rooms/SendMessageComposer-test.js @@ -0,0 +1,83 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import RoomViewStore from "../../../../src/stores/RoomViewStore"; +import {createMessageContent} from "../../../../src/components/views/rooms/SendMessageComposer"; +import EditorModel from "../../../../src/editor/model"; +import {createPartCreator, createRenderer} from "../../../editor/mock"; + +jest.mock("../../../../src/stores/RoomViewStore"); + +describe('', () => { + describe("createMessageContent", () => { + RoomViewStore.getQuotingEvent.mockReturnValue(false); + const permalinkCreator = jest.fn(); + + it("sends plaintext messages correctly", () => { + const model = new EditorModel([], createPartCreator(), createRenderer()); + model.update("hello world", "insertText", {offset: 11, atNodeEnd: true}); + + const content = createMessageContent(model, permalinkCreator); + + expect(content).toEqual({ + body: "hello world", + msgtype: "m.text", + }); + }); + + it("sends markdown messages correctly", () => { + const model = new EditorModel([], createPartCreator(), createRenderer()); + model.update("hello *world*", "insertText", {offset: 13, atNodeEnd: true}); + + const content = createMessageContent(model, permalinkCreator); + + expect(content).toEqual({ + body: "hello *world*", + msgtype: "m.text", + format: "org.matrix.custom.html", + formatted_body: "hello world", + }); + }); + + it("strips /me from messages and marks them as m.emote accordingly", () => { + const model = new EditorModel([], createPartCreator(), createRenderer()); + model.update("/me blinks __quickly__", "insertText", {offset: 22, atNodeEnd: true}); + + const content = createMessageContent(model, permalinkCreator); + + expect(content).toEqual({ + body: "blinks __quickly__", + msgtype: "m.emote", + format: "org.matrix.custom.html", + formatted_body: "blinks quickly", + }); + }); + + it("allows sending double-slash escaped slash commands correctly", () => { + const model = new EditorModel([], createPartCreator(), createRenderer()); + model.update("//dev/null is my favourite place", "insertText", {offset: 32, atNodeEnd: true}); + + const content = createMessageContent(model, permalinkCreator); + + expect(content).toEqual({ + body: "/dev/null is my favourite place", + msgtype: "m.text", + }); + }); + }); +}); + + diff --git a/test/editor/mock.js b/test/editor/mock.js index bb1a51d14b..6de65cf23d 100644 --- a/test/editor/mock.js +++ b/test/editor/mock.js @@ -67,3 +67,13 @@ export function createPartCreator(completions = []) { }; return new PartCreator(new MockRoom(), new MockClient(), autoCompleteCreator); } + +export function createRenderer() { + const render = (c) => { + render.caret = c; + render.count += 1; + }; + render.count = 0; + render.caret = null; + return render; +} diff --git a/test/editor/model-test.js b/test/editor/model-test.js index 826dde3d68..2a3584d508 100644 --- a/test/editor/model-test.js +++ b/test/editor/model-test.js @@ -15,17 +15,7 @@ limitations under the License. */ import EditorModel from "../../src/editor/model"; -import {createPartCreator} from "./mock"; - -function createRenderer() { - const render = (c) => { - render.caret = c; - render.count += 1; - }; - render.count = 0; - render.caret = null; - return render; -} +import {createPartCreator, createRenderer} from "./mock"; describe('editor/model', function() { describe('plain text manipulation', function() { From 516dd25797f4ad5c8fb53a6471ef65cf212879a7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 22 Jan 2020 14:24:10 +0000 Subject: [PATCH 9/9] fix typo in fallback codepath --- src/components/views/rooms/SendMessageComposer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index a857e40f55..6a60037036 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -187,7 +187,7 @@ export default class SendMessageComposer extends React.Component { // be extra resilient when somehow the AutocompleteWrapperModel or // CommandPartCreator fails to insert a command part, so we don't send // a command as a message - if (firstPart.text.startsWith("/") && firstPart.text.startsWith("//") && !firstPart.text.startsWith("//") + if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//") && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { return true; }