From a850f19cd40486a6328ac93c49fa1be3fe7d1945 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 26 Oct 2015 13:54:54 +0000 Subject: [PATCH 01/12] Separate out the activity watcher from presence code so I can hook read receipts into it without tangling it into the presence code. --- src/Presence.js | 66 +++++++++++++++-------------- src/UserActivity.js | 57 +++++++++++++++++++++++++ src/controllers/pages/MatrixChat.js | 3 ++ 3 files changed, 95 insertions(+), 31 deletions(-) create mode 100644 src/UserActivity.js diff --git a/src/Presence.js b/src/Presence.js index d77058abd8..1f5617514a 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -15,58 +15,54 @@ limitations under the License. */ var MatrixClientPeg = require("./MatrixClientPeg"); +var dis = require("./dispatcher"); // Time in ms after that a user is considered as unavailable/away var UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins var PRESENCE_STATES = ["online", "offline", "unavailable"]; -// The current presence state -var state, timer; - -module.exports = { +class Presence { /** * Start listening the user activity to evaluate his presence state. * Any state change will be sent to the Home Server. */ - start: function() { - var self = this; + start() { this.running = true; - if (undefined === state) { - // The user is online if they move the mouse or press a key - document.onmousemove = function() { self._resetTimer(); }; - document.onkeypress = function() { self._resetTimer(); }; + if (undefined === this.state) { this._resetTimer(); + this.dispatcherRef = dis.register(this._onUserActivity.bind(this)); } - }, + } /** * Stop tracking user activity */ - stop: function() { + stop() { this.running = false; - if (timer) { - clearTimeout(timer); - timer = undefined; + if (this.timer) { + clearInterval(this.timer); + this.timer = undefined; + dis.unregister(this.dispatcherRef); } - state = undefined; - }, + this.state = undefined; + } /** * Get the current presence state. * @returns {string} the presence state (see PRESENCE enum) */ - getState: function() { - return state; - }, + getState() { + return this.state; + } /** * Set the presence state. * If the state has changed, the Home Server will be notified. * @param {string} newState the new presence state (see PRESENCE enum) */ - setState: function(newState) { - if (newState === state) { + setState(newState) { + if (newState === this.state) { return; } if (PRESENCE_STATES.indexOf(newState) === -1) { @@ -75,33 +71,41 @@ module.exports = { if (!this.running) { return; } - state = newState; - MatrixClientPeg.get().setPresence(state).done(function() { + var old_state = this.state; + this.state = newState; + MatrixClientPeg.get().setPresence(this.state).done(function() { console.log("Presence: %s", newState); }, function(err) { console.error("Failed to set presence: %s", err); + this.state = old_state; }); - }, + } /** * Callback called when the user made no action on the page for UNAVAILABLE_TIME ms. * @private */ - _onUnavailableTimerFire: function() { + _onUnavailableTimerFire() { this.setState("unavailable"); - }, + } + + _onUserActivity() { + this._resetTimer(); + } /** * Callback called when the user made an action on the page * @private */ - _resetTimer: function() { + _resetTimer() { var self = this; this.setState("online"); // Re-arm the timer - clearTimeout(timer); - timer = setTimeout(function() { + clearTimeout(this.timer); + this.timer = setTimeout(function() { self._onUnavailableTimerFire(); }, UNAVAILABLE_TIME_MS); } -}; +} + +module.exports = new Presence(); diff --git a/src/UserActivity.js b/src/UserActivity.js new file mode 100644 index 0000000000..46a46f0b0e --- /dev/null +++ b/src/UserActivity.js @@ -0,0 +1,57 @@ +/* +Copyright 2015 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +var dis = require("./dispatcher"); + +var MIN_DISPATCH_INTERVAL = 1 * 1000; + +/** + * This class watches for user activity (moving the mouse or pressing a key) + * and dispatches the user_activity action at times when the user is interacting + * with the app (but at a much lower frequency than mouse move events) + */ +class UserActivity { + + /** + * Start listening to user activity + */ + start() { + document.onmousemove = this._onUserActivity.bind(this); + document.onkeypress = this._onUserActivity.bind(this); + this.lastActivityAt = (new Date).getTime(); + this.lastDispatchAt = 0; + } + + /** + * Stop tracking user activity + */ + stop() { + document.onmousemove = undefined; + document.onkeypress = undefined; + } + + _onUserActivity() { + this.lastActivityAt = (new Date).getTime(); + if (this.lastDispatchAt < this.lastActivityAt - MIN_DISPATCH_INTERVAL) { + this.lastDispatchAt = this.lastActivityAt; + dis.dispatch({ + action: 'user_activity' + }); + } + } +} + +module.exports = new UserActivity(); diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index 2a712b22f6..97fc3a1fe1 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/controllers/pages/MatrixChat.js @@ -16,6 +16,7 @@ limitations under the License. var MatrixClientPeg = require("../../MatrixClientPeg"); var RoomListSorter = require("../../RoomListSorter"); +var UserActivity = require("../../UserActivity"); var Presence = require("../../Presence"); var dis = require("../../dispatcher"); @@ -92,6 +93,7 @@ module.exports = { window.localStorage.clear(); } Notifier.stop(); + UserActivity.stop(); Presence.stop(); MatrixClientPeg.get().stopClient(); MatrixClientPeg.get().removeAllListeners(); @@ -316,6 +318,7 @@ module.exports = { }); }); Notifier.start(); + UserActivity.start(); Presence.start(); cli.startClient(); }, From 2365fe8ceb92d908e3050019cfb9f25ce7d3b002 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 28 Oct 2015 15:15:35 +0000 Subject: [PATCH 02/12] Refresh room & member avatars when a roommember.name event comes in --- src/controllers/atoms/MemberAvatar.js | 20 ++++++++++++++++-- src/controllers/atoms/RoomAvatar.js | 27 +++++++++++++++++++++---- src/controllers/organisms/MemberList.js | 5 +++++ src/controllers/organisms/RoomList.js | 5 +++++ src/controllers/pages/MatrixChat.js | 2 +- 5 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/controllers/atoms/MemberAvatar.js b/src/controllers/atoms/MemberAvatar.js index a94b4291a0..e170d2e04c 100644 --- a/src/controllers/atoms/MemberAvatar.js +++ b/src/controllers/atoms/MemberAvatar.js @@ -35,6 +35,10 @@ module.exports = { } }, + componentWillReceiveProps: function(nextProps) { + this.refreshUrl(); + }, + defaultAvatarUrl: function(member, width, height, resizeMethod) { if (this.skinnedDefaultAvatarUrl) { return this.skinnedDefaultAvatarUrl(member, width, height, resizeMethod); @@ -52,7 +56,7 @@ module.exports = { }); }, - getInitialState: function() { + _computeUrl: function() { var url = this.props.member.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), this.props.width, @@ -68,8 +72,20 @@ module.exports = { this.props.resizeMethod ); } + return url; + }, + + refreshUrl: function() { + var newUrl = this._computeUrl(); + if (newUrl != this.currentUrl) { + this.currentUrl = newUrl; + this.setState({imageUrl: newUrl}); + } + }, + + getInitialState: function() { return { - imageUrl: url + imageUrl: this._computeUrl() }; } }; diff --git a/src/controllers/atoms/RoomAvatar.js b/src/controllers/atoms/RoomAvatar.js index 6c55345ead..061a12eb14 100644 --- a/src/controllers/atoms/RoomAvatar.js +++ b/src/controllers/atoms/RoomAvatar.js @@ -41,10 +41,29 @@ module.exports = { }, componentWillReceiveProps: function(nextProps) { - this._update(); - this.setState({ - imageUrl: this._nextUrl() - }); + this.refreshImageUrl(); + }, + + refreshImageUrl: function(nextProps) { + // If the list has changed, we start from scratch and re-check, but + // don't do so unless the list has changed or we'd re-try fetching + // images each time we re-rendered + var newList = this.getUrlList(); + var differs = false; + for (var i = 0; i < newList.length && i < this.urlList.length; ++i) { + if (this.urlList[i] != newList[i]) differs = true; + } + if (this.urlList.length != newList.length) differs = true; + + if (differs) { + console.log("list differs"); + this._update(); + this.setState({ + imageUrl: this._nextUrl() + }); + } else { + console.log("list is the same"); + } }, _update: function() { diff --git a/src/controllers/organisms/MemberList.js b/src/controllers/organisms/MemberList.js index 48fef531bb..a2298a20ba 100644 --- a/src/controllers/organisms/MemberList.js +++ b/src/controllers/organisms/MemberList.js @@ -38,6 +38,7 @@ module.exports = { componentWillMount: function() { var cli = MatrixClientPeg.get(); cli.on("RoomState.members", this.onRoomStateMember); + cli.on("RoomMember.name", this.onRoomMemberName); cli.on("Room", this.onRoom); // invites }, @@ -97,6 +98,10 @@ module.exports = { this._updateList(); }, + onRoomMemberName: function(ev, member) { + this._updateList(); + }, + _updateList: function() { this.memberDict = this.getMemberDict(); diff --git a/src/controllers/organisms/RoomList.js b/src/controllers/organisms/RoomList.js index ff3522d9c3..6b5a4c4722 100644 --- a/src/controllers/organisms/RoomList.js +++ b/src/controllers/organisms/RoomList.js @@ -29,6 +29,7 @@ module.exports = { cli.on("Room.timeline", this.onRoomTimeline); cli.on("Room.name", this.onRoomName); cli.on("RoomState.events", this.onRoomStateEvents); + cli.on("RoomMember.name", this.onRoomMemberName); var rooms = this.getRoomList(); this.setState({ @@ -89,6 +90,10 @@ module.exports = { this.refreshRoomList(); }, + onRoomMemberName: function(ev, member) { + this.refreshRoomList(); + }, + refreshRoomList: function() { var rooms = this.getRoomList(); this.setState({ diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index 97fc3a1fe1..edb55eb1b0 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/controllers/pages/MatrixChat.js @@ -320,7 +320,7 @@ module.exports = { Notifier.start(); UserActivity.start(); Presence.start(); - cli.startClient(); + cli.startClient({resolveInvitesToProfiles: true}); }, onKeyDown: function(ev) { From c46f40c816ff63cdaee7ca3839583ac6b78aca51 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 28 Oct 2015 18:02:50 +0000 Subject: [PATCH 03/12] bump js-sdk -> 0.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f28cfdf215..ff7d75e712 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "flux": "^2.0.3", "glob": "^5.0.14", "linkifyjs": "^2.0.0-beta.4", - "matrix-js-sdk": "^0.2.2", + "matrix-js-sdk": "^0.3.0", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^0.13.3", From 7c9b773bf8bb162d3e16c662d626d5c7ddf3e9e5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 3 Nov 2015 11:22:18 +0000 Subject: [PATCH 04/12] unintentionally comitted logging --- src/controllers/atoms/RoomAvatar.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/controllers/atoms/RoomAvatar.js b/src/controllers/atoms/RoomAvatar.js index 061a12eb14..57c9a71842 100644 --- a/src/controllers/atoms/RoomAvatar.js +++ b/src/controllers/atoms/RoomAvatar.js @@ -56,13 +56,10 @@ module.exports = { if (this.urlList.length != newList.length) differs = true; if (differs) { - console.log("list differs"); this._update(); this.setState({ imageUrl: this._nextUrl() }); - } else { - console.log("list is the same"); } }, From 5a72f199e1b69f0acfb6f6a50f593849e4a3e319 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 3 Nov 2015 11:41:18 +0000 Subject: [PATCH 05/12] listen for read receipts --- src/controllers/organisms/RoomView.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index 931dbb5bcb..925f896556 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -43,6 +43,7 @@ module.exports = { this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.name", this.onRoomName); + MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping); this.atBottom = true; }, @@ -59,6 +60,7 @@ module.exports = { if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); + MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping); } }, @@ -149,6 +151,12 @@ module.exports = { } }, + onRoomReceipt: function(receiptEvent, room) { + if (room.roomId == this.props.roomId) { + this.forceUpdate(); + } + }, + onRoomMemberTyping: function(ev, member) { this.forceUpdate(); }, From 86ef0e762ed50773d307e6f66d598bef4df311f5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 3 Nov 2015 14:08:51 +0000 Subject: [PATCH 06/12] Merge code to send read receipts into react-sdk RoomView controller --- src/controllers/organisms/RoomView.js | 55 ++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index 925f896556..2dc9e1bf26 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -89,6 +89,9 @@ module.exports = { messageWrapper.scrollTop = messageWrapper.scrollHeight; } break; + case 'user_activity': + this.sendReadReceipt(); + break; } }, @@ -172,6 +175,8 @@ module.exports = { messageWrapper.scrollTop = messageWrapper.scrollHeight; + this.sendReadReceipt(); + this.fillSpace(); } }, @@ -354,7 +359,7 @@ module.exports = { } } ret.unshift( -
  • +
  • ); ++count; } @@ -446,5 +451,53 @@ module.exports = { uploadingRoomSettings: false, }); } + }, + + _collectEventNode: function(eventId, node) { + if (this.eventNodes == undefined) this.eventNodes = {}; + this.eventNodes[eventId] = node; + }, + + _indexForEventId(evId) { + for (var i = 0; i < this.state.room.timeline.length; ++i) { + if (evId == this.state.room.timeline[i].getId()) { + return i; + } + } + return null; + }, + + sendReadReceipt: function() { + var currentReadUpToEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId); + var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId); + + var lastReadEventIndex = this._getLastDisplayedEventIndex(); + if (lastReadEventIndex === null) return; + + if (lastReadEventIndex > currentReadUpToEventIndex) { + MatrixClientPeg.get().sendReadReceipt(this.state.room.timeline[lastReadEventIndex]); + } + }, + + _getLastDisplayedEventIndex: function() { + if (this.eventNodes === undefined) return null; + + var messageWrapper = this.refs.messageWrapper; + if (messageWrapper === undefined) return null; + var wrapperRect = messageWrapper.getDOMNode().getBoundingClientRect(); + + for (var i = this.state.room.timeline.length-1; i >= 0; --i) { + var ev = this.state.room.timeline[i]; + var node = this.eventNodes[ev.getId()]; + if (node === undefined) continue; + + var domNode = node.getDOMNode(); + var boundingRect = domNode.getBoundingClientRect(); + + if (boundingRect.bottom < wrapperRect.bottom) { + return i; + } + } + return null; } }; From f9385b455a1be6598dca926935fc00211cf0df36 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 5 Nov 2015 13:27:03 +0000 Subject: [PATCH 07/12] Don't try to send read receipts if the room is null --- src/controllers/organisms/RoomView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index 2dc9e1bf26..7ab59b497a 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -468,6 +468,7 @@ module.exports = { }, sendReadReceipt: function() { + if (!this.state.room) return; var currentReadUpToEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId); var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId); From d8edbd2e3c79984a01c7ae6dec0bc9b45e60af90 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 5 Nov 2015 14:45:48 +0000 Subject: [PATCH 08/12] Requires js-sdk develop --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ff7d75e712..c1c7f5fbe5 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "flux": "^2.0.3", "glob": "^5.0.14", "linkifyjs": "^2.0.0-beta.4", - "matrix-js-sdk": "^0.3.0", + "matrix-js-sdk": "https://github.com/matrix-org/matrix-js-sdk.git#develop", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^0.13.3", From f4e65f8e17524994321d62c52a65dbc084dcd720 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 5 Nov 2015 15:07:46 +0000 Subject: [PATCH 09/12] Remove name event listener --- src/controllers/organisms/MemberList.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/organisms/MemberList.js b/src/controllers/organisms/MemberList.js index a2298a20ba..4dfe5330e1 100644 --- a/src/controllers/organisms/MemberList.js +++ b/src/controllers/organisms/MemberList.js @@ -46,6 +46,7 @@ module.exports = { if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room", this.onRoom); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); + MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName); MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn); } }, From 95cdbe3a48b081c988e87c32918e424cab6370ee Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 17 Nov 2015 17:36:15 +0000 Subject: [PATCH 10/12] stop launch from wedging solid for 5 minutes >:( --- src/controllers/pages/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index 2fdb9eb8ef..ea34ec13b3 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/controllers/pages/MatrixChat.js @@ -351,7 +351,7 @@ module.exports = { Notifier.start(); UserActivity.start(); Presence.start(); - cli.startClient({resolveInvitesToProfiles: true}); + cli.startClient(); }, onKeyDown: function(ev) { From 0df0935b9c4079c8d75e749de59cc4cafb5900db Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 18 Nov 2015 09:57:14 +0000 Subject: [PATCH 11/12] Fix presence exception. Yay, javascript. --- src/Presence.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Presence.js b/src/Presence.js index 1f5617514a..e776cca078 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -73,11 +73,12 @@ class Presence { } var old_state = this.state; this.state = newState; + var self = this; MatrixClientPeg.get().setPresence(this.state).done(function() { console.log("Presence: %s", newState); }, function(err) { console.error("Failed to set presence: %s", err); - this.state = old_state; + self.state = old_state; }); } From 31b083d93ec3870ba62bb9e43c5e01979500bc6a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 18 Nov 2015 14:51:06 +0000 Subject: [PATCH 12/12] new Date() syntax & units on var name --- src/UserActivity.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/UserActivity.js b/src/UserActivity.js index 46a46f0b0e..cee1b4efe2 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.js @@ -31,8 +31,8 @@ class UserActivity { start() { document.onmousemove = this._onUserActivity.bind(this); document.onkeypress = this._onUserActivity.bind(this); - this.lastActivityAt = (new Date).getTime(); - this.lastDispatchAt = 0; + this.lastActivityAtTs = new Date().getTime(); + this.lastDispatchAtTs = 0; } /** @@ -44,9 +44,9 @@ class UserActivity { } _onUserActivity() { - this.lastActivityAt = (new Date).getTime(); - if (this.lastDispatchAt < this.lastActivityAt - MIN_DISPATCH_INTERVAL) { - this.lastDispatchAt = this.lastActivityAt; + this.lastActivityAtTs = (new Date).getTime(); + if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL) { + this.lastDispatchAtTs = this.lastActivityAtTs; dis.dispatch({ action: 'user_activity' });