Use EventComments on the poll and calendar pages and publish comments through one publishComment

This commit is contained in:
Jon Staab 2026-09-25 21:50:13 -07:00
parent 9c3d77a2f2
commit b77c8eafd1
11 changed files with 84 additions and 210 deletions

View file

@ -10,7 +10,7 @@ and NIP-43 tables in [SKILL.md](SKILL.md). Routes are under `src/routes/`.
|---|---|---|---|---| |---|---|---|---|---|
| Chat | `MESSAGE` (9) | none | `src/app/rooms.ts` | `spaces/[relay]/chat`, `spaces/[relay]/[h]` (`RoomChat`) | | Chat | `MESSAGE` (9) | none | `src/app/rooms.ts` | `spaces/[relay]/chat`, `spaces/[relay]/[h]` (`RoomChat`) |
| Threads | `THREAD` (11) | `Thread` | none | `spaces/[relay]/threads`, `threads/[id]` (`ThreadCreate`) | | Threads | `THREAD` (11) | `Thread` | none | `spaces/[relay]/threads`, `threads/[id]` (`ThreadCreate`) |
| Comments | `COMMENT` (1111) | `Comment` | `src/app/content.ts` (`makeCommentFilter`) | `CommentCompose`, `EventReply` | | Comments | `COMMENT` (1111) | `Comment` | `src/app/content.ts` (`makeCommentFilter`), `src/app/rooms.ts` (`publishComment`) | `CommentCompose`, `EventReply`, `EventComments` |
| Articles | `LONG_FORM` (30023) | `Article` | `src/app/articles.ts` | `spaces/[relay]/articles`, `articles/create`, `articles/[address]` | | Articles | `LONG_FORM` (30023) | `Article` | `src/app/articles.ts` | `spaces/[relay]/articles`, `articles/create`, `articles/[address]` |
| Calendar | `EVENT_TIME` (31923) | `TimeEvent` | `src/app/feeds.ts` (`makeCalendarFeed`) | `spaces/[relay]/calendar`, `calendar/[address]` (`CalendarEventForm`) | | Calendar | `EVENT_TIME` (31923) | `TimeEvent` | `src/app/feeds.ts` (`makeCalendarFeed`) | `spaces/[relay]/calendar`, `calendar/[address]` (`CalendarEventForm`) |
| Classifieds | `CLASSIFIED` (30402) | `Classified` | `src/app/classifieds.ts` | `spaces/[relay]/classifieds`, `classifieds/[address]` (`ClassifiedForm`) | | Classifieds | `CLASSIFIED` (30402) | `Classified` | `src/app/classifieds.ts` | `spaces/[relay]/classifieds`, `classifieds/[address]` (`ClassifiedForm`) |

View file

