diff --git a/src/components/CalendarEventContent/index.tsx b/src/components/CalendarEventContent/index.tsx
index af8319d4..2e1ed58d 100644
--- a/src/components/CalendarEventContent/index.tsx
+++ b/src/components/CalendarEventContent/index.tsx
@@ -51,7 +51,7 @@ export default function CalendarEventContent({
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { pubkey: myPubkey, publish } = useNostr()
- const { rsvps, isFetching, getRsvpStatus: getStatus } = useFetchCalendarRsvps(event)
+ const { rsvps, isFetching, getRsvpStatus: getStatus, applyRsvp } = useFetchCalendarRsvps(event)
const meta = useMemo(() => {
if (!isCalendarEventKind(event.kind)) return null
@@ -88,7 +88,8 @@ export default function CalendarEventContent({
return [...httpRs, ...(meta.image?.trim() ? [meta.image.trim()] : [])]
}, [meta, event])
- const myRsvp = myPubkey ? rsvps.find((r) => r.pubkey === myPubkey) : undefined
+ const myPk = myPubkey?.toLowerCase()
+ const myRsvp = myPk ? rsvps.find((r) => r.pubkey.toLowerCase() === myPk) : 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.
@@ -108,12 +109,13 @@ export default function CalendarEventContent({
new Set([organizerPubkey, ...participantPubkeys, ...rsvps.map((r) => r.pubkey)])
)
return allPubkeys.map((pubkey) => {
- const rsvp = rsvps.find((r) => r.pubkey === pubkey)
+ const pk = pubkey.toLowerCase()
+ const rsvp = rsvps.find((r) => r.pubkey.toLowerCase() === pk)
return {
pubkey,
role: roleByPubkey.get(pubkey),
status: (rsvp ? getStatus(rsvp) : null) as RsvpStatus | null,
- isOrganizer: pubkey === organizerPubkey
+ isOrganizer: pk === organizerPubkey.toLowerCase()
}
})
}, [event.pubkey, event.tags, rsvps])
@@ -142,7 +144,8 @@ export default function CalendarEventContent({
}
try {
const draft = createCalendarRsvpDraftEvent(event, status)
- await publish(draft)
+ const signed = await publish(draft)
+ applyRsvp(signed)
toast.success(t('RSVP updated'))
} catch (err) {
toast.error(err instanceof Error ? err.message : t('Failed to update RSVP'))
diff --git a/src/components/OthersRelayList/index.tsx b/src/components/OthersRelayList/index.tsx
index 2a2a4d2a..3821c6ee 100644
--- a/src/components/OthersRelayList/index.tsx
+++ b/src/components/OthersRelayList/index.tsx
@@ -1,5 +1,7 @@
import { useSmartRelayNavigation } from '@/PageManager'
+import RelaySettingsKindNotice from '@/components/RelaySettingsKindNotice'
import { Badge } from '@/components/ui/badge'
+import { kinds } from 'nostr-tools'
import { useFetchRelayInfo, useFetchRelayList } from '@/hooks'
import { toRelay } from '@/lib/link'
import { userIdToPubkey } from '@/lib/pubkey'
@@ -19,6 +21,7 @@ export default function OthersRelayList({ userId }: { userId: string }) {
return (
+
{showingRelayListFallback && (
+ {t('relaySettingsEventKindsSession')}
+
+ )
+ }
+
+ if (kinds.length === 0) return null
+
+ const kindsLabel = formatKindList(kinds)
+ const body =
+ variant === 'view'
+ ? t('relaySettingsEventKindsView', { kinds: kindsLabel })
+ : kinds.length === 1
+ ? t('relaySettingsEventKindsEditOne', { kind: kindsLabel })
+ : t('relaySettingsEventKindsEditMany', { kinds: kindsLabel })
+
+ return (
+
+ {t('relaySettingsEventKindsLabel')}:{' '}
+ {kindsLabel}
+ {body}
+
+ )
+}
diff --git a/src/hooks/useFetchCalendarRsvps.tsx b/src/hooks/useFetchCalendarRsvps.tsx
index 5f72e5ef..6b56e455 100644
--- a/src/hooks/useFetchCalendarRsvps.tsx
+++ b/src/hooks/useFetchCalendarRsvps.tsx
@@ -1,25 +1,23 @@
import { ExtendedKind } from '@/constants'
+import { isCalendarEventKind } from '@/lib/calendar-event'
+import {
+ calendarEventHexId,
+ calendarRsvpMatchesCalendarEvent,
+ parseCalendarRsvpStatus
+} from '@/lib/calendar-rsvp-match'
import {
getReplaceableCoordinateFromEvent,
normalizeReplaceableCoordinateString
} from '@/lib/event'
-import { isCalendarEventKind } from '@/lib/calendar-event'
-import client from '@/services/client.service'
-import { queryService } from '@/services/client.service'
+import { relayHintsFromEventTags } from '@/lib/relay-list-builder'
+import client, { queryService } from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import { useNostr } from '@/providers/NostrProvider'
import { Event } from 'nostr-tools'
-import { useEffect, useState } from 'react'
+import { useCallback, useEffect, useState } from 'react'
import { normalizeAnyRelayUrl } from '@/lib/url'
import { FAST_READ_RELAY_URLS } from '@/constants'
import { userReadInboxUrls, userWriteOutboxUrls } 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)
@@ -38,11 +36,25 @@ function mergeRsvpList(events: Event[]): Event[] {
return acc
}
+function filterMatchingRsvps(calendarEvent: Event, events: Event[]): Event[] {
+ return events.filter((ev) => calendarRsvpMatchesCalendarEvent(calendarEvent, ev))
+}
+
export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
const { relayList, cacheRelayListEvent } = useNostr()
const [rsvps, setRsvps] = useState
([])
const [isFetching, setIsFetching] = useState(false)
+ const applyRsvp = useCallback(
+ (evt: Event) => {
+ if (!calendarEvent || !isCalendarEventKind(calendarEvent.kind)) return
+ if (!calendarRsvpMatchesCalendarEvent(calendarEvent, evt)) return
+ void indexedDb.putCalendarRsvpEventRow(evt).catch(() => undefined)
+ setRsvps((prev) => mergeRsvp(prev, evt))
+ },
+ [calendarEvent]
+ )
+
useEffect(() => {
if (!calendarEvent || !isCalendarEventKind(calendarEvent.kind)) {
setRsvps([])
@@ -52,34 +64,35 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
let cancelled = false
setIsFetching(true)
- const coordinate = normalizeReplaceableCoordinateString(
- getReplaceableCoordinateFromEvent(calendarEvent)
- )
const userRead = userReadInboxUrls(relayList, cacheRelayListEvent)
const userWrite = userWriteOutboxUrls(relayList, cacheRelayListEvent)
void (async () => {
- const fromSession = client.getSessionCalendarRsvpsForCalendarEvent(calendarEvent)
+ const fromSession = filterMatchingRsvps(
+ calendarEvent,
+ client.getSessionCalendarRsvpsForCalendarEvent(calendarEvent)
+ )
setRsvps(mergeRsvpList(fromSession))
const idbP = indexedDb
- .getCalendarRsvpEventsByParentCoordinate(coordinate)
+ .getCalendarRsvpEventsForCalendarEvent(calendarEvent)
.catch((): Event[] => [])
void idbP.then((rows) => {
if (cancelled) return
- setRsvps(mergeRsvpList([...rows, ...fromSession]))
+ setRsvps(mergeRsvpList(filterMatchingRsvps(calendarEvent, [...rows, ...fromSession])))
})
const baseUrls = new Set([
...FAST_READ_RELAY_URLS.map((url) => normalizeAnyRelayUrl(url) || url),
...userRead.map((url) => normalizeAnyRelayUrl(url) || url),
- ...userWrite.map((url) => normalizeAnyRelayUrl(url) || url)
+ ...userWrite.map((url) => normalizeAnyRelayUrl(url) || url),
+ ...relayHintsFromEventTags(calendarEvent).map((url) => normalizeAnyRelayUrl(url) || url),
+ ...client.getSeenEventRelayUrls(calendarEvent.id).map((url) => normalizeAnyRelayUrl(url) || url)
].filter(Boolean) as string[])
const organizerPubkey = calendarEvent.pubkey
try {
- let relayUrls: string[]
try {
const organizerRelays = await client.fetchRelayList(organizerPubkey)
if (!cancelled) {
@@ -93,17 +106,17 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
if (u) baseUrls.add(u)
})
}
- relayUrls = Array.from(baseUrls)
} catch {
- relayUrls = Array.from(baseUrls)
+ // keep baseUrls
}
if (cancelled) return
- const urls = relayUrls?.length ? relayUrls : Array.from(baseUrls)
- const calendarHexId = /^[0-9a-f]{64}$/i.test(calendarEvent.id)
- ? calendarEvent.id.toLowerCase()
- : calendarEvent.id
+
+ const coordinate = normalizeReplaceableCoordinateString(
+ getReplaceableCoordinateFromEvent(calendarEvent)
+ )
+ const calendarHexId = calendarEventHexId(calendarEvent)
const events = await queryService.fetchEvents(
- urls,
+ Array.from(baseUrls),
[
{
kinds: [ExtendedKind.CALENDAR_EVENT_RSVP],
@@ -123,12 +136,16 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
}
)
if (cancelled) return
- const fromRelay = events ?? []
+ const fromRelay = filterMatchingRsvps(calendarEvent, events ?? [])
const fromIdb = await idbP
await Promise.allSettled(
fromRelay.map((ev) => indexedDb.putCalendarRsvpEventRow(ev).catch(() => undefined))
)
- setRsvps(mergeRsvpList([...fromIdb, ...fromSession, ...fromRelay]))
+ setRsvps(
+ mergeRsvpList(
+ filterMatchingRsvps(calendarEvent, [...fromIdb, ...fromSession, ...fromRelay])
+ )
+ )
} finally {
if (!cancelled) setIsFetching(false)
}
@@ -137,38 +154,23 @@ export function useFetchCalendarRsvps(calendarEvent: Event | undefined) {
return () => {
cancelled = true
}
- }, [calendarEvent?.id, calendarEvent?.kind, calendarEvent?.pubkey, relayList])
+ }, [calendarEvent, relayList, cacheRelayListEvent])
- // 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 = normalizeReplaceableCoordinateString(
- getReplaceableCoordinateFromEvent(calendarEvent)
- )
- const calId = /^[0-9a-f]{64}$/i.test(calendarEvent.id)
- ? calendarEvent.id.toLowerCase()
- : calendarEvent.id
const handler = (e: CustomEvent) => {
- const evt = e.detail
- if (evt.kind !== ExtendedKind.CALENDAR_EVENT_RSVP) return
- const aTag = evt.tags.find(tagNameEquals('a'))
- const aCoord = aTag?.[1] ? normalizeReplaceableCoordinateString(aTag[1]) : ''
- const eTag = evt.tags.find(tagNameEquals('e'))?.[1]?.trim().toLowerCase()
- const matchesA = aCoord !== '' && aCoord === coordinate
- const matchesE = eTag && /^[0-9a-f]{64}$/.test(eTag) && eTag === calId
- if (!matchesA && !matchesE) return
- void indexedDb.putCalendarRsvpEventRow(evt).catch(() => undefined)
- setRsvps((prev) => mergeRsvp(prev, evt))
+ applyRsvp(e.detail)
}
client.addEventListener('newEvent', handler as EventListener)
return () => client.removeEventListener('newEvent', handler as EventListener)
- }, [calendarEvent?.id, calendarEvent?.kind, calendarEvent?.pubkey])
+ }, [calendarEvent, applyRsvp])
return {
rsvps,
isFetching,
- getRsvpStatus
+ getRsvpStatus: parseCalendarRsvpStatus,
+ applyRsvp
}
}
diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts
index 92d2dac7..c7c4c770 100644
--- a/src/i18n/locales/de.ts
+++ b/src/i18n/locales/de.ts
@@ -842,6 +842,15 @@ export default {
'HTTP relays': 'HTTP-Relays',
httpRelaysDescription:
'HTTPS-Index-Relays (z. B. REST /api/events/filter). Gleiche Lese-/Schreib-/beides-Rollen wie Mailbox-Relays; gespeichert als Kind 10243. Liste leeren und speichern, um eine leere Liste zu veröffentlichen.',
+ relaySettingsEventKindsLabel: 'Nostr-Event-Kind',
+ relaySettingsEventKindsEditOne:
+ 'Speichern veröffentlicht hier ein ersetzbares Listen-Event dieses Kinds auf deinen Relays.',
+ relaySettingsEventKindsEditMany:
+ 'Speichern veröffentlicht hier ersetzbare Listen-Events dieser Kinds auf deinen Relays.',
+ relaySettingsEventKindsView:
+ 'Zeigt die veröffentlichte Relay-Liste dieses Nutzers aus Kind {{kinds}}, sofern der Client sie geladen hat.',
+ relaySettingsEventKindsSession:
+ 'Kein ersetzbares Relay-Listen-Event — dieser Tab zeigt nur Session-Bewertung und Strafen im Speicher (nichts wird veröffentlicht).',
'HTTP relays saved': 'HTTP-Relays gespeichert',
'Failed to save HTTP relay list': 'HTTP-Relay-Liste konnte nicht gespeichert werden',
'HTTP relays must start with https:// or http://':
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 31db8ea1..8a7792f4 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -862,6 +862,15 @@ export default {
'HTTP relays': 'HTTP relays',
httpRelaysDescription:
'HTTPS index relays (e.g. REST /api/events/filter). Same read/write/both roles as mailbox relays; stored as kind 10243. Clear the list and save to publish an empty list.',
+ relaySettingsEventKindsLabel: 'Nostr event kind',
+ relaySettingsEventKindsEditOne:
+ 'Saving here publishes a replaceable list event of this kind to your relays.',
+ relaySettingsEventKindsEditMany:
+ 'Saving here publishes replaceable list events of these kinds to your relays.',
+ relaySettingsEventKindsView:
+ 'Shows this user’s published relay list from kind {{kinds}} when the client has fetched it.',
+ relaySettingsEventKindsSession:
+ 'No replaceable relay list event — this tab only shows in-memory session relay scoring and strikes (nothing is published).',
'HTTP relays saved': 'HTTP relays saved',
'Failed to save HTTP relay list': 'Failed to save HTTP relay list',
'HTTP relays must start with https:// or http://':
diff --git a/src/lib/calendar-rsvp-match.test.ts b/src/lib/calendar-rsvp-match.test.ts
new file mode 100644
index 00000000..5fafa455
--- /dev/null
+++ b/src/lib/calendar-rsvp-match.test.ts
@@ -0,0 +1,105 @@
+import { describe, expect, it } from 'vitest'
+import { ExtendedKind } from '@/constants'
+import {
+ calendarEventHexId,
+ calendarRsvpMatchesCalendarEvent,
+ calendarRsvpParentKeyFromEventId,
+ parseCalendarRsvpStatus
+} from '@/lib/calendar-rsvp-match'
+import type { Event } from 'nostr-tools'
+
+const ORG = 'b'.repeat(63) + 'c'
+const D = 'purple-prague'
+const CAL_ID = 'a'.repeat(64)
+
+function calendarEvent(overrides: Partial = {}): Event {
+ return {
+ id: CAL_ID,
+ pubkey: ORG,
+ created_at: 1_700_000_000,
+ kind: ExtendedKind.CALENDAR_EVENT_TIME,
+ tags: [['d', D]],
+ content: '',
+ sig: 'sig',
+ ...overrides
+ }
+}
+
+function rsvp(tags: string[][], pubkey = 'c'.repeat(63) + 'd'): Event {
+ return {
+ id: 'f'.repeat(64),
+ pubkey,
+ created_at: 1_700_000_100,
+ kind: ExtendedKind.CALENDAR_EVENT_RSVP,
+ tags,
+ content: '',
+ sig: 'sig'
+ }
+}
+
+describe('calendarRsvpMatchesCalendarEvent', () => {
+ const cal = calendarEvent()
+ const coord = `${ExtendedKind.CALENDAR_EVENT_TIME}:${ORG}:${D}`
+
+ it('matches via normalized a tag', () => {
+ expect(
+ calendarRsvpMatchesCalendarEvent(
+ cal,
+ rsvp([
+ ['a', coord],
+ ['status', 'accepted']
+ ])
+ )
+ ).toBe(true)
+ })
+
+ it('matches via e tag when a is absent', () => {
+ expect(
+ calendarRsvpMatchesCalendarEvent(
+ cal,
+ rsvp([
+ ['e', CAL_ID.toUpperCase()],
+ ['status', 'tentative']
+ ])
+ )
+ ).toBe(true)
+ })
+
+ it('rejects wrong coordinate and wrong event id', () => {
+ expect(
+ calendarRsvpMatchesCalendarEvent(
+ cal,
+ rsvp([
+ ['a', `${ExtendedKind.CALENDAR_EVENT_TIME}:${ORG}:other`],
+ ['e', 'b'.repeat(64)]
+ ])
+ )
+ ).toBe(false)
+ })
+})
+
+describe('parseCalendarRsvpStatus', () => {
+ it('parses accepted, tentative, declined', () => {
+ expect(parseCalendarRsvpStatus(rsvp([['status', 'Accepted']]))).toBe('accepted')
+ expect(parseCalendarRsvpStatus(rsvp([['status', 'TENTATIVE']]))).toBe('tentative')
+ expect(parseCalendarRsvpStatus(rsvp([['status', 'declined']]))).toBe('declined')
+ })
+
+ it('returns undefined for missing or invalid status', () => {
+ expect(parseCalendarRsvpStatus(rsvp([]))).toBeUndefined()
+ expect(parseCalendarRsvpStatus(rsvp([['status', 'maybe']]))).toBeUndefined()
+ })
+})
+
+describe('calendarEventHexId', () => {
+ it('lowercases 64-char hex ids', () => {
+ expect(calendarEventHexId({ ...calendarEvent(), id: CAL_ID.toUpperCase() })).toBe(CAL_ID)
+ })
+})
+
+describe('calendarRsvpParentKeyFromEventId', () => {
+ it('builds e: prefix key', () => {
+ expect(calendarRsvpParentKeyFromEventId(CAL_ID)).toBe(`e:${CAL_ID}`)
+ expect(calendarRsvpParentKeyFromEventId('not-hex')).toBe('')
+ })
+})
diff --git a/src/lib/calendar-rsvp-match.ts b/src/lib/calendar-rsvp-match.ts
new file mode 100644
index 00000000..35a6f35b
--- /dev/null
+++ b/src/lib/calendar-rsvp-match.ts
@@ -0,0 +1,38 @@
+import { ExtendedKind } from '@/constants'
+import {
+ getReplaceableCoordinateFromEvent,
+ normalizeReplaceableCoordinateString
+} from '@/lib/event'
+import { tagNameEquals } from '@/lib/tag'
+import { Event } from 'nostr-tools'
+
+export type CalendarRsvpStatus = 'accepted' | 'tentative' | 'declined'
+
+export function calendarEventHexId(event: Event): string {
+ return /^[0-9a-f]{64}$/i.test(event.id) ? event.id.toLowerCase() : event.id
+}
+
+/** Whether kind 31925 references this calendar note (31922 / 31923) via `a` and/or `e`. */
+export function calendarRsvpMatchesCalendarEvent(calendarEvent: Event, rsvp: Event): boolean {
+ if (rsvp.kind !== ExtendedKind.CALENDAR_EVENT_RSVP) return false
+ const coordNorm = normalizeReplaceableCoordinateString(
+ getReplaceableCoordinateFromEvent(calendarEvent)
+ )
+ const calId = calendarEventHexId(calendarEvent)
+ const rawA = rsvp.tags.find(tagNameEquals('a'))?.[1]?.trim()
+ if (rawA && normalizeReplaceableCoordinateString(rawA) === coordNorm) return true
+ const eTag = rsvp.tags.find(tagNameEquals('e'))?.[1]?.trim().toLowerCase()
+ return Boolean(eTag && /^[0-9a-f]{64}$/.test(eTag) && eTag === calId)
+}
+
+export function parseCalendarRsvpStatus(rsvp: Event): CalendarRsvpStatus | undefined {
+ const status = rsvp.tags.find(tagNameEquals('status'))?.[1]?.trim().toLowerCase()
+ if (status === 'accepted' || status === 'tentative' || status === 'declined') return status
+ return undefined
+}
+
+/** IndexedDB parent key for RSVPs that only tag the calendar event id (`e`). */
+export function calendarRsvpParentKeyFromEventId(hexId: string): string {
+ const id = hexId.trim().toLowerCase()
+ return /^[0-9a-f]{64}$/.test(id) ? `e:${id}` : ''
+}
diff --git a/src/pages/secondary/RelaySettingsPage/index.tsx b/src/pages/secondary/RelaySettingsPage/index.tsx
index d0a125a9..7210190b 100644
--- a/src/pages/secondary/RelaySettingsPage/index.tsx
+++ b/src/pages/secondary/RelaySettingsPage/index.tsx
@@ -3,6 +3,7 @@ import HttpRelaysSetting from '@/components/HttpRelaysSetting'
import JsonViewDialog from '@/components/JsonViewDialog'
import MailboxSetting from '@/components/MailboxSetting'
import FavoriteRelaysSetting from '@/components/FavoriteRelaysSetting'
+import RelaySettingsKindNotice from '@/components/RelaySettingsKindNotice'
import SessionRelaysTab from '@/components/SessionRelaysTab'
import { RefreshButton } from '@/components/RefreshButton'
import { Button } from '@/components/ui/button'
@@ -23,6 +24,12 @@ import { kinds } from 'nostr-tools'
import { forwardRef, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
+const FAVORITE_TAB_KINDS = [
+ ExtendedKind.FAVORITE_RELAYS,
+ ExtendedKind.BLOCKED_RELAYS,
+ kinds.Relaysets
+] as const
+
const RelaySettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => {
const { t } = useTranslation()
const { registerPrimaryPanelRefresh } = usePrimaryNoteView()
@@ -128,19 +135,24 @@ const RelaySettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?:
{t('Cache Relays')}
{t('Session relays')}
-
+
+
-
+
+
-
+
+
-
+
+
-
+
+
diff --git a/src/services/client-events.service.ts b/src/services/client-events.service.ts
index 36626889..18b0282d 100644
--- a/src/services/client-events.service.ts
+++ b/src/services/client-events.service.ts
@@ -47,6 +47,7 @@ import {
} from './event-archive.service'
import { getDefaultSessionLruMaxSync } from '@/lib/event-archive-config'
import { isCalendarEventKind } from '@/lib/calendar-event'
+import { calendarRsvpMatchesCalendarEvent } from '@/lib/calendar-rsvp-match'
import { citationPickerMatchesQuery } from '@/lib/citation-picker-search'
import { profileKind0MatchesSearchQuery } from '@/lib/profile-metadata-search'
import { shouldDropEventOnIngest, type ShouldDropEventOnIngestOptions } from '@/lib/event-ingest-filter'
@@ -1045,25 +1046,10 @@ export class EventService {
*/
getSessionCalendarRsvpsForCalendarEvent(calendarEvent: NEvent): NEvent[] {
if (!isCalendarEventKind(calendarEvent.kind)) return []
- const coordNorm = normalizeReplaceableCoordinateString(
- getReplaceableCoordinateFromEvent(calendarEvent)
- )
- const calId = /^[0-9a-f]{64}$/i.test(calendarEvent.id)
- ? calendarEvent.id.toLowerCase()
- : calendarEvent.id
const out: NEvent[] = []
for (const [, event] of this.sessionEventCache.entries()) {
- if (event.kind !== ExtendedKind.CALENDAR_EVENT_RSVP) continue
if (shouldDropEventOnIngest(event)) continue
- const rawA = event.tags.find(tagNameEquals('a'))?.[1]?.trim()
- if (rawA && normalizeReplaceableCoordinateString(rawA) === coordNorm) {
- out.push(event)
- continue
- }
- const eTag = event.tags.find(tagNameEquals('e'))?.[1]?.trim().toLowerCase()
- if (eTag && /^[0-9a-f]{64}$/.test(eTag) && eTag === calId) {
- out.push(event)
- }
+ if (calendarRsvpMatchesCalendarEvent(calendarEvent, event)) out.push(event)
}
return out.sort((a, b) => b.created_at - a.created_at)
}
diff --git a/src/services/indexed-db.service.ts b/src/services/indexed-db.service.ts
index 8e0117da..bfb354fd 100644
--- a/src/services/indexed-db.service.ts
+++ b/src/services/indexed-db.service.ts
@@ -15,6 +15,7 @@ import {
getCalendarOccurrenceWindowMs,
isCalendarEventKind
} from '@/lib/calendar-event'
+import { calendarEventHexId, calendarRsvpParentKeyFromEventId } from '@/lib/calendar-rsvp-match'
import {
getReplaceableCoordinate,
getReplaceableCoordinateFromEvent,
@@ -3784,12 +3785,20 @@ class IndexedDbService {
})
}
- /** Persist a NIP-52 RSVP (31925). Indexed by normalized `a` parent coordinate. */
+ /** Persist a NIP-52 RSVP (31925). Indexed by normalized `a` coordinate or `e:`. */
async putCalendarRsvpEventRow(ev: Event): Promise {
if (ev.kind !== ExtendedKind.CALENDAR_EVENT_RSVP) return
const rawA = ev.tags.find(tagNameEquals('a'))?.[1]?.trim()
- if (!rawA) return
- const parentCoordinate = normalizeReplaceableCoordinateString(rawA)
+ const rawE = ev.tags.find(tagNameEquals('e'))?.[1]?.trim()
+ const eHex =
+ rawE && /^[0-9a-f]{64}$/i.test(rawE) ? rawE.toLowerCase() : ''
+ let parentCoordinate = ''
+ if (rawA) {
+ parentCoordinate = normalizeReplaceableCoordinateString(rawA)
+ } else if (eHex) {
+ parentCoordinate = `e:${eHex}`
+ }
+ if (!parentCoordinate) return
await this.initPromise
if (!this.db?.objectStoreNames.contains(StoreNames.CALENDAR_RSVP_EVENTS)) return
@@ -3860,7 +3869,29 @@ class IndexedDbService {
})
}
- /** Cached RSVPs for a calendar replaceable coordinate (`kind:pubkey:d`). */
+ /** RSVPs for a calendar note: `a` coordinate index plus `e:` rows. */
+ async getCalendarRsvpEventsForCalendarEvent(calendarEvent: Event, limit = 400): Promise {
+ const coord = normalizeReplaceableCoordinateString(
+ getReplaceableCoordinateFromEvent(calendarEvent)
+ )
+ const eKey = calendarRsvpParentKeyFromEventId(calendarEventHexId(calendarEvent))
+ const [byCoord, byE] = await Promise.all([
+ this.getCalendarRsvpEventsByParentCoordinate(coord, limit),
+ eKey ? this.getCalendarRsvpEventsByParentCoordinate(eKey, limit) : Promise.resolve([])
+ ])
+ const seen = new Set()
+ const out: Event[] = []
+ for (const ev of [...byCoord, ...byE]) {
+ const id = ev.id.toLowerCase()
+ if (seen.has(id)) continue
+ seen.add(id)
+ out.push(ev)
+ }
+ out.sort((a, b) => b.created_at - a.created_at)
+ return out.slice(0, limit)
+ }
+
+ /** Cached RSVPs for a calendar replaceable coordinate (`kind:pubkey:d`) or `e:`. */
async getCalendarRsvpEventsByParentCoordinate(
parentCoordinate: string,
limit = 400