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;
}
/* 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;
top: 0;
left: 0;
@ -56,7 +87,7 @@ html {
z-index: 2000;
}
.mx_Dialog_Wrapper {
.mx_Dialog_wrapper {
position: fixed;
top: 0;
left: 0;

View file

@ -14,60 +14,3 @@ See the License for the specific language governing permissions and
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 {
cursor: pointer;
display: table-row;
height: 49px;
position: relative;
}
.mx_MemberTile_avatar {
display: table-cell;
padding-left: 14px;
padding-right: 12px;
padding-top: 3px;
padding-bottom: 3px;
@ -31,6 +32,10 @@ limitations under the License.
position: relative;
}
.mx_MemberTile_inviteTile {
cursor: pointer;
}
.mx_MemberTile_inviteEditing {
display: initial ! important;
}
@ -50,14 +55,14 @@ limitations under the License.
font-size: 14px;
padding: 9px;
margin-top: 6px;
margin-left: 14px;
}
.mx_MemberTile_power {
z-index: 10;
position: absolute;
width: 48px;
height: 48px;
left: -4px;
left: 10px;
top: -1px;
}
@ -68,6 +73,33 @@ limitations under the License.
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 {
display: table-cell;
vertical-align: middle;
@ -77,25 +109,22 @@ limitations under the License.
.mx_MemberTile_nameSpan {
}
*/
.mx_MemberTile_unavailable .mx_MemberTile_avatar,
.mx_MemberTile_unavailable .mx_MemberTile_name,
.mx_MemberTile_unavailable .mx_MemberTile_nameSpan
{
opacity: 0.75;
opacity: 0.66;
}
.mx_MemberTile_offline .mx_MemberTile_avatar,
.mx_MemberTile_offline .mx_MemberTile_name,
.mx_MemberTile_offline .mx_MemberTile_nameSpan
{
opacity: 0.5;
opacity: 0.25;
}
.mx_MemberTile_zalgo {
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;
overflow-y: auto;
border-radius: 8px;
padding: 20px 14px 14px 24px;
background-color: #fff;
order: 1;
@ -57,5 +56,5 @@ limitations under the License.
}
.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() {
console.log("onClickDiv triggered");
this.setState({
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',
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() {
var activeAgo = "unknown";
if (this.state.active >= 0) {
activeAgo = this.getDuration(this.state.active);
var interactButton, kickButton, banButton, muteButton, giveModButton;
if (this.props.member.userId === MatrixClientPeg.get().credentials.userId) {
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) {
kickButton = <div className="mx_MemberInfo_button" onClick={this.onKick}>
kickButton = <div className="mx_ContextualMenu_field" onClick={this.onKick}>
Kick
</div>;
}
if (this.state.can.ban) {
banButton = <div className="mx_MemberInfo_button" onClick={this.onBan}>
banButton = <div className="mx_ContextualMenu_field" onClick={this.onBan}>
Ban
</div>;
}
if (this.state.can.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}
</div>;
}
if (this.state.can.modifyLevel) {
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}
</div>
}
var opLabel;
if (this.state.isTargetMod) {
var level = this.props.member.powerLevelNorm + "%";
opLabel = <div className="mx_MemberInfo_field">Moderator ({level})</div>
}
return (
<div className="mx_MemberInfo">
<img className="mx_MemberInfo_chevron" src="img/chevron-right.png" width="9" height="16" />
<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>
<div>
{interactButton}
{muteButton}
{kickButton}
{banButton}

View file

@ -21,6 +21,7 @@ var React = require('react');
var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
var ComponentBroker = require('../../../../src/ComponentBroker');
var Modal = require("../../../../src/Modal");
var ContextualMenu = require("../../../../src/ContextualMenu");
var MemberTileController = require("../../../../src/controllers/molecules/MemberTile");
var MemberInfo = ComponentBroker.get('molecules/MemberInfo');
var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog");
@ -34,11 +35,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 +43,58 @@ module.exports = React.createClass({
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() {
var isMyUser = MatrixClientPeg.get().credentials.userId == this.props.member.userId;
@ -66,35 +114,47 @@ module.exports = React.createClass({
}
}
mainClassName += presenceClass;
if (this.state.hover || this.state.menu) {
mainClassName += " mx_MemberTile_hover";
}
var name = this.props.member.name;
if (isMyUser) name += " (me)";
var leave = isMyUser ? <span className="mx_MemberTile_leave" onClick={this.onLeaveClick}>X</span> : null;
// 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 = this.state.hover ? "mx_MemberTile_nameSpan" : "mx_MemberTile_name";
var nameClass = "mx_MemberTile_name";
if (zalgo.test(name)) {
nameClass += " mx_MemberTile_zalgo";
}
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 =
<div className="mx_MemberTile_nameWrapper">
<MemberInfo member={this.props.member} />
<span className={nameClass}>{name}</span>
{leave}
<div className="mx_MemberTile_details">
{ leave }
<div className="mx_MemberTile_userId">{ this.props.member.userId }</div>
{ presence }
</div>
}
else {
nameEl =
<div className={nameClass}>
{name}
{leave}
{ name }
</div>
}
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">
<MemberAvatar member={this.props.member} />
{ power }

View file

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

View file

@ -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() {