diff --git a/src/components/Nip22ReplyNoteList/index.tsx b/src/components/Nip22ReplyNoteList/index.tsx index 4342938..9dbf1db 100644 --- a/src/components/Nip22ReplyNoteList/index.tsx +++ b/src/components/Nip22ReplyNoteList/index.tsx @@ -1,5 +1,6 @@ import { Separator } from '@/components/ui/separator' import { BIG_RELAY_URLS, COMMENT_EVENT_KIND } from '@/constants' +import { isCommentEvent, isProtectedEvent } from '@/lib/event' import { tagNameEquals } from '@/lib/tag' import { cn } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' @@ -10,7 +11,6 @@ import { Event as NEvent } from 'nostr-tools' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import ReplyNote from '../ReplyNote' -import { isCommentEvent } from '@/lib/event' const LIMIT = 100 @@ -62,8 +62,13 @@ export default function Nip22ReplyNoteList({ try { const relayList = await client.fetchRelayList(event.pubkey) + const relayUrls = relayList.read.concat(BIG_RELAY_URLS) + if (isProtectedEvent(event)) { + const seenOn = client.getSeenEventRelayUrls(event.id) + relayUrls.unshift(...seenOn) + } const { closer, timelineKey } = await client.subscribeTimeline( - relayList.read.concat(BIG_RELAY_URLS).slice(0, 4), + relayUrls.slice(0, 4), { '#E': [event.id], kinds: [COMMENT_EVENT_KIND], diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx index 49836ab..b1cf30f 100644 --- a/src/components/ReplyNoteList/index.tsx +++ b/src/components/ReplyNoteList/index.tsx @@ -1,5 +1,6 @@ import { Separator } from '@/components/ui/separator' -import { isReplyNoteEvent } from '@/lib/event' +import { BIG_RELAY_URLS } from '@/constants' +import { isProtectedEvent, isReplyNoteEvent } from '@/lib/event' import { isReplyETag, isRootETag } from '@/lib/tag' import { cn } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' @@ -10,7 +11,6 @@ import { Event as NEvent, kinds } from 'nostr-tools' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import ReplyNote from '../ReplyNote' -import { BIG_RELAY_URLS } from '@/constants' const LIMIT = 100 @@ -56,8 +56,13 @@ export default function ReplyNoteList({ event, className }: { event: NEvent; cla try { const relayList = await client.fetchRelayList(event.pubkey) + const relayUrls = relayList.read.concat(BIG_RELAY_URLS) + if (isProtectedEvent(event)) { + const seenOn = client.getSeenEventRelayUrls(event.id) + relayUrls.unshift(...seenOn) + } const { closer, timelineKey } = await client.subscribeTimeline( - relayList.read.concat(BIG_RELAY_URLS).slice(0, 4), + relayUrls.slice(0, 4), { '#e': [event.id], kinds: [kinds.ShortTextNote], diff --git a/src/lib/event.ts b/src/lib/event.ts index da73606..cdfa925 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -41,6 +41,10 @@ export function isPictureEvent(event: Event) { return event.kind === PICTURE_EVENT_KIND } +export function isProtectedEvent(event: Event) { + return event.tags.some(([tagName]) => tagName === '-') +} + export function getParentEventId(event?: Event) { if (!event || !isReplyNoteEvent(event)) return undefined return event.tags.find(isReplyETag)?.[1] ?? event.tags.find(tagNameEquals('e'))?.[1] diff --git a/src/services/client.service.ts b/src/services/client.service.ts index f3edcc9..f4cdf81 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -16,6 +16,7 @@ import { SimplePool, VerifiedEvent } from 'nostr-tools' +import { AbstractRelay } from 'nostr-tools/abstract-relay' type TTimelineRef = [string, number] @@ -23,7 +24,7 @@ class ClientService extends EventTarget { static instance: ClientService private defaultRelayUrls: string[] = BIG_RELAY_URLS - private pool = new SimplePool() + private pool: SimplePool private timelines: Record< string, @@ -84,6 +85,8 @@ class ClientService extends EventTarget { constructor() { super() + this.pool = new SimplePool() + this.pool.trackRelays = true } public static getInstance(): ClientService { @@ -117,13 +120,21 @@ class ClientService extends EventTarget { const result = await Promise.any( relayUrls.map(async (url) => { const relay = await this.pool.ensureRelay(url) - return relay.publish(event).catch((error) => { - if (error instanceof Error && error.message.startsWith('auth-required:') && signer) { - relay.auth((authEvt: EventTemplate) => signer(authEvt)).then(() => relay.publish(event)) - } else { - throw error - } - }) + return relay + .publish(event) + .then((reason) => { + this.trackEventSeenOn(event.id, relay) + return reason + }) + .catch((error) => { + if (error instanceof Error && error.message.startsWith('auth-required:') && signer) { + relay + .auth((authEvt: EventTemplate) => signer(authEvt)) + .then(() => relay.publish(event)) + } else { + throw error + } + }) }) ) this.dispatchEvent(new CustomEvent('eventPublished', { detail: event })) @@ -203,6 +214,7 @@ class ClientService extends EventTarget { }, onevent: (evt: NEvent) => { that.eventDataLoader.prime(evt.id, Promise.resolve(evt)) + that.trackEventSeenOn(evt.id, relay) // not eosed yet, push to events if (eosedCount < startedCount) { return events.push(evt) @@ -537,6 +549,10 @@ class ClientService extends EventTarget { } } + getSeenEventRelayUrls(eventId: string) { + return Array.from(this.pool.seenOn.get(eventId)?.values() || []).map((relay) => relay.url) + } + private async fetchEventById(relayUrls: string[], id: string): Promise { const event = await this.fetchEventFromDefaultRelaysDataloader.load(id) if (event) { @@ -740,6 +756,15 @@ class ClientService extends EventTarget { return followListEvents.sort((a, b) => b.created_at - a.created_at)[0] } + + private trackEventSeenOn(eventId: string, relay: AbstractRelay) { + let set = this.pool.seenOn.get(eventId) + if (!set) { + set = new Set() + this.pool.seenOn.set(eventId, set) + } + set.add(relay) + } } const instance = ClientService.getInstance()