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.
144 lines
5.0 KiB
144 lines
5.0 KiB
import { ExtendedKind } from '@/constants' |
|
import { |
|
getParentEventHexId, |
|
getReplaceableCoordinateFromEvent, |
|
getRootEventHexId, |
|
isNip18RepostKind, |
|
isReplaceableEvent, |
|
isReplyNoteEvent, |
|
normalizeReplaceableCoordinateString, |
|
resolveDeclaredThreadRootEventHex |
|
} from '@/lib/event' |
|
import { kinds } from 'nostr-tools' |
|
import type { Event } from 'nostr-tools' |
|
|
|
/** Max `e` ids per REQ filter shard (relay limits). */ |
|
export const NOTIFICATION_THREAD_WATCH_E_CHUNK = 24 |
|
/** Max `a` coordinates per REQ filter shard. */ |
|
export const NOTIFICATION_THREAD_WATCH_A_CHUNK = 16 |
|
/** Cap stored refs driving live `#e` / `#a` shards (newest wins by list tag order). */ |
|
export const NOTIFICATION_THREAD_WATCH_MAX_E_IDS = 120 |
|
export const NOTIFICATION_THREAD_WATCH_MAX_A_COORDS = 60 |
|
|
|
export type TThreadWatchListRefs = { |
|
eHexLower: Set<string> |
|
aCoordLower: Set<string> |
|
} |
|
|
|
export function emptyThreadWatchRefs(): TThreadWatchListRefs { |
|
return { eHexLower: new Set<string>(), aCoordLower: new Set<string>() } |
|
} |
|
|
|
export function parseThreadWatchListRefs(ev: Event | null | undefined): TThreadWatchListRefs { |
|
const eHexLower = new Set<string>() |
|
const aCoordLower = new Set<string>() |
|
if (!ev?.tags) return { eHexLower, aCoordLower } |
|
for (const t of ev.tags) { |
|
if ((t[0] === 'e' || t[0] === 'E') && t[1] && /^[0-9a-f]{64}$/i.test(t[1])) { |
|
eHexLower.add(t[1].toLowerCase()) |
|
} |
|
if ((t[0] === 'a' || t[0] === 'A') && t[1]) { |
|
const n = normalizeReplaceableCoordinateString(t[1]) |
|
if (n) aCoordLower.add(n) |
|
} |
|
} |
|
return { eHexLower, aCoordLower } |
|
} |
|
|
|
function addResolvedHexCandidates(event: Event, into: Set<string>) { |
|
const add = (h?: string) => { |
|
if (!h || !/^[0-9a-f]{64}$/i.test(h)) return |
|
const L = h.toLowerCase() |
|
into.add(L) |
|
into.add(resolveDeclaredThreadRootEventHex(L)) |
|
} |
|
add(getRootEventHexId(event)) |
|
add(getParentEventHexId(event)) |
|
for (const t of event.tags) { |
|
if ((t[0] === 'e' || t[0] === 'E') && t[1] && /^[0-9a-f]{64}$/i.test(t[1])) { |
|
add(t[1]) |
|
} |
|
} |
|
} |
|
|
|
function listNormalizedACoordsFromEvent(event: Event): string[] { |
|
const out: string[] = [] |
|
for (const t of event.tags) { |
|
if ((t[0] === 'a' || t[0] === 'A') && t[1]) { |
|
const n = normalizeReplaceableCoordinateString(t[1]) |
|
if (n) out.push(n) |
|
} |
|
} |
|
return [...new Set(out)] |
|
} |
|
|
|
export function threadWatchMatchesRefs( |
|
event: Event, |
|
refs: TThreadWatchListRefs |
|
): boolean { |
|
if (!refs.eHexLower.size && !refs.aCoordLower.size) return false |
|
const hexCandidates = new Set<string>() |
|
addResolvedHexCandidates(event, hexCandidates) |
|
for (const h of hexCandidates) { |
|
if (refs.eHexLower.has(h)) return true |
|
} |
|
for (const ac of listNormalizedACoordsFromEvent(event)) { |
|
if (refs.aCoordLower.has(ac)) return true |
|
} |
|
return false |
|
} |
|
|
|
/** |
|
* True if the list contains this **exact** event (`e` = {@link Event.id}, or `a` = replaceable coordinate). |
|
* Use for per-note bell UI and for writing list updates. For “any reply in this thread”, use {@link threadWatchMatchesRefs}. |
|
*/ |
|
export function eventHasExactNotificationThreadWatchRef(event: Event, refs: TThreadWatchListRefs): boolean { |
|
if (!refs.eHexLower.size && !refs.aCoordLower.size) return false |
|
if (isReplaceableEvent(event.kind)) { |
|
const n = normalizeReplaceableCoordinateString(getReplaceableCoordinateFromEvent(event)) |
|
return !!n && refs.aCoordLower.has(n) |
|
} |
|
return refs.eHexLower.has(event.id.toLowerCase()) |
|
} |
|
|
|
/** Replies, reactions, reposts, zaps-on-note, comments, poll votes, highlights — not plain top-level notes. */ |
|
export function isNotificationThreadInteractionEvent(event: Event): boolean { |
|
if (event.kind === kinds.ShortTextNote) return isReplyNoteEvent(event) |
|
if (event.kind === kinds.Reaction || event.kind === ExtendedKind.EXTERNAL_REACTION) return true |
|
if (isNip18RepostKind(event.kind)) return true |
|
if (event.kind === kinds.Zap) { |
|
return event.tags.some( |
|
(t) => t[0] === 'e' || t[0] === 'E' || t[0] === 'a' || t[0] === 'A' |
|
) |
|
} |
|
if ( |
|
event.kind === ExtendedKind.MONERO_TIP_DISCLOSURE || |
|
event.kind === ExtendedKind.MONERO_TIP_RECEIPT |
|
) { |
|
return event.tags.some( |
|
(t) => t[0] === 'e' || t[0] === 'E' || t[0] === 'a' || t[0] === 'A' |
|
) |
|
} |
|
if (event.kind === ExtendedKind.PAYMENT_NOTIFICATION) return true |
|
if (event.kind === ExtendedKind.COMMENT || event.kind === ExtendedKind.VOICE_COMMENT) return true |
|
if (event.kind === ExtendedKind.POLL_RESPONSE) return true |
|
if (event.kind === kinds.Highlights) return true |
|
return false |
|
} |
|
|
|
export function extractEHexIdsForNotificationReq(refs: TThreadWatchListRefs): string[] { |
|
return [...refs.eHexLower].slice(0, NOTIFICATION_THREAD_WATCH_MAX_E_IDS) |
|
} |
|
|
|
export function extractACoordsForNotificationReq(refs: TThreadWatchListRefs): string[] { |
|
return [...refs.aCoordLower].slice(0, NOTIFICATION_THREAD_WATCH_MAX_A_COORDS) |
|
} |
|
|
|
export function chunkArray<T>(arr: T[], size: number): T[][] { |
|
if (size <= 0) return arr.length ? [arr] : [] |
|
const out: T[][] = [] |
|
for (let i = 0; i < arr.length; i += size) { |
|
out.push(arr.slice(i, i + size)) |
|
} |
|
return out |
|
}
|
|
|