diff --git a/package-lock.json b/package-lock.json index 09c029f4..3e3526fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "imwald", - "version": "23.8.0", + "version": "23.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "imwald", - "version": "23.8.0", + "version": "23.8.1", "license": "MIT", "dependencies": { "@asciidoctor/core": "^3.0.4", diff --git a/package.json b/package.json index e52a138b..22a220f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "imwald", - "version": "23.8.0", + "version": "23.8.1", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "private": true, "type": "module", diff --git a/src/components/ContentPreview/NormalContentPreview.tsx b/src/components/ContentPreview/NormalContentPreview.tsx index 33fae2e3..c4439a8f 100644 --- a/src/components/ContentPreview/NormalContentPreview.tsx +++ b/src/components/ContentPreview/NormalContentPreview.tsx @@ -1,4 +1,5 @@ import { useEmojiInfosForEvent } from '@/hooks' +import { stripTrailingStringifiedNostrEvent } from '@/lib/nostr-event-json' import { Event } from 'nostr-tools' import Content from './Content' @@ -10,5 +11,6 @@ export default function NormalContentPreview({ className?: string }) { const emojiInfos = useEmojiInfosForEvent(event) - return + const content = stripTrailingStringifiedNostrEvent(event.content) + return } diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index 1e9cef9a..ac1aa0c4 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -32,13 +32,17 @@ import type { HighlightData } from '@/components/PostEditor/HighlightEditor' import { Event, kinds } from 'nostr-tools' import { isCalendarEventKind } from '@/lib/calendar-event' import { mergeTranslatedNote, useNoteTranslation } from '@/lib/note-translation-display' -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { getWebBookmarkArticleUrl, getWebExternalReactionTargetUrl, isRssThreadSyntheticParentEvent } from '@/lib/rss-article' +import { + findTrailingStringifiedNostrEvent, + type StringifiedNostrEventMatch +} from '@/lib/nostr-event-json' import { CreateHighlightContext } from './CreateHighlightContext' import SelectionHighlightTrigger from './SelectionHighlightTrigger' import AudioPlayer from '../AudioPlayer' @@ -50,10 +54,11 @@ import NoteOptions from '../NoteOptions' import ParentNotePreview from '../ParentNotePreview' import UserAvatar from '../UserAvatar' import Username from '../Username' -import { MessageSquare } from 'lucide-react' +import { MessageSquare, Repeat2 } from 'lucide-react' import CommunityDefinition from './CommunityDefinition' import GroupMetadata from './GroupMetadata' import Highlight from './Highlight' +import ContentPreview from '../ContentPreview' import IValue from './IValue' import LiveEvent from './LiveEvent' @@ -102,6 +107,105 @@ function isStringifiedJsonContent(content?: string): boolean { } } +function cacheEmbeddedRepostTarget(hostEvent: Event, targetEvent: Event) { + client.addEventToCache(targetEvent) + const targetSeenOn = client.getSeenEventRelays(targetEvent.id) + if (targetSeenOn.length > 0) return + client.getSeenEventRelays(hostEvent.id).forEach((relay) => { + client.trackEventSeenOn(targetEvent.id, relay) + }) +} + +function StringifiedNostrEventPreviewCard({ + hostEvent, + targetEvent, + className +}: { + hostEvent: Event + targetEvent: Event + className?: string +}) { + const { t } = useTranslation() + + useEffect(() => { + cacheEmbeddedRepostTarget(hostEvent, targetEvent) + }, [hostEvent.id, targetEvent]) + + return ( +
+
+ + {t('Boost')} +
+
+ +
+ +
+
+
+ ) +} + +function StringifiedNostrEventContent({ + hostEvent, + match, + className, + hideMetadata, + autoLoadMedia, + fullCalendarInvite +}: { + hostEvent: Event + match: StringifiedNostrEventMatch + className?: string + hideMetadata?: boolean + autoLoadMedia: boolean + fullCalendarInvite?: { event: Event; naddr: string } +}) { + const textEvent = match.textBefore.trim() + ? { ...hostEvent, content: match.textBefore } + : undefined + + return ( +
+ {textEvent ? ( + + ) : null} + +
+ ) +} + +function RepostEventContent({ event, className }: { event: Event; className?: string }) { + const embeddedEvent = findTrailingStringifiedNostrEvent(event.content) + if (embeddedEvent) { + return ( + + ) + } + return +} + export default function Note({ event, originalNoteId, @@ -200,6 +304,19 @@ export default function Note({ hideMetadata?: boolean className?: string } = {}) => { + const embeddedEvent = findTrailingStringifiedNostrEvent(displayEvent.content ?? '') + if (embeddedEvent) { + return ( + + ) + } if (isStringifiedJsonContent(displayEvent.content)) { return (
 setShowNsfw(true)} />
   } else if (isNip25ReactionKind(event.kind)) {
     content = null
-  } else if (isNip18RepostKind(event.kind) || event.kind === ExtendedKind.POLL_RESPONSE) {
+  } else if (isNip18RepostKind(event.kind)) {
+    content = 
+  } else if (event.kind === ExtendedKind.POLL_RESPONSE) {
     content = 
   } else if (event.kind === kinds.Highlights) {
     // Try to render the Highlight component with error boundary
diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx
index 45865c3a..6bfdb824 100644
--- a/src/components/NoteList/index.tsx
+++ b/src/components/NoteList/index.tsx
@@ -520,6 +520,14 @@ function eventTagValues(event: Event, tagName: string): string[] {
     .map((tag) => tag[1] as string)
 }
 
+function comparableLocalTagValue(tagName: string, value: unknown): string {
+  const text = String(value).trim()
+  const tagKey = tagName.toLowerCase()
+  if (tagKey === 't') return text.toLowerCase()
+  if ((tagKey === 'p' || tagKey === 'e') && /^[0-9a-f]{64}$/i.test(text)) return text.toLowerCase()
+  return text
+}
+
 function eventMatchesSubRequestFilter(event: Event, filter: Filter): boolean {
   const ids = Array.isArray(filter.ids) ? filter.ids : undefined
   if (ids && ids.length > 0 && !ids.includes(event.id)) return false
@@ -536,16 +544,8 @@ function eventMatchesSubRequestFilter(event: Event, filter: Filter): boolean {
     const tagName = key.slice(1)
     const eventValues = eventTagValues(event, tagName)
     if (eventValues.length === 0) return false
-    const matched =
-      tagName.toLowerCase() === 't'
-        ? (() => {
-            const allowed = new Set(values.map((v) => String(v).toLowerCase()))
-            return eventValues.some((v) => allowed.has(v.toLowerCase()))
-          })()
-        : (() => {
-            const allowed = new Set(values.map((v) => String(v)))
-            return eventValues.some((v) => allowed.has(v))
-          })()
+    const allowed = new Set(values.map((v) => comparableLocalTagValue(tagName, v)))
+    const matched = eventValues.some((v) => allowed.has(comparableLocalTagValue(tagName, v)))
     if (!matched) return false
   }
 
@@ -2219,15 +2219,23 @@ const NoteList = forwardRef(
 
               void (async () => {
                 try {
-                  const [diskRaw, fromPub, fromArch] = await Promise.all([
-                    client.getTimelineDiskSnapshotEvents(
-                      mappedSubRequests as Array<{ urls: string[]; filter: TSubRequestFilter }>
-                    ),
-                    indexedDb.getCachedPublicationEventsByKinds(localLayerCap * 2, kindsForScan),
+                  const filterAwareDiskReq = mappedSubRequests as Array<{
+                    urls: string[]
+                    filter: TSubRequestFilter
+                  }>
+                  const [diskRaw, filterAwareLocalRaw, fromPub, fromArch] = await Promise.all([
+                    client.getTimelineDiskSnapshotEvents(filterAwareDiskReq),
+                    client.getLocalFeedEvents(filterAwareDiskReq, {
+                      maxRowsScanned: 50_000,
+                      maxMatches: localLayerCap * 3
+                    }),
+                    indexedDb.getCachedPublicationEventsByKinds(localLayerCap * 2, kindsForScan, {
+                      scanBudget: 50_000
+                    }),
                     indexedDb.scanEventArchiveByKinds({
                       kinds: kindsForScan,
                       since: sinceTightest,
-                      maxRowsScanned: 10_000,
+                      maxRowsScanned: 50_000,
                       maxMatches: localLayerCap * 2
                     })
                   ])
@@ -2239,6 +2247,11 @@ const NoteList = forwardRef(
                     seen.add(ev.id)
                     combinedRaw.push(ev)
                   }
+                  for (const ev of filterAwareLocalRaw) {
+                    if (seen.has(ev.id)) continue
+                    seen.add(ev.id)
+                    combinedRaw.push(ev)
+                  }
                   for (const ev of fromPub) {
                     if (seen.has(ev.id)) continue
                     if (!matchesSpellLocal(ev)) continue
diff --git a/src/lib/event-kind1111-parent.test.ts b/src/lib/event-kind1111-parent.test.ts
index d09f60ec..04ec0e57 100644
--- a/src/lib/event-kind1111-parent.test.ts
+++ b/src/lib/event-kind1111-parent.test.ts
@@ -1,6 +1,12 @@
 import { describe, expect, it } from 'vitest'
 import { nip19 } from 'nostr-tools'
-import { getParentBech32Id, getParentEventHexId, getRootEventHexId } from './event'
+import {
+  collectEmbeddedEventPrefetchTargets,
+  getParentBech32Id,
+  getParentEventHexId,
+  getRootBech32Id,
+  getRootEventHexId
+} from './event'
 
 /** Kind 1111 sample: E/e point at a kind-1 parent; must not resolve parent hex to the comment id. */
 const fiatjafCommentSample = {
@@ -55,4 +61,32 @@ describe('kind 1111 parent / root resolution', () => {
       expect(decoded.data.id).not.toBe(ev.id)
     }
   })
+
+  it('keeps the parent author hint in parent/root nevent ids', () => {
+    const ev = { ...fiatjafCommentSample } as any
+    const parentBech32 = getParentBech32Id(ev)
+    const rootBech32 = getRootBech32Id(ev)
+
+    for (const pointer of [parentBech32, rootBech32]) {
+      expect(pointer).toBeTruthy()
+      const decoded = nip19.decode(pointer!)
+      expect(decoded.type).toBe('nevent')
+      if (decoded.type === 'nevent') {
+        expect(decoded.data.author).toBe(
+          '1f79058c77a224e5be226c8f024cacdad4d741855d75ed9f11473ba8eb86e1cb'
+        )
+      }
+    }
+  })
+
+  it('prefetches uppercase thread e-tags used by NIP-22 comments', () => {
+    const ev = {
+      ...fiatjafCommentSample,
+      tags: fiatjafCommentSample.tags.filter((tag) => tag[0] !== 'e')
+    } as any
+
+    expect(collectEmbeddedEventPrefetchTargets(ev).hexIds).toContain(
+      '2c88e6bdf1d51d52037078624b21f07eefd86f3413be78efdb64e4931bb6bc99'
+    )
+  })
 })
diff --git a/src/lib/event.ts b/src/lib/event.ts
index 89fb9f85..480f322d 100644
--- a/src/lib/event.ts
+++ b/src/lib/event.ts
@@ -586,8 +586,8 @@ export function collectEmbeddedEventPrefetchTargets(event: Event): {
   }
 
   for (const tag of event.tags) {
-    if (tag[0] === 'e' && tag[1]) addHex(tag[1])
-    if (tag[0] === 'a' && tag[3]) addHex(tag[3])
+    if ((tag[0] === 'e' || tag[0] === 'E') && tag[1]) addHex(tag[1])
+    if ((tag[0] === 'a' || tag[0] === 'A') && tag[3]) addHex(tag[3])
   }
 
   for (const full of event.content.match(EMBEDDED_EVENT_REGEX) ?? []) {
diff --git a/src/lib/feed-local-event-match.test.ts b/src/lib/feed-local-event-match.test.ts
index c63a166f..3d8da9bd 100644
--- a/src/lib/feed-local-event-match.test.ts
+++ b/src/lib/feed-local-event-match.test.ts
@@ -35,6 +35,14 @@ describe('eventMatchesLocalFeedFilter', () => {
     ).toBe(true)
   })
 
+  it('matches hex mention tags case-insensitively for local cache warmup', () => {
+    expect(
+      eventMatchesLocalFeedFilter(event({ tags: [['p', 'C'.repeat(64)]] }), {
+        '#p': ['c'.repeat(64)]
+      })
+    ).toBe(true)
+  })
+
   it('rejects events outside any filter constraint', () => {
     expect(eventMatchesLocalFeedFilter(event({ kind: 6 }), { kinds: [1] })).toBe(false)
     expect(eventMatchesLocalFeedFilter(event(), { since: 1001 })).toBe(false)
diff --git a/src/lib/feed-local-event-match.ts b/src/lib/feed-local-event-match.ts
index bd452ed3..68bcc9c3 100644
--- a/src/lib/feed-local-event-match.ts
+++ b/src/lib/feed-local-event-match.ts
@@ -1,12 +1,16 @@
 import type { Event, Filter } from 'nostr-tools'
 
+function comparableTagValue(tagName: string, value: unknown): string {
+  const text = String(value).trim()
+  const tagKey = tagName.toLowerCase()
+  if (tagKey === 't') return text.toLowerCase()
+  if ((tagKey === 'p' || tagKey === 'e') && /^[0-9a-f]{64}$/i.test(text)) return text.toLowerCase()
+  return text
+}
+
 function valuesMatchTag(tagName: string, eventValues: string[], filterValues: unknown[]): boolean {
-  if (tagName.toLowerCase() === 't') {
-    const allowed = new Set(filterValues.map((v) => String(v).toLowerCase()))
-    return eventValues.some((v) => allowed.has(v.toLowerCase()))
-  }
-  const allowed = new Set(filterValues.map((v) => String(v)))
-  return eventValues.some((v) => allowed.has(v))
+  const allowed = new Set(filterValues.map((v) => comparableTagValue(tagName, v)))
+  return eventValues.some((v) => allowed.has(comparableTagValue(tagName, v)))
 }
 
 export function eventMatchesLocalFeedFilter(event: Event, filter: Filter): boolean {
diff --git a/src/lib/nostr-event-json.test.ts b/src/lib/nostr-event-json.test.ts
new file mode 100644
index 00000000..721343cf
--- /dev/null
+++ b/src/lib/nostr-event-json.test.ts
@@ -0,0 +1,53 @@
+import { describe, expect, it } from 'vitest'
+import type { Event } from 'nostr-tools'
+import {
+  findTrailingStringifiedNostrEvent,
+  isNostrEventJson,
+  stripTrailingStringifiedNostrEvent
+} from './nostr-event-json'
+
+function event(overrides: Partial = {}): Event {
+  return {
+    id: 'a'.repeat(64),
+    pubkey: 'b'.repeat(64),
+    created_at: 1000,
+    kind: 1,
+    tags: [['p', 'c'.repeat(64)]],
+    content: 'original note',
+    sig: 'd'.repeat(128),
+    ...overrides
+  }
+}
+
+describe('nostr event JSON helpers', () => {
+  it('recognizes serialized nostr events', () => {
+    expect(isNostrEventJson(event())).toBe(true)
+    expect(isNostrEventJson({ ...event(), id: 'not-hex' })).toBe(false)
+    expect(isNostrEventJson({ ...event(), tags: [['p', 1]] })).toBe(false)
+  })
+
+  it('extracts a whole stringified event', () => {
+    const target = event()
+    const match = findTrailingStringifiedNostrEvent(JSON.stringify(target))
+
+    expect(match?.event.id).toBe(target.id)
+    expect(match?.textBefore).toBe('')
+  })
+
+  it('extracts trailing event JSON after quote text', () => {
+    const target = event({ content: 'quoted target' })
+    const content = `This is my comment before the boost.\n\n${JSON.stringify(target)}`
+    const match = findTrailingStringifiedNostrEvent(content)
+
+    expect(match?.event.content).toBe('quoted target')
+    expect(match?.textBefore).toBe('This is my comment before the boost.')
+    expect(stripTrailingStringifiedNostrEvent(content)).toBe('This is my comment before the boost.')
+  })
+
+  it('leaves ordinary JSON alone', () => {
+    const content = 'Here is config {"theme":"dark"}'
+
+    expect(findTrailingStringifiedNostrEvent(content)).toBeNull()
+    expect(stripTrailingStringifiedNostrEvent(content)).toBe(content)
+  })
+})
diff --git a/src/lib/nostr-event-json.ts b/src/lib/nostr-event-json.ts
new file mode 100644
index 00000000..6a5c942e
--- /dev/null
+++ b/src/lib/nostr-event-json.ts
@@ -0,0 +1,83 @@
+import type { Event } from 'nostr-tools'
+
+const HEX_64_RE = /^[0-9a-f]{64}$/i
+const HEX_SIG_RE = /^[0-9a-f]{128}$/i
+
+export type StringifiedNostrEventMatch = {
+  event: Event
+  textBefore: string
+  jsonText: string
+}
+
+function isStringArrayArray(value: unknown): value is string[][] {
+  return (
+    Array.isArray(value) &&
+    value.every((tag) => Array.isArray(tag) && tag.every((part) => typeof part === 'string'))
+  )
+}
+
+export function isNostrEventJson(value: unknown): value is Event {
+  if (!value || typeof value !== 'object') return false
+  const event = value as Partial
+  return (
+    typeof event.id === 'string' &&
+    HEX_64_RE.test(event.id) &&
+    typeof event.pubkey === 'string' &&
+    HEX_64_RE.test(event.pubkey) &&
+    typeof event.created_at === 'number' &&
+    Number.isFinite(event.created_at) &&
+    typeof event.kind === 'number' &&
+    Number.isFinite(event.kind) &&
+    isStringArrayArray(event.tags) &&
+    typeof event.content === 'string' &&
+    typeof event.sig === 'string' &&
+    HEX_SIG_RE.test(event.sig)
+  )
+}
+
+function parseNostrEventJson(raw: string): Event | null {
+  try {
+    const parsed = JSON.parse(raw)
+    return isNostrEventJson(parsed) ? parsed : null
+  } catch {
+    return null
+  }
+}
+
+/**
+ * Some clients append a full serialized event after quote/repost text. Treat a trailing event JSON
+ * object as structured data instead of showing it as prose.
+ */
+export function findTrailingStringifiedNostrEvent(content: string): StringifiedNostrEventMatch | null {
+  const trimmed = content.trimEnd()
+  if (!trimmed) return null
+
+  const whole = parseNostrEventJson(trimmed)
+  if (whole) {
+    return {
+      event: whole,
+      textBefore: '',
+      jsonText: trimmed
+    }
+  }
+
+  let start = trimmed.lastIndexOf('{')
+  while (start >= 0) {
+    const jsonText = trimmed.slice(start)
+    const event = parseNostrEventJson(jsonText)
+    if (event) {
+      return {
+        event,
+        textBefore: trimmed.slice(0, start).trimEnd(),
+        jsonText
+      }
+    }
+    start = trimmed.lastIndexOf('{', start - 1)
+  }
+
+  return null
+}
+
+export function stripTrailingStringifiedNostrEvent(content: string): string {
+  return findTrailingStringifiedNostrEvent(content)?.textBefore ?? content
+}
diff --git a/src/lib/parent-reply-blurb.ts b/src/lib/parent-reply-blurb.ts
index 2869f2db..ef1ba9e0 100644
--- a/src/lib/parent-reply-blurb.ts
+++ b/src/lib/parent-reply-blurb.ts
@@ -4,13 +4,14 @@ import {
   getLongFormArticleMetadataFromEvent
 } from '@/lib/event-metadata'
 import { tagNameEquals } from '@/lib/tag'
+import { stripTrailingStringifiedNostrEvent } from '@/lib/nostr-event-json'
 import { Event, kinds } from 'nostr-tools'
 
 export const PARENT_REPLY_BLURB_MAX = 150
 
 /** Strip common markdown / asciidoc / HTML so parent reply strips stay one line (matches NotePage preview). */
 export function stripMarkupForPreview(content: string): string {
-  let text = content
+  let text = stripTrailingStringifiedNostrEvent(content)
   text = text.replace(/^#{1,6}\s+/gm, '')
   text = text.replace(/\*\*([^*]+)\*\*/g, '$1')
   text = text.replace(/\*([^*]+)\*/g, '$1')
diff --git a/src/pages/secondary/NotePage/index.tsx b/src/pages/secondary/NotePage/index.tsx
index 65852d7b..d73d1cb1 100644
--- a/src/pages/secondary/NotePage/index.tsx
+++ b/src/pages/secondary/NotePage/index.tsx
@@ -90,6 +90,27 @@ function getEventTypeName(kind: number): string {
   }
 }
 
+function eventPointerHexId(pointer: string | undefined): string | undefined {
+  const raw = pointer?.trim()
+  if (!raw) return undefined
+  if (/^[0-9a-f]{64}$/i.test(raw)) return raw.toLowerCase()
+  try {
+    const decoded = nip19.decode(raw)
+    if (decoded.type === 'note') return decoded.data.toLowerCase()
+    if (decoded.type === 'nevent') return decoded.data.id.toLowerCase()
+  } catch {
+    /* invalid pointer */
+  }
+  return undefined
+}
+
+function eventPointersReferenceSameNote(a: string | undefined, b: string | undefined): boolean {
+  if (!a || !b) return false
+  const aHex = eventPointerHexId(a)
+  const bHex = eventPointerHexId(b)
+  return aHex != null && bHex != null ? aHex === bHex : a === b
+}
+
 const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }: { id?: string; index?: number; hideTitlebar?: boolean; initialEvent?: Event }, ref) => {
   const { t } = useTranslation()
   const { registerPrimaryPanelRefresh } = usePrimaryNoteView()
@@ -108,12 +129,13 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }:
   const rootEventId = useMemo(() => {
     if (!finalEvent) return undefined
     const rootHex = getRootEventHexId(finalEvent)?.toLowerCase()
+    const rootBech32Id = getRootBech32Id(finalEvent)
     if (rootHex && /^[0-9a-f]{64}$/i.test(rootHex)) {
       const resolvedRootHex = resolveDeclaredThreadRootEventHex(rootHex)
       if (resolvedRootHex === finalEvent.id.toLowerCase()) return undefined
-      return resolvedRootHex
+      return resolvedRootHex === rootHex ? rootBech32Id ?? resolvedRootHex : resolvedRootHex
     }
-    return getRootBech32Id(finalEvent)
+    return rootBech32Id
   }, [finalEvent])
   const rootITag = useMemo(
     () => (finalEvent?.kind === ExtendedKind.COMMENT ? finalEvent.tags.find(tagNameEquals('I')) : undefined),
@@ -488,7 +510,7 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }:
       
{rootITag && } {rootEventId && - rootEventId !== parentEventId && + !eventPointersReferenceSameNote(rootEventId, parentEventId) && (isFetchingRootEvent || rootEventForStrip) && ( { const trimmed = id.trim() let hexId: string | undefined + let pointerHasFetchHints = false if (/^[0-9a-f]{64}$/i.test(trimmed)) { hexId = trimmed.toLowerCase() } else { @@ -305,6 +306,7 @@ export class EventService { break case 'nevent': hexId = data.id + pointerHasFetchHints = Boolean(data.author || data.relays?.length) break case 'naddr': { const fromSession = this.getSessionEventIfMatchingNaddr({ @@ -338,8 +340,8 @@ export class EventService { this.eventDataLoader.clear(hexId) } } - if (opts?.relayHints?.length) { - const hinted = await this._fetchEvent(trimmed, opts.relayHints) + if (opts?.relayHints?.length || pointerHasFetchHints) { + const hinted = await this._fetchEvent(trimmed, opts?.relayHints) if (hinted && !shouldDropEventOnIngest(hinted)) return hinted } const loaded = await this.eventDataLoader.load(hexId ?? trimmed) @@ -1042,6 +1044,7 @@ export class EventService { private async _fetchEvent(id: string, extraRelayHints?: string[]): Promise { let filter: Filter | undefined let relays: string[] = [] + let authorHintPubkey: string | undefined if (extraRelayHints?.length) { relays = [ ...new Set( @@ -1063,6 +1066,9 @@ export class EventService { case 'nevent': filter = { ids: [data.id], limit: 1 } if (data.relays) relays = [...new Set([...relays, ...data.relays])] + if (data.author && /^[0-9a-f]{64}$/i.test(data.author)) { + authorHintPubkey = data.author.toLowerCase() + } break case 'naddr': { const pk = data.pubkey.toLowerCase() @@ -1121,7 +1127,7 @@ export class EventService { } // Always try comprehensive relay list (author's outboxes + user's inboxes + hints + seen + defaults) - const event = await this.tryHarderToFetchEvent(relays, filter, true) + const event = await this.tryHarderToFetchEvent(relays, filter, true, authorHintPubkey) if (event && !shouldDropEventOnIngest(event)) { this.addEventToCache(event) return event @@ -1167,7 +1173,8 @@ export class EventService { private async tryHarderToFetchEvent( relayHints: string[], filter: Filter, - alreadyFetchedFromBigRelays = false + alreadyFetchedFromBigRelays = false, + authorHintPubkey?: string ): Promise { // Get seen relays if we have an event ID const seenRelays = filter.ids?.length ? client.getSeenEventRelayUrls(filter.ids[0]) : [] @@ -1177,7 +1184,7 @@ export class EventService { ? parseReplaceableAtagCoordinate(filter['#a'][0] as string) : null const authorPubkey = - filter.authors?.length === 1 ? filter.authors[0] : parsedAtag?.pubkey + filter.authors?.length === 1 ? filter.authors[0] : parsedAtag?.pubkey ?? authorHintPubkey // Build comprehensive relay list const relayUrls = await buildComprehensiveRelayListForEvents(authorPubkey, relayHints, seenRelays, [])