From e7c7fac112829870979b2ee082a3e624bffffe85 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Fri, 25 Sep 2026 15:59:54 -0700 Subject: [PATCH] Publish and read RSVPs through the CalendarRsvp domain kind, and read calendar times and titles through TimeEvent --- src/app/calendar.ts | 60 +++++++++++--------------- src/app/components/CalendarRsvp.svelte | 29 ++++++------- src/app/feeds.ts | 17 +++++--- src/app/reminders.ts | 25 ++++++----- src/app/title.ts | 3 +- 5 files changed, 62 insertions(+), 72 deletions(-) diff --git a/src/app/calendar.ts b/src/app/calendar.ts index 979a720d..eee59e5c 100644 --- a/src/app/calendar.ts +++ b/src/app/calendar.ts @@ -6,15 +6,17 @@ import { pushToMapKey, secondsToDate, sortBy, + spec, uniqBy, } from "@welshman/lib" -import {EVENT_RSVP, getAddress, makeEvent, tagSpec, tagValue} from "@welshman/util" +import {EVENT_RSVP, getAddress, tagSpec, tagValue} from "@welshman/util" import type {TrustedEvent} from "@welshman/util" -import {TimeEvent} from "@welshman/domain" +import {CalendarRsvp, TimeEvent} from "@welshman/domain" +import type {RsvpStatus} from "@welshman/domain" import {synced} from "@welshman/store" -import {deletes, reader, relays, thunks} from "@app/core" +import {deletes, reader, relays, thunks, writer} from "@app/core" import {deriveEvents} from "@app/repository" -import {PROTECTED} from "@app/rooms" +import {toSpace} from "@app/rooms" import {kv} from "@app/storage" // Views @@ -230,12 +232,6 @@ export const layoutMultiDayBars = ( // RSVPs (NIP-52) -export enum RsvpStatus { - Accepted = "accepted", - Declined = "declined", - Tentative = "tentative", -} - export const makeRsvpFilter = (event: TrustedEvent) => ({ kinds: [EVENT_RSVP], "#a": [getAddress(event)], @@ -243,10 +239,7 @@ export const makeRsvpFilter = (event: TrustedEvent) => ({ export const deriveRsvps = (event: TrustedEvent) => deriveEvents([makeRsvpFilter(event)]) -export const getRsvpStatus = (rsvp: TrustedEvent) => tagValue(tagSpec("status"), rsvp.tags) - -export const isAttending = (status?: string) => - status === RsvpStatus.Accepted || status === RsvpStatus.Tentative +export const isAttending = (status?: RsvpStatus) => status === "accepted" || status === "tentative" // A relay can still hold a replaced RSVP, so the newest per person is the one that counts. export const getRsvpsByStatus = (rsvps: TrustedEvent[]) => { @@ -256,37 +249,32 @@ export const getRsvpsByStatus = (rsvps: TrustedEvent[]) => { ) const byStatus = (status: RsvpStatus) => - latest.filter(rsvp => getRsvpStatus(rsvp) === status).map(rsvp => rsvp.pubkey) + latest.filter(rsvp => reader(CalendarRsvp)(rsvp).status() === status).map(rsvp => rsvp.pubkey) return { latest, - accepted: byStatus(RsvpStatus.Accepted), - tentative: byStatus(RsvpStatus.Tentative), - declined: byStatus(RsvpStatus.Declined), + accepted: byStatus("accepted"), + tentative: byStatus("tentative"), + declined: byStatus("declined"), } } // An RSVP is addressable, so identifying it by its target makes a new one supersede the old. export const publishRsvp = async (url: string, event: TrustedEvent, status: RsvpStatus) => { - const address = getAddress(event) - const h = tagValue(tagSpec("h"), event.tags) - const tags = [ - ["a", address, url], - ["e", event.id, url], - ["d", address], - ["status", status], - ["p", event.pubkey], - ] + const eventWriter = await toSpace( + writer(CalendarRsvp) + .setIdentifier(getAddress(event)) + .setCalendarEventFromEvent(event) + .setStatus(status) + // The writer hints the organizer's outbox, but a space's event lives on the space relay. + .dropTags(spec(["a"])) + .dropTags(spec(["e"])) + .addTags(["a", getAddress(event), url], ["e", event.id, url]), + url, + reader(TimeEvent)(event).room(), + ) - if (await relays.get().hasNip(url, 70)) { - tags.push(PROTECTED) - } - - if (h) { - tags.push(["h", h]) - } - - return thunks.get().publish({relays: [url], event: makeEvent(EVENT_RSVP, {tags})}) + return thunks.get().publish({relays: [url], event: await eventWriter.renderTemplate()}) } export const retractRsvp = async (url: string, rsvp: TrustedEvent) => { diff --git a/src/app/components/CalendarRsvp.svelte b/src/app/components/CalendarRsvp.svelte index fed35ecc..f4a4ce4d 100644 --- a/src/app/components/CalendarRsvp.svelte +++ b/src/app/components/CalendarRsvp.svelte @@ -1,6 +1,8 @@ @@ -71,15 +66,15 @@
Are you going?
- - - diff --git a/src/app/feeds.ts b/src/app/feeds.ts index eeb09e07..87d070ac 100644 --- a/src/app/feeds.ts +++ b/src/app/feeds.ts @@ -15,10 +15,9 @@ import { hexTags, isReplaceableKind, matchFilters, - tagSpec, - tagValue, tagValues, } from "@welshman/util" +import {TimeEvent} from "@welshman/domain" import type {Maybe} from "@welshman/lib" import type {Filter, TrustedEvent} from "@welshman/util" import {mergeRepositoryUpdates} from "@welshman/net" @@ -27,7 +26,7 @@ import {createScroller} from "@lib/html" import type {ScrollerOpts} from "@lib/html" import {daysBetween} from "@lib/util" import {EVENT_CONTEXT_KINDS, REACTION_KINDS} from "@app/content" -import {app, network} from "@app/core" +import {app, network, reader} from "@app/core" import {getEventsForUrl} from "@app/repository" const noEvents: TrustedEvent[] = [] @@ -682,13 +681,17 @@ export const makeCalendarFeed = ({ let oldest = now() let newest = now() - const getStart = (event: TrustedEvent) => parseInt(tagValue(tagSpec("start"), event.tags) || "") - - const getEnd = (event: TrustedEvent) => parseInt(tagValue(tagSpec("end"), event.tags) || "") + const getStart = (event: TrustedEvent) => reader(TimeEvent)(event).start() ?? 0 const compareByStart = (a: TrustedEvent, b: TrustedEvent) => getStart(a) - getStart(b) || compareEventsAsc(a, b) + const isValid = (event: TrustedEvent) => { + const timeEvent = reader(TimeEvent)(event) + + return timeEvent.start() && timeEvent.end() && !seen.has(event.id) + } + const events = writable( uniqBy( e => e.id, @@ -697,7 +700,7 @@ export const makeCalendarFeed = ({ ) const insertEvents = (newEvents: TrustedEvent[]) => { - const valid = newEvents.filter(e => !isNaN(getStart(e)) && !isNaN(getEnd(e)) && !seen.has(e.id)) + const valid = newEvents.filter(isValid) if (valid.length === 0) { return diff --git a/src/app/reminders.ts b/src/app/reminders.ts index 4d67f1ed..2e5397ed 100644 --- a/src/app/reminders.ts +++ b/src/app/reminders.ts @@ -1,9 +1,10 @@ -import {MINUTE, formatTimestampRelative, int, ms, now, uniq} from "@welshman/lib" -import {EVENT_RSVP, addressTags, getIdFilters, isSignedEvent, matchTag} from "@welshman/util" +import {MINUTE, first, formatTimestampRelative, int, ms, now, uniq} from "@welshman/lib" +import {EVENT_RSVP, getIdFilters, isSignedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util" +import {CalendarRsvp} from "@welshman/domain" import {buildUrl} from "@lib/util" -import {getEventStart, getEventTitle, getRsvpStatus, isAttending} from "@app/calendar" -import {app, network, roomLists} from "@app/core" +import {getEventStart, getEventTitle, isAttending} from "@app/calendar" +import {app, network, reader, roomLists} from "@app/core" import {PUSH_SERVER} from "@app/env" import {pushState} from "@app/push/adapters/common" import {notificationSettings} from "@app/settings" @@ -70,14 +71,16 @@ const loadAttendedEvents = async (pubkey: string) => { await $network.request({signal, filters, relays, autoClose: true}) - const attended = app - .get() - .repository.query(filters) - .flatMap(rsvp => { - const tag = matchTag(addressTags("a"), rsvp.tags) + const {repository, tracker} = app.get() - return tag?.[2] && isAttending(getRsvpStatus(rsvp)) ? [{address: tag[1], relay: tag[2]}] : [] - }) + // An RSVP only goes to the space holding its event, so where it was seen is where the event is. + const attended = repository.query(filters).flatMap(rsvp => { + const rsvpReader = reader(CalendarRsvp)(rsvp) + const address = rsvpReader.calendarEvent() + const relay = first(Array.from(tracker.getRelays(rsvp.id))) + + return address && relay && isAttending(rsvpReader.status()) ? [{address, relay}] : [] + }) await $network.request({ signal, diff --git a/src/app/title.ts b/src/app/title.ts index 84500fba..df213df8 100644 --- a/src/app/title.ts +++ b/src/app/title.ts @@ -3,6 +3,7 @@ import {tagSpec, tagValue} from "@welshman/util" import {displayPubkey} from "@welshman/domain" import {makeRoomKey} from "@welshman/app" import {decodePubkey} from "@lib/util" +import {getEventTitle} from "@app/calendar" import {app, profiles, rooms} from "@app/core" import {PLATFORM_NAME} from "@app/env" import {decodeRelay} from "@app/relays" @@ -137,7 +138,7 @@ export const getPageTitle = ({page, pubkey}: PageTitleContext) => { } if (routeId === "/spaces/[relay]/calendar/[address]") { - return makeTitle(tagValue(tagSpec("title"), event?.tags || []) || "Event") + return makeTitle((event && getEventTitle(event)) || "Event") } if (routeId === "/spaces/[relay]/classifieds/[address]") {