// In-app browser-history integration so Back/Forward step through a module's sub-views and // overlays before leaving the page — not straight back to the previous module. // // Two primitives share the existing page-history (App stamps `sfPage` in history.state): // - useHistorySubview: a module's active sub-view rides in history.state._sub, so Back inside // a module (e.g. Messages thread → list) returns to the parent view first. // - useBackToClose: while an overlay/modal is mounted it occupies one history entry; Back // closes the topmost one. A module-level stack keeps nesting correct — closing one modal by // its own button pops exactly its entry without tripping the modals underneath. // // setPage() pushes a clean { sfPage } entry (dropping _sub/_ov), so each page starts at its root. import { useEffect, useRef, useState } from "react"; // --- Sub-views (value carried in history.state._sub) --------------------------------------- export function useHistorySubview(root: T): { view: T; open: (next: T) => void; back: () => void; } { const [view, setView] = useState(() => (window.history.state?._sub as T) ?? root); useEffect(() => { const onPop = () => setView((window.history.state?._sub as T) ?? root); window.addEventListener("popstate", onPop); return () => window.removeEventListener("popstate", onPop); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return { view, open: (next: T) => { setView(next); window.history.pushState({ ...window.history.state, _sub: next }, ""); }, back: () => window.history.back(), }; } // --- Overlays / modals (one history entry each, nesting-safe) ------------------------------- const overlayStack: number[] = []; let overlayCounter = 0; // Set while we pop our OWN entry programmatically (button/ESC close) so the other overlays' // popstate handlers don't mistake it for a user Back and close themselves too. let suppressPop = false; /** While the calling component is mounted, Back (or the browser/mouse back button) closes it via * `onClose` instead of navigating away. Mount the component only while the overlay is open. */ export function useBackToClose(onClose: () => void): void { const cb = useRef(onClose); cb.current = onClose; useEffect(() => { const token = ++overlayCounter; overlayStack.push(token); window.history.pushState({ ...window.history.state, _ov: token }, ""); const onPop = () => { if (suppressPop) { suppressPop = false; return; } if (overlayStack[overlayStack.length - 1] === token) { overlayStack.pop(); cb.current(); } }; window.addEventListener("popstate", onPop); return () => { window.removeEventListener("popstate", onPop); const idx = overlayStack.lastIndexOf(token); if (idx !== -1) { // Closed programmatically (not via Back): drop our own history entry, and tell the // remaining overlays' handlers to ignore the resulting popstate. A one-shot listener // clears the flag on that very popstate even when NO overlay remains underneath to // consume it — otherwise suppressPop leaks `true` and silently swallows the NEXT // overlay's first Back (reopen-after-button-close → first Back does nothing). overlayStack.splice(idx, 1); suppressPop = true; const clearSuppress = () => { suppressPop = false; window.removeEventListener("popstate", clearSuppress); }; window.addEventListener("popstate", clearSuppress); window.history.back(); } }; }, []); }