fix(activities): scope detail-page query by NIP-52 d-tag

`useActivityDetail.load()` previously asked every relay for every
kind-31922/31923 event and raced a 5s timeout to find the one
matching the route param. On a cold refresh of the detail page, the
race was often lost — the store starts empty (no feed subscription
to populate it), the relay sprays the whole calendar, and the
matching event may arrive after the timeout, leaving the user with
"Activity not found" on a valid URL.

Add a `dTags` field to `CalendarEventFilters` and emit it as the
nostr `#d` tag filter. Detail-page subscribe + query both scope to
the single activity, so the relay resolves a parameterized-replaceable
lookup in milliseconds instead of streaming the whole calendar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-30 08:03:55 +02:00
commit cb6e1351fb
2 changed files with 13 additions and 4 deletions

View file

@ -32,18 +32,24 @@ export function useActivityDetail(activityId: string) {
isLoading.value = true isLoading.value = true
error.value = null error.value = null
// Subscribe and wait for this specific event // Scope both the subscription and the one-shot query to this
// activity's d-tag. Without this scope, the query asks every
// relay for every kind-31922/31923 event and races a 5s timeout
// to find ours — on a cold page refresh that race is often lost
// even when the activity is reachable.
const detailFilters = { dTags: [activityId] }
unsubscribe = nostrService.subscribeToCalendarEvents( unsubscribe = nostrService.subscribeToCalendarEvents(
(incoming) => { (incoming) => {
store.upsertActivity(incoming) store.upsertActivity(incoming)
if (incoming.id === activityId) { if (incoming.id === activityId) {
isLoading.value = false isLoading.value = false
} }
} },
detailFilters
) )
// Also do a one-shot query const results = await nostrService.queryCalendarEvents(detailFilters)
const results = await nostrService.queryCalendarEvents()
store.upsertActivities(results) store.upsertActivities(results)
// If we still don't have it after query, stop loading // If we still don't have it after query, stop loading

View file

@ -25,6 +25,8 @@ export interface CalendarEventFilters {
hashtags?: string[] hashtags?: string[]
/** Filter by geohash prefix (NIP-52 'g' tag) */ /** Filter by geohash prefix (NIP-52 'g' tag) */
geohash?: string geohash?: string
/** Filter by NIP-52 'd' tag — scopes the query to specific parameterized-replaceable events */
dTags?: string[]
} }
/** /**
@ -168,6 +170,7 @@ export class ActivitiesNostrService extends BaseService {
if (filters?.authors?.length) filter.authors = filters.authors if (filters?.authors?.length) filter.authors = filters.authors
if (filters?.hashtags?.length) filter['#t'] = filters.hashtags if (filters?.hashtags?.length) filter['#t'] = filters.hashtags
if (filters?.geohash) filter['#g'] = [filters.geohash] if (filters?.geohash) filter['#g'] = [filters.geohash]
if (filters?.dTags?.length) filter['#d'] = filters.dTags
return [filter] return [filter]
} }