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:
parent
eb400a8a6a
commit
3ad1a72d84
|
|
@ -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>
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import Reactions from './Reactions.svelte';
|
import Reactions from './Reactions.svelte';
|
||||||
import ReplyList from './ReplyList.svelte';
|
import ReplyList from './ReplyList.svelte';
|
||||||
import ReplyComposer from './ReplyComposer.svelte';
|
import ReplyComposer from './ReplyComposer.svelte';
|
||||||
|
import ExternalClientLinks from './ExternalClientLinks.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
|
|
@ -71,6 +72,7 @@
|
||||||
|
|
||||||
{#if dtag}
|
{#if dtag}
|
||||||
<Reactions {dtag} />
|
<Reactions {dtag} />
|
||||||
|
<ExternalClientLinks {dtag} />
|
||||||
<ReplyComposer {dtag} eventId={event.id} onPublished={handlePublished} />
|
<ReplyComposer {dtag} eventId={event.id} onPublished={handlePublished} />
|
||||||
<ReplyList {dtag} optimistic={optimisticReplies} />
|
<ReplyList {dtag} optimistic={optimisticReplies} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { NostrEvent, Profile } from '$lib/nostr/loaders';
|
import type { NostrEvent, Profile } from '$lib/nostr/loaders';
|
||||||
import { getProfile } from '$lib/nostr/profileCache';
|
import { getProfile } from '$lib/nostr/profileCache';
|
||||||
|
import { buildNjumpProfileUrl } from '$lib/nostr/naddr';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
|
|
@ -10,6 +11,7 @@
|
||||||
|
|
||||||
const date = $derived(new Date(event.created_at * 1000).toLocaleString('de-DE'));
|
const date = $derived(new Date(event.created_at * 1000).toLocaleString('de-DE'));
|
||||||
const npubPrefix = $derived(event.pubkey.slice(0, 12) + '…');
|
const npubPrefix = $derived(event.pubkey.slice(0, 12) + '…');
|
||||||
|
const profileUrl = $derived(buildNjumpProfileUrl(event.pubkey));
|
||||||
|
|
||||||
let profile = $state<Profile | null>(null);
|
let profile = $state<Profile | null>(null);
|
||||||
|
|
||||||
|
|
@ -25,7 +27,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<li class="reply">
|
<li class="reply">
|
||||||
<div class="header">
|
<a class="header" href={profileUrl} target="_blank" rel="noopener">
|
||||||
{#if profile?.picture}
|
{#if profile?.picture}
|
||||||
<img class="avatar" src={profile.picture} alt={displayName} />
|
<img class="avatar" src={profile.picture} alt={displayName} />
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -35,7 +37,7 @@
|
||||||
<span class="name">{displayName}</span>
|
<span class="name">{displayName}</span>
|
||||||
<span class="date">{date}</span>
|
<span class="date">{date}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
<div class="content">{event.content}</div>
|
<div class="content">{event.content}</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
@ -50,6 +52,17 @@
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 0.4rem;
|
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 {
|
.avatar {
|
||||||
flex: 0 0 32px;
|
flex: 0 0 32px;
|
||||||
|
|
|
||||||
|
|
@ -32,3 +32,34 @@ export function buildNaddr(args: NaddrArgs): string {
|
||||||
export function buildHablaLink(args: NaddrArgs): string {
|
export function buildHablaLink(args: NaddrArgs): string {
|
||||||
return `${HABLA_BASE}${buildNaddr(args)}`;
|
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}` }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue