| 11 | 11 | | Search, |
| 12 | 12 | | Settings, |
| 13 | 13 | | } from "lucide-react"; |
| 14 | | − | import { useEffect, useRef, useState } from "react"; |
| 14 | + | import { useEffect, useMemo, useRef, useState } from "react"; |
| 15 | 15 | | import { |
| 16 | 16 | | Form, |
| 17 | 17 | | isRouteErrorResponse, |
| ⋯ |
| 69 | 69 | | import { registrationMode } from "./lib/registration.server"; |
| 70 | 70 | | import { addresses } from "./lib/addresses.server"; |
| 71 | 71 | | import { useSignUpCopy } from "./lib/registration"; |
| 72 | | − | import { RELOADED_KEY, reloadFixes } from "./lib/stale-build"; |
| 72 | + | import { RELOADED_KEY, RELOAD_GIVE_UP_MS, clientNavigated, reloadFixes, reloadedBefore } from "./lib/stale-build"; |
| 73 | 73 | | import { useNonce } from "./lib/nonce"; |
| 74 | 74 | | import { isNeedsSignIn } from "./lib/website-token"; |
| 75 | 75 | | import { LiveNotifications } from "./components/notifications/live-notifications"; |
| ⋯ |
| 658 | 658 | | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { |
| 659 | 659 | | const location = useLocation(); |
| 660 | 660 | | const matches = useMatches(); |
| 661 | + | const href = location.pathname + location.search + location.hash; |
| 661 | 662 | | // A tab left open across a deploy: load the address again as a whole |
| 662 | | − | // page, once, rather than show an error the reload fixes. |
| 663 | | − | const reload = reloadFixes({ |
| 663 | + | // page, once, rather than show an error the reload fixes. A document load |
| 664 | + | // never reloads, so the server and the hydrating client render the same. |
| 665 | + | const reloadable = reloadFixes({ |
| 664 | 666 | | error, |
| 665 | 667 | | status: isRouteErrorResponse(error) ? error.status : undefined, |
| 666 | | − | // The first page a tab loads has the key "default"; a client navigation's is its own. |
| 667 | | − | clientNavigation: location.key !== "default", |
| 668 | + | clientNavigation: clientNavigated(location.key), |
| 668 | 669 | | caughtByCatchAll: matches.at(-1)?.id === "routes/not-found", |
| 669 | 670 | | }); |
| 670 | | − | const href = location.pathname + location.search + location.hash; |
| 671 | + | // Already loaded again once, or the reload never started: show the page. |
| 672 | + | const tried = useMemo(() => reloadable && reloadedBefore(href), [reloadable, href]); |
| 673 | + | const [gaveUp, setGaveUp] = useState<string | null>(null); |
| 674 | + | const reload = reloadable && !tried && gaveUp !== href; |
| 671 | 675 | | useEffect(() => { |
| 672 | 676 | | if (!reload) return; |
| 673 | 677 | | try { |
| 674 | | − | if (sessionStorage.getItem(RELOADED_KEY) === href) return; |
| 675 | 678 | | sessionStorage.setItem(RELOADED_KEY, href); |
| 676 | 679 | | } catch { |
| 677 | 680 | | // No session storage: reload anyway; a document load never asks again. |
| 678 | 681 | | } |
| 679 | 682 | | window.location.assign(href); |
| 683 | + | const timer = window.setTimeout(() => setGaveUp(href), RELOAD_GIVE_UP_MS); |
| 684 | + | return () => window.clearTimeout(timer); |
| 680 | 685 | | }, [reload, href]); |
| 681 | 686 | | if (reload) { |
| 682 | 687 | | return ( |