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:
parent
d3e8b23c86
commit
ee7eb461c4
12 changed files with 1777 additions and 21 deletions
156
src/composables/useSocial.ts
Normal file
156
src/composables/useSocial.ts
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue