From 53f31e49dad440ae3bf87d6511d02c73139ecd05 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 13 Jan 2016 17:46:36 +0000 Subject: [PATCH 1/7] Implement tab-complete for slash commands This needed a new interface function `getOverrideSuffix()` so we didn't suffix commands at the start with ": ". All seems to work. --- src/SlashCommands.js | 6 +++++ src/TabComplete.js | 33 +++++++++++++++------------ src/TabCompleteEntries.js | 29 +++++++++++++++++++++++ src/components/structures/RoomView.js | 6 ++++- 4 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 1dd7ecb08f..3de3943b9e 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -322,5 +322,11 @@ module.exports = { } } return null; // not a command + }, + + getCommandList: function() { + return Object.keys(commands).map(function(cmd) { + return "/" + cmd; + }); } }; diff --git a/src/TabComplete.js b/src/TabComplete.js index 6690802d5d..3b117ca689 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -58,7 +58,7 @@ class TabComplete { // assign onClick listeners for each entry to complete the text this.list.forEach((l) => { l.onClick = () => { - this.completeTo(l.getText()); + this.completeTo(l); } }); } @@ -93,10 +93,10 @@ class TabComplete { /** * Do an auto-complete with the given word. This terminates the tab-complete. - * @param {string} someVal + * @param {Entry} entry The tab-complete entry to complete to. */ - completeTo(someVal) { - this.textArea.value = this._replaceWith(someVal, true); + completeTo(entry) { + this.textArea.value = this._replaceWith(entry.getText(), true, entry.getOverrideSuffix()); this.stopTabCompleting(); // keep focus on the text area this.textArea.focus(); @@ -222,8 +222,9 @@ class TabComplete { if (!this.inPassiveMode) { // set textarea to this new value this.textArea.value = this._replaceWith( - this.matchedList[this.currentIndex].text, - this.currentIndex !== 0 // don't suffix the original text! + this.matchedList[this.currentIndex].getText(), + this.currentIndex !== 0, // don't suffix the original text! + this.matchedList[this.currentIndex].getOverrideSuffix() ); } @@ -243,7 +244,7 @@ class TabComplete { } } - _replaceWith(newVal, includeSuffix) { + _replaceWith(newVal, includeSuffix, overrideSuffix) { // The regex to replace the input matches a character of whitespace AND // the partial word. If we just use string.replace() with the regex it will // replace the partial word AND the character of whitespace. We want to @@ -258,13 +259,17 @@ class TabComplete { boundaryChar = ""; } - var replacementText = ( - boundaryChar + newVal + ( - includeSuffix ? - (this.isFirstWord ? this.opts.startingWordSuffix : this.opts.wordSuffix) : - "" - ) - ); + var suffix = ""; + if (includeSuffix) { + if (overrideSuffix) { + suffix = overrideSuffix; + } + else { + suffix = (this.isFirstWord ? this.opts.startingWordSuffix : this.opts.wordSuffix); + } + } + + var replacementText = boundaryChar + newVal + suffix; return this.originalText.replace(MATCH_REGEX, function() { return replacementText; // function form to avoid `$` special-casing }); diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index d3efc0d2f1..4b7fbc5d0e 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -42,6 +42,14 @@ class Entry { return null; } + /** + * @return {?string} The suffix to override whatever the default is, or null to + * not do this. + */ + getOverrideSuffix() { + return null; + } + /** * Called when this entry is clicked. */ @@ -50,6 +58,26 @@ class Entry { } } +class CommandEntry extends Entry { + constructor(command) { + super(command); + } + + getKey() { + return this.getText(); + } + + getOverrideSuffix() { + return " "; // force a space after the command. + } +} + +CommandEntry.fromStrings = function(commandArray) { + return commandArray.map(function(cmd) { + return new CommandEntry(cmd); + }); +} + class MemberEntry extends Entry { constructor(member) { super(member.name || member.userId); @@ -99,3 +127,4 @@ MemberEntry.fromMemberList = function(members) { module.exports.Entry = Entry; module.exports.MemberEntry = MemberEntry; +module.exports.CommandEntry = CommandEntry; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 8e193b25ab..bc6438a97c 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -35,7 +35,9 @@ var sdk = require('../../index'); var CallHandler = require('../../CallHandler'); var TabComplete = require("../../TabComplete"); var MemberEntry = require("../../TabCompleteEntries").MemberEntry; +var CommandEntry = require("../../TabCompleteEntries").CommandEntry; var Resend = require("../../Resend"); +var SlashCommands = require("../../SlashCommands"); var dis = require("../../dispatcher"); var Tinter = require("../../Tinter"); @@ -416,7 +418,9 @@ module.exports = React.createClass({ return; } this.tabComplete.setCompletionList( - MemberEntry.fromMemberList(room.getJoinedMembers()) + MemberEntry.fromMemberList(room.getJoinedMembers()).concat( + CommandEntry.fromStrings(SlashCommands.getCommandList()) + ) ); }, From 864d10f4124fd62bd462c24f88e8cdf078dc0522 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 14 Jan 2016 11:39:24 +0000 Subject: [PATCH 2/7] Make individual Entrys responsible for determining suffixes This makes it cleaner as CommandEntry always wants a space, but MemberEntry wants a space or ": " depending on if it is the first word or not. --- src/TabComplete.js | 21 ++++++++------------- src/TabCompleteEntries.js | 10 +++++++--- src/components/structures/RoomView.js | 2 -- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/TabComplete.js b/src/TabComplete.js index 3b117ca689..59f3cec3a0 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -32,8 +32,6 @@ const MATCH_REGEX = /(^|\s)(\S+)$/; class TabComplete { constructor(opts) { - opts.startingWordSuffix = opts.startingWordSuffix || ""; - opts.wordSuffix = opts.wordSuffix || ""; opts.allowLooping = opts.allowLooping || false; opts.autoEnterTabComplete = opts.autoEnterTabComplete || false; opts.onClickCompletes = opts.onClickCompletes || false; @@ -96,7 +94,9 @@ class TabComplete { * @param {Entry} entry The tab-complete entry to complete to. */ completeTo(entry) { - this.textArea.value = this._replaceWith(entry.getText(), true, entry.getOverrideSuffix()); + this.textArea.value = this._replaceWith( + entry.getText(), true, entry.getSuffix(this.isFirstWord) + ); this.stopTabCompleting(); // keep focus on the text area this.textArea.focus(); @@ -224,7 +224,7 @@ class TabComplete { this.textArea.value = this._replaceWith( this.matchedList[this.currentIndex].getText(), this.currentIndex !== 0, // don't suffix the original text! - this.matchedList[this.currentIndex].getOverrideSuffix() + this.matchedList[this.currentIndex].getSuffix(this.isFirstWord) ); } @@ -244,7 +244,7 @@ class TabComplete { } } - _replaceWith(newVal, includeSuffix, overrideSuffix) { + _replaceWith(newVal, includeSuffix, suffix) { // The regex to replace the input matches a character of whitespace AND // the partial word. If we just use string.replace() with the regex it will // replace the partial word AND the character of whitespace. We want to @@ -259,14 +259,9 @@ class TabComplete { boundaryChar = ""; } - var suffix = ""; - if (includeSuffix) { - if (overrideSuffix) { - suffix = overrideSuffix; - } - else { - suffix = (this.isFirstWord ? this.opts.startingWordSuffix : this.opts.wordSuffix); - } + suffix = suffix || ""; + if (!includeSuffix) { + suffix = ""; } var replacementText = boundaryChar + newVal + suffix; diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index 4b7fbc5d0e..79e0a9a46b 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -43,10 +43,10 @@ class Entry { } /** - * @return {?string} The suffix to override whatever the default is, or null to + * @return {?string} The suffix to append to the tab-complete, or null to * not do this. */ - getOverrideSuffix() { + getSuffix(isFirstWord) { return null; } @@ -67,7 +67,7 @@ class CommandEntry extends Entry { return this.getText(); } - getOverrideSuffix() { + getSuffix(isFirstWord) { return " "; // force a space after the command. } } @@ -94,6 +94,10 @@ class MemberEntry extends Entry { getKey() { return this.member.userId; } + + getSuffix(isFirstWord) { + return isFirstWord ? ": " : " "; + } } MemberEntry.fromMemberList = function(members) { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index bc6438a97c..abfa2e27f1 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -96,8 +96,6 @@ module.exports = React.createClass({ // xchat-style tab complete, add a colon if tab // completing at the start of the text this.tabComplete = new TabComplete({ - startingWordSuffix: ": ", - wordSuffix: " ", allowLooping: false, autoEnterTabComplete: true, onClickCompletes: true, From b67131f0c83fd5da063d77512e784008b02509b7 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 14 Jan 2016 14:39:58 +0000 Subject: [PATCH 3/7] Add a Command class; add Entry.getFillText() getFillText() serves to decouple the text displayed in the auto-complete list via getText() and the text actually filled into the box via getFillText(). This allows us to display command + args on the list but only fill the command part. A Command class has been added to provide some structure when extracting the command name and args. Manually tested and it works. --- src/SlashCommands.js | 104 +++++++++++------- src/TabComplete.js | 4 +- src/TabCompleteEntries.js | 23 +++- src/components/structures/RoomView.js | 2 +- src/components/views/rooms/MessageComposer.js | 2 +- 5 files changed, 86 insertions(+), 49 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 3de3943b9e..83b4b52ffc 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -20,6 +20,31 @@ var dis = require("./dispatcher"); var encryption = require("./encryption"); var Tinter = require("./Tinter"); + +class Command { + constructor(name, paramArgs, runFn) { + this.name = name; + this.paramArgs = paramArgs; + this.runFn = runFn; + } + + getCommand() { + return "/" + this.name; + } + + getCommandWithArgs() { + return this.getCommand() + " " + this.paramArgs; + } + + run(roomId, args) { + return this.runFn.bind(this)(roomId, args); + } + + getUsage() { + return "Usage: " + this.getCommandWithArgs() + } +} + var reject = function(msg) { return { error: msg @@ -34,18 +59,17 @@ var success = function(promise) { var commands = { // Change your nickname - nick: function(room_id, args) { + nick: new Command("nick", "", function(room_id, args) { if (args) { return success( MatrixClientPeg.get().setDisplayName(args) ); } - return reject("Usage: /nick "); - }, + return reject(this.getUsage()); + }), // Changes the colorscheme of your current room - tint: function(room_id, args) { - + tint: new Command("tint", " []", function(room_id, args) { if (args) { var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); if (matches) { @@ -62,10 +86,10 @@ var commands = { ); } } - return reject("Usage: /tint []"); - }, + return reject(this.getUsage()); + }), - encrypt: function(room_id, args) { + encrypt: new Command("encrypt", "", function(room_id, args) { if (args == "on") { var client = MatrixClientPeg.get(); var members = client.getRoom(room_id).currentState.members; @@ -81,21 +105,21 @@ var commands = { ); } - return reject("Usage: encrypt "); - }, + return reject(this.getUsage()); + }), // Change the room topic - topic: function(room_id, args) { + topic: new Command("topic", "", function(room_id, args) { if (args) { return success( MatrixClientPeg.get().setRoomTopic(room_id, args) ); } - return reject("Usage: /topic "); - }, + return reject(this.getUsage()); + }), // Invite a user - invite: function(room_id, args) { + invite: new Command("invite", "", function(room_id, args) { if (args) { var matches = args.match(/^(\S+)$/); if (matches) { @@ -104,11 +128,11 @@ var commands = { ); } } - return reject("Usage: /invite "); - }, + return reject(this.getUsage()); + }), // Join a room - join: function(room_id, args) { + join: new Command("join", "", function(room_id, args) { if (args) { var matches = args.match(/^(\S+)$/); if (matches) { @@ -151,17 +175,17 @@ var commands = { ); } } - return reject("Usage: /join "); - }, + return reject(this.getUsage()); + }), - part: function(room_id, args) { + part: new Command("part", "[#alias:domain]", function(room_id, args) { var targetRoomId; if (args) { var matches = args.match(/^(\S+)$/); if (matches) { var room_alias = matches[1]; if (room_alias[0] !== '#') { - return reject("Usage: /part [#alias:domain]"); + return reject(this.getUsage()); } if (!room_alias.match(/:/)) { var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, ''); @@ -198,10 +222,10 @@ var commands = { dis.dispatch({action: 'view_next_room'}); }) ); - }, + }), // Kick a user from the room with an optional reason - kick: function(room_id, args) { + kick: new Command("kick", " []", function(room_id, args) { if (args) { var matches = args.match(/^(\S+?)( +(.*))?$/); if (matches) { @@ -210,11 +234,11 @@ var commands = { ); } } - return reject("Usage: /kick []"); - }, + return reject(this.getUsage()); + }), // Ban a user from the room with an optional reason - ban: function(room_id, args) { + ban: new Command("ban", " []", function(room_id, args) { if (args) { var matches = args.match(/^(\S+?)( +(.*))?$/); if (matches) { @@ -223,11 +247,11 @@ var commands = { ); } } - return reject("Usage: /ban []"); - }, + return reject(this.getUsage()); + }), // Unban a user from the room - unban: function(room_id, args) { + unban: new Command("unban", "", function(room_id, args) { if (args) { var matches = args.match(/^(\S+)$/); if (matches) { @@ -237,11 +261,11 @@ var commands = { ); } } - return reject("Usage: /unban "); - }, + return reject(this.getUsage()); + }), // Define the power level of a user - op: function(room_id, args) { + op: new Command("op", " []", function(room_id, args) { if (args) { var matches = args.match(/^(\S+?)( +(\d+))?$/); var powerLevel = 50; // default power level for op @@ -266,11 +290,11 @@ var commands = { } } } - return reject("Usage: /op []"); - }, + return reject(this.getUsage()); + }), // Reset the power level of a user - deop: function(room_id, args) { + deop: new Command("deop", "", function(room_id, args) { if (args) { var matches = args.match(/^(\S+)$/); if (matches) { @@ -289,8 +313,8 @@ var commands = { ); } } - return reject("Usage: /deop "); - } + return reject(this.getUsage()); + }) }; // helpful aliases @@ -315,7 +339,7 @@ module.exports = { var args = bits[3]; if (cmd === "me") return null; if (commands[cmd]) { - return commands[cmd](roomId, args); + return commands[cmd].run(roomId, args); } else { return reject("Unrecognised command: " + input); @@ -325,8 +349,8 @@ module.exports = { }, getCommandList: function() { - return Object.keys(commands).map(function(cmd) { - return "/" + cmd; + return Object.keys(commands).map(function(cmdKey) { + return commands[cmdKey]; }); } }; diff --git a/src/TabComplete.js b/src/TabComplete.js index 59f3cec3a0..8886e21af9 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -95,7 +95,7 @@ class TabComplete { */ completeTo(entry) { this.textArea.value = this._replaceWith( - entry.getText(), true, entry.getSuffix(this.isFirstWord) + entry.getFillText(), true, entry.getSuffix(this.isFirstWord) ); this.stopTabCompleting(); // keep focus on the text area @@ -222,7 +222,7 @@ class TabComplete { if (!this.inPassiveMode) { // set textarea to this new value this.textArea.value = this._replaceWith( - this.matchedList[this.currentIndex].getText(), + this.matchedList[this.currentIndex].getFillText(), this.currentIndex !== 0, // don't suffix the original text! this.matchedList[this.currentIndex].getSuffix(this.isFirstWord) ); diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index 79e0a9a46b..9aef7736a8 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -28,6 +28,14 @@ class Entry { return this.text; } + /** + * @return {string} The text to insert into the input box. Most of the time + * this is the same as getText(). + */ + getFillText() { + return this.text; + } + /** * @return {ReactClass} Raw JSX */ @@ -59,12 +67,17 @@ class Entry { } class CommandEntry extends Entry { - constructor(command) { - super(command); + constructor(cmd, cmdWithArgs) { + super(cmdWithArgs); + this.cmd = cmd; + } + + getFillText() { + return this.cmd; } getKey() { - return this.getText(); + return this.getFillText(); } getSuffix(isFirstWord) { @@ -72,9 +85,9 @@ class CommandEntry extends Entry { } } -CommandEntry.fromStrings = function(commandArray) { +CommandEntry.fromCommands = function(commandArray) { return commandArray.map(function(cmd) { - return new CommandEntry(cmd); + return new CommandEntry(cmd.getCommand(), cmd.getCommandWithArgs()); }); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index abfa2e27f1..6f5f9a97a3 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -417,7 +417,7 @@ module.exports = React.createClass({ } this.tabComplete.setCompletionList( MemberEntry.fromMemberList(room.getJoinedMembers()).concat( - CommandEntry.fromStrings(SlashCommands.getCommandList()) + CommandEntry.fromCommands(SlashCommands.getCommandList()) ) ); }, diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index a3ad033acc..930725570b 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -341,7 +341,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText); } - sendMessagePromise.then(function() { + sendMessagePromise.done(function() { dis.dispatch({ action: 'message_sent' }); From 84a7fc16401eea018b74a8ee0ad6452c72bfc394 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 14 Jan 2016 16:29:01 +0000 Subject: [PATCH 4/7] Tweak how command aliases are set This prevents multiple commands of the same name being returned in getCommandList() --- src/SlashCommands.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 83b4b52ffc..938bf062cb 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -318,7 +318,9 @@ var commands = { }; // helpful aliases -commands.j = commands.join; +var aliases = { + j: "join" +} module.exports = { /** @@ -338,6 +340,9 @@ module.exports = { var cmd = bits[1].substring(1).toLowerCase(); var args = bits[3]; if (cmd === "me") return null; + if (aliases[cmd]) { + cmd = aliases[cmd]; + } if (commands[cmd]) { return commands[cmd].run(roomId, args); } From 42dc1be3410ee42466064b44e2f1286d53603757 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 14 Jan 2016 16:29:30 +0000 Subject: [PATCH 5/7] fix descriptions a bit and sort the slash commands when tab-completing --- src/SlashCommands.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 938bf062cb..ca3a010791 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -69,7 +69,7 @@ var commands = { }), // Changes the colorscheme of your current room - tint: new Command("tint", " []", function(room_id, args) { + tint: new Command("tint", " []", function(room_id, args) { if (args) { var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); if (matches) { @@ -89,7 +89,7 @@ var commands = { return reject(this.getUsage()); }), - encrypt: new Command("encrypt", "", function(room_id, args) { + encrypt: new Command("encrypt", "", function(room_id, args) { if (args == "on") { var client = MatrixClientPeg.get(); var members = client.getRoom(room_id).currentState.members; @@ -354,7 +354,7 @@ module.exports = { }, getCommandList: function() { - return Object.keys(commands).map(function(cmdKey) { + return Object.keys(commands).sort().map(function(cmdKey) { return commands[cmdKey]; }); } From 4430e16707e669eb7d29beb629cdc0d74f25ba50 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 14 Jan 2016 16:29:41 +0000 Subject: [PATCH 6/7] apply CSS to slashcommand autocompletes --- src/components/views/rooms/TabCompleteBar.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/TabCompleteBar.js b/src/components/views/rooms/TabCompleteBar.js index c640d6aa5b..ea74706f29 100644 --- a/src/components/views/rooms/TabCompleteBar.js +++ b/src/components/views/rooms/TabCompleteBar.js @@ -18,6 +18,7 @@ limitations under the License. var React = require('react'); var MatrixClientPeg = require("../../../MatrixClientPeg"); +var CommandEntry = require("../../../TabCompleteEntries").CommandEntry; module.exports = React.createClass({ displayName: 'TabCompleteBar', @@ -31,8 +32,9 @@ module.exports = React.createClass({
{this.props.entries.map(function(entry, i) { return ( -
+
{entry.getImageJsx()} {entry.getText()} From 66bc30c0bc28de4c038a933e014ec5002adf9d5d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 14 Jan 2016 17:33:52 +0000 Subject: [PATCH 7/7] Add /me to the list --- src/SlashCommands.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index ca3a010791..4eb2adad5d 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -354,8 +354,12 @@ module.exports = { }, getCommandList: function() { - return Object.keys(commands).sort().map(function(cmdKey) { + // Return all the commands plus /me which isn't handled like normal commands + var cmds = Object.keys(commands).sort().map(function(cmdKey) { return commands[cmdKey]; - }); + }) + cmds.push(new Command("me", "", function(){})); + + return cmds; } };