1const timeToConsiderActiveForAwhile = 300000 2const timeToConsiderInactive = 60000 3 4type NotifyActiveFunction = (isActive: boolean) => void 5type Reason = 'blur' | 'focus' | 'mouseKeyboard' | 'timeout' 6type State = 'appActive' | 'afterActiveCheck' | 'appInactive' | 'blurred' | 'focused' 7// State machine 8// appActive: User is active. Tell redux we're active. In 5 minutes go to 'afterActiveCheck'. If blur go 'appInactive' immediately 9// afterActiveCheck: User was 'appActive', in a window of 1 minute see if any keyboard / mouse happens. If so go to 'appInactive', else go appActive 10// appInactive: Wait for focus or keyboard/mouse, then go to 'appActive'. Tell redux we're inactive 11// blurred: App in background, wait for focus. Tell redux we're inactive 12// focused: App in foreground but no input yet, wait for keyboard/mouse 13 14class InputMonitor { 15 notifyActive?: NotifyActiveFunction 16 private state: State 17 private timeoutID?: ReturnType<typeof setInterval> 18 19 constructor() { 20 window.addEventListener('focus', this.onFocus) 21 window.addEventListener('blur', this.onBlur) 22 this.state = 'appInactive' 23 this.transition('focus') 24 } 25 26 private nextState = (reason: Reason) => { 27 switch (reason) { 28 case 'blur': 29 return 'blurred' 30 case 'focus': 31 return 'focused' 32 case 'mouseKeyboard': 33 return 'appActive' 34 case 'timeout': 35 return this.state === 'appActive' ? 'afterActiveCheck' : 'appInactive' 36 } 37 } 38 39 private exitState = (next: State) => { 40 switch (next) { 41 case 'focused': 42 this.unlistenForMouseKeyboard() 43 break 44 case 'afterActiveCheck': 45 this.unlistenForMouseKeyboard() 46 break 47 case 'appInactive': 48 this.unlistenForMouseKeyboard() 49 break 50 } 51 } 52 private enterState = (next: State) => { 53 switch (next) { 54 case 'focused': 55 this.listenForMouseKeyboard() 56 break 57 case 'blurred': 58 this.notifyActive && this.notifyActive(false) 59 break 60 case 'appActive': 61 this.notifyActive && this.notifyActive(true) 62 console.log('InputMonitor: 5 minute timeout') 63 this.timeoutID = setTimeout(() => this.transition('timeout'), timeToConsiderActiveForAwhile) 64 break 65 case 'afterActiveCheck': 66 this.listenForMouseKeyboard() 67 console.log('InputMonitor: 1 minute timeout') 68 this.timeoutID = setTimeout(() => this.transition('timeout'), timeToConsiderInactive) 69 break 70 case 'appInactive': 71 console.log('InputMonitor: Inactive') 72 this.listenForMouseKeyboard() 73 this.notifyActive && this.notifyActive(false) 74 break 75 } 76 } 77 78 private clearTimers = () => { 79 // always kill timers 80 this.timeoutID && clearTimeout(this.timeoutID) 81 this.timeoutID = undefined 82 console.log('InputMonitor: Timer cleared') 83 } 84 85 private listenerOptions = { 86 capture: true, 87 passive: true, 88 } 89 90 private listenForMouseKeyboard = () => { 91 this.unlistenForMouseKeyboard() 92 console.log('InputMonitor: adding mouseKeyboard events') 93 window.addEventListener('mousemove', this.onMouseKeyboard, this.listenerOptions) 94 window.addEventListener('keypress', this.onMouseKeyboard, this.listenerOptions) 95 } 96 97 private unlistenForMouseKeyboard = () => { 98 console.log('InputMonitor: removing mouseKeyboard events') 99 window.removeEventListener('mousemove', this.onMouseKeyboard, this.listenerOptions) 100 window.removeEventListener('keypress', this.onMouseKeyboard, this.listenerOptions) 101 } 102 103 private transition = (reason: Reason) => { 104 this.clearTimers() 105 106 const nextState = this.nextState(reason) 107 console.log('InputMonitor: transition', this.state, nextState) 108 if (nextState === this.state) return 109 this.exitState(this.state) 110 this.enterState(nextState) 111 this.state = nextState 112 } 113 114 private onBlur = () => { 115 this.transition('blur') 116 } 117 private onFocus = () => { 118 this.transition('focus') 119 } 120 private onMouseKeyboard = () => { 121 this.transition('mouseKeyboard') 122 } 123} 124 125export default InputMonitor 126