From b77c8eafd1f41cdc1d58cf49f7ddd2bde1e83c7a Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Fri, 25 Sep 2026 21:50:13 -0700 Subject: [PATCH] Use EventComments on the poll and calendar pages and publish comments through one publishComment --- .agents/skills/flotilla-model/kinds.md | 2 +- .agents/skills/flotilla-state/SKILL.md | 4 +- e2e/specs/community.spec.ts | 19 +++-- .../components/CalendarEventDiscussion.svelte | 78 ------------------- src/app/components/CommentCompose.svelte | 37 ++++----- src/app/components/EventComments.svelte | 10 +-- src/app/components/EventReply.svelte | 31 ++------ src/app/rooms.ts | 31 +++++++- .../[relay]/articles/[address]/+page.svelte | 2 +- .../[relay]/calendar/[address]/+page.svelte | 10 +-- .../spaces/[relay]/polls/[id]/+page.svelte | 70 ++--------------- 11 files changed, 84 insertions(+), 210 deletions(-) delete mode 100644 src/app/components/CalendarEventDiscussion.svelte diff --git a/.agents/skills/flotilla-model/kinds.md b/.agents/skills/flotilla-model/kinds.md index d8589748..2a57e120 100644 --- a/.agents/skills/flotilla-model/kinds.md +++ b/.agents/skills/flotilla-model/kinds.md @@ -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`) | | 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]` | | 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`) | diff --git a/.agents/skills/flotilla-state/SKILL.md b/.agents/skills/flotilla-state/SKILL.md index 2908b0a2..a65ebaeb 100644 --- a/.agents/skills/flotilla-state/SKILL.md +++ b/.agents/skills/flotilla-state/SKILL.md @@ -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 honours the `send_delay` window does, because `Command` cannot carry it: room chat -(`RoomChat.svelte`), the comment composers (`CommentCompose.svelte` and `EventReply.svelte`) and -`publishRoomQuote` in `rooms.ts`. So do the push adapters and `ProfileDelete.svelte`. DMs go through +(`RoomChat.svelte`), and `publishComment` (behind both comment composers) and `publishRoomQuote` in +`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`). NIP-86 calls (`relayManagement.get().forUrl(url)`) are not thunks. They return `{result, error}`, and the caller handles `error`. diff --git a/e2e/specs/community.spec.ts b/e2e/specs/community.spec.ts index 72a81cdb..3ab9d7ca 100644 --- a/e2e/specs/community.spec.ts +++ b/e2e/specs/community.spec.ts @@ -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 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")}) 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) @@ -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 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")}) 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}) => { diff --git a/src/app/components/CalendarEventDiscussion.svelte b/src/app/components/CalendarEventDiscussion.svelte deleted file mode 100644 index 6c85b84c..00000000 --- a/src/app/components/CalendarEventDiscussion.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - -
- {#if ordered.length === 0 && !showReply} -
-

No comments yet — start the conversation.

- -
- {:else} - {#if !showAll && ordered.length > 4} -
- -
- {/if} - {#each ordered.slice(0, showAll ? undefined : 4) as reply (reply.id)} - -
- - -
-
- {/each} - {#if showReply} - - {:else} -
- -
- {/if} - {/if} -
diff --git a/src/app/components/CommentCompose.svelte b/src/app/components/CommentCompose.svelte index 9d89f75f..e30978f9 100644 --- a/src/app/components/CommentCompose.svelte +++ b/src/app/components/CommentCompose.svelte @@ -1,8 +1,7 @@
-
-
+

{nodes.length === 0 ? "Discussion about this post" @@ -66,7 +66,7 @@

-
+
{#if url && replyTo?.id === event.id}
diff --git a/src/app/components/EventReply.svelte b/src/app/components/EventReply.svelte index 187d298b..a851ec84 100644 --- a/src/app/components/EventReply.svelte +++ b/src/app/components/EventReply.svelte @@ -2,8 +2,6 @@ import {onMount} from "svelte" import {writable} from "svelte/store" import type {TrustedEvent} from "@welshman/util" - import {tagSpec, tagValue} from "@welshman/util" - import {Comment} from "@welshman/domain" import {isMobile, preventDefault} from "@lib/html" import {fly} from "@lib/transition" import Paperclip from "@assets/icons/paperclip-2.svg?dataurl" @@ -12,12 +10,10 @@ import Spinner from "@lib/components/Spinner.svelte" import EditorContent from "@app/editor/EditorContent.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 {DraftKey} from "@app/drafts" import {pushToast} from "@app/toast" - import {thunks, writer} from "@app/core" - import {loadSendDelay} from "@app/settings" type Values = { content?: string | object @@ -33,7 +29,6 @@ } const {url, event, parent, onClose, onClearParent, onSubmit}: Props = $props() - const h = $derived(tagValue(tagSpec("h"), event.tags)) const draftKey = new DraftKey(`reply:${event.id}:${parent?.id || ""}`) const initialValues = draftKey.get() const uploading = writable(false) @@ -47,8 +42,7 @@ } const ed = await editor - let content = ed.getText({blockSeparator: "\n"}).trim() - let tags = ed.storage.nostr.getEditorTags() + const content = ed.getText({blockSeparator: "\n"}).trim() if (!content) { return pushToast({ @@ -57,24 +51,11 @@ }) } - if (parent) { - ;({content, tags} = await prependParent(parent, {content, tags}, url)) - } - - const eventWriter = await toSpace( - writer(Comment) - .setContent(content) - .addTags(...tags) - .setRootFromEvent(event) - .setParentFromEvent(event), + const tags = ed.storage.nostr.getEditorTags() + const thunk = await publishComment({ url, - h, - ) - - const thunk = $thunks.publish({ - relays: [url], - event: await eventWriter.renderTemplate(), - delay: await loadSendDelay(), + event, + ...(await prependParent(parent, {content, tags}, url)), }) draftKey.clear() diff --git a/src/app/rooms.ts b/src/app/rooms.ts index 516bfc80..ac302a6c 100644 --- a/src/app/rooms.ts +++ b/src/app/rooms.ts @@ -14,7 +14,7 @@ import { toNostrURI, } 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 {MembershipStatus, RoomLists, makeRoomKey, createSearch, publish} from "@welshman/app" import type {Room, RoomMeta} from "@welshman/app" @@ -33,6 +33,7 @@ import { } from "@app/core" import {deriveSpacePermissions, deriveUserIsSpaceStaff} from "@app/management" import {makeRoomPath} from "@app/routes" +import {loadSendDelay} from "@app/settings" 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 ( eventWriter: EventWriter, {url, h, shareToChat}: {url: string; h?: string; shareToChat?: boolean}, diff --git a/src/routes/spaces/[relay]/articles/[address]/+page.svelte b/src/routes/spaces/[relay]/articles/[address]/+page.svelte index b46be76d..b36a9fcb 100644 --- a/src/routes/spaces/[relay]/articles/[address]/+page.svelte +++ b/src/routes/spaces/[relay]/articles/[address]/+page.svelte @@ -128,7 +128,7 @@
- +
diff --git a/src/routes/spaces/[relay]/calendar/[address]/+page.svelte b/src/routes/spaces/[relay]/calendar/[address]/+page.svelte index 0b65d11a..c4607923 100644 --- a/src/routes/spaces/[relay]/calendar/[address]/+page.svelte +++ b/src/routes/spaces/[relay]/calendar/[address]/+page.svelte @@ -20,7 +20,7 @@ import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte" import CalendarEventMeta from "@app/components/CalendarEventMeta.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 CalendarRsvp from "@app/components/CalendarRsvp.svelte" import {deriveRsvps, getRsvpsByStatus, makeRsvpFilter} from "@app/calendar" @@ -37,7 +37,7 @@ const event = $events.one(address, [url]).$ const timeEvent = derived(event, $event => ($event ? reader(TimeEvent)($event) : undefined)) 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([])) const people = $derived(getRsvpsByStatus($rsvps)) @@ -113,8 +113,8 @@ aria-pressed={tab === "discussion"} onclick={showTab("discussion")}> Discussion - {#if $replies.length > 0} - {$replies.length} + {#if $comments.length > 0} + {$comments.length} {/if} -
- {/if} - {#each $comments.slice(0, showAll ? undefined : 4) as reply (reply.id)} - -
- - -
-
- {/each} -
- {#if showReply} - - {:else} -
- + +
+ +
- {/if} +
+ {:else}
{#await sleep(5000)}