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