You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

108 lines
4.4 KiB

import {
getParentEventHexId,
getQuotedEventHexIdFromQTags,
getRootATag,
getRootEventHexId,
isNip25ReactionKind,
kind1QuotesThreadRoot
} from '@/lib/event'
import { getZapInfoFromEvent } from '@/lib/event-metadata'
import { getFirstHexEventIdFromETags } from '@/lib/tag'
import {
canonicalizeRssArticleUrl,
getArticleUrlFromCommentITags,
getHighlightSourceHttpUrl
} from '@/lib/rss-article'
import client from '@/services/client.service'
import type { Event } 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. */
export type TThreadRootRef =
| { type: 'E'; id: string; pubkey: string }
| { type: 'A'; id: string; eventId: string; pubkey: string; relay?: string }
| { type: 'I'; id: string }
/** Whether a newly published/fetched reply belongs to the thread rooted at `root`. */
export function eventReplyMatchesThreadRoot(evt: Event, root: TThreadRootRef): boolean {
if (root.type === 'I') {
const u = getArticleUrlFromCommentITags(evt)
if (u && canonicalizeRssArticleUrl(u) === canonicalizeRssArticleUrl(root.id)) return true
if (evt.kind === kinds.Highlights) {
const hu = getHighlightSourceHttpUrl(evt)
return !!hu && canonicalizeRssArticleUrl(hu) === canonicalizeRssArticleUrl(root.id)
}
return false
}
if (root.type === 'A') {
const coord = getRootATag(evt)?.[1]
if (coord === root.id) return true
const rootHex = getRootEventHexId(evt)
if (rootHex && (rootHex === root.eventId || rootHex === root.id)) return true
return kind1QuotesThreadRoot(evt, root)
}
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)
}
/**
* Whether `evt` should appear in the reply list for note `opEvent` with thread root `root`.
* Stricter than treating any kind-1 with an `e` tag as a reply: requires thread root / #q to match (so notes that only
* tag the quoted inner note as `e`+`root` do not show under the quoter's thread).
* For quote posts, also drops kind-1 replies whose **parent** is the embedded quoted id but not the OP.
*/
export function replyBelongsToNoteThread(evt: Event, opEvent: Event, root: TThreadRootRef): boolean {
if (root.type === 'I') {
return eventReplyMatchesThreadRoot(evt, root)
}
if (!eventReplyMatchesThreadRoot(evt, root)) return false
if (root.type === 'A') return true
if (opEvent.kind !== kinds.ShortTextNote) return true
const quotedHex = getQuotedEventHexIdFromQTags(opEvent)?.toLowerCase()
if (!quotedHex) return true
const parentHex = getParentEventHexId(evt)?.toLowerCase()
if (!parentHex) return true
const rootId = root.id.trim().toLowerCase()
if (parentHex === quotedHex && parentHex !== rootId) return false
return true
}