spa: kommentar-author klickbar (njump) + external-client-links am post

Zwei Erweiterungen, die Community-Interaktion an Nostr-native Clients
auslagern statt in der SPA nachzubauen:

1. ReplyItem-Header ist jetzt ein <a href=https://njump.me/<npub>
   target=_blank>. Klick auf Avatar/Name öffnet das vollständige
   Profil des Kommentar-Authors mit allen Events.
2. Neue ExternalClientLinks.svelte zwischen Reactions und Composer:
   dezente Box mit "In Nostr-Client öffnen" — drei Links (Habla,
   Yakihonne, njump) über naddr, damit Leser Thread-Replies,
   Reactions, Teilen dort nutzen können, wo die volle Nostr-Social-
   Layer läuft.

Nostr-Helper erweitert:
- buildNpub(hex) — npub1…-Bech32-Encoding
- buildNjumpProfileUrl(hex) — njump.me/<npub>
- externalClientLinks({pubkey, kind, identifier}) — Liste der drei
  etablierten Langform-Viewer mit naddr1…-URLs.

npm run check: 0 errors, 611 files. Deploy live.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jörg Lohrer 2026-04-15 18:04:23 +02:00
parent eb400a8a6a
commit 3ad1a72d84
4 changed files with 104 additions and 2 deletions

View File

@ -0,0 +1,56 @@
<script lang="ts">
import { externalClientLinks } from '$lib/nostr/naddr';
import { AUTHOR_PUBKEY_HEX } from '$lib/nostr/config';
interface Props {
dtag: string;
}
let { dtag }: Props = $props();
const links = $derived(
externalClientLinks({
pubkey: AUTHOR_PUBKEY_HEX,
kind: 30023,
identifier: dtag
})
);
</script>
<section class="external">
<span class="label">In Nostr-Client öffnen (für Threads, Reactions, Teilen):</span>
<ul>
{#each links as l}
<li><a href={l.url} target="_blank" rel="noopener">{l.label}</a></li>
{/each}
</ul>
</section>
<style>
.external {
margin: 2rem 0 1rem;
padding: 0.8rem 1rem;
background: var(--code-bg);
border-radius: 4px;
font-size: 0.9rem;
}
.label {
display: block;
color: var(--muted);
margin-bottom: 0.4rem;
}
ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 0.8rem;
}
li a {
color: var(--accent);
text-decoration: none;
}
li a:hover {
text-decoration: underline;
}
</style>

View File

@ -5,6 +5,7 @@
import Reactions from './Reactions.svelte';
import ReplyList from './ReplyList.svelte';
import ReplyComposer from './ReplyComposer.svelte';
import ExternalClientLinks from './ExternalClientLinks.svelte';
interface Props {
event: NostrEvent;
@ -71,6 +72,7 @@
{#if dtag}
<Reactions {dtag} />
<ExternalClientLinks {dtag} />
<ReplyComposer {dtag} eventId={event.id} onPublished={handlePublished} />
<ReplyList {dtag} optimistic={optimisticReplies} />
{/if}

View File

@ -2,6 +2,7 @@
import { onMount } from 'svelte';
import type { NostrEvent, Profile } from '$lib/nostr/loaders';
import { getProfile } from '$lib/nostr/profileCache';
import { buildNjumpProfileUrl } from '$lib/nostr/naddr';
interface Props {
event: NostrEvent;
@ -10,6 +11,7 @@
const date = $derived(new Date(event.created_at * 1000).toLocaleString('de-DE'));
const npubPrefix = $derived(event.pubkey.slice(0, 12) + '…');
const profileUrl = $derived(buildNjumpProfileUrl(event.pubkey));
let profile = $state<Profile | null>(null);
@ -25,7 +27,7 @@
</script>
<li class="reply">
<div class="header">
<a class="header" href={profileUrl} target="_blank" rel="noopener">
{#if profile?.picture}
<img class="avatar" src={profile.picture} alt={displayName} />
{:else}
@ -35,7 +37,7 @@
<span class="name">{displayName}</span>
<span class="date">{date}</span>
</div>
</div>
</a>
<div class="content">{event.content}</div>
</li>
@ -50,6 +52,17 @@
gap: 0.6rem;
align-items: center;
margin-bottom: 0.4rem;
color: inherit;
text-decoration: none;
border-radius: 4px;
padding: 2px;
margin-left: -2px;
}
.header:hover {
background: var(--code-bg);
}
.header:hover .name {
color: var(--accent);
}
.avatar {
flex: 0 0 32px;

View File

@ -32,3 +32,34 @@ export function buildNaddr(args: NaddrArgs): string {
export function buildHablaLink(args: NaddrArgs): string {
return `${HABLA_BASE}${buildNaddr(args)}`;
}
/**
* `npub1…`-Bech32-String für einen Pubkey für Profil-Links außerhalb
* der SPA (z. B. njump.me).
*/
export function buildNpub(pubkeyHex: string): string {
return nip19.npubEncode(pubkeyHex);
}
/**
* njump.me-Profil-URL. Öffnet das Nostr-native Profil-Browser mit
* vollständiger Event-Historie.
*/
export function buildNjumpProfileUrl(pubkeyHex: string): string {
return `https://njump.me/${buildNpub(pubkeyHex)}`;
}
/**
* Liste externer Nostr-Clients für Post öffnen in "-Links.
* Nutzt naddr, damit jeder Client das addressable Event adressieren kann.
*/
export function externalClientLinks(
args: NaddrArgs
): { label: string; url: string }[] {
const naddr = buildNaddr(args);
return [
{ label: 'Habla', url: `https://habla.news/a/${naddr}` },
{ label: 'Yakihonne', url: `https://yakihonne.com/article/${naddr}` },
{ label: 'njump', url: `https://njump.me/${naddr}` }
];
}