Add ProfileService and integrate profiles management into NostrFeed module

- Introduced ProfileService to handle user profiles, including fetching and displaying profile information.
- Updated NostrFeed module to register ProfileService in the DI container and initialize it during installation.
- Enhanced NostrFeed.vue to utilize the profiles service for displaying user names alongside posts.
- Created useProfiles composable for managing profile-related functionality, including fetching and subscribing to profile updates.

These changes improve user engagement by providing richer profile information within the feed, enhancing the overall user experience.
This commit is contained in:
padreug 2025-09-17 01:11:53 +02:00
parent 310612a2c5
commit 45391cbaa1
6 changed files with 443 additions and 47 deletions

View file

@ -0,0 +1,275 @@
import { ref, reactive } from 'vue'
import { BaseService } from '@/core/base/BaseService'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import type { Event as NostrEvent, Filter } from 'nostr-tools'
export interface UserProfile {
pubkey: string
name?: string
display_name?: string
about?: string
picture?: string
nip05?: string
updated_at: number
}
export class ProfileService extends BaseService {
protected readonly metadata = {
name: 'ProfileService',
version: '1.0.0',
dependencies: []
}
protected relayHub: any = null
// Profile cache - reactive for UI updates
private _profiles = reactive(new Map<string, UserProfile>())
private _isLoading = ref(false)
private currentSubscription: string | null = null
private currentUnsubscribe: (() => void) | null = null
// Track which profiles we've requested to avoid duplicate requests
private requestedProfiles = new Set<string>()
protected async onInitialize(): Promise<void> {
console.log('ProfileService: Starting initialization...')
this.relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
if (!this.relayHub) {
throw new Error('RelayHub service not available')
}
console.log('ProfileService: Initialization complete')
}
/**
* Get profile for a pubkey, fetching if not cached
*/
async getProfile(pubkey: string): Promise<UserProfile | null> {
// Return cached profile if available
if (this._profiles.has(pubkey)) {
return this._profiles.get(pubkey)!
}
// If not requested yet, fetch it
if (!this.requestedProfiles.has(pubkey)) {
await this.fetchProfile(pubkey)
}
return this._profiles.get(pubkey) || null
}
/**
* Get display name for a pubkey (returns formatted pubkey if no profile)
*/
getDisplayName(pubkey: string): string {
const profile = this._profiles.get(pubkey)
if (profile?.display_name) return profile.display_name
if (profile?.name) return profile.name
// Return formatted pubkey as fallback
return `${pubkey.slice(0, 8)}...${pubkey.slice(-4)}`
}
/**
* Fetch profile for specific pubkey
*/
private async fetchProfile(pubkey: string): Promise<void> {
if (!this.relayHub || this.requestedProfiles.has(pubkey)) {
return
}
this.requestedProfiles.add(pubkey)
try {
if (!this.relayHub.isConnected) {
await this.relayHub.connect()
}
const subscriptionId = `profile-${pubkey}-${Date.now()}`
const filter: Filter = {
kinds: [0], // Profile metadata
authors: [pubkey],
limit: 1
}
console.log(`ProfileService: Fetching profile for ${pubkey.slice(0, 8)}...`)
const unsubscribe = this.relayHub.subscribe({
id: subscriptionId,
filters: [filter],
onEvent: (event: NostrEvent) => {
this.handleProfileEvent(event)
},
onEose: () => {
console.log(`Profile subscription ${subscriptionId} complete`)
// Clean up subscription after getting the profile
if (unsubscribe) {
unsubscribe()
}
}
})
} catch (error) {
console.error(`Failed to fetch profile for ${pubkey}:`, error)
this.requestedProfiles.delete(pubkey) // Allow retry
}
}
/**
* Handle incoming profile event
*/
private handleProfileEvent(event: NostrEvent): void {
try {
const metadata = JSON.parse(event.content)
const profile: UserProfile = {
pubkey: event.pubkey,
name: metadata.name,
display_name: metadata.display_name,
about: metadata.about,
picture: metadata.picture,
nip05: metadata.nip05,
updated_at: event.created_at
}
// Only update if this is newer than what we have
const existing = this._profiles.get(event.pubkey)
if (!existing || event.created_at > existing.updated_at) {
this._profiles.set(event.pubkey, profile)
console.log(`ProfileService: Updated profile for ${event.pubkey.slice(0, 8)}...`, profile.display_name || profile.name)
}
} catch (error) {
console.error('Failed to parse profile metadata:', error)
}
}
/**
* Bulk fetch profiles for multiple pubkeys
*/
async fetchProfiles(pubkeys: string[]): Promise<void> {
const unfetchedPubkeys = pubkeys.filter(pubkey =>
!this._profiles.has(pubkey) && !this.requestedProfiles.has(pubkey)
)
if (unfetchedPubkeys.length === 0) return
console.log(`ProfileService: Bulk fetching ${unfetchedPubkeys.length} profiles`)
try {
if (!this.relayHub?.isConnected) {
await this.relayHub?.connect()
}
const subscriptionId = `profiles-bulk-${Date.now()}`
// Mark all as requested
unfetchedPubkeys.forEach(pubkey => this.requestedProfiles.add(pubkey))
const filter: Filter = {
kinds: [0],
authors: unfetchedPubkeys,
limit: unfetchedPubkeys.length
}
const unsubscribe = this.relayHub.subscribe({
id: subscriptionId,
filters: [filter],
onEvent: (event: NostrEvent) => {
this.handleProfileEvent(event)
},
onEose: () => {
console.log(`Bulk profile subscription ${subscriptionId} complete`)
if (unsubscribe) {
unsubscribe()
}
}
})
} catch (error) {
console.error('Failed to bulk fetch profiles:', error)
// Remove from requested so they can be retried
unfetchedPubkeys.forEach(pubkey => this.requestedProfiles.delete(pubkey))
}
}
/**
* Subscribe to real-time profile updates for active users
*/
async subscribeToProfileUpdates(pubkeys: string[]): Promise<void> {
if (this.currentSubscription) {
await this.unsubscribeFromProfiles()
}
if (pubkeys.length === 0) return
try {
if (!this.relayHub?.isConnected) {
await this.relayHub?.connect()
}
const subscriptionId = `profile-updates-${Date.now()}`
const filter: Filter = {
kinds: [0],
authors: pubkeys
}
console.log(`ProfileService: Subscribing to profile updates for ${pubkeys.length} users`)
const unsubscribe = this.relayHub.subscribe({
id: subscriptionId,
filters: [filter],
onEvent: (event: NostrEvent) => {
this.handleProfileEvent(event)
},
onEose: () => {
console.log(`Profile updates subscription ${subscriptionId} ready`)
}
})
this.currentSubscription = subscriptionId
this.currentUnsubscribe = unsubscribe
} catch (error) {
console.error('Failed to subscribe to profile updates:', error)
}
}
/**
* Unsubscribe from profile updates
*/
async unsubscribeFromProfiles(): Promise<void> {
if (this.currentUnsubscribe) {
this.currentUnsubscribe()
this.currentSubscription = null
this.currentUnsubscribe = null
}
}
/**
* Clear profile cache
*/
clearCache(): void {
this._profiles.clear()
this.requestedProfiles.clear()
}
/**
* Get all cached profiles
*/
get profiles(): Map<string, UserProfile> {
return this._profiles
}
/**
* Cleanup
*/
protected async onDestroy(): Promise<void> {
await this.unsubscribeFromProfiles()
this.clearCache()
}
}