diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 751e146..331c5d4 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -101,9 +101,6 @@ const messages: LocaleMessages = { }, detail: { getTicket: 'Get Ticket', - going: 'Going', - maybe: 'Maybe', - notGoing: 'Not Going', contactOrganizer: 'Contact Organizer', organizer: 'Organizer', location: 'Location', diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index edc5d82..6b42a69 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -101,9 +101,6 @@ const messages: LocaleMessages = { }, detail: { getTicket: 'Obtener boleto', - going: 'Voy', - maybe: 'Tal vez', - notGoing: 'No voy', contactOrganizer: 'Contactar organizador', organizer: 'Organizador', location: 'Ubicación', diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index f42cc4b..ff76d79 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -101,9 +101,6 @@ const messages: LocaleMessages = { }, detail: { getTicket: 'Obtenir un billet', - going: 'Présent', - maybe: 'Peut-être', - notGoing: 'Absent', contactOrganizer: "Contacter l'organisateur", organizer: 'Organisateur', location: 'Lieu', diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 3aebdf0..63e8fc3 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -76,9 +76,6 @@ export interface LocaleMessages { categories: Record detail: { getTicket: string - going: string - maybe: string - notGoing: string contactOrganizer: string organizer: string location: string diff --git a/src/modules/events/components/RSVPButton.vue b/src/modules/events/components/RSVPButton.vue deleted file mode 100644 index 0cda9bf..0000000 --- a/src/modules/events/components/RSVPButton.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - diff --git a/src/modules/events/composables/useRSVP.ts b/src/modules/events/composables/useRSVP.ts deleted file mode 100644 index b6c31c5..0000000 --- a/src/modules/events/composables/useRSVP.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { ref, onMounted, onUnmounted } from 'vue' -import type { EventTemplate, Event as NostrEvent } from 'nostr-tools' -import { tryInjectService, SERVICE_TOKENS } from '@/core/di-container' -import { useAuth } from '@/composables/useAuthService' -import { signEventViaLnbits } from '@/lib/nostr/signing' -import { NIP52_KINDS, type RSVPStatus } from '../types/nip52' - -/** - * NIP-52 RSVP (kind 31925) for responding to calendar events. - * - * Each RSVP is an addressable event with: - * d-tag: unique identifier for this RSVP - * a-tag: reference to the calendar event (kind:pubkey:d-tag) - * status tag: 'accepted' | 'declined' | 'tentative' - */ - -interface RSVPEntry { - status: RSVPStatus - eventId: string - createdAt: number -} - -// Cache: eventCoord -> user's own (latest) RSVP entry -const rsvpCache = ref>(new Map()) -// Cache: eventCoord -> (pubkey -> latest RSVP entry from that pubkey). -// NIP-52 RSVPs (kind 31925) are replaceable per (kind, pubkey, d-tag), so a -// user's earlier RSVP for an event is superseded by their later one. The -// "going" count is derived from this map (count of pubkeys whose *latest* -// RSVP has status === 'accepted'), not by summing every accepted event seen -// — that would double-count replacements and never decrement on flip. -const rsvpStates = ref>>(new Map()) -const isLoaded = ref(false) - -// Coords with an in-flight publish — used to disable RSVP buttons so fast -// clicks don't race each other. -const pendingCoords = ref>(new Set()) - -// Last successfully-published `created_at` per coord. NIP-01 created_at is -// integer seconds, so two clicks in the same wall-clock second produce the -// same timestamp and most relays treat the second one as a duplicate / -// older replacement and silently drop it. We bump past the previous -// timestamp so each click is strictly newer. -const lastPublishAt = new Map() - -function upsertRSVPState(coord: string, pubkey: string, entry: RSVPEntry) { - let inner = rsvpStates.value.get(coord) - if (!inner) { - inner = new Map() - } - const existing = inner.get(pubkey) - if (existing && existing.createdAt >= entry.createdAt) return - inner.set(pubkey, entry) - // Re-set on the outer map so the ref's reactive proxy notifies dependents - // (Vue 3's deep reevent doesn't reach into nested Map values). - rsvpStates.value.set(coord, inner) -} - -export function useRSVP() { - const { isAuthenticated, currentUser } = useAuth() - let unsubscribe: (() => void) | null = null - - /** - * Get the user's RSVP status for an event. - */ - function getMyRSVP(eventKind: number, pubkey: string, dTag: string): RSVPStatus | null { - const coord = `${eventKind}:${pubkey}:${dTag}` - return rsvpCache.value.get(coord)?.status ?? null - } - - /** - * RSVP count for an event = number of pubkeys whose latest RSVP for - * this event has status 'accepted'. - */ - function getRSVPCount(eventKind: number, pubkey: string, dTag: string): number { - const coord = `${eventKind}:${pubkey}:${dTag}` - const inner = rsvpStates.value.get(coord) - if (!inner) return 0 - let count = 0 - for (const entry of inner.values()) { - if (entry.status === 'accepted') count++ - } - return count - } - - /** - * Load the user's RSVPs and counts for visible events from relays. - */ - function loadRSVPs() { - const relayHub = tryInjectService(SERVICE_TOKENS.RELAY_HUB) - if (!relayHub) return - - // Subscribe to all RSVPs (for counts) and filter user's own - unsubscribe = relayHub.subscribe({ - id: `rsvps-${Date.now()}`, - filters: [{ - kinds: [NIP52_KINDS.RSVP], - limit: 500, - }], - onEvent: (event: NostrEvent) => { - const aTag = event.tags.find(t => t[0] === 'a')?.[1] - if (!aTag) return - - const statusTag = event.tags.find(t => t[0] === 'status')?.[1] as RSVPStatus | undefined - // Also check 'l' tag pattern used by some clients - const lStatus = event.tags.find(t => t[0] === 'l' && t[2] === 'status')?.[1] as RSVPStatus | undefined - const status = statusTag ?? lStatus - if (!status || !['accepted', 'declined', 'tentative'].includes(status)) return - - const entry: RSVPEntry = { - status, - eventId: event.id, - createdAt: event.created_at, - } - - // Per-pubkey latest-wins state — drives the count. - upsertRSVPState(aTag, event.pubkey, entry) - - // User's own RSVP cache (used by getMyRSVP). - if (currentUser.value?.pubkey && event.pubkey === currentUser.value.pubkey) { - const existing = rsvpCache.value.get(aTag) - if (!existing || event.created_at > existing.createdAt) { - rsvpCache.value.set(aTag, entry) - } - } - }, - onEose: () => { - isLoaded.value = true - }, - }) - } - - /** - * Whether a publish is currently in flight for the given event. Bind - * to the RSVP buttons' `:disabled` so users can't queue racing clicks. - */ - function isPending(eventKind: number, pubkey: string, dTag: string): boolean { - const coord = `${eventKind}:${pubkey}:${dTag}` - return pendingCoords.value.has(coord) - } - - /** - * Publish an RSVP for an event. - * Clicking the same status again removes the RSVP (publishes 'declined'). - * - * Returns the status that was published on success, or null if the publish - * was rejected, blocked, or threw — caller should toast accordingly. - */ - async function setRSVP( - eventKind: number, - eventPubkey: string, - eventDTag: string, - status: RSVPStatus - ): Promise { - if (!isAuthenticated.value || !currentUser.value?.pubkey) return null - - const coord = `${eventKind}:${eventPubkey}:${eventDTag}` - - // Throttle: refuse a second click while the first is still publishing. - if (pendingCoords.value.has(coord)) return null - - // Toggle: if already this status, decline instead. - const currentStatus = getMyRSVP(eventKind, eventPubkey, eventDTag) - const newStatus = currentStatus === status ? 'declined' : status - - const dTag = `rsvp-${eventDTag}` - - // Strictly-monotonic created_at per coord so two clicks in the same - // wall-clock second don't both stamp the same timestamp (relays would - // dedupe the second one as a non-newer replacement). - const now = Math.floor(Date.now() / 1000) - const previous = lastPublishAt.get(coord) ?? 0 - const createdAt = Math.max(now, previous + 1) - - const template: EventTemplate = { - kind: NIP52_KINDS.RSVP, - created_at: createdAt, - content: '', - tags: [ - ['d', dTag], - ['a', coord], - ['status', newStatus], - ['L', 'status'], - ['l', newStatus, 'status'], - ['p', eventPubkey], - ], - } - - let signedEvent: NostrEvent - try { - signedEvent = await signEventViaLnbits(template) - } catch (err) { - console.error('[useRSVP] signEventViaLnbits failed:', err) - return null - } - - const relayHub = tryInjectService(SERVICE_TOKENS.RELAY_HUB) - if (!relayHub) return null - - pendingCoords.value.add(coord) - try { - const result = await relayHub.publishEvent(signedEvent) - if (!result || result.success <= 0) { - // No relay accepted the event — leave caches untouched so the UI - // continues to reflect the last known-good state. - return null - } - - const entry: RSVPEntry = { - status: newStatus, - eventId: signedEvent.id, - createdAt: signedEvent.created_at, - } - // Update both the user-scoped cache and the all-users state so the - // count flips immediately rather than waiting for the relay to echo - // our own event back through the subscription. - rsvpCache.value.set(coord, entry) - if (currentUser.value.pubkey) { - upsertRSVPState(coord, currentUser.value.pubkey, entry) - } - lastPublishAt.set(coord, signedEvent.created_at) - return newStatus - } finally { - pendingCoords.value.delete(coord) - } - } - - onMounted(() => { - if (!isLoaded.value) { - loadRSVPs() - } - }) - - onUnmounted(() => { - if (unsubscribe) { - unsubscribe() - } - }) - - return { - getMyRSVP, - getRSVPCount, - setRSVP, - isPending, - isLoaded, - loadRSVPs, - } -} - diff --git a/src/modules/events/views/EventDetailPage.vue b/src/modules/events/views/EventDetailPage.vue index cf24c80..b67aa66 100644 --- a/src/modules/events/views/EventDetailPage.vue +++ b/src/modules/events/views/EventDetailPage.vue @@ -12,10 +12,8 @@ import { } from 'lucide-vue-next' import { useEventDetail } from '../composables/useEventDetail' import BookmarkButton from '../components/BookmarkButton.vue' -import RSVPButton from '../components/RSVPButton.vue' import OrganizerCard from '../components/OrganizerCard.vue' import PurchaseTicketDialog from '../components/PurchaseTicketDialog.vue' -import { NIP52_KINDS } from '../types/nip52' import { useAuth } from '@/composables/useAuthService' import { useEventsStore } from '../stores/events' import { useOwnedTickets } from '../composables/useOwnedTickets' @@ -285,19 +283,6 @@ function goToMyTickets() {

{{ event.description }}

- - - -