Merge pull request #807 from matrix-org/matthew/quick-search

Improve RoomList performance via side-stepping React
This commit is contained in:
Matthew Hodgson 2017-04-20 11:26:04 +01:00 committed by GitHub
commit 0ad1d8caf3
7 changed files with 268 additions and 60 deletions

View file

@ -67,7 +67,7 @@
"react": "^15.4.0", "react": "^15.4.0",
"react-addons-css-transition-group": "15.3.2", "react-addons-css-transition-group": "15.3.2",
"react-dom": "^15.4.0", "react-dom": "^15.4.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#39d858c",
"sanitize-html": "^1.11.1", "sanitize-html": "^1.11.1",
"text-encoding-utf-8": "^1.0.1", "text-encoding-utf-8": "^1.0.1",
"velocity-vector": "vector-im/velocity#059e3b2", "velocity-vector": "vector-im/velocity#059e3b2",

View file

@ -0,0 +1,62 @@
/*
Copyright 2017 Vector Creations 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.
*/
// singleton which dispatches invocations of a given type & argument
// rather than just a type (as per EventEmitter and Flux's dispatcher etc)
//
// This means you can have a single point which listens for an EventEmitter event
// and then dispatches out to one of thousands of RoomTiles (for instance) rather than
// having each RoomTile register for the EventEmitter event and having to
// iterate over all of them.
class ConstantTimeDispatcher {
constructor() {
// type -> arg -> [ listener(arg, params) ]
this.listeners = {};
}
register(type, arg, listener) {
if (!this.listeners[type]) this.listeners[type] = {};
if (!this.listeners[type][arg]) this.listeners[type][arg] = [];
this.listeners[type][arg].push(listener);
}
unregister(type, arg, listener) {
if (this.listeners[type] && this.listeners[type][arg]) {
var i = this.listeners[type][arg].indexOf(listener);
if (i > -1) {
this.listeners[type][arg].splice(i, 1);
}
}
else {
console.warn("Unregistering unrecognised listener (type=" + type + ", arg=" + arg + ")");
}
}
dispatch(type, arg, params) {
if (!this.listeners[type] || !this.listeners[type][arg]) {
console.warn("No registered listeners for dispatch (type=" + type + ", arg=" + arg + ")");
return;
}
this.listeners[type][arg].forEach(listener=>{
listener.call(arg, params);
});
}
}
if (!global.constantTimeDispatcher) {
global.constantTimeDispatcher = new ConstantTimeDispatcher();
}
module.exports = global.constantTimeDispatcher;

View file

@ -32,4 +32,5 @@ module.exports = {
DELETE: 46, DELETE: 46,
KEY_D: 68, KEY_D: 68,
KEY_E: 69, KEY_E: 69,
KEY_K: 75,
}; };

View file

@ -523,6 +523,7 @@ var TimelinePanel = React.createClass({
this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0);
dis.dispatch({ dis.dispatch({
action: 'on_room_read', action: 'on_room_read',
room: this.props.timelineSet.room,
}); });
} }
} }

View file

