You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

115 lines
4.2 KiB

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<Event[]>([])
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<string>([
...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<Event>) => {
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
}
}