[MM 7970] Maintain online status while the Desktop App is in the background ... and other things. (#993)
* monitor os-level user activity * disable eslint warnings * add tests for UserActivityMonitor * couple more tests * udpate headers of new files
This commit is contained in:
parent
2cf0acc38c
commit
2cfc735d6d
|
@ -45,6 +45,7 @@ export default class MattermostView extends React.Component {
|
|||
this.goForward = this.goForward.bind(this);
|
||||
this.getSrc = this.getSrc.bind(this);
|
||||
this.handleDeepLink = this.handleDeepLink.bind(this);
|
||||
this.handleUserActivityUpdate = this.handleUserActivityUpdate.bind(this);
|
||||
|
||||
this.webviewRef = React.createRef();
|
||||
}
|
||||
|
@ -186,6 +187,14 @@ export default class MattermostView extends React.Component {
|
|||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// start listening for user status updates from main
|
||||
ipcRenderer.on('user-activity-update', this.handleUserActivityUpdate);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// stop listening for user status updates from main
|
||||
ipcRenderer.removeListener('user-activity-update', this.handleUserActivityUpdate);
|
||||
}
|
||||
|
||||
reload() {
|
||||
|
@ -253,6 +262,11 @@ export default class MattermostView extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
handleUserActivityUpdate(event, status) {
|
||||
// pass user activity update to the webview
|
||||
this.webviewRef.current.send('user-activity-update', status);
|
||||
}
|
||||
|
||||
render() {
|
||||
const errorView = this.state.errorInfo ? (
|
||||
<ErrorView
|
||||
|
|
17
src/browser/js/WebappConnector.js
Normal file
17
src/browser/js/WebappConnector.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import EventEmitter from 'events';
|
||||
|
||||
/**
|
||||
* Provides the bridging infrastructure to connect the desktop and webapp together
|
||||
*/
|
||||
export default class WebappConnector extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.active = true;
|
||||
|
||||
window.webappConnector = this;
|
||||
}
|
||||
}
|
|
@ -6,11 +6,14 @@
|
|||
import {ipcRenderer, webFrame} from 'electron';
|
||||
|
||||
import EnhancedNotification from '../js/notification';
|
||||
import WebappConnector from '../js/WebappConnector';
|
||||
|
||||
const UNREAD_COUNT_INTERVAL = 1000;
|
||||
//eslint-disable-next-line no-magic-numbers
|
||||
const CLEAR_CACHE_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours
|
||||
|
||||
const webappConnector = new WebappConnector();
|
||||
|
||||
Notification = EnhancedNotification; // eslint-disable-line no-global-assign, no-native-reassign
|
||||
|
||||
Reflect.deleteProperty(global.Buffer); // http://electron.atom.io/docs/tutorial/security/#buffer-global
|
||||
|
@ -194,6 +197,11 @@ function setSpellChecker() {
|
|||
setSpellChecker();
|
||||
ipcRenderer.on('set-spellchecker', setSpellChecker);
|
||||
|
||||
// push user activity updates to the webapp via the communication bridge
|
||||
ipcRenderer.on('user-activity-update', (event, {userIsActive, isSystemEvent}) => {
|
||||
webappConnector.emit('user-activity-update', {userIsActive, manual: isSystemEvent});
|
||||
});
|
||||
|
||||
// mattermost-webapp is SPA. So cache is not cleared due to no navigation.
|
||||
// We needed to manually clear cache to free memory in long-term-use.
|
||||
// http://seenaburns.com/debugging-electron-memory-usage/
|
||||
|
|
10
src/main.js
10
src/main.js
|
@ -32,6 +32,7 @@ import AppStateManager from './main/AppStateManager';
|
|||
import initCookieManager from './main/cookieManager';
|
||||
import {shouldBeHiddenOnStartup} from './main/utils';
|
||||
import SpellChecker from './main/SpellChecker';
|
||||
import UserActivityMonitor from './main/UserActivityMonitor';
|
||||
|
||||
// pull out required electron components like this
|
||||
// as not all components can be referenced before the app is ready
|
||||
|
@ -51,6 +52,7 @@ const argv = parseArgv(process.argv.slice(1));
|
|||
const hideOnStartup = shouldBeHiddenOnStartup(argv);
|
||||
const loginCallbackMap = new Map();
|
||||
const certificateStore = CertificateStore.load(path.resolve(app.getPath('userData'), 'certificate.json'));
|
||||
const userActivityMonitor = new UserActivityMonitor();
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
|
@ -418,6 +420,14 @@ function initializeAfterAppReady() {
|
|||
config.setRegistryConfigData(registryConfig.data);
|
||||
mainWindow.registryConfigData = registryConfig.data;
|
||||
|
||||
// listen for status updates and pass on to renderer
|
||||
userActivityMonitor.on('status', (status) => {
|
||||
mainWindow.webContents.send('user-activity-update', status);
|
||||
});
|
||||
|
||||
// start monitoring user activity (needs to be started after the app is ready)
|
||||
userActivityMonitor.startMonitoring();
|
||||
|
||||
if (shouldShowTrayIcon) {
|
||||
// set up tray icon
|
||||
trayIcon = new Tray(trayImages.normal);
|
||||
|
|
158
src/main/UserActivityMonitor.js
Normal file
158
src/main/UserActivityMonitor.js
Normal file
|
@ -0,0 +1,158 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import EventEmitter from 'events';
|
||||
|
||||
import electron from 'electron';
|
||||
|
||||
const {app} = electron;
|
||||
|
||||
/**
|
||||
* Monitors system idle time, listens for system events and fires status updates as needed
|
||||
*/
|
||||
export default class UserActivityMonitor extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.isActive = true;
|
||||
this.forceInactive = false;
|
||||
this.idleTime = 0;
|
||||
this.lastStatusUpdate = 0;
|
||||
this.systemIdleTimeIntervalID = -1;
|
||||
|
||||
this.config = {
|
||||
internalUpdateFrequencyMs: 1 * 1000, // eslint-disable-line no-magic-numbers
|
||||
statusUpdateThresholdMs: 60 * 1000, // eslint-disable-line no-magic-numbers
|
||||
activityTimeoutSec: 5 * 60, // eslint-disable-line no-magic-numbers
|
||||
};
|
||||
|
||||
// NOTE: binding needed to prevent error; fat arrow class methods don't work in current setup
|
||||
// Error: "Error: async hook stack has become corrupted (actual: #, expected: #)"
|
||||
this.handleSystemGoingAway = this.handleSystemGoingAway.bind(this);
|
||||
this.handleSystemComingBack = this.handleSystemComingBack.bind(this);
|
||||
}
|
||||
|
||||
get userIsActive() {
|
||||
return this.forceInactive ? false : this.isActive;
|
||||
}
|
||||
|
||||
get userIdleTime() {
|
||||
return this.idleTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin monitoring system events and idle time at defined frequency
|
||||
*
|
||||
* @param {Object} config - overide internal configuration defaults
|
||||
* @param {nunber} config.internalUpdateFrequencyMs
|
||||
* @param {nunber} config.statusUpdateThresholdMs
|
||||
* @param {nunber} config.activityTimeoutSec
|
||||
* @emits {error} emitted when method is called before the app is ready
|
||||
* @emits {error} emitted when this method has previously been called but not subsequently stopped
|
||||
*/
|
||||
startMonitoring(config = {}) {
|
||||
if (!app.isReady()) {
|
||||
this.emit('error', new Error('UserActivityMonitor.startMonitoring can only be called after app is ready'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.systemIdleTimeIntervalID >= 0) {
|
||||
this.emit('error', new Error('User activity monitoring is already in progress'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.config = Object.assign({}, this.config, config);
|
||||
|
||||
// NOTE: electron.powerMonitor cannot be referenced until the app is ready
|
||||
electron.powerMonitor.on('suspend', this.handleSystemGoingAway);
|
||||
electron.powerMonitor.on('resume', this.handleSystemComingBack);
|
||||
electron.powerMonitor.on('shutdown', this.handleSystemGoingAway);
|
||||
electron.powerMonitor.on('lock-screen', this.handleSystemGoingAway);
|
||||
electron.powerMonitor.on('unlock-screen', this.handleSystemComingBack);
|
||||
|
||||
this.systemIdleTimeIntervalID = setInterval(() => {
|
||||
try {
|
||||
electron.powerMonitor.querySystemIdleTime((idleTime) => {
|
||||
this.updateIdleTime(idleTime);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Error getting system idle time:', err);
|
||||
}
|
||||
}, this.config.internalUpdateFrequencyMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop monitoring system events and idle time
|
||||
*/
|
||||
stopMonitoring() {
|
||||
electron.powerMonitor.off('suspend', this.handleSystemGoingAway);
|
||||
electron.powerMonitor.off('resume', this.handleSystemComingBack);
|
||||
electron.powerMonitor.off('shutdown', this.handleSystemGoingAway);
|
||||
electron.powerMonitor.off('lock-screen', this.handleSystemGoingAway);
|
||||
electron.powerMonitor.off('unlock-screen', this.handleSystemComingBack);
|
||||
|
||||
clearInterval(this.systemIdleTimeIntervalID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates internal idle time properties and conditionally triggers updates to user activity status
|
||||
*
|
||||
* @param {integer} idleTime
|
||||
* @private
|
||||
*/
|
||||
updateIdleTime(idleTime) {
|
||||
this.idleTime = idleTime;
|
||||
|
||||
if (this.idleTime > this.config.activityTimeoutSec) {
|
||||
this.updateUserActivityStatus(false);
|
||||
} else if (!this.forceInactive && this.idleTime < this.config.activityTimeoutSec) {
|
||||
this.updateUserActivityStatus(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates user activity status if changed and triggers a status update
|
||||
*
|
||||
* @param {boolean} isActive
|
||||
* @param {boolean} isSystemEvent – indicates whether the update was triggered by a system event (log in/out, screesaver on/off etc)
|
||||
* @private
|
||||
*/
|
||||
updateUserActivityStatus(isActive = false, isSystemEvent = false) {
|
||||
const now = Date.now();
|
||||
if (isActive !== this.isActive) {
|
||||
this.isActive = isActive;
|
||||
this.sendStatusUpdate(now, isSystemEvent);
|
||||
} else if (now - this.lastStatusUpdate > this.config.statusUpdateThresholdMs) {
|
||||
this.sendStatusUpdate(now, isSystemEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an update with user activity status and current system idle time
|
||||
*
|
||||
* @emits {status} emitted at regular, definable intervals providing an update on user active status and idle time
|
||||
* @private
|
||||
*/
|
||||
sendStatusUpdate(now = Date.now(), isSystemEvent = false) {
|
||||
this.lastStatusUpdate = now;
|
||||
this.emit('status', {
|
||||
userIsActive: this.isActive,
|
||||
idleTime: this.idleTime,
|
||||
isSystemEvent,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* System event handlers
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
handleSystemGoingAway() {
|
||||
this.forceInactive = true;
|
||||
this.updateUserActivityStatus(false, true);
|
||||
}
|
||||
handleSystemComingBack() {
|
||||
this.forceInactive = false;
|
||||
this.updateUserActivityStatus(true, true);
|
||||
}
|
||||
}
|
89
test/specs/main/user_activity_monitor_test.js
Normal file
89
test/specs/main/user_activity_monitor_test.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import assert from 'assert';
|
||||
|
||||
import UserActivityMonitor from '../../../src/main/UserActivityMonitor';
|
||||
|
||||
describe('UserActivityMonitor', () => {
|
||||
describe('updateIdleTime', () => {
|
||||
it('should set idle time to provided value', () => {
|
||||
const userActivityMonitor = new UserActivityMonitor();
|
||||
const idleTime = Math.round(Date.now() / 1000);
|
||||
userActivityMonitor.updateIdleTime(idleTime);
|
||||
assert.equal(userActivityMonitor.userIdleTime, idleTime);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUserActivityStatus', () => {
|
||||
let userActivityMonitor;
|
||||
|
||||
beforeEach(() => {
|
||||
userActivityMonitor = new UserActivityMonitor();
|
||||
});
|
||||
|
||||
it('should set user status to active', () => {
|
||||
userActivityMonitor.updateUserActivityStatus(true);
|
||||
assert.equal(userActivityMonitor.userIsActive, true);
|
||||
});
|
||||
it('should set user status to inactive', () => {
|
||||
userActivityMonitor.updateUserActivityStatus(false);
|
||||
assert.equal(userActivityMonitor.userIsActive, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleSystemGoingAway', () => {
|
||||
it('should set user status to inactive and forceInactive to true', () => {
|
||||
const userActivityMonitor = new UserActivityMonitor();
|
||||
userActivityMonitor.isActive = true;
|
||||
userActivityMonitor.forceInactive = false;
|
||||
userActivityMonitor.handleSystemGoingAway();
|
||||
assert.equal(!userActivityMonitor.userIsActive && userActivityMonitor.forceInactive, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleSystemComingBack', () => {
|
||||
it('should set user status to active and forceInactive to false', () => {
|
||||
const userActivityMonitor = new UserActivityMonitor();
|
||||
userActivityMonitor.isActive = false;
|
||||
userActivityMonitor.forceInactive = true;
|
||||
userActivityMonitor.handleSystemComingBack();
|
||||
assert.equal(userActivityMonitor.userIsActive && !userActivityMonitor.forceInactive, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendStatusUpdate', () => {
|
||||
let userActivityMonitor;
|
||||
|
||||
beforeEach(() => {
|
||||
userActivityMonitor = new UserActivityMonitor();
|
||||
});
|
||||
|
||||
it('should emit a non-system triggered status event indicating a user is active', () => {
|
||||
userActivityMonitor.on('status', ({userIsActive, isSystemEvent}) => {
|
||||
assert.equal(userIsActive && !isSystemEvent, true);
|
||||
});
|
||||
userActivityMonitor.updateUserActivityStatus(true, false);
|
||||
});
|
||||
|
||||
it('should emit a non-system triggered status event indicating a user is inactive', () => {
|
||||
userActivityMonitor.on('status', ({userIsActive, isSystemEvent}) => {
|
||||
assert.equal(!userIsActive && !isSystemEvent, true);
|
||||
});
|
||||
userActivityMonitor.updateUserActivityStatus(false, false);
|
||||
});
|
||||
|
||||
it('should emit a system triggered status event indicating a user is active', () => {
|
||||
userActivityMonitor.on('status', ({userIsActive, isSystemEvent}) => {
|
||||
assert.equal(userIsActive && isSystemEvent, true);
|
||||
});
|
||||
userActivityMonitor.updateUserActivityStatus(true, true);
|
||||
});
|
||||
|
||||
it('should emit a system triggered status event indicating a user is inactive', () => {
|
||||
userActivityMonitor.on('status', ({userIsActive, isSystemEvent}) => {
|
||||
assert.equal(!userIsActive && isSystemEvent, true);
|
||||
});
|
||||
userActivityMonitor.updateUserActivityStatus(false, true);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue