From ed5cb4a819daa2c8eff0393e5e7537e7799faa4e Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 23 May 2026 13:27:11 +0200 Subject: [PATCH] bug-fixes --- src/components/ContentPreview/index.tsx | 2 +- src/components/Note/Superchat.tsx | 152 ++++------- .../Note/SuperchatCommentMarkdown.tsx | 31 +++ src/components/Note/Zap.tsx | 237 ++++++------------ src/components/Note/index.tsx | 58 +++-- src/components/PaytoDialog/index.tsx | 22 +- .../Profile/ProfileWallSuperchats.tsx | 4 +- src/components/Profile/index.tsx | 1 + src/components/ProfileAbout/index.tsx | 15 +- src/components/ReplyNote/index.tsx | 9 +- .../TurnIntoSuperchatButton/index.tsx | 8 +- src/hooks/usePaymentAttestationStatus.tsx | 111 +++++--- src/lib/feed-local-event-match.ts | 5 +- src/lib/navigation-related-events.ts | 23 ++ 14 files changed, 345 insertions(+), 333 deletions(-) create mode 100644 src/components/Note/SuperchatCommentMarkdown.tsx diff --git a/src/components/ContentPreview/index.tsx b/src/components/ContentPreview/index.tsx index 05afcd94..cb6ee0d6 100644 --- a/src/components/ContentPreview/index.tsx +++ b/src/components/ContentPreview/index.tsx @@ -315,7 +315,7 @@ export default function ContentPreview({ if (previewDensity === 'compact') { return (
- +
) } diff --git a/src/components/Note/Superchat.tsx b/src/components/Note/Superchat.tsx index 6ba8710b..9d8f79ad 100644 --- a/src/components/Note/Superchat.tsx +++ b/src/components/Note/Superchat.tsx @@ -1,27 +1,25 @@ import { useFetchEvent } from '@/hooks' +import { openNoteFromFetchOrCache } from '@/lib/navigation-related-events' import { parsePaytoTagType } from '@/lib/payto' +import { relayHintsFromEventTags } from '@/lib/relay-list-builder' import { getPaymentNotificationInfo, getSuperchatReferenceFetchId } from '@/lib/superchat' -import { toNote, toProfile } from '@/lib/link' +import { toProfile } from '@/lib/link' import { cn } from '@/lib/utils' import { Event } from 'nostr-tools' import { useMemo, type MouseEvent } from 'react' import { useTranslation } from 'react-i18next' import { useSmartNoteNavigationOptional, useSecondaryPageOptional } from '@/PageManager' import Username from '../Username' -import UserAvatar from '../UserAvatar' import SuperchatPaymentMethodLabel from './SuperchatPaymentMethodLabel' +import SuperchatCommentMarkdown from './SuperchatCommentMarkdown' import TurnIntoSuperchatButton from '../TurnIntoSuperchatButton' export default function Superchat({ event, - className, - omitSenderHeading, - variant = 'default' + className }: { event: Event className?: string - omitSenderHeading?: boolean - variant?: 'default' | 'compact' }) { const { t } = useTranslation() const info = useMemo(() => getPaymentNotificationInfo(event), [event]) @@ -38,17 +36,16 @@ export default function Superchat({ [info] ) - const { event: targetEvent } = useFetchEvent(referencedFetchId) + const threadRelayHints = useMemo(() => relayHintsFromEventTags(event), [event]) + const threadFetchOpts = useMemo( + () => (threadRelayHints.length ? { relayHints: threadRelayHints } : undefined), + [threadRelayHints] + ) + const { event: targetEvent } = useFetchEvent(referencedFetchId, undefined, threadFetchOpts) if (!info) { return ( -
+
[{t('Invalid superchat')}]
) @@ -57,106 +54,55 @@ export default function Superchat({ const { senderPubkey, recipientPubkey, comment } = info const hasThreadTarget = Boolean(targetEvent || referencedFetchId) const hasTarget = hasThreadTarget || Boolean(recipientPubkey) + const hasMetaLine = + (recipientPubkey && recipientPubkey !== senderPubkey) || hasTarget const openTarget = (e: MouseEvent) => { e.stopPropagation() - if (targetEvent) { - navigateToNote(toNote(targetEvent), targetEvent) - } else if (referencedFetchId) { - navigateToNote(toNote(referencedFetchId)) + if (referencedFetchId) { + openNoteFromFetchOrCache(navigateToNote, referencedFetchId, targetEvent) } else if (recipientPubkey) { push(toProfile(recipientPubkey)) } } - if (variant === 'compact') { - const hasMetaLine = - (recipientPubkey && recipientPubkey !== senderPubkey) || hasTarget - - return ( -
-
- - {t('Superchat')} -
- {hasMetaLine ? ( -
- {recipientPubkey && recipientPubkey !== senderPubkey ? ( - - {t('to')}{' '} - - - ) : null} - {hasTarget ? ( - - ) : null} -
- ) : null} - {comment ? ( -

- {comment} -

- ) : null} -
- ) - } - return ( -
- {hasTarget ? ( - - ) : null} - -
-
- -
-
- {!omitSenderHeading && ( -
- - - {t('Superchat')} - {recipientPubkey && recipientPubkey !== senderPubkey && ( - - {t('to')} - - - - )} -
- )} - - {comment ? ( -
-

- {comment} -

-
+
+ {hasMetaLine ? ( +
+ {recipientPubkey && recipientPubkey !== senderPubkey ? ( + + {t('to')}{' '} + + + ) : null} + {hasTarget ? ( + ) : null}
+ ) : null} +
+ + {t('Superchat')}
- + {comment ? ( + + ) : null} +
) } diff --git a/src/components/Note/SuperchatCommentMarkdown.tsx b/src/components/Note/SuperchatCommentMarkdown.tsx new file mode 100644 index 00000000..e615581f --- /dev/null +++ b/src/components/Note/SuperchatCommentMarkdown.tsx @@ -0,0 +1,31 @@ +import MarkdownArticle from './MarkdownArticle/MarkdownArticle' +import { cn } from '@/lib/utils' +import { Event } from 'nostr-tools' +import { useMemo } from 'react' + +export default function SuperchatCommentMarkdown({ + event, + comment, + className +}: { + event: Event + comment: string + className?: string +}) { + const previewEvent = useMemo( + () => ({ ...event, content: comment }) as Event, + [event, comment] + ) + + return ( + + ) +} diff --git a/src/components/Note/Zap.tsx b/src/components/Note/Zap.tsx index eb19a3e9..075504ed 100644 --- a/src/components/Note/Zap.tsx +++ b/src/components/Note/Zap.tsx @@ -2,205 +2,128 @@ import { useFetchEvent } from '@/hooks' import { getZapInfoFromEvent } from '@/lib/event-metadata' import { shouldHideInteractions } from '@/lib/event-filtering' import { formatAmount } from '@/lib/lightning' +import { openNoteFromFetchOrCache } from '@/lib/navigation-related-events' +import { relayHintsFromEventTags } from '@/lib/relay-list-builder' import { getSuperchatPaytoType } from '@/lib/superchat' -import { toNote, toProfile } from '@/lib/link' +import { toProfile } from '@/lib/link' import { cn } from '@/lib/utils' import { Event } from 'nostr-tools' import { useMemo, type MouseEvent } from 'react' import { useTranslation } from 'react-i18next' import { useSmartNoteNavigationOptional, useSecondaryPageOptional } from '@/PageManager' import Username from '../Username' -import UserAvatar from '../UserAvatar' import SuperchatPaymentMethodLabel from './SuperchatPaymentMethodLabel' +import SuperchatCommentMarkdown from './SuperchatCommentMarkdown' import TurnIntoSuperchatButton from '../TurnIntoSuperchatButton' export default function Zap({ event, - className, - /** When the parent row already shows the zapper (e.g. reply list), hide the duplicate sender line. */ - omitSenderHeading, - /** Dense thread row (e.g. kind 1111–sized), not the full note card. */ - variant = 'default' + className }: { event: Event className?: string - omitSenderHeading?: boolean - variant?: 'default' | 'compact' }) { - // In quiet mode, we need to check the target event (if this is a zap receipt for an event) - // For profile zaps, we can't check quiet mode since we don't have an event - const zapInfo = useMemo(() => getZapInfoFromEvent(event), [event]) - const { event: targetEvent } = useFetchEvent(zapInfo?.eventId) - - // Check if the target event (if any) is in quiet mode - const inQuietMode = targetEvent ? shouldHideInteractions(targetEvent) : false - - // Hide zap receipts in quiet mode as they contain emojis and text - if (inQuietMode) { - return null - } const { t } = useTranslation() - const { navigateToNote } = useSmartNoteNavigationOptional() - const secondaryPage = useSecondaryPageOptional() - const push = secondaryPage?.push ?? ((url: string) => { window.location.href = url }) - - if (!zapInfo || !zapInfo.senderPubkey || (variant === 'default' && !zapInfo.amount)) { - return ( -
- [{t('Invalid zap receipt')}] -
- ) - } + const zapInfo = useMemo(() => getZapInfoFromEvent(event), [event]) + const zapRelayHints = useMemo(() => relayHintsFromEventTags(event), [event]) + const zapFetchOpts = useMemo( + () => (zapRelayHints.length ? { relayHints: zapRelayHints } : undefined), + [zapRelayHints] + ) + const { event: targetEvent } = useFetchEvent(zapInfo?.eventId, undefined, zapFetchOpts) - // Determine if this is an event zap or profile zap - const isEventZap = targetEvent || zapInfo?.eventId - const isProfileZap = !isEventZap && zapInfo?.recipientPubkey + const isEventZap = Boolean(targetEvent || zapInfo?.eventId) + const isProfileZap = Boolean(!isEventZap && zapInfo?.recipientPubkey) - // For event zaps, we need to determine the recipient from the zapped event const actualRecipientPubkey = useMemo(() => { if (isEventZap && targetEvent) { - // Event zap - recipient is the author of the zapped event return targetEvent.pubkey - } else if (isProfileZap) { - // Profile zap - recipient is directly specified + } + if (isProfileZap) { return zapInfo?.recipientPubkey } return undefined }, [isEventZap, isProfileZap, targetEvent, zapInfo?.recipientPubkey]) - const { senderPubkey, recipientPubkey, amount, comment } = zapInfo const paytoType = useMemo(() => getSuperchatPaytoType(event), [event]) + const { navigateToNote } = useSmartNoteNavigationOptional() + const secondaryPage = useSecondaryPageOptional() + const push = secondaryPage?.push ?? ((url: string) => { window.location.href = url }) - const openZapTarget = (e: MouseEvent) => { - e.stopPropagation() - if (isEventZap) { - if (targetEvent) { - navigateToNote(toNote(targetEvent), targetEvent) - } else if (zapInfo.eventId) { - navigateToNote(toNote(zapInfo.eventId)) - } - } else if (isProfileZap && actualRecipientPubkey) { - push(toProfile(actualRecipientPubkey)) - } - } + const inQuietMode = targetEvent ? shouldHideInteractions(targetEvent) : false - if (variant === 'compact') { - const hasMetaLine = - (recipientPubkey && recipientPubkey !== senderPubkey) || isEventZap || isProfileZap + if (inQuietMode) { + return null + } + if (!zapInfo || !zapInfo.senderPubkey) { return ( -
-
- - {t('Superchat')} -
- {hasMetaLine ? ( -
- {recipientPubkey && recipientPubkey !== senderPubkey && ( - - {t('zapped')}{' '} - - - )} - {(isEventZap || isProfileZap) && ( - - )} -
- ) : null} - {comment ? ( -

- {comment} -

- ) : null} +
+ [{t('Invalid zap receipt')}]
) } - return ( -
- + const { senderPubkey, recipientPubkey, amount, comment } = zapInfo -
-
- -
-
- {!omitSenderHeading && ( -
- - - {t('Superchat')} - {recipientPubkey && recipientPubkey !== senderPubkey && ( - - {t('zapped')} - - - - )} -
- )} + const openZapTarget = (e: MouseEvent) => { + e.stopPropagation() + if (isEventZap && zapInfo?.eventId) { + openNoteFromFetchOrCache(navigateToNote, zapInfo.eventId, targetEvent) + } else if (isProfileZap && actualRecipientPubkey) { + push(toProfile(actualRecipientPubkey)) + } + } - {comment ? ( -
-

- {comment} -

-
- ) : null} + const hasMetaLine = + (recipientPubkey && recipientPubkey !== senderPubkey) || isEventZap || isProfileZap -
- - {formatAmount(amount)} + return ( +
+ {hasMetaLine ? ( +
+ {recipientPubkey && recipientPubkey !== senderPubkey && ( + + {t('zapped')}{' '} + - {t('sats')} -
+ )} + {(isEventZap || isProfileZap) && ( + + )}
+ ) : null} +
+ + {t('Superchat')} + {amount != null ? ( + + {formatAmount(amount)} {t('sats')} + + ) : null}
- + {comment ? ( + + ) : null} +
) } - diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index c500af99..9d02837e 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -250,10 +250,17 @@ export default function Note({ const { navigateToNote } = useSmartNoteNavigationOptional() const screenSize = useScreenSizeOptional() const isSmallScreen = screenSize?.isSmallScreen ?? false - const parentEventId = useMemo( - () => (hideParentNotePreview ? undefined : getParentBech32Id(event)), - [event, hideParentNotePreview] - ) + const parentEventId = useMemo(() => { + if (hideParentNotePreview) return undefined + if ( + event.kind === ExtendedKind.PAYMENT_NOTIFICATION || + event.kind === ExtendedKind.ZAP_RECEIPT || + event.kind === ExtendedKind.ZAP_REQUEST + ) { + return undefined + } + return getParentBech32Id(event) + }, [event, hideParentNotePreview]) const parentFetchRelayHints = useMemo(() => relayHintsFromEventTags(event), [event]) const contentPolicy = useContentPolicyOptional() const defaultShowNsfw = contentPolicy?.defaultShowNsfw ?? true @@ -683,24 +690,43 @@ export default function Note({ maxFileSizeKb={showFull ? 2048 : 500} deferRemoteAvatar={deferAuthorAvatar} /> -
-
+ {showFull ? ( +
+
+ + +
+
+ + +
+
+ ) : ( +
+ + + +
-
- - -
-
+ )} )}
diff --git a/src/components/PaytoDialog/index.tsx b/src/components/PaytoDialog/index.tsx index 27546e0e..5f9032bb 100644 --- a/src/components/PaytoDialog/index.tsx +++ b/src/components/PaytoDialog/index.tsx @@ -188,7 +188,7 @@ export default function PaytoDialog({ @@ -206,7 +206,8 @@ export default function PaytoDialog({ -
+
+
{isLightning && open ? (
)} +
{canOfferPostPayment ? ( - + ) : ( diff --git a/src/components/Profile/ProfileWallSuperchats.tsx b/src/components/Profile/ProfileWallSuperchats.tsx index d387e8d9..9c0ed80a 100644 --- a/src/components/Profile/ProfileWallSuperchats.tsx +++ b/src/components/Profile/ProfileWallSuperchats.tsx @@ -32,9 +32,9 @@ export default function ProfileWallSuperchats({
{superchats.map((event) => event.kind === ExtendedKind.PAYMENT_NOTIFICATION ? ( - + ) : ( - + ) )}
diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index 7b4923db..43d297e0 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -547,6 +547,7 @@ export default function Profile({
{/* Display websites - show first one prominently, others below */} diff --git a/src/components/ProfileAbout/index.tsx b/src/components/ProfileAbout/index.tsx index 240a3831..3b661e9b 100644 --- a/src/components/ProfileAbout/index.tsx +++ b/src/components/ProfileAbout/index.tsx @@ -18,7 +18,16 @@ import { EmbeddedWebsocketUrl } from '../Embedded' -export default function ProfileAbout({ about, className }: { about?: string; className?: string }) { +export default function ProfileAbout({ + about, + className, + profilePubkey +}: { + about?: string + className?: string + /** Profile owner pubkey — enables post-payment / message flow on payto links. */ + profilePubkey?: string +}) { const normalized = replaceStandardEmojiShortcodesInContent(about ?? '', []) if (!normalized.trim()) return null @@ -56,7 +65,7 @@ export default function ProfileAbout({ about, className }: { about?: string; cla } if (node.type === 'payto') { return ( - + ) } if (node.type === 'hashtag') { @@ -119,7 +128,7 @@ export default function ProfileAbout({ about, className }: { about?: string; cla const label = String(token.text ?? href) if (href.startsWith('payto://')) { out.push( - + {label} ) diff --git a/src/components/ReplyNote/index.tsx b/src/components/ReplyNote/index.tsx index 4e77e4fc..3ed80c6d 100644 --- a/src/components/ReplyNote/index.tsx +++ b/src/components/ReplyNote/index.tsx @@ -163,7 +163,10 @@ export default function ReplyNote({
- ) : parentEventId ? ( + ) : parentEventId && + event.kind !== kinds.Zap && + event.kind !== ExtendedKind.PAYMENT_NOTIFICATION && + event.kind !== ExtendedKind.ZAP_RECEIPT ? ( ) : event.kind === kinds.Zap ? ( - + ) : event.kind === ExtendedKind.PAYMENT_NOTIFICATION ? ( - + ) : isNip18RepostKind(event.kind) ? null : ( { + if (attested || checking || publishing) return checkLogin(async () => { setPublishing(true) try { const draft = await createPaymentAttestationDraftEvent(event, { addClientTag: true }) - await publish(draft, { disableFallbacks: true }) + const published = await publish(draft, { disableFallbacks: true }) + if (published) { + markAttested(published) + } requestProfileWallRefresh(recipientPubkey) showSimplePublishSuccess(t('Superchat attested')) } catch (error) { diff --git a/src/hooks/usePaymentAttestationStatus.tsx b/src/hooks/usePaymentAttestationStatus.tsx index a50c8a8f..9de3a8f8 100644 --- a/src/hooks/usePaymentAttestationStatus.tsx +++ b/src/hooks/usePaymentAttestationStatus.tsx @@ -5,8 +5,26 @@ import { getSuperchatPaymentRecipientPubkey } from '@/lib/superchat' import client from '@/services/client.service' +import indexedDb from '@/services/indexed-db.service' import { Event as NostrEvent } from 'nostr-tools' -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react' + +function attestationFilter(recipientPubkey: string, targetEventId: string) { + return { + kinds: [ExtendedKind.PAYMENT_ATTESTATION], + authors: [recipientPubkey], + '#e': [targetEventId], + limit: 5 + } +} + +function resolveAttestationMatch( + attestations: NostrEvent[], + targetEventId: string, + recipientPubkey: string +): NostrEvent | undefined { + return findPaymentAttestationForTarget(attestations, targetEventId, recipientPubkey) +} export function usePaymentAttestationStatus(targetEvent: NostrEvent | undefined) { const [attested, setAttested] = useState(false) @@ -16,60 +34,85 @@ export function usePaymentAttestationStatus(targetEvent: NostrEvent | undefined) const recipientPubkey = targetEvent ? getSuperchatPaymentRecipientPubkey(targetEvent) : null const targetId = targetEvent?.id?.toLowerCase() - useEffect(() => { + const filter = useMemo( + () => + targetEvent?.id && recipientPubkey + ? attestationFilter(recipientPubkey, targetEvent.id) + : null, + [targetEvent?.id, recipientPubkey] + ) + + const applyMatch = useCallback((match: NostrEvent | undefined) => { + if (!match) return + setAttestationEvent(match) + setAttested(true) + }, []) + + const markAttested = useCallback( + (attestation: NostrEvent) => { + if (!targetEvent?.id || !recipientPubkey) return + if (attestation.kind !== ExtendedKind.PAYMENT_ATTESTATION) return + if (attestation.pubkey.toLowerCase() !== recipientPubkey.toLowerCase()) return + const attestedId = getPaymentAttestationTargetId(attestation) + if (attestedId?.toLowerCase() !== targetEvent.id.toLowerCase()) return + applyMatch(attestation) + }, + [applyMatch, recipientPubkey, targetEvent?.id] + ) + + useLayoutEffect(() => { setAttested(false) setAttestationEvent(null) - if (!targetEvent?.id || !recipientPubkey) return + if (!targetEvent?.id || !recipientPubkey || !filter) return + + const sessionHits = client.eventService.getSessionEventsMatchingFilters([filter], 5) + applyMatch(resolveAttestationMatch(sessionHits, targetEvent.id, recipientPubkey)) + }, [applyMatch, filter, recipientPubkey, targetEvent?.id]) + + useEffect(() => { + if (!targetEvent?.id || !recipientPubkey || !filter) return let cancelled = false setChecking(true) - void client - .fetchEvents( - [], - { - kinds: [ExtendedKind.PAYMENT_ATTESTATION], - authors: [recipientPubkey], - '#e': [targetEvent.id], - limit: 5 - }, - { cache: true, eoseTimeout: 4000, globalTimeout: 10_000 } - ) - .then((attestations) => { + void (async () => { + try { + const [idbAttestations, localFeedAttestations, relayAttestations] = await Promise.all([ + indexedDb.getPaymentAttestationsForTargetEvent(targetEvent.id, 20), + client.getLocalFeedEvents([{ urls: [], filter }], { maxMatches: 5 }), + client.fetchEvents([], filter, { + cache: true, + eoseTimeout: 4000, + globalTimeout: 10_000 + }) + ]) + if (cancelled) return - const match = findPaymentAttestationForTarget(attestations, targetEvent.id, recipientPubkey) - setAttestationEvent(match ?? null) - setAttested(Boolean(match)) - }) - .catch(() => { + + const merged = [...idbAttestations, ...localFeedAttestations, ...relayAttestations] + applyMatch(resolveAttestationMatch(merged, targetEvent.id, recipientPubkey)) + } catch { /* optional */ - }) - .finally(() => { + } finally { if (!cancelled) setChecking(false) - }) + } + })() return () => { cancelled = true } - }, [targetEvent, recipientPubkey, targetId]) + }, [applyMatch, filter, recipientPubkey, targetEvent?.id, targetId]) useEffect(() => { if (!targetEvent?.id || !recipientPubkey) return const handleAttestation = (data: globalThis.Event) => { - const evt = (data as CustomEvent).detail - if (!evt || evt.kind !== ExtendedKind.PAYMENT_ATTESTATION) return - if (evt.pubkey.toLowerCase() !== recipientPubkey.toLowerCase()) return - const attestedId = getPaymentAttestationTargetId(evt) - if (attestedId?.toLowerCase() === targetEvent.id.toLowerCase()) { - setAttested(true) - setAttestationEvent(evt) - } + markAttested((data as CustomEvent).detail) } client.addEventListener('newEvent', handleAttestation) return () => client.removeEventListener('newEvent', handleAttestation) - }, [targetEvent?.id, recipientPubkey]) + }, [markAttested, targetEvent?.id, recipientPubkey]) - return { attested, attestationEvent, checking, recipientPubkey } + return { attested, attestationEvent, checking, recipientPubkey, markAttested } } diff --git a/src/lib/feed-local-event-match.ts b/src/lib/feed-local-event-match.ts index 68bcc9c3..500fd126 100644 --- a/src/lib/feed-local-event-match.ts +++ b/src/lib/feed-local-event-match.ts @@ -15,8 +15,9 @@ function valuesMatchTag(tagName: string, eventValues: string[], filterValues: un export function eventMatchesLocalFeedFilter(event: Event, filter: Filter): boolean { if (Array.isArray(filter.ids) && filter.ids.length > 0 && !filter.ids.includes(event.id)) return false - if (Array.isArray(filter.authors) && filter.authors.length > 0 && !filter.authors.includes(event.pubkey)) { - return false + if (Array.isArray(filter.authors) && filter.authors.length > 0) { + const allowedAuthors = new Set(filter.authors.map((author) => author.toLowerCase())) + if (!allowedAuthors.has(event.pubkey.toLowerCase())) return false } if (Array.isArray(filter.kinds) && filter.kinds.length > 0 && !filter.kinds.includes(event.kind)) return false if (typeof filter.since === 'number' && event.created_at < filter.since) return false diff --git a/src/lib/navigation-related-events.ts b/src/lib/navigation-related-events.ts index 1b052fb7..f3ef5c11 100644 --- a/src/lib/navigation-related-events.ts +++ b/src/lib/navigation-related-events.ts @@ -1,4 +1,5 @@ import { getParentBech32Id, getRootBech32Id } from '@/lib/event' +import { toNote } from '@/lib/link' import client from '@/services/client.service' import type { Event } from 'nostr-tools' @@ -18,3 +19,25 @@ export function getCachedThreadContextEvents(forEvent: Event): Event[] { tryAdd(getRootBech32Id(forEvent)) return [...byId.values()] } + +export type NavigateToNoteFn = (url: string, event?: Event, relatedEvents?: Event[]) => void + +/** Prefer a fetched event, else the session cache — same seeding as parent preview clicks in feeds. */ +export function resolveCachedNoteEvent(fetched: Event | undefined, noteId?: string): Event | undefined { + if (fetched) return fetched + if (!noteId?.trim()) return undefined + return client.peekSessionCachedEvent(noteId.trim()) +} + +export function openNoteFromFetchOrCache( + navigateToNote: NavigateToNoteFn, + noteId: string, + fetched?: Event +): void { + const resolved = resolveCachedNoteEvent(fetched, noteId) + if (resolved) { + navigateToNote(toNote(resolved), resolved, getCachedThreadContextEvents(resolved)) + return + } + navigateToNote(toNote(noteId)) +}