@ -47,6 +47,12 @@ export default React.createClass({
this.props.onFinished(false); this.props.onFinished(false);
}, },
componentDidMount: function() {
if (this.props.focus) {
this.refs.button.focus();
}
},
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const cancelButton = this.props.hasCancelButton ? ( const cancelButton = this.props.hasCancelButton ? (
@ -63,7 +69,7 @@ export default React.createClass({
{this.props.description} {this.props.description}
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}> <button ref="button" className="mx_Dialog_primary" onClick={this.onOk}>
{this.props.button} {this.props.button}
</button> </button>
{this.props.extraButtons} {this.props.extraButtons}

View file

@ -21,13 +21,13 @@ var GeminiScrollbar = require('react-gemini-scrollbar');
var MatrixClientPeg = require("../../../MatrixClientPeg"); var MatrixClientPeg = require("../../../MatrixClientPeg");
var CallHandler = require('../../../CallHandler'); var CallHandler = require('../../../CallHandler');
var RoomListSorter = require("../../../RoomListSorter"); var RoomListSorter = require("../../../RoomListSorter");
var Unread = require('../../../Unread');
var dis = require("../../../dispatcher"); var dis = require("../../../dispatcher");
var sdk = require('../../../index'); var sdk = require('../../../index');
var rate_limited_func = require('../../../ratelimitedfunc'); var rate_limited_func = require('../../../ratelimitedfunc');
var Rooms = require('../../../Rooms'); var Rooms = require('../../../Rooms');
import DMRoomMap from '../../../utils/DMRoomMap'; import DMRoomMap from '../../../utils/DMRoomMap';
var Receipt = require('../../../utils/Receipt'); var Receipt = require('../../../utils/Receipt');
var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
var HIDE_CONFERENCE_CHANS = true; var HIDE_CONFERENCE_CHANS = true;
@ -37,10 +37,16 @@ module.exports = React.createClass({
propTypes: { propTypes: {
ConferenceHandler: React.PropTypes.any, ConferenceHandler: React.PropTypes.any,
collapsed: React.PropTypes.bool.isRequired, collapsed: React.PropTypes.bool.isRequired,
currentRoom: React.PropTypes.string, selectedRoom: React.PropTypes.string,
searchFilter: React.PropTypes.string, searchFilter: React.PropTypes.string,
}, },
shouldComponentUpdate: function(nextProps, nextState) {
if (nextProps.collapsed !== this.props.collapsed) return true;
if (nextProps.searchFilter !== this.props.searchFilter) return true;
return false;
},
getInitialState: function() { getInitialState: function() {
return { return {
isLoadingLeftRooms: false, isLoadingLeftRooms: false,
@ -57,12 +63,18 @@ module.exports = React.createClass({
cli.on("Room.name", this.onRoomName); cli.on("Room.name", this.onRoomName);
cli.on("Room.tags", this.onRoomTags); cli.on("Room.tags", this.onRoomTags);
cli.on("Room.receipt", this.onRoomReceipt); cli.on("Room.receipt", this.onRoomReceipt);
cli.on("RoomState.events", this.onRoomStateEvents); cli.on("RoomState.members", this.onRoomStateMember);
cli.on("RoomMember.name", this.onRoomMemberName); cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("accountData", this.onAccountData); cli.on("accountData", this.onAccountData);
// lookup for which lists a given roomId is currently in.
this.listsForRoomId = {};
var s = this.getRoomLists(); var s = this.getRoomLists();
this.setState(s); this.setState(s);
// order of the sublists
this.listOrder = [];
}, },
componentDidMount: function() { componentDidMount: function() {
@ -71,7 +83,22 @@ module.exports = React.createClass({
this._updateStickyHeaders(true); this._updateStickyHeaders(true);
}, },
componentDidUpdate: function() { componentWillReceiveProps: function(nextProps) {
// short-circuit react when the room changes
// to avoid rerendering all the sublists everywhere
if (nextProps.selectedRoom !== this.props.selectedRoom) {
if (this.props.selectedRoom) {
constantTimeDispatcher.dispatch(
"RoomTile.select", this.props.selectedRoom, {}
);
}
constantTimeDispatcher.dispatch(
"RoomTile.select", nextProps.selectedRoom, { selected: true }
);
}
},
componentDidUpdate: function(prevProps, prevState) {
// Reinitialise the stickyHeaders when the component is updated // Reinitialise the stickyHeaders when the component is updated
this._updateStickyHeaders(true); this._updateStickyHeaders(true);
this._repositionIncomingCallBox(undefined, false); this._repositionIncomingCallBox(undefined, false);
@ -97,10 +124,24 @@ module.exports = React.createClass({
} }
break; break;
case 'on_room_read': case 'on_room_read':
// Force an update because the notif count state is too deep to cause // poke the right RoomTile to refresh, using the constantTimeDispatcher
// an update. This forces the local echo of reading notifs to be // to avoid each and every RoomTile registering to the 'on_room_read' event
// reflected by the RoomTiles. // XXX: if we like the constantTimeDispatcher we might want to dispatch
this.forceUpdate(); // directly from TimelinePanel rather than needlessly bouncing via here.
constantTimeDispatcher.dispatch(
"RoomTile.refresh", payload.room.roomId, {}
);
// also have to poke the right list(s)
var lists = this.listsForRoomId[payload.room.roomId];
if (lists) {
lists.forEach(list=>{
constantTimeDispatcher.dispatch(
"RoomSubList.refreshHeader", list, { room: payload.room }
);
});
}
break; break;
} }
}, },
@ -114,7 +155,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags); MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags);
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName); MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData); MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
} }
@ -123,10 +164,14 @@ module.exports = React.createClass({
}, },
onRoom: function(room) { onRoom: function(room) {
// XXX: this happens rarely; ideally we should only update the correct
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList(); this._delayedRefreshRoomList();
}, },
onDeleteRoom: function(roomId) { onDeleteRoom: function(roomId) {
// XXX: this happens rarely; ideally we should only update the correct
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList(); this._delayedRefreshRoomList();
}, },
@ -149,6 +194,10 @@ module.exports = React.createClass({
} }
}, },
_onMouseOver: function(ev) {
this._lastMouseOverTs = Date.now();
},
onSubListHeaderClick: function(isHidden, scrollToPosition) { onSubListHeaderClick: function(isHidden, scrollToPosition) {
// The scroll area has expanded or contracted, so re-calculate sticky headers positions // The scroll area has expanded or contracted, so re-calculate sticky headers positions
this._updateStickyHeaders(true, scrollToPosition); this._updateStickyHeaders(true, scrollToPosition);
@ -158,41 +207,89 @@ module.exports = React.createClass({
if (toStartOfTimeline) return; if (toStartOfTimeline) return;
if (!room) return; if (!room) return;
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
this._delayedRefreshRoomList();
// rather than regenerate our full roomlists, which is very heavy, we poke the
// correct sublists to just re-sort themselves. This isn't enormously reacty,
// but is much faster than the default react reconciler, or having to do voodoo
// with shouldComponentUpdate and a pleaseRefresh property or similar.
var lists = this.listsForRoomId[room.roomId];
if (lists) {
lists.forEach(list=>{
constantTimeDispatcher.dispatch("RoomSubList.sort", list, { room: room });
});
}
// we have to explicitly hit the roomtile which just changed
constantTimeDispatcher.dispatch(
"RoomTile.refresh", room.roomId, {}
);
}, },
onRoomReceipt: function(receiptEvent, room) { onRoomReceipt: function(receiptEvent, room) {
// because if we read a notification, it will affect notification count // because if we read a notification, it will affect notification count
// only bother updating if there's a receipt from us // only bother updating if there's a receipt from us
if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) { if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) {
this._delayedRefreshRoomList(); var lists = this.listsForRoomId[room.roomId];
if (lists) {
lists.forEach(list=>{
constantTimeDispatcher.dispatch(
"RoomSubList.refreshHeader", list, { room: room }
);
});
}
// we have to explicitly hit the roomtile which just changed
constantTimeDispatcher.dispatch(
"RoomTile.refresh", room.roomId, {}
);
} }
}, },
onRoomName: function(room) { onRoomName: function(room) {
this._delayedRefreshRoomList(); constantTimeDispatcher.dispatch(
"RoomTile.refresh", room.roomId, {}
);
}, },
onRoomTags: function(event, room) { onRoomTags: function(event, room) {
// XXX: this happens rarely; ideally we should only update the correct
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList(); this._delayedRefreshRoomList();
}, },
onRoomStateEvents: function(ev, state) { onRoomStateMember: function(ev, state, member) {
this._delayedRefreshRoomList(); constantTimeDispatcher.dispatch(
"RoomTile.refresh", member.roomId, {}
);
}, },
onRoomMemberName: function(ev, member) { onRoomMemberName: function(ev, member) {
this._delayedRefreshRoomList(); constantTimeDispatcher.dispatch(
"RoomTile.refresh", member.roomId, {}
);
}, },
onAccountData: function(ev) { onAccountData: function(ev) {
if (ev.getType() == 'm.direct') { if (ev.getType() == 'm.direct') {
// XXX: this happens rarely; ideally we should only update the correct
// sublists when it does (e.g. via a constantTimeDispatch to the right sublist)
this._delayedRefreshRoomList();
}
else if (ev.getType() == 'm.push_rules') {
this._delayedRefreshRoomList(); this._delayedRefreshRoomList();
} }
}, },
_delayedRefreshRoomList: new rate_limited_func(function() { _delayedRefreshRoomList: new rate_limited_func(function() {
this.refreshRoomList(); // if the mouse has been moving over the RoomList in the last 500ms
// then delay the refresh further to avoid bouncing around under the
// cursor
if (Date.now() - this._lastMouseOverTs > 500) {
this.refreshRoomList();
}
else {
this._delayedRefreshRoomList();
}
}, 500), }, 500),
refreshRoomList: function() { refreshRoomList: function() {
@ -200,14 +297,13 @@ module.exports = React.createClass({
// (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs)) // (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
// ); // );
// TODO: rather than bluntly regenerating and re-sorting everything // TODO: ideally we'd calculate this once at start, and then maintain
// every time we see any kind of room change from the JS SDK // any changes to it incrementally, updating the appropriate sublists
// we could do incremental updates on our copy of the state // as needed.
// based on the room which has actually changed. This would stop // Alternatively we'd do something magical with Immutable.js or similar.
// us re-rendering all the sublists every time anything changes anywhere
// in the state of the client.
this.setState(this.getRoomLists()); this.setState(this.getRoomLists());
this._lastRefreshRoomListTs = Date.now();
// this._lastRefreshRoomListTs = Date.now();
}, },
getRoomLists: function() { getRoomLists: function() {
@ -221,6 +317,9 @@ module.exports = React.createClass({
s.lists["m.lowpriority"] = []; s.lists["m.lowpriority"] = [];
s.lists["im.vector.fake.archived"] = []; s.lists["im.vector.fake.archived"] = [];
this.listsForRoomId = {};
var otherTagNames = {};
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
MatrixClientPeg.get().getRooms().forEach(function(room) { MatrixClientPeg.get().getRooms().forEach(function(room) {
@ -232,7 +331,12 @@ module.exports = React.createClass({
// ", target = " + me.events.member.getStateKey() + // ", target = " + me.events.member.getStateKey() +
// ", prevMembership = " + me.events.member.getPrevContent().membership); // ", prevMembership = " + me.events.member.getPrevContent().membership);
if (!self.listsForRoomId[room.roomId]) {
self.listsForRoomId[room.roomId] = [];
}
if (me.membership == "invite") { if (me.membership == "invite") {
self.listsForRoomId[room.roomId].push("im.vector.fake.invite");
s.lists["im.vector.fake.invite"].push(room); s.lists["im.vector.fake.invite"].push(room);
} }
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) { else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
@ -243,23 +347,27 @@ module.exports = React.createClass({
{ {
// Used to split rooms via tags // Used to split rooms via tags
var tagNames = Object.keys(room.tags); var tagNames = Object.keys(room.tags);
if (tagNames.length) { if (tagNames.length) {
for (var i = 0; i < tagNames.length; i++) { for (var i = 0; i < tagNames.length; i++) {
var tagName = tagNames[i]; var tagName = tagNames[i];
s.lists[tagName] = s.lists[tagName] || []; s.lists[tagName] = s.lists[tagName] || [];
s.lists[tagNames[i]].push(room); s.lists[tagName].push(room);
self.listsForRoomId[room.roomId].push(tagName);
otherTagNames[tagName] = 1;
} }
} }
else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged) // "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
s.lists["im.vector.fake.direct"].push(room); s.lists["im.vector.fake.direct"].push(room);
} }
else { else {
self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
s.lists["im.vector.fake.recent"].push(room); s.lists["im.vector.fake.recent"].push(room);
} }
} }
else if (me.membership === "leave") { else if (me.membership === "leave") {
self.listsForRoomId[room.roomId].push("im.vector.fake.archived");
s.lists["im.vector.fake.archived"].push(room); s.lists["im.vector.fake.archived"].push(room);
} }
else { else {
@ -280,8 +388,10 @@ module.exports = React.createClass({
const me = room.getMember(MatrixClientPeg.get().credentials.userId); const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me && Rooms.looksLikeDirectMessageRoom(room, me)) { if (me && Rooms.looksLikeDirectMessageRoom(room, me)) {
self.listsForRoomId[room.roomId].push("im.vector.fake.direct");
s.lists["im.vector.fake.direct"].push(room); s.lists["im.vector.fake.direct"].push(room);
} else { } else {
self.listsForRoomId[room.roomId].push("im.vector.fake.recent");
s.lists["im.vector.fake.recent"].push(room); s.lists["im.vector.fake.recent"].push(room);
} }
} }
@ -298,6 +408,8 @@ module.exports = React.createClass({
newMDirectEvent[otherPerson.userId] = roomList; newMDirectEvent[otherPerson.userId] = roomList;
} }
console.warn("Resetting room DM state to be " + JSON.stringify(newMDirectEvent));
// if this fails, fine, we'll just do the same thing next time we get the room lists // if this fails, fine, we'll just do the same thing next time we get the room lists
MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done(); MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done();
} }
@ -306,6 +418,21 @@ module.exports = React.createClass({
// we actually apply the sorting to this when receiving the prop in RoomSubLists. // we actually apply the sorting to this when receiving the prop in RoomSubLists.
// we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down
/*
this.listOrder = [
"im.vector.fake.invite",
"m.favourite",
"im.vector.fake.recent",
"im.vector.fake.direct",
Object.keys(otherTagNames).filter(tagName=>{
return (!tagName.match(/^m\.(favourite|lowpriority)$/));
}).sort(),
"m.lowpriority",
"im.vector.fake.archived"
];
*/
return s; return s;
}, },
@ -464,14 +591,14 @@ module.exports = React.createClass({
return ( return (
<GeminiScrollbar className="mx_RoomList_scrollbar" <GeminiScrollbar className="mx_RoomList_scrollbar"
autoshow={true} onScroll={ self._whenScrolling } ref="gemscroll"> autoshow={true} onScroll={ self._whenScrolling } ref="gemscroll">
<div className="mx_RoomList"> <div className="mx_RoomList" onMouseOver={ this._onMouseOver }>
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] } <RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
label="Invites" label="Invites"
editable={ false } editable={ false }
order="recent" order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter } searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick } onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } /> onShowMoreRooms={ self.onShowMoreRooms } />
@ -482,9 +609,9 @@ module.exports = React.createClass({
verb="favourite" verb="favourite"
editable={ true } editable={ true }
order="manual" order="manual"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter } searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick } onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } /> onShowMoreRooms={ self.onShowMoreRooms } />
@ -495,9 +622,9 @@ module.exports = React.createClass({
verb="tag direct chat" verb="tag direct chat"
editable={ true } editable={ true }
order="recent" order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
alwaysShowHeader={ true } alwaysShowHeader={ true }
searchFilter={ self.props.searchFilter } searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick } onHeaderClick={ self.onSubListHeaderClick }
@ -508,14 +635,14 @@ module.exports = React.createClass({
editable={ true } editable={ true }
verb="restore" verb="restore"
order="recent" order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter } searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick } onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } /> onShowMoreRooms={ self.onShowMoreRooms } />
{ Object.keys(self.state.lists).map(function(tagName) { { Object.keys(self.state.lists).sort().map(function(tagName) {
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
return <RoomSubList list={ self.state.lists[tagName] } return <RoomSubList list={ self.state.lists[tagName] }
key={ tagName } key={ tagName }
@ -524,9 +651,9 @@ module.exports = React.createClass({
verb={ "tag as " + tagName } verb={ "tag as " + tagName }
editable={ true } editable={ true }
order="manual" order="manual"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter } searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick } onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />; onShowMoreRooms={ self.onShowMoreRooms } />;
@ -540,9 +667,9 @@ module.exports = React.createClass({
verb="demote" verb="demote"
editable={ true } editable={ true }
order="recent" order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall } incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
searchFilter={ self.props.searchFilter } searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick } onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } /> onShowMoreRooms={ self.onShowMoreRooms } />
@ -551,8 +678,8 @@ module.exports = React.createClass({
label="Historical" label="Historical"
editable={ false } editable={ false }
order="recent" order="recent"
selectedRoom={ self.props.selectedRoom }
collapsed={ self.props.collapsed } collapsed={ self.props.collapsed }
selectedRoom={ self.props.selectedRoom }
alwaysShowHeader={ true } alwaysShowHeader={ true }
startAsHidden={ true } startAsHidden={ true }
showSpinner={ self.state.isLoadingLeftRooms } showSpinner={ self.state.isLoadingLeftRooms }

View file

@ -27,6 +27,8 @@ var RoomNotifs = require('../../../RoomNotifs');
var FormattingUtils = require('../../../utils/FormattingUtils'); var FormattingUtils = require('../../../utils/FormattingUtils');
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
var UserSettingsStore = require('../../../UserSettingsStore'); var UserSettingsStore = require('../../../UserSettingsStore');
var constantTimeDispatcher = require('../../../ConstantTimeDispatcher');
var Unread = require('../../../Unread');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomTile', displayName: 'RoomTile',
@ -36,12 +38,10 @@ module.exports = React.createClass({
connectDropTarget: React.PropTypes.func, connectDropTarget: React.PropTypes.func,
onClick: React.PropTypes.func, onClick: React.PropTypes.func,
isDragging: React.PropTypes.bool, isDragging: React.PropTypes.bool,
selectedRoom: React.PropTypes.string,
room: React.PropTypes.object.isRequired, room: React.PropTypes.object.isRequired,
collapsed: React.PropTypes.bool.isRequired, collapsed: React.PropTypes.bool.isRequired,
selected: React.PropTypes.bool.isRequired,
unread: React.PropTypes.bool.isRequired,
highlight: React.PropTypes.bool.isRequired,
isInvite: React.PropTypes.bool.isRequired, isInvite: React.PropTypes.bool.isRequired,
incomingCall: React.PropTypes.object, incomingCall: React.PropTypes.object,
}, },
@ -54,10 +54,11 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
return({ return({
hover : false, hover: false,
badgeHover : false, badgeHover: false,
menuDisplayed: false, menuDisplayed: false,
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
selected: this.props.room ? (this.props.selectedRoom === this.props.room.roomId) : false,
}); });
}, },
@ -79,23 +80,32 @@ module.exports = React.createClass({
} }
}, },
onAccountData: function(accountDataEvent) {
if (accountDataEvent.getType() == 'm.push_rules') {
this.setState({
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
});
}
},
componentWillMount: function() { componentWillMount: function() {
MatrixClientPeg.get().on("accountData", this.onAccountData); constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
constantTimeDispatcher.register("RoomTile.select", this.props.room.roomId, this.onSelect);
this.onRefresh();
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
var cli = MatrixClientPeg.get(); constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
if (cli) { constantTimeDispatcher.unregister("RoomTile.select", this.props.room.roomId, this.onSelect);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData); },
}
componentWillReceiveProps: function(nextProps) {
this.onRefresh();
},
onRefresh: function(params) {
this.setState({
unread: Unread.doesRoomHaveUnreadMessages(this.props.room),
highlight: this.props.room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite,
});
},
onSelect: function(params) {
this.setState({
selected: params.selected,
});
}, },
onClick: function() { onClick: function() {
@ -169,13 +179,13 @@ module.exports = React.createClass({
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight"); // var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge(); const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge();
const mentionBadges = this.props.highlight && this._shouldShowMentionBadge(); const mentionBadges = this.state.highlight && this._shouldShowMentionBadge();
const badges = notifBadges || mentionBadges; const badges = notifBadges || mentionBadges;
var classes = classNames({ var classes = classNames({
'mx_RoomTile': true, 'mx_RoomTile': true,
'mx_RoomTile_selected': this.props.selected, 'mx_RoomTile_selected': this.state.selected,
'mx_RoomTile_unread': this.props.unread, 'mx_RoomTile_unread': this.state.unread,
'mx_RoomTile_unreadNotify': notifBadges, 'mx_RoomTile_unreadNotify': notifBadges,
'mx_RoomTile_highlight': mentionBadges, 'mx_RoomTile_highlight': mentionBadges,
'mx_RoomTile_invited': (me && me.membership == 'invite'), 'mx_RoomTile_invited': (me && me.membership == 'invite'),
@ -221,7 +231,7 @@ module.exports = React.createClass({
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed, 'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
}); });
if (this.props.selected) { if (this.state.selected) {
let nameSelected = <EmojiText>{name}</EmojiText>; let nameSelected = <EmojiText>{name}</EmojiText>;
label = <div title={ name } className={ nameClasses }>{ nameSelected }</div>; label = <div title={ name } className={ nameClasses }>{ nameSelected }</div>;
@ -255,7 +265,8 @@ module.exports = React.createClass({
let ret = ( let ret = (
<div> { /* Only native elements can be wrapped in a DnD object. */} <div> { /* Only native elements can be wrapped in a DnD object. */}
<AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> <AccessibleButton className={classes} tabIndex="0" onClick={this.onClick}
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className={avatarClasses}> <div className={avatarClasses}>
<div className="mx_RoomTile_avatar_container"> <div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} /> <RoomAvatar room={this.props.room} width={24} height={24} />