refactor: Remove unused components and clean up identity management
- Delete IdentityDialog.vue, useIdentity.ts, useNostr.ts, useNostrFeed.ts, useNostrFeedPreloader.ts, useSocial.ts, and related Nostr client files to streamline the codebase. - Consolidate identity management and feed handling logic to improve maintainability and reduce complexity. - Ensure that the application remains functional while enhancing overall clarity for future development.
This commit is contained in:
parent
06bcc4b91e
commit
b074cc4ca3
11 changed files with 299 additions and 2184 deletions
|
|
@ -1,193 +0,0 @@
|
|||
import { ref, computed } from 'vue'
|
||||
import { IdentityManager, type NostrIdentity, type NostrProfile } from '@/lib/nostr/identity'
|
||||
|
||||
const currentIdentity = ref<NostrIdentity | null>(null)
|
||||
const currentProfile = ref<NostrProfile | null>(null)
|
||||
const isAuthenticated = computed(() => !!currentIdentity.value)
|
||||
|
||||
export function useIdentity() {
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
/**
|
||||
* Initialize identity on app start
|
||||
*/
|
||||
async function initialize(password?: string): Promise<void> {
|
||||
try {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
const identity = await IdentityManager.loadIdentity(password)
|
||||
if (identity) {
|
||||
currentIdentity.value = identity
|
||||
currentProfile.value = IdentityManager.loadProfile()
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to initialize identity'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate new identity
|
||||
*/
|
||||
async function generateNewIdentity(password?: string): Promise<NostrIdentity> {
|
||||
try {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
const identity = IdentityManager.generateIdentity()
|
||||
await IdentityManager.saveIdentity(identity, password)
|
||||
|
||||
currentIdentity.value = identity
|
||||
currentProfile.value = null
|
||||
|
||||
return identity
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to generate identity'
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import existing identity
|
||||
*/
|
||||
async function importIdentity(privateKey: string, password?: string): Promise<NostrIdentity> {
|
||||
try {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
const identity = IdentityManager.importIdentity(privateKey)
|
||||
await IdentityManager.saveIdentity(identity, password)
|
||||
|
||||
currentIdentity.value = identity
|
||||
currentProfile.value = IdentityManager.loadProfile()
|
||||
|
||||
return identity
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to import identity'
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user profile
|
||||
*/
|
||||
async function updateProfile(profile: NostrProfile): Promise<void> {
|
||||
try {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
IdentityManager.saveProfile(profile)
|
||||
currentProfile.value = profile
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to update profile'
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign out and clear identity
|
||||
*/
|
||||
function signOut(): void {
|
||||
IdentityManager.clearIdentity()
|
||||
currentIdentity.value = null
|
||||
currentProfile.value = null
|
||||
error.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if identity is stored
|
||||
*/
|
||||
function hasStoredIdentity(): boolean {
|
||||
return IdentityManager.hasStoredIdentity()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if stored identity is encrypted
|
||||
*/
|
||||
function isStoredIdentityEncrypted(): boolean {
|
||||
return IdentityManager.isStoredIdentityEncrypted()
|
||||
}
|
||||
|
||||
/**
|
||||
* Load identity with password (for encrypted identities)
|
||||
*/
|
||||
async function loadWithPassword(password: string): Promise<void> {
|
||||
try {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
const identity = await IdentityManager.loadIdentity(password)
|
||||
if (identity) {
|
||||
currentIdentity.value = identity
|
||||
currentProfile.value = IdentityManager.loadProfile()
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to load identity'
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current identity info
|
||||
*/
|
||||
const identityInfo = computed(() => {
|
||||
if (!currentIdentity.value) return null
|
||||
|
||||
return {
|
||||
npub: currentIdentity.value.npub,
|
||||
publicKey: currentIdentity.value.publicKey,
|
||||
shortPubkey: currentIdentity.value.publicKey.slice(0, 8) + '...' + currentIdentity.value.publicKey.slice(-8)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Get profile display info
|
||||
*/
|
||||
const profileDisplay = computed(() => {
|
||||
if (!currentProfile.value && !identityInfo.value) return null
|
||||
|
||||
return {
|
||||
name: currentProfile.value?.name || currentProfile.value?.display_name || identityInfo.value?.shortPubkey || 'Anonymous',
|
||||
displayName: currentProfile.value?.display_name || currentProfile.value?.name,
|
||||
about: currentProfile.value?.about,
|
||||
picture: currentProfile.value?.picture,
|
||||
website: currentProfile.value?.website,
|
||||
lightningAddress: currentProfile.value?.lud16
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
// State
|
||||
currentIdentity: computed(() => currentIdentity.value),
|
||||
currentProfile: computed(() => currentProfile.value),
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
error,
|
||||
identityInfo,
|
||||
profileDisplay,
|
||||
|
||||
// Actions
|
||||
initialize,
|
||||
generateNewIdentity,
|
||||
importIdentity,
|
||||
updateProfile,
|
||||
signOut,
|
||||
hasStoredIdentity,
|
||||
isStoredIdentityEncrypted,
|
||||
loadWithPassword
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance for global state
|
||||
export const identity = useIdentity()
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { storeToRefs } from 'pinia'
|
||||
import { useNostrStore } from '@/stores/nostr'
|
||||
import type { NostrClientConfig } from '@/lib/nostr/client'
|
||||
|
||||
export function useNostr(config?: NostrClientConfig) {
|
||||
const store = useNostrStore()
|
||||
|
||||
// If custom relays are provided, update the store
|
||||
if (config?.relays) {
|
||||
store.setRelayUrls(config.relays)
|
||||
}
|
||||
|
||||
// Return reactive refs from the store
|
||||
const { isConnected, isConnecting, error } = storeToRefs(store)
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
isConnecting,
|
||||
error,
|
||||
connect: store.connect,
|
||||
disconnect: store.disconnect
|
||||
}
|
||||
}
|
||||
|
|
@ -1,254 +0,0 @@
|
|||
import { ref, readonly } from 'vue'
|
||||
import type { NostrNote } from '@/lib/nostr/client'
|
||||
import { useRelayHub } from '@/composables/useRelayHub'
|
||||
import { useNostrStore } from '@/stores/nostr'
|
||||
import { config as globalConfig } from '@/lib/config'
|
||||
import { notificationManager } from '@/lib/notifications/manager'
|
||||
|
||||
export interface NostrFeedConfig {
|
||||
relays?: string[]
|
||||
feedType?: 'all' | 'announcements' | 'events' | 'general'
|
||||
limit?: number
|
||||
includeReplies?: boolean
|
||||
}
|
||||
|
||||
export function useNostrFeed(config: NostrFeedConfig = {}) {
|
||||
const relayHub = useRelayHub()
|
||||
const nostrStore = useNostrStore()
|
||||
|
||||
// State
|
||||
const notes = ref<NostrNote[]>([])
|
||||
const isLoading = ref(false)
|
||||
const error = ref<Error | null>(null)
|
||||
const isConnected = ref(false)
|
||||
|
||||
// Get admin/moderator pubkeys from centralized config
|
||||
const adminPubkeys = globalConfig.nostr?.adminPubkeys || []
|
||||
|
||||
// Track last seen note timestamp to avoid duplicate notifications
|
||||
let lastSeenTimestamp = Math.floor(Date.now() / 1000)
|
||||
|
||||
// Load notes from localStorage immediately (synchronous)
|
||||
const loadFromStorage = () => {
|
||||
const storageKey = `nostr-feed-${config.feedType || 'all'}`
|
||||
const storedNotes = localStorage.getItem(storageKey)
|
||||
|
||||
if (storedNotes) {
|
||||
try {
|
||||
const parsedNotes = JSON.parse(storedNotes) as NostrNote[]
|
||||
notes.value = parsedNotes
|
||||
console.log(`Loaded ${parsedNotes.length} notes from localStorage`)
|
||||
|
||||
// Update last seen timestamp from stored notes
|
||||
if (notes.value.length > 0) {
|
||||
lastSeenTimestamp = Math.max(lastSeenTimestamp, Math.max(...notes.value.map(note => note.created_at)))
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse stored notes:', err)
|
||||
localStorage.removeItem(storageKey)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Load notes from localStorage first, then fetch new ones
|
||||
const loadNotes = async (isRefresh = false) => {
|
||||
try {
|
||||
// First, try to load from localStorage immediately (only if not refreshing)
|
||||
if (!isRefresh) {
|
||||
const hasStoredData = loadFromStorage()
|
||||
|
||||
// Only show loading if we don't have stored data
|
||||
if (!hasStoredData) {
|
||||
isLoading.value = true
|
||||
}
|
||||
} else {
|
||||
// For refresh, always show loading
|
||||
isLoading.value = true
|
||||
}
|
||||
|
||||
error.value = null
|
||||
|
||||
// Connect to Nostr using the centralized relay hub
|
||||
await relayHub.connect()
|
||||
isConnected.value = relayHub.isConnected.value
|
||||
|
||||
if (!isConnected.value) {
|
||||
throw new Error('Failed to connect to Nostr relays')
|
||||
}
|
||||
|
||||
// Configure fetch options based on feed type
|
||||
const fetchOptions: any = {
|
||||
limit: config.limit || 50,
|
||||
includeReplies: config.includeReplies || false
|
||||
}
|
||||
|
||||
// Filter by authors based on feed type
|
||||
if (config.feedType === 'announcements') {
|
||||
if (adminPubkeys.length > 0) {
|
||||
fetchOptions.authors = adminPubkeys
|
||||
} else {
|
||||
notes.value = []
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch new notes using the relay hub
|
||||
const newNotes = await relayHub.queryEvents([
|
||||
{
|
||||
kinds: [1], // TEXT_NOTE
|
||||
limit: fetchOptions.limit,
|
||||
authors: fetchOptions.authors
|
||||
}
|
||||
])
|
||||
|
||||
// Client-side filtering for 'general' feed (exclude admin posts)
|
||||
let filteredNotes = newNotes
|
||||
if (config.feedType === 'general' && adminPubkeys.length > 0) {
|
||||
filteredNotes = newNotes.filter(note => !adminPubkeys.includes(note.pubkey))
|
||||
}
|
||||
|
||||
// For refresh, replace all notes. For normal load, merge with existing
|
||||
if (isRefresh) {
|
||||
notes.value = filteredNotes
|
||||
} else {
|
||||
// Merge with existing notes, avoiding duplicates
|
||||
const existingIds = new Set(notes.value.map(note => note.id))
|
||||
const uniqueNewNotes = filteredNotes.filter(note => !existingIds.has(note.id))
|
||||
|
||||
if (uniqueNewNotes.length > 0) {
|
||||
// Add new notes to the beginning
|
||||
notes.value.unshift(...uniqueNewNotes)
|
||||
}
|
||||
}
|
||||
|
||||
// Limit the array size to prevent memory issues
|
||||
if (notes.value.length > 100) {
|
||||
notes.value = notes.value.slice(0, 100)
|
||||
}
|
||||
|
||||
// Save to localStorage
|
||||
const storageKey = `nostr-feed-${config.feedType || 'all'}`
|
||||
localStorage.setItem(storageKey, JSON.stringify(notes.value))
|
||||
console.log(`Loaded ${notes.value.length} notes`)
|
||||
|
||||
// Update last seen timestamp
|
||||
if (notes.value.length > 0) {
|
||||
lastSeenTimestamp = Math.max(lastSeenTimestamp, Math.max(...notes.value.map(note => note.created_at)))
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err : new Error('Failed to load notes')
|
||||
console.error('Error loading notes:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Real-time subscription for new notes
|
||||
let unsubscribe: (() => void) | null = null
|
||||
|
||||
const subscribeToFeedUpdates = () => {
|
||||
try {
|
||||
// Subscribe to real-time notes using the relay hub
|
||||
unsubscribe = relayHub.subscribe({
|
||||
id: `feed-${config.feedType || 'all'}`,
|
||||
filters: [{ kinds: [1] }], // TEXT_NOTE
|
||||
onEvent: (event: any) => {
|
||||
// Only process notes newer than last seen
|
||||
if (event.created_at > lastSeenTimestamp) {
|
||||
// Check if note should be included based on feed type
|
||||
const shouldInclude = shouldIncludeNote(event)
|
||||
if (shouldInclude) {
|
||||
// Add to beginning of notes array
|
||||
notes.value.unshift(event)
|
||||
|
||||
// Limit the array size to prevent memory issues
|
||||
if (notes.value.length > 100) {
|
||||
notes.value = notes.value.slice(0, 100)
|
||||
}
|
||||
|
||||
// Save to localStorage
|
||||
const storageKey = `nostr-feed-${config.feedType || 'all'}`
|
||||
localStorage.setItem(storageKey, JSON.stringify(notes.value))
|
||||
}
|
||||
|
||||
// Send notification if appropriate (only for admin announcements when not in announcements feed)
|
||||
if (config.feedType !== 'announcements' && adminPubkeys.includes(event.pubkey)) {
|
||||
notificationManager.notifyForNote(event, nostrStore.account?.pubkey)
|
||||
}
|
||||
|
||||
// Update last seen timestamp
|
||||
lastSeenTimestamp = Math.max(lastSeenTimestamp, event.created_at)
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to start real-time subscription:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const shouldIncludeNote = (note: NostrNote): boolean => {
|
||||
if (config.feedType === 'announcements') {
|
||||
return adminPubkeys.length > 0 && adminPubkeys.includes(note.pubkey)
|
||||
}
|
||||
if (config.feedType === 'general' && adminPubkeys.length > 0) {
|
||||
return !adminPubkeys.includes(note.pubkey)
|
||||
}
|
||||
// For other feed types, include all notes
|
||||
return true
|
||||
}
|
||||
|
||||
const connectToFeed = async () => {
|
||||
try {
|
||||
console.log('Connecting to Nostr feed...')
|
||||
await relayHub.connect()
|
||||
isConnected.value = relayHub.isConnected.value
|
||||
console.log('Connected to Nostr feed')
|
||||
} catch (err) {
|
||||
console.error('Error connecting to feed:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const disconnectFromFeed = () => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe()
|
||||
unsubscribe = null
|
||||
}
|
||||
isConnected.value = false
|
||||
}
|
||||
|
||||
const resetFeedState = () => {
|
||||
notes.value = []
|
||||
error.value = null
|
||||
isLoading.value = false
|
||||
isConnected.value = false
|
||||
lastSeenTimestamp = Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
disconnectFromFeed()
|
||||
}
|
||||
|
||||
// Initialize by loading from storage immediately
|
||||
loadFromStorage()
|
||||
|
||||
return {
|
||||
// State
|
||||
notes: readonly(notes),
|
||||
isLoading: readonly(isLoading),
|
||||
error: readonly(error),
|
||||
isConnected: readonly(isConnected),
|
||||
|
||||
// Actions
|
||||
loadNotes,
|
||||
connectToFeed,
|
||||
disconnectFromFeed,
|
||||
subscribeToFeedUpdates,
|
||||
resetFeedState,
|
||||
cleanup
|
||||
}
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import { ref, computed } from 'vue'
|
||||
import { useNostrFeed, type NostrFeedConfig } from './useNostrFeed'
|
||||
|
||||
export function useNostrFeedPreloader(config: NostrFeedConfig = {}) {
|
||||
const feed = useNostrFeed(config)
|
||||
|
||||
// Preload state
|
||||
const isPreloading = ref(false)
|
||||
const isPreloaded = ref(false)
|
||||
const preloadError = ref<Error | null>(null)
|
||||
|
||||
// Check if feed data is available in localStorage
|
||||
const hasStoredData = computed(() => {
|
||||
const storageKey = `nostr-feed-${config.feedType || 'all'}`
|
||||
const storedData = localStorage.getItem(storageKey)
|
||||
return storedData !== null
|
||||
})
|
||||
|
||||
// Check if we should show loading (only if no cached data and actually loading)
|
||||
const shouldShowLoading = computed(() => {
|
||||
return (feed.isLoading.value || isPreloading.value) && feed.notes.value.length === 0
|
||||
})
|
||||
|
||||
// Preload feed data
|
||||
const preloadFeed = async () => {
|
||||
if (isPreloaded.value || isPreloading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isPreloading.value = true
|
||||
preloadError.value = null
|
||||
|
||||
console.log('Preloading Nostr feed...')
|
||||
|
||||
// Connect and load notes
|
||||
await feed.connectToFeed()
|
||||
await feed.loadNotes()
|
||||
|
||||
// Subscribe to updates
|
||||
feed.subscribeToFeedUpdates()
|
||||
|
||||
isPreloaded.value = true
|
||||
console.log('Nostr feed preloaded successfully')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to preload Nostr feed:', error)
|
||||
preloadError.value = error instanceof Error ? error : new Error('Failed to preload feed')
|
||||
} finally {
|
||||
isPreloading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Reset preload state
|
||||
const resetPreload = () => {
|
||||
isPreloaded.value = false
|
||||
isPreloading.value = false
|
||||
preloadError.value = null
|
||||
feed.resetFeedState()
|
||||
}
|
||||
|
||||
// Auto-preload if we have stored data
|
||||
const autoPreload = async () => {
|
||||
if (hasStoredData.value && !isPreloaded.value && !isPreloading.value) {
|
||||
await preloadFeed()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
isPreloading: computed(() => isPreloading.value),
|
||||
isPreloaded: computed(() => isPreloaded.value),
|
||||
preloadError: computed(() => preloadError.value),
|
||||
hasStoredData,
|
||||
shouldShowLoading,
|
||||
|
||||
// Actions
|
||||
preloadFeed,
|
||||
resetPreload,
|
||||
autoPreload,
|
||||
|
||||
// Expose feed methods
|
||||
...feed
|
||||
}
|
||||
}
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
import { ref } from 'vue'
|
||||
import type { NostrNote } from '@/lib/nostr/client'
|
||||
import { createTextNote, createReaction, createProfileMetadata } from '@/lib/nostr/events'
|
||||
import { identity } from '@/composables/useIdentity'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { useRelayHub } from './useRelayHub'
|
||||
|
||||
export function useSocial() {
|
||||
const relayHub = useRelayHub()
|
||||
const isPublishing = ref(false)
|
||||
const profiles = ref(new Map<string, any>())
|
||||
|
||||
/**
|
||||
* Publish a text note
|
||||
*/
|
||||
async function publishNote(content: string, replyTo?: string): Promise<void> {
|
||||
if (!identity.isAuthenticated.value || !identity.currentIdentity.value) {
|
||||
throw new Error('Must be logged in to publish notes')
|
||||
}
|
||||
|
||||
try {
|
||||
isPublishing.value = true
|
||||
|
||||
await relayHub.connect()
|
||||
const event = createTextNote(content, identity.currentIdentity.value, replyTo)
|
||||
await relayHub.publishEvent(event)
|
||||
|
||||
toast.success(replyTo ? 'Reply published!' : 'Note published!')
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to publish note'
|
||||
toast.error(message)
|
||||
throw error
|
||||
} finally {
|
||||
isPublishing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a reaction to a note
|
||||
*/
|
||||
async function publishReaction(targetEventId: string, targetAuthor: string, reaction: string = '👍'): Promise<void> {
|
||||
if (!identity.isAuthenticated.value || !identity.currentIdentity.value) {
|
||||
throw new Error('Must be logged in to react to notes')
|
||||
}
|
||||
|
||||
try {
|
||||
await relayHub.connect()
|
||||
const event = createReaction(targetEventId, targetAuthor, reaction, identity.currentIdentity.value)
|
||||
await relayHub.publishEvent(event)
|
||||
|
||||
toast.success('Reaction added!')
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to add reaction'
|
||||
toast.error(message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish profile metadata
|
||||
*/
|
||||
async function publishProfile(profileData: any): Promise<void> {
|
||||
if (!identity.isAuthenticated.value || !identity.currentIdentity.value) {
|
||||
throw new Error('Must be logged in to update profile')
|
||||
}
|
||||
|
||||
try {
|
||||
isPublishing.value = true
|
||||
|
||||
await relayHub.connect()
|
||||
const event = createProfileMetadata(profileData, identity.currentIdentity.value)
|
||||
await relayHub.publishEvent(event)
|
||||
|
||||
toast.success('Profile updated on Nostr!')
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to update profile'
|
||||
toast.error(message)
|
||||
throw error
|
||||
} finally {
|
||||
isPublishing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch replies to a note
|
||||
*/
|
||||
async function fetchReplies(noteId: string): Promise<NostrNote[]> {
|
||||
try {
|
||||
await relayHub.connect()
|
||||
return await relayHub.queryEvents([
|
||||
{
|
||||
kinds: [1], // TEXT_NOTE
|
||||
'#e': [noteId] // Reply to specific event
|
||||
}
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch replies:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and cache user profiles
|
||||
*/
|
||||
async function fetchProfiles(pubkeys: string[]): Promise<void> {
|
||||
// Filter out already cached profiles
|
||||
const uncachedPubkeys = pubkeys.filter(pubkey => !profiles.value.has(pubkey))
|
||||
|
||||
if (uncachedPubkeys.length === 0) return
|
||||
|
||||
try {
|
||||
await relayHub.connect()
|
||||
const fetchedProfiles = await relayHub.queryEvents([
|
||||
{
|
||||
kinds: [0], // PROFILE_METADATA
|
||||
authors: uncachedPubkeys
|
||||
}
|
||||
])
|
||||
|
||||
// Update cache - convert events to profile map
|
||||
fetchedProfiles.forEach((event) => {
|
||||
try {
|
||||
const profileData = JSON.parse(event.content)
|
||||
profiles.value.set(event.pubkey, profileData)
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse profile data for', event.pubkey)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profiles:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached profile or return default
|
||||
*/
|
||||
function getProfile(pubkey: string) {
|
||||
return profiles.value.get(pubkey) || {
|
||||
name: pubkey.slice(0, 8) + '...',
|
||||
display_name: undefined,
|
||||
about: undefined,
|
||||
picture: undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name for a pubkey
|
||||
*/
|
||||
function getDisplayName(pubkey: string): string {
|
||||
const profile = getProfile(pubkey)
|
||||
return profile.display_name || profile.name || pubkey.slice(0, 8) + '...'
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
isPublishing,
|
||||
profiles,
|
||||
|
||||
// Actions
|
||||
publishNote,
|
||||
publishReaction,
|
||||
publishProfile,
|
||||
fetchReplies,
|
||||
fetchProfiles,
|
||||
getProfile,
|
||||
getDisplayName
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance for global use
|
||||
export const social = useSocial()
|
||||
Loading…
Add table
Add a link
Reference in a new issue