Improve permalinks

This commit is contained in:
Jon Staab 2026-09-21 10:49:17 -07:00
parent 4b8fcdf196
commit 2b26c033f6
5 changed files with 26 additions and 27 deletions

View file

@ -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 @@
<p class="flex h-10 items-center justify-center py-20">
{#if reachedStartOfHistory}
End of message history
{:else}
<Spinner loading>Looking for messages...</Spinner>
{/if}
</p>
{/if}

View file

@ -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<string, string>()
// 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<string | undefined>(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) => {

View file

@ -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),

View file

@ -9,6 +9,6 @@
const {children}: Props = $props()
</script>
{#key $page.url.searchParams.get("at")}
{#key [$page.url.searchParams.get("at"), $page.url.searchParams.get("event")].join()}
{@render children?.()}
{/key}

View file

@ -1,7 +1,7 @@
<script lang="ts">
import {onDestroy} from "svelte"
import * as nip19 from "nostr-tools/nip19"
import {call, sleep, spec, tryCatch} from "@welshman/lib"
import {page} from "$app/state"
import {sleep, spec} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {deriveEventsAsc} from "@welshman/store"
import {getCommentFiltersForRoot, tagValue, tagSpec} from "@welshman/util"
@ -19,7 +19,7 @@
import {network} from "@app/core"
import {makeFeedContext} from "@app/feeds"
import {decodeRelay} from "@app/relays"
import {makeSpacePath, scrollToEvent} from "@app/routes"
import {getPermalinkTarget, makeSpacePath, scrollToEvent} from "@app/routes"
import type {PageProps} from "./$types"
const REPLY_BATCH_SIZE = 20
@ -42,19 +42,7 @@
// A permalink's target. Replies stream in newest first, so the post it names is usually here
// long before the ones above it — its position is only right once the thread has finished
// arriving, so this is kept and re-read rather than acted on the first time it 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
}
}
}),
)
let target: string | undefined = $state(getPermalinkTarget(page.url))
let revealed = $state(REPLY_BATCH_SIZE)