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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user