diff --git a/src/component-index.js b/src/component-index.js
index 9fe15adfc6..7ae15ba12c 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -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.MemberTile'] = require('./components/views/rooms/MemberTile');
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.RoomList'] = require('./components/views/rooms/RoomList');
module.exports.components['views.rooms.RoomPreviewBar'] = require('./components/views/rooms/RoomPreviewBar');
diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js
index 21c717aac5..f209006b1c 100644
--- a/src/components/views/avatars/MemberAvatar.js
+++ b/src/components/views/avatars/MemberAvatar.js
@@ -24,10 +24,16 @@ module.exports = React.createClass({
displayName: 'MemberAvatar',
propTypes: {
- member: React.PropTypes.object.isRequired,
+ member: React.PropTypes.object,
width: React.PropTypes.number,
height: React.PropTypes.number,
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() {
@@ -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) {
this.refreshUrl();
},
- defaultAvatarUrl: function(member, width, height, resizeMethod) {
- return Avatar.defaultAvatarUrlForString(member.userId);
- },
-
onError: function(ev) {
// 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;
}
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,
this.props.width,
this.props.height,
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() {
- var newUrl = this._computeUrl();
+ var newUrl = this._getMemberImageUrl();
if (newUrl != this.currentUrl) {
this.currentUrl = newUrl;
this.setState({imageUrl: newUrl});
}
},
- getInitialState: function() {
- return {
- imageUrl: this._computeUrl()
- };
- },
-
-
- ///////////////
-
render: function() {
- // XXX: recalculates default avatar url constantly
- if (this.state.imageUrl === this.defaultAvatarUrl(this.props.member)) {
- var initial;
- if (this.props.member.name[0])
- initial = this.props.member.name[0].toUpperCase();
- if (initial === '@' && this.props.member.name[1])
- initial = this.props.member.name[1].toUpperCase();
-
+ var name = this.props.customDisplayName || this.props.member.name;
+
+ if (this.state.imageUrl === this.state.defaultImageUrl) {
+ var initialLetter = this._getInitialLetter();
return (
{ initial }
- { initialLetter }
+
);
@@ -104,9 +114,8 @@ module.exports = React.createClass({
+ title={name}
+ {...this.props} />
);
}
});
diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js
index b0e2baa3d3..eac5466e88 100644
--- a/src/components/views/rooms/MemberList.js
+++ b/src/components/views/rooms/MemberList.js
@@ -15,6 +15,7 @@ limitations under the License.
*/
var React = require('react');
var classNames = require('classnames');
+var Matrix = require("matrix-js-sdk");
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
var sdk = require('../../../index');
@@ -229,7 +230,8 @@ module.exports = React.createClass({
var MemberTile = sdk.getComponent("rooms.MemberTile");
var self = this;
- return self.state.members.filter(function(userId) {
+
+ var memberList = self.state.members.filter(function(userId) {
var m = self.memberDict[userId];
return m.membership == membership;
}).map(function(userId) {
@@ -238,6 +240,31 @@ module.exports = React.createClass({
);
});
+
+ 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(
+
+ )
+ })
+ }
+ }
+
+ return memberList;
},
onPopulateInvite: function(e) {
diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js
index 32cc619f13..4752c4d539 100644
--- a/src/components/views/rooms/MemberTile.js
+++ b/src/components/views/rooms/MemberTile.js
@@ -26,20 +26,19 @@ var Modal = require("../../../Modal");
module.exports = React.createClass({
displayName: 'MemberTile',
+ propTypes: {
+ member: React.PropTypes.any, // RoomMember
+ onFinished: React.PropTypes.func,
+ customDisplayName: React.PropTypes.string // for 3pid invites
+ },
+
getInitialState: function() {
return {};
},
- onLeaveClick: function() {
- dis.dispatch({
- action: 'leave_room',
- room_id: this.props.member.roomId,
- });
- this.props.onFinished();
- },
-
shouldComponentUpdate: function(nextProps, nextState) {
if (this.state.hover !== nextState.hover) return true;
+ if (!this.props.member) { return false; } // e.g. 3pid members
if (
this.member_last_modified_time === undefined ||
this.member_last_modified_time < nextProps.member.getLastModifiedTime()
@@ -65,44 +64,25 @@ module.exports = React.createClass({
},
onClick: function(e) {
+ if (!this.props.member) { return; } // e.g. 3pid members
+
dis.dispatch({
action: 'view_user',
member: this.props.member,
});
},
- 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";
+ _getDisplayName: function() {
+ if (this.props.customDisplayName) {
+ return this.props.customDisplayName;
}
- 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";
+ return this.props.member.name;
},
getPowerLabel: function() {
+ if (!this.props.member) {
+ return this._getDisplayName();
+ }
var label = this.props.member.userId;
if (this.state.isTargetMod) {
label += " - Mod (" + this.props.member.powerLevelNorm + "%)";
@@ -111,71 +91,74 @@ module.exports = React.createClass({
},
render: function() {
- this.member_last_modified_time = this.props.member.getLastModifiedTime();
- if (this.props.member.user) {
- this.user_last_modified_time = this.props.member.user.getLastModifiedTime();
- }
-
- 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 = ;
- // }
+ var member = this.props.member;
+ var isMyUser = false;
+ var name = this._getDisplayName();
+ var active = -1;
var presenceClass = "mx_MemberTile_offline";
- var mainClassName = "mx_MemberTile ";
- if (this.props.member.user) {
- if (this.props.member.user.presence === "online") {
- presenceClass = "mx_MemberTile_online";
- }
- else if (this.props.member.user.presence === "unavailable") {
- presenceClass = "mx_MemberTile_unavailable";
+
+ if (member) {
+ if (member.user) {
+ this.user_last_modified_time = member.user.getLastModifiedTime();
+
+ // FIXME: make presence data update whenever User.presence changes...
+ 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 = ;
+ // }
}
+
+
+ var mainClassName = "mx_MemberTile ";
mainClassName += presenceClass;
if (this.state.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 ? : null;
-
var nameEl;
if (this.state.hover) {
- var presence;
- // FIXME: make presence data update whenever User.presence changes...
- var active = this.props.member.user ? ((Date.now() - (this.props.member.user.lastPresenceTs - this.props.member.user.lastActiveAgo)) || -1) : -1;
- if (active >= 0) {
- presence =
{ this.getPrettyPresence(this.props.member.user) } { this.getDuration(active) } ago
;
- }
- else {
- presence = { this.getPrettyPresence(this.props.member.user) }
;
- }
-
- nameEl =
+ var presenceState = (member && member.user) ? member.user.presence : null;
+ var PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
+ nameEl = (
{ name }
- { presence }
+
+ );
}
else {
- nameEl =
+ nameEl = (
{ name }
+ );
}
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
+
return (
-
- { power }
+
{ nameEl }
diff --git a/src/components/views/rooms/PresenceLabel.js b/src/components/views/rooms/PresenceLabel.js
new file mode 100644
index 0000000000..4ecad5b3df
--- /dev/null
+++ b/src/components/views/rooms/PresenceLabel.js
@@ -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 (
+
+ { this.getPrettyPresence(this.props.presenceState) } { this.getDuration(this.props.activeAgo) } ago
+
+ );
+ }
+ else {
+ return (
+
+ { this.getPrettyPresence(this.props.presenceState) }
+
+ );
+ }
+ }
+});