diff --git a/package.json b/package.json index 2da20805c1..d51f3a05ba 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.14.2", diff --git a/src/Presence.js b/src/Presence.js index d77058abd8..e776cca078 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,42 @@ module.exports = { if (!this.running) { return; } - state = newState; - MatrixClientPeg.get().setPresence(state).done(function() { + 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); + self.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..cee1b4efe2 --- /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.lastActivityAtTs = new Date().getTime(); + this.lastDispatchAtTs = 0; + } + + /** + * Stop tracking user activity + */ + stop() { + document.onmousemove = undefined; + document.onkeypress = undefined; + } + + _onUserActivity() { + this.lastActivityAtTs = (new Date).getTime(); + if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL) { + this.lastDispatchAtTs = this.lastActivityAtTs; + dis.dispatch({ + action: 'user_activity' + }); + } + } +} + +module.exports = new UserActivity(); 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..57c9a71842 100644 --- a/src/controllers/atoms/RoomAvatar.js +++ b/src/controllers/atoms/RoomAvatar.js @@ -41,10 +41,26 @@ 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) { + this._update(); + this.setState({ + imageUrl: this._nextUrl() + }); + } }, _update: function() { diff --git a/src/controllers/organisms/MemberList.js b/src/controllers/organisms/MemberList.js index 7d4326ef69..8b6733fbeb 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 }, @@ -45,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); } }, @@ -97,6 +99,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/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index 105392de0b..3a5d432ebd 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); } }, @@ -87,6 +89,9 @@ module.exports = { messageWrapper.scrollTop = messageWrapper.scrollHeight; } break; + case 'user_activity': + this.sendReadReceipt(); + break; } }, @@ -149,6 +154,12 @@ module.exports = { } }, + onRoomReceipt: function(receiptEvent, room) { + if (room.roomId == this.props.roomId) { + this.forceUpdate(); + } + }, + onRoomMemberTyping: function(ev, member) { this.forceUpdate(); }, @@ -164,6 +175,8 @@ module.exports = { messageWrapper.scrollTop = messageWrapper.scrollHeight; + this.sendReadReceipt(); + this.fillSpace(); } }, @@ -346,7 +359,7 @@ module.exports = { } } ret.unshift( -
  • +
  • ); ++count; } @@ -438,5 +451,54 @@ 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() { + if (!this.state.room) return; + 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; } }; diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index c2f0589688..4655011a45 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"); @@ -104,6 +105,7 @@ module.exports = { window.localStorage.clear(); } Notifier.stop(); + UserActivity.stop(); Presence.stop(); MatrixClientPeg.get().stopClient(); MatrixClientPeg.get().removeAllListeners(); @@ -362,6 +364,7 @@ module.exports = { }); }); Notifier.start(); + UserActivity.start(); Presence.start(); cli.startClient(); },