Merge pull request #175 from vector-im/matthew/userlist
Reskin the userlist as per the design
This commit is contained in:
commit
616b4fe0f1
17 changed files with 333 additions and 204 deletions
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
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
BIN
skins/base/img/edit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 498 B |
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
35
skins/base/views/molecules/ContextualMenu.js
Normal file
35
skins/base/views/molecules/ContextualMenu.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -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}
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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
72
src/ContextualMenu.js
Normal 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};
|
||||||
|
},
|
||||||
|
};
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue