Merge branch 'develop' into kegan/slash-command-tab-complete

This commit is contained in:
Kegan Dougal 2016-01-14 11:12:06 +00:00
commit f4be4880b8
5 changed files with 210 additions and 106 deletions

View file

@ -64,6 +64,7 @@ module.exports.components['views.rooms.MemberInfo'] = require('./components/view
module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList'); module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList');
module.exports.components['views.rooms.MemberTile'] = require('./components/views/rooms/MemberTile'); module.exports.components['views.rooms.MemberTile'] = require('./components/views/rooms/MemberTile');
module.exports.components['views.rooms.MessageComposer'] = require('./components/views/rooms/MessageComposer'); module.exports.components['views.rooms.MessageComposer'] = require('./components/views/rooms/MessageComposer');
module.exports.components['views.rooms.PresenceLabel'] = require('./components/views/rooms/PresenceLabel');
module.exports.components['views.rooms.RoomHeader'] = require('./components/views/rooms/RoomHeader'); module.exports.components['views.rooms.RoomHeader'] = require('./components/views/rooms/RoomHeader');
module.exports.components['views.rooms.RoomList'] = require('./components/views/rooms/RoomList'); module.exports.components['views.rooms.RoomList'] = require('./components/views/rooms/RoomList');
module.exports.components['views.rooms.RoomPreviewBar'] = require('./components/views/rooms/RoomPreviewBar'); module.exports.components['views.rooms.RoomPreviewBar'] = require('./components/views/rooms/RoomPreviewBar');

View file

