From c0e5d1d13b7d086e0202a33739d53675928cefa1 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 13 Jan 2016 15:55:28 +0000 Subject: [PATCH 1/4] Factor out presence text. Do prep work for displaying 3pid invites on memberlist. Factored out presence to PresenceLabel. --- src/component-index.js | 1 + src/components/views/rooms/MemberList.js | 30 +++++++- src/components/views/rooms/MemberTile.js | 51 +++---------- src/components/views/rooms/PresenceLabel.js | 84 +++++++++++++++++++++ 4 files changed, 125 insertions(+), 41 deletions(-) create mode 100644 src/components/views/rooms/PresenceLabel.js diff --git a/src/component-index.js b/src/component-index.js index 9fe15adfc6..7ae15ba12c 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -64,6 +64,7 @@ module.exports.components['views.rooms.MemberInfo'] = require('./components/view module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList'); module.exports.components['views.rooms.MemberTile'] = require('./components/views/rooms/MemberTile'); module.exports.components['views.rooms.MessageComposer'] = require('./components/views/rooms/MessageComposer'); +module.exports.components['views.rooms.PresenceLabel'] = require('./components/views/rooms/PresenceLabel'); module.exports.components['views.rooms.RoomHeader'] = require('./components/views/rooms/RoomHeader'); module.exports.components['views.rooms.RoomList'] = require('./components/views/rooms/RoomList'); module.exports.components['views.rooms.RoomPreviewBar'] = require('./components/views/rooms/RoomPreviewBar'); diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index b0e2baa3d3..f4d017733d 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -229,7 +229,8 @@ module.exports = React.createClass({ var MemberTile = sdk.getComponent("rooms.MemberTile"); var self = this; - return self.state.members.filter(function(userId) { + + var memberList = self.state.members.filter(function(userId) { var m = self.memberDict[userId]; return m.membership == membership; }).map(function(userId) { @@ -238,6 +239,33 @@ module.exports = React.createClass({ ); }); + + if (membership === "invite") { + // include 3pid invites (m.room.third_party_invite) state events. + // The HS may have already converted these into m.room.member invites so + // we shouldn't add them if the 3pid invite state key (token) is in the + // member invite (content.third_party_invite.signed.token) + var room = MatrixClientPeg.get().getRoom(this.props.roomId); + if (room) { + room.currentState.getStateEvents("m.room.third_party_invite").forEach( + function(e) { + // discard all invites which have a m.room.member event since we've + // already added them. + var memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey()); + if (memberEvent) { + console.log("Found match => %s", memberEvent.getStateKey()); + return; + } + console.log("Display match => "); + /* + memberList.push( + + ) */ + }) + } + } + + return memberList; }, onPopulateInvite: function(e) { diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 32cc619f13..0a32441a00 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -26,6 +26,11 @@ var Modal = require("../../../Modal"); module.exports = React.createClass({ displayName: 'MemberTile', + propTypes: { + member: React.PropTypes.any.isRequired, // RoomMember + onFinished: React.PropTypes.func + }, + getInitialState: function() { return {}; }, @@ -71,37 +76,6 @@ module.exports = React.createClass({ }); }, - getDuration: function(time) { - if (!time) return; - var t = parseInt(time / 1000); - var s = t % 60; - var m = parseInt(t / 60) % 60; - var h = parseInt(t / (60 * 60)) % 24; - var d = parseInt(t / (60 * 60 * 24)); - if (t < 60) { - if (t < 0) { - return "0s"; - } - return s + "s"; - } - if (t < 60 * 60) { - return m + "m"; - } - if (t < 24 * 60 * 60) { - return h + "h"; - } - return d + "d "; - }, - - getPrettyPresence: function(user) { - if (!user) return "Unknown"; - var presence = user.presence; - if (presence === "online") return "Online"; - if (presence === "unavailable") return "Idle"; // XXX: is this actually right? - if (presence === "offline") return "Offline"; - return "Unknown"; - }, - getPowerLabel: function() { var label = this.props.member.userId; if (this.state.isTargetMod) { @@ -111,6 +85,8 @@ module.exports = React.createClass({ }, render: function() { + var PresenceLabel = sdk.getComponent("rooms.PresenceLabel"); + this.member_last_modified_time = this.props.member.getLastModifiedTime(); if (this.props.member.user) { this.user_last_modified_time = this.props.member.user.getLastModifiedTime(); @@ -144,22 +120,17 @@ module.exports = React.createClass({ var nameEl; if (this.state.hover) { - var presence; // FIXME: make presence data update whenever User.presence changes... var active = this.props.member.user ? ((Date.now() - (this.props.member.user.lastPresenceTs - this.props.member.user.lastActiveAgo)) || -1) : -1; - if (active >= 0) { - presence =
{ this.getPrettyPresence(this.props.member.user) } { this.getDuration(active) } ago
; - } - else { - presence =
{ this.getPrettyPresence(this.props.member.user) }
; - } - nameEl = + nameEl = (
{ name }
- { presence } +
+ ); } else { nameEl = diff --git a/src/components/views/rooms/PresenceLabel.js b/src/components/views/rooms/PresenceLabel.js new file mode 100644 index 0000000000..4ecad5b3df --- /dev/null +++ b/src/components/views/rooms/PresenceLabel.js @@ -0,0 +1,84 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +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. +*/ + +'use strict'; + +var React = require('react'); + +var MatrixClientPeg = require('../../../MatrixClientPeg'); +var sdk = require('../../../index'); + +module.exports = React.createClass({ + displayName: 'PresenceLabel', + + propTypes: { + activeAgo: React.PropTypes.number, + presenceState: React.PropTypes.string + }, + + getDefaultProps: function() { + return { + ago: -1, + presenceState: null + }; + }, + + getDuration: function(time) { + if (!time) return; + var t = parseInt(time / 1000); + var s = t % 60; + var m = parseInt(t / 60) % 60; + var h = parseInt(t / (60 * 60)) % 24; + var d = parseInt(t / (60 * 60 * 24)); + if (t < 60) { + if (t < 0) { + return "0s"; + } + return s + "s"; + } + if (t < 60 * 60) { + return m + "m"; + } + if (t < 24 * 60 * 60) { + return h + "h"; + } + return d + "d "; + }, + + getPrettyPresence: function(presence) { + if (presence === "online") return "Online"; + if (presence === "unavailable") return "Idle"; // XXX: is this actually right? + if (presence === "offline") return "Offline"; + return "Unknown"; + }, + + render: function() { + if (this.props.activeAgo >= 0) { + return ( +
+ { this.getPrettyPresence(this.props.presenceState) } { this.getDuration(this.props.activeAgo) } ago +
+ ); + } + else { + return ( +
+ { this.getPrettyPresence(this.props.presenceState) } +
+ ); + } + } +}); From 8c9352c484ef8e367dce0837686c492bb61acc51 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 13 Jan 2016 16:55:28 +0000 Subject: [PATCH 2/4] Make MemberAvatar and MemberTile work without RoomMember objects --- src/components/views/avatars/MemberAvatar.js | 71 ++++++++------- src/components/views/rooms/MemberList.js | 9 +- src/components/views/rooms/MemberTile.js | 96 +++++++++++--------- 3 files changed, 98 insertions(+), 78 deletions(-) diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js index 21c717aac5..f209006b1c 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.js @@ -24,10 +24,16 @@ module.exports = React.createClass({ displayName: 'MemberAvatar', propTypes: { - member: React.PropTypes.object.isRequired, + member: React.PropTypes.object, width: React.PropTypes.number, height: React.PropTypes.number, resizeMethod: React.PropTypes.string, + /** + * The custom display name to use for this member. This can serve as a + * drop in replacement for RoomMember objects, or as a clobber name on + * an existing RoomMember. Used for 3pid invites. + */ + customDisplayName: React.PropTypes.string }, getDefaultProps: function() { @@ -38,64 +44,68 @@ module.exports = React.createClass({ } }, + getInitialState: function() { + var defaultImageUrl = Avatar.defaultAvatarUrlForString( + this.props.customDisplayName || this.props.member.userId + ) + return { + imageUrl: this._getMemberImageUrl() || defaultImageUrl, + defaultImageUrl: defaultImageUrl + }; + }, + componentWillReceiveProps: function(nextProps) { this.refreshUrl(); }, - defaultAvatarUrl: function(member, width, height, resizeMethod) { - return Avatar.defaultAvatarUrlForString(member.userId); - }, - onError: function(ev) { // don't tightloop if the browser can't load a data url - if (ev.target.src == this.defaultAvatarUrl(this.props.member)) { + if (ev.target.src == this.state.defaultImageUrl) { return; } this.setState({ - imageUrl: this.defaultAvatarUrl(this.props.member) + imageUrl: this.state.defaultImageUrl }); }, - _computeUrl: function() { + _getMemberImageUrl: function() { + if (!this.props.member) { return null; } + return Avatar.avatarUrlForMember(this.props.member, this.props.width, this.props.height, this.props.resizeMethod); }, + _getInitialLetter: function() { + var name = this.props.customDisplayName || this.props.member.name; + var initial = name[0]; + if (initial === '@' && name[1]) { + initial = name[1]; + } + return initial.toUpperCase(); + }, + refreshUrl: function() { - var newUrl = this._computeUrl(); + var newUrl = this._getMemberImageUrl(); if (newUrl != this.currentUrl) { this.currentUrl = newUrl; this.setState({imageUrl: newUrl}); } }, - getInitialState: function() { - return { - imageUrl: this._computeUrl() - }; - }, - - - /////////////// - render: function() { - // XXX: recalculates default avatar url constantly - if (this.state.imageUrl === this.defaultAvatarUrl(this.props.member)) { - var initial; - if (this.props.member.name[0]) - initial = this.props.member.name[0].toUpperCase(); - if (initial === '@' && this.props.member.name[1]) - initial = this.props.member.name[1].toUpperCase(); - + var name = this.props.customDisplayName || this.props.member.name; + + if (this.state.imageUrl === this.state.defaultImageUrl) { + var initialLetter = this._getInitialLetter(); return ( - { initialLetter } + ); @@ -104,9 +114,8 @@ module.exports = React.createClass({ + title={name} + {...this.props} /> ); } }); diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index f4d017733d..eac5466e88 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -15,6 +15,7 @@ limitations under the License. */ var React = require('react'); var classNames = require('classnames'); +var Matrix = require("matrix-js-sdk"); var MatrixClientPeg = require("../../../MatrixClientPeg"); var Modal = require("../../../Modal"); var sdk = require('../../../index'); @@ -253,14 +254,12 @@ module.exports = React.createClass({ // already added them. var memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey()); if (memberEvent) { - console.log("Found match => %s", memberEvent.getStateKey()); return; } - console.log("Display match => "); - /* memberList.push( - - ) */ + + ) }) } } diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 0a32441a00..4752c4d539 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -27,24 +27,18 @@ module.exports = React.createClass({ displayName: 'MemberTile', propTypes: { - member: React.PropTypes.any.isRequired, // RoomMember - onFinished: React.PropTypes.func + member: React.PropTypes.any, // RoomMember + onFinished: React.PropTypes.func, + customDisplayName: React.PropTypes.string // for 3pid invites }, getInitialState: function() { return {}; }, - onLeaveClick: function() { - dis.dispatch({ - action: 'leave_room', - room_id: this.props.member.roomId, - }); - this.props.onFinished(); - }, - shouldComponentUpdate: function(nextProps, nextState) { if (this.state.hover !== nextState.hover) return true; + if (!this.props.member) { return false; } // e.g. 3pid members if ( this.member_last_modified_time === undefined || this.member_last_modified_time < nextProps.member.getLastModifiedTime() @@ -70,13 +64,25 @@ module.exports = React.createClass({ }, onClick: function(e) { + if (!this.props.member) { return; } // e.g. 3pid members + dis.dispatch({ action: 'view_user', member: this.props.member, }); }, + _getDisplayName: function() { + if (this.props.customDisplayName) { + return this.props.customDisplayName; + } + return this.props.member.name; + }, + getPowerLabel: function() { + if (!this.props.member) { + return this._getDisplayName(); + } var label = this.props.member.userId; if (this.state.isTargetMod) { label += " - Mod (" + this.props.member.powerLevelNorm + "%)"; @@ -85,68 +91,74 @@ module.exports = React.createClass({ }, render: function() { - var PresenceLabel = sdk.getComponent("rooms.PresenceLabel"); - - this.member_last_modified_time = this.props.member.getLastModifiedTime(); - if (this.props.member.user) { - this.user_last_modified_time = this.props.member.user.getLastModifiedTime(); - } - - var isMyUser = MatrixClientPeg.get().credentials.userId == this.props.member.userId; - - var power; - // if (this.props.member && this.props.member.powerLevelNorm > 0) { - // var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png"; - // power = ; - // } + var member = this.props.member; + var isMyUser = false; + var name = this._getDisplayName(); + var active = -1; var presenceClass = "mx_MemberTile_offline"; - var mainClassName = "mx_MemberTile "; - if (this.props.member.user) { - if (this.props.member.user.presence === "online") { - presenceClass = "mx_MemberTile_online"; - } - else if (this.props.member.user.presence === "unavailable") { - presenceClass = "mx_MemberTile_unavailable"; + + if (member) { + if (member.user) { + this.user_last_modified_time = member.user.getLastModifiedTime(); + + // FIXME: make presence data update whenever User.presence changes... + active = ( + (Date.now() - (member.user.lastPresenceTs - member.user.lastActiveAgo)) || -1 + ); + + if (member.user.presence === "online") { + presenceClass = "mx_MemberTile_online"; + } + else if (member.user.presence === "unavailable") { + presenceClass = "mx_MemberTile_unavailable"; + } } + this.member_last_modified_time = member.getLastModifiedTime(); + isMyUser = MatrixClientPeg.get().credentials.userId == member.userId; + + // if (this.props.member && this.props.member.powerLevelNorm > 0) { + // var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png"; + // power = ; + // } } + + + var mainClassName = "mx_MemberTile "; mainClassName += presenceClass; if (this.state.hover) { mainClassName += " mx_MemberTile_hover"; } - var name = this.props.member.name; - // if (isMyUser) name += " (me)"; // this does nothing other than introduce line wrapping and pain - //var leave = isMyUser ? : null; - var nameEl; if (this.state.hover) { - // FIXME: make presence data update whenever User.presence changes... - var active = this.props.member.user ? ((Date.now() - (this.props.member.user.lastPresenceTs - this.props.member.user.lastActiveAgo)) || -1) : -1; - + var presenceState = (member && member.user) ? member.user.presence : null; + var PresenceLabel = sdk.getComponent("rooms.PresenceLabel"); nameEl = (
{ name }
+ presenceState={presenceState} />
); } else { - nameEl = + nameEl = (
{ name }
+ ); } var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + return (
- - { power } +
{ nameEl }
From 864d10f4124fd62bd462c24f88e8cdf078dc0522 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 14 Jan 2016 11:39:24 +0000 Subject: [PATCH 3/4] 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 4/4] 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' });