From 0a91511f05c2bf3cc7511cffd8f4487a366caa15 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 15 Apr 2017 12:13:29 +0100 Subject: [PATCH 01/17] cmd-k for quick search --- src/KeyCode.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/KeyCode.js b/src/KeyCode.js index c9cac01239..f164dbc15c 100644 --- a/src/KeyCode.js +++ b/src/KeyCode.js @@ -32,4 +32,5 @@ module.exports = { DELETE: 46, KEY_D: 68, KEY_E: 69, + KEY_K: 75, }; From 691639d1e06e8c7384dfedcc4514f024a77fc0af Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 15 Apr 2017 13:23:52 +0100 Subject: [PATCH 02/17] track RoomTile focus in RoomList, and stop the RoomList from updating during mouseOver --- src/components/views/rooms/RoomList.js | 62 ++++++++++++++++++++++++-- src/components/views/rooms/RoomTile.js | 11 ++++- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 59346d5f4d..0da741df19 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -63,12 +63,15 @@ module.exports = React.createClass({ var s = this.getRoomLists(); this.setState(s); + + this.focusedRoomTileRoomId = null; }, componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); // Initialise the stickyHeaders when the component is created this._updateStickyHeaders(true); + document.addEventListener('keydown', this._onKeyDown); }, componentDidUpdate: function() { @@ -100,6 +103,8 @@ module.exports = React.createClass({ // Force an update because the notif count state is too deep to cause // an update. This forces the local echo of reading notifs to be // reflected by the RoomTiles. + // + // FIXME: we should surely just be refreshing the right tile... this.forceUpdate(); break; } @@ -120,6 +125,8 @@ module.exports = React.createClass({ } // cancel any pending calls to the rate_limited_funcs this._delayedRefreshRoomList.cancelPendingCall(); + document.removeEventListener('keydown', this._onKeyDown); + }, onRoom: function(room) { @@ -149,6 +156,35 @@ module.exports = React.createClass({ } }, + _onMouseOver: function(ev) { + this._lastMouseOverTs = Date.now(); + }, + + _onKeyDown: function(ev) { + if (!this.focusedRoomTileRoomId) return; + let handled = false; + + switch (ev.keyCode) { + case KeyCode.UP: + this._onMoveFocus(true); + handled = true; + break; + case KeyCode.DOWN: + this._onMoveFocus(false); + handled = true; + break; + } + + if (handled) { + ev.stopPropagation(); + ev.preventDefault(); + } + }, + + _onMoveFocus: function(up) { + + }, + onSubListHeaderClick: function(isHidden, scrollToPosition) { // The scroll area has expanded or contracted, so re-calculate sticky headers positions this._updateStickyHeaders(true, scrollToPosition); @@ -192,7 +228,15 @@ module.exports = React.createClass({ }, _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), refreshRoomList: function() { @@ -207,7 +251,8 @@ module.exports = React.createClass({ // us re-rendering all the sublists every time anything changes anywhere // in the state of the client. this.setState(this.getRoomLists()); - this._lastRefreshRoomListTs = Date.now(); + + // this._lastRefreshRoomListTs = Date.now(); }, getRoomLists: function() { @@ -457,6 +502,10 @@ module.exports = React.createClass({ this.refs.gemscroll.forceUpdate(); }, + onRoomTileFocus: function(roomId) { + this.focusedRoomTileRoomId = roomId; + }, + render: function() { var RoomSubList = sdk.getComponent('structures.RoomSubList'); var self = this; @@ -464,7 +513,7 @@ module.exports = React.createClass({ return ( -
+
{ Object.keys(self.state.lists).map(function(tagName) { @@ -529,6 +582,7 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } + onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } />; } @@ -545,6 +599,7 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } + onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } />
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 06b05e9299..cff5c2f623 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -35,6 +35,7 @@ module.exports = React.createClass({ connectDragSource: React.PropTypes.func, connectDropTarget: React.PropTypes.func, onClick: React.PropTypes.func, + onFocus: React.PropTypes.func, isDragging: React.PropTypes.bool, room: React.PropTypes.object.isRequired, @@ -104,6 +105,12 @@ module.exports = React.createClass({ } }, + onFocus: function() { + if (this.props.onFocus) { + this.props.onFocus(this.props.room.roomId); + } + }, + onMouseEnter: function() { this.setState( { hover : true }); this.badgeOnMouseEnter(); @@ -255,7 +262,9 @@ module.exports = React.createClass({ let ret = (
{ /* Only native elements can be wrapped in a DnD object. */} - +
From da569c2c8d3b8ea031566f4b9b5bd4f40e5cc465 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 17 Apr 2017 20:58:43 +0100 Subject: [PATCH 03/17] 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* --- src/ConstantTimeDispatcher.js | 62 +++++++++++++++ src/components/structures/TimelinePanel.js | 3 + src/components/views/rooms/RoomList.js | 88 ++++++++++++++++------ src/components/views/rooms/RoomTile.js | 7 ++ 4 files changed, 136 insertions(+), 24 deletions(-) create mode 100644 src/ConstantTimeDispatcher.js diff --git a/src/ConstantTimeDispatcher.js b/src/ConstantTimeDispatcher.js new file mode 100644 index 0000000000..265ee11fd4 --- /dev/null +++ b/src/ConstantTimeDispatcher.js @@ -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; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 8cd820c284..296565488c 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -523,6 +523,9 @@ var TimelinePanel = React.createClass({ this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); dis.dispatch({ action: 'on_room_read', + payload: { + room: this.props.timelineSet.room + } }); } } diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 0da741df19..2a70f14724 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -28,6 +28,7 @@ var rate_limited_func = require('../../../ratelimitedfunc'); var Rooms = require('../../../Rooms'); import DMRoomMap from '../../../utils/DMRoomMap'; var Receipt = require('../../../utils/Receipt'); +var constantTimeDispatcher = require('../../../ConstantTimeDispatcher'); var HIDE_CONFERENCE_CHANS = true; @@ -57,13 +58,16 @@ module.exports = React.createClass({ cli.on("Room.name", this.onRoomName); cli.on("Room.tags", this.onRoomTags); 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("accountData", this.onAccountData); var s = this.getRoomLists(); this.setState(s); + // lookup for which lists a given roomId is currently in. + this.listsForRoomId = {}; + this.focusedRoomTileRoomId = null; }, @@ -100,12 +104,13 @@ module.exports = React.createClass({ } break; case 'on_room_read': - // Force an update because the notif count state is too deep to cause - // an update. This forces the local echo of reading notifs to be - // reflected by the RoomTiles. - // - // FIXME: we should surely just be refreshing the right tile... - this.forceUpdate(); + // poke the right RoomTile to refresh, using the constantTimeDispatcher + // to avoid each and every RoomTile registering to the 'on_room_read' event + // XXX: if we like the constantTimeDispatcher we might want to dispatch + // directly from TimelinePanel rather than needlessly bouncing via here. + constantTimeDispatcher.dispatch( + "RoomTile.refresh", payload.room.roomId, {} + ); break; } }, @@ -119,7 +124,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags); 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("accountData", this.onAccountData); } @@ -130,10 +135,14 @@ module.exports = React.createClass({ }, 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(); }, 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(); }, @@ -194,35 +203,60 @@ module.exports = React.createClass({ if (toStartOfTimeline) return; if (!room) 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) { // because if we read a notification, it will affect notification count // only bother updating if there's a receipt from us 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) { - this._delayedRefreshRoomList(); + constantTimeDispatcher.dispatch( + "RoomTile.refresh", room.roomId, {} + ); }, 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(); }, - onRoomStateEvents: function(ev, state) { - this._delayedRefreshRoomList(); - }, + // onRoomStateEvents: function(ev, state) { + // this._delayedRefreshRoomList(); + // }, onRoomMemberName: function(ev, member) { - this._delayedRefreshRoomList(); + constantTimeDispatcher.dispatch( + "RoomTile.refresh", member.room.roomId, {} + ); }, onAccountData: function(ev) { 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(); } }, @@ -244,12 +278,10 @@ module.exports = React.createClass({ // (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs)) // ); - // TODO: rather than bluntly regenerating and re-sorting everything - // every time we see any kind of room change from the JS SDK - // we could do incremental updates on our copy of the state - // based on the room which has actually changed. This would stop - // us re-rendering all the sublists every time anything changes anywhere - // in the state of the client. + // TODO: ideally we'd calculate this once at start, and then maintain + // any changes to it incrementally, updating the appropriate sublists + // as needed. + // Alternatively we'd do something magical with Immutable.js or similar. this.setState(this.getRoomLists()); // this._lastRefreshRoomListTs = Date.now(); @@ -266,18 +298,19 @@ module.exports = React.createClass({ s.lists["m.lowpriority"] = []; s.lists["im.vector.fake.archived"] = []; + this.listsForRoomId = {}; + const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); MatrixClientPeg.get().getRooms().forEach(function(room) { const me = room.getMember(MatrixClientPeg.get().credentials.userId); if (!me) return; - // console.log("room = " + room.name + ", me.membership = " + me.membership + // ", sender = " + me.events.member.getSender() + // ", target = " + me.events.member.getStateKey() + // ", prevMembership = " + me.events.member.getPrevContent().membership); - if (me.membership == "invite") { + self.listsForRoomId[room.roomId].push("im.vector.fake.invite"); s.lists["im.vector.fake.invite"].push(room); } 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 var tagNames = Object.keys(room.tags); - if (tagNames.length) { for (var i = 0; i < tagNames.length; i++) { var tagName = tagNames[i]; s.lists[tagName] = s.lists[tagName] || []; s.lists[tagNames[i]].push(room); + self.listsForRoomId[room.roomId].push(tagNames[i]); } } else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { // "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); } else { + self.listsForRoomId[room.roomId].push("im.vector.fake.recent"); s.lists["im.vector.fake.recent"].push(room); } } else if (me.membership === "leave") { + self.listsForRoomId[room.roomId].push("im.vector.fake.archived"); s.lists["im.vector.fake.archived"].push(room); } else { @@ -325,8 +361,10 @@ module.exports = React.createClass({ const me = room.getMember(MatrixClientPeg.get().credentials.userId); if (me && Rooms.looksLikeDirectMessageRoom(room, me)) { + self.listsForRoomId[room.roomId].push("im.vector.fake.direct"); s.lists["im.vector.fake.direct"].push(room); } else { + self.listsForRoomId[room.roomId].push("im.vector.fake.recent"); s.lists["im.vector.fake.recent"].push(room); } } @@ -343,6 +381,8 @@ module.exports = React.createClass({ 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 MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done(); } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index cff5c2f623..ac682f710a 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -27,6 +27,7 @@ var RoomNotifs = require('../../../RoomNotifs'); var FormattingUtils = require('../../../utils/FormattingUtils'); import AccessibleButton from '../elements/AccessibleButton'; var UserSettingsStore = require('../../../UserSettingsStore'); +var constantTimeDispatcher = require('../../../ConstantTimeDispatcher'); module.exports = React.createClass({ displayName: 'RoomTile', @@ -89,16 +90,22 @@ module.exports = React.createClass({ }, componentWillMount: function() { + constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh); MatrixClientPeg.get().on("accountData", this.onAccountData); }, componentWillUnmount: function() { + constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh); var cli = MatrixClientPeg.get(); if (cli) { MatrixClientPeg.get().removeListener("accountData", this.onAccountData); } }, + onRefresh: function() { + this.forceUpdate(); + }, + onClick: function() { if (this.props.onClick) { this.props.onClick(this.props.room.roomId); From 9591ad31e6c95d7748072702c45d10bdb1c4d841 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 18 Apr 2017 02:43:29 +0100 Subject: [PATCH 04/17] fix bugs, experiment with focus pulling, make it vaguely work --- src/components/views/rooms/RoomList.js | 143 +++++++++++++++++++++++-- src/components/views/rooms/RoomTile.js | 4 +- 2 files changed, 136 insertions(+), 11 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 2a70f14724..cb692ff253 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -27,6 +27,7 @@ var sdk = require('../../../index'); var rate_limited_func = require('../../../ratelimitedfunc'); var Rooms = require('../../../Rooms'); import DMRoomMap from '../../../utils/DMRoomMap'; +import KeyCode from '../../../KeyCode'; var Receipt = require('../../../utils/Receipt'); var constantTimeDispatcher = require('../../../ConstantTimeDispatcher'); @@ -68,7 +69,13 @@ module.exports = React.createClass({ // lookup for which lists a given roomId is currently in. this.listsForRoomId = {}; - this.focusedRoomTileRoomId = null; + // order of the sublists + this.listOrder = []; + + // this.focusedRoomTileRoomId = null; + this.focusedElement = null; + // this.focusedPosition = null; + // this.focusMoving = false; }, componentDidMount: function() { @@ -170,7 +177,7 @@ module.exports = React.createClass({ }, _onKeyDown: function(ev) { - if (!this.focusedRoomTileRoomId) return; + if (!this.focusedElement) return; let handled = false; switch (ev.keyCode) { @@ -191,7 +198,61 @@ module.exports = React.createClass({ }, _onMoveFocus: function(up) { + // cheat and move focus by faking tab/shift-tab. This lets us do things + // like collapse/uncollapse room headers & truncated lists without having + // to reimplement the entirety of the keyboard navigation logic. + // + // this simply doens't work, as for security apparently you can't inject + // UI events any more - c.f. this note from + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent + // + // Note: manually firing an event does not generate the default action + // associated with that event. For example, manually firing a key event + // does not cause that letter to appear in a focused text input. In the + // case of UI events, this is important for security reasons, as it + // prevents scripts from simulating user actions that interact with the + // browser itself. +/* + var event = document.createEvent('Event'); + event.initEvent('keydown', true, true); + event.keyCode = 9; + event.shiftKey = up ? true : false; + document.dispatchEvent(event); +*/ + // alternatively, this is the beginning of moving the focus through the list, + // navigating the pure datastructure of the list contents, but doesn't let + // you navigate through other things +/* + this.focusMoving = true; + if (this.focusPosition) { + if (up) { + this.focusPosition.index++; + if (this.focusPosition.index > this.listsForRoomId[this.focusPosition.list].length) { + // move to the next sublist + } + } + else { + this.focusPosition.index--; + if (this.focusPosition.index < 0) { + // move to the previous sublist + } + } + } +*/ + // alternatively, we can just try to manually implementing the focus switch at the DOM level. + // ignores tabindex. + var element = this.focusedElement; + if (up) { + element = element.parentElement.previousElementSibling.firstElementChild; + } + else { + element = element.parentElement.nextElementSibling.firstElementChild; + } + + if (element) { + element.focus(); + } }, onSubListHeaderClick: function(isHidden, scrollToPosition) { @@ -208,19 +269,27 @@ module.exports = React.createClass({ // 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]; + var lists = this.listsForRoomId[room.roomId]; if (lists) { lists.forEach(list=>{ constantTimeDispatcher.dispatch("RoomSubList.sort", list, { room: room }); }); } + +/* + if (this.focusPosition && lists.indexOf(this.focusPosition.list) > -1) { + // if we're reordering the list which currently have focus, recalculate + // our focus offset + this.focusPosition = null; + } +*/ }, onRoomReceipt: function(receiptEvent, room) { // because if we read a notification, it will affect notification count // only bother updating if there's a receipt from us if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) { - var lists = this.listsByRoomId[room.roomId]; + var lists = this.listsForRoomId[room.roomId]; if (lists) { lists.forEach(list=>{ constantTimeDispatcher.dispatch( @@ -274,6 +343,12 @@ module.exports = React.createClass({ }, 500), refreshRoomList: function() { +/* + // if we're regenerating the list, then the chances are the contents + // or ordering is changing - forget our cached focus position + this.focusPosition = null; +*/ + // console.log("DEBUG: Refresh room list delta=%s ms", // (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs)) // ); @@ -299,16 +374,23 @@ module.exports = React.createClass({ s.lists["im.vector.fake.archived"] = []; this.listsForRoomId = {}; + var otherTagNames = {}; const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); MatrixClientPeg.get().getRooms().forEach(function(room) { const me = room.getMember(MatrixClientPeg.get().credentials.userId); if (!me) return; + // console.log("room = " + room.name + ", me.membership = " + me.membership + // ", sender = " + me.events.member.getSender() + // ", target = " + me.events.member.getStateKey() + // ", prevMembership = " + me.events.member.getPrevContent().membership); + + if (!self.listsForRoomId[room.roomId]) { + self.listsForRoomId[room.roomId] = []; + } + if (me.membership == "invite") { self.listsForRoomId[room.roomId].push("im.vector.fake.invite"); s.lists["im.vector.fake.invite"].push(room); @@ -325,8 +407,9 @@ module.exports = React.createClass({ for (var i = 0; i < tagNames.length; i++) { var tagName = tagNames[i]; s.lists[tagName] = s.lists[tagName] || []; - s.lists[tagNames[i]].push(room); - self.listsForRoomId[room.roomId].push(tagNames[i]); + s.lists[tagName].push(room); + self.listsForRoomId[room.roomId].push(tagName); + otherTagNames[tagName] = 1; } } else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { @@ -391,6 +474,21 @@ module.exports = React.createClass({ // 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; }, @@ -542,8 +640,35 @@ module.exports = React.createClass({ this.refs.gemscroll.forceUpdate(); }, - onRoomTileFocus: function(roomId) { - this.focusedRoomTileRoomId = roomId; + onRoomTileFocus: function(roomId, event) { + // this.focusedRoomTileRoomId = roomId; + this.focusedElement = event ? event.target : null; + + /* + if (roomId && !this.focusPosition) { + var list = this.listsForRoomId[roomId]; + if (list) { + console.warn("Focused to room " + roomId + " not in a list?!"); + } + else { + this.focusPosition = { + list: list, + index: this.state.lists[list].findIndex(room=>{ + return room.roomId == roomId; + }), + }; + } + } + + if (!roomId) { + if (this.focusMoving) { + this.focusMoving = false; + } + else { + this.focusPosition = null; + } + } + */ }, render: function() { @@ -608,7 +733,7 @@ module.exports = React.createClass({ onRoomTileFocus={ self.onRoomTileFocus } 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))$/)) { return Date: Tue, 18 Apr 2017 17:12:42 +0100 Subject: [PATCH 05/17] nudge focus shortcut code further to working --- src/components/views/rooms/RoomList.js | 132 ++++++++----------------- 1 file changed, 43 insertions(+), 89 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index cb692ff253..5d5caef95f 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -72,10 +72,7 @@ module.exports = React.createClass({ // order of the sublists this.listOrder = []; - // this.focusedRoomTileRoomId = null; this.focusedElement = null; - // this.focusedPosition = null; - // this.focusMoving = false; }, componentDidMount: function() { @@ -198,60 +195,58 @@ module.exports = React.createClass({ }, _onMoveFocus: function(up) { - // cheat and move focus by faking tab/shift-tab. This lets us do things - // like collapse/uncollapse room headers & truncated lists without having - // to reimplement the entirety of the keyboard navigation logic. - // - // this simply doens't work, as for security apparently you can't inject - // UI events any more - c.f. this note from - // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent - // - // Note: manually firing an event does not generate the default action - // associated with that event. For example, manually firing a key event - // does not cause that letter to appear in a focused text input. In the - // case of UI events, this is important for security reasons, as it - // prevents scripts from simulating user actions that interact with the - // browser itself. -/* - var event = document.createEvent('Event'); - event.initEvent('keydown', true, true); - event.keyCode = 9; - event.shiftKey = up ? true : false; - document.dispatchEvent(event); -*/ + var element = this.focusedElement; - // alternatively, this is the beginning of moving the focus through the list, - // navigating the pure datastructure of the list contents, but doesn't let - // you navigate through other things -/* - this.focusMoving = true; - if (this.focusPosition) { - if (up) { - this.focusPosition.index++; - if (this.focusPosition.index > this.listsForRoomId[this.focusPosition.list].length) { - // move to the next sublist + // unclear why this isn't needed... + // var descending = (up == this.focusDirection) ? this.focusDescending : !this.focusDescending; + // this.focusDirection = up; + + var descending = false; + var classes; + + do { + var child = up ? element.lastElementChild : element.firstElementChild; + var sibling = up ? element.previousElementSibling : element.nextElementSibling; + + if (descending) { + if (child) { + element = child; + } + else if (sibling) { + element = sibling; + } + else { + descending = false; + element = element.parentElement; } } else { - this.focusPosition.index--; - if (this.focusPosition.index < 0) { - // move to the previous sublist + if (sibling) { + element = sibling; + descending = true; + } + else { + element = element.parentElement; } } - } -*/ - // alternatively, we can just try to manually implementing the focus switch at the DOM level. - // ignores tabindex. - var element = this.focusedElement; - if (up) { - element = element.parentElement.previousElementSibling.firstElementChild; - } - else { - element = element.parentElement.nextElementSibling.firstElementChild; - } + + if (element) { + classes = element.classList; + if (classes.contains("mx_LeftPanel")) { // we hit the top + element = up ? element.lastElementChild : element.firstElementChild; + descending = true; + } + } + + } while(element && !( + classes.contains("mx_RoomTile") || + classes.contains("mx_SearchBox_search") || + classes.contains("mx_RoomSubList_ellipsis"))); if (element) { element.focus(); + this.focusedElement = element; + this.focusedDescending = descending; } }, @@ -275,14 +270,6 @@ module.exports = React.createClass({ constantTimeDispatcher.dispatch("RoomSubList.sort", list, { room: room }); }); } - -/* - if (this.focusPosition && lists.indexOf(this.focusPosition.list) > -1) { - // if we're reordering the list which currently have focus, recalculate - // our focus offset - this.focusPosition = null; - } -*/ }, onRoomReceipt: function(receiptEvent, room) { @@ -343,12 +330,6 @@ module.exports = React.createClass({ }, 500), refreshRoomList: function() { -/* - // if we're regenerating the list, then the chances are the contents - // or ordering is changing - forget our cached focus position - this.focusPosition = null; -*/ - // console.log("DEBUG: Refresh room list delta=%s ms", // (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs)) // ); @@ -641,34 +622,7 @@ module.exports = React.createClass({ }, onRoomTileFocus: function(roomId, event) { - // this.focusedRoomTileRoomId = roomId; this.focusedElement = event ? event.target : null; - - /* - if (roomId && !this.focusPosition) { - var list = this.listsForRoomId[roomId]; - if (list) { - console.warn("Focused to room " + roomId + " not in a list?!"); - } - else { - this.focusPosition = { - list: list, - index: this.state.lists[list].findIndex(room=>{ - return room.roomId == roomId; - }), - }; - } - } - - if (!roomId) { - if (this.focusMoving) { - this.focusMoving = false; - } - else { - this.focusPosition = null; - } - } - */ }, render: function() { From 062963b32f2aa4166c5e6145e12aa31d76ce1a9f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 18 Apr 2017 17:49:28 +0100 Subject: [PATCH 06/17] move focus-via-up/down cursors to LeftPanel --- src/components/views/rooms/RoomList.js | 81 -------------------------- 1 file changed, 81 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 5d5caef95f..3ea80837d6 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -27,7 +27,6 @@ var sdk = require('../../../index'); var rate_limited_func = require('../../../ratelimitedfunc'); var Rooms = require('../../../Rooms'); import DMRoomMap from '../../../utils/DMRoomMap'; -import KeyCode from '../../../KeyCode'; var Receipt = require('../../../utils/Receipt'); var constantTimeDispatcher = require('../../../ConstantTimeDispatcher'); @@ -71,15 +70,12 @@ module.exports = React.createClass({ // order of the sublists this.listOrder = []; - - this.focusedElement = null; }, componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); // Initialise the stickyHeaders when the component is created this._updateStickyHeaders(true); - document.addEventListener('keydown', this._onKeyDown); }, componentDidUpdate: function() { @@ -173,83 +169,6 @@ module.exports = React.createClass({ this._lastMouseOverTs = Date.now(); }, - _onKeyDown: function(ev) { - if (!this.focusedElement) return; - let handled = false; - - switch (ev.keyCode) { - case KeyCode.UP: - this._onMoveFocus(true); - handled = true; - break; - case KeyCode.DOWN: - this._onMoveFocus(false); - handled = true; - break; - } - - if (handled) { - ev.stopPropagation(); - ev.preventDefault(); - } - }, - - _onMoveFocus: function(up) { - var element = this.focusedElement; - - // unclear why this isn't needed... - // var descending = (up == this.focusDirection) ? this.focusDescending : !this.focusDescending; - // this.focusDirection = up; - - var descending = false; - var classes; - - do { - var child = up ? element.lastElementChild : element.firstElementChild; - var sibling = up ? element.previousElementSibling : element.nextElementSibling; - - if (descending) { - if (child) { - element = child; - } - else if (sibling) { - element = sibling; - } - else { - descending = false; - element = element.parentElement; - } - } - else { - if (sibling) { - element = sibling; - descending = true; - } - else { - element = element.parentElement; - } - } - - if (element) { - classes = element.classList; - if (classes.contains("mx_LeftPanel")) { // we hit the top - element = up ? element.lastElementChild : element.firstElementChild; - descending = true; - } - } - - } while(element && !( - classes.contains("mx_RoomTile") || - classes.contains("mx_SearchBox_search") || - classes.contains("mx_RoomSubList_ellipsis"))); - - if (element) { - element.focus(); - this.focusedElement = element; - this.focusedDescending = descending; - } - }, - onSubListHeaderClick: function(isHidden, scrollToPosition) { // The scroll area has expanded or contracted, so re-calculate sticky headers positions this._updateStickyHeaders(true, scrollToPosition); From c1c3956df4e2c03b738483fef59f44fd2dd749a7 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 18 Apr 2017 19:28:24 +0100 Subject: [PATCH 07/17] fix bugs, and handle shortcircuit react when updating roomtile --- src/components/structures/TimelinePanel.js | 4 +- src/components/views/rooms/RoomList.js | 44 +++++++++++++++------- src/components/views/rooms/RoomTile.js | 27 ++++++++----- 3 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 296565488c..3aec582f89 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -523,9 +523,7 @@ var TimelinePanel = React.createClass({ this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); dis.dispatch({ action: 'on_room_read', - payload: { - room: this.props.timelineSet.room - } + room: this.props.timelineSet.room, }); } } diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 3ea80837d6..739c288598 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -21,7 +21,6 @@ var GeminiScrollbar = require('react-gemini-scrollbar'); var MatrixClientPeg = require("../../../MatrixClientPeg"); var CallHandler = require('../../../CallHandler'); var RoomListSorter = require("../../../RoomListSorter"); -var Unread = require('../../../Unread'); var dis = require("../../../dispatcher"); var sdk = require('../../../index'); var rate_limited_func = require('../../../ratelimitedfunc'); @@ -38,7 +37,7 @@ module.exports = React.createClass({ propTypes: { ConferenceHandler: React.PropTypes.any, collapsed: React.PropTypes.bool.isRequired, - currentRoom: React.PropTypes.string, + selectedRoom: React.PropTypes.string, searchFilter: React.PropTypes.string, }, @@ -57,17 +56,16 @@ module.exports = React.createClass({ cli.on("Room.timeline", this.onRoomTimeline); cli.on("Room.name", this.onRoomName); cli.on("Room.tags", this.onRoomTags); - cli.on("Room.receipt", this.onRoomReceipt); // cli.on("RoomState.events", this.onRoomStateEvents); cli.on("RoomMember.name", this.onRoomMemberName); cli.on("accountData", this.onAccountData); - var s = this.getRoomLists(); - this.setState(s); - // lookup for which lists a given roomId is currently in. this.listsForRoomId = {}; + var s = this.getRoomLists(); + this.setState(s); + // order of the sublists this.listOrder = []; }, @@ -78,6 +76,21 @@ module.exports = React.createClass({ this._updateStickyHeaders(true); }, + 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() { // Reinitialise the stickyHeaders when the component is updated this._updateStickyHeaders(true); @@ -111,6 +124,17 @@ module.exports = React.createClass({ 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; } }, @@ -123,7 +147,6 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags); - MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); // MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName); MatrixClientPeg.get().removeListener("accountData", this.onAccountData); @@ -556,7 +579,6 @@ module.exports = React.createClass({ label="Invites" editable={ false } order="recent" - selectedRoom={ self.props.selectedRoom } incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } @@ -570,7 +592,6 @@ module.exports = React.createClass({ verb="favourite" editable={ true } order="manual" - selectedRoom={ self.props.selectedRoom } incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } @@ -584,7 +605,6 @@ module.exports = React.createClass({ verb="tag direct chat" editable={ true } order="recent" - selectedRoom={ self.props.selectedRoom } incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } alwaysShowHeader={ true } @@ -598,7 +618,6 @@ module.exports = React.createClass({ editable={ true } verb="restore" order="recent" - selectedRoom={ self.props.selectedRoom } incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } @@ -615,7 +634,6 @@ module.exports = React.createClass({ verb={ "tag as " + tagName } editable={ true } order="manual" - selectedRoom={ self.props.selectedRoom } incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } @@ -632,7 +650,6 @@ module.exports = React.createClass({ verb="demote" editable={ true } order="recent" - selectedRoom={ self.props.selectedRoom } incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } @@ -644,7 +661,6 @@ module.exports = React.createClass({ label="Historical" editable={ false } order="recent" - selectedRoom={ self.props.selectedRoom } collapsed={ self.props.collapsed } alwaysShowHeader={ true } startAsHidden={ true } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index fc91d10d76..db997fff3e 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -28,6 +28,7 @@ var FormattingUtils = require('../../../utils/FormattingUtils'); import AccessibleButton from '../elements/AccessibleButton'; var UserSettingsStore = require('../../../UserSettingsStore'); var constantTimeDispatcher = require('../../../ConstantTimeDispatcher'); +var Unread = require('../../../Unread'); module.exports = React.createClass({ displayName: 'RoomTile', @@ -41,9 +42,6 @@ module.exports = React.createClass({ room: React.PropTypes.object.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, incomingCall: React.PropTypes.object, }, @@ -91,19 +89,30 @@ module.exports = React.createClass({ componentWillMount: function() { constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh); + constantTimeDispatcher.register("RoomTile.select", this.props.room.roomId, this.onSelect); MatrixClientPeg.get().on("accountData", this.onAccountData); }, componentWillUnmount: function() { constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh); + constantTimeDispatcher.unregister("RoomTile.select", this.props.room.roomId, this.onSelect); var cli = MatrixClientPeg.get(); if (cli) { MatrixClientPeg.get().removeListener("accountData", this.onAccountData); } }, - onRefresh: function() { - this.forceUpdate(); + onRefresh: function(params) { + this.setState({ + unread: Unread.doesRoomHaveUnreadMessages(this.props.room), + highlight: this.props.room.getUnreadNotificationCount('highlight') > 0 || this.props.label === 'Invites', + }); + }, + + onSelect: function(params) { + this.setState({ + selected: params.selected, + }); }, onClick: function() { @@ -183,13 +192,13 @@ module.exports = React.createClass({ // var highlightCount = this.props.room.getUnreadNotificationCount("highlight"); const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge(); - const mentionBadges = this.props.highlight && this._shouldShowMentionBadge(); + const mentionBadges = this.state.highlight && this._shouldShowMentionBadge(); const badges = notifBadges || mentionBadges; var classes = classNames({ 'mx_RoomTile': true, - 'mx_RoomTile_selected': this.props.selected, - 'mx_RoomTile_unread': this.props.unread, + 'mx_RoomTile_selected': this.state.selected, + 'mx_RoomTile_unread': this.state.unread, 'mx_RoomTile_unreadNotify': notifBadges, 'mx_RoomTile_highlight': mentionBadges, 'mx_RoomTile_invited': (me && me.membership == 'invite'), @@ -235,7 +244,7 @@ module.exports = React.createClass({ 'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed, }); - if (this.props.selected) { + if (this.state.selected) { let nameSelected = {name}; label =
{ nameSelected }
; From 015a4480e29feb6e7aa9545947b6a03faf7b7ce0 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 18 Apr 2017 22:36:54 +0100 Subject: [PATCH 08/17] oops, wire up Room.receipt again, and refresh roomtiles on Room.timeline --- src/components/views/rooms/RoomList.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 739c288598..64871d1c0f 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -56,6 +56,7 @@ module.exports = React.createClass({ cli.on("Room.timeline", this.onRoomTimeline); cli.on("Room.name", this.onRoomName); cli.on("Room.tags", this.onRoomTags); + cli.on("Room.receipt", this.onRoomReceipt); // cli.on("RoomState.events", this.onRoomStateEvents); cli.on("RoomMember.name", this.onRoomMemberName); cli.on("accountData", this.onAccountData); @@ -147,6 +148,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags); + MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); // MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName); MatrixClientPeg.get().removeListener("accountData", this.onAccountData); @@ -212,6 +214,11 @@ module.exports = React.createClass({ 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) { @@ -226,6 +233,11 @@ module.exports = React.createClass({ ); }); } + + // we have to explicitly hit the roomtile which just changed + constantTimeDispatcher.dispatch( + "RoomTile.refresh", room.roomId, {} + ); } }, From 8389a67c758d4b9dc352b816eefdf388bed7d938 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 18 Apr 2017 22:54:30 +0100 Subject: [PATCH 09/17] we don't need RoomTile specific focus in the end --- src/components/views/rooms/RoomList.js | 11 ----------- src/components/views/rooms/RoomTile.js | 10 +--------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 64871d1c0f..25e19da770 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -575,10 +575,6 @@ module.exports = React.createClass({ this.refs.gemscroll.forceUpdate(); }, - onRoomTileFocus: function(roomId, event) { - this.focusedElement = event ? event.target : null; - }, - render: function() { var RoomSubList = sdk.getComponent('structures.RoomSubList'); var self = this; @@ -595,7 +591,6 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } - onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } /> { Object.keys(self.state.lists).sort().map(function(tagName) { @@ -650,7 +642,6 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } - onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } />; } @@ -666,7 +657,6 @@ module.exports = React.createClass({ collapsed={ self.props.collapsed } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } - onRoomTileFocus={ self.onRoomTileFocus } onShowMoreRooms={ self.onShowMoreRooms } />
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index db997fff3e..f18df52eee 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -37,7 +37,6 @@ module.exports = React.createClass({ connectDragSource: React.PropTypes.func, connectDropTarget: React.PropTypes.func, onClick: React.PropTypes.func, - onFocus: React.PropTypes.func, isDragging: React.PropTypes.bool, room: React.PropTypes.object.isRequired, @@ -121,12 +120,6 @@ module.exports = React.createClass({ } }, - onFocus: function(event) { - if (this.props.onFocus) { - this.props.onFocus(this.props.room.roomId, event); - } - }, - onMouseEnter: function() { this.setState( { hover : true }); this.badgeOnMouseEnter(); @@ -279,8 +272,7 @@ module.exports = React.createClass({ let ret = (
{ /* Only native elements can be wrapped in a DnD object. */} + onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
From 093b9a0b52a3dfbee682f224e4fe6ca23565a38f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 18 Apr 2017 23:29:28 +0100 Subject: [PATCH 10/17] kick the roomtile on RoomState.members --- src/components/views/rooms/RoomList.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 25e19da770..e510de08a4 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -57,7 +57,7 @@ module.exports = React.createClass({ cli.on("Room.name", this.onRoomName); cli.on("Room.tags", this.onRoomTags); 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("accountData", this.onAccountData); @@ -149,7 +149,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags); 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("accountData", this.onAccountData); } @@ -253,9 +253,11 @@ module.exports = React.createClass({ this._delayedRefreshRoomList(); }, - // onRoomStateEvents: function(ev, state) { - // this._delayedRefreshRoomList(); - // }, + onRoomStateMember: function(ev, state, member) { + constantTimeDispatcher.dispatch( + "RoomTile.refresh", member.roomId, {} + ); + }, onRoomMemberName: function(ev, member) { constantTimeDispatcher.dispatch( From abf2300c0d37a8785ff9813dca3972e9e7a090b5 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 19 Apr 2017 00:09:03 +0100 Subject: [PATCH 11/17] highlight invites correctly --- src/components/views/rooms/RoomTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index f18df52eee..31ffdf7e12 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -104,7 +104,7 @@ module.exports = React.createClass({ onRefresh: function(params) { this.setState({ unread: Unread.doesRoomHaveUnreadMessages(this.props.room), - highlight: this.props.room.getUnreadNotificationCount('highlight') > 0 || this.props.label === 'Invites', + highlight: this.props.room.getUnreadNotificationCount('highlight') > 0 || this.props.isInvite, }); }, From 4a9c16868249a6685e2219189fe2a7186d168362 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 19 Apr 2017 00:13:01 +0100 Subject: [PATCH 12/17] fix invite highlights --- src/components/views/rooms/RoomTile.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 31ffdf7e12..dc2d9a4b25 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -101,6 +101,10 @@ module.exports = React.createClass({ } }, + componentWillReceiveProps: function(nextProps) { + this.onRefresh(); + }, + onRefresh: function(params) { this.setState({ unread: Unread.doesRoomHaveUnreadMessages(this.props.room), From fb6252a16b6d324a7e7b9cf99c716d8eaf2050b7 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 19 Apr 2017 00:16:17 +0100 Subject: [PATCH 13/17] fix invite highlights take 3 --- src/components/views/rooms/RoomTile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index dc2d9a4b25..1f6063e37c 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -90,6 +90,7 @@ module.exports = React.createClass({ constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh); constantTimeDispatcher.register("RoomTile.select", this.props.room.roomId, this.onSelect); MatrixClientPeg.get().on("accountData", this.onAccountData); + this.onRefresh(); }, componentWillUnmount: function() { From 9f99224a1fee849426ca184e72eedba9c3626f32 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 19 Apr 2017 17:59:06 +0100 Subject: [PATCH 14/17] fix bugs from PR review --- src/components/views/rooms/RoomList.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index e510de08a4..3916261dda 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -75,6 +75,12 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); // Initialise the stickyHeaders when the component is created this._updateStickyHeaders(true); + + if (this.props.selectedRoom) { + constantTimeDispatcher.dispatch( + "RoomTile.select", this.props.selectedRoom, { selected: true } + ); + } }, componentWillReceiveProps: function(nextProps) { @@ -155,8 +161,6 @@ module.exports = React.createClass({ } // cancel any pending calls to the rate_limited_funcs this._delayedRefreshRoomList.cancelPendingCall(); - document.removeEventListener('keydown', this._onKeyDown); - }, onRoom: function(room) { From 8da07740d1efb3b3b0389b29eacae1e73c86a344 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 19 Apr 2017 23:34:29 +0100 Subject: [PATCH 15/17] bump react-gemini-scrollbar --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cb3cdfa63f..5c96a74f5b 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "react": "^15.4.0", "react-addons-css-transition-group": "15.3.2", "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", "text-encoding-utf-8": "^1.0.1", "velocity-vector": "vector-im/velocity#059e3b2", From 90f526bdeba68c201fdd7535ace3fe23fc5034a7 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 20 Apr 2017 00:42:13 +0100 Subject: [PATCH 16/17] autofocus doesn't seem to work on this button --- src/components/views/dialogs/QuestionDialog.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/QuestionDialog.js b/src/components/views/dialogs/QuestionDialog.js index 6012541b94..8e20b0d2bc 100644 --- a/src/components/views/dialogs/QuestionDialog.js +++ b/src/components/views/dialogs/QuestionDialog.js @@ -47,6 +47,12 @@ export default React.createClass({ this.props.onFinished(false); }, + componentDidMount: function() { + if (this.props.focus) { + this.refs.button.focus(); + } + }, + render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const cancelButton = this.props.hasCancelButton ? ( @@ -63,7 +69,7 @@ export default React.createClass({ {this.props.description}
- {this.props.extraButtons} From 5a3b4b6a60bcc198e8d15d15c0e8e8499522854f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 20 Apr 2017 01:12:57 +0100 Subject: [PATCH 17/17] various bug fixes: don't redraw RoomList when the selectedRoom changes keep passing selectedRoom through to RoomTiles so they have correct initial state handle onAccountData at the RoomList, not RoomTile level Fix some typos --- src/components/views/rooms/RoomList.js | 26 ++++++++++++++++++-------- src/components/views/rooms/RoomTile.js | 21 +++++---------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 3916261dda..979b14eaaf 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -41,6 +41,12 @@ module.exports = React.createClass({ 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() { return { isLoadingLeftRooms: false, @@ -75,12 +81,6 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); // Initialise the stickyHeaders when the component is created this._updateStickyHeaders(true); - - if (this.props.selectedRoom) { - constantTimeDispatcher.dispatch( - "RoomTile.select", this.props.selectedRoom, { selected: true } - ); - } }, componentWillReceiveProps: function(nextProps) { @@ -98,7 +98,7 @@ module.exports = React.createClass({ } }, - componentDidUpdate: function() { + componentDidUpdate: function(prevProps, prevState) { // Reinitialise the stickyHeaders when the component is updated this._updateStickyHeaders(true); this._repositionIncomingCallBox(undefined, false); @@ -265,7 +265,7 @@ module.exports = React.createClass({ onRoomMemberName: function(ev, member) { constantTimeDispatcher.dispatch( - "RoomTile.refresh", member.room.roomId, {} + "RoomTile.refresh", member.roomId, {} ); }, @@ -275,6 +275,9 @@ module.exports = React.createClass({ // sublists when it does (e.g. via a constantTimeDispatch to the right sublist) this._delayedRefreshRoomList(); } + else if (ev.getType() == 'm.push_rules') { + this._delayedRefreshRoomList(); + } }, _delayedRefreshRoomList: new rate_limited_func(function() { @@ -595,6 +598,7 @@ module.exports = React.createClass({ order="recent" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } onShowMoreRooms={ self.onShowMoreRooms } /> @@ -607,6 +611,7 @@ module.exports = React.createClass({ order="manual" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } onShowMoreRooms={ self.onShowMoreRooms } /> @@ -619,6 +624,7 @@ module.exports = React.createClass({ order="recent" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } alwaysShowHeader={ true } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } @@ -631,6 +637,7 @@ module.exports = React.createClass({ order="recent" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } onShowMoreRooms={ self.onShowMoreRooms } /> @@ -646,6 +653,7 @@ module.exports = React.createClass({ order="manual" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } onShowMoreRooms={ self.onShowMoreRooms } />; @@ -661,6 +669,7 @@ module.exports = React.createClass({ order="recent" incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } searchFilter={ self.props.searchFilter } onHeaderClick={ self.onSubListHeaderClick } onShowMoreRooms={ self.onShowMoreRooms } /> @@ -670,6 +679,7 @@ module.exports = React.createClass({ editable={ false } order="recent" collapsed={ self.props.collapsed } + selectedRoom={ self.props.selectedRoom } alwaysShowHeader={ true } startAsHidden={ true } showSpinner={ self.state.isLoadingLeftRooms } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 1f6063e37c..5d896e8beb 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -38,6 +38,7 @@ module.exports = React.createClass({ connectDropTarget: React.PropTypes.func, onClick: React.PropTypes.func, isDragging: React.PropTypes.bool, + selectedRoom: React.PropTypes.string, room: React.PropTypes.object.isRequired, collapsed: React.PropTypes.bool.isRequired, @@ -53,10 +54,11 @@ module.exports = React.createClass({ getInitialState: function() { return({ - hover : false, - badgeHover : false, + hover: false, + badgeHover: false, menuDisplayed: false, notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), + selected: this.props.room ? (this.props.selectedRoom === this.props.room.roomId) : false, }); }, @@ -78,28 +80,15 @@ module.exports = React.createClass({ } }, - onAccountData: function(accountDataEvent) { - if (accountDataEvent.getType() == 'm.push_rules') { - this.setState({ - notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId), - }); - } - }, - componentWillMount: function() { constantTimeDispatcher.register("RoomTile.refresh", this.props.room.roomId, this.onRefresh); constantTimeDispatcher.register("RoomTile.select", this.props.room.roomId, this.onSelect); - MatrixClientPeg.get().on("accountData", this.onAccountData); - this.onRefresh(); + this.onRefresh(); }, componentWillUnmount: function() { constantTimeDispatcher.unregister("RoomTile.refresh", this.props.room.roomId, this.onRefresh); constantTimeDispatcher.unregister("RoomTile.select", this.props.room.roomId, this.onSelect); - var cli = MatrixClientPeg.get(); - if (cli) { - MatrixClientPeg.get().removeListener("accountData", this.onAccountData); - } }, componentWillReceiveProps: function(nextProps) {