- 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>
97 lines
3.1 KiB
TypeScript
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 };
|
|
}
|