Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
WIP on multiple user activity timeouts
Browse files Browse the repository at this point in the history
  • Loading branch information
bwindels committed Dec 4, 2018
1 parent 12ca38f commit d5267a8
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 13 deletions.
25 changes: 22 additions & 3 deletions src/UserActivity.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
*/
Expand Down Expand Up @@ -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();
41 changes: 31 additions & 10 deletions src/components/structures/TimelinePanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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.
Expand Down
59 changes: 59 additions & 0 deletions src/utils/Timer.js
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit d5267a8

Please sign in to comment.