mirror of
https://github.com/element-hq/element-web
synced 2024-11-28 20:38:55 +03:00
add constantTimeDispatcher and use it for strategic refreshes.
constantTimeDispatcher lets you poke a specific react component to do something without having to do any O(N) operations. This is useful if you have thousands of RoomTiles in a RoomSubList and want to just tell one of them to update, without either having to do a full comparison of this.props.list or have each and every RoomTile subscribe to a generic event from flux or node's eventemitter *UNTESTED*
This commit is contained in:
parent
691639d1e0
commit
da569c2c8d
4 changed files with 136 additions and 24 deletions
62
src/ConstantTimeDispatcher.js
Normal file
62
src/ConstantTimeDispatcher.js
Normal 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;
|
|
@ -523,6 +523,9 @@ 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',
|
||||||
|
payload: {
|
||||||
|
room: this.props.timelineSet.room
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ 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;
|
||||||
|
|
||||||
|
@ -57,13 +58,16 @@ 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.events", this.onRoomStateEvents);
|
||||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||||
cli.on("accountData", this.onAccountData);
|
cli.on("accountData", this.onAccountData);
|
||||||
|
|
||||||
var s = this.getRoomLists();
|
var s = this.getRoomLists();
|
||||||
this.setState(s);
|
this.setState(s);
|
||||||
|
|
||||||
|
// lookup for which lists a given roomId is currently in.
|
||||||
|
this.listsForRoomId = {};
|
||||||
|
|
||||||
this.focusedRoomTileRoomId = null;
|
this.focusedRoomTileRoomId = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -100,12 +104,13 @@ 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
|
||||||
//
|
// directly from TimelinePanel rather than needlessly bouncing via here.
|
||||||
// FIXME: we should surely just be refreshing the right tile...
|
constantTimeDispatcher.dispatch(
|
||||||
this.forceUpdate();
|
"RoomTile.refresh", payload.room.roomId, {}
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -119,7 +124,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.events", this.onRoomStateEvents);
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
@ -130,10 +135,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();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -194,35 +203,60 @@ 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.listsByRoomId[room.roomId];
|
||||||
|
if (lists) {
|
||||||
|
lists.forEach(list=>{
|
||||||
|
constantTimeDispatcher.dispatch("RoomSubList.sort", list, { room: room });
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
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.listsByRoomId[room.roomId];
|
||||||
|
if (lists) {
|
||||||
|
lists.forEach(list=>{
|
||||||
|
constantTimeDispatcher.dispatch(
|
||||||
|
"RoomSubList.refreshHeader", list, { room: room }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
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) {
|
// onRoomStateEvents: function(ev, state) {
|
||||||
this._delayedRefreshRoomList();
|
// this._delayedRefreshRoomList();
|
||||||
},
|
// },
|
||||||
|
|
||||||
onRoomMemberName: function(ev, member) {
|
onRoomMemberName: function(ev, member) {
|
||||||
this._delayedRefreshRoomList();
|
constantTimeDispatcher.dispatch(
|
||||||
|
"RoomTile.refresh", member.room.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();
|
this._delayedRefreshRoomList();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -244,12 +278,10 @@ 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();
|
||||||
|
@ -266,18 +298,19 @@ 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 = {};
|
||||||
|
|
||||||
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
||||||
|
|
||||||
MatrixClientPeg.get().getRooms().forEach(function(room) {
|
MatrixClientPeg.get().getRooms().forEach(function(room) {
|
||||||
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
if (!me) return;
|
if (!me) return;
|
||||||
|
|
||||||
// console.log("room = " + room.name + ", me.membership = " + me.membership +
|
// console.log("room = " + room.name + ", me.membership = " + me.membership +
|
||||||
// ", sender = " + me.events.member.getSender() +
|
// ", sender = " + me.events.member.getSender() +
|
||||||
// ", target = " + me.events.member.getStateKey() +
|
// ", target = " + me.events.member.getStateKey() +
|
||||||
// ", prevMembership = " + me.events.member.getPrevContent().membership);
|
// ", prevMembership = " + me.events.member.getPrevContent().membership);
|
||||||
|
|
||||||
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)) {
|
||||||
|
@ -288,23 +321,26 @@ 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[tagNames[i]].push(room);
|
||||||
|
self.listsForRoomId[room.roomId].push(tagNames[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 {
|
||||||
|
@ -325,8 +361,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -343,6 +381,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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ 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');
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomTile',
|
displayName: 'RoomTile',
|
||||||
|
@ -89,16 +90,22 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
|
||||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh);
|
||||||
var cli = MatrixClientPeg.get();
|
var cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onRefresh: function() {
|
||||||
|
this.forceUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
onClick: function() {
|
onClick: function() {
|
||||||
if (this.props.onClick) {
|
if (this.props.onClick) {
|
||||||
this.props.onClick(this.props.room.roomId);
|
this.props.onClick(this.props.room.roomId);
|
||||||
|
|
Loading…
Reference in a new issue