Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf238c6bff |
@@ -1,15 +1,17 @@
|
||||
<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} from "@app/groups"
|
||||
import {PROTECTED, prependParent} from "@app/groups"
|
||||
import {makeEditor} from "@app/editor"
|
||||
import {DraftKey} from "@app/drafts"
|
||||
import {pushToast} from "@app/toast"
|
||||
@@ -18,8 +20,17 @@
|
||||
content?: string | object
|
||||
}
|
||||
|
||||
const {url, event, onClose, onSubmit} = $props()
|
||||
const draftKey = new DraftKey<Values>(`reply:${event.id}`)
|
||||
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 initialValues = draftKey.get()
|
||||
const shouldProtect = canEnforceNip70(url)
|
||||
const uploading = writable(false)
|
||||
@@ -31,8 +42,8 @@
|
||||
if ($uploading) return
|
||||
|
||||
const ed = await editor
|
||||
const content = ed.getText({blockSeparator: "\n"}).trim()
|
||||
const tags = ed.storage.nostr.getEditorTags()
|
||||
let content = ed.getText({blockSeparator: "\n"}).trim()
|
||||
let tags = ed.storage.nostr.getEditorTags()
|
||||
|
||||
if (await shouldProtect) {
|
||||
tags.push(PROTECTED)
|
||||
@@ -45,6 +56,10 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
;({content, tags} = prependParent(parent, {content, tags}, url))
|
||||
}
|
||||
|
||||
draftKey.clear()
|
||||
onSubmit(publishComment({event, content, tags, relays: [url]}))
|
||||
}
|
||||
@@ -87,6 +102,9 @@
|
||||
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} />
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<div class="hidden shrink-0 md:flex md:items-center">
|
||||
{@render leading?.()}
|
||||
</div>
|
||||
<div class="min-w-0 leading-none">
|
||||
<div class="min-w-0">
|
||||
{@render title?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 SquareArrowRightUp from "@assets/icons/square-arrow-right-up.svg?dataurl"
|
||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
@@ -22,12 +22,11 @@
|
||||
url: string
|
||||
threadId: string
|
||||
event: TrustedEvent
|
||||
number: number
|
||||
threadPubkey: string
|
||||
onReply: (event: TrustedEvent) => void
|
||||
}
|
||||
|
||||
const {url, threadId, event, number, threadPubkey, onReply}: Props = $props()
|
||||
const {url, threadId, event, threadPubkey, onReply}: Props = $props()
|
||||
|
||||
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
|
||||
const handle = deriveHandleForPubkey(event.pubkey)
|
||||
@@ -38,7 +37,7 @@
|
||||
|
||||
const copyPermalink = () => {
|
||||
const path = makeSpacePath(url, "threads", threadId)
|
||||
const link = `${PLATFORM_URL}${path}#post-${number}`
|
||||
const link = `${PLATFORM_URL}${path}#post-${event.id}`
|
||||
|
||||
clip(link)
|
||||
}
|
||||
@@ -47,7 +46,7 @@
|
||||
</script>
|
||||
|
||||
<article
|
||||
id="post-{number}"
|
||||
id="post-{event.id}"
|
||||
data-event={event.id}
|
||||
class="border-b border-base-content/15 bg-base-100">
|
||||
<div class="flex flex-col md:flex-row">
|
||||
@@ -73,8 +72,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}>
|
||||
Post #{number}
|
||||
<Icon icon={SquareArrowRightUp} size={3} />
|
||||
<Icon icon={LinkRound} size={3} />
|
||||
Permalink
|
||||
</Button>
|
||||
</div>
|
||||
<div class="px-3 py-4 sm:px-4">
|
||||
|
||||
@@ -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} from "@welshman/lib"
|
||||
import {sortBy, partition, spec, max, pushToMapKey, groupBy} 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 {roomsByUrl} from "@app/groups"
|
||||
import {displayRoom} from "@app/groups"
|
||||
import {makeCommentFilter} from "@app/content"
|
||||
import {makeFeed} from "@app/feeds"
|
||||
import {pushModal} from "@app/modal"
|
||||
@@ -43,25 +43,9 @@
|
||||
|
||||
const items = sortBy(e => -max([...(scores.get(e.id) || []), e.created_at]), threads)
|
||||
|
||||
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())),
|
||||
),
|
||||
)
|
||||
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()))
|
||||
|
||||
return {items, boards}
|
||||
})
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
const search = params.toString()
|
||||
|
||||
goto(`${$page.url.pathname}${search ? `?${search}` : ""}`, {
|
||||
replaceState: true,
|
||||
keepFocus: true,
|
||||
noScroll: true,
|
||||
})
|
||||
@@ -92,6 +91,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
const clearReplyParent = () => {
|
||||
if ($event) {
|
||||
replyTo = $event
|
||||
}
|
||||
}
|
||||
|
||||
const scrollToPost = (hash: string) => {
|
||||
const element = document.getElementById(hash)
|
||||
|
||||
@@ -102,23 +107,30 @@
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -145,13 +157,11 @@
|
||||
<PageContent class="flex flex-col">
|
||||
{#if $event}
|
||||
<div class="border-y border-base-content/15 bg-base-100">
|
||||
{#each pagePosts as post, i (post.id)}
|
||||
{@const number = (currentPage - 1) * POSTS_PER_PAGE + i + 1}
|
||||
{#each pagePosts as post (post.id)}
|
||||
<ThreadPost
|
||||
{url}
|
||||
threadId={id}
|
||||
event={post}
|
||||
{number}
|
||||
threadPubkey={$event.pubkey}
|
||||
onReply={openReply} />
|
||||
{/each}
|
||||
@@ -159,8 +169,14 @@
|
||||
{#if pageCount > 1}
|
||||
<ThreadPagination page={currentPage} {pageCount} onPage={setPage} />
|
||||
{/if}
|
||||
{#if showReply && replyTo}
|
||||
<EventReply {url} event={replyTo} onClose={closeReply} onSubmit={closeReply} />
|
||||
{#if showReply && replyTo && $event}
|
||||
<EventReply
|
||||
{url}
|
||||
event={$event}
|
||||
parent={replyTo.id === $event.id ? undefined : replyTo}
|
||||
onClose={closeReply}
|
||||
onClearParent={clearReplyParent}
|
||||
onSubmit={closeReply} />
|
||||
{:else}
|
||||
<div class="flex justify-end p-4">
|
||||
<Button class="btn btn-primary" onclick={openThreadReply}>
|
||||
|
||||
Reference in New Issue
Block a user