From 2b26c033f6993cfdb6a776e4918fe1c815ec964d Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Mon, 21 Sep 2026 10:49:17 -0700 Subject: [PATCH] Improve permalinks --- src/app/components/RoomChat.svelte | 11 +++++++--- src/app/routes.ts | 16 +++++++++++---- src/lib/components/base.css | 4 +--- src/routes/spaces/[relay]/[h]/+layout.svelte | 2 +- .../spaces/[relay]/threads/[id]/+page.svelte | 20 ++++--------------- 5 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/app/components/RoomChat.svelte b/src/app/components/RoomChat.svelte index 486b25b5..72410045 100644 --- a/src/app/components/RoomChat.svelte +++ b/src/app/components/RoomChat.svelte @@ -6,7 +6,7 @@ import type {Readable} from "svelte/store" import {debounce} from "throttle-debounce" import cx from "classnames" - import {now, ifLet, ago, MINUTE} from "@welshman/lib" + import {now, ifLet, ago, spec, MINUTE} from "@welshman/lib" import type {Maybe} from "@welshman/lib" import type {TrustedEvent, EventContent} from "@welshman/util" import {relay, stamp, MESSAGE, RELAY_ADD_MEMBER, ROOM_ADD_MEMBER} from "@welshman/util" @@ -47,7 +47,7 @@ import {isFeedLoading, makeFeed, makeFeedContext, makeScrollLoader} from "@app/feeds" import {pageLoading} from "@app/loading" import {checked, deferredRoomPath, setChecked} from "@app/notifications" - import {highlightedEvent, makeRoomPath} from "@app/routes" + import {getPermalinkTarget, highlightedEvent, makeRoomPath} from "@app/routes" import {pendingShare, type Share} from "@app/share" import {pushToast} from "@app/toast" @@ -117,6 +117,7 @@ : readable(MembershipStatus.Granted) const at = $derived(parseInt($page.url.searchParams.get("at")!)) const inviteCode = $derived($page.url.searchParams.get("code") || "") + const target = $derived(getPermalinkTarget($page.url)) const join = async () => { if (h) { @@ -334,7 +335,9 @@ } if (!released && !pinned && !isNaN(at)) { - const targetEvent = $events.find(event => event.created_at >= at) + // The pointer only helps once that message has arrived, so the anchor still carries the jump. + const pointedAt = target ? $events.find(spec({id: target})) : undefined + const targetEvent = pointedAt ?? $events.find(event => event.created_at >= at) if (targetEvent) { scrollToRow(targetEvent.id, {highlight: true, pin: true}) @@ -697,6 +700,8 @@

{#if reachedStartOfHistory} End of message history + {:else} + Looking for messages... {/if}

{/if} diff --git a/src/app/routes.ts b/src/app/routes.ts index 70815960..05d6af41 100644 --- a/src/app/routes.ts +++ b/src/app/routes.ts @@ -1,7 +1,7 @@ import {get, writable} from "svelte/store" import * as nip19 from "nostr-tools/nip19" import {page} from "$app/stores" -import {identity} from "@welshman/lib" +import {identity, tryCatch} from "@welshman/lib" import type {TrustedEvent} from "@welshman/util" import { CLASSIFIED, @@ -32,8 +32,7 @@ export let lastChatUrl: string | undefined = undefined export const lastPageBySpaceUrl = new Map() -// The event a link pointed at. A store rather than a class set on the node, because each row owns -// its own class attribute and drops anything written behind its back on the next render. +// A store rather than a class on the node, which each row's class attribute would overwrite. export const highlightedEvent = writable(undefined) // The space the user was in most recently, so the space menu can be opened from a page that isn't @@ -237,7 +236,16 @@ export const makeEventPermalink = (event: TrustedEvent, url?: string) => { return path } - return `${PLATFORM_URL}${path}#${nip19.neventEncode({id: event.id, relays: urls})}` + const pointer = nip19.neventEncode({id: event.id, relays: urls}) + + return `${PLATFORM_URL}${path}${path.includes("?") ? "&" : "?"}event=${pointer}` +} + +export const getPermalinkTarget = (url: URL) => { + const pointer = url.searchParams.get("event") + const decoded = pointer ? tryCatch(() => nip19.decode(pointer)) : undefined + + return decoded?.type === "nevent" ? decoded.data.id : undefined } export const scrollToEvent = (id: string) => { diff --git a/src/lib/components/base.css b/src/lib/components/base.css index ace68fe5..89d496c1 100644 --- a/src/lib/components/base.css +++ b/src/lib/components/base.css @@ -129,9 +129,7 @@ width: var(--rule-bleed-r, 100vw); } } -/* Marks the event a link pointed at. The mark stays until another link is followed, so the reader - can scroll away and still find their place. The wash is a spread inset shadow rather than a - background because a row's hover rule sets its own background and would paint over it. */ +/* The wash is an inset shadow because a row's hover rule sets a background that would cover it. */ @utility highlight-target { box-shadow: inset 3px 0 0 var(--primary), diff --git a/src/routes/spaces/[relay]/[h]/+layout.svelte b/src/routes/spaces/[relay]/[h]/+layout.svelte index bc9b64e1..ed3c3159 100644 --- a/src/routes/spaces/[relay]/[h]/+layout.svelte +++ b/src/routes/spaces/[relay]/[h]/+layout.svelte @@ -9,6 +9,6 @@ const {children}: Props = $props() -{#key $page.url.searchParams.get("at")} +{#key [$page.url.searchParams.get("at"), $page.url.searchParams.get("event")].join()} {@render children?.()} {/key} diff --git a/src/routes/spaces/[relay]/threads/[id]/+page.svelte b/src/routes/spaces/[relay]/threads/[id]/+page.svelte index 3388bb3d..0fcec8be 100644 --- a/src/routes/spaces/[relay]/threads/[id]/+page.svelte +++ b/src/routes/spaces/[relay]/threads/[id]/+page.svelte @@ -1,7 +1,7 @@