import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useEffect, useState } from "react"; import { api, type ChangeEvent, type EntryPatch, type EntryView } from "./api.ts"; import { applyEntryPatch } from "./edits.ts"; /** Pending Changes-inbox events (first 200; the badge shows "200+" beyond that). */ export function usePendingChanges() { return useQuery({ queryKey: ["changes", "pending"], queryFn: api.changes, staleTime: 30_000 }); } export function useMe() { return useQuery({ queryKey: ["me"], queryFn: api.me, retry: false, staleTime: Infinity }); } export function useSources() { return useQuery({ queryKey: ["sources"], queryFn: api.sources, staleTime: 60_000 }); } /** Pending events that touch an entry or one of its reads/plays. */ export function changesFor(items: ChangeEvent[] | undefined, entryId: string, engagementIds: Set): ChangeEvent[] { return (items ?? []).filter( (c) => c.targetId !== null && ((c.targetEntity === "entry" && c.targetId === entryId) || (c.targetEntity === "engagement" && engagementIds.has(c.targetId))), ); } export function useDebounced(value: T, ms = 220): T { const [v, setV] = useState(value); useEffect(() => { const t = setTimeout(() => setV(value), ms); return () => clearTimeout(t); }, [value, ms]); return v; } /** * Shared save path for every detail edit. The cached entry is updated optimistically (rolled back on * error) and PATCHes for one entry run one at a time (mutation scope), in the order they were made. * Use `edit(cur => patch)` when the next value depends on the current one (tags, custom fields): it * builds from the latest cache, so quick successive edits compose. */ export function useEntryPatch(id: string) { const qc = useQueryClient(); const key = ["entry", id]; const mutationKey = ["entry-patch", id]; const m = useMutation({ mutationKey, scope: { id: `entry-patch:${id}` }, mutationFn: (patch: EntryPatch) => api.patchEntry(id, patch), onMutate: async (patch) => { await qc.cancelQueries({ queryKey: key }); const prev = qc.getQueryData(key); if (prev) qc.setQueryData(key, applyEntryPatch(prev, patch)); return { prev }; }, onError: (_e, _patch, ctx) => { if (ctx?.prev) qc.setQueryData(key, ctx.prev); void qc.invalidateQueries({ queryKey: key }); }, onSuccess: (entry) => { // Only the last queued edit writes the server's answer, so earlier answers don't flash old values. if (qc.isMutating({ mutationKey }) <= 1) qc.setQueryData(key, entry); }, onSettled: () => { void qc.invalidateQueries({ queryKey: ["entries"] }); void qc.invalidateQueries({ queryKey: ["history", id] }); }, }); const edit = (build: (cur: EntryView) => EntryPatch | null) => { const cur = qc.getQueryData(key); const patch = cur ? build(cur) : null; if (patch) m.mutate(patch); }; return { ...m, edit }; }