From 2e2cecdd4fc72b56614d7548293cd8dda810b3ff Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 21 Jul 2015 11:26:02 +0100 Subject: [PATCH 1/4] Implement kick/ban/[un]mute buttons on member list dialogs. --- skins/base/views/molecules/MemberInfo.js | 20 +++ src/controllers/molecules/MemberInfo.js | 182 ++++++++++++++++++++++- 2 files changed, 196 insertions(+), 6 deletions(-) diff --git a/skins/base/views/molecules/MemberInfo.js b/skins/base/views/molecules/MemberInfo.js index 3a81185ba9..036a3fd62d 100644 --- a/skins/base/views/molecules/MemberInfo.js +++ b/skins/base/views/molecules/MemberInfo.js @@ -67,6 +67,23 @@ module.exports = React.createClass({ if (this.state.active >= 0) { activeAgo = this.getDuration(this.state.active); } + var kickButton, banButton, muteButton; + if (this.state.can.kick) { + kickButton =
+ Kick +
; + } + if (this.state.can.ban) { + banButton =
+ Ban +
; + } + if (this.state.can.mute) { + var muteLabel = this.state.muted ? "Unmute" : "Mute"; + muteButton =
+ {muteLabel} +
; + } return (
@@ -81,6 +98,9 @@ module.exports = React.createClass({
Presence: {this.state.presence}
Last active: {activeAgo}
Start chat
+ {muteButton} + {kickButton} + {banButton}
); } diff --git a/src/controllers/molecules/MemberInfo.js b/src/controllers/molecules/MemberInfo.js index 96f8bb8ca8..fd04df4d55 100644 --- a/src/controllers/molecules/MemberInfo.js +++ b/src/controllers/molecules/MemberInfo.js @@ -18,15 +18,25 @@ limitations under the License. * State vars: * 'presence' : string (online|offline|unavailable etc) * 'active' : number (ms ago; can be -1) + * 'can': { + * kick: boolean, + * ban: boolean, + * mute: boolean + * }, + * 'muted': boolean */ 'use strict'; var MatrixClientPeg = require("../../MatrixClientPeg"); var dis = require("../../dispatcher"); +var Modal = require("../../Modal"); +var ComponentBroker = require('../../ComponentBroker'); +var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog"); module.exports = { componentDidMount: function() { var self = this; + // listen for presence changes function updateUserState(event, user) { if (!self.props.member) { return; } @@ -40,20 +50,116 @@ module.exports = { MatrixClientPeg.get().on("User.presence", updateUserState); this.userPresenceFn = updateUserState; - if (this.props.member) { - var usr = MatrixClientPeg.get().getUser(this.props.member.userId); - if (!usr) { + // listen for power level changes + function updatePowerLevel(event, member) { + if (!self.props.member) { return; } + + if (member.roomId !== self.props.member.roomId) { return; } + // only interested in changes to us or them + var myUserId = MatrixClientPeg.get().credentials.userId; + if ([myUserId, self.props.member.userId].indexOf(member.userId) === -1) { + return; + } + self.setState({ + can: self._calculateOpsPermissions(), + muted: self._isMuted(self.props.member) + }); + } + MatrixClientPeg.get().on("RoomMember.powerLevel", updatePowerLevel); + this.updatePowerLevelFn = updatePowerLevel; + + // work out the current state + if (this.props.member) { + var usr = MatrixClientPeg.get().getUser(this.props.member.userId) || {}; this.setState({ - presence: usr.presence, - active: usr.lastActiveAgo + presence: usr.presence || "offline", + active: usr.lastActiveAgo || -1, + can: this._calculateOpsPermissions(), + muted: this._isMuted(this.props.member) }); } }, componentWillUnmount: function() { MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn); + MatrixClientPeg.get().removeListener( + "RoomMember.powerLevel", this.updatePowerLevelFn + ); + }, + + onKick: function() { + var roomId = this.props.member.roomId; + var target = this.props.member.userId; + var self = this; + 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 + }); + }); + }, + + onBan: function() { + var roomId = this.props.member.roomId; + var target = this.props.member.userId; + var self = this; + 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 + }); + }); + }, + + onMuteToggle: function() { + var roomId = this.props.member.roomId; + var target = this.props.member.userId; + var self = this; + var room = MatrixClientPeg.get().getRoom(roomId); + if (!room) { + return; + } + var powerLevelEvent = room.currentState.getStateEvents( + "m.room.power_levels", "" + ); + if (!powerLevelEvent) { + return; + } + var isMuted = this.state.muted; + var powerLevels = powerLevelEvent.getContent(); + var levelToSend = ( + (powerLevels.events ? powerLevels.events["m.room.message"] : null) || + powerLevels.events_default + ); + var level; + if (isMuted) { // unmute + level = levelToSend; + } + else { // mute + level = levelToSend - 1; + } + + 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 + }); + }); }, onChatClick: function() { @@ -108,8 +214,72 @@ module.exports = { getInitialState: function() { return { presence: "offline", - active: -1 + active: -1, + can: { + kick: false, + ban: false, + mute: false + }, + muted: false } + }, + + _isMuted: function(member) { + var room = MatrixClientPeg.get().getRoom(member.roomId); + if (!room) { + return false; + } + var powerLevels = room.currentState.getStateEvents( + "m.room.power_levels", "" + ); + if (!powerLevels) { + return false; + } + powerLevels = powerLevels.getContent(); + var levelToSend = ( + (powerLevels.events ? powerLevels.events["m.room.message"] : null) || + powerLevels.events_default + ); + return member.powerLevel < levelToSend; + }, + + _calculateOpsPermissions: function() { + var can = { + kick: false, + ban: false, + mute: false + }; + var them = this.props.member; + var room = MatrixClientPeg.get().getRoom(this.props.member.roomId); + if (!room) { + console.error("No room found"); + return can; + } + var myUserId = MatrixClientPeg.get().credentials.userId; + var me = room.getMember(myUserId); + var powerLevels = room.currentState.getStateEvents( + "m.room.power_levels", "" + ); + if (powerLevels) { + powerLevels = powerLevels.getContent(); + } + else { + console.log("No power level event found in %s", room.roomId); + return can; // no power level event, don't allow anything. + } + var canAffectUser = them.powerLevel < me.powerLevel; + if (!canAffectUser) { + console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel); + return can; + } + var editPowerLevel = ( + (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || + powerLevels.state_default + ); + can.kick = me.powerLevel >= powerLevels.kick; + can.ban = me.powerLevel >= powerLevels.ban; + can.mute = me.powerLevel >= editPowerLevel; + return can; } }; From 13f04f77dc542dd3522022d6e889f31a4276d727 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 21 Jul 2015 11:43:18 +0100 Subject: [PATCH 2/4] Tidy up power level logic. Add 'Moderator' label to members. --- skins/base/views/molecules/MemberInfo.js | 6 +- src/controllers/molecules/MemberInfo.js | 79 ++++++++++++------------ 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/skins/base/views/molecules/MemberInfo.js b/skins/base/views/molecules/MemberInfo.js index 036a3fd62d..45db0c308e 100644 --- a/skins/base/views/molecules/MemberInfo.js +++ b/skins/base/views/molecules/MemberInfo.js @@ -84,7 +84,10 @@ module.exports = React.createClass({ {muteLabel} ; } - + var opLabel; + if (this.state.isTargetOp) { + opLabel =
Moderator
+ } return (
@@ -95,6 +98,7 @@ module.exports = React.createClass({ width="128" height="128" alt=""/>
{this.props.member.userId}
+ {opLabel}
Presence: {this.state.presence}
Last active: {activeAgo}
Start chat
diff --git a/src/controllers/molecules/MemberInfo.js b/src/controllers/molecules/MemberInfo.js index fd04df4d55..d335eec5f6 100644 --- a/src/controllers/molecules/MemberInfo.js +++ b/src/controllers/molecules/MemberInfo.js @@ -23,7 +23,8 @@ limitations under the License. * ban: boolean, * mute: boolean * }, - * 'muted': boolean + * 'muted': boolean, + * 'isTargetOp': boolean */ 'use strict'; @@ -62,10 +63,7 @@ module.exports = { if ([myUserId, self.props.member.userId].indexOf(member.userId) === -1) { return; } - self.setState({ - can: self._calculateOpsPermissions(), - muted: self._isMuted(self.props.member) - }); + self.setState(self._calculateOpsPermissions()); } MatrixClientPeg.get().on("RoomMember.powerLevel", updatePowerLevel); this.updatePowerLevelFn = updatePowerLevel; @@ -73,12 +71,10 @@ module.exports = { // work out the current state if (this.props.member) { var usr = MatrixClientPeg.get().getUser(this.props.member.userId) || {}; - this.setState({ - presence: usr.presence || "offline", - active: usr.lastActiveAgo || -1, - can: this._calculateOpsPermissions(), - muted: this._isMuted(this.props.member) - }); + var memberState = this._calculateOpsPermissions(); + memberState.presence = usr.presence || "offline"; + memberState.active = usr.lastActiveAgo || -1; + this.setState(memberState); } }, @@ -220,53 +216,43 @@ module.exports = { ban: false, mute: false }, - muted: false + muted: false, + isTargetOp: false } }, - _isMuted: function(member) { - var room = MatrixClientPeg.get().getRoom(member.roomId); + _calculateOpsPermissions: function() { + var defaultPerms = { + can: {}, + muted: false + }; + var room = MatrixClientPeg.get().getRoom(this.props.member.roomId); if (!room) { - return false; + return defaultPerms; } var powerLevels = room.currentState.getStateEvents( "m.room.power_levels", "" ); if (!powerLevels) { - return false; + return defaultPerms; } - powerLevels = powerLevels.getContent(); - var levelToSend = ( - (powerLevels.events ? powerLevels.events["m.room.message"] : null) || - powerLevels.events_default - ); - return member.powerLevel < levelToSend; + var me = room.getMember(MatrixClientPeg.get().credentials.userId); + var them = this.props.member; + return { + can: this._calculateCanPermissions( + me, them, powerLevels.getContent() + ), + muted: this._isMuted(them, powerLevels.getContent()), + isTargetOp: them.powerLevel >= me.powerLevel && them.powerLevel > 0 + }; }, - _calculateOpsPermissions: function() { + _calculateCanPermissions: function(me, them, powerLevels) { var can = { kick: false, ban: false, mute: false }; - var them = this.props.member; - var room = MatrixClientPeg.get().getRoom(this.props.member.roomId); - if (!room) { - console.error("No room found"); - return can; - } - var myUserId = MatrixClientPeg.get().credentials.userId; - var me = room.getMember(myUserId); - var powerLevels = room.currentState.getStateEvents( - "m.room.power_levels", "" - ); - if (powerLevels) { - powerLevels = powerLevels.getContent(); - } - else { - console.log("No power level event found in %s", room.roomId); - return can; // no power level event, don't allow anything. - } var canAffectUser = them.powerLevel < me.powerLevel; if (!canAffectUser) { console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel); @@ -280,6 +266,17 @@ module.exports = { can.ban = me.powerLevel >= powerLevels.ban; can.mute = me.powerLevel >= editPowerLevel; return can; + }, + + _isMuted: function(member, powerLevelContent) { + if (!powerLevelContent || !member) { + return false; + } + var levelToSend = ( + (powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) || + powerLevelContent.events_default + ); + return member.powerLevel < levelToSend; } }; From 2454a71b38857861e43b88aa0eaab40871a41397 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 21 Jul 2015 13:24:59 +0100 Subject: [PATCH 3/4] Implement create/remove mod button. --- skins/base/views/molecules/MemberInfo.js | 15 +++++-- src/controllers/molecules/MemberInfo.js | 53 ++++++++++++++++++++---- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/skins/base/views/molecules/MemberInfo.js b/skins/base/views/molecules/MemberInfo.js index 45db0c308e..8e1e383f69 100644 --- a/skins/base/views/molecules/MemberInfo.js +++ b/skins/base/views/molecules/MemberInfo.js @@ -67,7 +67,7 @@ module.exports = React.createClass({ if (this.state.active >= 0) { activeAgo = this.getDuration(this.state.active); } - var kickButton, banButton, muteButton; + var kickButton, banButton, muteButton, giveModButton; if (this.state.can.kick) { kickButton =
Kick @@ -84,9 +84,17 @@ module.exports = React.createClass({ {muteLabel}
; } + if (this.state.can.modifyLevel) { + var giveOpLabel = this.state.isTargetMod ? "Revoke Mod" : "Make Mod"; + giveModButton =
+ {giveOpLabel} +
+ } + var opLabel; - if (this.state.isTargetOp) { - opLabel =
Moderator
+ if (this.state.isTargetMod) { + var level = this.props.member.powerLevelNorm + "%"; + opLabel =
Moderator ({level})
} return (
@@ -105,6 +113,7 @@ module.exports = React.createClass({ {muteButton} {kickButton} {banButton} + {giveModButton}
); } diff --git a/src/controllers/molecules/MemberInfo.js b/src/controllers/molecules/MemberInfo.js index d335eec5f6..4f222c5dbf 100644 --- a/src/controllers/molecules/MemberInfo.js +++ b/src/controllers/molecules/MemberInfo.js @@ -21,10 +21,11 @@ limitations under the License. * 'can': { * kick: boolean, * ban: boolean, - * mute: boolean + * mute: boolean, + * modifyLevel: boolean * }, * 'muted': boolean, - * 'isTargetOp': boolean + * 'isTargetMod': boolean */ 'use strict'; @@ -158,6 +159,40 @@ module.exports = { }); }, + onModToggle: function() { + var roomId = this.props.member.roomId; + var target = this.props.member.userId; + var room = MatrixClientPeg.get().getRoom(roomId); + if (!room) { + return; + } + var powerLevelEvent = room.currentState.getStateEvents( + "m.room.power_levels", "" + ); + if (!powerLevelEvent) { + return; + } + var me = room.getMember(MatrixClientPeg.get().credentials.userId); + if (!me) { + return; + } + var defaultLevel = powerLevelEvent.getContent().users_default; + var modLevel = me.powerLevel - 1; + // 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 + }); + }); + }, + 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. @@ -214,17 +249,19 @@ module.exports = { can: { kick: false, ban: false, - mute: false + mute: false, + modifyLevel: false }, muted: false, - isTargetOp: false + isTargetMod: false } }, _calculateOpsPermissions: function() { var defaultPerms = { can: {}, - muted: false + muted: false, + modifyLevel: false }; var room = MatrixClientPeg.get().getRoom(this.props.member.roomId); if (!room) { @@ -243,7 +280,7 @@ module.exports = { me, them, powerLevels.getContent() ), muted: this._isMuted(them, powerLevels.getContent()), - isTargetOp: them.powerLevel >= me.powerLevel && them.powerLevel > 0 + isTargetMod: them.powerLevel > powerLevels.getContent().users_default }; }, @@ -251,7 +288,8 @@ module.exports = { var can = { kick: false, ban: false, - mute: false + mute: false, + modifyLevel: false }; var canAffectUser = them.powerLevel < me.powerLevel; if (!canAffectUser) { @@ -265,6 +303,7 @@ module.exports = { can.kick = me.powerLevel >= powerLevels.kick; can.ban = me.powerLevel >= powerLevels.ban; can.mute = me.powerLevel >= editPowerLevel; + can.modifyLevel = me.powerLevel > them.powerLevel; return can; }, From b8e1927e82949bd2ddca3519bea66603aaf98244 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 21 Jul 2015 14:13:59 +0100 Subject: [PATCH 4/4] Use getDefaultProps instead of setting porps --- skins/base/views/organisms/ErrorDialog.js | 29 +++-------------- src/controllers/organisms/ErrorDialog.js | 39 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 25 deletions(-) create mode 100644 src/controllers/organisms/ErrorDialog.js diff --git a/skins/base/views/organisms/ErrorDialog.js b/skins/base/views/organisms/ErrorDialog.js index 847e796f4a..fd34a734af 100644 --- a/skins/base/views/organisms/ErrorDialog.js +++ b/skins/base/views/organisms/ErrorDialog.js @@ -17,7 +17,7 @@ limitations under the License. 'use strict'; /* - * Usage: + * Usage: * Modal.createDialog(ErrorDialog, { * title: "some text", (default: "Error") * description: "some more text", @@ -28,31 +28,11 @@ limitations under the License. */ var React = require('react'); +var ErrorDialogController = require("../../../../src/controllers/organisms/ErrorDialog"); module.exports = React.createClass({ displayName: 'ErrorDialog', - - // can't use getDefaultProps, see Modal.js - componentWillMount: function() { - if (!this.props.title) { - this.props.title = "Error"; - } - if (!this.props.description) { - this.props.description = "An error has occurred."; - } - if (!this.props.button) { - this.props.button = "OK"; - } - if (this.props.focus === undefined) { - this.props.focus = true; - } - if (!this.props.onClose) { - var self = this; - this.props.onClose = function() { - self.props.onFinished(); - }; - } - }, + mixins: [ErrorDialogController], render: function() { return ( @@ -61,11 +41,10 @@ module.exports = React.createClass({ {this.props.title} {this.props.description}
- ); } }); - diff --git a/src/controllers/organisms/ErrorDialog.js b/src/controllers/organisms/ErrorDialog.js new file mode 100644 index 0000000000..73f66c87e6 --- /dev/null +++ b/src/controllers/organisms/ErrorDialog.js @@ -0,0 +1,39 @@ +/* +Copyright 2015 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"); + +module.exports = { + propTypes: { + title: React.PropTypes.string, + description: React.PropTypes.string, + button: React.PropTypes.string, + focus: React.PropTypes.bool, + onFinished: React.PropTypes.func.isRequired, + }, + + getDefaultProps: function() { + var self = this; + return { + title: "Error", + description: "An error has occurred.", + button: "OK", + focus: true, + }; + }, +};