2026-06-08 22:33:38 +00:00
|
|
|
import {derived} from "svelte/store"
|
2026-08-17 15:42:14 +00:00
|
|
|
import {pushToMapKey, shuffle, sortBy, uniqBy} from "@welshman/lib"
|
2026-06-08 22:33:38 +00:00
|
|
|
import {
|
2026-07-28 16:16:26 +00:00
|
|
|
COMMENT,
|
|
|
|
|
addressTags,
|
2026-06-08 22:33:38 +00:00
|
|
|
getAddress,
|
2026-08-17 15:42:14 +00:00
|
|
|
getIdAndAddress,
|
2026-07-28 16:16:26 +00:00
|
|
|
hexTags,
|
|
|
|
|
tagSpec,
|
|
|
|
|
tagValues,
|
|
|
|
|
topicTags,
|
2026-06-08 22:33:38 +00:00
|
|
|
} from "@welshman/util"
|
|
|
|
|
import type {TrustedEvent} from "@welshman/util"
|
|
|
|
|
import {withGetter} from "@welshman/store"
|
2026-07-28 16:16:26 +00:00
|
|
|
import {getCommentTagValues, getReplyTagValues} from "@welshman/domain"
|
2026-08-14 17:12:18 +00:00
|
|
|
import {FollowLists, MuteLists} from "@welshman/app"
|
|
|
|
|
import {deriveUserItem, profiles, user} from "@app/core"
|
2026-06-08 22:33:38 +00:00
|
|
|
import {DEFAULT_PUBKEYS} from "@app/env"
|
|
|
|
|
|
2026-08-14 17:12:18 +00:00
|
|
|
export const bootstrapPubkeys = derived(deriveUserItem(FollowLists), $userFollowList => {
|
|
|
|
|
const appPubkeys = DEFAULT_PUBKEYS.split(",")
|
|
|
|
|
const userPubkeys = shuffle($userFollowList?.pubkeys() ?? [])
|
2026-07-28 16:16:26 +00:00
|
|
|
|
2026-08-14 17:12:18 +00:00
|
|
|
return userPubkeys.length > 5 ? userPubkeys : [...userPubkeys, ...appPubkeys]
|
|
|
|
|
})
|
2026-07-28 16:16:26 +00:00
|
|
|
|
|
|
|
|
// Ids and addresses of an event's immediate parents, falling back to its thread roots.
|
|
|
|
|
const getParents = ({kind, tags}: TrustedEvent) => {
|
|
|
|
|
const {roots, replies} = kind === COMMENT ? getCommentTagValues(tags) : getReplyTagValues(tags)
|
|
|
|
|
|
|
|
|
|
return replies.length > 0 ? replies : roots
|
|
|
|
|
}
|
2026-06-08 22:33:38 +00:00
|
|
|
|
2026-08-17 15:42:14 +00:00
|
|
|
export type CommentNode = {
|
|
|
|
|
comment: TrustedEvent
|
|
|
|
|
children: CommentNode[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const buildCommentTree = (root: TrustedEvent, comments: TrustedEvent[]) => {
|
|
|
|
|
const byParent = new Map<string, TrustedEvent[]>()
|
|
|
|
|
|
|
|
|
|
// A comment names its parent by id, and by address too when the parent is addressable, so
|
|
|
|
|
// index it under every value it gives and dedupe on the way back out.
|
|
|
|
|
for (const comment of comments) {
|
|
|
|
|
for (const parent of getParents(comment)) {
|
|
|
|
|
pushToMapKey(byParent, parent, comment)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Nothing stops a comment from naming several parents, which would let the tree cycle, so
|
|
|
|
|
// walk down from the root and keep each comment at the first place it turns up.
|
|
|
|
|
const seen = new Set<string>()
|
|
|
|
|
|
|
|
|
|
const build = (parent: TrustedEvent): CommentNode[] => {
|
|
|
|
|
const children = uniqBy(
|
|
|
|
|
e => e.id,
|
|
|
|
|
getIdAndAddress(parent).flatMap(value => byParent.get(value) ?? []),
|
|
|
|
|
).filter(e => !seen.has(e.id))
|
|
|
|
|
|
|
|
|
|
for (const child of children) {
|
|
|
|
|
seen.add(child.id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return children.map(comment => ({comment, children: build(comment)}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nodes = build(root)
|
|
|
|
|
|
|
|
|
|
// A comment can name a parent we don't have — a superseded version of an addressable root,
|
|
|
|
|
// or a parent that failed to load — so adopt whatever's left rather than dropping it.
|
|
|
|
|
// `comments` is oldest first, so a parent is always adopted before its own children.
|
|
|
|
|
for (const comment of comments) {
|
|
|
|
|
if (!seen.has(comment.id)) {
|
|
|
|
|
seen.add(comment.id)
|
|
|
|
|
nodes.push({comment, children: build(comment)})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sortBy(node => node.comment.created_at, nodes)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 22:33:38 +00:00
|
|
|
export const isEventMuted = withGetter(
|
2026-08-14 17:12:18 +00:00
|
|
|
derived([user, deriveUserItem(MuteLists)], ([$user, $muteList]) => {
|
|
|
|
|
const tags = $muteList?.tags() ?? []
|
2026-07-28 16:16:26 +00:00
|
|
|
const mutedEvents = new Set(tagValues(hexTags("e"), tags))
|
|
|
|
|
const mutedPubkeys = new Set(tagValues(hexTags("p"), tags))
|
|
|
|
|
const mutedAddresses = new Set(tagValues(addressTags("a"), tags))
|
|
|
|
|
const mutedTopics = new Set(tagValues(topicTags("t"), tags))
|
|
|
|
|
const mutedWords = tagValues(tagSpec("word"), tags)
|
2026-06-08 22:33:38 +00:00
|
|
|
const regex =
|
|
|
|
|
mutedWords.length > 0
|
|
|
|
|
? new RegExp(`\\b(${mutedWords.map(w => w.toLowerCase().trim()).join("|")})\\b`)
|
2026-07-15 16:30:17 +00:00
|
|
|
: undefined
|
2026-06-08 22:33:38 +00:00
|
|
|
|
|
|
|
|
return (e: TrustedEvent) => {
|
2026-08-14 17:12:18 +00:00
|
|
|
if (!$muteList) return false
|
2026-07-28 16:16:26 +00:00
|
|
|
if ($user.pubkey === e.pubkey) return false
|
2026-06-08 22:33:38 +00:00
|
|
|
if (mutedPubkeys.has(e.pubkey)) return true
|
|
|
|
|
if (mutedEvents.has(e.id)) return true
|
|
|
|
|
if (mutedAddresses.has(getAddress(e))) return true
|
2026-07-28 16:16:26 +00:00
|
|
|
if (getParents(e).some(v => mutedEvents.has(v) || mutedAddresses.has(v))) return true
|
|
|
|
|
if (tagValues(topicTags("t"), e.tags).some(t => mutedTopics.has(t))) return true
|
2026-06-08 22:33:38 +00:00
|
|
|
|
|
|
|
|
if (regex) {
|
2026-07-28 16:16:26 +00:00
|
|
|
const profile = profiles.get().get(e.pubkey)
|
|
|
|
|
|
|
|
|
|
if (profile?.display().toLowerCase().match(regex)) return true
|
|
|
|
|
if (profile?.nip05()?.match(regex)) return true
|
2026-06-08 22:33:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
)
|