Show the thread created from a message on the message

This commit is contained in:
Coracle-Bot 2026-09-09 08:32:14 +00:00
parent caafda9553
commit 5cf598d2ac
6 changed files with 31 additions and 3 deletions

View file

@ -709,6 +709,7 @@ Acceptance:
- "Create a Thread" from a room message's menu opens the thread composer - "Create a Thread" from a room message's menu opens the thread composer
pre-filled with a quote of that message. pre-filled with a quote of that message.
- Publishing files the thread under that room's board. - Publishing files the thread under that room's board.
- The message it was created from carries a link to the thread, which opens it.
- Opening the thread shows the quoted original message as part of the opening - Opening the thread shows the quoted original message as part of the opening
post. post.

View file

@ -948,6 +948,7 @@ test("US-045 turn a chat message into a thread", async ({seed, as}) => {
await composer.getByRole("button", {name: "Create Thread"}).click() await composer.getByRole("button", {name: "Create Thread"}).click()
await expect(page.getByRole("heading", {name: "Create a Thread"})).toHaveCount(0) await expect(page.getByRole("heading", {name: "Create a Thread"})).toHaveCount(0)
await expect(message.getByRole("link", {name: "Deploy failures"})).toBeVisible()
await page.goto(`${spacePath(url)}/threads`) await page.goto(`${spacePath(url)}/threads`)

View file

@ -12,6 +12,7 @@
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import { import {
MESSAGE, MESSAGE,
THREAD,
getCommentFiltersForParent, getCommentFiltersForParent,
getIdOrAddress, getIdOrAddress,
matchTag, matchTag,
@ -19,6 +20,7 @@
tagValue, tagValue,
} from "@welshman/util" } from "@welshman/util"
import {isMobile} from "@lib/html" import {isMobile} from "@lib/html"
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl" import Pen from "@assets/icons/pen.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl" import Reply from "@assets/icons/reply-2.svg?dataurl"
import ReplyAlt from "@assets/icons/reply.svg?dataurl" import ReplyAlt from "@assets/icons/reply.svg?dataurl"
@ -43,7 +45,7 @@
import {ENABLE_ZAPS} from "@app/env" import {ENABLE_ZAPS} from "@app/env"
import type {FeedContext} from "@app/feeds" import type {FeedContext} from "@app/feeds"
import {deriveEvent, deriveEventsForUrl} from "@app/repository" import {deriveEvent, deriveEventsForUrl} from "@app/repository"
import {makeContentPath} from "@app/routes" import {makeContentPath, makeThreadPath} from "@app/routes"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
type Props = { type Props = {
@ -85,6 +87,8 @@
$innerEvent && makeContentPath(url, $innerEvent.kind, getIdOrAddress($innerEvent)), $innerEvent && makeContentPath(url, $innerEvent.kind, getIdOrAddress($innerEvent)),
) )
const threads = deriveEventsForUrl(url, [{kinds: [THREAD], "#q": [event.id]}])
const commenterDisplays = $derived( const commenterDisplays = $derived(
deriveDisplaysByPubkey(uniq($innerComments.map(e => e.pubkey)), url), deriveDisplaysByPubkey(uniq($innerComments.map(e => e.pubkey)), url),
) )
@ -154,6 +158,16 @@
{createReaction} {createReaction}
reactionClass="tip-right" reactionClass="tip-right"
innerEvent={$innerEvent} /> innerEvent={$innerEvent} />
{#each $threads as thread (thread.id)}
<Link
href={makeThreadPath(url, thread.id)}
class="button button-xs button-neutral gap-1 rounded-full">
<Icon icon={NotesMinimalistic} />
<span class="max-w-48 truncate">
{tagValue(tagSpec("title"), thread.tags) || "Thread"}
</span>
</Link>
{/each}
{#if path && $innerComments.length > 0} {#if path && $innerComments.length > 0}
{@const pubkeys = $innerComments.map(e => e.pubkey)} {@const pubkeys = $innerComments.map(e => e.pubkey)}
{@const isOwn = pubkeys.includes($user.pubkey)} {@const isOwn = pubkeys.includes($user.pubkey)}

View file

@ -53,6 +53,7 @@
pushModal(ThreadCreate, { pushModal(ThreadCreate, {
url, url,
h, h,
quote: event,
initialValues: {content: toNostrURI(nip19.neventEncode({...event, relays: [url]}))}, initialValues: {content: toNostrURI(nip19.neventEncode({...event, relays: [url]}))},
}) })
} }

View file

@ -64,7 +64,12 @@
const createThread = () => const createThread = () =>
pushModal( pushModal(
ThreadCreate, ThreadCreate,
{url, h, initialValues: {content: toNostrURI(nip19.neventEncode({...event, relays: [url]}))}}, {
url,
h,
quote: event,
initialValues: {content: toNostrURI(nip19.neventEncode({...event, relays: [url]}))},
},
{replaceState: true}, {replaceState: true},
) )

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import {writable} from "svelte/store" import {writable} from "svelte/store"
import type {TrustedEvent} from "@welshman/util"
import {relay} from "@welshman/util" import {relay} from "@welshman/util"
import {Thread} from "@welshman/domain" import {Thread} from "@welshman/domain"
import {publish} from "@welshman/app" import {publish} from "@welshman/app"
@ -32,10 +33,11 @@
url: string url: string
h?: string h?: string
shareToChat?: boolean shareToChat?: boolean
quote?: TrustedEvent
initialValues?: Values initialValues?: Values
} }
const {url, h, shareToChat = false, initialValues}: Props = $props() const {url, h, shareToChat = false, quote, initialValues}: Props = $props()
const draftKey = new DraftKey<Values>(`thread:${url}:${h ?? ""}`) const draftKey = new DraftKey<Values>(`thread:${url}:${h ?? ""}`)
const draft = draftKey.get() const draft = draftKey.get()
const shouldProtect = $relays.hasNip(url, 70) const shouldProtect = $relays.hasNip(url, 70)
@ -81,6 +83,10 @@
eventWriter.setRoom(url, h) eventWriter.setRoom(url, h)
} }
if (quote) {
eventWriter.addQuote(quote)
}
const thunk = await command(eventWriter).then(publish) const thunk = await command(eventWriter).then(publish)
const error = await thunk.waitForError() const error = await thunk.waitForError()