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">
|
||||
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 {
|
||||
event: NostrEvent;
|
||||
|
|
@ -7,15 +9,33 @@
|
|||
let { event }: Props = $props();
|
||||
|
||||
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>
|
||||
|
||||
<li class="reply">
|
||||
<div class="header">
|
||||
{#if profile?.picture}
|
||||
<img class="avatar" src={profile.picture} alt={displayName} />
|
||||
{:else}
|
||||
<div class="avatar avatar-placeholder" aria-hidden="true"></div>
|
||||
{/if}
|
||||
<div class="meta">
|
||||
<span class="author">{authorNpub}</span>
|
||||
<span class="sep">·</span>
|
||||
<span class="name">{displayName}</span>
|
||||
<span class="date">{date}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">{event.content}</div>
|
||||
</li>
|
||||
|
||||
|
|
@ -25,20 +45,43 @@
|
|||
padding: 0.8rem 0;
|
||||
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 {
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
margin-bottom: 0.3rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.author {
|
||||
font-family: monospace;
|
||||
}
|
||||
.sep {
|
||||
margin: 0 0.4rem;
|
||||
opacity: 0.5;
|
||||
.name {
|
||||
color: var(--fg);
|
||||
font-weight: 500;
|
||||
word-break: break-word;
|
||||
}
|
||||
.content {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
margin-left: calc(32px + 0.6rem);
|
||||
}
|
||||
@media (max-width: 479px) {
|
||||
.content {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</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 events = await collectEvents(relays, {
|
||||
kinds: [0],
|
||||
authors: [AUTHOR_PUBKEY_HEX],
|
||||
authors: [pubkey],
|
||||
limit: 1
|
||||
});
|
||||
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">
|
||||
import { onMount } from 'svelte';
|
||||
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 PostCard from '$lib/components/PostCard.svelte';
|
||||
import LoadingOrError from '$lib/components/LoadingOrError.svelte';
|
||||
|
|
@ -13,7 +15,7 @@
|
|||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const [p, list] = await Promise.all([loadProfile(), loadPostList()]);
|
||||
const [p, list] = await Promise.all([getProfile(AUTHOR_PUBKEY_HEX), loadPostList()]);
|
||||
profile = p;
|
||||
posts = list;
|
||||
loading = false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue