feat: Implement comprehensive Nostr identity and social features

## Core Identity Management
- Add secure key generation and import functionality
- Implement AES-GCM encryption with PBKDF2 key derivation
- Create password-protected identity storage
- Add browser-compatible crypto utilities (no Buffer dependency)

## User Interface
- Build identity management dialog with tabs for setup and profile
- Add navbar integration with user dropdown and mobile support
- Create password unlock dialog for encrypted identities
- Integrate vue-sonner for toast notifications

## Nostr Protocol Integration
- Implement event creation (notes, reactions, profiles, contacts)
- Add reply thread detection and engagement metrics
- Create social interaction composables for publishing
- Support multi-relay publishing with failure handling
- Add profile fetching and caching system

## Security Features
- Web Crypto API with 100k PBKDF2 iterations
- Secure random salt and IV generation
- Automatic password prompts for encrypted storage
- Legacy support for unencrypted identities

## Technical Improvements
- Replace all Buffer usage with browser-native APIs
- Add comprehensive error handling and validation
- Implement reactive state management with Vue composables
- Create reusable crypto utility functions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
padreug 2025-07-02 16:25:20 +02:00
parent d3e8b23c86
commit ee7eb461c4
12 changed files with 1777 additions and 21 deletions

View file

@ -0,0 +1,156 @@
import { ref } from 'vue'
import { NostrClient, type NostrNote } from '@/lib/nostr/client'
import { createTextNote, createReaction, createProfileMetadata } from '@/lib/nostr/events'
import { identity } from '@/composables/useIdentity'
import { toast } from 'vue-sonner'
export function useSocial(relayUrls: string[]) {
const client = new NostrClient({ relays: relayUrls })
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 client.connect()
const event = createTextNote(content, identity.currentIdentity.value, replyTo)
await client.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 client.connect()
const event = createReaction(targetEventId, targetAuthor, reaction, identity.currentIdentity.value)
await client.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 client.connect()
const event = createProfileMetadata(profileData, identity.currentIdentity.value)
await client.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 client.connect()
return await client.fetchReplies(noteId)
} 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 client.connect()
const fetchedProfiles = await client.fetchProfiles(uncachedPubkeys)
// Update cache
fetchedProfiles.forEach((profile, pubkey) => {
profiles.value.set(pubkey, profile)
})
} 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
const relayUrls = JSON.parse(import.meta.env.VITE_NOSTR_RELAYS as string)
export const social = useSocial(relayUrls)