diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 1dd7ecb08f..4eb2adad5d 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,12 +313,14 @@ var commands = { ); } } - return reject("Usage: /deop "); - } + return reject(this.getUsage()); + }) }; // helpful aliases -commands.j = commands.join; +var aliases = { + j: "join" +} module.exports = { /** @@ -314,13 +340,26 @@ 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](roomId, args); + return commands[cmd].run(roomId, args); } else { return reject("Unrecognised command: " + input); } } return null; // not a command + }, + + getCommandList: function() { + // 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; } }; diff --git a/src/TabComplete.js b/src/TabComplete.js index 6690802d5d..8886e21af9 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; @@ -58,7 +56,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 +91,12 @@ 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.getFillText(), true, entry.getSuffix(this.isFirstWord) + ); 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].getFillText(), + this.currentIndex !== 0, // don't suffix the original text! + this.matchedList[this.currentIndex].getSuffix(this.isFirstWord) ); } @@ -243,7 +244,7 @@ class TabComplete { } } - _replaceWith(newVal, includeSuffix) { + _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 @@ -258,13 +259,12 @@ class TabComplete { boundaryChar = ""; } - var replacementText = ( - boundaryChar + newVal + ( - includeSuffix ? - (this.isFirstWord ? this.opts.startingWordSuffix : this.opts.wordSuffix) : - "" - ) - ); + suffix = suffix || ""; + if (!includeSuffix) { + suffix = ""; + } + + 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..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 */ @@ -42,6 +50,14 @@ class Entry { return null; } + /** + * @return {?string} The suffix to append to the tab-complete, or null to + * not do this. + */ + getSuffix(isFirstWord) { + return null; + } + /** * Called when this entry is clicked. */ @@ -50,6 +66,31 @@ class Entry { } } +class CommandEntry extends Entry { + constructor(cmd, cmdWithArgs) { + super(cmdWithArgs); + this.cmd = cmd; + } + + getFillText() { + return this.cmd; + } + + getKey() { + return this.getFillText(); + } + + getSuffix(isFirstWord) { + return " "; // force a space after the command. + } +} + +CommandEntry.fromCommands = function(commandArray) { + return commandArray.map(function(cmd) { + return new CommandEntry(cmd.getCommand(), cmd.getCommandWithArgs()); + }); +} + class MemberEntry extends Entry { constructor(member) { super(member.name || member.userId); @@ -66,6 +107,10 @@ class MemberEntry extends Entry { getKey() { return this.member.userId; } + + getSuffix(isFirstWord) { + return isFirstWord ? ": " : " "; + } } MemberEntry.fromMemberList = function(members) { @@ -99,3 +144,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 842bb1273d..d0bfdf77a8 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"); @@ -94,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, @@ -422,7 +422,9 @@ module.exports = React.createClass({ return; } this.tabComplete.setCompletionList( - MemberEntry.fromMemberList(room.getJoinedMembers()) + MemberEntry.fromMemberList(room.getJoinedMembers()).concat( + 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' }); 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()}