diff --git a/skins/base/img/edit.png b/skins/base/img/edit.png new file mode 100644 index 0000000000..2686885f79 Binary files /dev/null and b/skins/base/img/edit.png differ diff --git a/skins/base/views/molecules/MemberTile.js b/skins/base/views/molecules/MemberTile.js index 6e3cb85982..7c66616b01 100644 --- a/skins/base/views/molecules/MemberTile.js +++ b/skins/base/views/molecules/MemberTile.js @@ -34,11 +34,6 @@ module.exports = React.createClass({ displayName: 'MemberTile', mixins: [MemberTileController], - // XXX: should these be in the controller? - getInitialState: function() { - return { 'hover': false }; - }, - mouseEnter: function(e) { this.setState({ 'hover': true }); }, @@ -47,6 +42,11 @@ module.exports = React.createClass({ this.setState({ 'hover': false }); }, + onClick: function(e) { + this.setState({ 'menu': true }); + this.setState(this._calculateOpsPermissions()); + }, + getDuration: function(time) { if (!time) return; var t = parseInt(time / 1000); @@ -78,6 +78,14 @@ module.exports = React.createClass({ return "Unknown"; }, + getPowerLabel: function() { + var label = this.props.member.userId; + if (this.state.isTargetMod) { + label += " - Mod (" + this.props.member.powerLevelNorm + "%)"; + } + return label; + }, + render: function() { var isMyUser = MatrixClientPeg.get().credentials.userId == this.props.member.userId; @@ -102,7 +110,7 @@ module.exports = React.createClass({ } var name = this.props.member.name; - if (isMyUser) name += " (me)"; + // if (isMyUser) name += " (me)"; // this does nothing other than introduce line wrapping and pain var leave = isMyUser ? : null; var nameClass = "mx_MemberTile_name"; @@ -110,6 +118,41 @@ module.exports = React.createClass({ nameClass += " mx_MemberTile_zalgo"; } + var menu; + if (this.state.menu) { + var kickButton, banButton, muteButton, giveModButton; + 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} +
; + } + if (this.state.can.modifyLevel) { + var giveOpLabel = this.state.isTargetMod ? "Revoke Mod" : "Make Mod"; + giveModButton =
+ {giveOpLabel} +
+ } + menu =
+ +
Chat
+ {muteButton} + {kickButton} + {banButton} + {giveModButton} +
; + } + var nameEl; if (this.state.hover) { var presence; @@ -122,11 +165,10 @@ module.exports = React.createClass({ presence =
{ this.getPrettyPresence(this.props.member.user) }
; } - // nameEl =
{ leave } -
{ this.props.member.userId }
+
{ this.props.member.userId }
{ presence }
} @@ -138,7 +180,8 @@ module.exports = React.createClass({ } return ( -
+
+ { menu }
{ power } diff --git a/skins/base/views/organisms/RoomView.js b/skins/base/views/organisms/RoomView.js index b6bfedf190..eff25d965a 100644 --- a/skins/base/views/organisms/RoomView.js +++ b/skins/base/views/organisms/RoomView.js @@ -72,7 +72,7 @@ module.exports = React.createClass({ if (!this.state.numUnreadMessages) { return ""; } - return this.state.numUnreadMessages + " new messages"; + return this.state.numUnreadMessages + " new message" + (this.state.numUnreadMessages > 1 ? "s" : ""); }, scrollToBottom: function() { diff --git a/src/controllers/molecules/MemberTile.js b/src/controllers/molecules/MemberTile.js index ae4682847a..19c08731c8 100644 --- a/src/controllers/molecules/MemberTile.js +++ b/src/controllers/molecules/MemberTile.js @@ -25,13 +25,169 @@ var Loader = require("react-loader"); var MatrixClientPeg = require("../../MatrixClientPeg"); module.exports = { - onClick: function() { - dis.dispatch({ - action: 'view_user', - user_id: this.props.member.userId + // onClick: function() { + // dis.dispatch({ + // action: 'view_user', + // user_id: this.props.member.userId + // }); + // }, + + 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 + }); + }); + }, + + 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. + var rooms = MatrixClientPeg.get().getRooms(); + var userIds = [ + this.props.member.userId, + MatrixClientPeg.get().credentials.userId + ]; + var existingRoomId = null; + for (var i = 0; i < rooms.length; i++) { + var members = rooms[i].getJoinedMembers(); + if (members.length === 2) { + var hasTargetUsers = true; + for (var j = 0; j < members.length; j++) { + if (userIds.indexOf(members[j].userId) === -1) { + hasTargetUsers = false; + break; + } + } + if (hasTargetUsers) { + existingRoomId = rooms[i].roomId; + break; + } + } + } + + if (existingRoomId) { + dis.dispatch({ + action: 'view_room', + room_id: existingRoomId + }); + } + else { + MatrixClientPeg.get().createRoom({ + invite: [this.props.member.userId], + preset: "private_chat" + }).done(function(res) { + dis.dispatch({ + action: 'view_room', + room_id: res.room_id + }); + }, function(err) { + console.error( + "Failed to create room: %s", JSON.stringify(err) + ); + }); + } + }, + onLeaveClick: function() { var roomId = this.props.member.roomId; Modal.createDialog(QuestionDialog, { @@ -56,5 +212,84 @@ module.exports = { } } }); - } + }, + + getInitialState: function() { + return { + hover: false, + menu: false, + + // presence: "offline", + // active: -1, + can: { + kick: false, + ban: false, + mute: false, + modifyLevel: false + }, + muted: false, + isTargetMod: false, + } + }, + + _calculateOpsPermissions: function() { + var defaultPerms = { + can: {}, + muted: false, + modifyLevel: false + }; + var room = MatrixClientPeg.get().getRoom(this.props.member.roomId); + if (!room) { + return defaultPerms; + } + var powerLevels = room.currentState.getStateEvents( + "m.room.power_levels", "" + ); + if (!powerLevels) { + return defaultPerms; + } + 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()), + isTargetMod: them.powerLevel > powerLevels.getContent().users_default + }; + }, + + _calculateCanPermissions: function(me, them, powerLevels) { + var can = { + kick: false, + ban: false, + mute: false, + modifyLevel: false + }; + 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; + can.modifyLevel = me.powerLevel > them.powerLevel; + 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; + }, }; diff --git a/src/controllers/organisms/MemberList.js b/src/controllers/organisms/MemberList.js index 912b142a13..3eef007ed4 100644 --- a/src/controllers/organisms/MemberList.js +++ b/src/controllers/organisms/MemberList.js @@ -61,7 +61,9 @@ module.exports = { function updateUserState(event, user) { var tile = self.refs[user.userId]; if (tile) { - tile.forceUpdate(); + // update the whole list to get the order right, not just this cell... + self.forceUpdate(); + // tile.forceUpdate(); } } MatrixClientPeg.get().on("User.presence", updateUserState);