diff --git a/app/components/Layout.tsx b/app/components/Layout.tsx index 23b4402d2..41f5d1195 100644 --- a/app/components/Layout.tsx +++ b/app/components/Layout.tsx @@ -8,6 +8,7 @@ import { LoadingIndicatorBar } from "~/components/LoadingIndicator"; import SkipNavContent from "~/components/SkipNavContent"; import SkipNavLink from "~/components/SkipNavLink"; import env from "~/env"; +import useAutoRefresh from "~/hooks/useAutoRefresh"; import useKeyDown from "~/hooks/useKeyDown"; import { MenuProvider } from "~/hooks/useMenuContext"; import useStores from "~/hooks/useStores"; @@ -28,6 +29,8 @@ const Layout: React.FC = ({ const { ui } = useStores(); const sidebarCollapsed = !sidebar || ui.sidebarIsClosed; + useAutoRefresh(); + useKeyDown(".", (event) => { if (isModKey(event)) { ui.toggleCollapsedSidebar(); diff --git a/app/hooks/useAutoRefresh.ts b/app/hooks/useAutoRefresh.ts new file mode 100644 index 000000000..017170037 --- /dev/null +++ b/app/hooks/useAutoRefresh.ts @@ -0,0 +1,33 @@ +import * as React from "react"; +import { Minute } from "@shared/utils/time"; +import Logger from "~/utils/Logger"; +import useIdle from "./useIdle"; +import useInterval from "./useInterval"; +import usePageVisibility from "./usePageVisibility"; + +/** + * Hook to reload the app around once a day to stop old code from running. + */ +export default function useAutoRefresh() { + const [minutes, setMinutes] = React.useState(0); + const isVisible = usePageVisibility(); + const isIdle = useIdle(15 * Minute); + + useInterval(() => { + setMinutes((prev) => prev + 1); + + if (minutes >= 60 * 24) { + if (isVisible) { + Logger.debug("lifecycle", "Skipping reload due to app visible"); + return; + } + if (!isIdle) { + Logger.debug("lifecycle", "Skipping reload due to user activity"); + return; + } + + Logger.debug("lifecycle", "Auto-reloading app…"); + window.location.reload(); + } + }, Minute); +} diff --git a/app/hooks/useIdle.ts b/app/hooks/useIdle.ts index fa15e237e..b560a7d85 100644 --- a/app/hooks/useIdle.ts +++ b/app/hooks/useIdle.ts @@ -1,4 +1,5 @@ import * as React from "react"; +import { Minute } from "@shared/utils/time"; const activityEvents = [ "click", @@ -18,7 +19,7 @@ const activityEvents = [ * @param {number} timeToIdle * @returns boolean if the user is idle */ -export default function useIdle(timeToIdle: number = 3 * 60 * 1000) { +export default function useIdle(timeToIdle: number = 3 * Minute) { const [isIdle, setIsIdle] = React.useState(false); const timeout = React.useRef>(); diff --git a/app/hooks/useInterval.ts b/app/hooks/useInterval.ts new file mode 100644 index 000000000..29c5e30fb --- /dev/null +++ b/app/hooks/useInterval.ts @@ -0,0 +1,32 @@ +import * as React from "react"; + +type Callback = () => void; + +/** + * Hook to set up an interval that calls a callback. + * + * @param callback The callback to call. + * @param delay The delay in milliseconds. + */ +export default function useInterval(callback: Callback, delay: number) { + const savedCallback = React.useRef(); + + // Remember the latest callback. + React.useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + // Set up the interval. + React.useEffect(() => { + function tick() { + savedCallback.current?.(); + } + + if (delay !== null) { + const id = setInterval(tick, delay); + return () => clearInterval(id); + } + + return undefined; + }, [delay]); +}