diff --git a/src/Avatar.js b/src/Avatar.js
index e97ed6b673..0ef6c8d07b 100644
--- a/src/Avatar.js
+++ b/src/Avatar.js
@@ -15,7 +15,7 @@ limitations under the License.
*/
'use strict';
-
+var ContentRepo = require("matrix-js-sdk").ContentRepo;
var MatrixClientPeg = require('./MatrixClientPeg');
module.exports = {
@@ -37,6 +37,17 @@ module.exports = {
return url;
},
+ avatarUrlForUser: function(user, width, height, resizeMethod) {
+ var url = ContentRepo.getHttpUriForMxc(
+ MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
+ width, height, resizeMethod
+ );
+ if (!url || url.length === 0) {
+ return null;
+ }
+ return url;
+ },
+
defaultAvatarUrlForString: function(s) {
var images = [ '76cfa6', '50e2c2', 'f4c371' ];
var total = 0;
diff --git a/src/Entities.js b/src/Entities.js
new file mode 100644
index 0000000000..47103bfb65
--- /dev/null
+++ b/src/Entities.js
@@ -0,0 +1,107 @@
+/*
+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.
+*/
+
+var React = require('react');
+var sdk = require('./index');
+
+/*
+ * Converts various data models to Entity objects.
+ *
+ * Entity objects provide an interface for UI components to use to display
+ * members in a data-agnostic way. This means they don't need to care if the
+ * underlying data model is a RoomMember, User or 3PID data structure, it just
+ * cares about rendering.
+ */
+
+class Entity {
+ constructor(model) {
+ this.model = model;
+ }
+
+ getJsx() {
+ return null;
+ }
+
+ matches(queryString) {
+ return false;
+ }
+}
+
+class MemberEntity extends Entity {
+ getJsx() {
+ var MemberTile = sdk.getComponent("rooms.MemberTile");
+ return (
+
+ );
+ }
+
+ matches(queryString) {
+ return this.model.name.toLowerCase().indexOf(queryString.toLowerCase()) === 0;
+ }
+}
+
+class UserEntity extends Entity {
+
+ constructor(model, showInviteButton, inviteFn) {
+ super(model);
+ this.showInviteButton = Boolean(showInviteButton);
+ this.inviteFn = inviteFn;
+ }
+
+ onClick() {
+ if (this.inviteFn) {
+ this.inviteFn(this.model.userId);
+ }
+ }
+
+ getJsx() {
+ var UserTile = sdk.getComponent("rooms.UserTile");
+ return (
+
+ );
+ }
+
+ matches(queryString) {
+ var name = this.model.displayName || this.model.userId;
+ return name.toLowerCase().indexOf(queryString.toLowerCase()) === 0;
+ }
+}
+
+
+module.exports = {
+ /**
+ * @param {RoomMember[]} members
+ * @return {Entity[]}
+ */
+ fromRoomMembers: function(members) {
+ return members.map(function(m) {
+ return new MemberEntity(m);
+ });
+ },
+
+ /**
+ * @param {User[]} users
+ * @param {boolean} showInviteButton
+ * @param {Function} inviteFn Called with the user ID.
+ * @return {Entity[]}
+ */
+ fromUsers: function(users, showInviteButton, inviteFn) {
+ return users.map(function(u) {
+ return new UserEntity(u, showInviteButton, inviteFn);
+ })
+ }
+};
diff --git a/src/component-index.js b/src/component-index.js
index 5a395d5696..2446b26b8d 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -65,6 +65,7 @@ module.exports.components['views.messages.MVideoBody'] = require('./components/v
module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody');
module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent');
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');
+module.exports.components['views.rooms.EntityTile'] = require('./components/views/rooms/EntityTile');
module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile');
module.exports.components['views.rooms.MemberInfo'] = require('./components/views/rooms/MemberInfo');
module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList');
@@ -76,8 +77,10 @@ module.exports.components['views.rooms.RoomList'] = require('./components/views/
module.exports.components['views.rooms.RoomPreviewBar'] = require('./components/views/rooms/RoomPreviewBar');
module.exports.components['views.rooms.RoomSettings'] = require('./components/views/rooms/RoomSettings');
module.exports.components['views.rooms.RoomTile'] = require('./components/views/rooms/RoomTile');
+module.exports.components['views.rooms.SearchableEntityList'] = require('./components/views/rooms/SearchableEntityList');
module.exports.components['views.rooms.SearchResultTile'] = require('./components/views/rooms/SearchResultTile');
module.exports.components['views.rooms.TabCompleteBar'] = require('./components/views/rooms/TabCompleteBar');
+module.exports.components['views.rooms.UserTile'] = require('./components/views/rooms/UserTile');
module.exports.components['views.settings.ChangeAvatar'] = require('./components/views/settings/ChangeAvatar');
module.exports.components['views.settings.ChangeDisplayName'] = require('./components/views/settings/ChangeDisplayName');
module.exports.components['views.settings.ChangePassword'] = require('./components/views/settings/ChangePassword');
diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js
new file mode 100644
index 0000000000..55629aa8b0
--- /dev/null
+++ b/src/components/views/rooms/EntityTile.js
@@ -0,0 +1,139 @@
+/*
+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');
+
+
+var PRESENCE_CLASS = {
+ "offline": "mx_EntityTile_offline",
+ "online": "mx_EntityTile_online",
+ "unavailable": "mx_EntityTile_unavailable"
+};
+
+module.exports = React.createClass({
+ displayName: 'EntityTile',
+
+ propTypes: {
+ name: React.PropTypes.string,
+ title: React.PropTypes.string,
+ avatarJsx: React.PropTypes.any, //
+ presenceState: React.PropTypes.string,
+ presenceActiveAgo: React.PropTypes.number,
+ showInviteButton: React.PropTypes.bool,
+ shouldComponentUpdate: React.PropTypes.func,
+ onClick: React.PropTypes.func
+ },
+
+ getDefaultProps: function() {
+ return {
+ shouldComponentUpdate: function(nextProps, nextState) { return false; },
+ onClick: function() {},
+ presenceState: "offline",
+ presenceActiveAgo: -1,
+ showInviteButton: false,
+ };
+ },
+
+ getInitialState: function() {
+ return {
+ hover: false
+ };
+ },
+
+ shouldComponentUpdate: function(nextProps, nextState) {
+ if (this.state.hover !== nextState.hover) return true;
+ return this.props.shouldComponentUpdate(nextProps, nextState);
+ },
+
+ mouseEnter: function(e) {
+ this.setState({ 'hover': true });
+ },
+
+ mouseLeave: function(e) {
+ this.setState({ 'hover': false });
+ },
+
+ render: function() {
+ var presenceClass = PRESENCE_CLASS[this.props.presenceState];
+ var mainClassName = "mx_EntityTile ";
+ mainClassName += presenceClass;
+ if (this.state.hover) {
+ mainClassName += " mx_EntityTile_hover";
+ }
+
+ var nameEl;
+ if (this.state.hover) {
+ var PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
+ nameEl = (
+
+
+
{ this.props.name }
+
+
+ );
+ }
+ else {
+ nameEl = (
+
+ { this.props.name }
+
+ );
+ }
+
+ var inviteButton;
+ if (this.props.showInviteButton) {
+ inviteButton = (
+
+
+
+ );
+ }
+
+ var power;
+ var powerLevel = this.props.powerLevel;
+ if (powerLevel >= 50 && powerLevel < 99) {
+ power = ;
+ }
+ if (powerLevel >= 99) {
+ power = ;
+ }
+
+
+ var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
+ var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
+
+ var av = this.props.avatarJsx || ;
+
+ return (
+
+
+ {av}
+
+ { nameEl }
+ { power }
+ { inviteButton }
+
+ );
+ }
+});
diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js
index 3e3221992e..9d20f5ab55 100644
--- a/src/components/views/rooms/MemberList.js
+++ b/src/components/views/rooms/MemberList.js
@@ -19,6 +19,7 @@ var Matrix = require("matrix-js-sdk");
var q = require('q');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
+var Entities = require("../../../Entities");
var sdk = require('../../../index');
var GeminiScrollbar = require('react-gemini-scrollbar');
@@ -70,8 +71,19 @@ module.exports = React.createClass({
self.setState({
members: self.roomMembers()
});
+ // Lazy-load the complete user list for inviting new users
+ // TODO: Keep this list bleeding-edge up-to-date. Practically speaking,
+ // it will do for now not being updated as random new users join different
+ // rooms as this list will be reloaded every room swap.
+ var room = MatrixClientPeg.get().getRoom(self.props.roomId);
+ self.userList = MatrixClientPeg.get().getUsers().filter(function(u) {
+ return !room.hasMembershipState(u.userId, "join");
+ });
}, 50);
+
+ setTimeout
+
// Attach a SINGLE listener for global presence changes then locate the
// member tile and re-render it. This is more efficient than every tile
// evar attaching their own listener.
@@ -263,13 +275,23 @@ module.exports = React.createClass({
return latB - latA;
},
- makeMemberTiles: function(membership) {
+ onSearchQueryChanged: function(input) {
+ this.setState({
+ searchQuery: input
+ });
+ },
+
+ makeMemberTiles: function(membership, query) {
var MemberTile = sdk.getComponent("rooms.MemberTile");
+ query = (query || "").toLowerCase();
var self = this;
var memberList = self.state.members.filter(function(userId) {
var m = self.memberDict[userId];
+ if (query && m.name.toLowerCase().indexOf(query) !== 0) {
+ return false;
+ }
return m.membership == membership;
}).map(function(userId) {
var m = self.memberDict[userId];
@@ -284,6 +306,7 @@ module.exports = React.createClass({
// 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);
+ var EntityTile = sdk.getComponent("rooms.EntityTile");
if (room) {
room.currentState.getStateEvents("m.room.third_party_invite").forEach(
function(e) {
@@ -294,8 +317,8 @@ module.exports = React.createClass({
return;
}
memberList.push(
-
+
)
})
}
@@ -304,11 +327,6 @@ module.exports = React.createClass({
return memberList;
},
- onPopulateInvite: function(e) {
- this.onInvite(this.refs.invite.value);
- e.preventDefault();
- },
-
inviteTile: function() {
if (this.state.inviting) {
var Loader = sdk.getComponent("elements.Spinner");
@@ -316,17 +334,20 @@ module.exports = React.createClass({
);
} else {
+ var SearchableEntityList = sdk.getComponent("rooms.SearchableEntityList");
+
return (
-
+
);
}
},
render: function() {
var invitedSection = null;
- var invitedMemberTiles = this.makeMemberTiles('invite');
+ var invitedMemberTiles = this.makeMemberTiles('invite', this.state.searchQuery);
if (invitedMemberTiles.length > 0) {
invitedSection = (
@@ -343,7 +364,7 @@ module.exports = React.createClass({
{this.inviteTile()}
- {this.makeMemberTiles('join')}
+ {this.makeMemberTiles('join', this.state.searchQuery)}
{invitedSection}
diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js
index 0f40180274..d5c124ad21 100644
--- a/src/components/views/rooms/MemberTile.js
+++ b/src/components/views/rooms/MemberTile.js
@@ -27,9 +27,7 @@ module.exports = React.createClass({
displayName: 'MemberTile',
propTypes: {
- member: React.PropTypes.any, // RoomMember
- onFinished: React.PropTypes.func,
- customDisplayName: React.PropTypes.string // for 3pid invites
+ member: React.PropTypes.any.isRequired, // RoomMember
},
getInitialState: function() {
@@ -37,13 +35,11 @@ module.exports = React.createClass({
},
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()
) {
- return true
+ return true;
}
if (
nextProps.member.user &&
@@ -55,17 +51,7 @@ module.exports = React.createClass({
return false;
},
- mouseEnter: function(e) {
- this.setState({ 'hover': true });
- },
-
- mouseLeave: function(e) {
- this.setState({ 'hover': false });
- },
-
onClick: function(e) {
- if (!this.props.member) { return; } // e.g. 3pid members
-
dis.dispatch({
action: 'view_user',
member: this.props.member,
@@ -73,115 +59,42 @@ module.exports = React.createClass({
},
_getDisplayName: function() {
- if (this.props.customDisplayName) {
- return this.props.customDisplayName;
- }
return this.props.member.name;
},
getPowerLabel: function() {
- if (!this.props.member) {
- return this._getDisplayName();
- }
- var label = this.props.member.userId + " (power " + this.props.member.powerLevel + ")";
- return label;
+ return this.props.member.userId + " (power " + this.props.member.powerLevel + ")";
},
render: function() {
- var member = this.props.member;
- var isMyUser = false;
- var name = this._getDisplayName();
- var active = -1;
- var presenceClass = "mx_MemberTile_offline";
-
- 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 power;
- if (this.props.member) {
- var powerLevel = this.props.member.powerLevel;
- if (powerLevel >= 50 && powerLevel < 99) {
- power =
;
- }
- if (powerLevel >= 99) {
- power =
;
- }
- }
- }
-
- var mainClassName = "mx_MemberTile ";
- mainClassName += presenceClass;
- if (this.state.hover) {
- mainClassName += " mx_MemberTile_hover";
- }
-
- var nameEl;
- if (this.state.hover && this.props.member) {
- var presenceState = (member && member.user) ? member.user.presence : null;
- var PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
- nameEl = (
-
-
-
{ name }
-
-
- );
- }
- else {
- nameEl = (
-
- { name }
-
- );
- }
-
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
+ var EntityTile = sdk.getComponent('rooms.EntityTile');
- var av;
- if (member) {
- av = (
-
+ var member = this.props.member;
+ var name = this._getDisplayName();
+ var active = -1;
+ var presenceState = member.user ? member.user.presence : null;
+
+ var av = (
+
+ );
+
+ 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
);
}
- else {
- av = (
-
- );
- }
-
+ this.member_last_modified_time = member.getLastModifiedTime();
+
return (
-
-
- { av }
- { power }
-
- { nameEl }
-
+
);
}
});
diff --git a/src/components/views/rooms/SearchableEntityList.js b/src/components/views/rooms/SearchableEntityList.js
new file mode 100644
index 0000000000..b6232362ac
--- /dev/null
+++ b/src/components/views/rooms/SearchableEntityList.js
@@ -0,0 +1,119 @@
+/*
+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.
+*/
+var React = require('react');
+var MatrixClientPeg = require("../../../MatrixClientPeg");
+var Modal = require("../../../Modal");
+var GeminiScrollbar = require('react-gemini-scrollbar');
+
+// A list capable of displaying entities which conform to the SearchableEntity
+// interface which is an object containing getJsx(): Jsx and matches(query: string): boolean
+var SearchableEntityList = React.createClass({
+ displayName: 'SearchableEntityList',
+
+ propTypes: {
+ searchPlaceholderText: React.PropTypes.string,
+ emptyQueryShowsAll: React.PropTypes.bool,
+ showInputBox: React.PropTypes.bool,
+ onQueryChanged: React.PropTypes.func, // fn(inputText)
+ onSubmit: React.PropTypes.func, // fn(inputText)
+ entities: React.PropTypes.array
+ },
+
+ getDefaultProps: function() {
+ return {
+ showInputBox: true,
+ searchPlaceholderText: "Search",
+ entities: [],
+ emptyQueryShowsAll: false,
+ onSubmit: function() {},
+ onQueryChanged: function(input) {}
+ };
+ },
+
+ getInitialState: function() {
+ return {
+ query: "",
+ results: this.getSearchResults("")
+ };
+ },
+
+ componentWillUnmount: function() {
+ // pretend the query box was blanked out else filters could still be
+ // applied to other components which rely on onQueryChanged.
+ this.props.onQueryChanged("");
+ },
+
+ /**
+ * Public-facing method to set the input query text to the given input.
+ * @param {string} input
+ */
+ setQuery: function(input) {
+ this.setState({
+ query: input,
+ results: this.getSearchResults(input)
+ });
+ },
+
+ onQueryChanged: function(ev) {
+ var q = ev.target.value;
+ this.setState({
+ query: q,
+ results: this.getSearchResults(q)
+ });
+ this.props.onQueryChanged(q);
+ },
+
+ onQuerySubmit: function(ev) {
+ ev.preventDefault();
+ this.props.onSubmit(this.state.query);
+ },
+
+ getSearchResults: function(query) {
+ if (!query || query.length === 0) {
+ return this.props.emptyQueryShowsAll ? this.props.entities : []
+ }
+ return this.props.entities.filter(function(e) {
+ return e.matches(query);
+ });
+ },
+
+ render: function() {
+ var inputBox;
+
+ if (this.props.showInputBox) {
+ inputBox = (
+
+ );
+ }
+
+ return (
+
+ {inputBox}
+
+ {this.state.results.map((entity) => {
+ return entity.getJsx();
+ })}
+
+
+ );
+ }
+});
+
+ module.exports = SearchableEntityList;
diff --git a/src/components/views/rooms/UserTile.js b/src/components/views/rooms/UserTile.js
new file mode 100644
index 0000000000..6597796764
--- /dev/null
+++ b/src/components/views/rooms/UserTile.js
@@ -0,0 +1,56 @@
+/*
+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 Avatar = require("../../../Avatar");
+var MatrixClientPeg = require('../../../MatrixClientPeg');
+var sdk = require('../../../index');
+var dis = require('../../../dispatcher');
+var Modal = require("../../../Modal");
+
+module.exports = React.createClass({
+ displayName: 'UserTile',
+
+ propTypes: {
+ user: React.PropTypes.any.isRequired // User
+ },
+
+ render: function() {
+ var EntityTile = sdk.getComponent("rooms.EntityTile");
+ var user = this.props.user;
+ var name = user.displayName || user.userId;
+ var active = -1;
+
+ // FIXME: make presence data update whenever User.presence changes...
+ active = (
+ (Date.now() - (user.lastPresenceTs - user.lastActiveAgo)) || -1
+ );
+
+ var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
+ var avatarJsx = (
+
+ );
+
+ return (
+
+ );
+ }
+});