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
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 |
|
}
|
|
|