Merge pull request #175 from vector-im/matthew/userlist

Reskin the userlist as per the design
This commit is contained in:
David Baker 2015-09-22 15:27:21 +01:00
commit 616b4fe0f1
17 changed files with 333 additions and 204 deletions

View file

@ -43,9 +43,40 @@ html {
overflow: -moz-scrollbars-none; overflow: -moz-scrollbars-none;
} }
/* FIXME: why is all the dialog stuff in here rather than in per-component files? */ .mx_ContextualMenu_background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 1.0;
z-index: 2000;
}
.mx_Dialog_Background { .mx_ContextualMenu {
border: 1px solid #a9dbf4;
border-radius: 8px;
background-color: #fff;
color: #747474;
position: fixed;
z-index: 2001;
padding: 6px;
}
.mx_ContextualMenu_chevron {
padding: 12px;
position: absolute;
right: -21px;
top: 0px;
}
.mx_ContextualMenu_field {
padding: 3px 6px 3px 6px;
cursor: pointer;
}
.mx_Dialog_background {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@ -56,7 +87,7 @@ html {
z-index: 2000; z-index: 2000;
} }
.mx_Dialog_Wrapper { .mx_Dialog_wrapper {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;

View file

@ -14,60 +14,3 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_MemberInfo {
text-align: center;
border: 1px solid #a9dbf4;
border-radius: 8px;
background-color: #fff;
position: absolute;
width: 200px;
margin-left: -295px;
margin-top: 0px;
z-index: 1000;
padding: 6px;
}
.mx_MemberInfo_chevron {
padding: 12px;
position: absolute;
right: -21px;
top: 0px;
}
/*
* a hacky shim to extend the hitmask of the overlay to overlap
* better with the main menu itself
*/
.mx_MemberInfo_shim {
position: absolute;
left: 212px;
width: 40px;
height: 100%;
}
.mx_MemberInfo_avatar {
padding: 6px;
}
.mx_MemberInfo_avatarImg {
border-radius: 128px;
}
.mx_MemberInfo_field {
padding: 6px;
overflow: hidden;
text-overflow: ellipsis;
}
.mx_MemberInfo_button {
vertical-align: middle;
max-width: 100px;
height: 36px;
background-color: #50e3c2;
line-height: 36px;
border-radius: 36px;
color: #fff;
margin: auto;
margin-top: 6px;
margin-bottom: 6px;
}

View file

@ -15,13 +15,14 @@ limitations under the License.
*/ */
.mx_MemberTile { .mx_MemberTile {
cursor: pointer;
display: table-row; display: table-row;
height: 49px; height: 49px;
position: relative;
} }
.mx_MemberTile_avatar { .mx_MemberTile_avatar {
display: table-cell; display: table-cell;
padding-left: 14px;
padding-right: 12px; padding-right: 12px;
padding-top: 3px; padding-top: 3px;
padding-bottom: 3px; padding-bottom: 3px;
@ -31,6 +32,10 @@ limitations under the License.
position: relative; position: relative;
} }
.mx_MemberTile_inviteTile {
cursor: pointer;
}
.mx_MemberTile_inviteEditing { .mx_MemberTile_inviteEditing {
display: initial ! important; display: initial ! important;
} }
@ -50,14 +55,14 @@ limitations under the License.
font-size: 14px; font-size: 14px;
padding: 9px; padding: 9px;
margin-top: 6px; margin-top: 6px;
margin-left: 14px;
} }
.mx_MemberTile_power { .mx_MemberTile_power {
z-index: 10;
position: absolute; position: absolute;
width: 48px; width: 48px;
height: 48px; height: 48px;
left: -4px; left: 10px;
top: -1px; top: -1px;
} }
@ -68,6 +73,33 @@ limitations under the License.
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.mx_MemberTile_details {
display: table-cell;
padding-right: 14px;
vertical-align: middle;
}
.mx_MemberTile_hover {
background-color: #f0f0f0;
font-size: 12px;
color: #747474;
}
.mx_MemberTile_userId {
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
}
.mx_MemberTile_leave {
cursor: pointer;
margin-top: 8px;
margin-right: -4px;
margin-left: 6px;
float: right;
}
/*
.mx_MemberTile_nameWrapper { .mx_MemberTile_nameWrapper {
display: table-cell; display: table-cell;
vertical-align: middle; vertical-align: middle;
@ -77,25 +109,22 @@ limitations under the License.
.mx_MemberTile_nameSpan { .mx_MemberTile_nameSpan {
} }
*/
.mx_MemberTile_unavailable .mx_MemberTile_avatar, .mx_MemberTile_unavailable .mx_MemberTile_avatar,
.mx_MemberTile_unavailable .mx_MemberTile_name, .mx_MemberTile_unavailable .mx_MemberTile_name,
.mx_MemberTile_unavailable .mx_MemberTile_nameSpan .mx_MemberTile_unavailable .mx_MemberTile_nameSpan
{ {
opacity: 0.75; opacity: 0.66;
} }
.mx_MemberTile_offline .mx_MemberTile_avatar, .mx_MemberTile_offline .mx_MemberTile_avatar,
.mx_MemberTile_offline .mx_MemberTile_name, .mx_MemberTile_offline .mx_MemberTile_name,
.mx_MemberTile_offline .mx_MemberTile_nameSpan .mx_MemberTile_offline .mx_MemberTile_nameSpan
{ {
opacity: 0.5; opacity: 0.25;
} }
.mx_MemberTile_zalgo { .mx_MemberTile_zalgo {
font-family: Helvetica, Arial, Sans-Serif; font-family: Helvetica, Arial, Sans-Serif;
} }
.mx_MemberTile_leave {
float: right;
}

View file

@ -42,7 +42,6 @@ limitations under the License.
border: 1px solid #a9dbf4; border: 1px solid #a9dbf4;
overflow-y: auto; overflow-y: auto;
border-radius: 8px; border-radius: 8px;
padding: 20px 14px 14px 24px;
background-color: #fff; background-color: #fff;
order: 1; order: 1;
@ -57,5 +56,5 @@ limitations under the License.
} }
.mx_MemberList h2 { .mx_MemberList h2 {
margin-top: 0px; margin: 14px;
} }

