import { ExtendedKind } from '@/constants' import { getReplaceableCoordinateFromEvent } from '@/lib/event' import { isCalendarEventKind } from '@/lib/calendar-event' import client from '@/services/client.service' import { queryService } from '@/services/client.service' import { useNostr } from '@/providers/NostrProvider' import { Event } from 'nostr-tools' import { useEffect, useState } from 'react' import { normalizeAnyRelayUrl } from '@/lib/url' import { FAST_READ_RELAY_URLS } from '@/constants' import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays' import { tagNameEquals } from '@/lib/tag' function getRsvpStatus(rsvp: Event): 'accepted' | 'tentative' | 'declined' | undefined { const status = rsvp.tags.find(tagNameEquals('status'))?.[1] if (status === 'accepted' || status === 'tentative' || status === 'declined') return status return undefined } function mergeRsvp(prev: Event[], evt: Event): Event[] { const next = prev.filter((e) => e.id !== evt.id) const samePubkey = next.find((e) => e.pubkey === evt.pubkey) if (samePubkey && samePubkey.created_at >= evt.created_at) return next const withoutSamePubkey = samePubkey ? next.filter((e) => e.pubkey !== evt.pubkey) : next return [...withoutSamePubkey, evt].sort((a, b) => b.created_at - a.created_at) } export function useFetchCalendarRsvps(calendarEvent: Event | undefined) { const { relayList } = useNostr() const [rsvps, setRsvps] = useState([]) const [isFetching, setIsFetching] = useState(false) useEffect(() => { if (!calendarEvent || !isCalendarEventKind(calendarEvent.kind)) { setRsvps([]) return } let cancelled = false setIsFetching(true) const coordinate = getReplaceableCoordinateFromEvent(calendarEvent) const userRead = userReadRelaysWithHttp(relayList) const baseUrls = new Set([ ...FAST_READ_RELAY_URLS.map((url) => normalizeAnyRelayUrl(url) || url), ...userRead.map((url) => normalizeAnyRelayUrl(url) || url) ].filter(Boolean) as string[]) // Include organizer's relays so RSVPs are found when viewing an attendee's profile (RSVPs are often on organizer's outbox/inbox) const organizerPubkey = calendarEvent.pubkey client .fetchRelayList(organizerPubkey) .then((organizerRelays) => { if (cancelled) return ;[ ...(organizerRelays?.httpRead ?? []), ...(organizerRelays?.read ?? []), ...(organizerRelays?.httpWrite ?? []), ...(organizerRelays?.write ?? []) ].forEach((url) => { const u = normalizeAnyRelayUrl(url) if (u) baseUrls.add(u) }) return Array.from(baseUrls) }) .catch(() => Array.from(baseUrls)) .then((relayUrls: string[] | undefined) => { if (cancelled) return const urls = relayUrls?.length ? relayUrls : Array.from(baseUrls) return queryService.fetchEvents( urls, { kinds: [ExtendedKind.CALENDAR_EVENT_RSVP], '#a': [coordinate], limit: 200 }, { firstRelayResultGraceMs: false } ) }) .then((events) => { if (cancelled) return setRsvps(events ?? []) }) .finally(() => { if (!cancelled) setIsFetching(false) }) return () => { cancelled = true } }, [calendarEvent?.id, calendarEvent?.kind, calendarEvent?.pubkey, relayList]) // When we publish an RSVP, NostrProvider calls client.emitNewEvent(event). Merge it into rsvps so the UI updates immediately. useEffect(() => { if (!calendarEvent || !isCalendarEventKind(calendarEvent.kind)) return const coordinate = getReplaceableCoordinateFromEvent(calendarEvent) const handler = (e: CustomEvent) => { const evt = e.detail if (evt.kind !== ExtendedKind.CALENDAR_EVENT_RSVP) return const aTag = evt.tags.find(tagNameEquals('a')) if (aTag?.[1] !== coordinate) return setRsvps((prev) => mergeRsvp(prev, evt)) } client.addEventListener('newEvent', handler as EventListener) return () => client.removeEventListener('newEvent', handler as EventListener) }, [calendarEvent?.id, calendarEvent?.kind]) return { rsvps, isFetching, getRsvpStatus } }