From 4894ef5a3181f5c47c5e9a8df6778ffea81445c4 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 17 Mar 2026 12:02:16 +0100 Subject: [PATCH] bug-fix calendar events --- src/components/CalendarEventContent/index.tsx | 81 +++++++++++++++++++ src/hooks/useFetchCalendarRsvps.tsx | 25 ++++++ src/i18n/locales/de.ts | 3 + src/i18n/locales/en.ts | 3 + 4 files changed, 112 insertions(+) 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')}
+ +
+ )} ) } 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',