Browse Source

add responses to zaps and reactions in the reply thread

imwald
Silberengel 1 month ago
parent
commit
1bfd1dfbad
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 19
      src/components/ContentPreview/index.tsx
  4. 19
      src/components/Note/ReactionEmojiDisplay.tsx
  5. 34
      src/components/Note/Zap.tsx
  6. 28
      src/components/ParentNotePreview/index.tsx
  7. 30
      src/components/ReplyNote/index.tsx
  8. 3
      src/components/ReplyNoteList/index.tsx
  9. 2
      src/constants.ts
  10. 45
      src/lib/thread-reply-root-match.ts
  11. 12
      src/providers/ReplyProvider.tsx
  12. 4
      src/services/client-events.service.ts
  13. 27
      src/services/note-stats.service.ts

4
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "jumble-imwald", "name": "jumble-imwald",
"version": "21.0.0", "version": "21.0.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "jumble-imwald", "name": "jumble-imwald",
"version": "21.0.0", "version": "21.0.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@asciidoctor/core": "^3.0.4", "@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "jumble-imwald", "name": "jumble-imwald",
"version": "21.0.0", "version": "21.0.2",
"description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble", "description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble",
"private": true, "private": true,
"type": "module", "type": "module",

19
src/components/ContentPreview/index.tsx