@ -379,8 +379,8 @@ Plugin mutators already return a `Command`: `roomLists.get().addRelay(url).then(
Some call sites call `thunks.get().publish({event, relays, delay})` directly. Anything that Some call sites call `thunks.get().publish({event, relays, delay})` directly. Anything that
honours the `send_delay` window does, because `Command` cannot carry it: room chat honours the `send_delay` window does, because `Command` cannot carry it: room chat
(`RoomChat.svelte`), the comment composers (`CommentCompose.svelte` and `EventReply.svelte`) and (`RoomChat.svelte`), and `publishComment` (behind both comment composers) and `publishRoomQuote` in
`publishRoomQuote` in `rooms.ts`. So do the push adapters and `ProfileDelete.svelte`. DMs go through `rooms.ts`. So do the push adapters and `ProfileDelete.svelte`. DMs go through
`wraps.get().publish({event, recipients})`, which returns a merged thunk (see `reactions.ts`). `wraps.get().publish({event, recipients})`, which returns a merged thunk (see `reactions.ts`).
NIP-86 calls (`relayManagement.get().forUrl(url)`) are not thunks. They return NIP-86 calls (`relayManagement.get().forUrl(url)`) are not thunks. They return
`{result, error}`, and the caller handles `error`. `{result, error}`, and the caller handles `error`.

View file

@ -573,14 +573,17 @@ test("US-052 comment on and react to community posts", async ({seed, as}) => {
await expect(heroCard.getByRole("button", {name: PARTY})).toBeVisible() await expect(heroCard.getByRole("button", {name: PARTY})).toBeVisible()
await bob.getByRole("button", {name: /^Discussion/}).click() await bob.getByRole("button", {name: /^Discussion/}).click()
await bob.getByRole("button", {name: "Leave comment"}).click() await bob.getByRole("button", {name: "Add a comment"}).click()
const reply = bob.locator("form").filter({has: bob.locator(".note-editor")}) const reply = bob.locator("form").filter({has: bob.locator(".note-editor")})
await noteEditor(reply).pressSequentially("Is there parking at The Green?") await noteEditor(reply).pressSequentially("Is there parking at The Green?")
await reply.getByRole("button", {name: "Post Reply"}).click() await reply.getByRole("button", {name: "Comment"}).click()
await expect(bob.getByText("Is there parking at The Green?")).toBeVisible() // The composer holds what was typed until the relay confirms it, so match the comment itself.
await expect(
bob.locator('[data-component="Comment"]').filter({hasText: "Is there parking at The Green?"}),
).toBeVisible()
const alice = await as(users.alice, calendarPath) const alice = await as(users.alice, calendarPath)
@ -594,14 +597,18 @@ test("US-052 comment on and react to community posts", async ({seed, as}) => {
await expect(alice.getByText("Is there parking at The Green?")).toBeVisible() await expect(alice.getByText("Is there parking at The Green?")).toBeVisible()
await alice.getByRole("button", {name: "Leave comment"}).click() await alice.getByRole("button", {name: "Add a comment"}).click()
const alicesReply = alice.locator("form").filter({has: alice.locator(".note-editor")}) const alicesReply = alice.locator("form").filter({has: alice.locator(".note-editor")})
await noteEditor(alicesReply).pressSequentially("Yes, in the field behind the pub.") await noteEditor(alicesReply).pressSequentially("Yes, in the field behind the pub.")
await alicesReply.getByRole("button", {name: "Post Reply"}).click() await alicesReply.getByRole("button", {name: "Comment"}).click()
await expect(alice.getByText("Yes, in the field behind the pub.")).toBeVisible() await expect(
alice
.locator('[data-component="Comment"]')
.filter({hasText: "Yes, in the field behind the pub."}),
).toBeVisible()
}) })
test("US-053 browse and search the library", async ({seed, as}) => { test("US-053 browse and search the library", async ({seed, as}) => {

View file

@ -1,78 +0,0 @@
<script lang="ts">
import {sortBy} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import SortVertical from "@assets/icons/sort-vertical.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import NoteCard from "@app/components/NoteCard.svelte"
import NoteContent from "@app/components/NoteContent.svelte"
import CommentActions from "@app/components/CommentActions.svelte"
import EventReply from "@app/components/EventReply.svelte"
import type {FeedContext} from "@app/feeds"
type Props = {
url: string
event: TrustedEvent
replies: TrustedEvent[]
context: FeedContext
}
const {url, event, replies, context}: Props = $props()
const expand = () => {
showAll = true
}
const openReply = () => {
showReply = true
}
const closeReply = () => {
showReply = false
}
let showAll = $state(false)
let showReply = $state(false)
const ordered = $derived(sortBy(reply => reply.created_at, replies))
</script>
<div class="flex flex-col gap-2 sm:gap-4">
{#if ordered.length === 0 && !showReply}
<div class="flex flex-col items-center gap-3 py-6">
<p class="opacity-75">No comments yet — start the conversation.</p>
<Button class="button button-primary" onclick={openReply}>
<Icon icon={Reply} />
Leave comment
</Button>
</div>
{:else}
{#if !showAll && ordered.length > 4}
<div class="flex justify-center">
<Button class="button button-link" onclick={expand}>
<Icon icon={SortVertical} />
Show all {ordered.length} replies
</Button>
</div>
{/if}
{#each ordered.slice(0, showAll ? undefined : 4) as reply (reply.id)}
<NoteCard event={reply} {url} class="card z-feature w-full">
<div class="ml-12 flex flex-col gap-3">
<NoteContent showEntire event={reply} {url} />
<CommentActions event={reply} {url} {context} />
</div>
</NoteCard>
{/each}
{#if showReply}
<EventReply {url} {event} onClose={closeReply} onSubmit={closeReply} />
{:else}
<div class="flex justify-end px-2 pb-2">
<Button class="button button-primary" onclick={openReply}>
<Icon icon={Reply} />
Leave comment
</Button>
</div>
{/if}
{/if}
</div>

View file

@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import {writable} from "svelte/store" import {writable} from "svelte/store"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {tagSpec, tagValue} from "@welshman/util" import {Note} from "@welshman/domain"
import {Comment, Note} from "@welshman/domain"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl" import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@ -12,7 +11,7 @@
import {thunks, writer} from "@app/core" import {thunks, writer} from "@app/core"
import {DraftKey} from "@app/drafts" import {DraftKey} from "@app/drafts"
import {makeEditor} from "@app/editor" import {makeEditor} from "@app/editor"
import {toSpace} from "@app/rooms" import {publishComment} from "@app/rooms"
import {loadSendDelay} from "@app/settings" import {loadSendDelay} from "@app/settings"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
@ -31,8 +30,6 @@
const {event, onCancel, onSubmit, parent, url, noun = "Comment"}: Props = $props() const {event, onCancel, onSubmit, parent, url, noun = "Comment"}: Props = $props()
const h = $derived(tagValue(tagSpec("h"), event.tags))
const draftKey = new DraftKey<Values>(`comment:${event.id}:${parent?.id ?? ""}`) const draftKey = new DraftKey<Values>(`comment:${event.id}:${parent?.id ?? ""}`)
const initialValues = draftKey.get() const initialValues = draftKey.get()
const uploading = writable(false) const uploading = writable(false)
@ -40,26 +37,19 @@
const selectFiles = () => editor.then(ed => ed.commands.selectFiles()) const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
// A reply to a room event is a NIP-22 comment, and a reply to a kind 1 note is a NIP-10 note. // A reply to a room event is a NIP-22 comment, and a reply to a kind 1 note is a NIP-10 note.
const renderReply = async (content: string, tags: string[][]) => { const publishReply = async (content: string, tags: string[][]) => {
if (url) { if (url) {
const eventWriter = await toSpace( return publishComment({url, event, parent, content, tags})
writer(Comment)
.setContent(content)
.addTags(...tags)
.setRootFromEvent(event)
.setParentFromEvent(parent ?? event),
url,
h,
)
return {relays: [url], event: await eventWriter.renderTemplate()}
} }
return writer(Note) return $thunks.publish({
.setContent(content) ...(await writer(Note)
.addTags(...tags) .setContent(content)
.setParent(parent ?? event) .addTags(...tags)
.render() .setParent(parent ?? event)
.render()),
delay: await loadSendDelay(),
})
} }
const submit = async () => { const submit = async () => {
@ -77,8 +67,7 @@
loading = true loading = true
try { try {
const reply = await renderReply(content, ed.storage.nostr.getEditorTags()) const thunk = await publishReply(content, ed.storage.nostr.getEditorTags())
const thunk = $thunks.publish({...reply, delay: await loadSendDelay()})
const error = await thunk.waitForError() const error = await thunk.waitForError()
if (error) { if (error) {

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import cx from "classnames"
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {getCommentFiltersForRoot} from "@welshman/util" import {getCommentFiltersForRoot} from "@welshman/util"
@ -16,9 +17,10 @@
event: TrustedEvent event: TrustedEvent
url?: string url?: string
context: FeedContext context: FeedContext
columnClass?: string
} }
const {event, url, context}: Props = $props() const {event, url, context, columnClass}: Props = $props()
const relays = removeUndefined([url]) const relays = removeUndefined([url])
const filters = getCommentFiltersForRoot([event]) const filters = getCommentFiltersForRoot([event])
@ -48,10 +50,8 @@
</script> </script>
<section class="flex flex-col"> <section class="flex flex-col">
<!-- The heading rule is a page-level divider, so it runs full width; the comments below it
stay in the article's reading column. -->
<div class="border-b px-5 sm:px-8" style="border-color: var(--line)"> <div class="border-b px-5 sm:px-8" style="border-color: var(--line)">
<div class="mx-auto flex w-full max-w-[68ch] flex-wrap items-center justify-between gap-2 py-3"> <div class={cx("flex w-full flex-wrap items-center justify-between gap-2 py-3", columnClass)}>
<h2 class="text-lg font-bold"> <h2 class="text-lg font-bold">
{nodes.length === 0 {nodes.length === 0
? "Discussion about this post" ? "Discussion about this post"
@ -66,7 +66,7 @@
</div> </div>
</div> </div>
<div class="px-5 sm:px-8"> <div class="px-5 sm:px-8">
<div class="mx-auto flex w-full max-w-[68ch] flex-col pb-10"> <div class={cx("flex w-full flex-col pb-10", columnClass)}>
{#if url && replyTo?.id === event.id} {#if url && replyTo?.id === event.id}
<div class="py-4"> <div class="py-4">
<CommentCompose {url} {event} onCancel={clearReplyTo} onSubmit={clearReplyTo} /> <CommentCompose {url} {event} onCancel={clearReplyTo} onSubmit={clearReplyTo} />

View file

@ -2,8 +2,6 @@
import {onMount} from "svelte" import {onMount} from "svelte"
import {writable} from "svelte/store" import {writable} from "svelte/store"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {tagSpec, tagValue} from "@welshman/util"
import {Comment} from "@welshman/domain"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl" import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
@ -12,12 +10,10 @@
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import EditorContent from "@app/editor/EditorContent.svelte" import EditorContent from "@app/editor/EditorContent.svelte"
import ComposeParent from "@app/components/ComposeParent.svelte" import ComposeParent from "@app/components/ComposeParent.svelte"
import {prependParent, toSpace} from "@app/rooms" import {prependParent, publishComment} from "@app/rooms"
import {makeEditor} from "@app/editor" import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/drafts" import {DraftKey} from "@app/drafts"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {thunks, writer} from "@app/core"
import {loadSendDelay} from "@app/settings"
type Values = { type Values = {
content?: string | object content?: string | object
@ -33,7 +29,6 @@
} }
const {url, event, parent, onClose, onClearParent, onSubmit}: Props = $props() const {url, event, parent, onClose, onClearParent, onSubmit}: Props = $props()
const h = $derived(tagValue(tagSpec("h"), event.tags))
const draftKey = new DraftKey<Values>(`reply:${event.id}:${parent?.id || ""}`) const draftKey = new DraftKey<Values>(`reply:${event.id}:${parent?.id || ""}`)
const initialValues = draftKey.get() const initialValues = draftKey.get()
const uploading = writable(false) const uploading = writable(false)
@ -47,8 +42,7 @@
} }
const ed = await editor const ed = await editor
let content = ed.getText({blockSeparator: "\n"}).trim() const content = ed.getText({blockSeparator: "\n"}).trim()
let tags = ed.storage.nostr.getEditorTags()
if (!content) { if (!content) {
return pushToast({ return pushToast({
@ -57,24 +51,11 @@
}) })
} }
if (parent) { const tags = ed.storage.nostr.getEditorTags()
;({content, tags} = await prependParent(parent, {content, tags}, url)) const thunk = await publishComment({
}
const eventWriter = await toSpace(
writer(Comment)
.setContent(content)
.addTags(...tags)
.setRootFromEvent(event)
.setParentFromEvent(event),
url, url,
h, event,
) ...(await prependParent(parent, {content, tags}, url)),
const thunk = $thunks.publish({
relays: [url],
event: await eventWriter.renderTemplate(),
delay: await loadSendDelay(),
}) })
draftKey.clear() draftKey.clear()

View file

@ -14,7 +14,7 @@ import {
toNostrURI, toNostrURI,
} from "@welshman/util" } from "@welshman/util"
import type {EventContent, TrustedEvent} from "@welshman/util" import type {EventContent, TrustedEvent} from "@welshman/util"
import {Message} from "@welshman/domain" import {Comment, Message} from "@welshman/domain"
import type {BaseEventReader, EventWriter} from "@welshman/domain" import type {BaseEventReader, EventWriter} from "@welshman/domain"
import {MembershipStatus, RoomLists, makeRoomKey, createSearch, publish} from "@welshman/app" import {MembershipStatus, RoomLists, makeRoomKey, createSearch, publish} from "@welshman/app"
import type {Room, RoomMeta} from "@welshman/app" import type {Room, RoomMeta} from "@welshman/app"
@ -33,6 +33,7 @@ import {
} from "@app/core" } from "@app/core"
import {deriveSpacePermissions, deriveUserIsSpaceStaff} from "@app/management" import {deriveSpacePermissions, deriveUserIsSpaceStaff} from "@app/management"
import {makeRoomPath} from "@app/routes" import {makeRoomPath} from "@app/routes"
import {loadSendDelay} from "@app/settings"
export const PROTECTED = ["-"] export const PROTECTED = ["-"]
@ -139,6 +140,34 @@ export const publishRoomQuote = async ({
}) })
} }
export const publishComment = async ({
url,
event,
parent = event,
content,
tags,
}: EventContent & {
url: string
event: TrustedEvent
parent?: TrustedEvent
}) => {
const eventWriter = await toSpace(
writer(Comment)
.setContent(content)
.addTags(...tags)
.setRootFromEvent(event)
.setParentFromEvent(parent),
url,
tagValue(tagSpec("h"), event.tags),
)
return thunks.get().publish({
relays: [url],
event: await eventWriter.renderTemplate(),
delay: await loadSendDelay(),
})
}
export const publishToSpace = async ( export const publishToSpace = async (
eventWriter: EventWriter<BaseEventReader>, eventWriter: EventWriter<BaseEventReader>,
{url, h, shareToChat}: {url: string; h?: string; shareToChat?: boolean}, {url, h, shareToChat}: {url: string; h?: string; shareToChat?: boolean},

View file

@ -128,7 +128,7 @@
</div> </div>
</div> </div>
<div class="border-t" style="border-color: var(--line)"> <div class="border-t" style="border-color: var(--line)">
<EventComments event={$event} {url} {context} /> <EventComments event={$event} {url} {context} columnClass="mx-auto max-w-[68ch]" />
</div> </div>
</div> </div>
<ArticleAuthorSidebar {url} event={$event} /> <ArticleAuthorSidebar {url} event={$event} />

View file

@ -20,7 +20,7 @@
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte" import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
import CalendarEventMeta from "@app/components/CalendarEventMeta.svelte" import CalendarEventMeta from "@app/components/CalendarEventMeta.svelte"
import CalendarEventDate from "@app/components/CalendarEventDate.svelte" import CalendarEventDate from "@app/components/CalendarEventDate.svelte"
import CalendarEventDiscussion from "@app/components/CalendarEventDiscussion.svelte" import EventComments from "@app/components/EventComments.svelte"
import CalendarEventPeople from "@app/components/CalendarEventPeople.svelte" import CalendarEventPeople from "@app/components/CalendarEventPeople.svelte"
import CalendarRsvp from "@app/components/CalendarRsvp.svelte" import CalendarRsvp from "@app/components/CalendarRsvp.svelte"
import {deriveRsvps, getRsvpsByStatus, makeRsvpFilter} from "@app/calendar" import {deriveRsvps, getRsvpsByStatus, makeRsvpFilter} from "@app/calendar"
@ -37,7 +37,7 @@
const event = $events.one(address, [url]).$ const event = $events.one(address, [url]).$
const timeEvent = derived(event, $event => ($event ? reader(TimeEvent)($event) : undefined)) const timeEvent = derived(event, $event => ($event ? reader(TimeEvent)($event) : undefined))
const filters = $derived($event ? getCommentFiltersForRoot([$event]) : []) const filters = $derived($event ? getCommentFiltersForRoot([$event]) : [])
const replies = $derived($events.desc(filters).$) const comments = $derived($events.asc(filters).$)
const rsvps = $derived($event ? deriveRsvps($event) : readable<TrustedEvent[]>([])) const rsvps = $derived($event ? deriveRsvps($event) : readable<TrustedEvent[]>([]))
const people = $derived(getRsvpsByStatus($rsvps)) const people = $derived(getRsvpsByStatus($rsvps))
@ -113,8 +113,8 @@
aria-pressed={tab === "discussion"} aria-pressed={tab === "discussion"}
onclick={showTab("discussion")}> onclick={showTab("discussion")}>
Discussion Discussion
{#if $replies.length > 0} {#if $comments.length > 0}
<span class="opacity-75">{$replies.length}</span> <span class="opacity-75">{$comments.length}</span>
{/if} {/if}
</Button> </Button>
<Button <Button
@ -138,7 +138,7 @@
{/if} {/if}
</div> </div>
{:else if tab === "discussion"} {:else if tab === "discussion"}
<CalendarEventDiscussion {url} event={$event} replies={$replies} {context} /> <EventComments {url} event={$event} {context} />
{:else} {:else}
<CalendarEventPeople {url} event={$event} rsvps={$rsvps} /> <CalendarEventPeople {url} event={$event} rsvps={$rsvps} />
{/if} {/if}

View file

@ -1,17 +1,14 @@
<script lang="ts"> <script lang="ts">
import {onDestroy, onMount} from "svelte" import {onDestroy, onMount} from "svelte"
import {sleep} from "@welshman/lib" import {sleep} from "@welshman/lib"
import {POLL, POLL_RESPONSE, getCommentFiltersForRoot} from "@welshman/util" import {POLL, POLL_RESPONSE} from "@welshman/util"
import SortVertical from "@assets/icons/sort-vertical.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import PageContent from "@lib/components/PageContent.svelte" import PageContent from "@lib/components/PageContent.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import SpaceBar from "@app/components/SpaceBar.svelte" import SpaceBar from "@app/components/SpaceBar.svelte"
import NoteCard from "@app/components/NoteCard.svelte" import NoteCard from "@app/components/NoteCard.svelte"
import NoteContent from "@app/components/NoteContent.svelte" import NoteContent from "@app/components/NoteContent.svelte"
import CommentActions from "@app/components/CommentActions.svelte" import CommentActions from "@app/components/CommentActions.svelte"
import EventReply from "@app/components/EventReply.svelte" import EventComments from "@app/components/EventComments.svelte"
import {events, network} from "@app/core" import {events, network} from "@app/core"
import {makeFeedContext} from "@app/feeds" import {makeFeedContext} from "@app/feeds"
import {decodeRelay} from "@app/relays" import {decodeRelay} from "@app/relays"
@ -23,28 +20,11 @@
const url = decodeRelay(relay) const url = decodeRelay(relay)
const context = makeFeedContext({relays: [url]}) const context = makeFeedContext({relays: [url]})
const event = $events.one(id, [url]).$ const event = $events.one(id, [url]).$
const filters = $derived($event ? getCommentFiltersForRoot([$event]) : [])
const comments = $derived($events.asc(filters).$)
const back = () => history.back() const back = () => history.back()
const openReply = () => {
showReply = true
}
const closeReply = () => {
showReply = false
}
const expand = () => {
showAll = true
}
onDestroy(context.cleanup) onDestroy(context.cleanup)
let showAll = $state(false)
let showReply = $state(false)
onMount(() => { onMount(() => {
const controller = new AbortController() const controller = new AbortController()
@ -61,16 +41,6 @@
controller.abort() controller.abort()
} }
}) })
$effect(() => {
if (filters.length > 0) {
const controller = new AbortController()
$network.request({relays: [url], filters, signal: controller.signal})
return () => controller.abort()
}
})
</script> </script>
<SpaceBar {back}> <SpaceBar {back}>
@ -81,37 +51,13 @@
<PageContent class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4"> <PageContent class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
{#if $event} {#if $event}
<div class="flex flex-col gap-3"> <NoteCard event={$event} {url} class="card z-feature w-full">
<NoteCard event={$event} {url} class="card z-feature w-full"> <div class="ml-12 flex flex-col gap-3">
<div class="flex flex-col gap-3 ml-12 flex flex-col gap-3"> <NoteContent showEntire event={$event} {url} />
<NoteContent showEntire event={$event} {url} /> <CommentActions showActivity {url} event={$event} {context} />
<CommentActions showActivity {url} event={$event} {context} />
</div>
</NoteCard>
{#if !showAll && $comments.length > 4}
<div class="flex justify-center">
<Button class="button button-link" onclick={expand}>
<Icon icon={SortVertical} />
Show all {$comments.length} comments
</Button>
</div>
{/if}
{#each $comments.slice(0, showAll ? undefined : 4) as reply (reply.id)}
<NoteCard event={reply} {url} class="card z-feature w-full">
<div class="flex flex-col gap-3 ml-12">
<NoteContent showEntire event={reply} {url} />
<CommentActions event={reply} {url} {context} />
</div>
</NoteCard>
{/each}
</div>
{#if showReply}
<EventReply {url} event={$event} onClose={closeReply} onSubmit={closeReply} />
{:else}
<div class="flex justify-end">
<Button class="button button-primary" onclick={openReply}>Comment on this poll</Button>
</div> </div>
{/if} </NoteCard>
<EventComments event={$event} {url} {context} />
{:else} {:else}
<div class="flex justify-center py-20"> <div class="flex justify-center py-20">
{#await sleep(5000)} {#await sleep(5000)}