flotilla/src/routes/spaces/[relay]/threads/[id]/+page.svelte

203 lines
5.5 KiB
Svelte
Raw Normal View History

2024-10-23 21:20:24 +00:00
<script lang="ts">
2024-11-15 22:03:54 +00:00
import {onMount} from "svelte"
import * as nip19 from "nostr-tools/nip19"
2024-10-23 21:20:24 +00:00
import {page} from "$app/stores"
import {goto} from "$app/navigation"
2025-11-20 23:54:06 +00:00
import {sleep} from "@welshman/lib"
2025-10-13 20:35:02 +00:00
import type {MakeNonOptional} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {COMMENT, getTagValue} from "@welshman/util"
2025-04-09 22:32:18 +00:00
import {repository} from "@welshman/app"
import {request} from "@welshman/net"
import {deriveEventsById, deriveEventsAsc} from "@welshman/store"
2025-09-10 22:26:39 +00:00
import Reply from "@assets/icons/reply-2.svg?dataurl"
2024-10-23 21:20:24 +00:00
import Icon from "@lib/components/Icon.svelte"
2025-03-19 17:30:36 +00:00
import PageContent from "@lib/components/PageContent.svelte"
2024-10-24 18:52:02 +00:00
import Spinner from "@lib/components/Spinner.svelte"
2024-10-23 21:20:24 +00:00
import Button from "@lib/components/Button.svelte"
import Link from "@lib/components/Link.svelte"
import SpaceBar from "@app/components/SpaceBar.svelte"
import ThreadPost from "@app/components/ThreadPost.svelte"
import ThreadPagination from "@app/components/ThreadPagination.svelte"
2025-02-06 18:24:31 +00:00
import EventReply from "@app/components/EventReply.svelte"
import RoomName from "@app/components/RoomName.svelte"
import {deriveEvent} from "@app/repository"
import {decodeRelay} from "@app/relays"
import {makeSpacePath, scrollToEvent} from "@app/routes"
const POSTS_PER_PAGE = 20
2024-10-23 21:20:24 +00:00
2025-10-13 20:35:02 +00:00
const {relay, id} = $page.params as MakeNonOptional<typeof $page.params>
2024-10-23 21:20:24 +00:00
const url = decodeRelay(relay)
2025-11-20 23:54:06 +00:00
const event = deriveEvent(id, [url])
2024-10-24 20:05:25 +00:00
const filters = [{kinds: [COMMENT], "#E": [id]}]
const replies = deriveEventsAsc(deriveEventsById({filters, repository}))
2024-10-23 21:20:24 +00:00
const back = () => history.back()
const posts = $derived.by(() => {
if (!$event) return []
return [$event, ...$replies]
})
const replyCount = $derived(Math.max(0, posts.length - 1))
const h = $derived(getTagValue("h", $event?.tags || []))
const pageCount = $derived(Math.max(1, Math.ceil(posts.length / POSTS_PER_PAGE)))
const currentPage = $derived.by(() => {
const raw = parseInt($page.url.searchParams.get("page") || "1")
if (Number.isNaN(raw) || raw < 1) return 1
if (raw > pageCount) return pageCount
return raw
})
const pagePosts = $derived(
posts.slice((currentPage - 1) * POSTS_PER_PAGE, currentPage * POSTS_PER_PAGE),
)
const setPage = (nextPage: number) => {
const params = new URLSearchParams($page.url.searchParams)
if (nextPage <= 1) {
params.delete("page")
} else {
params.set("page", String(nextPage))
}
const search = params.toString()
goto(`${$page.url.pathname}${search ? `?${search}` : ""}`, {
keepFocus: true,
noScroll: true,
})
}
const openReply = (post: TrustedEvent) => {
replyTo = post
2024-10-23 23:16:28 +00:00
showReply = true
}
2024-10-23 21:20:24 +00:00
2024-10-23 23:16:28 +00:00
const closeReply = () => {
showReply = false
replyTo = undefined
2024-10-23 21:20:24 +00:00
}
2024-10-23 23:16:28 +00:00
const openThreadReply = () => {
if ($event) {
openReply($event)
}
}
const clearReplyParent = () => {
if ($event) {
replyTo = $event
}
2024-11-21 19:52:29 +00:00
}
2025-02-04 00:37:14 +00:00
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("nevent1")) return
let eventId: string
try {
const decoded = nip19.decode(hash)
if (decoded.type !== "nevent") return
eventId = decoded.data.id
} catch {
return
}
const index = posts.findIndex(post => post.id === eventId)
if (index < 0) return
hashHandled = true
const targetPage = Math.ceil((index + 1) / POSTS_PER_PAGE)
if (targetPage !== currentPage) {
setPage(targetPage)
}
setTimeout(() => scrollToEvent(posts[index]!.id), 100)
})
2024-10-24 18:52:02 +00:00
onMount(() => {
2025-04-11 16:27:19 +00:00
const controller = new AbortController()
request({relays: [url], filters, signal: controller.signal})
2024-11-14 18:53:13 +00:00
2024-11-15 22:03:54 +00:00
return () => {
2025-04-11 16:27:19 +00:00
controller.abort()
2024-11-15 22:03:54 +00:00
}
2024-11-14 18:53:13 +00:00
})
2024-10-23 21:20:24 +00:00
</script>
<SpaceBar {back} class="!h-auto min-h-20 py-3">
2025-03-19 17:30:36 +00:00
{#snippet title()}
<div class="flex min-w-0 flex-col gap-0.5">
<h1 class="ellipsize font-bold sm:text-xl">
{getTagValue("title", $event?.tags || []) || ""}
</h1>
<p class="text-xs opacity-75">
{replyCount}
{replyCount === 1 ? "reply" : "replies"}
{#if h}
· <Link href={makeSpacePath(url, h)} class="link">#<RoomName {url} {h} /></Link>
{/if}
</p>
</div>
2025-03-19 17:30:36 +00:00
{/snippet}
</SpaceBar>
2025-03-19 17:30:36 +00:00
<PageContent noPad class="flex flex-col">
2024-10-24 18:52:02 +00:00
{#if $event}
<div class="bg-surface border-y" style="border-color: var(--line)">
{#each pagePosts as post (post.id)}
<ThreadPost {url} event={post} threadPubkey={$event.pubkey} onReply={openReply} />
2025-03-19 17:30:36 +00:00
{/each}
</div>
{#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} />
2025-03-19 17:30:36 +00:00
{:else}
<div class="flex justify-end p-4">
<Button class="button button-primary" onclick={openThreadReply}>
2025-09-10 22:26:39 +00:00
<Icon icon={Reply} />
2025-03-19 17:30:36 +00:00
Reply to thread
2024-11-21 19:52:29 +00:00
</Button>
</div>
{/if}
2024-10-24 18:52:02 +00:00
{:else}
2026-06-04 21:31:07 +00:00
<div class="flex justify-center py-20">
{#await sleep(5000)}
<Spinner loading>Loading thread...</Spinner>
{:then}
<p>Failed to load thread.</p>
{/await}
</div>
2024-10-24 18:52:02 +00:00
{/if}
2025-03-19 17:30:36 +00:00
</PageContent>