"use client"; import { createContext, useContext, useEffect } from "react"; import { notesQuery, useNotes, type NotesTarget, type UseNotes } from "@/lib/annotations/useNotes"; // ONE handle on a notes.json per page. // // Every write carries the token its handle last read, so two handles on the // same file in one page -- the timeline's row notes and the final video's // timed notes, say -- would 409 each other on every other write. A page wraps // itself in , and every notes component under it shares // that handle; a component outside any provider (or under one for another // file) opens its own. const Ctx = createContext<{ key: string; notes: UseNotes } | null>(null); /** Something on the page wrote to a project's notes server-side (an edit note): re-read them. */ export const NOTES_CHANGED = "umtool:notes-changed"; export function announceNotesChanged(project: string) { window.dispatchEvent(new CustomEvent(NOTES_CHANGED, { detail: { project } })); } /** Re-read `notes` when the page says its project's notes changed under it. */ export function useNotesRefresh(target: NotesTarget | null, notes: UseNotes) { const project = target && "project" in target ? target.project : null; const { reload } = notes; useEffect(() => { if (!project) return; const on = (e: Event) => { if ((e as CustomEvent).detail?.project === project) void reload(); }; window.addEventListener(NOTES_CHANGED, on); return () => window.removeEventListener(NOTES_CHANGED, on); }, [project, reload]); } export function NotesProvider({ target, children }: { target: NotesTarget; children: React.ReactNode }) { const notes = useNotes(target); useNotesRefresh(target, notes); return {children}; } /** * Share a handle the page already holds (the article reader's) with the notes * components under it, so they write with ITS token rather than opening a * second one on the same file. */ export function ShareNotes({ target, notes, children }: { target: NotesTarget; notes: UseNotes; children: React.ReactNode }) { return {children}; } /** The page's shared handle for `target`, or a handle of this component's own. */ export function useSharedNotes(target: NotesTarget | null): UseNotes { const ctx = useContext(Ctx); const shared = !!(ctx && target && ctx.key === notesQuery(target)); const own = useNotes(shared ? null : target); useNotesRefresh(shared ? null : target, own); return shared ? ctx!.notes : own; } /** True when a write's response says the server wrote notes too (an edit on a generated manifest). */ export const wroteNotes = (j: unknown): boolean => { const e = (j as { editNotes?: { added?: number; updated?: number; deleted?: number } | null })?.editNotes; return !!e && (e.added ?? 0) + (e.updated ?? 0) + (e.deleted ?? 0) > 0; };