Put a create thread button on every board and drop the room picker
This commit is contained in:
parent
76bbacdbd6
commit
98290e1a9b
5 changed files with 40 additions and 58 deletions
|
|
@ -682,8 +682,10 @@ Acceptance:
|
|||
|
||||
- Creating a thread from a room's compose menu files it under that room's board
|
||||
and posts a quote of it into the room's chat.
|
||||
- Creating a thread from the top-level Threads page files it under a "General"
|
||||
board, or under whichever board is picked from its board selector.
|
||||
- Each board on the Threads page has its own create button, and a thread started
|
||||
from it is filed under that board.
|
||||
- The "General" board is always present, so a thread belonging to no room can be
|
||||
started from the Threads page.
|
||||
- Each board row shows the topic title, author, reply count, and last-post time.
|
||||
|
||||
### US-043 — Reply to a thread and to a specific post
|
||||
|
|
|
|||
|
|
@ -676,7 +676,13 @@ test("US-042 start a thread and see it filed under its room", async ({seed, as})
|
|||
await shortDate(page, at(4, HOUR) + 60),
|
||||
)
|
||||
|
||||
await pageBar(page).getByRole("button", {name: "Create", exact: true}).click()
|
||||
const general = page
|
||||
.locator("section")
|
||||
.filter({has: page.getByRole("heading", {name: "General", exact: true})})
|
||||
|
||||
// Each board creates its own threads, so the room comes from the button that was clicked rather
|
||||
// than from a picker.
|
||||
await general.getByRole("button", {name: "Create", exact: true}).click()
|
||||
|
||||
const fromThreads = modal(page, "Create a Thread")
|
||||
|
||||
|
|
@ -686,18 +692,12 @@ test("US-042 start a thread and see it filed under its room", async ({seed, as})
|
|||
|
||||
await expect(page.getByRole("heading", {name: "Create a Thread"})).toHaveCount(0)
|
||||
|
||||
const general = page
|
||||
.locator("section")
|
||||
.filter({has: page.getByRole("heading", {name: "General", exact: true})})
|
||||
|
||||
await expect(general.getByRole("row").filter({hasText: "Open floor"})).toBeVisible()
|
||||
|
||||
// The threads page has no room of its own, so the form offers one.
|
||||
await pageBar(page).getByRole("button", {name: "Create", exact: true}).click()
|
||||
await lounge.getByRole("button", {name: "Create", exact: true}).click()
|
||||
|
||||
const toLounge = modal(page, "Create a Thread")
|
||||
|
||||
await toLounge.getByRole("combobox").selectOption({label: "Lounge"})
|
||||
await toLounge.getByPlaceholder("What is this thread about?").fill("Carpet swatches")
|
||||
await editorOf(toLounge).pressSequentially("Beige is a choice.")
|
||||
await toLounge.getByRole("button", {name: "Create Thread"}).click()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
<script lang="ts">
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import Add from "@assets/icons/add.svg?dataurl"
|
||||
import {matchMd} from "@lib/theme"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
||||
import ThreadBoardItem from "@app/components/ThreadBoardItem.svelte"
|
||||
import ThreadCreate from "@app/components/ThreadCreate.svelte"
|
||||
import type {FeedContext} from "@app/feeds"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
|
|
@ -13,6 +18,8 @@
|
|||
}
|
||||
|
||||
const {url, h, threads, context}: Props = $props()
|
||||
|
||||
const createThread = () => pushModal(ThreadCreate, {url, h})
|
||||
</script>
|
||||
|
||||
<section class="card card-flat p-0">
|
||||
|
|
@ -25,12 +32,20 @@
|
|||
General
|
||||
{/if}
|
||||
</h2>
|
||||
<span class="text-content-muted text-sm">
|
||||
{threads.length}
|
||||
{threads.length === 1 ? "Topic" : "Topics"}
|
||||
</span>
|
||||
<div class="flex shrink-0 items-center gap-3">
|
||||
<span class="text-content-muted text-sm">
|
||||
{threads.length}
|
||||
{threads.length === 1 ? "Topic" : "Topics"}
|
||||
</span>
|
||||
<Button class="button button-primary button-sm" onclick={createThread}>
|
||||
<Icon icon={Add} />
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
{#if $matchMd}
|
||||
{#if threads.length === 0}
|
||||
<p class="text-content-muted p-4 text-sm">No topics yet.</p>
|
||||
{:else if $matchMd}
|
||||
<div class="scroll-container overflow-x-auto pb-4">
|
||||
<table class="w-full min-w-[640px] border-collapse">
|
||||
<thead
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {writable} from "svelte/store"
|
||||
import {sortBy} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {relay} from "@welshman/util"
|
||||
import {Thread} from "@welshman/domain"
|
||||
|
|
@ -22,13 +21,7 @@
|
|||
import {command, relays, writer} from "@app/core"
|
||||
import {DraftKey} from "@app/drafts"
|
||||
import {makeEditor} from "@app/editor"
|
||||
import {
|
||||
deriveOtherRooms,
|
||||
deriveUserRooms,
|
||||
displayRoom,
|
||||
publishRoomQuote,
|
||||
roomComparator,
|
||||
} from "@app/rooms"
|
||||
import {displayRoom, publishRoomQuote} from "@app/rooms"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
type Values = {
|
||||
|
|
@ -40,19 +33,16 @@
|
|||
type Props = {
|
||||
url: string
|
||||
h?: string
|
||||
selectRoom?: boolean
|
||||
shareToChat?: boolean
|
||||
quote?: TrustedEvent
|
||||
initialValues?: Values
|
||||
}
|
||||
|
||||
const {url, h, selectRoom = false, shareToChat = false, quote, initialValues}: Props = $props()
|
||||
const {url, h, shareToChat = false, quote, initialValues}: Props = $props()
|
||||
const draftKey = new DraftKey<Values>(`thread:${url}:${h ?? ""}`)
|
||||
const draft = draftKey.get()
|
||||
const shouldProtect = $relays.hasNip(url, 70)
|
||||
const userRooms = deriveUserRooms(url)
|
||||
const otherRooms = deriveOtherRooms(url)
|
||||
const roomOptions = $derived(sortBy(roomComparator(url), [...$userRooms, ...$otherRooms]))
|
||||
const room = h ?? initialValues?.h ?? draft?.h ?? ""
|
||||
|
||||
const uploading = writable(false)
|
||||
|
||||
|
|
@ -121,7 +111,6 @@
|
|||
|
||||
let title = $state(initialValues?.title ?? draft?.title ?? "")
|
||||
let content = $state(initialValues?.content ?? draft?.content ?? "")
|
||||
let room = $state(h ?? initialValues?.h ?? draft?.h ?? "")
|
||||
|
||||
const onChange = (json: object) => {
|
||||
content = json
|
||||
|
|
@ -145,7 +134,9 @@
|
|||
<ModalBody>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Create a Thread</ModalTitle>
|
||||
<ModalSubtitle>Share a link, or start a discussion.</ModalSubtitle>
|
||||
<ModalSubtitle>
|
||||
Share a link, or start a discussion in {room ? displayRoom(url, room) : "General"}.
|
||||
</ModalSubtitle>
|
||||
</ModalHeader>
|
||||
<div class="flex flex-col gap-8 relative">
|
||||
<Field>
|
||||
|
|
@ -164,21 +155,6 @@
|
|||
</label>
|
||||
{/snippet}
|
||||
</Field>
|
||||
{#if selectRoom && roomOptions.length > 0}
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
<p>Board</p>
|
||||
{/snippet}
|
||||
{#snippet input()}
|
||||
<select class="select input w-full" bind:value={room}>
|
||||
<option value="">General</option>
|
||||
{#each roomOptions as option (option)}
|
||||
<option value={option}>{displayRoom(url, option)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/snippet}
|
||||
</Field>
|
||||
{/if}
|
||||
<Field>
|
||||
{#snippet label()}
|
||||
<p>Message*</p>
|
||||
|
|
|
|||
|
|
@ -8,19 +8,15 @@
|
|||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {THREAD, tagValue, tagSpec} from "@welshman/util"
|
||||
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
|
||||
import Add from "@assets/icons/add.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import ThreadBoard from "@app/components/ThreadBoard.svelte"
|
||||
import ThreadCreate from "@app/components/ThreadCreate.svelte"
|
||||
import {decodeRelay} from "@app/relays"
|
||||
import {displayRoom} from "@app/rooms"
|
||||
import {makeCommentFilter} from "@app/content"
|
||||
import {isFeedLoading, makeFeed, makeFeedContext, makeScrollLoader} from "@app/feeds"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
const url = decodeRelay($page.params.relay!)
|
||||
const context = makeFeedContext({relays: [url]})
|
||||
|
|
@ -34,8 +30,6 @@
|
|||
let element: HTMLElement | undefined = $state()
|
||||
let events: Readable<TrustedEvent[]> = $state(readable([]))
|
||||
|
||||
const createThread = () => pushModal(ThreadCreate, {url, selectRoom: true})
|
||||
|
||||
const threadFeed = $derived.by(() => {
|
||||
const scores = new Map<string, number[]>()
|
||||
const [threads, comments] = partition(spec({kind: THREAD}), $events)
|
||||
|
|
@ -50,7 +44,8 @@
|
|||
|
||||
const items = sortBy(e => -max([...(scores.get(e.id) || []), e.created_at]), threads)
|
||||
|
||||
const byRoom = groupBy(e => tagValue(tagSpec("h"), e.tags) || "", items)
|
||||
const grouped = groupBy(e => tagValue(tagSpec("h"), e.tags) || "", items)
|
||||
const byRoom = new Map<string, TrustedEvent[]>([["", []], ...grouped])
|
||||
const roomName = (h: string) => (h ? displayRoom(url, h) : "general").toLowerCase()
|
||||
const boards = sortBy(([h]) => roomName(h), Array.from(byRoom.entries()))
|
||||
|
||||
|
|
@ -84,12 +79,6 @@
|
|||
{#snippet title()}
|
||||
<strong>Threads</strong>
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<Button class="button button-primary button-sm" onclick={createThread}>
|
||||
<Icon icon={Add} />
|
||||
Create
|
||||
</Button>
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
|
||||
<PageContent bind:element class="flex flex-col gap-2 p-2 sm:gap-4 sm:p-4">
|
||||
|
|
|
|||
Loading…
Reference in a new issue