BIN
skins/base/img/delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,006 B

BIN
skins/base/img/edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

View file

@ -33,7 +33,6 @@ module.exports = React.createClass({
}, },
onClickDiv: function() { onClickDiv: function() {
console.log("onClickDiv triggered");
this.setState({ this.setState({
phase: this.Phases.Edit, phase: this.Phases.Edit,
}) })

View file

@ -0,0 +1,35 @@
/*
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');
var classNames = require('classnames');
var dis = require("../../../../src/dispatcher");
var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
module.exports = React.createClass({
displayName: 'ContextualMenu',
render: function() {
return (
<div className="mx_ContextualMenu">
</div>
);
}
});

View file

@ -27,84 +27,41 @@ module.exports = React.createClass({
displayName: 'MemberInfo', displayName: 'MemberInfo',
mixins: [MemberInfoController], mixins: [MemberInfoController],
componentDidMount: function() {
var self = this;
var memberInfo = this.getDOMNode();
var memberListScroll = document.getElementsByClassName("mx_MemberList_border")[0];
if (memberListScroll) {
memberInfo.style.top = (memberInfo.parentElement.offsetTop - memberListScroll.scrollTop) + "px";
}
},
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 ";
},
render: function() { render: function() {
var activeAgo = "unknown"; var interactButton, kickButton, banButton, muteButton, giveModButton;
if (this.state.active >= 0) { if (this.props.member.userId === MatrixClientPeg.get().credentials.userId) {
activeAgo = this.getDuration(this.state.active); interactButton = <div className="mx_ContextualMenu_field" onClick={this.onLeaveClick}>Leave room</div>;
} }
var kickButton, banButton, muteButton, giveModButton; else {
interactButton = <div className="mx_ContextualMenu_field" onClick={this.onChatClick}>Start chat</div>;
}
if (this.state.can.kick) { if (this.state.can.kick) {
kickButton = <div className="mx_MemberInfo_button" onClick={this.onKick}> kickButton = <div className="mx_ContextualMenu_field" onClick={this.onKick}>
Kick Kick
</div>; </div>;
} }
if (this.state.can.ban) { if (this.state.can.ban) {
banButton = <div className="mx_MemberInfo_button" onClick={this.onBan}> banButton = <div className="mx_ContextualMenu_field" onClick={this.onBan}>
Ban Ban
</div>; </div>;
} }
if (this.state.can.mute) { if (this.state.can.mute) {
var muteLabel = this.state.muted ? "Unmute" : "Mute"; var muteLabel = this.state.muted ? "Unmute" : "Mute";
muteButton = <div className="mx_MemberInfo_button" onClick={this.onMuteToggle}> muteButton = <div className="mx_ContextualMenu_field" onClick={this.onMuteToggle}>
{muteLabel} {muteLabel}
</div>; </div>;
} }
if (this.state.can.modifyLevel) { if (this.state.can.modifyLevel) {
var giveOpLabel = this.state.isTargetMod ? "Revoke Mod" : "Make Mod"; var giveOpLabel = this.state.isTargetMod ? "Revoke Mod" : "Make Mod";
giveModButton = <div className="mx_MemberInfo_button" onClick={this.onModToggle}> giveModButton = <div className="mx_ContextualMenu_field" onClick={this.onModToggle}>
{giveOpLabel} {giveOpLabel}
</div> </div>
} }
var opLabel;
if (this.state.isTargetMod) {
var level = this.props.member.powerLevelNorm + "%";
opLabel = <div className="mx_MemberInfo_field">Moderator ({level})</div>
}
return ( return (
<div className="mx_MemberInfo"> <div>
<img className="mx_MemberInfo_chevron" src="img/chevron-right.png" width="9" height="16" /> {interactButton}
<div className="mx_MemberInfo_shim"></div>
<div className="mx_MemberInfo_avatar">
<MemberAvatar member={this.props.member} width={128} height={128} />
</div>
<div className="mx_MemberInfo_field">{this.props.member.userId}</div>
{opLabel}
<div className="mx_MemberInfo_field">Presence: {this.state.presence}</div>
<div className="mx_MemberInfo_field">Last active: {activeAgo}</div>
<div className="mx_MemberInfo_button" onClick={this.onChatClick}>Start chat</div>
{muteButton} {muteButton}
{kickButton} {kickButton}
{banButton} {banButton}

View file

@ -21,6 +21,7 @@ var React = require('react');
var MatrixClientPeg = require("../../../../src/MatrixClientPeg"); var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
var ComponentBroker = require('../../../../src/ComponentBroker'); var ComponentBroker = require('../../../../src/ComponentBroker');
var Modal = require("../../../../src/Modal"); var Modal = require("../../../../src/Modal");
var ContextualMenu = require("../../../../src/ContextualMenu");
var MemberTileController = require("../../../../src/controllers/molecules/MemberTile"); var MemberTileController = require("../../../../src/controllers/molecules/MemberTile");
var MemberInfo = ComponentBroker.get('molecules/MemberInfo'); var MemberInfo = ComponentBroker.get('molecules/MemberInfo');
var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog"); var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog");
@ -34,11 +35,6 @@ module.exports = React.createClass({
displayName: 'MemberTile', displayName: 'MemberTile',
mixins: [MemberTileController], mixins: [MemberTileController],
// XXX: should these be in the controller?
getInitialState: function() {
return { 'hover': false };
},
mouseEnter: function(e) { mouseEnter: function(e) {
this.setState({ 'hover': true }); this.setState({ 'hover': true });
}, },
@ -47,6 +43,58 @@ module.exports = React.createClass({
this.setState({ 'hover': false }); this.setState({ 'hover': false });
}, },
onClick: function(e) {
var self = this;
self.setState({ 'menu': true });
ContextualMenu.createMenu(MemberInfo, {
member: self.props.member,
right: window.innerWidth - e.pageX,
top: e.pageY,
onFinished: function() {
self.setState({ 'menu': false });
}
});
},
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) {
label += " - Mod (" + this.props.member.powerLevelNorm + "%)";
}
return label;
},
render: function() { render: function() {
var isMyUser = MatrixClientPeg.get().credentials.userId == this.props.member.userId; var isMyUser = MatrixClientPeg.get().credentials.userId == this.props.member.userId;
@ -66,35 +114,47 @@ module.exports = React.createClass({
} }
} }
mainClassName += presenceClass; mainClassName += presenceClass;
if (this.state.hover || this.state.menu) {
mainClassName += " mx_MemberTile_hover";
}
var name = this.props.member.name; 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 ? <span className="mx_MemberTile_leave" onClick={this.onLeaveClick}>X</span> : null; var leave = isMyUser ? <img className="mx_MemberTile_leave" src="img/delete.png" width="10" height="10" onClick={this.onLeaveClick}/> : null;
var nameClass = this.state.hover ? "mx_MemberTile_nameSpan" : "mx_MemberTile_name"; var nameClass = "mx_MemberTile_name";
if (zalgo.test(name)) { if (zalgo.test(name)) {
nameClass += " mx_MemberTile_zalgo"; nameClass += " mx_MemberTile_zalgo";
} }
var nameEl; var nameEl;
if (this.state.hover) { if (this.state.hover || this.state.menu) {
var presence;
// FIXME: make presence data update whenever User.presence changes...
var active = this.props.member.user ? (this.props.member.user.lastActiveAgo || -1) : -1;
if (active >= 0) {
presence = <div className="mx_MemberTile_presence">{ this.getPrettyPresence(this.props.member.user) } for { this.getDuration(active) }</div>;
}
else {
presence = <div className="mx_MemberTile_presence">{ this.getPrettyPresence(this.props.member.user) }</div>;
}
nameEl = nameEl =
<div className="mx_MemberTile_nameWrapper"> <div className="mx_MemberTile_details">
<MemberInfo member={this.props.member} /> { leave }
<span className={nameClass}>{name}</span> <div className="mx_MemberTile_userId">{ this.props.member.userId }</div>
{leave} { presence }
</div> </div>
} }
else { else {
nameEl = nameEl =
<div className={nameClass}> <div className={nameClass}>
{name} { name }
{leave}
</div> </div>
} }
return ( 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 }>
<div className="mx_MemberTile_avatar"> <div className="mx_MemberTile_avatar">
<MemberAvatar member={this.props.member} /> <MemberAvatar member={this.props.member} />
{ power } { power }

View file

@ -75,14 +75,9 @@ module.exports = React.createClass({
}, },
inviteTile: function() { inviteTile: function() {
// if (this.state.inviting) {
// return (
// <div></div>
// );
// }
var classes = classNames({ var classes = classNames({
mx_MemberTile: true, mx_MemberTile: true,
mx_MemberTile_inviteTile: true,
mx_MemberTile_inviteEditing: this.state.editing, mx_MemberTile_inviteEditing: this.state.editing,
}); });

View file

@ -72,7 +72,7 @@ module.exports = React.createClass({
if (!this.state.numUnreadMessages) { if (!this.state.numUnreadMessages) {
return ""; return "";
} }
return this.state.numUnreadMessages + " new messages"; return this.state.numUnreadMessages + " new message" + (this.state.numUnreadMessages > 1 ? "s" : "");
}, },
scrollToBottom: function() { scrollToBottom: function() {

72
src/ContextualMenu.js Normal file
View file

@ -0,0 +1,72 @@
/*
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');
var q = require('q');
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
// pass in a custom control as the actual body.
module.exports = {
ContextualMenuContainerId: "mx_ContextualMenu_Container",
getOrCreateContainer: function() {
var container = document.getElementById(this.ContextualMenuContainerId);
if (!container) {
container = document.createElement("div");
container.id = this.ContextualMenuContainerId;
document.body.appendChild(container);
}
return container;
},
createMenu: function (Element, props) {
var self = this;
var closeMenu = function() {
React.unmountComponentAtNode(self.getOrCreateContainer());
if (props && props.onFinished) props.onFinished.apply(null, arguments);
};
var position = {
top: props.top - 20,
right: props.right + 8,
};
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the menu from a button click!
var menu = (
<div className="mx_ContextualMenu_wrapper">
<div className="mx_ContextualMenu" style={position}>
<img className="mx_ContextualMenu_chevron" src="img/chevron-right.png" width="9" height="16" />
<Element {...props} onFinished={closeMenu}/>
</div>
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
</div>
);
React.render(menu, this.getOrCreateContainer());
return {close: closeMenu};
},
};

View file

@ -47,11 +47,11 @@ module.exports = {
// FIXME: If a dialog uses getDefaultProps it clobbers the onFinished // FIXME: If a dialog uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the dialog from a button click! // property set here so you can't close the dialog from a button click!
var dialog = ( var dialog = (
<div className="mx_Dialog_Wrapper"> <div className="mx_Dialog_wrapper">
<div className="mx_Dialog"> <div className="mx_Dialog">
<Element {...props} onFinished={closeDialog}/> <Element {...props} onFinished={closeDialog}/>
</div> </div>
<div className="mx_Dialog_Background" onClick={closeDialog}></div> <div className="mx_Dialog_background" onClick={closeDialog}></div>
</div> </div>
); );

View file

@ -16,8 +16,6 @@ limitations under the License.
/* /*
* State vars: * State vars:
* 'presence' : string (online|offline|unavailable etc)
* 'active' : number (ms ago; can be -1)
* 'can': { * 'can': {
* kick: boolean, * kick: boolean,
* ban: boolean, * ban: boolean,
@ -34,58 +32,21 @@ var dis = require("../../dispatcher");
var Modal = require("../../Modal"); var Modal = require("../../Modal");
var ComponentBroker = require('../../ComponentBroker'); var ComponentBroker = require('../../ComponentBroker');
var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog"); var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog");
var QuestionDialog = ComponentBroker.get("organisms/QuestionDialog");
var Loader = require("react-loader");
module.exports = { module.exports = {
componentDidMount: function() { componentDidMount: function() {
var self = this; var self = this;
// listen for presence changes
function updateUserState(event, user) {
if (!self.props.member) { return; }
if (user.userId === self.props.member.userId) {
self.setState({
presence: user.presence,
active: user.lastActiveAgo
});
}
}
MatrixClientPeg.get().on("User.presence", updateUserState);
this.userPresenceFn = updateUserState;
// 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(self._calculateOpsPermissions());
}
MatrixClientPeg.get().on("RoomMember.powerLevel", updatePowerLevel);
this.updatePowerLevelFn = updatePowerLevel;
// work out the current state // work out the current state
if (this.props.member) { if (this.props.member) {
var usr = MatrixClientPeg.get().getUser(this.props.member.userId) || {}; var usr = MatrixClientPeg.get().getUser(this.props.member.userId) || {};
var memberState = this._calculateOpsPermissions(); var memberState = this._calculateOpsPermissions();
memberState.presence = usr.presence || "offline";
memberState.active = usr.lastActiveAgo || -1;
this.setState(memberState); this.setState(memberState);
} }
}, },
componentWillUnmount: function() {
MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn);
MatrixClientPeg.get().removeListener(
"RoomMember.powerLevel", this.updatePowerLevelFn
);
},
onKick: function() { onKick: function() {
var roomId = this.props.member.roomId; var roomId = this.props.member.roomId;
var target = this.props.member.userId; var target = this.props.member.userId;
@ -100,6 +61,7 @@ module.exports = {
description: err.message description: err.message
}); });
}); });
this.props.onFinished();
}, },
onBan: function() { onBan: function() {
@ -116,6 +78,7 @@ module.exports = {
description: err.message description: err.message
}); });
}); });
this.props.onFinished();
}, },
onMuteToggle: function() { onMuteToggle: function() {
@ -124,12 +87,14 @@ module.exports = {
var self = this; var self = this;
var room = MatrixClientPeg.get().getRoom(roomId); var room = MatrixClientPeg.get().getRoom(roomId);
if (!room) { if (!room) {
this.props.onFinished();
return; return;
} }
var powerLevelEvent = room.currentState.getStateEvents( var powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", "" "m.room.power_levels", ""
); );
if (!powerLevelEvent) { if (!powerLevelEvent) {
this.props.onFinished();
return; return;
} }
var isMuted = this.state.muted; var isMuted = this.state.muted;
@ -157,6 +122,7 @@ module.exports = {
description: err.message description: err.message
}); });
}); });
this.props.onFinished();
}, },
onModToggle: function() { onModToggle: function() {
@ -164,16 +130,19 @@ module.exports = {
var target = this.props.member.userId; var target = this.props.member.userId;
var room = MatrixClientPeg.get().getRoom(roomId); var room = MatrixClientPeg.get().getRoom(roomId);
if (!room) { if (!room) {
this.props.onFinished();
return; return;
} }
var powerLevelEvent = room.currentState.getStateEvents( var powerLevelEvent = room.currentState.getStateEvents(
"m.room.power_levels", "" "m.room.power_levels", ""
); );
if (!powerLevelEvent) { if (!powerLevelEvent) {
this.props.onFinished();
return; return;
} }
var me = room.getMember(MatrixClientPeg.get().credentials.userId); var me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (!me) { if (!me) {
this.props.onFinished();
return; return;
} }
var defaultLevel = powerLevelEvent.getContent().users_default; var defaultLevel = powerLevelEvent.getContent().users_default;
@ -191,6 +160,7 @@ module.exports = {
description: err.message description: err.message
}); });
}); });
this.props.onFinished();
}, },
onChatClick: function() { onChatClick: function() {
@ -240,12 +210,40 @@ module.exports = {
); );
}); });
} }
this.props.onFinished();
},
// FIXME: this is horribly duplicated with MemberTile's onLeaveClick.
// Not sure what the right solution to this is.
onLeaveClick: function() {
var roomId = this.props.member.roomId;
Modal.createDialog(QuestionDialog, {
title: "Leave room",
description: "Are you sure you want to leave the room?",
onFinished: function(should_leave) {
if (should_leave) {
var d = MatrixClientPeg.get().leave(roomId);
var modal = Modal.createDialog(Loader);
d.then(function() {
modal.close();
dis.dispatch({action: 'view_next_room'});
}, function(err) {
modal.close();
Modal.createDialog(ErrorDialog, {
title: "Failed to leave room",
description: err.toString()
});
});
}
}
});
this.props.onFinished();
}, },
getInitialState: function() { getInitialState: function() {
return { return {
presence: "offline",
active: -1,
can: { can: {
kick: false, kick: false,
ban: false, ban: false,

View file

@ -25,14 +25,23 @@ var Loader = require("react-loader");
var MatrixClientPeg = require("../../MatrixClientPeg"); var MatrixClientPeg = require("../../MatrixClientPeg");
module.exports = { module.exports = {
onClick: function() { // onClick: function() {
dis.dispatch({ // dis.dispatch({
action: 'view_user', // action: 'view_user',
user_id: this.props.member.userId // user_id: this.props.member.userId
}); // });
// },
getInitialState: function() {
return {
hover: false,
menu: false,
}
}, },
onLeaveClick: function() { onLeaveClick: function(ev) {
ev.stopPropagation();
ev.preventDefault();
var roomId = this.props.member.roomId; var roomId = this.props.member.roomId;
Modal.createDialog(QuestionDialog, { Modal.createDialog(QuestionDialog, {
title: "Leave room", title: "Leave room",
@ -56,5 +65,5 @@ module.exports = {
} }
} }
}); });
} }
}; };

View file

@ -61,7 +61,9 @@ module.exports = {
function updateUserState(event, user) { function updateUserState(event, user) {
var tile = self.refs[user.userId]; var tile = self.refs[user.userId];
if (tile) { 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); MatrixClientPeg.get().on("User.presence", updateUserState);