Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
985baabb79
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 4
      src/components/ContentPreview/NormalContentPreview.tsx
  4. 125
      src/components/Note/index.tsx
  5. 45
      src/components/NoteList/index.tsx
  6. 36
      src/lib/event-kind1111-parent.test.ts
  7. 4
      src/lib/event.ts
  8. 8
      src/lib/feed-local-event-match.test.ts
  9. 16
      src/lib/feed-local-event-match.ts
  10. 53
      src/lib/nostr-event-json.test.ts
  11. 83
      src/lib/nostr-event-json.ts
  12. 3
      src/lib/parent-reply-blurb.ts
  13. 28
      src/pages/secondary/NotePage/index.tsx
  14. 17
      src/services/client-events.service.ts

4
package-lock.json generated

@ -1,12 +1,12 @@ @@ -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",

2
package.json

@ -1,6 +1,6 @@ @@ -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",

4
src/components/ContentPreview/NormalContentPreview.tsx

@ -1,4 +1,5 @@ @@ -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({ @@ -10,5 +11,6 @@ export default function NormalContentPreview({
className?: string
}) {
const emojiInfos = useEmojiInfosForEvent(event)
return <Content content={event.content} className={className} emojiInfos={emojiInfos} />
const content = stripTrailingStringifiedNostrEvent(event.content)
return <Content content={content} className={className} emojiInfos={emojiInfos} />
}

125
src/components/Note/index.tsx

@ -32,13 +32,17 @@ import type { HighlightData } from '@/components/PostEditor/HighlightEditor' @@ -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' @@ -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 { @@ -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 (
<div
data-embedded-note
className={cn(
'not-prose rounded-lg border border-border bg-card p-3 text-card-foreground shadow-sm',
className
)}
>
<div className="mb-2 flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
<Repeat2 className="size-4 shrink-0" aria-hidden />
<span>{t('Boost')}</span>
</div>
<div className="flex min-w-0 gap-2">
<UserAvatar
userId={targetEvent.pubkey}
size="tiny"
className="mt-0.5 shrink-0"
deferRemoteAvatar={false}
/>
<div className="min-w-0 flex-1">
<ContentPreview event={targetEvent} className="line-clamp-4" />
</div>
</div>
</div>
)
}
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 (
<div className={cn('space-y-2', className)}>
{textEvent ? (
<MarkdownArticle
event={textEvent}
hideMetadata={hideMetadata}
lazyMedia={!autoLoadMedia}
fullCalendarInvite={fullCalendarInvite}
/>
) : null}
<StringifiedNostrEventPreviewCard hostEvent={hostEvent} targetEvent={match.event} />
</div>
)
}
function RepostEventContent({ event, className }: { event: Event; className?: string }) {
const embeddedEvent = findTrailingStringifiedNostrEvent(event.content)
if (embeddedEvent) {
return (
<StringifiedNostrEventPreviewCard
hostEvent={event}
targetEvent={embeddedEvent.event}
className={className}
/>
)
}
return <NotificationEventCard className={className} event={event} />
}
export default function Note({
event,
originalNoteId,
@ -200,6 +304,19 @@ export default function Note({ @@ -200,6 +304,19 @@ export default function Note({
hideMetadata?: boolean
className?: string
} = {}) => {
const embeddedEvent = findTrailingStringifiedNostrEvent(displayEvent.content ?? '')
if (embeddedEvent) {
return (
<StringifiedNostrEventContent
hostEvent={displayEvent}
match={embeddedEvent}
className={className}
hideMetadata={hideMetadata}
autoLoadMedia={autoLoadMedia}
fullCalendarInvite={fullCalendarInvite}
/>
)
}
if (isStringifiedJsonContent(displayEvent.content)) {
return (
<pre
@ -268,7 +385,9 @@ export default function Note({ @@ -268,7 +385,9 @@ export default function Note({
content = <NsfwNote show={() => 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 = <RepostEventContent className="mt-2" event={displayEvent} />
} else if (event.kind === ExtendedKind.POLL_RESPONSE) {
content = <NotificationEventCard className="mt-2" event={displayEvent} />
} else if (event.kind === kinds.Highlights) {
// Try to render the Highlight component with error boundary

45
src/components/NoteList/index.tsx

@ -520,6 +520,14 @@ function eventTagValues(event: Event, tagName: string): string[] { @@ -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 { @@ -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( @@ -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( @@ -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

36
src/lib/event-kind1111-parent.test.ts

@ -1,6 +1,12 @@ @@ -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', () => { @@ -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'
)
})
})

4
src/lib/event.ts

@ -586,8 +586,8 @@ export function collectEmbeddedEventPrefetchTargets(event: Event): { @@ -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) ?? []) {

8
src/lib/feed-local-event-match.test.ts

@ -35,6 +35,14 @@ describe('eventMatchesLocalFeedFilter', () => { @@ -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)

16
src/lib/feed-local-event-match.ts

@ -1,12 +1,16 @@ @@ -1,12 +1,16 @@
import type { Event, Filter } from 'nostr-tools'
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()))
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
}
const allowed = new Set(filterValues.map((v) => String(v)))
return eventValues.some((v) => allowed.has(v))
function valuesMatchTag(tagName: string, eventValues: string[], filterValues: unknown[]): boolean {
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 {

53
src/lib/nostr-event-json.test.ts

@ -0,0 +1,53 @@ @@ -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> = {}): 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)
})
})

83
src/lib/nostr-event-json.ts

@ -0,0 +1,83 @@ @@ -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<Event>
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
}

3
src/lib/parent-reply-blurb.ts

@ -4,13 +4,14 @@ import { @@ -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')

28
src/pages/secondary/NotePage/index.tsx

@ -90,6 +90,27 @@ function getEventTypeName(kind: number): string { @@ -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 }: @@ -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 }: @@ -488,7 +510,7 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }:
<div className="px-4 pt-3 w-full">
{rootITag && <ExternalRoot value={rootITag[1]} />}
{rootEventId &&
rootEventId !== parentEventId &&
!eventPointersReferenceSameNote(rootEventId, parentEventId) &&
(isFetchingRootEvent || rootEventForStrip) && (
<ParentNote
key={`root-note-${finalEvent.id}`}

17
src/services/client-events.service.ts

@ -294,6 +294,7 @@ export class EventService { @@ -294,6 +294,7 @@ export class EventService {
async fetchEvent(id: string, opts?: { relayHints?: string[] }): Promise<NEvent | undefined> {
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 { @@ -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 { @@ -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 { @@ -1042,6 +1044,7 @@ export class EventService {
private async _fetchEvent(id: string, extraRelayHints?: string[]): Promise<NEvent | undefined> {
let filter: Filter | undefined
let relays: string[] = []
let authorHintPubkey: string | undefined
if (extraRelayHints?.length) {
relays = [
...new Set(
@ -1063,6 +1066,9 @@ export class EventService { @@ -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 { @@ -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 { @@ -1167,7 +1173,8 @@ export class EventService {
private async tryHarderToFetchEvent(
relayHints: string[],
filter: Filter,
alreadyFetchedFromBigRelays = false
alreadyFetchedFromBigRelays = false,
authorHintPubkey?: string
): Promise<NEvent | undefined> {
// 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 { @@ -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, [])

Loading…
Cancel
Save