"use client"; /** * usePresenceAnnouncer (TASK-0093, spec R5) — the polite live region for a * page's presence: "Ana is editing this page" / "Ana left" / "Ana joined", * rate-limited to ≤ 1 announcement per second, STATE CHANGES ONLY (a peer * typing fires cursor frames at ~8/s — those never announce). * * The pure state machine (the rate limiter + the join/edit/leave state * detection) is {@link PresenceAnnouncer} (lib/presence-announcer) — this * hook only owns the DOM: the `role=status` live-region text, fed from the * store's roster (others only), flushed on an idle tick so a change that * lands inside the 1 s window is still delivered (not lost). * * Returns the current announcement text (the component renders it in the * live region). */ import { useEffect, useRef, useState } from "react"; import { PresenceAnnouncer } from "@/lib/presence-announcer"; import { useBlockPatchStore } from "@/stores/block-patch-store"; import type { Presence } from "@/lib/types"; const EMPTY_PRESENCE: Record = {}; const FLUSH_INTERVAL_MS = 1000; export function usePresenceAnnouncer(pageId: string | null, selfUserId?: string | null): string { const [text, setText] = useState(""); const announcerRef = useRef(null); useEffect(() => { if (pageId === null) return; const announcer = new PresenceAnnouncer(); announcerRef.current = announcer; const emit = (a: { text: string } | null): void => { if (a !== null) setText(a.text); }; const feed = (state: { presence: Record> }): void => { const roster = Object.values(state.presence[pageId] ?? EMPTY_PRESENCE); emit(announcer.update(selfUserId ? roster.filter((p) => p.userId !== selfUserId) : roster)); }; feed(useBlockPatchStore.getState()); const off = useBlockPatchStore.subscribe(feed); // Flush a change that was queued inside the 1 s window (so it is // delivered, not lost) — the announcer emits at most once per window. const id = window.setInterval(() => emit(announcer.flush()), FLUSH_INTERVAL_MS); return () => { off(); window.clearInterval(id); announcerRef.current = null; announcer.reset(); setText(""); }; }, [pageId, selfUserId]); return text; }