Add a "Share link" action to posts

This commit is contained in:
dtonon 2026-08-28 21:35:47 +01:00
parent bfad456d82
commit 0f487eb33a

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount, tick } from "svelte"; import { onMount, tick } from "svelte";
import { page } from "$app/state"; import { page } from "$app/state";
import { goto } from "$app/navigation"; import { afterNavigate, goto } from "$app/navigation";
import * as nip19 from "@nostr/tools/nip19"; import * as nip19 from "@nostr/tools/nip19";
import { import {
threadDetailStore, threadDetailStore,
@ -208,6 +208,20 @@
); );
} }
// The OP shares the thread URL, a reply its anchored URL.
async function shareLink(p: PostData, isOp: boolean) {
if (!detail) return;
openMenuId = null;
const url =
page.url.origin + `/thread/${detail.id}` + (isOp ? "" : `#post-${p.id}`);
try {
await navigator.clipboard.writeText(url);
showToast("Link copied");
} catch {
showToast("Could not copy the link");
}
}
$effect(() => { $effect(() => {
if (!openMenuId) return; if (!openMenuId) return;
const close = () => (openMenuId = null); const close = () => (openMenuId = null);
@ -255,20 +269,26 @@
return clearHighlights; return clearHighlights;
}); });
// Arriving with a #post-… hash (a reply link): the target only exists once // Arriving with a #post-… hash (a reply link): scroll to the post once per
// the replies have loaded, so scroll when it appears, once per navigation. // navigation. It may only exist once the replies have loaded, so re-check as
// they stream in; on a direct load it is already there and SvelteKit's own
// deep-link scroll runs after ours, so afterNavigate applies ours again.
let scrolledHashFor = ""; let scrolledHashFor = "";
$effect(() => { function scrollToHash(force = false) {
const hash = page.url.hash; const hash = page.url.hash;
allPosts; // Re-check as replies stream in
if (!threadEl || !hash.startsWith("#post-")) return; if (!threadEl || !hash.startsWith("#post-")) return;
const key = `${page.params.id}|${hash}`; const key = `${page.params.id}|${hash}`;
if (scrolledHashFor === key) return; if (!force && scrolledHashFor === key) return;
const el = document.getElementById(hash.slice(1)); const el = document.getElementById(hash.slice(1));
if (!el) return; if (!el) return;
scrolledHashFor = key; scrolledHashFor = key;
scrollToPost(el); scrollToPost(el);
}
$effect(() => {
allPosts;
scrollToHash();
}); });
afterNavigate(() => scrollToHash(true));
// On desktop the thread scrolls inside <main> under a sticky title with a // On desktop the thread scrolls inside <main> under a sticky title with a
// fade below it, so place the post under both; elsewhere the scroll margin // fade below it, so place the post under both; elsewhere the scroll margin
@ -470,9 +490,6 @@
> >
</div> </div>
<div class="ml-4 flex flex-shrink-0 items-center gap-1"> <div class="ml-4 flex flex-shrink-0 items-center gap-1">
{#if canDeletePost(p)}
{@const tooOld =
!canModerate && !withinSelfDeleteWindow(p.createdAt)}
<div class="relative"> <div class="relative">
<button <button
onclick={(e) => toggleMenu(p.id, e)} onclick={(e) => toggleMenu(p.id, e)}
@ -497,6 +514,18 @@
role="menu" role="menu"
class="absolute top-7 right-0 z-20 w-36 rounded-lg border border-neutral-100 bg-white py-1 text-sm shadow-lg dark:border-neutral-800 dark:bg-neutral-900" class="absolute top-7 right-0 z-20 w-36 rounded-lg border border-neutral-100 bg-white py-1 text-sm shadow-lg dark:border-neutral-800 dark:bg-neutral-900"
> >
<button
role="menuitem"
onclick={(e) => {
e.stopPropagation();
shareLink(p, index === 0);
}}
class="w-full px-3 py-1.5 text-left text-neutral-700 hover:bg-neutral-50 dark:text-neutral-300 dark:hover:bg-neutral-800"
>Share link</button
>
{#if canDeletePost(p)}
{@const tooOld =
!canModerate && !withinSelfDeleteWindow(p.createdAt)}
<button <button
role="menuitem" role="menuitem"
disabled={tooOld} disabled={tooOld}
@ -507,10 +536,10 @@
class="w-full px-3 py-1.5 text-left text-red-600 hover:bg-red-50 disabled:cursor-not-allowed disabled:text-neutral-400 disabled:hover:bg-transparent dark:disabled:text-neutral-600" class="w-full px-3 py-1.5 text-left text-red-600 hover:bg-red-50 disabled:cursor-not-allowed disabled:text-neutral-400 disabled:hover:bg-transparent dark:disabled:text-neutral-600"
>Delete{tooOld ? " (too old)" : ""}</button >Delete{tooOld ? " (too old)" : ""}</button
> >
</div>
{/if} {/if}
</div> </div>
{/if} {/if}
</div>
<span class="text-sm text-neutral-400 dark:text-neutral-500" <span class="text-sm text-neutral-400 dark:text-neutral-500"
>{formatDate(p.createdAt)}</span >{formatDate(p.createdAt)}</span
> >