Browse Source

bug-fix calendar events

imwald
Silberengel 1 month ago
parent
commit
4894ef5a31
  1. 81
      src/components/CalendarEventContent/index.tsx
  2. 25
      src/hooks/useFetchCalendarRsvps.tsx
  3. 3
      src/i18n/locales/de.ts
  4. 3
      src/i18n/locales/en.ts

81
src/components/CalendarEventContent/index.tsx

@ -5,10 +5,14 @@ import {
formatCalendarDate, formatCalendarDate,
isCalendarEventKind isCalendarEventKind
} from '@/lib/calendar-event' } from '@/lib/calendar-event'
import { tagNameEquals } from '@/lib/tag'
import { useFetchCalendarRsvps } from '@/hooks/useFetchCalendarRsvps' import { useFetchCalendarRsvps } from '@/hooks/useFetchCalendarRsvps'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { toProfile } from '@/lib/link'
import { useSecondaryPage } from '@/PageManager'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useMemo } from 'react'
import { Button } from '../ui/button' import { Button } from '../ui/button'
import { Calendar, Video, CheckCircle, HelpCircle, XCircle } from 'lucide-react' import { Calendar, Video, CheckCircle, HelpCircle, XCircle } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@ -18,6 +22,8 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger DropdownMenuTrigger
} from '../ui/dropdown-menu' } from '../ui/dropdown-menu'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
import { toast } from 'sonner' import { toast } from 'sonner'
type RsvpStatus = 'accepted' | 'tentative' | 'declined' type RsvpStatus = 'accepted' | 'tentative' | 'declined'
@ -32,6 +38,7 @@ export default function CalendarEventContent({
showRsvp?: boolean showRsvp?: boolean
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { push } = useSecondaryPage()
const { pubkey: myPubkey, publish } = useNostr() const { pubkey: myPubkey, publish } = useNostr()
const { rsvps, isFetching, getRsvpStatus: getStatus } = useFetchCalendarRsvps(event) 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 myRsvp = myPubkey ? rsvps.find((r) => r.pubkey === myPubkey) : undefined
const myStatus = myRsvp ? getStatus(myRsvp) : 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) => { const handleRsvp = async (status: RsvpStatus) => {
if (!myPubkey) { if (!myPubkey) {
toast.error(t('You need to log in to RSVP')) toast.error(t('You need to log in to RSVP'))
@ -159,6 +186,60 @@ export default function CalendarEventContent({
</DropdownMenu> </DropdownMenu>
)} )}
</div> </div>
{attendeesList.length > 0 && (
<div className="mt-3 pt-3 border-t border-border/60">
<div className="text-xs font-medium text-muted-foreground mb-2">{t('Attendees')}</div>
<ul className="space-y-1.5">
{attendeesList.map(({ pubkey, status, isOrganizer }) => (
<li key={pubkey}>
<button
type="button"
onClick={() => push(toProfile(pubkey))}
className={cn(
'w-full flex items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs',
'hover:bg-muted/60 transition-colors min-w-0'
)}
>
<UserAvatar userId={pubkey} size="xSmall" className="shrink-0" />
<span className="min-w-0 truncate flex-1">
<Username userId={pubkey} className="text-foreground" skeletonClassName="h-3" />
</span>
<span className="shrink-0 flex items-center gap-1.5 text-muted-foreground">
{isOrganizer && (
<span className="inline-flex items-center rounded bg-muted px-1.5 py-0.5 text-[10px]">
{t('Organizer')}
</span>
)}
{status === 'accepted' && (
<span className="flex items-center gap-1 text-green-600" title={t('Accepted')}>
<CheckCircle className="size-3.5" />
<span className="text-[10px]">{t('Accepted')}</span>
</span>
)}
{status === 'tentative' && (
<span className="flex items-center gap-1 text-amber-600" title={t('Tentative')}>
<HelpCircle className="size-3.5" />
<span className="text-[10px]">{t('Tentative')}</span>
</span>
)}
{status === 'declined' && (
<span className="flex items-center gap-1 text-muted-foreground" title={t('Declined')}>
<XCircle className="size-3.5" />
<span className="text-[10px]">{t('Declined')}</span>
</span>
)}
{status == null && (
<span className="text-[10px] italic text-muted-foreground">
{t('No response')}
</span>
)}
</span>
</button>
</li>
))}
</ul>
</div>
)}
</div> </div>
) )
} }

25
src/hooks/useFetchCalendarRsvps.tsx

@ -15,6 +15,14 @@ function getRsvpStatus(rsvp: Event): 'accepted' | 'tentative' | 'declined' | und
return undefined 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) { export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
const { relayList } = useNostr() const { relayList } = useNostr()
const [rsvps, setRsvps] = useState<Event[]>([]) const [rsvps, setRsvps] = useState<Event[]>([])
@ -57,6 +65,23 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
} }
}, [calendarEvent?.id, calendarEvent?.kind, relayList?.read]) }, [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<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 { return {
rsvps, rsvps,
isFetching, isFetching,

3
src/i18n/locales/de.ts

@ -103,6 +103,9 @@ export default {
'You need to log in to RSVP': 'Zum Antworten bitte anmelden', 'You need to log in to RSVP': 'Zum Antworten bitte anmelden',
'RSVP updated': 'Rückmeldung gesendet', 'RSVP updated': 'Rückmeldung gesendet',
'Failed to update RSVP': 'Rückmeldung konnte nicht gesendet werden', 'Failed to update RSVP': 'Rückmeldung konnte nicht gesendet werden',
Organizer: 'Veranstalter',
Attendees: 'Teilnehmer',
'No response': 'Keine Rückmeldung',
'Calendar Events': 'Kalendertermine', 'Calendar Events': 'Kalendertermine',
'Calendar Event': 'Kalendertermin', 'Calendar Event': 'Kalendertermin',
'Schedule in-person meeting': 'Präsenztermin planen', 'Schedule in-person meeting': 'Präsenztermin planen',

3
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', 'You need to log in to RSVP': 'You need to log in to RSVP',
'RSVP updated': 'RSVP updated', 'RSVP updated': 'RSVP updated',
'Failed to update RSVP': 'Failed to update RSVP', 'Failed to update RSVP': 'Failed to update RSVP',
Organizer: 'Organizer',
Attendees: 'Attendees',
'No response': 'No response',
'Calendar Events': 'Calendar Events', 'Calendar Events': 'Calendar Events',
'Calendar Event': 'Calendar Event', 'Calendar Event': 'Calendar Event',
'Schedule in-person meeting': 'Schedule in-person meeting', 'Schedule in-person meeting': 'Schedule in-person meeting',

Loading…
Cancel
Save