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:
parent
310612a2c5
commit
45391cbaa1
6 changed files with 443 additions and 47 deletions
275
src/modules/nostr-feed/services/ProfileService.ts
Normal file
275
src/modules/nostr-feed/services/ProfileService.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue