diff --git a/src/components/CalendarEventContent/index.tsx b/src/components/CalendarEventContent/index.tsx
index fccaf256..413439b7 100644
--- a/src/components/CalendarEventContent/index.tsx
+++ b/src/components/CalendarEventContent/index.tsx
@@ -5,10 +5,14 @@ import {
formatCalendarDate,
isCalendarEventKind
} from '@/lib/calendar-event'
+import { tagNameEquals } from '@/lib/tag'
import { useFetchCalendarRsvps } from '@/hooks/useFetchCalendarRsvps'
import { useNostr } from '@/providers/NostrProvider'
+import { toProfile } from '@/lib/link'
+import { useSecondaryPage } from '@/PageManager'
import { Event } from 'nostr-tools'
import { useTranslation } from 'react-i18next'
+import { useMemo } from 'react'
import { Button } from '../ui/button'
import { Calendar, Video, CheckCircle, HelpCircle, XCircle } from 'lucide-react'
import { cn } from '@/lib/utils'
@@ -18,6 +22,8 @@ import {
DropdownMenuItem,
DropdownMenuTrigger
} from '../ui/dropdown-menu'
+import UserAvatar from '../UserAvatar'
+import Username from '../Username'
import { toast } from 'sonner'
type RsvpStatus = 'accepted' | 'tentative' | 'declined'
@@ -32,6 +38,7 @@ export default function CalendarEventContent({
showRsvp?: boolean
}) {
const { t } = useTranslation()
+ const { push } = useSecondaryPage()
const { pubkey: myPubkey, publish } = useNostr()
const { rsvps, isFetching, getRsvpStatus: getStatus } = useFetchCalendarRsvps(event)
@@ -43,6 +50,26 @@ export default function CalendarEventContent({
const myRsvp = myPubkey ? rsvps.find((r) => r.pubkey === myPubkey) : undefined
const myStatus = myRsvp ? getStatus(myRsvp) : undefined
+ // Organizer + invitees (event p tags) + anyone who sent an RSVP. Each shows response: accepted/tentative/declined or no response.
+ const attendeesList = useMemo(() => {
+ const organizerPubkey = event.pubkey
+ const participantPubkeys = event.tags
+ .filter(tagNameEquals('p'))
+ .map((t) => t[1]?.trim())
+ .filter(Boolean) as string[]
+ const allPubkeys = Array.from(
+ new Set([organizerPubkey, ...participantPubkeys, ...rsvps.map((r) => r.pubkey)])
+ )
+ return allPubkeys.map((pubkey) => {
+ const rsvp = rsvps.find((r) => r.pubkey === pubkey)
+ return {
+ pubkey,
+ status: (rsvp ? getStatus(rsvp) : null) as RsvpStatus | null,
+ isOrganizer: pubkey === organizerPubkey
+ }
+ })
+ }, [event.pubkey, event.tags, rsvps])
+
const handleRsvp = async (status: RsvpStatus) => {
if (!myPubkey) {
toast.error(t('You need to log in to RSVP'))
@@ -159,6 +186,60 @@ export default function CalendarEventContent({
)}
+ {attendeesList.length > 0 && (
+
+
{t('Attendees')}
+
+ {attendeesList.map(({ pubkey, status, isOrganizer }) => (
+ -
+
+
+ ))}
+
+
+ )}
)
}
diff --git a/src/hooks/useFetchCalendarRsvps.tsx b/src/hooks/useFetchCalendarRsvps.tsx
index a74cbc8c..575bc9c6 100644
--- a/src/hooks/useFetchCalendarRsvps.tsx
+++ b/src/hooks/useFetchCalendarRsvps.tsx
@@ -15,6 +15,14 @@ function getRsvpStatus(rsvp: Event): 'accepted' | 'tentative' | 'declined' | und
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([])
@@ -57,6 +65,23 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
}
}, [calendarEvent?.id, calendarEvent?.kind, relayList?.read])
+ // 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,
diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts
index df1b6dd9..04f38c07 100644
--- a/src/i18n/locales/de.ts
+++ b/src/i18n/locales/de.ts
@@ -103,6 +103,9 @@ export default {
'You need to log in to RSVP': 'Zum Antworten bitte anmelden',
'RSVP updated': 'Rückmeldung gesendet',
'Failed to update RSVP': 'Rückmeldung konnte nicht gesendet werden',
+ Organizer: 'Veranstalter',
+ Attendees: 'Teilnehmer',
+ 'No response': 'Keine Rückmeldung',
'Calendar Events': 'Kalendertermine',
'Calendar Event': 'Kalendertermin',
'Schedule in-person meeting': 'Präsenztermin planen',
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 4e62f6aa..2f2637b5 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -157,6 +157,9 @@ export default {
'You need to log in to RSVP': 'You need to log in to RSVP',
'RSVP updated': 'RSVP updated',
'Failed to update RSVP': 'Failed to update RSVP',
+ Organizer: 'Organizer',
+ Attendees: 'Attendees',
+ 'No response': 'No response',
'Calendar Events': 'Calendar Events',
'Calendar Event': 'Calendar Event',
'Schedule in-person meeting': 'Schedule in-person meeting',