Browse Source

fix calendar rsvps

imwald
Silberengel 2 weeks ago
parent
commit
c85b63ef18
  1. 13
      src/components/CalendarEventContent/index.tsx
  2. 3
      src/components/OthersRelayList/index.tsx
  3. 60
      src/components/RelaySettingsKindNotice/index.tsx
  4. 98
      src/hooks/useFetchCalendarRsvps.tsx
  5. 9
      src/i18n/locales/de.ts
  6. 9
      src/i18n/locales/en.ts
  7. 105
      src/lib/calendar-rsvp-match.test.ts
  8. 38
      src/lib/calendar-rsvp-match.ts
  9. 22
      src/pages/secondary/RelaySettingsPage/index.tsx
  10. 18
      src/services/client-events.service.ts
  11. 39
      src/services/indexed-db.service.ts

13
src/components/CalendarEventContent/index.tsx

@ -51,7 +51,7 @@ export default function CalendarEventContent({ @@ -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({ @@ -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({ @@ -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({ @@ -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'))

3
src/components/OthersRelayList/index.tsx

@ -1,5 +1,7 @@ @@ -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 }) { @@ -19,6 +21,7 @@ export default function OthersRelayList({ userId }: { userId: string }) {
return (
<div className="space-y-4">
<RelaySettingsKindNotice kinds={[kinds.RelayList]} variant="view" />
{showingRelayListFallback && (
<p
className="rounded-md border border-amber-500/35 bg-amber-500/10 px-3 py-2 text-sm text-foreground"

60
src/components/RelaySettingsKindNotice/index.tsx

@ -0,0 +1,60 @@ @@ -0,0 +1,60 @@
import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
export type RelaySettingsKindNoticeVariant = 'edit' | 'view' | 'session'
type Props = {
kinds: readonly number[]
variant?: RelaySettingsKindNoticeVariant
className?: string
}
function formatKindList(kinds: readonly number[]): string {
return kinds.join(', ')
}
export default function RelaySettingsKindNotice({
kinds,
variant = 'edit',
className
}: Props) {
const { t } = useTranslation()
if (variant === 'session') {
return (
<p
className={cn(
'rounded-md border border-border/60 bg-muted/40 px-3 py-2 text-xs text-muted-foreground',
className
)}
role="note"
>
{t('relaySettingsEventKindsSession')}
</p>
)
}
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 (
<p
className={cn(
'rounded-md border border-border/60 bg-muted/40 px-3 py-2 text-xs text-muted-foreground',
className
)}
role="note"
>
<span className="font-medium text-foreground">{t('relaySettingsEventKindsLabel')}:</span>{' '}
<span className="font-mono tabular-nums">{kindsLabel}</span>
<span className="block mt-1">{body}</span>
</p>
)
}

98
src/hooks/useFetchCalendarRsvps.tsx

@ -1,25 +1,23 @@ @@ -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[] { @@ -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<Event[]>([])
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) { @@ -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<string>([
...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) { @@ -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) { @@ -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) { @@ -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<Event>) => {
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
}
}

9
src/i18n/locales/de.ts

@ -842,6 +842,15 @@ export default { @@ -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://':

9
src/i18n/locales/en.ts

@ -862,6 +862,15 @@ export default { @@ -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://':

105
src/lib/calendar-rsvp-match.test.ts

@ -0,0 +1,105 @@ @@ -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> = {}): 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('')
})
})

38
src/lib/calendar-rsvp-match.ts

@ -0,0 +1,38 @@ @@ -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}` : ''
}

22
src/pages/secondary/RelaySettingsPage/index.tsx

@ -3,6 +3,7 @@ import HttpRelaysSetting from '@/components/HttpRelaysSetting' @@ -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' @@ -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?: @@ -128,19 +135,24 @@ const RelaySettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?:
<TabsTrigger value="cache-relays" className="w-full sm:w-auto">{t('Cache Relays')}</TabsTrigger>
<TabsTrigger value="session-relays" className="w-full sm:w-auto">{t('Session relays')}</TabsTrigger>
</TabsList>
<TabsContent value="favorite-relays">
<TabsContent value="favorite-relays" className="space-y-4">
<RelaySettingsKindNotice kinds={FAVORITE_TAB_KINDS} />
<FavoriteRelaysSetting />
</TabsContent>
<TabsContent value="mailbox">
<TabsContent value="mailbox" className="space-y-4">
<RelaySettingsKindNotice kinds={[kinds.RelayList]} />
<MailboxSetting />
</TabsContent>
<TabsContent value="http-relays">
<TabsContent value="http-relays" className="space-y-4">
<RelaySettingsKindNotice kinds={[ExtendedKind.HTTP_RELAY_LIST]} />
<HttpRelaysSetting />
</TabsContent>
<TabsContent value="cache-relays">
<TabsContent value="cache-relays" className="space-y-4">
<RelaySettingsKindNotice kinds={[ExtendedKind.CACHE_RELAYS]} />
<CacheRelaysSetting />
</TabsContent>
<TabsContent value="session-relays">
<TabsContent value="session-relays" className="space-y-4">
<RelaySettingsKindNotice kinds={[]} variant="session" />
<SessionRelaysTab />
</TabsContent>
</Tabs>

18
src/services/client-events.service.ts

@ -47,6 +47,7 @@ import { @@ -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 { @@ -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)
}

39
src/services/indexed-db.service.ts

@ -15,6 +15,7 @@ import { @@ -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 { @@ -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:<calendar-id>`. */
async putCalendarRsvpEventRow(ev: Event): Promise<void> {
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 { @@ -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:<event-id>` rows. */
async getCalendarRsvpEventsForCalendarEvent(calendarEvent: Event, limit = 400): Promise<Event[]> {
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<string>()
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:<hex>`. */
async getCalendarRsvpEventsByParentCoordinate(
parentCoordinate: string,
limit = 400

Loading…
Cancel
Save