import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useSkateRelays } from '@/hooks/useSkateRelays'; import { EVENT_UPDATE_KIND, parseEventUpdate, type EventUpdate, type EventUpdateKind, type SkateEvent, } from '@/lib/types'; import { eventCoord } from '@/hooks/useEventRsvps'; import { isAdminPubkey } from '@/lib/admins'; /** * Host "Event Updates" (kind 7103) — a public broadcast pinned to a calendar * event. Hosts use it to say "rain, moved to Sunday" / "meet at the back ledge". * Everyone who RSVP'd is p-tagged so they get a notification; anyone browsing * the event page also sees the update thread. */ export function useEventUpdates(coord: string | undefined) { const { querySkateContent, getQueryKeySuffix } = useSkateRelays(); return useQuery({ queryKey: ['event-updates', coord, getQueryKeySuffix()], enabled: !!coord, staleTime: 30_000, queryFn: async (c) => { const signal = AbortSignal.any([c.signal, AbortSignal.timeout(6000)]); const events = await querySkateContent( [{ kinds: [EVENT_UPDATE_KIND], '#a': [coord!], limit: 100 }], { signal }, ); const updates: EventUpdate[] = []; for (const ev of events) { const u = parseEventUpdate(ev); if (u && u.eventCoord === coord) updates.push(u); } // Newest first. return updates.sort((a, b) => b.createdAt - a.createdAt); }, }); } /** * Publish a host update. Only the event's own author should call this (the UI * gates it). All `notifyPubkeys` (RSVPers) are p-tagged so they get pinged. */ export function usePostEventUpdate() { const { mutateAsync: publish, isPending } = useNostrPublish(); const { user } = useCurrentUser(); const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: async (opts: { event: SkateEvent; message: string; updateKind?: EventUpdateKind; /** RSVPer pubkeys to notify (deduped, minus the host). */ notifyPubkeys?: string[]; }) => { if (!user) throw new Error('You must be logged in to post an update.'); const isHost = user.pubkey === opts.event.pubkey; const isAdmin = isAdminPubkey(user.pubkey); if (!isHost && !isAdmin) { throw new Error('Only the event host or a SkateSpots admin can post updates.'); } const coord = eventCoord(opts.event); const kind = opts.updateKind ?? 'update'; const notify = Array.from( new Set((opts.notifyPubkeys ?? []).filter((pk) => pk && pk !== user.pubkey)), ); const tags: string[][] = [ ['a', coord], ['e', opts.event.id], ['update-kind', kind], ['t', 'skateboarding'], ['alt', `Update to a skate event: ${opts.event.title}`], // Mark moderator actions so clients can show "Admin" instead of "Host". // (An admin posting on an event they don't own is a moderation action.) ...(!isHost && isAdmin ? [['admin', 'true']] : []), ...notify.map((pk) => ['p', pk]), ]; return publish({ kind: EVENT_UPDATE_KIND, content: opts.message.trim(), tags, }); }, onSuccess: (_data, variables) => { const coord = eventCoord(variables.event); queryClient.invalidateQueries({ queryKey: ['event-updates', coord] }); }, }); return { ...mutation, isPending: isPending || mutation.isPending }; }