This commit is contained in:
+61
-18
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Badge,
|
||||
Banner,
|
||||
@@ -44,6 +44,9 @@ export default function App() {
|
||||
const [account, setAccount] = useState<Account | null>(null);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [jobs, setJobs] = useState<Job[]>([]);
|
||||
const statusRequest = useRef<AbortController | null>(null);
|
||||
const jobStatus = useRef<string | null>(null);
|
||||
const [pageVisible, setPageVisible] = useState(!document.hidden);
|
||||
const [page, setPage] = useState<WorkspacePage>(() =>
|
||||
pageFromHash(location.hash),
|
||||
);
|
||||
@@ -124,17 +127,32 @@ export default function App() {
|
||||
document.documentElement.style.setProperty("--chat-space", "0px");
|
||||
};
|
||||
}, [chatOffset, chatWidth]);
|
||||
const refresh = useCallback(async () => {
|
||||
const refresh = useCallback(async (includeAccount = true) => {
|
||||
// Polls never overlap. An explicit refresh supersedes a read started before the user's action.
|
||||
if (!includeAccount && statusRequest.current) return;
|
||||
statusRequest.current?.abort();
|
||||
const controller = new AbortController();
|
||||
statusRequest.current = controller;
|
||||
try {
|
||||
const [nextAccount, nextJobs] = await Promise.all([
|
||||
api<Account>("/account"),
|
||||
api<Job[]>("/sync-jobs"),
|
||||
]);
|
||||
setAccount(nextAccount);
|
||||
const options = { signal: controller.signal };
|
||||
const nextJobs = await api<Job[]>("/sync-jobs", options);
|
||||
const nextStatus = nextJobs
|
||||
.map((job) => `${job.id}:${job.status}`)
|
||||
.join("|");
|
||||
// Job transitions can change connection state or the last sync time; progress alone cannot.
|
||||
if (includeAccount || jobStatus.current !== nextStatus) {
|
||||
const nextAccount = await api<Account>("/account", options);
|
||||
if (controller.signal.aborted) return;
|
||||
setAccount(nextAccount);
|
||||
}
|
||||
if (controller.signal.aborted) return;
|
||||
jobStatus.current = nextStatus;
|
||||
setJobs(nextJobs);
|
||||
setPollError("");
|
||||
} catch (error) {
|
||||
setPollError((error as Error).message);
|
||||
if (!controller.signal.aborted) setPollError((error as Error).message);
|
||||
} finally {
|
||||
if (statusRequest.current === controller) statusRequest.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -160,10 +178,40 @@ export default function App() {
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (!authenticated) return;
|
||||
void refresh();
|
||||
const timer = window.setInterval(() => void refresh(), 3000);
|
||||
return () => clearInterval(timer);
|
||||
}, [authenticated, refresh]);
|
||||
const resume = () => {
|
||||
setPageVisible(!document.hidden);
|
||||
if (!document.hidden) void refresh();
|
||||
else statusRequest.current?.abort();
|
||||
};
|
||||
resume();
|
||||
window.addEventListener("focus", resume);
|
||||
window.addEventListener("online", resume);
|
||||
document.addEventListener("visibilitychange", resume);
|
||||
return () => {
|
||||
window.removeEventListener("focus", resume);
|
||||
window.removeEventListener("online", resume);
|
||||
document.removeEventListener("visibilitychange", resume);
|
||||
statusRequest.current?.abort();
|
||||
};
|
||||
}, [authenticated, page, showJobs, refresh]);
|
||||
const hasRunningJobs = jobs.some((job) =>
|
||||
["queued", "running"].includes(job.status),
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!authenticated || !pageVisible || (!hasRunningJobs && !pollError))
|
||||
return;
|
||||
let stopped = false;
|
||||
let timer: number;
|
||||
const poll = async () => {
|
||||
await refresh(!!pollError);
|
||||
if (!stopped) timer = window.setTimeout(() => void poll(), 3000);
|
||||
};
|
||||
timer = window.setTimeout(() => void poll(), 3000);
|
||||
return () => {
|
||||
stopped = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [authenticated, pageVisible, hasRunningJobs, pollError, refresh]);
|
||||
useEffect(() => {
|
||||
document.body.setAttribute("theme-mode", account?.theme ?? "light");
|
||||
}, [account?.theme]);
|
||||
@@ -235,12 +283,7 @@ export default function App() {
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : !authenticated ? (
|
||||
<Login
|
||||
onLogin={() => {
|
||||
setAuthenticated(true);
|
||||
void refresh();
|
||||
}}
|
||||
/>
|
||||
<Login onLogin={() => setAuthenticated(true)} />
|
||||
) : (
|
||||
<div
|
||||
className={`workspace ${sidebarCollapsed ? "sidebar-collapsed" : ""}`}
|
||||
|
||||
Reference in New Issue
Block a user