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:
padreug 2025-08-13 10:11:28 +02:00
parent 06bcc4b91e
commit b074cc4ca3
11 changed files with 299 additions and 2184 deletions

View file

@ -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()

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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()