// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import {EventEmitter} from 'events'; import electron from 'electron'; import log from 'electron-log'; const {app} = electron; /** * Monitors system idle time, listens for system events and fires status updates as needed */ export default class UserActivityMonitor extends EventEmitter { isActive: boolean; idleTime: number; lastSetActive?: number; systemIdleTimeIntervalID: number; config: { updateFrequencyMs: number; inactiveThresholdMs: number; statusUpdateThresholdMs: number; }; constructor() { super(); this.isActive = true; this.idleTime = 0; this.systemIdleTimeIntervalID = -1; this.config = { updateFrequencyMs: 1 * 1000, // eslint-disable-line no-magic-numbers inactiveThresholdMs: 60 * 1000, // eslint-disable-line no-magic-numbers statusUpdateThresholdMs: 60 * 1000, // eslint-disable-line no-magic-numbers }; } get userIsActive() { return 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 {number} config.updateFrequencyMs - internal update clock frequency for monitoring idleTime * @param {number} config.inactiveThresholdMs - the number of milliseconds that idleTime needs to reach to internally be considered inactive * @param {number} config.statusUpdateThresholdMs - minimum amount of time before sending a new status update * @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); // TODO: Node typings don't map Timeout to number, but then clearInterval requires a number? this.systemIdleTimeIntervalID = setInterval(() => { try { this.updateIdleTime(electron.powerMonitor.getSystemIdleTime()); } catch (err) { log.error('Error getting system idle time:', err); } }, this.config.updateFrequencyMs) as unknown as number; } /** * Stop monitoring system events and idle time */ stopMonitoring() { clearInterval(this.systemIdleTimeIntervalID); } /** * Updates internal idle time and sets internal user activity state * * @param {integer} idleTime * @private */ updateIdleTime(idleTime: number) { this.idleTime = idleTime; if (idleTime * 1000 > this.config.inactiveThresholdMs) { // eslint-disable-line no-magic-numbers this.setActivityState(false); } else { this.setActivityState(true); } } /** * Updates user active state and conditionally 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 */ setActivityState(isActive = false, isSystemEvent = false) { this.isActive = isActive; if (isSystemEvent) { this.sendStatusUpdate(true); return; } const now = Date.now(); if (isActive && (this.lastSetActive == null || now - this.lastSetActive >= this.config.statusUpdateThresholdMs)) { this.sendStatusUpdate(false); this.lastSetActive = now; } else if (!isActive) { delete this.lastSetActive; } } /** * 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(isSystemEvent = false) { this.emit('status', { userIsActive: this.isActive, idleTime: this.idleTime, isSystemEvent, }); } }