From 8d7b640d0f3b78a874db880098fc850d18d09b38 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Sat, 22 Aug 2026 11:20:33 -0700 Subject: [PATCH] Fix thread permalinks --- .../spaces/[relay]/threads/[id]/+page.svelte | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/src/routes/spaces/[relay]/threads/[id]/+page.svelte b/src/routes/spaces/[relay]/threads/[id]/+page.svelte index 05c10621..bc0d151d 100644 --- a/src/routes/spaces/[relay]/threads/[id]/+page.svelte +++ b/src/routes/spaces/[relay]/threads/[id]/+page.svelte @@ -2,7 +2,7 @@ import * as nip19 from "nostr-tools/nip19" import {page} from "$app/stores" import {goto} from "$app/navigation" - import {sleep, spec} from "@welshman/lib" + import {call, sleep, spec, tryCatch} from "@welshman/lib" import type {MakeNonOptional} from "@welshman/lib" import type {TrustedEvent} from "@welshman/util" import {deriveEventsAsc} from "@welshman/store" @@ -98,40 +98,46 @@ let showReply = $state(false) let replyTo: TrustedEvent | undefined = $state() - let hashHandled = $state(false) + + // A permalink's target, read once because setPage drops the hash. Replies stream in newest + // first, so the post a permalink names is usually here long before the ones above it — its + // index, and the page it lands on, are only right once the thread has finished arriving. So + // this is kept and re-evaluated rather than acted on the first time the post shows up. + let target: string | undefined = $state( + call(() => { + const hash = window.location.hash.replace(/^#/, "") + + if (hash.startsWith("nevent1")) { + const decoded = tryCatch(() => nip19.decode(hash)) + + if (decoded?.type === "nevent") { + return decoded.data.id + } + } + }), + ) + + // Paginating by hand hands control back to the reader. + const goToPage = (nextPage: number) => { + target = undefined + + setPage(nextPage) + } $effect(() => { - if (hashHandled || posts.length === 0) return + if (!target) 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(spec({id: eventId})) + const index = posts.findIndex(spec({id: target})) 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) + setTimeout(() => scrollToEvent(target!), 100) }) $effect(() => { @@ -170,7 +176,7 @@ {/each} {#if pageCount > 1} - + {/if} {#if showReply && replyTo && $event}