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