@ -24,10 +24,16 @@ module.exports = React.createClass({
displayName: 'MemberAvatar', displayName: 'MemberAvatar',
propTypes: { propTypes: {
member: React.PropTypes.object.isRequired, member: React.PropTypes.object,
width: React.PropTypes.number, width: React.PropTypes.number,
height: React.PropTypes.number, height: React.PropTypes.number,
resizeMethod: React.PropTypes.string, resizeMethod: React.PropTypes.string,
/**
* The custom display name to use for this member. This can serve as a
* drop in replacement for RoomMember objects, or as a clobber name on
* an existing RoomMember. Used for 3pid invites.
*/
customDisplayName: React.PropTypes.string
}, },
getDefaultProps: function() { getDefaultProps: function() {
@ -38,64 +44,68 @@ module.exports = React.createClass({
} }
}, },
getInitialState: function() {
var defaultImageUrl = Avatar.defaultAvatarUrlForString(
this.props.customDisplayName || this.props.member.userId
)
return {
imageUrl: this._getMemberImageUrl() || defaultImageUrl,
defaultImageUrl: defaultImageUrl
};
},
componentWillReceiveProps: function(nextProps) { componentWillReceiveProps: function(nextProps) {
this.refreshUrl(); this.refreshUrl();
}, },
defaultAvatarUrl: function(member, width, height, resizeMethod) {
return Avatar.defaultAvatarUrlForString(member.userId);
},
onError: function(ev) { onError: function(ev) {
// don't tightloop if the browser can't load a data url // don't tightloop if the browser can't load a data url
if (ev.target.src == this.defaultAvatarUrl(this.props.member)) { if (ev.target.src == this.state.defaultImageUrl) {
return; return;
} }
this.setState({ this.setState({
imageUrl: this.defaultAvatarUrl(this.props.member) imageUrl: this.state.defaultImageUrl
}); });
}, },
_computeUrl: function() { _getMemberImageUrl: function() {
if (!this.props.member) { return null; }
return Avatar.avatarUrlForMember(this.props.member, return Avatar.avatarUrlForMember(this.props.member,
this.props.width, this.props.width,
this.props.height, this.props.height,
this.props.resizeMethod); this.props.resizeMethod);
}, },
_getInitialLetter: function() {
var name = this.props.customDisplayName || this.props.member.name;
var initial = name[0];
if (initial === '@' && name[1]) {
initial = name[1];
}
return initial.toUpperCase();
},
refreshUrl: function() { refreshUrl: function() {
var newUrl = this._computeUrl(); var newUrl = this._getMemberImageUrl();
if (newUrl != this.currentUrl) { if (newUrl != this.currentUrl) {
this.currentUrl = newUrl; this.currentUrl = newUrl;
this.setState({imageUrl: newUrl}); this.setState({imageUrl: newUrl});
} }
}, },
getInitialState: function() {
return {
imageUrl: this._computeUrl()
};
},
///////////////
render: function() { render: function() {
// XXX: recalculates default avatar url constantly var name = this.props.customDisplayName || this.props.member.name;
if (this.state.imageUrl === this.defaultAvatarUrl(this.props.member)) {
var initial; if (this.state.imageUrl === this.state.defaultImageUrl) {
if (this.props.member.name[0]) var initialLetter = this._getInitialLetter();
initial = this.props.member.name[0].toUpperCase();
if (initial === '@' && this.props.member.name[1])
initial = this.props.member.name[1].toUpperCase();
return ( return (
<span className="mx_MemberAvatar" {...this.props}> <span className="mx_MemberAvatar" {...this.props}>
<span className="mx_MemberAvatar_initial" aria-hidden="true" <span className="mx_MemberAvatar_initial" aria-hidden="true"
style={{ fontSize: (this.props.width * 0.65) + "px", style={{ fontSize: (this.props.width * 0.65) + "px",
width: this.props.width + "px", width: this.props.width + "px",
lineHeight: this.props.height + "px" }}>{ initial }</span> lineHeight: this.props.height + "px" }}>{ initialLetter }</span>
<img className="mx_MemberAvatar_image" src={this.state.imageUrl} title={this.props.member.name} <img className="mx_MemberAvatar_image" src={this.state.imageUrl} title={name}
onError={this.onError} width={this.props.width} height={this.props.height} /> onError={this.onError} width={this.props.width} height={this.props.height} />
</span> </span>
); );
@ -104,9 +114,8 @@ module.exports = React.createClass({
<img className="mx_MemberAvatar mx_MemberAvatar_image" src={this.state.imageUrl} <img className="mx_MemberAvatar mx_MemberAvatar_image" src={this.state.imageUrl}
onError={this.onError} onError={this.onError}
width={this.props.width} height={this.props.height} width={this.props.width} height={this.props.height}
title={this.props.member.name} title={name}
{...this.props} {...this.props} />
/>
); );
} }
}); });

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
var React = require('react'); var React = require('react');
var classNames = require('classnames'); var classNames = require('classnames');
var Matrix = require("matrix-js-sdk");
var MatrixClientPeg = require("../../../MatrixClientPeg"); var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal"); var Modal = require("../../../Modal");
var sdk = require('../../../index'); var sdk = require('../../../index');
@ -229,7 +230,8 @@ module.exports = React.createClass({
var MemberTile = sdk.getComponent("rooms.MemberTile"); var MemberTile = sdk.getComponent("rooms.MemberTile");
var self = this; var self = this;
return self.state.members.filter(function(userId) {
var memberList = self.state.members.filter(function(userId) {
var m = self.memberDict[userId]; var m = self.memberDict[userId];
return m.membership == membership; return m.membership == membership;
}).map(function(userId) { }).map(function(userId) {
@ -238,6 +240,31 @@ module.exports = React.createClass({
<MemberTile key={userId} member={m} ref={userId} /> <MemberTile key={userId} member={m} ref={userId} />
); );
}); });
if (membership === "invite") {
// include 3pid invites (m.room.third_party_invite) state events.
// The HS may have already converted these into m.room.member invites so
// we shouldn't add them if the 3pid invite state key (token) is in the
// member invite (content.third_party_invite.signed.token)
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (room) {
room.currentState.getStateEvents("m.room.third_party_invite").forEach(
function(e) {
// discard all invites which have a m.room.member event since we've
// already added them.
var memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey());
if (memberEvent) {
return;
}
memberList.push(
<MemberTile key={e.getStateKey()} ref={e.getStateKey()}
customDisplayName={e.getContent().display_name} />
)
})
}
}
return memberList;
}, },
onPopulateInvite: function(e) { onPopulateInvite: function(e) {

View file

@ -26,20 +26,19 @@ var Modal = require("../../../Modal");
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MemberTile', displayName: 'MemberTile',
propTypes: {
member: React.PropTypes.any, // RoomMember
onFinished: React.PropTypes.func,
customDisplayName: React.PropTypes.string // for 3pid invites
},
getInitialState: function() { getInitialState: function() {
return {}; return {};
}, },
onLeaveClick: function() {
dis.dispatch({
action: 'leave_room',
room_id: this.props.member.roomId,
});
this.props.onFinished();
},
shouldComponentUpdate: function(nextProps, nextState) { shouldComponentUpdate: function(nextProps, nextState) {
if (this.state.hover !== nextState.hover) return true; if (this.state.hover !== nextState.hover) return true;
if (!this.props.member) { return false; } // e.g. 3pid members
if ( if (
this.member_last_modified_time === undefined || this.member_last_modified_time === undefined ||
this.member_last_modified_time < nextProps.member.getLastModifiedTime() this.member_last_modified_time < nextProps.member.getLastModifiedTime()
@ -65,44 +64,25 @@ module.exports = React.createClass({
}, },
onClick: function(e) { onClick: function(e) {
if (!this.props.member) { return; } // e.g. 3pid members
dis.dispatch({ dis.dispatch({
action: 'view_user', action: 'view_user',
member: this.props.member, member: this.props.member,
}); });
}, },
getDuration: function(time) { _getDisplayName: function() {
if (!time) return; if (this.props.customDisplayName) {
var t = parseInt(time / 1000); return this.props.customDisplayName;
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 this.props.member.name;
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() { getPowerLabel: function() {
if (!this.props.member) {
return this._getDisplayName();
}
var label = this.props.member.userId; var label = this.props.member.userId;
if (this.state.isTargetMod) { if (this.state.isTargetMod) {
label += " - Mod (" + this.props.member.powerLevelNorm + "%)"; label += " - Mod (" + this.props.member.powerLevelNorm + "%)";
@ -111,71 +91,74 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
this.member_last_modified_time = this.props.member.getLastModifiedTime(); var member = this.props.member;
if (this.props.member.user) { var isMyUser = false;
this.user_last_modified_time = this.props.member.user.getLastModifiedTime(); var name = this._getDisplayName();
} var active = -1;
var isMyUser = MatrixClientPeg.get().credentials.userId == this.props.member.userId;
var power;
// if (this.props.member && this.props.member.powerLevelNorm > 0) {
// var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png";
// power = <img src={ img } className="mx_MemberTile_power" width="44" height="44" alt=""/>;
// }
var presenceClass = "mx_MemberTile_offline"; var presenceClass = "mx_MemberTile_offline";
var mainClassName = "mx_MemberTile ";
if (this.props.member.user) { if (member) {
if (this.props.member.user.presence === "online") { if (member.user) {
presenceClass = "mx_MemberTile_online"; this.user_last_modified_time = member.user.getLastModifiedTime();
}
else if (this.props.member.user.presence === "unavailable") { // FIXME: make presence data update whenever User.presence changes...
presenceClass = "mx_MemberTile_unavailable"; active = (
(Date.now() - (member.user.lastPresenceTs - member.user.lastActiveAgo)) || -1
);
if (member.user.presence === "online") {
presenceClass = "mx_MemberTile_online";
}
else if (member.user.presence === "unavailable") {
presenceClass = "mx_MemberTile_unavailable";
}
} }
this.member_last_modified_time = member.getLastModifiedTime();
isMyUser = MatrixClientPeg.get().credentials.userId == member.userId;
// if (this.props.member && this.props.member.powerLevelNorm > 0) {
// var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png";
// power = <img src={ img } className="mx_MemberTile_power" width="44" height="44" alt=""/>;
// }
} }
var mainClassName = "mx_MemberTile ";
mainClassName += presenceClass; mainClassName += presenceClass;
if (this.state.hover) { if (this.state.hover) {
mainClassName += " mx_MemberTile_hover"; mainClassName += " mx_MemberTile_hover";
} }
var name = this.props.member.name;
// 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 nameEl; var nameEl;
if (this.state.hover) { if (this.state.hover) {
var presence; var presenceState = (member && member.user) ? member.user.presence : null;
// FIXME: make presence data update whenever User.presence changes... var PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
var active = this.props.member.user ? ((Date.now() - (this.props.member.user.lastPresenceTs - this.props.member.user.lastActiveAgo)) || -1) : -1; nameEl = (
if (active >= 0) {
presence = <div className="mx_MemberTile_presence">{ this.getPrettyPresence(this.props.member.user) } { this.getDuration(active) } ago</div>;
}
else {
presence = <div className="mx_MemberTile_presence">{ this.getPrettyPresence(this.props.member.user) }</div>;
}
nameEl =
<div className="mx_MemberTile_details"> <div className="mx_MemberTile_details">
<img className="mx_MemberTile_chevron" src="img/member_chevron.png" width="8" height="12"/> <img className="mx_MemberTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
<div className="mx_MemberTile_userId">{ name }</div> <div className="mx_MemberTile_userId">{ name }</div>
{ presence } <PresenceLabel activeAgo={active}
presenceState={presenceState} />
</div> </div>
);
} }
else { else {
nameEl = nameEl = (
<div className="mx_MemberTile_name"> <div className="mx_MemberTile_name">
{ name } { name }
</div> </div>
);
} }
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
return ( return (
<div className={mainClassName} title={ this.getPowerLabel() } <div className={mainClassName} title={ this.getPowerLabel() }
onClick={ this.onClick } onMouseEnter={ this.mouseEnter } onClick={ this.onClick } onMouseEnter={ this.mouseEnter }
onMouseLeave={ this.mouseLeave }> onMouseLeave={ this.mouseLeave }>
<div className="mx_MemberTile_avatar"> <div className="mx_MemberTile_avatar">
<MemberAvatar member={this.props.member} width={36} height={36} /> <MemberAvatar member={this.props.member} width={36} height={36}
{ power } customDisplayName={this.props.customDisplayName} />
</div> </div>
{ nameEl } { nameEl }
</div> </div>

View file

@ -0,0 +1,84 @@
/*
Copyright 2015, 2016 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 MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
module.exports = React.createClass({
displayName: 'PresenceLabel',
propTypes: {
activeAgo: React.PropTypes.number,
presenceState: React.PropTypes.string
},
getDefaultProps: function() {
return {
ago: -1,
presenceState: null
};
},
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(presence) {
if (presence === "online") return "Online";
if (presence === "unavailable") return "Idle"; // XXX: is this actually right?
if (presence === "offline") return "Offline";
return "Unknown";
},
render: function() {
if (this.props.activeAgo >= 0) {
return (
<div className="mx_PresenceLabel">
{ this.getPrettyPresence(this.props.presenceState) } { this.getDuration(this.props.activeAgo) } ago
</div>
);
}
else {
return (
<div className="mx_PresenceLabel">
{ this.getPrettyPresence(this.props.presenceState) }
</div>
);
}
}
});