diff --git a/src/modules/events/components/BookmarkButton.vue b/src/modules/events/components/BookmarkButton.vue index a99d9c2..fc01311 100644 --- a/src/modules/events/components/BookmarkButton.vue +++ b/src/modules/events/components/BookmarkButton.vue @@ -1,12 +1,11 @@ diff --git a/src/modules/events/composables/useBookmarks.ts b/src/modules/events/composables/useBookmarks.ts index 57281a9..8af2f36 100644 --- a/src/modules/events/composables/useBookmarks.ts +++ b/src/modules/events/composables/useBookmarks.ts @@ -88,20 +88,9 @@ export function useBookmarks() { /** * Toggle bookmark for an event. Publishes updated NIP-51 bookmark list. - * - * Updates local state OPTIMISTICALLY so the UI (heart fill) responds - * instantly, then signs + publishes in the background. Signing routes - * through the remote LNbits signer and publishing hits relays, so - * awaiting both before flipping state made the heart lag ~1s. On - * failure the optimistic change is rolled back. Resolves to whether - * the change was persisted. */ - async function toggleBookmark( - eventKind: number, - pubkey: string, - dTag: string, - ): Promise { - if (!isAuthenticated.value || !currentUser.value?.pubkey) return false + async function toggleBookmark(eventKind: number, pubkey: string, dTag: string) { + if (!isAuthenticated.value || !currentUser.value?.pubkey) return const coord = `${eventKind}:${pubkey}:${dTag}` const newCoords = new Set(state.value.bookmarkedCoords) @@ -112,17 +101,6 @@ export function useBookmarks() { newCoords.add(coord) } - // Optimistic flip — preserve the prior state so we can roll back if - // signing or publishing fails. Keep lastEventId/lastCreatedAt until - // the real event is confirmed. - const prevState = state.value - state.value = { bookmarkedCoords: newCoords, lastEventId: prevState.lastEventId } - ;(state.value as any).lastCreatedAt = (prevState as any).lastCreatedAt - - function rollback() { - state.value = prevState - } - // Build and publish updated bookmark list const tags: string[][] = Array.from(newCoords).map(c => ['a', c]) @@ -138,25 +116,19 @@ export function useBookmarks() { signedEvent = await signEventViaLnbits(template) } catch (err) { console.error('[useBookmarks] signEventViaLnbits failed:', err) - rollback() - return false + return } const relayHub = tryInjectService(SERVICE_TOKENS.RELAY_HUB) - if (!relayHub) { - rollback() - return false - } + if (!relayHub) return const result = await relayHub.publishEvent(signedEvent) if (result.success > 0) { - state.value = { bookmarkedCoords: newCoords, lastEventId: signedEvent.id } - ;(state.value as any).lastCreatedAt = template.created_at - return true + state.value = { + bookmarkedCoords: newCoords, + lastEventId: signedEvent.id, + } } - - rollback() - return false } onMounted(() => { diff --git a/src/modules/events/composables/useEventLikes.ts b/src/modules/events/composables/useEventLikes.ts deleted file mode 100644 index 9bca9fd..0000000 --- a/src/modules/events/composables/useEventLikes.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { reactive } from 'vue' -import type { Event as NostrEvent } from 'nostr-tools' -import { tryInjectService, SERVICE_TOKENS } from '@/core/di-container' - -/** - * Live "like" counts for events. A like == the event appearing in a - * user's NIP-51 bookmark list (kind 10003) — the same action the heart - * performs (and what the Favorites page reads). - * - * One batched subscription covers every event coordinate that a mounted - * heart has registered, filtered by `#a`. It stays open after EOSE, so - * when anyone publishes/updates a bookmark list referencing a tracked - * event the relay pushes it live and the count increments for everyone - * in real time (Alice likes → Bob's count ticks up). - * - * Caveats (inherent to counting replaceable bookmark lists via `#a`): - * - An un-like by ANOTHER user only reflects on next load: their new - * list no longer contains the coord, so it no longer matches the - * filter and we never receive the update. Counts are correct on a - * fresh load (the un-liker is simply absent from the results). - * - The current user's own like/un-like updates instantly via setSelf(), - * driven by the optimistic heart state. - */ - -const BOOKMARK_KIND = 10003 - -// coord ("kind:pubkey:dTag") -> set of pubkeys who bookmarked it. -const authorsByCoord = new Map>() // plain map, for dedup -const counts = reactive(new Map()) // reactive mirror for the UI -const tracked = new Set() - -let unsubscribe: (() => void) | null = null -let resubTimer: ReturnType | null = null - -function setCount(coord: string, pubkey: string, present: boolean) { - let set = authorsByCoord.get(coord) - if (!set) { - set = new Set() - authorsByCoord.set(coord, set) - } - const had = set.has(pubkey) - if (present && !had) { - set.add(pubkey) - counts.set(coord, set.size) - } else if (!present && had) { - set.delete(pubkey) - counts.set(coord, set.size) - } -} - -function ingest(event: NostrEvent) { - // A bookmark list references many events via 'a' tags; credit the - // author to every coord we're tracking. - for (const tag of event.tags) { - if (tag[0] === 'a' && tracked.has(tag[1])) setCount(tag[1], event.pubkey, true) - } -} - -function resubscribe() { - const relayHub = tryInjectService(SERVICE_TOKENS.RELAY_HUB) - if (!relayHub) { - scheduleResubscribe() // relay hub not registered yet — retry shortly - return - } - if (unsubscribe) { - unsubscribe() - unsubscribe = null - } - const coords = [...tracked] - if (coords.length === 0) return - unsubscribe = relayHub.subscribe({ - id: 'event-likes-aggregate', - filters: [{ kinds: [BOOKMARK_KIND], '#a': coords }], - onEvent: (event: NostrEvent) => ingest(event), - }) -} - -function scheduleResubscribe() { - if (resubTimer) clearTimeout(resubTimer) - // Debounced so a burst of mounting hearts results in one (re)subscribe. - resubTimer = setTimeout(resubscribe, 250) -} - -export function useEventLikes() { - /** Register an event coordinate so its like count is fetched + kept live. */ - function track(coord: string) { - if (!coord || tracked.has(coord)) return - tracked.add(coord) - scheduleResubscribe() - } - - /** Reactive like count for a coordinate (0 when none/unknown). */ - function likeCount(coord: string): number { - return counts.get(coord) ?? 0 - } - - /** - * Reflect the current user's own like state immediately (optimistic), - * so their count matches the instant heart fill and their un-like - * decrements right away. - */ - function setSelf(coord: string, pubkey: string, liked: boolean) { - if (!coord || !pubkey) return - setCount(coord, pubkey, liked) - } - - return { track, likeCount, setSelf } -} diff --git a/src/modules/events/composables/useTicketPurchase.ts b/src/modules/events/composables/useTicketPurchase.ts index 5c3df37..4d4dc45 100644 --- a/src/modules/events/composables/useTicketPurchase.ts +++ b/src/modules/events/composables/useTicketPurchase.ts @@ -5,19 +5,12 @@ import { useAsyncOperation } from '@/core/composables/useAsyncOperation' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import type { PaymentService } from '@/core/services/PaymentService' import type { TicketApiService } from '../services/TicketApiService' -import { useOwnedTickets } from './useOwnedTickets' export function useTicketPurchase() { const { isAuthenticated, currentUser } = useAuth() const paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE) as PaymentService const ticketApi = injectService(SERVICE_TOKENS.TICKET_API) as TicketApiService - // Refresh the shared owned-tickets singleton after a purchase so the - // feed/calendar "My tickets" filter and EventCard owned badges update - // without a reload — purchase is exactly the "consumer that mutates - // the ticket set" useOwnedTickets's docs anticipate. - const { refresh: refreshOwnedTickets } = useOwnedTickets() - // Async operations const purchaseOperation = useAsyncOperation() @@ -185,12 +178,6 @@ export function useTicketPurchase() { clearInterval(checkInterval) } - // Ticket row(s) now exist — refresh the shared owned-tickets - // state so the feed/calendar My-tickets filter and owned - // badges reflect the purchase immediately (no reload). Runs in - // parallel with QR generation below. - void refreshOwnedTickets() - // Multi-ticket purchases come back with `ticketIds` (N rows // sharing one invoice). Single-ticket purchases include // `ticketId` only. Render one QR per row so each attendee