Thread context through to EventActivity
This commit is contained in:
parent
0dab2cc779
commit
ee09c975f8
13 changed files with 97 additions and 61 deletions
|
|
@ -56,7 +56,7 @@
|
|||
{createReaction}
|
||||
reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
<EventActivity {path} {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Article" />
|
||||
</ThunkStatusOrDeleted>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
{createReaction}
|
||||
reactionClass="tip tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
<EventActivity {path} {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Event">
|
||||
{#snippet customActions()}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
{createReaction}
|
||||
reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
<EventActivity {path} {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Listing">
|
||||
{#snippet customActions()}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
{createReaction}
|
||||
reactionClass="tip-left" />
|
||||
{#if showActivity && path}
|
||||
<EventActivity {url} {path} {event} />
|
||||
<EventActivity {path} {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Comment" />
|
||||
</ThunkStatusOrDeleted>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,19 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {max, gt, formatTimestampRelative} from "@welshman/lib"
|
||||
import {getCommentFiltersForRoot} from "@welshman/util"
|
||||
import {derived} from "svelte/store"
|
||||
import {filter, max, gt, spec, formatTimestampRelative} from "@welshman/lib"
|
||||
import {COMMENT} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {deriveEvents} from "@app/repository"
|
||||
import {network} from "@app/core"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import {deriveChecked} from "@app/notifications"
|
||||
|
||||
const {url, path, event}: {url: string; path: string; event: TrustedEvent} = $props()
|
||||
const {path, event, context}: {path: string; event: TrustedEvent; context: FeedContext} = $props()
|
||||
|
||||
const checked = deriveChecked(path)
|
||||
const filters = getCommentFiltersForRoot([event])
|
||||
const replies = deriveEvents(filters)
|
||||
const related = context.related(event)
|
||||
const replies = derived(related, $related => filter(spec({kind: COMMENT}), $related))
|
||||
const lastActive = $derived(max([...$replies, event].map(e => e.created_at)))
|
||||
|
||||
onMount(() => {
|
||||
$network.load({relays: [url], filters})
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="flex-inline button button-neutral button-xs gap-1 rounded-full">
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
{createReaction}
|
||||
reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
<EventActivity {path} {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} hideZap noun="Goal" />
|
||||
</ThunkStatusOrDeleted>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
{createReaction}
|
||||
reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
<EventActivity {path} {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Poll" />
|
||||
</ThunkStatusOrDeleted>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
{createReaction}
|
||||
reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
<EventActivity {path} {event} {context} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Thread" />
|
||||
</ThunkStatusOrDeleted>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,16 @@
|
|||
import {matchMd} from "@lib/theme"
|
||||
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
||||
import ThreadBoardItem from "@app/components/ThreadBoardItem.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
h: string
|
||||
threads: TrustedEvent[]
|
||||
context: FeedContext
|
||||
}
|
||||
|
||||
const {url, h, threads}: Props = $props()
|
||||
const {url, h, threads, context}: Props = $props()
|
||||
</script>
|
||||
|
||||
<section class="card card-flat p-0">
|
||||
|
|
@ -42,7 +44,7 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
{#each threads as event (event.id)}
|
||||
<ThreadBoardItem {url} {event} />
|
||||
<ThreadBoardItem {url} {event} {context} />
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
@ -50,7 +52,7 @@
|
|||
{:else}
|
||||
<div class="pb-4">
|
||||
{#each threads as event (event.id)}
|
||||
<ThreadBoardItem mobile {url} {event} />
|
||||
<ThreadBoardItem mobile {url} {event} {context} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,28 @@
|
|||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {formatTimestamp, max} from "@welshman/lib"
|
||||
import {derived} from "svelte/store"
|
||||
import {filter, formatTimestamp, max, spec} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {getCommentFiltersForRoot, tagSpec, tagValue} from "@welshman/util"
|
||||
import {COMMENT, tagSpec, tagValue} from "@welshman/util"
|
||||
import {fade} from "@lib/transition"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import ProfileName from "@app/components/ProfileName.svelte"
|
||||
import {deriveEventsForUrl} from "@app/repository"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import {notifications} from "@app/notifications"
|
||||
import {makeThreadPath} from "@app/routes"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
event: TrustedEvent
|
||||
context: FeedContext
|
||||
mobile?: boolean
|
||||
}
|
||||
|
||||
const {url, event, mobile = false}: Props = $props()
|
||||
const {url, event, context, mobile = false}: Props = $props()
|
||||
|
||||
const replies = deriveEventsForUrl(url, getCommentFiltersForRoot([event]))
|
||||
const related = context.related(event)
|
||||
const replies = derived(related, $related => filter(spec({kind: COMMENT}), $related))
|
||||
const replyCount = $derived($replies.length)
|
||||
const lastActive = $derived(max([...$replies, event].map(e => e.created_at)))
|
||||
const title = tagValue(tagSpec("title"), event.tags)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const makeDeleteFilter = (kinds: number[], extra: Filter = {}) => ({
|
|||
|
||||
export const REPOST_KINDS = [REPOST, GENERIC_REPOST]
|
||||
|
||||
export const REACTION_DISPLAY_KINDS = [REPORT, REACTION, ZAP_RECEIPT]
|
||||
export const EVENT_CONTEXT_KINDS = [REPORT, REACTION, ZAP_RECEIPT, COMMENT]
|
||||
|
||||
export const REACTION_KINDS = [REPORT, DELETE, REACTION]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import {readable, writable} from "svelte/store"
|
||||
import {derived, readable, writable} from "svelte/store"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {batch, between, call, int, now, on, sortBy, uniqBy, MONTH, YEAR} from "@welshman/lib"
|
||||
import {
|
||||
COMMENT,
|
||||
DELETE,
|
||||
EVENT_TIME,
|
||||
addressTags,
|
||||
getAddress,
|
||||
getCommentFiltersForRoot,
|
||||
getReplyFilters,
|
||||
hexTags,
|
||||
isReplaceableKind,
|
||||
matchFilters,
|
||||
tagSpec,
|
||||
tagValue,
|
||||
|
|
@ -17,12 +21,24 @@ import {mergeRepositoryUpdates} from "@welshman/net"
|
|||
import type {RepositoryUpdate} from "@welshman/net"
|
||||
import {createScroller} from "@lib/html"
|
||||
import {daysBetween} from "@lib/util"
|
||||
import {REACTION_DISPLAY_KINDS, REACTION_KINDS} from "@app/content"
|
||||
import {EVENT_CONTEXT_KINDS, REACTION_KINDS} from "@app/content"
|
||||
import {app, network} from "@app/core"
|
||||
import {getEventsForUrl} from "@app/repository"
|
||||
|
||||
const noEvents: TrustedEvent[] = []
|
||||
|
||||
// Reactions, zaps and reports point at their subject with `e`/`a`. A NIP-22 comment instead
|
||||
// points at its thread *root* with `E`/`A`, so filing it by those tags puts a whole thread in
|
||||
// the root's bucket — which is the scope a reply count wants.
|
||||
const getTargets = ({kind, tags}: TrustedEvent) =>
|
||||
kind === COMMENT
|
||||
? [...tagValues(hexTags("E"), tags), ...tagValues(addressTags("A"), tags)]
|
||||
: [...tagValues(hexTags("e"), tags), ...tagValues(addressTags("a"), tags)]
|
||||
|
||||
// A related event may point at a replaceable one by either id or address, so both are keys
|
||||
const getKeys = (event: TrustedEvent) =>
|
||||
isReplaceableKind(event.kind) ? [event.id, getAddress(event)] : [event.id]
|
||||
|
||||
export const makeFeedContext = ({relays}: {relays: string[] | Promise<string[]>}) => {
|
||||
const {repository} = app.get()
|
||||
const controller = new AbortController()
|
||||
|
|
@ -35,7 +51,7 @@ export const makeFeedContext = ({relays}: {relays: string[] | Promise<string[]>}
|
|||
// An event seen before its target was tracked stays unfiled, so that adding the target
|
||||
// later can pick it up out of the repository
|
||||
if (!targetsByEventId.has(event.id)) {
|
||||
const eventTargets = tagValues(hexTags("e"), event.tags).filter(target => targets.has(target))
|
||||
const eventTargets = getTargets(event).filter(target => targets.has(target))
|
||||
|
||||
if (eventTargets.length > 0) {
|
||||
targetsByEventId.set(event.id, eventTargets)
|
||||
|
|
@ -81,9 +97,10 @@ export const makeFeedContext = ({relays}: {relays: string[] | Promise<string[]>}
|
|||
|
||||
// What's already local — an earlier page, our own optimistic reactions — never comes
|
||||
// through the update listener, so file it before asking the network for the rest
|
||||
for (const event of repository.query(
|
||||
getReplyFilters(events, {kinds: REACTION_DISPLAY_KINDS}),
|
||||
)) {
|
||||
for (const event of repository.query([
|
||||
...getReplyFilters(events, {kinds: EVENT_CONTEXT_KINDS}),
|
||||
...getCommentFiltersForRoot(events),
|
||||
])) {
|
||||
addEvent(event, touched)
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +111,10 @@ export const makeFeedContext = ({relays}: {relays: string[] | Promise<string[]>}
|
|||
const context = await network.get().load({
|
||||
relays: urls,
|
||||
signal: controller.signal,
|
||||
filters: getReplyFilters(events, {kinds: REACTION_KINDS}),
|
||||
filters: [
|
||||
...getReplyFilters(events, {kinds: REACTION_KINDS}),
|
||||
...getCommentFiltersForRoot(events),
|
||||
],
|
||||
})
|
||||
|
||||
if (context.length > 0) {
|
||||
|
|
@ -114,7 +134,7 @@ export const makeFeedContext = ({relays}: {relays: string[] | Promise<string[]>}
|
|||
const touched = new Set<string>()
|
||||
|
||||
for (const event of added) {
|
||||
if (REACTION_DISPLAY_KINDS.includes(event.kind)) {
|
||||
if (EVENT_CONTEXT_KINDS.includes(event.kind)) {
|
||||
addEvent(event, touched)
|
||||
}
|
||||
}
|
||||
|
|
@ -130,35 +150,49 @@ export const makeFeedContext = ({relays}: {relays: string[] | Promise<string[]>}
|
|||
// Track an event so its context gets requested with the rest of the batch
|
||||
const add = (event: TrustedEvent) => {
|
||||
if (!targets.has(event.id)) {
|
||||
targets.add(event.id)
|
||||
for (const key of getKeys(event)) {
|
||||
targets.add(key)
|
||||
}
|
||||
|
||||
loadContext(event)
|
||||
}
|
||||
}
|
||||
|
||||
const relatedForKey = (key: string) =>
|
||||
readable(eventsByTarget.get(key) || noEvents, set => {
|
||||
let subscribers = subscribersByTarget.get(key)
|
||||
|
||||
if (!subscribers) {
|
||||
subscribers = new Set()
|
||||
subscribersByTarget.set(key, subscribers)
|
||||
}
|
||||
|
||||
subscribers.add(set)
|
||||
set(eventsByTarget.get(key) || noEvents)
|
||||
|
||||
return () => {
|
||||
subscribers.delete(set)
|
||||
|
||||
if (subscribers.size === 0) {
|
||||
subscribersByTarget.delete(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
add,
|
||||
related: (event: TrustedEvent): Readable<TrustedEvent[]> => {
|
||||
add(event)
|
||||
|
||||
return readable(eventsByTarget.get(event.id) || noEvents, set => {
|
||||
let subscribers = subscribersByTarget.get(event.id)
|
||||
const [key, ...rest] = getKeys(event)
|
||||
|
||||
if (!subscribers) {
|
||||
subscribers = new Set()
|
||||
subscribersByTarget.set(event.id, subscribers)
|
||||
}
|
||||
|
||||
subscribers.add(set)
|
||||
set(eventsByTarget.get(event.id) || noEvents)
|
||||
|
||||
return () => {
|
||||
subscribers.delete(set)
|
||||
|
||||
if (subscribers.size === 0) {
|
||||
subscribersByTarget.delete(event.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
// A replaceable event collects both its buckets, and something tagging it by id and
|
||||
// address at once lands in both
|
||||
return rest.length > 0
|
||||
? derived([relatedForKey(key), ...rest.map(relatedForKey)], buckets =>
|
||||
uniqBy(event => event.id, buckets.flat()),
|
||||
)
|
||||
: relatedForKey(key)
|
||||
},
|
||||
cleanup: () => {
|
||||
controller.abort()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {onDestroy, onMount} from "svelte"
|
||||
import {readable} from "svelte/store"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {page} from "$app/stores"
|
||||
|
|
@ -18,10 +18,13 @@
|
|||
import {decodeRelay} from "@app/relays"
|
||||
import {displayRoom} from "@app/rooms"
|
||||
import {makeCommentFilter} from "@app/content"
|
||||
import {makeFeed} from "@app/feeds"
|
||||
import {makeFeed, makeFeedContext} from "@app/feeds"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
const url = decodeRelay($page.params.relay!)
|
||||
const context = makeFeedContext({relays: [url]})
|
||||
|
||||
onDestroy(context.cleanup)
|
||||
|
||||
let loading = $state(true)
|
||||
let element: HTMLElement | undefined = $state()
|
||||
|
|
@ -54,6 +57,7 @@
|
|||
const feed = makeFeed({
|
||||
relays: [url],
|
||||
element: element!,
|
||||
onEvent: context.add,
|
||||
filters: [{kinds: [THREAD]}, makeCommentFilter([THREAD])],
|
||||
onBackwardExhausted: () => {
|
||||
loading = false
|
||||
|
|
@ -62,9 +66,7 @@
|
|||
|
||||
events = feed.events
|
||||
|
||||
return () => {
|
||||
feed.cleanup()
|
||||
}
|
||||
return () => feed.cleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -85,7 +87,7 @@
|
|||
|
||||
<PageContent bind:element class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
|
||||
{#each threadFeed.boards as [h, threads] (h || "general")}
|
||||
<ThreadBoard {url} {h} {threads} />
|
||||
<ThreadBoard {url} {h} {threads} {context} />
|
||||
{/each}
|
||||
<p class="flex h-10 items-center justify-center py-20">
|
||||
<Spinner {loading}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue