| 1 | import { useIsFocused } from "@react-navigation/native"; |
| 2 | import { useEffect, useLayoutEffect, useState } from "react"; |
| 3 | import { AppState } from "react-native"; |
| 4 | |
| 5 | /** |
| 6 | * Wall clock for elapsed-time labels, advanced once a second only while |
| 7 | * `enabled`, the screen is focused, and the app is active. Retained routes stay |
| 8 | * mounted when hidden, so an ungated interval would keep re-rendering them. |
| 9 | * The clock re-reads the time before paint when ticking resumes, so a label |
| 10 | * never shows the moment it paused. |
| 11 | */ |
| 12 | export function useVisibleSecondClock(enabled: boolean): number { |
| 13 | const focused = useIsFocused(); |
| 14 | const [appActive, setAppActive] = useState(() => AppState.currentState === "active"); |
| 15 | const [nowMs, setNowMs] = useState(() => Date.now()); |
| 16 | useEffect(() => { |
| 17 | const subscription = AppState.addEventListener("change", (state) => |
| 18 | setAppActive(state === "active"), |
| 19 | ); |
| 20 | // A change between the first render and this subscription has no event to replay. |
| 21 | // oxlint-disable-next-line react/set-state-in-effect -- Syncs state the listener missed. |
| 22 | setAppActive(AppState.currentState === "active"); |
| 23 | return () => subscription.remove(); |
| 24 | }, []); |
| 25 | const ticking = enabled && focused && appActive; |
| 26 | useLayoutEffect(() => { |
| 27 | if (!ticking) return; |
| 28 | // oxlint-disable-next-line react/set-state-in-effect -- Resuming reads the clock that stood still. |
| 29 | setNowMs(Date.now()); |
| 30 | const intervalId = setInterval(() => setNowMs(Date.now()), 1_000); |
| 31 | return () => clearInterval(intervalId); |
| 32 | }, [ticking]); |
| 33 | return nowMs; |
| 34 | } |