From f7aa8be1c1792fdd265afd1acf1d5721af0cf567 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 16 Dec 2015 16:06:29 +0000 Subject: [PATCH 1/8] Add a forget button. Add left rooms to the "historical" tab. Call /forget when the forget button is clicked. Number of shortcomings: - We need to lazy load the historical list (atm we never get the list of left rooms; things only go into that list if you leave the room whilst running) - Once a room is forgotten we need to physically nuke it from the JS SDK. - Need icon for forget room. --- src/components/structures/RoomView.js | 23 ++++++++++++++++++++--- src/components/views/rooms/RoomHeader.js | 13 ++++++++++++- src/components/views/rooms/RoomList.js | 3 +++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 6db2659986..072742cf10 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -824,7 +824,14 @@ module.exports = React.createClass({ action: 'leave_room', room_id: this.props.roomId, }); - this.props.onFinished(); + }, + + onForgetClick: function() { + MatrixClientPeg.get().forget(this.props.roomId).done(function() { + dis.dispatch({ action: 'view_next_room' }); + }, function(err) { + console.error("Failed to forget room: %s", err); + }); }, onRejectButtonClicked: function(ev) { @@ -1249,8 +1256,18 @@ module.exports = React.createClass({ return (
- + { fileDropTarget }
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 068dff85d6..aaf570305c 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -129,7 +129,17 @@ module.exports = React.createClass({ if (this.props.onLeaveClick) { leave_button =
- Leave room + Leave room +
; + } + + var forget_button; + if (this.props.onForgetClick) { + forget_button = +
+ Forget room
; } @@ -147,6 +157,7 @@ module.exports = React.createClass({ {cancel_button} {save_button}
+ { forget_button } { leave_button }
Search diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index a89dd55f1a..c48ed5880f 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -168,6 +168,9 @@ module.exports = React.createClass({ if (me && me.membership == "invite") { s.lists["im.vector.fake.invite"].push(room); } + else if (me && (me.membership === "leave" || me.membership === "ban")) { + s.lists["im.vector.fake.archived"].push(room); + } else { var shouldShowRoom = ( me && (me.membership == "join") From e8f82527d1435e821ceaa284fd1bac1367bb1f18 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 16 Dec 2015 16:27:46 +0000 Subject: [PATCH 2/8] Listen for room deletions and refresh the room list when it happens --- src/components/views/rooms/RoomList.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index c48ed5880f..f7dd48d871 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -45,6 +45,7 @@ module.exports = React.createClass({ componentWillMount: function() { var cli = MatrixClientPeg.get(); cli.on("Room", this.onRoom); + cli.on("deleteRoom", this.onDeleteRoom); cli.on("Room.timeline", this.onRoomTimeline); cli.on("Room.name", this.onRoomName); cli.on("Room.tags", this.onRoomTags); @@ -91,6 +92,10 @@ module.exports = React.createClass({ this.refreshRoomList(); }, + onDeleteRoom: function(roomId) { + this.refreshRoomList(); + }, + onRoomTimeline: function(ev, room, toStartOfTimeline) { if (toStartOfTimeline) return; From 22635f251d6b546f7d41d47406e56437209e2626 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 18 Dec 2015 11:55:43 +0000 Subject: [PATCH 3/8] Call through to syncLeftRooms when the archived header is clicked --- src/components/views/rooms/RoomList.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index dfdeb0145b..758d3500dd 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -96,6 +96,17 @@ module.exports = React.createClass({ this.refreshRoomList(); }, + onArchivedHeaderClick: function(isHidden) { + if (!isHidden) { + // we don't care about the response since it comes down via "Room" + // events. + MatrixClientPeg.get().syncLeftRooms().catch(function(err) { + console.error("Failed to sync left rooms: %s", err); + console.error(err); + }); + } + }, + onRoomTimeline: function(ev, room, toStartOfTimeline) { if (toStartOfTimeline) return; @@ -295,7 +306,7 @@ module.exports = React.createClass({ verb="demote" editable={ true } order="recent" - bottommost={ self.state.lists['im.vector.fake.archived'].length === 0 } + bottommost={ false } activityMap={ self.state.activityMap } selectedRoom={ self.props.selectedRoom } collapsed={ self.props.collapsed } /> @@ -307,7 +318,9 @@ module.exports = React.createClass({ bottommost={ true } activityMap={ self.state.activityMap } selectedRoom={ self.props.selectedRoom } - collapsed={ self.props.collapsed } /> + collapsed={ self.props.collapsed } + alwaysShowHeader={ true } + onHeaderClick= { self.onArchivedHeaderClick } />
); From 711fdd25afa4813829d7b46934843482554a7429 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 18 Dec 2015 15:13:59 +0000 Subject: [PATCH 4/8] Improve perf of refreshing room list. Show spinner when loading left rooms. When the JS SDK encounters a new room it will emit a flurry of events for things like state and room members. Refreshing the room list on each event is bad for performance. This is okay initially because the room list is only shown after the first sync, but when getting archived rooms it locks up for 15-30s as it thrashes. Add a 1s cap to refreshRoomList() which means that it will refresh *AT MOST* once every second. If it has been >1s since the last refresh it will immediately refresh. If it has been <1s it will wait the difference. --- src/components/views/rooms/RoomList.js | 55 +++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 758d3500dd..af20b25421 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -38,6 +38,7 @@ module.exports = React.createClass({ getInitialState: function() { return { activityMap: null, + isLoadingLeftRooms: false, lists: {}, } }, @@ -89,20 +90,24 @@ module.exports = React.createClass({ }, onRoom: function(room) { - this.refreshRoomList(); + this._delayedRefreshRoomList(); }, onDeleteRoom: function(roomId) { - this.refreshRoomList(); + this._delayedRefreshRoomList(); }, onArchivedHeaderClick: function(isHidden) { if (!isHidden) { + var self = this; + this.setState({ isLoadingLeftRooms: true }); // we don't care about the response since it comes down via "Room" // events. MatrixClientPeg.get().syncLeftRooms().catch(function(err) { console.error("Failed to sync left rooms: %s", err); console.error(err); + }).finally(function() { + self.setState({ isLoadingLeftRooms: false }); }); } }, @@ -143,22 +148,57 @@ module.exports = React.createClass({ }, onRoomName: function(room) { - this.refreshRoomList(); + this._delayedRefreshRoomList(); }, onRoomTags: function(event, room) { - this.refreshRoomList(); + this._delayedRefreshRoomList(); }, onRoomStateEvents: function(ev, state) { - setTimeout(this.refreshRoomList, 0); + this._delayedRefreshRoomList(); }, onRoomMemberName: function(ev, member) { - setTimeout(this.refreshRoomList, 0); + this._delayedRefreshRoomList(); + }, + + _delayedRefreshRoomList: function() { + // There can be 1000s of JS SDK events when rooms are initially synced; + // we don't want to do lots of work rendering until things have settled. + // Therefore, keep a 1s refresh buffer which will refresh the room list + // at MOST once every 1s to prevent thrashing. + var MAX_REFRESH_INTERVAL_MS = 1000; + var self = this; + + if (!self._lastRefreshRoomListTs) { + self.refreshRoomList(); // first refresh evar + } + else { + var timeWaitedMs = Date.now() - self._lastRefreshRoomListTs; + if (timeWaitedMs > MAX_REFRESH_INTERVAL_MS) { + clearTimeout(self._refreshRoomListTimerId); + self._refreshRoomListTimerId = null; + self.refreshRoomList(); // refreshed more than MAX_REFRESH_INTERVAL_MS ago + } + else { + // refreshed less than MAX_REFRESH_INTERVAL_MS ago, wait the difference + // if we aren't already waiting. If we are waiting then NOP, it will + // fire soon, promise! + if (!self._refreshRoomListTimerId) { + self._refreshRoomListTimerId = setTimeout(function() { + self.refreshRoomList(); + }, 10 + MAX_REFRESH_INTERVAL_MS - timeWaitedMs); // 10 is a buffer amount + } + } + } }, refreshRoomList: function() { + // console.log("DEBUG: Refresh room list delta=%s ms", + // (!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 @@ -166,6 +206,7 @@ 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(); }, getRoomLists: function() { @@ -320,6 +361,8 @@ module.exports = React.createClass({ selectedRoom={ self.props.selectedRoom } collapsed={ self.props.collapsed } alwaysShowHeader={ true } + startAsHidden={ true } + showSpinner={ self.state.isLoadingLeftRooms } onHeaderClick= { self.onArchivedHeaderClick } />
From c3bd81b83aa187a89feda3eb106b429d04723f84 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 18 Dec 2015 15:56:27 +0000 Subject: [PATCH 5/8] Make rooms the user is banned in be treated as a joined room for position in room list This is so users can still find the room they've been expelled from, rather than have it drop to the Historical section. --- src/components/views/rooms/RoomList.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index af20b25421..887f6adb5e 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -225,12 +225,12 @@ module.exports = React.createClass({ if (me && me.membership == "invite") { s.lists["im.vector.fake.invite"].push(room); } - else if (me && (me.membership === "leave" || me.membership === "ban")) { + else if (me && me.membership === "leave") { s.lists["im.vector.fake.archived"].push(room); } else { var shouldShowRoom = ( - me && (me.membership == "join") + me && (me.membership == "join" || me.membership === "ban") ); // hiding conf rooms only ever toggles shouldShowRoom to false From 461e3f46dce30d3a122b77add900813010951e64 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 18 Dec 2015 16:56:37 +0000 Subject: [PATCH 6/8] Show an ErrorDialog when failing to forget a room --- src/components/structures/RoomView.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 66a60ba8e1..04aa81d3e1 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -871,7 +871,12 @@ module.exports = React.createClass({ MatrixClientPeg.get().forget(this.props.roomId).done(function() { dis.dispatch({ action: 'view_next_room' }); }, function(err) { - console.error("Failed to forget room: %s", err); + var errCode = err.errcode || "unknown error code"; + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Error", + description: `Failed to forget room (${errCode})` + }); }); }, From f0ff62166b3620de2f24bafdd0ca510085f87e3b Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 18 Dec 2015 17:13:26 +0000 Subject: [PATCH 7/8] Remove bottommost prop - can't DND on the bottom list anymore --- src/components/views/rooms/RoomList.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index f1d467f5ab..6a19e21d27 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -415,7 +415,6 @@ module.exports = React.createClass({ verb="demote" editable={ true } order="recent" - bottommost={ false } activityMap={ self.state.activityMap } selectedRoom={ self.props.selectedRoom } incomingCall={ self.state.incomingCall } @@ -425,7 +424,6 @@ module.exports = React.createClass({ label="Historical" editable={ false } order="recent" - bottommost={ true } activityMap={ self.state.activityMap } selectedRoom={ self.props.selectedRoom } collapsed={ self.props.collapsed } From d1baf5854cf2b16162e63db5ec6ff15ea2f505fc Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 18 Dec 2015 17:23:46 +0000 Subject: [PATCH 8/8] Only display the MessageComposer if you're joined and not viewing search results --- src/components/structures/RoomView.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 683076d932..fc8eed490c 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1279,16 +1279,23 @@ module.exports = React.createClass({ } var messageComposer, searchInfo; - if (!this.state.searchResults) { + var canSpeak = ( + // joined and not showing search results + myMember && (myMember.membership == 'join') && !this.state.searchResults + ); + if (canSpeak) { messageComposer = } - else { + + // TODO: Why aren't we storing the term/scope/count in this format + // in this.state if this is what RoomHeader desires? + if (this.state.searchResults) { searchInfo = { searchTerm : this.state.searchTerm, searchScope : this.state.searchScope, searchCount : this.state.searchCount, - } + }; } var call = CallHandler.getCallForRoom(this.props.roomId);