From 1189a94bffd1b12deb9ae1b9072701adb2386b94 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Thu, 27 Nov 2025 17:25:23 +0100 Subject: [PATCH] refactor(wakelock): simplify logic into hook --- .../src/features/keep-awake/KeepAwake.tsx | 84 +---------------- .../src/features/keep-awake/useWakeLock.ts | 91 +++++++++++++++++++ 2 files changed, 96 insertions(+), 79 deletions(-) create mode 100644 apps/client/src/features/keep-awake/useWakeLock.ts diff --git a/apps/client/src/features/keep-awake/KeepAwake.tsx b/apps/client/src/features/keep-awake/KeepAwake.tsx index c8bb78da0..f115ed0c4 100644 --- a/apps/client/src/features/keep-awake/KeepAwake.tsx +++ b/apps/client/src/features/keep-awake/KeepAwake.tsx @@ -1,84 +1,10 @@ -import { use, useCallback, useEffect, useMemo, useState } from 'react'; -import { useSearchParams } from 'react-router'; - -import { PresetContext } from '../../common/context/PresetContext'; - -/** @url https://developer.mozilla.org/en-US/docs/Web/API/WakeLock */ -export default function KeepAwake() { - const { keepAwake } = useKeepAwakeOptions(); - const [wakeLockSentinel, setWakeLockSentinel] = useState(null); - - const removeLock = () => { - if (wakeLockSentinel) wakeLockSentinel.release().finally(() => setWakeLockSentinel(null)); - }; - - const acquireLock = () => { - if (!wakeLockSentinel || wakeLockSentinel.released) { - setWakeLockSentinel(null); - navigator.wakeLock - .request('screen') - .then((sentinel) => { - setWakeLockSentinel(sentinel); - }) - .catch(console.error); - } - }; - - useEffect(() => { - const controller = new AbortController(); - if (keepAwake) { - acquireLock(); - document.addEventListener( - 'visibilitychange', - () => { - if (wakeLockSentinel !== null && document.visibilityState === 'visible') { - acquireLock(); - } - }, - { signal: controller.signal }, - ); - } else { - removeLock(); - } - - return () => { - controller.abort(); - removeLock(); - }; - }, [keepAwake]); - - return <>; -} - -const keepAwakeKey = 'keep-awake'; - -function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URLSearchParams) { - // Helper to get value from either source, prioritizing defaultValues - return defaultValues?.has(keepAwakeKey) || searchParams.has(keepAwakeKey); -} +import { useWakelock } from './useWakeLock'; /** - * Hook exposes the keep awake options + * Composes the wakelock hook to contain re-renders */ -export function useKeepAwakeOptions() { - const [searchParams, setSearchParams] = useSearchParams(); - const maybePreset = use(PresetContext); +export default function KeepAwake() { + useWakelock(); - const keepAwake = useMemo(() => { - const defaultValues = maybePreset ? new URLSearchParams(maybePreset.search) : undefined; - return getOptionsFromParams(searchParams, defaultValues); - }, [maybePreset, searchParams]); - - const toggleKeepAwake = useCallback(() => { - setSearchParams((searchParams) => { - if (keepAwake) { - searchParams.delete(keepAwakeKey); - } else { - searchParams.set(keepAwakeKey, '1'); - } - return searchParams; - }); - }, [keepAwake]); - - return { keepAwake, toggleKeepAwake }; + return null; } diff --git a/apps/client/src/features/keep-awake/useWakeLock.ts b/apps/client/src/features/keep-awake/useWakeLock.ts new file mode 100644 index 000000000..b260e08e9 --- /dev/null +++ b/apps/client/src/features/keep-awake/useWakeLock.ts @@ -0,0 +1,91 @@ +import { use, useCallback, useEffect, useMemo, useRef } from 'react'; +import { useSearchParams } from 'react-router'; + +import { PresetContext } from '../../common/context/PresetContext'; + +// wakelock is only available in secure contexts +// however, that is covered by the navigator check +export const canUseWakeLock = typeof window !== 'undefined' && 'wakeLock' in navigator; + +/** @url https://developer.mozilla.org/en-US/docs/Web/API/WakeLock */ +export function useWakelock() { + const { keepAwake } = useKeepAwakeOptions(); + const wakeLockSentinelRef = useRef(null); + + useEffect(() => { + const removeLock = () => { + if (wakeLockSentinelRef.current) { + wakeLockSentinelRef.current.release().finally(() => { + wakeLockSentinelRef.current = null; + }); + } + }; + + const acquireLock = () => { + const sentinel = wakeLockSentinelRef.current; + if (!sentinel || sentinel.released) { + wakeLockSentinelRef.current = null; + navigator.wakeLock + .request('screen') + .then((sentinel) => { + wakeLockSentinelRef.current = sentinel; + }) + .catch(console.error); + } + }; + + const controller = new AbortController(); + if (keepAwake) { + acquireLock(); + document.addEventListener( + 'visibilitychange', + () => { + if (wakeLockSentinelRef.current !== null && document.visibilityState === 'visible') { + acquireLock(); + } + }, + { signal: controller.signal }, + ); + } else { + removeLock(); + } + + return () => { + controller.abort(); + removeLock(); + }; + }, [keepAwake]); +} + +const keepAwakeKey = 'keep-awake'; + +function getOptionsFromParams(searchParams: URLSearchParams, defaultValues?: URLSearchParams) { + // Helper to get value from either source, prioritizing defaultValues + return defaultValues?.has(keepAwakeKey) || searchParams.has(keepAwakeKey); +} + +/** + * Hook exposes the keep awake options + */ +export function useKeepAwakeOptions() { + const [searchParams, setSearchParams] = useSearchParams(); + const maybePreset = use(PresetContext); + + const keepAwake = useMemo(() => { + const defaultValues = maybePreset ? new URLSearchParams(maybePreset.search) : undefined; + return getOptionsFromParams(searchParams, defaultValues); + }, [maybePreset, searchParams]); + + const toggleKeepAwake = useCallback(() => { + setSearchParams((searchParams) => { + if (keepAwake) { + searchParams.delete(keepAwakeKey); + } else { + searchParams.set(keepAwakeKey, '1'); + } + return searchParams; + }); + }, [keepAwake, setSearchParams]); + + return { keepAwake, toggleKeepAwake }; +}