@ -31,6 +31,7 @@ import ApplicationHandlerRecommendation from '../ApplicationHandlerRecommendatio
import FollowPackPreview from './FollowPackPreview' import FollowPackPreview from './FollowPackPreview'
import ReactionEmojiDisplay from '../Note/ReactionEmojiDisplay' import ReactionEmojiDisplay from '../Note/ReactionEmojiDisplay'
import NoteKindLabel from '../Note/NoteKindLabel' import NoteKindLabel from '../Note/NoteKindLabel'
import Zap from '../Note/Zap'
import GitRepublicEventCard from '../Note/GitRepublicEventCard' import GitRepublicEventCard from '../Note/GitRepublicEventCard'
/** Inert event so hooks can run before `event` is defined. */ /** Inert event so hooks can run before `event` is defined. */
@ -62,10 +63,13 @@ function splitPreviewLayoutClasses(className?: string) {
export default function ContentPreview({ export default function ContentPreview({
event, event,
className className,
/** Inline parent lines (e.g. reply thread): zap receipts match compact thread styling. */
previewDensity
}: { }: {
event?: Event event?: Event
className?: string className?: string
previewDensity?: 'default' | 'compact'
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const reactionDisplay = useNotificationReactionDisplay(event ?? CONTENT_PREVIEW_HOOK_PLACEHOLDER) const reactionDisplay = useNotificationReactionDisplay(event ?? CONTENT_PREVIEW_HOOK_PLACEHOLDER)
@ -168,7 +172,18 @@ export default function ContentPreview({
return withKindRow(<LiveEventPreview event={event} />) return withKindRow(<LiveEventPreview event={event} />)
} }
if (event.kind === ExtendedKind.ZAP_REQUEST || event.kind === ExtendedKind.ZAP_RECEIPT) { if (event.kind === ExtendedKind.ZAP_REQUEST) {
return withKindRow(<ZapPreview event={event} />)
}
if (event.kind === ExtendedKind.ZAP_RECEIPT || event.kind === kinds.Zap) {
if (previewDensity === 'compact') {
return (
<div className={cn('min-w-0', previewOuter)}>
<Zap event={event} variant="compact" omitSenderHeading className={previewBody} />
</div>
)
}
return withKindRow(<ZapPreview event={event} />) return withKindRow(<ZapPreview event={event} />)
} }

19
src/components/Note/ReactionEmojiDisplay.tsx

@ -21,8 +21,8 @@ export default function ReactionEmojiDisplay({
className?: string className?: string
/** Truncate long reaction text beyond this length */ /** Truncate long reaction text beyond this length */
maxRawLength?: number maxRawLength?: number
/** Compact row (notification list at-a-glance) */ /** Compact row (notification list); `thread` matches reply-list density */
variant?: 'default' | 'compact' variant?: 'default' | 'compact' | 'thread'
}) { }) {
const sync = useMemo( const sync = useMemo(
() => resolveReactionEmojiSync(event, maxRawLength), () => resolveReactionEmojiSync(event, maxRawLength),
@ -69,10 +69,17 @@ export default function ReactionEmojiDisplay({
emoji={value} emoji={value}
classNames={{ classNames={{
img: img:
variant === 'compact' variant === 'thread'
? 'size-4 max-h-[1em] w-auto rounded-sm' ? 'size-3.5 max-h-[1em] w-auto rounded-sm opacity-90'
: 'size-7 max-h-[1.5em] w-auto rounded-sm', : variant === 'compact'
text: variant === 'compact' ? 'text-base leading-none' : 'text-2xl leading-none' ? 'size-4 max-h-[1em] w-auto rounded-sm'
: 'size-7 max-h-[1.5em] w-auto rounded-sm',
text:
variant === 'thread'
? 'text-sm leading-none'
: variant === 'compact'
? 'text-base leading-none'
: 'text-2xl leading-none'
}} }}
/> />
</span> </span>

34
src/components/Note/Zap.tsx

@ -46,8 +46,10 @@ export default function Zap({
return ( return (
<div <div
className={cn( className={cn(
'text-sm text-muted-foreground rounded-lg border border-border bg-muted/20', 'text-sm text-muted-foreground',
variant === 'compact' ? 'px-3 py-2' : 'p-4', variant === 'compact'
? 'py-0.5'
: 'rounded-lg border border-border bg-muted/20 p-4',
className className
)} )}
> >
@ -89,27 +91,25 @@ export default function Zap({
if (variant === 'compact') { if (variant === 'compact') {
return ( return (
<div <div className={cn('text-sm text-muted-foreground', className)}>
className={cn( <div className="flex flex-wrap items-center gap-x-1.5 gap-y-0.5">
'rounded-md border-l-2 border-primary/50 bg-primary/[0.06] pl-3 pr-2 py-2 text-sm text-foreground dark:bg-primary/[0.08]', <ZapIcon className="size-3.5 shrink-0 opacity-70" strokeWidth={2} aria-hidden />
className <span className="tabular-nums font-medium text-foreground/90">{formatAmount(amount)}</span>
)} <span>{t('sats')}</span>
>
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
<ZapIcon className="size-4 shrink-0 text-primary" strokeWidth={2} aria-hidden />
<span className="font-semibold tabular-nums text-foreground">{formatAmount(amount)}</span>
<span className="text-muted-foreground">{t('sats')}</span>
{recipientPubkey && recipientPubkey !== senderPubkey && ( {recipientPubkey && recipientPubkey !== senderPubkey && (
<span className="text-muted-foreground text-xs"> <span className="text-xs">
<span className="text-foreground/80">{t('zapped')}</span>{' '} <span>{t('zapped')}</span>{' '}
<Username userId={recipientPubkey} className="inline font-medium text-foreground" /> <Username
userId={recipientPubkey}
className="inline font-medium text-foreground/85 hover:text-foreground"
/>
</span> </span>
)} )}
{(isEventZap || isProfileZap) && ( {(isEventZap || isProfileZap) && (
<button <button
type="button" type="button"
onClick={openZapTarget} onClick={openZapTarget}
className="text-xs font-medium text-primary hover:underline" className="text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
> >
{isEventZap {isEventZap
? t('Zapped note') ? t('Zapped note')
@ -120,7 +120,7 @@ export default function Zap({
)} )}
</div> </div>
{comment ? ( {comment ? (
<p className="mt-2 text-sm leading-snug text-foreground/90 whitespace-pre-wrap break-words"> <p className="mt-1.5 pl-5 text-sm leading-snug text-muted-foreground whitespace-pre-wrap break-words">
{comment} {comment}
</p> </p>
) : null} ) : null}

28
src/components/ParentNotePreview/index.tsx

@ -13,11 +13,14 @@ import logger from '@/lib/logger'
export default function ParentNotePreview({ export default function ParentNotePreview({
eventId, eventId,
className, className,
onClick onClick,
/** Inline hint without pill background (e.g. reply thread rows). */
appearance = 'default'
}: { }: {
eventId: string eventId: string
className?: string className?: string
onClick?: React.MouseEventHandler<HTMLDivElement> | undefined onClick?: React.MouseEventHandler<HTMLDivElement> | undefined
appearance?: 'default' | 'subtle'
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { event, isFetching } = useFetchEvent(eventId) const { event, isFetching } = useFetchEvent(eventId)
@ -66,18 +69,17 @@ export default function ParentNotePreview({
const finalEvent = event || fallbackEvent const finalEvent = event || fallbackEvent
const finalIsFetching = isFetching || isFetchingFallback const finalIsFetching = isFetching || isFetchingFallback
const shellClass =
appearance === 'subtle'
? 'flex gap-1.5 items-center text-xs w-full max-w-full text-muted-foreground'
: 'flex gap-1 items-center text-sm rounded-full px-2 bg-muted w-fit max-w-full text-muted-foreground'
if (finalIsFetching) { if (finalIsFetching) {
return ( return (
<div <div data-parent-note-preview className={cn(shellClass, appearance === 'subtle' && 'w-full', className)}>
data-parent-note-preview
className={cn(
'flex gap-1 items-center text-sm rounded-full px-2 bg-muted w-44 max-w-full text-muted-foreground',
className
)}
>
<div className="shrink-0">{t('reply to')}</div> <div className="shrink-0">{t('reply to')}</div>
<Skeleton className="w-4 h-4 rounded-full" /> <Skeleton className="w-4 h-4 rounded-full" />
<div className="py-1 flex-1"> <div className={cn('flex-1 min-w-0', appearance === 'subtle' ? 'py-0' : 'py-1')}>
<Skeleton className="h-3" /> <Skeleton className="h-3" />
</div> </div>
</div> </div>
@ -99,7 +101,7 @@ export default function ParentNotePreview({
<div <div
data-parent-note-preview data-parent-note-preview
className={cn( className={cn(
'flex gap-1 items-center text-sm rounded-full px-2 bg-muted w-fit max-w-full text-muted-foreground', shellClass,
(finalEvent || (!finalEvent && !finalIsFetching)) && 'hover:text-foreground cursor-pointer', (finalEvent || (!finalEvent && !finalIsFetching)) && 'hover:text-foreground cursor-pointer',
className className
)} )}
@ -108,7 +110,11 @@ export default function ParentNotePreview({
<div className="shrink-0">{t('reply to')}</div> <div className="shrink-0">{t('reply to')}</div>
{finalEvent && <UserAvatar className="shrink-0" userId={finalEvent.pubkey} size="tiny" />} {finalEvent && <UserAvatar className="shrink-0" userId={finalEvent.pubkey} size="tiny" />}
<div className="truncate flex-1 min-w-0"> <div className="truncate flex-1 min-w-0">
<ContentPreview className="pointer-events-none" event={finalEvent} /> <ContentPreview
className="pointer-events-none"
event={finalEvent}
previewDensity={appearance === 'subtle' ? 'compact' : 'default'}
/>
</div> </div>
</div> </div>
) )

30
src/components/ReplyNote/index.tsx

@ -15,6 +15,7 @@ import { getZapInfoFromEvent } from '@/lib/event-metadata'
import { isMentioningMutedUsers, isNip25ReactionKind } from '@/lib/event' import { isMentioningMutedUsers, isNip25ReactionKind } from '@/lib/event'
import { getWebExternalReactionTargetUrl } from '@/lib/rss-article' import { getWebExternalReactionTargetUrl } from '@/lib/rss-article'
import { toNote } from '@/lib/link' import { toNote } from '@/lib/link'
import { cn } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/contexts/mute-list-context' import { useMuteList } from '@/contexts/mute-list-context'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
@ -126,14 +127,23 @@ export default function ReplyNote({
<NoteOptions event={event} className="shrink-0 [&_svg]:size-5" /> <NoteOptions event={event} className="shrink-0 [&_svg]:size-5" />
</div> </div>
</div> </div>
<NoteKindLabel kind={event.kind} event={event} size="small" className="mt-0.5" /> <NoteKindLabel
kind={event.kind}
event={event}
size="small"
className={cn(
'mt-0.5',
(isNip25ReactionKind(event.kind) || event.kind === kinds.Zap) && 'opacity-60'
)}
/>
{webReactionParentUrl ? ( {webReactionParentUrl ? (
<div className="mt-2 not-prose max-w-full" data-parent-note-preview> <div className="mt-1.5 not-prose max-w-full" data-parent-note-preview>
<WebPreview url={webReactionParentUrl} className="w-full" /> <WebPreview url={webReactionParentUrl} className="w-full" />
</div> </div>
) : parentEventId ? ( ) : parentEventId ? (
<ParentNotePreview <ParentNotePreview
className="mt-2" appearance="subtle"
className="mt-1.5"
eventId={parentEventId} eventId={parentEventId}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
@ -143,24 +153,24 @@ export default function ReplyNote({
) : null} ) : null}
{show ? ( {show ? (
isNip25ReactionKind(event.kind) ? ( isNip25ReactionKind(event.kind) ? (
<div className="mt-2 flex flex-wrap items-center gap-2 text-sm text-muted-foreground"> <div className="mt-1.5 flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-sm text-muted-foreground">
{reactionDisplay.status === 'pending' ? ( {reactionDisplay.status === 'pending' ? (
<Skeleton className="size-4 shrink-0 rounded-sm" aria-hidden /> <Skeleton className="size-3.5 shrink-0 rounded-sm" aria-hidden />
) : reactionDisplay.status === 'vote_up' ? ( ) : reactionDisplay.status === 'vote_up' ? (
<span className="text-base leading-none" aria-hidden> <span className="text-sm leading-none opacity-90" aria-hidden>
{DISCUSSION_UPVOTE_DISPLAY} {DISCUSSION_UPVOTE_DISPLAY}
</span> </span>
) : reactionDisplay.status === 'vote_down' ? ( ) : reactionDisplay.status === 'vote_down' ? (
<span className="text-base leading-none" aria-hidden> <span className="text-sm leading-none opacity-90" aria-hidden>
{DISCUSSION_DOWNVOTE_DISPLAY} {DISCUSSION_DOWNVOTE_DISPLAY}
</span> </span>
) : ( ) : (
<ReactionEmojiDisplay event={event} variant="compact" maxRawLength={64} /> <ReactionEmojiDisplay event={event} variant="thread" maxRawLength={64} />
)} )}
<span>{t(notificationReactionSummaryKey(reactionDisplay))}</span> <span className="text-foreground/85">{t(notificationReactionSummaryKey(reactionDisplay))}</span>
</div> </div>
) : event.kind === kinds.Zap ? ( ) : event.kind === kinds.Zap ? (
<Zap className="mt-2" event={event} omitSenderHeading variant="compact" /> <Zap className="mt-1.5" event={event} omitSenderHeading variant="compact" />
) : ( ) : (
<MarkdownArticle <MarkdownArticle
className="mt-2" className="mt-2"

3
src/components/ReplyNoteList/index.tsx

@ -382,9 +382,8 @@ function ReplyNoteList({
replyEvents.push(evt) replyEvents.push(evt)
}) })
// Prevent infinite loops by tracking processed event IDs // Include reactions (and every other kind) so BFS can find notes keyed under reaction / zap ids.
const newParentEventKeys = events const newParentEventKeys = events
.filter((evt) => !isNip25ReactionKind(evt.kind))
.map((evt) => evt.id) .map((evt) => evt.id)
.filter((id) => !processedEventIds.has(id)) .filter((id) => !processedEventIds.has(id))

2
src/constants.ts

@ -270,8 +270,6 @@ export const SEARCHABLE_RELAY_URLS = [
'wss://relay.wikifreedia.xyz', 'wss://relay.wikifreedia.xyz',
'wss://nostr.einundzwanzig.space', 'wss://nostr.einundzwanzig.space',
'wss://nostrelites.org', 'wss://nostrelites.org',
'wss://relay.nsec.app',
'wss://bucket.coracle.social',
'wss://spatia-arcana.com', 'wss://spatia-arcana.com',
'wss://nostr-pub.wellorder.net', 'wss://nostr-pub.wellorder.net',
'wss://pyramid.fiatjaf.com/', 'wss://pyramid.fiatjaf.com/',

45
src/lib/thread-reply-root-match.ts

@ -3,16 +3,55 @@ import {
getQuotedEventHexIdFromQTags, getQuotedEventHexIdFromQTags,
getRootATag, getRootATag,
getRootEventHexId, getRootEventHexId,
isNip25ReactionKind,
kind1QuotesThreadRoot kind1QuotesThreadRoot
} from '@/lib/event' } from '@/lib/event'
import { getZapInfoFromEvent } from '@/lib/event-metadata'
import { getFirstHexEventIdFromETags } from '@/lib/tag'
import { import {
canonicalizeRssArticleUrl, canonicalizeRssArticleUrl,
getArticleUrlFromCommentITags, getArticleUrlFromCommentITags,
getHighlightSourceHttpUrl getHighlightSourceHttpUrl
} from '@/lib/rss-article' } from '@/lib/rss-article'
import client from '@/services/client.service'
import type { Event } from 'nostr-tools' import type { Event } from 'nostr-tools'
import { kinds } from 'nostr-tools' import { kinds } from 'nostr-tools'
/** Reply whose direct parent is a zap receipt for this thread root (hex id). */
function replyParentIsZapToRootHex(reply: Event, rootHexLower: string): boolean {
const parentHex = getParentEventHexId(reply)
if (!parentHex || !/^[0-9a-f]{64}$/i.test(parentHex)) return false
const pl = parentHex.toLowerCase()
if (pl === rootHexLower) return false
const parentEv = client.peekSessionCachedEvent(pl)
if (!parentEv || parentEv.kind !== kinds.Zap) return false
const zapped = getZapInfoFromEvent(parentEv)?.originalEventId
return (
!!zapped &&
/^[0-9a-f]{64}$/i.test(zapped) &&
zapped.toLowerCase() === rootHexLower
)
}
function reactionTargetNoteHex(reaction: Event): string | undefined {
const fromParent = getParentEventHexId(reaction)
if (fromParent && /^[0-9a-f]{64}$/i.test(fromParent)) return fromParent.toLowerCase()
const first = getFirstHexEventIdFromETags(reaction.tags)
if (first && /^[0-9a-f]{64}$/i.test(first)) return first.toLowerCase()
return undefined
}
/** Reply whose direct parent is a NIP-25 / kind-17 reaction to this thread root note. */
function replyParentIsReactionToRootHex(reply: Event, rootHexLower: string): boolean {
const parentHex = getParentEventHexId(reply)
if (!parentHex || !/^[0-9a-f]{64}$/i.test(parentHex)) return false
const pl = parentHex.toLowerCase()
if (pl === rootHexLower) return false
const parentEv = client.peekSessionCachedEvent(pl)
if (!parentEv || !isNip25ReactionKind(parentEv.kind)) return false
return reactionTargetNoteHex(parentEv) === rootHexLower
}
/** Matches `ReplyNoteList` / discussion thread root shapes. */ /** Matches `ReplyNoteList` / discussion thread root shapes. */
export type TThreadRootRef = export type TThreadRootRef =
| { type: 'E'; id: string; pubkey: string } | { type: 'E'; id: string; pubkey: string }
@ -37,7 +76,11 @@ export function eventReplyMatchesThreadRoot(evt: Event, root: TThreadRootRef): b
if (rootHex && (rootHex === root.eventId || rootHex === root.id)) return true if (rootHex && (rootHex === root.eventId || rootHex === root.id)) return true
return kind1QuotesThreadRoot(evt, root) return kind1QuotesThreadRoot(evt, root)
} }
if (getRootEventHexId(evt) === root.id) return true const rid = root.id.trim().toLowerCase()
const evtRootHex = getRootEventHexId(evt)?.toLowerCase()
if (evtRootHex === rid) return true
if (replyParentIsZapToRootHex(evt, rid)) return true
if (replyParentIsReactionToRootHex(evt, rid)) return true
return kind1QuotesThreadRoot(evt, root) return kind1QuotesThreadRoot(evt, root)
} }

12
src/providers/ReplyProvider.tsx

@ -11,6 +11,7 @@ import {
getRootETag, getRootETag,
isNip25ReactionKind isNip25ReactionKind
} from '@/lib/event' } from '@/lib/event'
import { getFirstHexEventIdFromETags } from '@/lib/tag'
import client from '@/services/client.service' import client from '@/services/client.service'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { createContext, useCallback, useContext, useState } from 'react' import { createContext, useCallback, useContext, useState } from 'react'
@ -40,7 +41,16 @@ export function ReplyProvider({ children }: { children: React.ReactNode }) {
const newReplyEventMap = new Map<string, Event[]>() const newReplyEventMap = new Map<string, Event[]>()
replies.forEach((reply) => { replies.forEach((reply) => {
if (newReplyIdSet.has(reply.id)) return if (newReplyIdSet.has(reply.id)) return
if (isNip25ReactionKind(reply.kind)) return if (isNip25ReactionKind(reply.kind)) {
newReplyIdSet.add(reply.id)
client.addEventToCache(reply)
const targetHex = getFirstHexEventIdFromETags(reply.tags)
if (targetHex && /^[0-9a-f]{64}$/i.test(targetHex)) {
const key = targetHex.toLowerCase()
newReplyEventMap.set(key, [...(newReplyEventMap.get(key) || []), reply])
}
return
}
newReplyIdSet.add(reply.id) newReplyIdSet.add(reply.id)
client.addEventToCache(reply) client.addEventToCache(reply)

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

@ -512,8 +512,8 @@ export class EventService {
if (shouldDropEventOnIngest(ev)) continue if (shouldDropEventOnIngest(ev)) continue
const threadishKind1Quote = const threadishKind1Quote =
(root.type === 'E' || root.type === 'A') && kind1QuotesThreadRoot(ev, root) (root.type === 'E' || root.type === 'A') && kind1QuotesThreadRoot(ev, root)
if (!isReplyNoteEvent(ev) && !threadishKind1Quote) continue if (!isReplyNoteEvent(ev) && !threadishKind1Quote && !isNip25ReactionKind(ev.kind))
if (isNip25ReactionKind(ev.kind)) continue continue
if (seen.has(ev.id)) continue if (seen.has(ev.id)) continue
if (!linkRefs(ev).some((id) => threadKeys.has(id))) continue if (!linkRefs(ev).some((id) => threadKeys.has(id))) continue
out.push(ev) out.push(ev)

27
src/services/note-stats.service.ts

@ -5,7 +5,12 @@ import {
SEARCHABLE_RELAY_URLS SEARCHABLE_RELAY_URLS
} from '@/constants' } from '@/constants'
import { replaceStandardEmojiShortcodesInContent } from '@/lib/emoji-content' import { replaceStandardEmojiShortcodesInContent } from '@/lib/emoji-content'
import { getReplaceableCoordinateFromEvent, isNip18RepostKind, isReplaceableEvent } from '@/lib/event' import {
getParentEventHexId,
getReplaceableCoordinateFromEvent,
isNip18RepostKind,
isReplaceableEvent
} from '@/lib/event'
import { getZapInfoFromEvent } from '@/lib/event-metadata' import { getZapInfoFromEvent } from '@/lib/event-metadata'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { import {
@ -667,23 +672,11 @@ class NoteStatsService {
} }
} }
} else if (evt.kind === kinds.ShortTextNote) { } else if (evt.kind === kinds.ShortTextNote) {
const parentETag = evt.tags.find(([tagName, , , marker]) => { // Prefer NIP-10 reply parent (matches getParentETag), not the first of reply|root in tag order.
return tagName === 'e' && (marker === 'reply' || marker === 'root') const parentHex = getParentEventHexId(evt)
}) if (parentHex && /^[0-9a-f]{64}$/i.test(parentHex)) {
if (parentETag) { originalEventId = parentHex.toLowerCase()
originalEventId = parentETag[1]
} else {
const lastETag = evt.tags.findLast(
([tagName, tagValue, , marker]) =>
tagName === 'e' &&
!!tagValue &&
marker !== 'mention'
)
if (lastETag) {
originalEventId = lastETag[1]
}
} }
if (!originalEventId) { if (!originalEventId) {
const aTag = evt.tags.find(tagNameEquals('a')) const aTag = evt.tags.find(tagNameEquals('a'))
if (aTag) { if (aTag) {

Loading…
Cancel
Save