From a768b7fe4be1a2e112d09e9a225663a37ffc531b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson <matthew@matrix.org> Date: Mon, 18 Jan 2016 00:16:31 +0000 Subject: [PATCH 1/6] bring back power badges --- src/components/views/rooms/MemberTile.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 4752c4d539..3f12add550 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -120,8 +120,18 @@ module.exports = React.createClass({ // var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png"; // power = <img src={ img } className="mx_MemberTile_power" width="44" height="44" alt=""/>; // } - } + var power; + if (this.props.member) { + var powerLevel = this.props.member.powerLevel; + if (powerLevel >= 50 && powerLevel < 99) { + power = <img src="img/mod.svg" className="mx_MemberTile_power" width="16" height="17" alt="Mod"/>; + } + if (powerLevel >= 99) { + power = <img src="img/admin.svg" className="mx_MemberTile_power" width="16" height="17" alt="Admin"/>; + } + } + } var mainClassName = "mx_MemberTile "; mainClassName += presenceClass; @@ -159,6 +169,7 @@ module.exports = React.createClass({ <div className="mx_MemberTile_avatar"> <MemberAvatar member={this.props.member} width={36} height={36} customDisplayName={this.props.customDisplayName} /> + { power } </div> { nameEl } </div> From 430c90f4a4ba1481630aef7bd9d36360acd86b88 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson <matthew@matrix.org> Date: Sun, 17 Jan 2016 23:58:38 +0000 Subject: [PATCH 2/6] oops ,forgot PowerSelector --- .../views/elements/PowerSelector.js | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/components/views/elements/PowerSelector.js diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js new file mode 100644 index 0000000000..ad258a7522 --- /dev/null +++ b/src/components/views/elements/PowerSelector.js @@ -0,0 +1,102 @@ +/* +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 roles = { + 0: 'User', + 50: 'Moderator', + 100: 'Admin', +}; + +var reverseRoles = {}; +Object.keys(roles).forEach(function(key) { + reverseRoles[roles[key]] = key; +}); + +module.exports = React.createClass({ + displayName: 'PowerSelector', + + propTypes: { + value: React.PropTypes.number.isRequired, + disabled: React.PropTypes.bool, + onChange: React.PropTypes.func, + }, + + getInitialState: function() { + return { + custom: (roles[this.props.value] === undefined), + }; + }, + + onSelectChange: function(event) { + this.state.custom = (event.target.value === "Custom"); + this.props.onChange(this.getValue()); + }, + + onCustomChange: function(event) { + this.props.onChange(this.getValue()); + }, + + getValue: function() { + var value; + if (this.refs.select) { + value = reverseRoles[ this.refs.select.value ]; + if (this.refs.custom) { + if (value === undefined) value = parseInt( this.refs.custom.value ); + } + } + return value; + }, + + render: function() { + var customPicker; + if (this.state.custom) { + var input; + if (this.props.disabled) { + input = <span>{ this.props.value }</span> + } + else { + input = <input ref="custom" type="text" size="3" defaultValue={ this.props.value } onChange={ this.onCustomChange } /> + } + customPicker = <span> of { input }</span>; + } + + var selectValue = roles[this.props.value] || "Custom"; + var select; + if (this.props.disabled) { + select = <span>{ selectValue }</span>; + } + else { + select = + <select ref="select" defaultValue={ selectValue } onChange={ this.onSelectChange }> + <option value="User">User (0)</option> + <option value="Moderator">Moderator (50)</option> + <option value="Admin">Admin (100)</option> + <option value="Custom">Custom level</option> + </select> + } + + return ( + <span className="mx_PowerSelector"> + { select } + { customPicker } + </span> + ); + } +}); From 8e1ab8e6b49f0043509637b2498274eb60e0884e Mon Sep 17 00:00:00 2001 From: Matthew Hodgson <matthew@matrix.org> Date: Mon, 18 Jan 2016 01:18:02 +0000 Subject: [PATCH 3/6] vaguely skin MemberInfo correctly --- src/component-index.js | 11 +-- .../views/elements/PowerSelector.js | 10 ++- src/components/views/rooms/MemberInfo.js | 88 ++++++++++++++----- src/components/views/rooms/MemberTile.js | 5 +- 4 files changed, 83 insertions(+), 31 deletions(-) diff --git a/src/component-index.js b/src/component-index.js index 7ae15ba12c..19daeffb9e 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -23,15 +23,15 @@ limitations under the License. module.exports.components = {}; module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom'); -module.exports.components['structures.login.ForgotPassword'] = require('./components/structures/login/ForgotPassword'); -module.exports.components['structures.login.Login'] = require('./components/structures/login/Login'); -module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration'); -module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration'); module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat'); module.exports.components['structures.RoomView'] = require('./components/structures/RoomView'); module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel'); module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar'); module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings'); +module.exports.components['structures.login.ForgotPassword'] = require('./components/structures/login/ForgotPassword'); +module.exports.components['structures.login.Login'] = require('./components/structures/login/Login'); +module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration'); +module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration'); module.exports.components['views.avatars.MemberAvatar'] = require('./components/views/avatars/MemberAvatar'); module.exports.components['views.avatars.RoomAvatar'] = require('./components/views/avatars/RoomAvatar'); module.exports.components['views.create_room.CreateRoomButton'] = require('./components/views/create_room/CreateRoomButton'); @@ -41,6 +41,7 @@ module.exports.components['views.dialogs.ErrorDialog'] = require('./components/v module.exports.components['views.dialogs.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt'); module.exports.components['views.dialogs.QuestionDialog'] = require('./components/views/dialogs/QuestionDialog'); module.exports.components['views.elements.EditableText'] = require('./components/views/elements/EditableText'); +module.exports.components['views.elements.PowerSelector'] = require('./components/views/elements/PowerSelector'); module.exports.components['views.elements.ProgressBar'] = require('./components/views/elements/ProgressBar'); module.exports.components['views.elements.TintableSvg'] = require('./components/views/elements/TintableSvg'); module.exports.components['views.elements.UserSelector'] = require('./components/views/elements/UserSelector'); @@ -52,10 +53,10 @@ module.exports.components['views.login.LoginHeader'] = require('./components/vie module.exports.components['views.login.PasswordLogin'] = require('./components/views/login/PasswordLogin'); module.exports.components['views.login.RegistrationForm'] = require('./components/views/login/RegistrationForm'); module.exports.components['views.login.ServerConfig'] = require('./components/views/login/ServerConfig'); -module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent'); module.exports.components['views.messages.MFileBody'] = require('./components/views/messages/MFileBody'); module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody'); module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody'); +module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent'); module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody'); module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent'); module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody'); diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index ad258a7522..c47c9f3809 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -49,10 +49,16 @@ module.exports = React.createClass({ this.props.onChange(this.getValue()); }, - onCustomChange: function(event) { + onCustomBlur: function(event) { this.props.onChange(this.getValue()); }, + onCustomKeyDown: function(event) { + if (event.key == "Enter") { + this.props.onChange(this.getValue()); + } + }, + getValue: function() { var value; if (this.refs.select) { @@ -72,7 +78,7 @@ module.exports = React.createClass({ input = <span>{ this.props.value }</span> } else { - input = <input ref="custom" type="text" size="3" defaultValue={ this.props.value } onChange={ this.onCustomChange } /> + input = <input ref="custom" type="text" size="3" defaultValue={ this.props.value } onBlur={ this.onCustomBlur } onKeyDown={ this.onCustomKeyDown }/> } customPicker = <span> of { input }</span>; } diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index b5f0b88b40..c08ba38ab0 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -154,6 +154,7 @@ module.exports = React.createClass({ } var defaultLevel = powerLevelEvent.getContent().users_default; var modLevel = me.powerLevel - 1; + if (modLevel > 50 && defaultLevel < 50) modLevel = 50; // try to stick with the vector level defaults // toggle the level var newLevel = this.state.isTargetMod ? defaultLevel : modLevel; MatrixClientPeg.get().setPowerLevel(roomId, target, newLevel, powerLevelEvent).done( @@ -170,6 +171,36 @@ module.exports = React.createClass({ this.props.onFinished(); }, + onPowerChange: function(powerLevel) { + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + var roomId = this.props.member.roomId; + var target = this.props.member.userId; + var room = MatrixClientPeg.get().getRoom(roomId); + if (!room) { + this.props.onFinished(); + return; + } + var powerLevelEvent = room.currentState.getStateEvents( + "m.room.power_levels", "" + ); + if (!powerLevelEvent) { + this.props.onFinished(); + return; + } + MatrixClientPeg.get().setPowerLevel(roomId, target, powerLevel, powerLevelEvent).done( + function() { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Power change success"); + }, function(err) { + Modal.createDialog(ErrorDialog, { + title: "Failure to change power level", + description: err.message + }); + }); + this.props.onFinished(); + }, + onChatClick: function() { // check if there are any existing rooms with just us and them (1:1) // If so, just view that room. If not, create a private room with them. @@ -317,12 +348,11 @@ module.exports = React.createClass({ }, render: function() { - var interactButton, kickButton, banButton, muteButton, giveModButton, spinner; - if (this.props.member.userId === MatrixClientPeg.get().credentials.userId) { - interactButton = <div className="mx_MemberInfo_field" onClick={this.onLeaveClick}>Leave room</div>; - } - else { - interactButton = <div className="mx_MemberInfo_field" onClick={this.onChatClick}>Start chat</div>; + var startChat, kickButton, banButton, muteButton, giveModButton, spinner; + if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) { + // FIXME: we're referring to a vector component from react-sdk + var BottomLeftMenuTile = sdk.getComponent('rooms.BottomLeftMenuTile'); + startChat = <BottomLeftMenuTile collapsed={ false } img="img/create-big.svg" label="Start chat" onClick={ this.onChatClick }/> } if (this.state.creatingRoom) { @@ -347,34 +377,52 @@ module.exports = React.createClass({ </div>; } if (this.state.can.modifyLevel) { - var giveOpLabel = this.state.isTargetMod ? "Revoke Mod" : "Make Mod"; + var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator"; giveModButton = <div className="mx_MemberInfo_field" onClick={this.onModToggle}> {giveOpLabel} </div> } + var adminTools; + if (kickButton || banButton || muteButton || giveModButton) { + adminTools = + <div> + <h3>Admin tools</h3> + + <div className="mx_MemberInfo_buttons"> + {muteButton} + {kickButton} + {banButton} + {giveModButton} + </div> + </div> + } + var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + var PowerSelector = sdk.getComponent('elements.PowerSelector'); return ( <div className="mx_MemberInfo"> <img className="mx_MemberInfo_cancel" src="img/cancel.svg" width="18" height="18" onClick={this.onCancel}/> <div className="mx_MemberInfo_avatar"> <MemberAvatar member={this.props.member} width={48} height={48} /> </div> + <h2>{ this.props.member.name }</h2> - <div className="mx_MemberInfo_profileField"> - { this.props.member.userId } - </div> - <div className="mx_MemberInfo_profileField"> - power: { this.props.member.powerLevelNorm }% - </div> - <div className="mx_MemberInfo_buttons"> - {interactButton} - {muteButton} - {kickButton} - {banButton} - {giveModButton} - {spinner} + + <div className="mx_MemberInfo_profile"> + <div className="mx_MemberInfo_profileField"> + { this.props.member.userId } + </div> + <div className="mx_MemberInfo_profileField"> + Level: <b><PowerSelector value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b> + </div> </div> + + { startChat } + + { adminTools } + + { spinner } </div> ); } diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 3f12add550..f1b64efa42 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -83,10 +83,7 @@ module.exports = React.createClass({ if (!this.props.member) { return this._getDisplayName(); } - var label = this.props.member.userId; - if (this.state.isTargetMod) { - label += " - Mod (" + this.props.member.powerLevelNorm + "%)"; - } + var label = this.props.member.userId + " (power " + this.props.member.powerLevel + ")"; return label; }, From 6e3245a3b0a252fd278a4af92e0a0fea818fc29a Mon Sep 17 00:00:00 2001 From: Matthew Hodgson <matthew@matrix.org> Date: Mon, 18 Jan 2016 01:26:15 +0000 Subject: [PATCH 4/6] make linkify userid jump to memberinfo --- src/components/structures/MatrixChat.js | 10 ++++++++++ src/components/views/rooms/MemberInfo.js | 3 +++ 2 files changed, 13 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index e5af2a86b5..23be8d6587 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -636,6 +636,8 @@ module.exports = React.createClass({ onUserClick: function(event, userId) { event.preventDefault(); + + /* var MemberInfo = sdk.getComponent('rooms.MemberInfo'); var member = new Matrix.RoomMember(null, userId); ContextualMenu.createMenu(MemberInfo, { @@ -643,6 +645,14 @@ module.exports = React.createClass({ right: window.innerWidth - event.pageX, top: event.pageY }); + */ + + var member = new Matrix.RoomMember(null, userId); + if (!member) { return; } + dis.dispatch({ + action: 'view_user', + member: member, + }); }, onLogoutClick: function(event) { diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index c08ba38ab0..d78fc5a005 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -383,6 +383,9 @@ module.exports = React.createClass({ </div> } + // TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet + // e.g. clicking on a linkified userid in a room + var adminTools; if (kickButton || banButton || muteButton || giveModButton) { adminTools = From 8f83621c4cb4ae7decf4c64087cf1ca20d3e9b70 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson <matthew@matrix.org> Date: Mon, 18 Jan 2016 01:40:19 +0000 Subject: [PATCH 5/6] hide Mod button for muted users --- src/components/views/rooms/MemberInfo.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index d78fc5a005..b601798f3e 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -322,9 +322,15 @@ module.exports = React.createClass({ (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || powerLevels.state_default ); + var levelToSend = ( + (powerLevels.events ? powerLevels.events["m.room.message"] : null) || + powerLevels.events_default + ); + can.kick = me.powerLevel >= powerLevels.kick; can.ban = me.powerLevel >= powerLevels.ban; can.mute = me.powerLevel >= editPowerLevel; + can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend; can.modifyLevel = me.powerLevel > them.powerLevel; return can; }, @@ -376,7 +382,7 @@ module.exports = React.createClass({ {muteLabel} </div>; } - if (this.state.can.modifyLevel) { + if (this.state.can.toggleMod) { var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator"; giveModButton = <div className="mx_MemberInfo_field" onClick={this.onModToggle}> {giveOpLabel} From aa412bed0f7303c8ca6cbe79980cf78a19da5591 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson <matthew@matrix.org> Date: Mon, 18 Jan 2016 13:38:40 +0000 Subject: [PATCH 6/6] reindent promises as per dave's PR feedback --- src/components/views/rooms/MemberInfo.js | 134 ++++++++++++----------- 1 file changed, 71 insertions(+), 63 deletions(-) diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index b601798f3e..a8a601c2d6 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -58,15 +58,16 @@ module.exports = React.createClass({ var roomId = this.props.member.roomId; var target = this.props.member.userId; MatrixClientPeg.get().kick(roomId, target).done(function() { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Kick success"); - }, function(err) { - Modal.createDialog(ErrorDialog, { - title: "Kick error", - description: err.message - }); - }); + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Kick success"); + }, function(err) { + Modal.createDialog(ErrorDialog, { + title: "Kick error", + description: err.message + }); + } + ); this.props.onFinished(); }, @@ -74,16 +75,18 @@ module.exports = React.createClass({ var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var roomId = this.props.member.roomId; var target = this.props.member.userId; - MatrixClientPeg.get().ban(roomId, target).done(function() { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Ban success"); - }, function(err) { - Modal.createDialog(ErrorDialog, { - title: "Ban error", - description: err.message - }); - }); + MatrixClientPeg.get().ban(roomId, target).done( + function() { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Ban success"); + }, function(err) { + Modal.createDialog(ErrorDialog, { + title: "Ban error", + description: err.message + }); + } + ); this.props.onFinished(); }, @@ -118,16 +121,17 @@ module.exports = React.createClass({ } MatrixClientPeg.get().setPowerLevel(roomId, target, level, powerLevelEvent).done( - function() { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Mute toggle success"); - }, function(err) { - Modal.createDialog(ErrorDialog, { - title: "Mute error", - description: err.message - }); - }); + function() { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Mute toggle success"); + }, function(err) { + Modal.createDialog(ErrorDialog, { + title: "Mute error", + description: err.message + }); + } + ); this.props.onFinished(); }, @@ -158,16 +162,17 @@ module.exports = React.createClass({ // toggle the level var newLevel = this.state.isTargetMod ? defaultLevel : modLevel; MatrixClientPeg.get().setPowerLevel(roomId, target, newLevel, powerLevelEvent).done( - function() { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Mod toggle success"); - }, function(err) { - Modal.createDialog(ErrorDialog, { - title: "Mod error", - description: err.message - }); - }); + function() { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Mod toggle success"); + }, function(err) { + Modal.createDialog(ErrorDialog, { + title: "Mod error", + description: err.message + }); + } + ); this.props.onFinished(); }, @@ -188,16 +193,17 @@ module.exports = React.createClass({ return; } MatrixClientPeg.get().setPowerLevel(roomId, target, powerLevel, powerLevelEvent).done( - function() { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Power change success"); - }, function(err) { - Modal.createDialog(ErrorDialog, { - title: "Failure to change power level", - description: err.message - }); - }); + function() { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Power change success"); + }, function(err) { + Modal.createDialog(ErrorDialog, { + title: "Failure to change power level", + description: err.message + }); + } + ); this.props.onFinished(); }, @@ -240,20 +246,22 @@ module.exports = React.createClass({ MatrixClientPeg.get().createRoom({ invite: [this.props.member.userId], preset: "private_chat" - }).done(function(res) { - self.setState({ creatingRoom: false }); - dis.dispatch({ - action: 'view_room', - room_id: res.room_id - }); - self.props.onFinished(); - }, function(err) { - self.setState({ creatingRoom: false }); - console.error( - "Failed to create room: %s", JSON.stringify(err) - ); - self.props.onFinished(); - }); + }).done( + function(res) { + self.setState({ creatingRoom: false }); + dis.dispatch({ + action: 'view_room', + room_id: res.room_id + }); + self.props.onFinished(); + }, function(err) { + self.setState({ creatingRoom: false }); + console.error( + "Failed to create room: %s", JSON.stringify(err) + ); + self.props.onFinished(); + } + ); } },