Compare commits

..

1 Commits

Author SHA1 Message Date
userAdityaa d3abc817d4 chore: redesign threads as a linear phpBB-style forum view 2026-06-14 22:13:17 +05:30
5 changed files with 51 additions and 68 deletions
+5 -23
View File
@@ -1,17 +1,15 @@
<script lang="ts">
import {onMount} from "svelte"
import {writable} from "svelte/store"
import type {TrustedEvent} from "@welshman/util"
import {isMobile, preventDefault} from "@lib/html"
import {fly} from "@lib/transition"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
import {publishComment} from "@app/comments"
import {canEnforceNip70} from "@app/relays"
import {PROTECTED, prependParent} from "@app/groups"
import {PROTECTED} from "@app/groups"
import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/drafts"
import {pushToast} from "@app/toast"
@@ -20,17 +18,8 @@
content?: string | object
}
type Props = {
url: string
event: TrustedEvent
parent?: TrustedEvent
onClose: () => void
onClearParent?: () => void
onSubmit: (thunk: unknown) => void
}
const {url, event, parent, onClose, onClearParent, onSubmit}: Props = $props()
const draftKey = new DraftKey<Values>(`reply:${event.id}:${parent?.id || ""}`)
const {url, event, onClose, onSubmit} = $props()
const draftKey = new DraftKey<Values>(`reply:${event.id}`)
const initialValues = draftKey.get()
const shouldProtect = canEnforceNip70(url)
const uploading = writable(false)
@@ -42,8 +31,8 @@
if ($uploading) return
const ed = await editor
let content = ed.getText({blockSeparator: "\n"}).trim()
let tags = ed.storage.nostr.getEditorTags()
const content = ed.getText({blockSeparator: "\n"}).trim()
const tags = ed.storage.nostr.getEditorTags()
if (await shouldProtect) {
tags.push(PROTECTED)
@@ -56,10 +45,6 @@
})
}
if (parent) {
;({content, tags} = prependParent(parent, {content, tags}, url))
}
draftKey.clear()
onSubmit(publishComment({event, content, tags, relays: [url]}))
}
@@ -102,9 +87,6 @@
onsubmit={preventDefault(submit)}
class="left-content bottom-sai right-sai fixed z-feature mb-14 md:mb-0 w-full md:w-auto pr-2">
<div class="card2 mx-2 my-2 bg-alt shadow-md">
{#if parent}
<ChatComposeParent event={parent} clear={() => onClearParent?.()} verb="Replying to" />
{/if}
<div class="relative">
<div class="note-editor grow overflow-hidden">
<EditorContent {autofocus} {editor} />
+1 -1
View File
@@ -36,7 +36,7 @@
<div class="hidden shrink-0 md:flex md:items-center">
{@render leading?.()}
</div>
<div class="min-w-0">
<div class="min-w-0 leading-none">
{@render title?.()}
</div>
</div>
+7 -6
View File
@@ -4,7 +4,7 @@
import {COMMENT} from "@welshman/util"
import {deriveHandleForPubkey, deriveProfileDisplay, displayHandle} from "@welshman/app"
import Reply from "@assets/icons/reply-2.svg?dataurl"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import SquareArrowRightUp from "@assets/icons/square-arrow-right-up.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
@@ -22,11 +22,12 @@
url: string
threadId: string
event: TrustedEvent
number: number
threadPubkey: string
onReply: (event: TrustedEvent) => void
}
const {url, threadId, event, threadPubkey, onReply}: Props = $props()
const {url, threadId, event, number, threadPubkey, onReply}: Props = $props()
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
const handle = deriveHandleForPubkey(event.pubkey)
@@ -37,7 +38,7 @@
const copyPermalink = () => {
const path = makeSpacePath(url, "threads", threadId)
const link = `${PLATFORM_URL}${path}#post-${event.id}`
const link = `${PLATFORM_URL}${path}#post-${number}`
clip(link)
}
@@ -46,7 +47,7 @@
</script>
<article
id="post-{event.id}"
id="post-{number}"
data-event={event.id}
class="border-b border-base-content/15 bg-base-100">
<div class="flex flex-col md:flex-row">
@@ -72,8 +73,8 @@
class="flex flex-wrap items-center justify-between gap-2 border-b border-base-content/10 bg-base-200/40 px-3 py-2 text-xs sm:px-4 sm:text-sm">
<span class="opacity-75">{formatTimestamp(event.created_at)}</span>
<Button class="btn btn-ghost btn-xs h-auto min-h-0 gap-1 px-1 py-0" onclick={copyPermalink}>
<Icon icon={LinkRound} size={3} />
Permalink
Post #{number}
<Icon icon={SquareArrowRightUp} size={3} />
</Button>
</div>
<div class="px-3 py-4 sm:px-4">
+21 -5
View File
@@ -3,7 +3,7 @@
import {readable} from "svelte/store"
import type {Readable} from "svelte/store"
import {page} from "$app/stores"
import {sortBy, partition, spec, max, pushToMapKey, groupBy} from "@welshman/lib"
import {sortBy, partition, spec, max, pushToMapKey} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {THREAD, getTagValue} from "@welshman/util"
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
@@ -16,7 +16,7 @@
import ThreadBoard from "@app/components/ThreadBoard.svelte"
import ThreadCreate from "@app/components/ThreadCreate.svelte"
import {decodeRelay} from "@app/relays"
import {displayRoom} from "@app/groups"
import {roomsByUrl} from "@app/groups"
import {makeCommentFilter} from "@app/content"
import {makeFeed} from "@app/feeds"
import {pushModal} from "@app/modal"
@@ -43,9 +43,25 @@
const items = sortBy(e => -max([...(scores.get(e.id) || []), e.created_at]), threads)
const byRoom = groupBy(e => getTagValue("h", e.tags) || "", items)
const roomName = (h: string) => (h ? displayRoom(url, h) : "general").toLowerCase()
const boards = sortBy(([h]) => roomName(h), Array.from(byRoom.entries()))
const byRoom = new Map<string, TrustedEvent[]>()
for (const event of items) {
const h = getTagValue("h", event.tags) || ""
const roomThreads = byRoom.get(h) || []
roomThreads.push(event)
byRoom.set(h, roomThreads)
}
const roomOrder = new Map(($roomsByUrl.get(url) || []).map((room, index) => [room.h, index]))
const boards = sortBy(
([h]) => roomOrder.get(h) ?? Number.MAX_SAFE_INTEGER,
sortBy(
([h]) => (h ? 0 : 1),
sortBy(([h]) => h, Array.from(byRoom.entries())),
),
)
return {items, boards}
})
@@ -70,6 +70,7 @@
const search = params.toString()
goto(`${$page.url.pathname}${search ? `?${search}` : ""}`, {
replaceState: true,
keepFocus: true,
noScroll: true,
})
@@ -91,12 +92,6 @@
}
}
const clearReplyParent = () => {
if ($event) {
replyTo = $event
}
}
const scrollToPost = (hash: string) => {
const element = document.getElementById(hash)
@@ -107,30 +102,23 @@
let showReply = $state(false)
let replyTo: TrustedEvent | undefined = $state()
let hashHandled = $state(false)
$effect(() => {
if (hashHandled || posts.length === 0) return
const hash = window.location.hash.replace(/^#/, "")
if (!hash.startsWith("post-")) return
const eventId = hash.replace("post-", "")
const index = posts.findIndex(post => post.id === eventId)
if (index < 0) return
hashHandled = true
setPage(Math.ceil((index + 1) / POSTS_PER_PAGE))
setTimeout(() => scrollToPost(hash), 100)
})
onMount(() => {
const controller = new AbortController()
request({relays: [url], filters, signal: controller.signal})
const hash = window.location.hash.replace(/^#/, "")
if (hash.startsWith("post-")) {
const postNumber = parseInt(hash.replace("post-", ""))
if (!Number.isNaN(postNumber) && postNumber > 0) {
setPage(Math.ceil(postNumber / POSTS_PER_PAGE))
setTimeout(() => scrollToPost(hash), 100)
}
}
return () => {
controller.abort()
}
@@ -157,11 +145,13 @@
<PageContent class="flex flex-col">
{#if $event}
<div class="border-y border-base-content/15 bg-base-100">
{#each pagePosts as post (post.id)}
{#each pagePosts as post, i (post.id)}
{@const number = (currentPage - 1) * POSTS_PER_PAGE + i + 1}
<ThreadPost
{url}
threadId={id}
event={post}
{number}
threadPubkey={$event.pubkey}
onReply={openReply} />
{/each}
@@ -169,14 +159,8 @@
{#if pageCount > 1}
<ThreadPagination page={currentPage} {pageCount} onPage={setPage} />
{/if}
{#if showReply && replyTo && $event}
<EventReply
{url}
event={$event}
parent={replyTo.id === $event.id ? undefined : replyTo}
onClose={closeReply}
onClearParent={clearReplyParent}
onSubmit={closeReply} />
{#if showReply && replyTo}
<EventReply {url} event={replyTo} onClose={closeReply} onSubmit={closeReply} />
{:else}
<div class="flex justify-end p-4">
<Button class="btn btn-primary" onclick={openThreadReply}>