spa: avatar + name für kommentar-authoren via kind:0-profil
- loadProfile(pubkey?) akzeptiert jetzt optional einen Pubkey, default weiterhin AUTHOR_PUBKEY_HEX. - Neuer profileCache.ts: sessionsweiter Cache, Promise-Memoization — paralleles Nachladen derselben Pubkey teilt dieselbe Request. - ReplyItem lädt das kind:0-Profil des Kommentar-Authors on mount, zeigt Avatar (32px rund) + display_name/name. Fallback bei fehlendem Profil: Pubkey-Hex-Prefix (wie bisher). - Home-Page nutzt getProfile(AUTHOR_PUBKEY_HEX) statt loadProfile() direkt — gleicher Cache, kein doppeltes Fetchen. npm run check: 0 errors. E2E 3/3 grün. Deploy live. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
22935d6737
commit
eb400a8a6a
|
|
@ -1,5 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { NostrEvent } from '$lib/nostr/loaders';
|
import { onMount } from 'svelte';
|
||||||
|
import type { NostrEvent, Profile } from '$lib/nostr/loaders';
|
||||||
|
import { getProfile } from '$lib/nostr/profileCache';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
|
|
@ -7,14 +9,32 @@
|
||||||
let { event }: Props = $props();
|
let { event }: Props = $props();
|
||||||
|
|
||||||
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 authorNpub = $derived(event.pubkey.slice(0, 12) + '…');
|
const npubPrefix = $derived(event.pubkey.slice(0, 12) + '…');
|
||||||
|
|
||||||
|
let profile = $state<Profile | null>(null);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
profile = await getProfile(event.pubkey);
|
||||||
|
} catch {
|
||||||
|
profile = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayName = $derived(profile?.display_name || profile?.name || npubPrefix);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<li class="reply">
|
<li class="reply">
|
||||||
<div class="meta">
|
<div class="header">
|
||||||
<span class="author">{authorNpub}</span>
|
{#if profile?.picture}
|
||||||
<span class="sep">·</span>
|
<img class="avatar" src={profile.picture} alt={displayName} />
|
||||||
<span class="date">{date}</span>
|
{:else}
|
||||||
|
<div class="avatar avatar-placeholder" aria-hidden="true"></div>
|
||||||
|
{/if}
|
||||||
|
<div class="meta">
|
||||||
|
<span class="name">{displayName}</span>
|
||||||
|
<span class="date">{date}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">{event.content}</div>
|
<div class="content">{event.content}</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -25,20 +45,43 @@
|
||||||
padding: 0.8rem 0;
|
padding: 0.8rem 0;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.6rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
flex: 0 0 32px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
background: var(--code-bg);
|
||||||
|
}
|
||||||
|
.avatar-placeholder {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
.meta {
|
.meta {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
margin-bottom: 0.3rem;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
.author {
|
.name {
|
||||||
font-family: monospace;
|
color: var(--fg);
|
||||||
}
|
font-weight: 500;
|
||||||
.sep {
|
word-break: break-word;
|
||||||
margin: 0 0.4rem;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
margin-left: calc(32px + 0.6rem);
|
||||||
|
}
|
||||||
|
@media (max-width: 479px) {
|
||||||
|
.content {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -103,12 +103,16 @@ export async function loadPost(dtag: string): Promise<NostrEvent | null> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Profil-Event kind:0 (neueste Version) */
|
/**
|
||||||
export async function loadProfile(): Promise<Profile | null> {
|
* Profil-Event kind:0 (neueste Version).
|
||||||
|
* Default: Autoren-Pubkey der SPA. Optional: beliebiger Pubkey für
|
||||||
|
* die Anzeige fremder Kommentar-Autoren.
|
||||||
|
*/
|
||||||
|
export async function loadProfile(pubkey: string = AUTHOR_PUBKEY_HEX): Promise<Profile | null> {
|
||||||
const relays = get(readRelays);
|
const relays = get(readRelays);
|
||||||
const events = await collectEvents(relays, {
|
const events = await collectEvents(relays, {
|
||||||
kinds: [0],
|
kinds: [0],
|
||||||
authors: [AUTHOR_PUBKEY_HEX],
|
authors: [pubkey],
|
||||||
limit: 1
|
limit: 1
|
||||||
});
|
});
|
||||||
if (events.length === 0) return null;
|
if (events.length === 0) return null;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import type { Profile } from './loaders';
|
||||||
|
import { loadProfile } from './loaders';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sessionsweiter Cache für kind:0-Profile.
|
||||||
|
* Jeder Pubkey wird maximal einmal angefragt; mehrfache parallele
|
||||||
|
* Aufrufe teilen sich dieselbe Promise.
|
||||||
|
*/
|
||||||
|
const cache = new Map<string, Promise<Profile | null>>();
|
||||||
|
|
||||||
|
export function getProfile(pubkey: string): Promise<Profile | null> {
|
||||||
|
const existing = cache.get(pubkey);
|
||||||
|
if (existing) return existing;
|
||||||
|
const pending = loadProfile(pubkey);
|
||||||
|
cache.set(pubkey, pending);
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
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 { loadPostList, loadProfile } from '$lib/nostr/loaders';
|
import { loadPostList } from '$lib/nostr/loaders';
|
||||||
|
import { getProfile } from '$lib/nostr/profileCache';
|
||||||
|
import { AUTHOR_PUBKEY_HEX } from '$lib/nostr/config';
|
||||||
import ProfileCard from '$lib/components/ProfileCard.svelte';
|
import ProfileCard from '$lib/components/ProfileCard.svelte';
|
||||||
import PostCard from '$lib/components/PostCard.svelte';
|
import PostCard from '$lib/components/PostCard.svelte';
|
||||||
import LoadingOrError from '$lib/components/LoadingOrError.svelte';
|
import LoadingOrError from '$lib/components/LoadingOrError.svelte';
|
||||||
|
|
@ -13,7 +15,7 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
const [p, list] = await Promise.all([loadProfile(), loadPostList()]);
|
const [p, list] = await Promise.all([getProfile(AUTHOR_PUBKEY_HEX), loadPostList()]);
|
||||||
profile = p;
|
profile = p;
|
||||||
posts = list;
|
posts = list;
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue