From e3798e1b8572693f1471fe9f544b8470a85613b7 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson <matthew@matrix.org> Date: Sat, 15 Aug 2015 03:06:21 +0100 Subject: [PATCH] WIP fixing up the member list - just needs CSS and testing --- skins/base/img/edit.png | Bin 0 -> 498 bytes skins/base/views/molecules/MemberTile.js | 61 +++++- skins/base/views/organisms/RoomView.js | 2 +- src/controllers/molecules/MemberTile.js | 245 ++++++++++++++++++++++- src/controllers/organisms/MemberList.js | 4 +- 5 files changed, 296 insertions(+), 16 deletions(-) create mode 100644 skins/base/img/edit.png diff --git a/skins/base/img/edit.png b/skins/base/img/edit.png new file mode 100644 index 0000000000000000000000000000000000000000..2686885f792ed69bdb88518bfe6850301719e7f0 GIT binary patch literal 498 zcmV<O0S*3%P)<h;3K|Lk000e1NJLTq000sI000sQ1^@s6R?d!B00001b5ch_0Itp) z=>Px$tVu*cR5%f(l)+BIKoEwfO92Z+Lr{4D6%!NV(UTtZDbRyF8YUWIV)P00Y@!!0 zCIz`G2_n)KOUu^%*OY950?|3N&CYy(XJ%(tqbXK1t>d$<ERNK`;bf~ZXz-Mc<j z%iFoTaZFPeE?>G^MmkYTF;*(&li78Hv5fv3;`jlz`kdE0p6&6#bX@=BbZ4cZ^78wt z-aEW*THVIyV8}TS;tW2-Ahu?H9YlnX2^?>>AIDFuBNhx;h%r1+&R!vvD`rF|W|-{- z^w9DMWg->~SRgtYi=J#O>rymMr{{4xZ+uhR4M-Dh+h0o(7_;M-puw;^XsiZrjHx24 zOB(Iwbr}f;EHoIl7`qdrLE|E4n+AnD2}ZI)L&uz`4pk5$RZ-4|C<mETgh)0S%^WN= zW@a)^MExnl#9|jzTO^Y(9xOB%#vT(JI`F=Don4L^D_NnzaCR{EV&X=A+b1d-Y-&$N zLE-}pSZK)go2K=Iv7Z><ISXKJoAx6_WV8G3u>9N}SnrPL|AK~CFgUys<iCeDG8Btm oBLi`ppbGUGi3o)${^er(0mAFFToUNEk^lez07*qoM6N<$g8kFZ4*&oF literal 0 HcmV?d00001 diff --git a/skins/base/views/molecules/MemberTile.js b/skins/base/views/molecules/MemberTile.js index 6e3cb859..7c66616b 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 ? <img className="mx_MemberTile_leave" src="img/delete.png" width="10" height="10" onClick={this.onLeaveClick}/> : 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 = <div className="mx_MemberTile_menuItem" onClick={this.onKick}> + Kick + </div>; + } + if (this.state.can.ban) { + banButton = <div className="mx_MemberTile_menuItem" onClick={this.onBan}> + Ban + </div>; + } + if (this.state.can.mute) { + var muteLabel = this.state.muted ? "Unmute" : "Mute"; + muteButton = <div className="mx_MemberTile_menuItem" onClick={this.onMuteToggle}> + {muteLabel} + </div>; + } + if (this.state.can.modifyLevel) { + var giveOpLabel = this.state.isTargetMod ? "Revoke Mod" : "Make Mod"; + giveModButton = <div className="mx_MemberTile_menuItem" onClick={this.onModToggle}> + {giveOpLabel} + </div> + } + menu = <div className="mx_MemberTile_menu"> + <img className="mx_MemberTile_chevron" src="img/chevron-right.png" width="9" height="16" /> + <div className="mx_MemberTile_menuItem" onClick={this.onChatClick}>Chat</div> + {muteButton} + {kickButton} + {banButton} + {giveModButton} + </div>; + } + var nameEl; if (this.state.hover) { var presence; @@ -122,11 +165,10 @@ module.exports = React.createClass({ presence = <div className="mx_MemberTile_presence">{ this.getPrettyPresence(this.props.member.user) }</div>; } - // <MemberInfo member={this.props.member} /> nameEl = <div className="mx_MemberTile_details"> { leave } - <div className="mx_MemberTile_userId" title={ this.props.member.userId }>{ this.props.member.userId }</div> + <div className="mx_MemberTile_userId">{ this.props.member.userId }</div> { presence } </div> } @@ -138,7 +180,8 @@ module.exports = React.createClass({ } return ( - <div className={mainClassName} onMouseEnter={ this.mouseEnter } onMouseLeave={ this.mouseLeave }> + <div className={mainClassName} title={ this.getPowerLabel } onClick={ this.onClick } onMouseEnter={ this.mouseEnter } onMouseLeave={ this.mouseLeave }> + { menu } <div className="mx_MemberTile_avatar"> <MemberAvatar member={this.props.member} /> { power } diff --git a/skins/base/views/organisms/RoomView.js b/skins/base/views/organisms/RoomView.js index b6bfedf1..eff25d96 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 ae468284..19c08731 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 912b142a..3eef007e 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);