Use EventComments on the poll and calendar pages and publish comments through one publishComment
This commit is contained in:
parent
9c3d77a2f2
commit
b77c8eafd1
11 changed files with 84 additions and 210 deletions
|
|
@ -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`) |
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
||||||
|
|
|
||||||
|
|
@ -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}) => {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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},
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue