Add better muting, add EventReducer
This commit is contained in:
@@ -26,7 +26,7 @@
|
|||||||
<Icon icon={Reply} />
|
<Icon icon={Reply} />
|
||||||
<span>{$replies.length} {$replies.length === 1 ? "reply" : "replies"}</span>
|
<span>{$replies.length} {$replies.length === 1 ? "reply" : "replies"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn btn-neutral btn-xs relative hidden rounded-full sm:flex">
|
<div class="btn btn-neutral btn-xs relative rounded-full">
|
||||||
{#if gt(lastActive, $checked)}
|
{#if gt(lastActive, $checked)}
|
||||||
<div class="h-2 w-2 rounded-full bg-primary"></div>
|
<div class="h-2 w-2 rounded-full bg-primary"></div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {insertAt, lt, addToMapKey, parseJson} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {Router, addMaximalFallbacks} from "@welshman/router"
|
||||||
|
import {load} from "@welshman/net"
|
||||||
|
import {
|
||||||
|
getIdOrAddress,
|
||||||
|
getIdFilters,
|
||||||
|
getParentIdsAndAddrs,
|
||||||
|
getParentIdOrAddr,
|
||||||
|
verifyEvent,
|
||||||
|
ZAP_RESPONSE,
|
||||||
|
} from "@welshman/util"
|
||||||
|
import {repository, getValidZap} from "@welshman/app"
|
||||||
|
import {REPOST_KINDS, REACTION_KINDS, isEventMuted} from '@app/core/state'
|
||||||
|
|
||||||
|
export let events: TrustedEvent[]
|
||||||
|
export let depth = 0
|
||||||
|
export let showMuted = false
|
||||||
|
export let hideReplies = false
|
||||||
|
export let showDeleted = false
|
||||||
|
export let shouldSort = false
|
||||||
|
export let shouldAwait = false
|
||||||
|
export let items: TrustedEvent[] = []
|
||||||
|
|
||||||
|
const timestamps = new Map<string, number>()
|
||||||
|
const context = new Map<string, Set<TrustedEvent>>()
|
||||||
|
|
||||||
|
const shouldSkip = (event: TrustedEvent) => {
|
||||||
|
if (!showMuted && $isEventMuted(event)) return true
|
||||||
|
if (!showDeleted && repository.isDeleted(event)) return true
|
||||||
|
if (hideReplies && getParentIdOrAddr(event)) return true
|
||||||
|
if (timestamps.has(getIdOrAddress(event))) return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const getParent = async (event: TrustedEvent) => {
|
||||||
|
if (REPOST_KINDS.includes(event.kind)) {
|
||||||
|
const parent = parseJson(event.content)
|
||||||
|
|
||||||
|
if (parent && verifyEvent(parent)) {
|
||||||
|
return parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentIds = getParentIdsAndAddrs(event)
|
||||||
|
|
||||||
|
if (parentIds.length > 0) {
|
||||||
|
const filters = getIdFilters(parentIds)
|
||||||
|
const [cached] = repository.query(filters)
|
||||||
|
|
||||||
|
if (cached) return cached
|
||||||
|
|
||||||
|
const relays = Router.get().EventParents(event).policy(addMaximalFallbacks).getUrls()
|
||||||
|
const [parent] = await load({filters, relays})
|
||||||
|
|
||||||
|
return parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addEvent = async (event: TrustedEvent) => {
|
||||||
|
const original = event
|
||||||
|
let currentDepth = depth
|
||||||
|
|
||||||
|
timestamps.set(getIdOrAddress(event), original.created_at)
|
||||||
|
|
||||||
|
while (currentDepth > 0) {
|
||||||
|
const parent = await getParent(event)
|
||||||
|
|
||||||
|
// Unable to get the parent? we're done traversing parents
|
||||||
|
if (!parent) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip zaps that fail our zapper check
|
||||||
|
if (event.kind === ZAP_RESPONSE && !(await getValidZap(event, parent))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link the events, even if we end up skipping this one (since we deduplicate)
|
||||||
|
addToMapKey(context, getIdOrAddress(parent), event)
|
||||||
|
|
||||||
|
// Hide replies to deleted/muted parents, or parents we've already seen
|
||||||
|
if (shouldSkip(parent)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamps.set(getIdOrAddress(parent), original.created_at)
|
||||||
|
currentDepth--
|
||||||
|
event = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not displayable, skip it
|
||||||
|
if ([...REPOST_KINDS, ...REACTION_KINDS].includes(event.kind)) return
|
||||||
|
|
||||||
|
let inserted = false
|
||||||
|
|
||||||
|
if (shouldSort) {
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
if (lt(timestamps.get(getIdOrAddress(items[i])), original.created_at)) {
|
||||||
|
items = insertAt(i, event, items)
|
||||||
|
inserted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inserted) {
|
||||||
|
items = [...items, event]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addEvents = async (events: TrustedEvent[]) => {
|
||||||
|
for (const event of events) {
|
||||||
|
if (!shouldSkip(event)) {
|
||||||
|
const promise = addEvent(event)
|
||||||
|
|
||||||
|
if (shouldAwait) {
|
||||||
|
await promise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: addEvents(events)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each items as event, i (event.id)}
|
||||||
|
{@render children({i, event})}
|
||||||
|
{/each}
|
||||||
@@ -4,13 +4,13 @@
|
|||||||
import {formatTimestamp} from "@welshman/lib"
|
import {formatTimestamp} from "@welshman/lib"
|
||||||
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {userMuteList} from "@welshman/app"
|
|
||||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
import {goToEvent} from "@app/util/routes"
|
import {goToEvent} from "@app/util/routes"
|
||||||
|
import {isEventMuted} from "@app/core/state"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
event,
|
event,
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
muted = false
|
muted = false
|
||||||
}
|
}
|
||||||
|
|
||||||
let muted = $state(getPubkeyTagValues(getListTags($userMuteList)).includes(event.pubkey))
|
let muted = $state($isEventMuted(event))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 shadow-md {restProps.class}">
|
<div class="flex flex-col gap-2 shadow-md {restProps.class}">
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ import {
|
|||||||
ZAP_GOAL,
|
ZAP_GOAL,
|
||||||
ZAP_REQUEST,
|
ZAP_REQUEST,
|
||||||
ZAP_RESPONSE,
|
ZAP_RESPONSE,
|
||||||
|
REPOST,
|
||||||
|
GENERIC_REPOST,
|
||||||
asDecryptedEvent,
|
asDecryptedEvent,
|
||||||
getGroupTags,
|
getGroupTags,
|
||||||
getListTags,
|
getListTags,
|
||||||
@@ -111,6 +113,10 @@ import {
|
|||||||
getAddress,
|
getAddress,
|
||||||
Address,
|
Address,
|
||||||
getIdFilters,
|
getIdFilters,
|
||||||
|
getEventTagValues,
|
||||||
|
getAddressTagValues,
|
||||||
|
getParentIds,
|
||||||
|
getParentAddrs,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {
|
import type {
|
||||||
TrustedEvent,
|
TrustedEvent,
|
||||||
@@ -126,6 +132,7 @@ import {
|
|||||||
repository,
|
repository,
|
||||||
tracker,
|
tracker,
|
||||||
createSearch,
|
createSearch,
|
||||||
|
userMuteList,
|
||||||
userFollowList,
|
userFollowList,
|
||||||
ensurePlaintext,
|
ensurePlaintext,
|
||||||
makeOutboxLoader,
|
makeOutboxLoader,
|
||||||
@@ -135,6 +142,7 @@ import {
|
|||||||
makeUserLoader,
|
makeUserLoader,
|
||||||
manageRelay,
|
manageRelay,
|
||||||
displayProfileByPubkey,
|
displayProfileByPubkey,
|
||||||
|
getProfile,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {readFeed} from "@lib/feeds"
|
import {readFeed} from "@lib/feeds"
|
||||||
|
|
||||||
@@ -300,6 +308,8 @@ export const makeCommentFilter = (kinds: number[], extra: Filter = {}) => ({
|
|||||||
...extra,
|
...extra,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const REPOST_KINDS = [REPOST, GENERIC_REPOST]
|
||||||
|
|
||||||
export const REACTION_KINDS = [REPORT, DELETE, REACTION]
|
export const REACTION_KINDS = [REPORT, DELETE, REACTION]
|
||||||
|
|
||||||
if (ENABLE_ZAPS) {
|
if (ENABLE_ZAPS) {
|
||||||
@@ -998,6 +1008,43 @@ export const userFeedFavorites = makeUserData(feedFavoritesByPubkey, loadFeedFav
|
|||||||
|
|
||||||
export const loadUserFeedFavorites = makeUserLoader(loadFeedFavorites)
|
export const loadUserFeedFavorites = makeUserLoader(loadFeedFavorites)
|
||||||
|
|
||||||
|
// Mutes
|
||||||
|
|
||||||
|
export const isEventMuted = withGetter(
|
||||||
|
derived(userMuteList, $userMuteList => {
|
||||||
|
const pubkey = $userMuteList?.event.pubkey
|
||||||
|
const tags = getListTags($userMuteList)
|
||||||
|
const mutedEvents = new Set(getEventTagValues(tags))
|
||||||
|
const mutedPubkeys = new Set(getPubkeyTagValues(tags))
|
||||||
|
const mutedAddresses = new Set(getAddressTagValues(tags))
|
||||||
|
const mutedTopics = new Set(getTagValues("t", tags))
|
||||||
|
const mutedWords = getTagValues("word", tags)
|
||||||
|
const regex =
|
||||||
|
mutedWords.length > 0
|
||||||
|
? new RegExp(`\\b(${mutedWords.map(w => w.toLowerCase().trim()).join("|")})\\b`)
|
||||||
|
: null
|
||||||
|
|
||||||
|
return (e: TrustedEvent) => {
|
||||||
|
if (!pubkey) return false
|
||||||
|
if (pubkey === e.pubkey) return false
|
||||||
|
if (mutedPubkeys.has(e.pubkey)) return true
|
||||||
|
if (mutedEvents.has(e.id)) return true
|
||||||
|
if (mutedAddresses.has(getAddress(e))) return true
|
||||||
|
if (getParentIds(e).some(id => mutedEvents.has(id))) return true
|
||||||
|
if (getParentAddrs(e).some(address => mutedAddresses.has(address))) return true
|
||||||
|
if (getTagValues("t", e.tags).some(t => mutedTopics.has(t))) return true
|
||||||
|
|
||||||
|
if (regex) {
|
||||||
|
if (e.content?.toLowerCase().match(regex)) return true
|
||||||
|
if (displayProfileByPubkey(e.pubkey).toLowerCase().match(regex)) return true
|
||||||
|
if (tryCatch(() => getProfile(e.pubkey)?.nip05?.match(regex))) return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
// Other utils
|
// Other utils
|
||||||
|
|
||||||
export const encodeRelay = (url: string) =>
|
export const encodeRelay = (url: string) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user