SkateSpots/src/hooks/useEventUpdates.ts
shakespeare.diy 78f4a6d09a Host tools: attendee list w/ trust, event updates, edit-alerts, Hosting tab
- AttendeeList: tabbed Going/Interested with faces + names + a "who you trust"
  signal (reuses batch trust scores); replaces the plain avatar stack
- Event Updates (kind 7103): usePostEventUpdate + useEventUpdates; host-only
  composer with update/reschedule/relocation/cancelled categories that p-tags
  every RSVPer so they get notified; public feed on the event page
- EditEventPage: auto-post a reschedule/relocation update to notify attendees
  when the host changes time or location
- Activity bell: new 'event-update' notification type for RSVPers
- EventsPage: "Hosting" tab (logged-in) with a HostingDashboard listing the
  user's upcoming/past events, live RSVP counts, and quick edit/update actions
- Document Event Update kind 7103 in NIP.md

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-03 11:25:57 +07:00

97 lines
3.1 KiB
TypeScript

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';
/**
* 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.');
if (user.pubkey !== opts.event.pubkey) {
throw new Error('Only the event host 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}`],
...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 };
}