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