feat : comment feature
CI / Verify (push) Successful in 2m30s
CI / Build immutable images and deploy (push) Successful in 3m9s

This commit is contained in:
2026-10-08 01:40:08 +07:00 Unverified
parent 8c8815cda5
commit 5812d99715
65 changed files with 27013 additions and 10 deletions
+56
View File
@@ -0,0 +1,56 @@
"use client";
import { useEffect, useRef } from "react";
/** One stream per board/thread; open replies refresh through the board revision. */
export function useCommentEvents(scope: { target: string } | { admin: true }, refresh: (changed: boolean) => Promise<unknown>) {
const refreshRef = useRef(refresh);
useEffect(() => { refreshRef.current = refresh; }, [refresh]);
const query = "target" in scope ? `target=${encodeURIComponent(scope.target)}` : "scope=admin";
useEffect(() => {
let source: EventSource | null = null;
let timer: ReturnType<typeof setTimeout> | undefined;
let refreshing = false;
let dirty = false;
let pendingChange = false;
let connectedBefore = false;
let disposed = false;
async function flush() {
timer = undefined;
if (disposed) return;
if (refreshing) { dirty = true; return; }
refreshing = true;
const changed = pendingChange;
pendingChange = false;
try { await refreshRef.current(changed); }
finally {
refreshing = false;
if (dirty && !disposed) { dirty = false; schedule(); }
}
}
function schedule(changed = false) {
pendingChange ||= changed;
if (!timer && !disposed) timer = setTimeout(() => { void flush(); }, 250);
}
function connect() {
source?.close(); source = null;
if (document.visibilityState !== "visible") return;
source = new EventSource(`/api/comments/events?${query}`);
source.addEventListener("changed", () => schedule(true));
// Catch updates between the initial page response and stream subscription,
// and updates missed while disconnected. EventSource handles reconnects.
source.addEventListener("open", () => {
// A reconnect can have missed reply edits even if the root is unchanged.
schedule(connectedBefore);
connectedBefore = true;
});
}
connect();
document.addEventListener("visibilitychange", connect);
return () => {
disposed = true; source?.close();
if (timer) clearTimeout(timer);
document.removeEventListener("visibilitychange", connect);
};
}, [query]);
}