98 lines
2.6 KiB
TypeScript
98 lines
2.6 KiB
TypeScript
|
// Copyright 2020 Signal Messenger, LLC
|
||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||
|
|
||
|
import { throttle } from 'lodash';
|
||
|
|
||
|
// Idle timer - you're active for ACTIVE_TIMEOUT after one of these events
|
||
|
const ACTIVE_TIMEOUT = 15 * 1000;
|
||
|
const LISTENER_THROTTLE_TIME = 5 * 1000;
|
||
|
const ACTIVE_EVENTS = [
|
||
|
'click',
|
||
|
'keydown',
|
||
|
'mousedown',
|
||
|
'mousemove',
|
||
|
// 'scroll', // this is triggered by Timeline re-renders, can't use
|
||
|
'touchstart',
|
||
|
'wheel',
|
||
|
];
|
||
|
|
||
|
export class ActiveWindowService {
|
||
|
// This starting value might be wrong but we should get an update from the main process
|
||
|
// soon. We'd rather report that the window is inactive so we can show notifications.
|
||
|
private isInitialized = false;
|
||
|
|
||
|
private isFocused = false;
|
||
|
|
||
|
private activeCallbacks: Array<() => void> = [];
|
||
|
|
||
|
private lastActiveEventAt = -Infinity;
|
||
|
|
||
|
private callActiveCallbacks: () => void;
|
||
|
|
||
|
constructor() {
|
||
|
this.callActiveCallbacks = throttle(() => {
|
||
|
this.activeCallbacks.forEach(callback => callback());
|
||
|
}, LISTENER_THROTTLE_TIME);
|
||
|
}
|
||
|
|
||
|
// These types aren't perfectly accurate, but they make this class easier to test.
|
||
|
initialize(document: EventTarget, ipc: NodeJS.EventEmitter): void {
|
||
|
if (this.isInitialized) {
|
||
|
throw new Error(
|
||
|
'Active window service should not be initialized multiple times'
|
||
|
);
|
||
|
}
|
||
|
this.isInitialized = true;
|
||
|
|
||
|
this.lastActiveEventAt = Date.now();
|
||
|
|
||
|
const onActiveEvent = this.onActiveEvent.bind(this);
|
||
|
ACTIVE_EVENTS.forEach((eventName: string) => {
|
||
|
document.addEventListener(eventName, onActiveEvent, true);
|
||
|
});
|
||
|
|
||
|
// We don't know for sure that we'll get the right data over IPC so we use `unknown`.
|
||
|
ipc.on('set-window-focus', (_event: unknown, isFocused: unknown) => {
|
||
|
this.setWindowFocus(Boolean(isFocused));
|
||
|
});
|
||
|
}
|
||
|
|
||
|
isActive(): boolean {
|
||
|
return (
|
||
|
this.isFocused && Date.now() < this.lastActiveEventAt + ACTIVE_TIMEOUT
|
||
|
);
|
||
|
}
|
||
|
|
||
|
registerForActive(callback: () => void): void {
|
||
|
this.activeCallbacks.push(callback);
|
||
|
}
|
||
|
|
||
|
unregisterForActive(callback: () => void): void {
|
||
|
this.activeCallbacks = this.activeCallbacks.filter(
|
||
|
item => item !== callback
|
||
|
);
|
||
|
}
|
||
|
|
||
|
private onActiveEvent(): void {
|
||
|
this.updateState(() => {
|
||
|
this.lastActiveEventAt = Date.now();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
private setWindowFocus(isFocused: boolean): void {
|
||
|
this.updateState(() => {
|
||
|
this.isFocused = isFocused;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
private updateState(fn: () => void): void {
|
||
|
const wasActiveBefore = this.isActive();
|
||
|
fn();
|
||
|
const isActiveNow = this.isActive();
|
||
|
|
||
|
if (!wasActiveBefore && isActiveNow) {
|
||
|
this.callActiveCallbacks();
|
||
|
}
|
||
|
}
|
||
|
}
|