Merge pull request #103 from matrix-org/kegan/invite-search
Add global search with ability to invite from said list
This commit is contained in:
commit
c3c7c90de0
8 changed files with 495 additions and 126 deletions
|
@ -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;
|
||||
|
|
107
src/Entities.js
Normal file
107
src/Entities.js
Normal file
|
@ -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 (
|
||||
<MemberTile key={this.model.userId} member={this.model} />
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<UserTile key={this.model.userId} user={this.model}
|
||||
showInviteButton={this.showInviteButton} onClick={this.onClick.bind(this)} />
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
})
|
||||
}
|
||||
};
|
|
@ -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');
|
||||
|
|
139
src/components/views/rooms/EntityTile.js
Normal file
139
src/components/views/rooms/EntityTile.js
Normal file
|
@ -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, // <BaseAvatar />
|
||||
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 = (
|
||||
<div className="mx_EntityTile_details">
|
||||
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
|
||||
<div className="mx_EntityTile_name_hover">{ this.props.name }</div>
|
||||
<PresenceLabel activeAgo={this.props.presenceActiveAgo}
|
||||
presenceState={this.props.presenceState} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
nameEl = (
|
||||
<div className="mx_EntityTile_name">
|
||||
{ this.props.name }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var inviteButton;
|
||||
if (this.props.showInviteButton) {
|
||||
inviteButton = (
|
||||
<div className="mx_EntityTile_invite">
|
||||
<img src="img/plus.svg" width="16" height="16" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var power;
|
||||
var powerLevel = this.props.powerLevel;
|
||||
if (powerLevel >= 50 && powerLevel < 99) {
|
||||
power = <img src="img/mod.svg" className="mx_EntityTile_power" width="16" height="17" alt="Mod"/>;
|
||||
}
|
||||
if (powerLevel >= 99) {
|
||||
power = <img src="img/admin.svg" className="mx_EntityTile_power" width="16" height="17" alt="Admin"/>;
|
||||
}
|
||||
|
||||
|
||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
|
||||
var av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
|
||||
|
||||
return (
|
||||
<div className={mainClassName} title={ this.props.title }
|
||||
onClick={ this.props.onClick } onMouseEnter={ this.mouseEnter }
|
||||
onMouseLeave={ this.mouseLeave }>
|
||||
<div className="mx_EntityTile_avatar">
|
||||
{av}
|
||||
</div>
|
||||
{ nameEl }
|
||||
{ power }
|
||||
{ inviteButton }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -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(
|
||||
<MemberTile key={e.getStateKey()} ref={e.getStateKey()}
|
||||
customDisplayName={e.getContent().display_name} />
|
||||
<EntityTile key={e.getStateKey()} ref={e.getStateKey()}
|
||||
name={e.getContent().display_name} />
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -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({
|
|||
<Loader />
|
||||
);
|
||||
} else {
|
||||
var SearchableEntityList = sdk.getComponent("rooms.SearchableEntityList");
|
||||
|
||||
return (
|
||||
<form onSubmit={this.onPopulateInvite}>
|
||||
<input className="mx_MemberList_invite" ref="invite" id="mx_MemberList_invite" placeholder="Invite user (email)"/>
|
||||
</form>
|
||||
<SearchableEntityList searchPlaceholderText={"Invite / Search"}
|
||||
onSubmit={this.onInvite}
|
||||
onQueryChanged={this.onSearchQueryChanged}
|
||||
entities={Entities.fromUsers(this.userList || [], true, this.onInvite)} />
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var invitedSection = null;
|
||||
var invitedMemberTiles = this.makeMemberTiles('invite');
|
||||
var invitedMemberTiles = this.makeMemberTiles('invite', this.state.searchQuery);
|
||||
if (invitedMemberTiles.length > 0) {
|
||||
invitedSection = (
|
||||
<div className="mx_MemberList_invited">
|
||||
|
@ -343,7 +364,7 @@ module.exports = React.createClass({
|
|||
{this.inviteTile()}
|
||||
<div>
|
||||
<div className="mx_MemberList_wrapper">
|
||||
{this.makeMemberTiles('join')}
|
||||
{this.makeMemberTiles('join', this.state.searchQuery)}
|
||||
</div>
|
||||
</div>
|
||||
{invitedSection}
|
||||
|
|
|
@ -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 = <img src={ img } className="mx_MemberTile_power" width="44" height="44" alt=""/>;
|
||||
// }
|
||||
|
||||
var power;
|
||||
if (this.props.member) {
|
||||
var powerLevel = this.props.member.powerLevel;
|
||||
if (powerLevel >= 50 && powerLevel < 99) {
|
||||
power = <img src="img/mod.svg" className="mx_MemberTile_power" width="16" height="17" alt="Mod"/>;
|
||||
}
|
||||
if (powerLevel >= 99) {
|
||||
power = <img src="img/admin.svg" className="mx_MemberTile_power" width="16" height="17" alt="Admin"/>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = (
|
||||
<div className="mx_MemberTile_details">
|
||||
<img className="mx_MemberTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
|
||||
<div className="mx_MemberTile_userId">{ name }</div>
|
||||
<PresenceLabel activeAgo={active}
|
||||
presenceState={presenceState} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
nameEl = (
|
||||
<div className="mx_MemberTile_name">
|
||||
{ name }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
var EntityTile = sdk.getComponent('rooms.EntityTile');
|
||||
|
||||
var av;
|
||||
if (member) {
|
||||
av = (
|
||||
<MemberAvatar member={this.props.member} width={36} height={36} />
|
||||
);
|
||||
}
|
||||
else {
|
||||
av = (
|
||||
<BaseAvatar name={name} width={36} height={36} />
|
||||
var member = this.props.member;
|
||||
var name = this._getDisplayName();
|
||||
var active = -1;
|
||||
var presenceState = member.user ? member.user.presence : null;
|
||||
|
||||
var av = (
|
||||
<MemberAvatar member={member} width={36} height={36} />
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
this.member_last_modified_time = member.getLastModifiedTime();
|
||||
|
||||
return (
|
||||
<div className={mainClassName} title={ this.getPowerLabel() }
|
||||
onClick={ this.onClick } onMouseEnter={ this.mouseEnter }
|
||||
onMouseLeave={ this.mouseLeave }>
|
||||
<div className="mx_MemberTile_avatar">
|
||||
{ av }
|
||||
{ power }
|
||||
</div>
|
||||
{ nameEl }
|
||||
</div>
|
||||
<EntityTile {...this.props} presenceActiveAgo={active} presenceState={presenceState}
|
||||
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
|
||||
shouldComponentUpdate={this.shouldComponentUpdate.bind(this)}
|
||||
name={name} powerLevel={this.props.member.powerLevel} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
119
src/components/views/rooms/SearchableEntityList.js
Normal file
119
src/components/views/rooms/SearchableEntityList.js
Normal file
|
@ -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 = (
|
||||
<form onSubmit={this.onQuerySubmit}>
|
||||
<input className="mx_SearchableEntityList_query" type="text"
|
||||
onChange={this.onQueryChanged} value={this.state.query}
|
||||
placeholder={this.props.searchPlaceholderText} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{inputBox}
|
||||
<div className="mx_SearchableEntityList_list">
|
||||
{this.state.results.map((entity) => {
|
||||
return entity.getJsx();
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = SearchableEntityList;
|
56
src/components/views/rooms/UserTile.js
Normal file
56
src/components/views/rooms/UserTile.js
Normal file
|
@ -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 = (
|
||||
<BaseAvatar width={36} height={36} name={name} idName={user.userId}
|
||||
url={ Avatar.avatarUrlForUser(user, 36, 36, "crop") } />
|
||||
);
|
||||
|
||||
return (
|
||||
<EntityTile {...this.props} presenceState={user.presence} presenceActiveAgo={active}
|
||||
name={name} title={user.userId} avatarJsx={avatarJsx} />
|
||||
);
|
||||
}
|
||||
});
|
Loading…
Reference in a new issue