diff --git a/src/UserActivity.js b/src/UserActivity.js index c628ab41861e..22d2d35ebbeb 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.js @@ -15,6 +15,7 @@ limitations under the License. */ import dis from './dispatcher'; +import Timer from './utils/Timer'; const MIN_DISPATCH_INTERVAL_MS = 500; const CURRENTLY_ACTIVE_THRESHOLD_MS = 2000; @@ -25,6 +26,24 @@ const CURRENTLY_ACTIVE_THRESHOLD_MS = 2000; * with the app (but at a much lower frequency than mouse move events) */ class UserActivity { + + constructor() { + this._activityTimers = []; + } + + timeWhileActive(timeout) { + if (!this.userCurrentlyActive()) { + return Timer.abortedWith(new Error("not currently active")); + } + const timer = new Timer(timeout); + this._activityTimers.push(timer); + // remove when done or aborted + timer.promise().finally(() => { + const index = this._activityTimers.indexOf(timer); + this._activityTimers.splice(index, 1); + }); + return timer; + } /** * Start listening to user activity */ @@ -89,14 +108,14 @@ class UserActivity { const now = new Date().getTime(); const targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL_MS; if (now >= targetTime) { - dis.dispatch({ - action: 'user_activity_end', - }); this.activityEndTimer = undefined; + this._activityTimers.forEach((t) => t.abort()); + this._activityTimers = []; } else { this.activityEndTimer = setTimeout(this._onActivityEndTimer.bind(this), targetTime - now); } } } + module.exports = new UserActivity(); diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index c44655ff1876..1faaec2e250b 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -255,6 +255,9 @@ var TimelinePanel = React.createClass({ // (We could use isMounted, but facebook have deprecated that.) this.unmounted = true; + this._readReceiptActivityTimer && this._readReceiptActivityTimer.abort(); + this._readMarkerActivityTimer && this._readMarkerActivityTimer.abort(); + dis.unregister(this.dispatcherRef); const client = MatrixClientPeg.get(); @@ -371,17 +374,25 @@ var TimelinePanel = React.createClass({ } }, - onAction: function(payload) { + onAction: async function(payload) { switch (payload.action) { case 'user_activity': - case 'user_activity_end': - // we could treat user_activity_end differently and not - // send receipts for messages that have arrived between - // the actual user activity and the time they stopped - // being active, but let's see if this is actually - // necessary. - this.sendReadReceipt(); - this.updateReadMarker(); + if (!this._readReceiptActivityTimer || !this._readReceiptActivityTimer.isRunning()) { + // store to abort on unmount + this._readReceiptActivityTimer = UserActivity.timeWhileActive(500); + try { + await this._readReceiptActivityTimer.promise(); + this.sendReadReceipt(); + } catch(e) { /* aborted */ } + } + if (!this._readMarkerActivityTimer || !this._readMarkerActivityTimer.isRunning()) { + // store to abort on unmount + this._readMarkerActivityTimer = UserActivity.timeWhileActive(10000); + try { + await this._readMarkerActivityTimer.promise(); + this.updateReadMarker(); + } catch(e) { /* aborted */ } + } break; case 'ignore_state_changed': this.forceUpdate(); @@ -632,12 +643,22 @@ var TimelinePanel = React.createClass({ // if the read marker is on the screen, we can now assume we've caught up to the end // of the screen, so move the marker down to the bottom of the screen. - updateReadMarker: function() { + updateReadMarker: async function() { if (!this.props.manageReadMarkers) return; if (this.getReadMarkerPosition() !== 0) { return; } + if (this._readMarkerActivityTimer.isRunning()) { + return; + } + this._readMarkerActivityTimer = UserActivity.timeWhileActive(20000); + try { + await this._readMarkerActivityTimer.promise(); + } catch(e) { + return; //aborted + } + // move the RM to *after* the message at the bottom of the screen. This // avoids a problem whereby we never advance the RM if there is a huge // message which doesn't fit on the screen. diff --git a/src/utils/Timer.js b/src/utils/Timer.js new file mode 100644 index 000000000000..b519202212d8 --- /dev/null +++ b/src/utils/Timer.js @@ -0,0 +1,59 @@ +/* +Copyright 2018 New Vector 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. +*/ + + +export default class Timer { + + static abortedWith(error) { + return new Timer(null, Promise.reject(error)); + } + + constructor(timeout, promise = undefined) { + if (promise) { + this._isRunning = false; + this._resolve = null; + this._reject = null; + this._timerHandle = null; + this._promise = promise; + } else { + this._isRunning = true; + this._promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }).finally(() => { + this._isRunning = false; + }); + this._timerHandle = setTimeout(this._resolve, timeout); + } + } + + abort() { + if (this._timerHandle) { + clearTimeout(this._timerHandle); + } + if (this._reject) { + this._reject(new Error("aborted")); + } + } + + promise() { + return this._promise; + } + + isRunning() { + return this._isRunning; + } +}