"use client"; /** * usePresenceToasts (TASK-0093, spec R5) — the join/leave TOASTS for a * page's presence (the live region is the a11y channel; the toasts are the * visual one). Controlled by the `presenceToastsEnabled` setting (ON by * default) in the ui store. * * - Join: "{name} joined" (a peer newly present — a `presence` frame for * a userId not in the prior roster). * - Leave: "{name} left" (a `presence-leave`, or a peer dropped from the * roster — the 90 s prune surfaces as their frame not re-asserting; * we key on the store roster diff, so the prune toast fires too). * - Edit START/STOP are NOT toasted (the live region covers them; toasts * for every start/stop would be noisy — the spec says toasts are * join/leave). * * Deterministic (no timers — toasts auto-dismiss on their own duration). * Call once per page (the page view, next to `usePresenceAnnouncer`). */ import { useEffect, useRef } from "react"; import { toast, useUiStore } from "@/stores/ui-store"; import { useBlockPatchStore } from "@/stores/block-patch-store"; import type { Presence } from "@/lib/types"; const EMPTY_PRESENCE: Record = {}; export function usePresenceToasts(pageId: string | null, selfUserId?: string | null): void { const enabled = useUiStore((s) => s.presenceToastsEnabled); const prevRef = useRef | null>(null); useEffect(() => { if (pageId === null || !enabled) { prevRef.current = null; return; } const feed = (state: { presence: Record> }): void => { const roster = Object.values(state.presence[pageId] ?? EMPTY_PRESENCE); const now = new Map(); for (const p of roster) if (p.userId !== selfUserId) now.set(p.userId, p.name); const prev = prevRef.current; if (prev !== null) { for (const [userId, name] of now) { if (!prev.has(userId)) toast("info", `${name} joined`); } for (const [userId, name] of prev) { if (!now.has(userId)) toast("info", `${name} left`); } } prevRef.current = now; }; feed(useBlockPatchStore.getState()); const off = useBlockPatchStore.subscribe(feed); return () => { off(); prevRef.current = null; }; }, [pageId, enabled, selfUserId]); }