@@ -224,10 +230,27 @@ function EmptyState({
);
}
+function ConnectionNotice({
+ demoUnavailable,
+ message,
+}: {
+ demoUnavailable: boolean;
+ message: string | null;
+}) {
+ if (!message) return null;
+ return (
+
+ {message}
+ {demoUnavailable
+ ? ". Check public/demo-diff-data.json."
+ : " The last valid review stays visible while Diffsplain retries."}
+
+ );
+}
+
export default function Home() {
- const [snapshot, setSnapshot] = useState
(null);
- const [loadError, setLoadError] = useState(null);
- const [demoUnavailable, setDemoUnavailable] = useState(false);
+ const { demoUnavailable, loadError, snapshot } =
+ useLiveSnapshot();
const [selectedPath, setSelectedPath] = useState(null);
const [expandedFiles, setExpandedFiles] = useState>(new Set());
const [pickerOpen, setPickerOpen] = useState(false);
@@ -235,108 +258,23 @@ export default function Home() {
const [motion, setMotion] = useState<"next" | "previous" | "pick">("pick");
const [motionKey, setMotionKey] = useState(0);
const [clock, setClock] = useState(0);
- const latestVersion = useRef(null);
const touchStart = useRef(null);
const searchRef = useRef(null);
- const session = useMemo(
- () => new URLSearchParams(window.location.hash.slice(1)),
- [],
- );
- const [access, setAccess] = useState(() => session.get("access"));
-
- const refresh = useCallback(async () => {
- if (demoUnavailable) return;
-
- let usingBundledDemo = false;
- try {
- const liveUrl = new URL("diff-data.json", document.baseURI);
- liveUrl.searchParams.set("t", String(Date.now()));
- if (access) liveUrl.searchParams.set("access", access);
- const liveResponse = await fetch(liveUrl, {
- cache: "no-store",
- });
- usingBundledDemo =
- liveResponse.status === 404 ||
- liveResponse.headers.get("x-diffsplain-demo") === "true";
- const response = usingBundledDemo
- ? liveResponse.status === 404
- ? await fetch(new URL("demo-diff-data.json", document.baseURI))
- : liveResponse
- : liveResponse;
- if (!response.ok) throw new Error(`Snapshot returned ${response.status}`);
- const next = (await response.json()) as DiffSnapshot;
- if (!Array.isArray(next.files)) throw new Error("Snapshot has no files");
-
- if (next.version !== latestVersion.current) {
- latestVersion.current = next.version;
- setSnapshot(next);
- setSelectedPath((current) => {
- if (current && next.files.some((file) => file.path === current)) {
- return current;
- }
- return next.files[0]?.path ?? null;
- });
- }
- setLoadError(null);
- } catch (error) {
- const message =
- error instanceof Error ? error.message : "Could not read the snapshot";
- if (usingBundledDemo) {
- setDemoUnavailable(true);
- setLoadError(`Bundled demo is unavailable: ${message}`);
- return;
- }
- setLoadError(message);
- }
- }, [access, demoUnavailable]);
useEffect(() => {
- const initial = window.setTimeout(() => void refresh(), 0);
- let poll: number | undefined;
- let events: EventSource | undefined;
- const startPolling = () => {
- if (poll === undefined) {
- poll = window.setInterval(() => void refresh(), FALLBACK_POLL_MS);
- }
- };
- const stopPolling = () => {
- if (poll !== undefined) {
- window.clearInterval(poll);
- poll = undefined;
- }
- };
- if ("EventSource" in window) {
- const eventsUrl = new URL("events", document.baseURI);
- const project = session.get("project");
- if (project) eventsUrl.searchParams.set("project", project);
- if (access) eventsUrl.searchParams.set("access", access);
- events = new EventSource(eventsUrl);
- events.addEventListener("ready", () => {
- stopPolling();
- latestVersion.current = null;
- setSnapshot(null);
- void refresh();
- });
- events.addEventListener("update", () => void refresh());
- events.addEventListener("access", (event) => {
- const nextAccess = (event as MessageEvent).data;
- if (!/^[A-Za-z0-9_-]{32,}$/.test(nextAccess)) return;
- session.set("access", nextAccess);
- window.history.replaceState(null, "", `#${session}`);
- setAccess(nextAccess);
- });
- events.addEventListener("error", startPolling);
- } else {
- startPolling();
- }
const ticker = window.setInterval(() => setClock((value) => value + 1), 5_000);
- return () => {
- window.clearTimeout(initial);
- if (poll !== undefined) window.clearInterval(poll);
- events?.close();
- window.clearInterval(ticker);
- };
- }, [access, refresh, session]);
+ return () => window.clearInterval(ticker);
+ }, []);
+
+ useEffect(() => {
+ if (!snapshot) return;
+ setSelectedPath((current) => {
+ if (current && snapshot.files.some((file) => file.path === current)) {
+ return current;
+ }
+ return snapshot.files[0]?.path ?? null;
+ });
+ }, [snapshot]);
const files = useMemo(() => snapshot?.files ?? [], [snapshot]);
const currentIndex = Math.max(
@@ -407,18 +345,24 @@ export default function Home() {
return (
<>
- {loadError ? (
-
- {loadError}
- {demoUnavailable ? ". Check public/demo-diff-data.json." : ". Retrying…"}
-
- ) : null}
+
>
);
}
if (!files.length) {
- return ;
+ return (
+ <>
+
+
+ >
+ );
}
if (!currentFile) return ;
@@ -468,16 +412,7 @@ export default function Home() {
}}
>