20 changed files with 5999 additions and 120 deletions
@ -1,60 +1,183 @@ |
|||||||
<script lang='ts'> |
<script lang='ts'> |
||||||
import { Avatar } from 'flowbite-svelte'; |
import { Avatar } from 'flowbite-svelte'; |
||||||
import { type NDKUserProfile } from "@nostr-dev-kit/ndk"; |
import NDK, { type NDKUserProfile } from "@nostr-dev-kit/ndk"; |
||||||
import { ndkInstance } from '$lib/ndk'; |
import { ndkInstance } from '$lib/ndk'; |
||||||
import { userBadge } from '$lib/snippets/UserSnippets.svelte'; |
import { userBadge } from '$lib/snippets/UserSnippets.svelte'; |
||||||
let { pubkey, name = null } = $props(); |
|
||||||
|
|
||||||
const externalProfileDestination = './events?id=' |
// Component configuration types |
||||||
|
type AvatarSize = 'sm' | 'md' | 'lg'; |
||||||
|
|
||||||
let loading = $state(true); |
// Component props interface |
||||||
let anon = $state(false); |
interface $$Props { |
||||||
let npub = $state(''); |
pubkey: string; // Required: The Nostr public key of the user |
||||||
|
name?: string | null; // Optional: Display name override |
||||||
|
showAvatar?: boolean; // Optional: Whether to show the avatar (default: true) |
||||||
|
avatarSize?: AvatarSize; // Optional: Size of the avatar (default: 'md') |
||||||
|
} |
||||||
|
|
||||||
|
// Destructure and set default props |
||||||
|
let { |
||||||
|
pubkey, |
||||||
|
name = null, |
||||||
|
showAvatar = true, |
||||||
|
avatarSize = 'md' as AvatarSize |
||||||
|
} = $props(); |
||||||
|
|
||||||
|
console.log('[InlineProfile] Initialized with props:', { |
||||||
|
pubkey, |
||||||
|
name, |
||||||
|
showAvatar, |
||||||
|
avatarSize |
||||||
|
}); |
||||||
|
|
||||||
|
// Constants |
||||||
|
const EXTERNAL_PROFILE_DESTINATION = './events?id='; |
||||||
|
|
||||||
|
// Component state type definition |
||||||
|
type ProfileState = { |
||||||
|
loading: boolean; // Whether we're currently loading the profile |
||||||
|
error: string | null; // Any error that occurred during loading |
||||||
|
profile: NDKUserProfile | null; // The user's profile data |
||||||
|
npub: string; // The user's npub (bech32 encoded pubkey) |
||||||
|
}; |
||||||
|
|
||||||
|
// Initialize component state |
||||||
|
let state = $state<ProfileState>({ |
||||||
|
loading: true, |
||||||
|
error: null, |
||||||
|
profile: null, |
||||||
|
npub: '' |
||||||
|
}); |
||||||
|
|
||||||
|
// Derived values from state |
||||||
|
const pfp = $derived(state.profile?.image); |
||||||
|
const username = $derived(state.profile?.name); |
||||||
|
const isAnonymous = $derived(!state.profile?.name && !name); |
||||||
|
|
||||||
|
// Log derived values reactively when they change |
||||||
|
$effect(() => { |
||||||
|
console.log('[InlineProfile] Derived values updated:', { |
||||||
|
pfp, |
||||||
|
username, |
||||||
|
isAnonymous, |
||||||
|
hasProfile: !!state.profile, |
||||||
|
hasNpub: !!state.npub, |
||||||
|
profileState: { |
||||||
|
loading: state.loading, |
||||||
|
error: state.error, |
||||||
|
hasProfile: !!state.profile, |
||||||
|
npub: state.npub |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
let profile = $state<NDKUserProfile | null>(null); |
// Avatar size classes mapping |
||||||
let pfp = $derived(profile?.image); |
const avatarClasses: Record<AvatarSize, string> = { |
||||||
let username = $derived(profile?.name); |
sm: 'h-5 w-5', |
||||||
|
md: 'h-7 w-7', |
||||||
|
lg: 'h-9 w-9' |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Fetches user data from NDK |
||||||
|
* @param pubkey - The Nostr public key to fetch data for |
||||||
|
*/ |
||||||
async function fetchUserData(pubkey: string) { |
async function fetchUserData(pubkey: string) { |
||||||
let user; |
console.log('[InlineProfile] fetchUserData called with pubkey:', pubkey); |
||||||
user = $ndkInstance |
|
||||||
.getUser({ pubkey: pubkey ?? undefined }); |
|
||||||
|
|
||||||
npub = user.npub; |
if (!pubkey) { |
||||||
|
console.warn('[InlineProfile] No pubkey provided to fetchUserData'); |
||||||
|
state.error = 'No pubkey provided'; |
||||||
|
state.loading = false; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
console.log('[InlineProfile] Getting NDK instance'); |
||||||
|
const ndk = $ndkInstance as NDK; |
||||||
|
|
||||||
|
console.log('[InlineProfile] Creating NDK user object'); |
||||||
|
const user = ndk.getUser({ pubkey }); |
||||||
|
|
||||||
|
console.log('[InlineProfile] Getting npub'); |
||||||
|
state.npub = user.npub; |
||||||
|
console.log('[InlineProfile] Got npub:', state.npub); |
||||||
|
|
||||||
user.fetchProfile() |
console.log('[InlineProfile] Fetching user profile'); |
||||||
.then(userProfile => { |
const userProfile = await user.fetchProfile(); |
||||||
profile = userProfile; |
console.log('[InlineProfile] Got user profile:', { |
||||||
if (!profile?.name) anon = true; |
name: userProfile?.name, |
||||||
loading = false; |
displayName: userProfile?.displayName, |
||||||
|
nip05: userProfile?.nip05, |
||||||
|
hasImage: !!userProfile?.image |
||||||
}); |
}); |
||||||
|
|
||||||
|
state.profile = userProfile; |
||||||
|
state.loading = false; |
||||||
|
} catch (error) { |
||||||
|
console.error('[InlineProfile] Error fetching user data:', error); |
||||||
|
state.error = error instanceof Error ? error.message : 'Failed to fetch profile'; |
||||||
|
state.loading = false; |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
// Fetch data when component mounts |
/** |
||||||
|
* Shortens an npub string for display |
||||||
|
* @param long - The npub string to shorten |
||||||
|
* @returns Shortened npub string |
||||||
|
*/ |
||||||
|
function shortenNpub(long: string | undefined): string { |
||||||
|
if (!long) return ''; |
||||||
|
const shortened = `${long.slice(0, 8)}…${long.slice(-4)}`; |
||||||
|
console.log('[InlineProfile] Shortened npub:', { original: long, shortened }); |
||||||
|
return shortened; |
||||||
|
} |
||||||
|
|
||||||
|
// Effect to fetch user data when pubkey changes |
||||||
$effect(() => { |
$effect(() => { |
||||||
|
console.log('[InlineProfile] Effect triggered, pubkey:', pubkey); |
||||||
if (pubkey) { |
if (pubkey) { |
||||||
fetchUserData(pubkey); |
fetchUserData(pubkey); |
||||||
|
} else { |
||||||
|
console.warn('[InlineProfile] No pubkey available for effect'); |
||||||
} |
} |
||||||
}); |
}); |
||||||
|
|
||||||
function shortenNpub(long: string|undefined) { |
|
||||||
if (!long) return ''; |
|
||||||
return long.slice(0, 8) + '…' + long.slice(-4); |
|
||||||
} |
|
||||||
</script> |
</script> |
||||||
|
|
||||||
{#if loading} |
<!-- Component Template --> |
||||||
|
{#if state.loading} |
||||||
|
<!-- Loading state --> |
||||||
|
<span class="animate-pulse" title="Loading profile..."> |
||||||
{name ?? '…'} |
{name ?? '…'} |
||||||
{:else if anon } |
</span> |
||||||
{@render userBadge(npub, username)} |
{:else if state.error} |
||||||
{:else if npub } |
<!-- Error state --> |
||||||
<a href={externalProfileDestination + npub} title={name ?? username}> |
<span class="text-red-500" title={state.error}> |
||||||
<Avatar rounded |
{name ?? shortenNpub(pubkey)} |
||||||
class='h-7 w-7 mx-1 cursor-pointer inline bg-transparent' |
</span> |
||||||
|
{:else if isAnonymous} |
||||||
|
<!-- Anonymous user state --> |
||||||
|
{@render userBadge(pubkey, name)} |
||||||
|
{:else if state.npub} |
||||||
|
<!-- Authenticated user with profile --> |
||||||
|
<a |
||||||
|
href={EXTERNAL_PROFILE_DESTINATION + state.npub} |
||||||
|
title={name ?? username} |
||||||
|
class="inline-flex items-center hover:opacity-80 transition-opacity" |
||||||
|
> |
||||||
|
{#if showAvatar} |
||||||
|
<Avatar |
||||||
|
rounded |
||||||
|
class={`${avatarClasses[avatarSize]} mx-1 cursor-pointer inline bg-transparent`} |
||||||
src={pfp} |
src={pfp} |
||||||
alt={username} /> |
alt={username ?? 'User avatar'} |
||||||
{@render userBadge(npub, username)} |
/> |
||||||
|
{/if} |
||||||
|
{@render userBadge(pubkey, name)} |
||||||
</a> |
</a> |
||||||
{:else} |
{:else} |
||||||
{name ?? pubkey} |
<!-- Fallback state --> |
||||||
|
<span title="No profile data available"> |
||||||
|
{name ?? shortenNpub(pubkey)} |
||||||
|
</span> |
||||||
{/if} |
{/if} |
||||||
@ -1,15 +1,19 @@ |
|||||||
<script module lang='ts'> |
<script module lang='ts'> |
||||||
import { createProfileLink, createProfileLinkWithVerification } from '$lib/utils/nostrUtils'; |
import { createProfileLink, createProfileLinkWithVerification, toNpub } from '$lib/utils/nostrUtils'; |
||||||
|
|
||||||
export { userBadge }; |
export { userBadge }; |
||||||
</script> |
</script> |
||||||
|
|
||||||
{#snippet userBadge(identifier: string, displayText: string | undefined)} |
{#snippet userBadge(identifier: string, displayText: string | undefined)} |
||||||
{#await createProfileLinkWithVerification(identifier, displayText)} |
{#if toNpub(identifier)} |
||||||
{@html createProfileLink(identifier, displayText)} |
{#await createProfileLinkWithVerification(toNpub(identifier) as string, displayText)} |
||||||
|
{@html createProfileLink(toNpub(identifier) as string, displayText)} |
||||||
{:then html} |
{:then html} |
||||||
{@html html} |
{@html html} |
||||||
{:catch} |
{:catch} |
||||||
{@html createProfileLink(identifier, displayText)} |
{@html createProfileLink(toNpub(identifier) as string, displayText)} |
||||||
{/await} |
{/await} |
||||||
|
{:else} |
||||||
|
{displayText ?? ''} |
||||||
|
{/if} |
||||||
{/snippet} |
{/snippet} |
||||||
|
|||||||
Loading…
Reference in new issue