feat: update AppHeader and useAccounts for improved user profile handling

- Replaced robohash image source with user-defined profile picture in AppHeader.
- Enhanced name display with truncation for better UI consistency.
- Introduced caching and fetching of Nostr profile metadata in useAccounts.
- Added computed properties for active profile and profile picture to streamline user data retrieval.

This update improves the user experience by ensuring accurate profile representation and efficient data management.
This commit is contained in:
Dorian
2026-02-14 11:13:21 +00:00
parent 674c9f80c5
commit ca3d390180
2 changed files with 99 additions and 12 deletions

View File

@@ -193,12 +193,12 @@
class="profile-button flex items-center gap-2"
>
<img
v-if="nostrActivePubkey"
:src="`https://robohash.org/${nostrActivePubkey}.png`"
class="w-8 h-8 rounded-full"
v-if="nostrProfilePicture"
:src="nostrProfilePicture"
class="w-8 h-8 rounded-full object-cover"
alt="Avatar"
/>
<span class="text-white text-sm font-medium">{{ nostrActiveName }}</span>
<span class="text-white text-sm font-medium truncate max-w-[140px]">{{ nostrActiveName }}</span>
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': dropdownOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
@@ -268,12 +268,12 @@
<!-- Mobile -->
<div class="md:hidden flex items-center gap-2 mr-2">
<img
v-if="hasNostrSession && nostrActivePubkey"
:src="`https://robohash.org/${nostrActivePubkey}.png`"
class="w-7 h-7 rounded-full"
v-if="hasNostrSession && nostrProfilePicture"
:src="nostrProfilePicture"
class="w-7 h-7 rounded-full object-cover"
alt="Avatar"
/>
<span v-if="hasNostrSession" class="text-white text-sm font-medium">{{ nostrActiveName }}</span>
<span v-if="hasNostrSession" class="text-white text-sm font-medium truncate max-w-[100px]">{{ nostrActiveName }}</span>
<template v-else-if="isAuthenticated">
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-orange-500 to-pink-500 flex items-center justify-center text-xs font-bold text-white">
{{ userInitials }}
@@ -329,8 +329,9 @@ const route = useRoute()
const { user, isAuthenticated, isFilmmaker: isFilmmakerComputed, loginWithNostr: _appLoginWithNostr, logout: appLogout } = useAuth()
const {
isLoggedIn: nostrLoggedIn,
activePubkey: nostrActivePubkey,
activePubkey: _nostrActivePubkey,
activeName: nostrActiveName,
activeProfilePicture: nostrProfilePicture,
// Hidden in production for now (see template comment)
testPersonas: _testPersonas,
tastemakerPersonas: _tastemakerPersonas,

View File

@@ -1,11 +1,24 @@
import { ref, computed, onUnmounted } from 'vue'
import { ref, computed, watch, onUnmounted } from 'vue'
import { Accounts } from 'applesauce-accounts'
import { onlyEvents } from 'applesauce-relay'
import { accountManager, AmberClipboardSigner, AmberClipboardAccount } from '../lib/accounts'
import { TEST_PERSONAS, TASTEMAKER_PERSONAS } from '../data/testPersonas'
import { pool, APP_RELAYS, LOOKUP_RELAYS } from '../lib/relay'
import type { Subscription } from 'rxjs'
type Persona = { name: string; nsec: string; pubkey: string }
/** Nostr kind 0 profile metadata */
interface NostrProfileMetadata {
name?: string
display_name?: string
picture?: string
about?: string
nip05?: string
lud16?: string
banner?: string
}
/**
* Vue composable for Nostr account management.
* Wraps applesauce AccountManager observables as Vue reactive refs.
@@ -17,6 +30,9 @@ export function useAccounts() {
const isLoggingIn = ref(false)
const loginError = ref<string | null>(null)
/** Cached kind 0 profile metadata keyed by pubkey */
const profileCache = ref<Map<string, NostrProfileMetadata>>(new Map())
const subscriptions: Subscription[] = []
// Subscribe to active account changes
@@ -34,9 +50,24 @@ export function useAccounts() {
const isLoggedIn = computed(() => activeAccount.value !== null)
const activePubkey = computed(() => activeAccount.value?.pubkey ?? null)
/** The current user's kind 0 profile metadata (fetched from relays) */
const activeProfile = computed<NostrProfileMetadata | null>(() => {
if (!activePubkey.value) return null
return profileCache.value.get(activePubkey.value) ?? null
})
/**
* Display name: prefer Nostr profile → test persona → pubkey slice.
*/
const activeName = computed(() => {
if (!activeAccount.value) return null
// Check test personas for name
// First: check if we have kind 0 profile metadata
const profile = activeProfile.value
if (profile?.display_name) return profile.display_name
if (profile?.name) return profile.name
// Second: check test personas
const allPersonas: Persona[] = [
...(TEST_PERSONAS as unknown as Persona[]),
...(TASTEMAKER_PERSONAS as unknown as Persona[]),
@@ -44,9 +75,62 @@ export function useAccounts() {
const match = allPersonas.find(
(p) => p.pubkey === activeAccount.value?.pubkey,
)
return match?.name ?? activeAccount.value?.pubkey?.slice(0, 8) ?? 'Unknown'
if (match?.name) return match.name
// Fallback: first 8 chars of pubkey
return activeAccount.value?.pubkey?.slice(0, 8) ?? 'Unknown'
})
/**
* Profile picture URL: prefer Nostr profile picture → robohash fallback.
*/
const activeProfilePicture = computed(() => {
const profile = activeProfile.value
if (profile?.picture) return profile.picture
if (activePubkey.value) return `https://robohash.org/${activePubkey.value}.png`
return null
})
/**
* Fetch kind 0 profile metadata for a pubkey from relays.
* Results are cached to avoid redundant relay queries.
*/
function fetchProfile(pubkey: string) {
if (!pubkey || profileCache.value.has(pubkey)) return
const relays = [...APP_RELAYS, ...LOOKUP_RELAYS]
try {
const profileSub = pool
.subscription(relays, [
{ kinds: [0], authors: [pubkey], limit: 1 },
])
.pipe(onlyEvents())
.subscribe({
next: (event: any) => {
try {
const metadata: NostrProfileMetadata = JSON.parse(event.content)
const updated = new Map(profileCache.value)
updated.set(pubkey, metadata)
profileCache.value = updated
} catch {
// Invalid JSON in profile event
}
},
})
// Close subscription after 5 seconds
setTimeout(() => profileSub.unsubscribe(), 5000)
subscriptions.push(profileSub)
} catch (err) {
console.error(`[useAccounts] Failed to fetch profile for ${pubkey}:`, err)
}
}
// Auto-fetch profile when active account changes
watch(activePubkey, (pubkey) => {
if (pubkey) fetchProfile(pubkey)
}, { immediate: true })
/**
* Login with a browser extension (NIP-07)
*/
@@ -170,6 +254,8 @@ export function useAccounts() {
loginError,
activePubkey,
activeName,
activeProfile,
activeProfilePicture,
// Login methods
loginWithExtension,