-
Notifications
You must be signed in to change notification settings - Fork 101
Add idle auto-lock setting #1153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v2
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' | ||
| import { createIdleTracker } from './useIdleAutoLock' | ||
|
|
||
| function createMockWindow() { | ||
| const listeners = new Map<string, Set<EventListener>>() | ||
| const addSpy = vi.fn((event: string, handler: EventListener) => { | ||
| if (!listeners.has(event)) listeners.set(event, new Set()) | ||
| listeners.get(event)!.add(handler) | ||
| }) | ||
| const removeSpy = vi.fn((event: string, handler: EventListener) => { | ||
| listeners.get(event)?.delete(handler) | ||
| }) | ||
| const win = { | ||
| addEventListener: addSpy, | ||
| removeEventListener: removeSpy, | ||
| dispatch(event: string) { | ||
| listeners.get(event)?.forEach((handler) => handler(new Event(event))) | ||
| }, | ||
| } as unknown as Window & { dispatch: (event: string) => void } | ||
| return { win, addSpy, removeSpy } | ||
| } | ||
|
|
||
| describe('createIdleTracker', () => { | ||
| beforeEach(() => { | ||
| vi.useFakeTimers() | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| vi.useRealTimers() | ||
| }) | ||
|
|
||
| it('fires callback after timeout period of inactivity', () => { | ||
| const onLock = vi.fn() | ||
| const { win } = createMockWindow() | ||
| const tracker = createIdleTracker(onLock, 5 * 60 * 1_000, win) | ||
| tracker.start() | ||
|
|
||
| vi.advanceTimersByTime(4 * 60 * 1_000) | ||
| expect(onLock).not.toHaveBeenCalled() | ||
|
|
||
| vi.advanceTimersByTime(1 * 60 * 1_000) | ||
| expect(onLock).toHaveBeenCalledTimes(1) | ||
| }) | ||
|
|
||
| it('resets timer on user activity', () => { | ||
| const onLock = vi.fn() | ||
| const { win } = createMockWindow() | ||
| const tracker = createIdleTracker(onLock, 5 * 60 * 1_000, win) | ||
| tracker.start() | ||
|
|
||
| // Advance 4 minutes | ||
| vi.advanceTimersByTime(4 * 60 * 1_000) | ||
| expect(onLock).not.toHaveBeenCalled() | ||
|
|
||
| // Simulate mouse activity | ||
| win.dispatch('mousemove') | ||
|
|
||
| // 4 more minutes from activity (would be 8 total, but timer restarted) | ||
| vi.advanceTimersByTime(4 * 60 * 1_000) | ||
| expect(onLock).not.toHaveBeenCalled() | ||
|
|
||
| // 1 more minute → 5 since last activity | ||
| vi.advanceTimersByTime(1 * 60 * 1_000) | ||
| expect(onLock).toHaveBeenCalledTimes(1) | ||
| }) | ||
|
|
||
| it('cleans up timers and listeners on stop', () => { | ||
| const onLock = vi.fn() | ||
| const { win, removeSpy } = createMockWindow() | ||
| const tracker = createIdleTracker(onLock, 5 * 60 * 1_000, win) | ||
| tracker.start() | ||
|
|
||
| tracker.stop() | ||
| expect(removeSpy).toHaveBeenCalled() | ||
|
|
||
| vi.advanceTimersByTime(10 * 60 * 1_000) | ||
| expect(onLock).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('resets on keyboard activity', () => { | ||
| const onLock = vi.fn() | ||
| const { win } = createMockWindow() | ||
| const tracker = createIdleTracker(onLock, 1 * 60 * 1_000, win) | ||
| tracker.start() | ||
|
|
||
| vi.advanceTimersByTime(50_000) | ||
| win.dispatch('keydown') | ||
|
|
||
| vi.advanceTimersByTime(50_000) | ||
| expect(onLock).not.toHaveBeenCalled() | ||
|
|
||
| vi.advanceTimersByTime(10_000) | ||
| expect(onLock).toHaveBeenCalledTimes(1) | ||
| }) | ||
|
|
||
| it('registers listeners for all activity events', () => { | ||
| const onLock = vi.fn() | ||
| const { win, addSpy } = createMockWindow() | ||
| const tracker = createIdleTracker(onLock, 60_000, win) | ||
| tracker.start() | ||
|
|
||
| const calls = addSpy.mock.calls.map((c) => c[0]) | ||
| expect(calls).toContain('mousemove') | ||
| expect(calls).toContain('mousedown') | ||
| expect(calls).toContain('keydown') | ||
| expect(calls).toContain('touchstart') | ||
| expect(calls).toContain('scroll') | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { useEffect, useRef } from 'react' | ||
|
|
||
| const ACTIVITY_EVENTS = ['mousemove', 'mousedown', 'keydown', 'touchstart', 'scroll'] as const | ||
|
|
||
| export function createIdleTracker(onLock: () => void, timeoutMs: number, win: Window) { | ||
| let timer: ReturnType<typeof setTimeout> | undefined | ||
|
|
||
| const resetTimer = () => { | ||
| if (timer) clearTimeout(timer) | ||
| timer = setTimeout(onLock, timeoutMs) | ||
| } | ||
|
|
||
| return { | ||
| start() { | ||
| resetTimer() | ||
| for (const event of ACTIVITY_EVENTS) { | ||
| win.addEventListener(event, resetTimer, { passive: true }) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can/should |
||
| } | ||
| }, | ||
| stop() { | ||
| if (timer) clearTimeout(timer) | ||
| timer = undefined | ||
| for (const event of ACTIVITY_EVENTS) { | ||
| win.removeEventListener(event, resetTimer) | ||
| } | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| export function useIdleAutoLock(onLock: () => void, timeoutMinutes: number) { | ||
| const onLockRef = useRef(onLock) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just curious.. why does not the timer not reset when |
||
|
|
||
| useEffect(() => { | ||
| onLockRef.current = onLock | ||
| }) | ||
|
|
||
| useEffect(() => { | ||
| if (timeoutMinutes <= 0) return | ||
|
|
||
| const tracker = createIdleTracker(() => onLockRef.current(), timeoutMinutes * 60 * 1_000, window) | ||
| tracker.start() | ||
|
|
||
| return () => tracker.stop() | ||
| }, [timeoutMinutes]) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ export type JamSettings = { | |
| privateMode: boolean | ||
| currencyUnit: Currency | ||
| cheatsheetForceOpenAt?: number | ||
| autoLockTimeout?: number | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can this be seconds instead of minutes and called |
||
| } | ||
|
|
||
| interface JamSettingsStoreState { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: If
startreturns the stop function,AbortControllercan be used to simplify the listener removal.