From 17c07c37a0224d822bd63d0a102be2b4562014c3 Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 5 Sep 2025 01:44:15 +0200 Subject: [PATCH] Refactor chat and market modules for improved integration and maintainability - Remove deprecated Nostr chat and relay hub components, transitioning to a modular chat service for better encapsulation. - Update App.vue and Navbar.vue to utilize the new chat module, enhancing user experience with automatic peer management. - Simplify event handling and connection logic in ChatComponent.vue, ensuring compatibility with the new chat service architecture. - Adjust market settings and order history components to reflect changes in the chat module, improving overall coherence in the application structure. - Clean up unused imports and streamline configuration access for better performance and maintainability. --- src/App.vue | 38 +- src/components/layout/Navbar.vue | 7 +- src/composables/useEvents.ts | 55 -- src/composables/useNostrChat.ts | 928 ------------------ src/composables/useNostrclientHub.ts | 198 ---- src/composables/useOrderEvents.ts | 314 ------ src/composables/useRelayHub.ts | 283 ------ src/composables/useTicketPurchase.ts | 242 ----- src/composables/useUserTickets.ts | 123 --- src/lib/config/index.ts | 12 +- src/modules/chat/components/ChatComponent.vue | 49 +- .../components/PurchaseTicketDialog.vue | 2 +- .../market/components/MarketSettings.vue | 5 +- .../market/components/OrderHistory.vue | 9 +- src/modules/market/composables/useMarket.ts | 13 +- .../nostr-feed/components/NostrFeed.vue | 4 +- src/pages/Home.vue | 3 +- 17 files changed, 63 insertions(+), 2222 deletions(-) delete mode 100644 src/composables/useEvents.ts delete mode 100644 src/composables/useNostrChat.ts delete mode 100644 src/composables/useNostrclientHub.ts delete mode 100644 src/composables/useOrderEvents.ts delete mode 100644 src/composables/useRelayHub.ts delete mode 100644 src/composables/useTicketPurchase.ts delete mode 100644 src/composables/useUserTickets.ts diff --git a/src/App.vue b/src/App.vue index 9832863..5052772 100644 --- a/src/App.vue +++ b/src/App.vue @@ -7,9 +7,7 @@ import LoginDialog from '@/components/auth/LoginDialog.vue' import { Toaster } from '@/components/ui/sonner' import 'vue-sonner/style.css' import { useMarketPreloader } from '@/composables/useMarketPreloader' -import { nostrChat } from '@/composables/useNostrChat' import { auth } from '@/composables/useAuth' -import { relayHubComposable } from '@/composables/useRelayHub' import { toast } from 'vue-sonner' const route = useRoute() @@ -18,8 +16,7 @@ const showLoginDialog = ref(false) // Initialize preloader const marketPreloader = useMarketPreloader() -// Initialize relay hub -const relayHub = relayHubComposable +// Relay hub initialization is now handled by the base module // Hide navbar on login page const showNavbar = computed(() => { @@ -33,18 +30,7 @@ async function handleLoginSuccess() { // Trigger preloading after successful login marketPreloader.preloadMarket() - // Connect to chat - if (!nostrChat.isConnected.value) { - try { - await nostrChat.connect() - - // Load peers and subscribe to all for notifications - const peers = await nostrChat.loadPeers() - await nostrChat.subscribeToAllPeersForNotifications(peers) - } catch (error) { - console.error('Failed to initialize chat:', error) - } - } + // Chat initialization is now handled by the chat module } onMounted(async () => { @@ -55,12 +41,7 @@ onMounted(async () => { console.error('Failed to initialize authentication:', error) } - // Initialize relay hub - try { - await relayHub.initialize() - } catch (error) { - console.error('Failed to initialize relay hub:', error) - } + // Relay hub initialization is handled by the base module }) // Watch for authentication changes and trigger preloading @@ -70,18 +51,7 @@ watch(() => auth.isAuthenticated.value, async (isAuthenticated) => { console.log('User authenticated, triggering market preload...') marketPreloader.preloadMarket() } - if (!nostrChat.isConnected.value) { - console.log('User authenticated, connecting to chat...') - try { - await nostrChat.connect() - - // Load peers and subscribe to all for notifications - const peers = await nostrChat.loadPeers() - await nostrChat.subscribeToAllPeersForNotifications(peers) - } catch (error) { - console.error('Failed to initialize chat:', error) - } - } + // Chat connection is now handled by the chat module automatically } }, { immediate: true }) diff --git a/src/components/layout/Navbar.vue b/src/components/layout/Navbar.vue index 592305b..cc19ce0 100644 --- a/src/components/layout/Navbar.vue +++ b/src/components/layout/Navbar.vue @@ -15,7 +15,7 @@ import { LogoutConfirmDialog } from '@/components/ui/LogoutConfirmDialog' import { auth } from '@/composables/useAuth' import { useMarketPreloader } from '@/composables/useMarketPreloader' import { useMarketStore } from '@/stores/market' -import { nostrChat } from '@/composables/useNostrChat' +import { tryInjectService, SERVICE_TOKENS } from '@/core/di-container' import { useModularNavigation } from '@/composables/useModularNavigation' interface NavigationItem { @@ -45,9 +45,12 @@ const totalBalance = computed(() => { }, 0) }) +// Try to get chat service from DI (may not be available if chat module not loaded) +const chatService = tryInjectService(SERVICE_TOKENS.CHAT_SERVICE) + // Compute total unread messages (reactive) const totalUnreadMessages = computed(() => { - return nostrChat.totalUnreadCount.value + return chatService?.totalUnreadCount?.value || 0 }) // Compute cart item count diff --git a/src/composables/useEvents.ts b/src/composables/useEvents.ts deleted file mode 100644 index ea98ad1..0000000 --- a/src/composables/useEvents.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { computed } from 'vue' -import { useAsyncState } from '@vueuse/core' -import type { Event } from '@/lib/types/event' -import { fetchEvents } from '@/lib/api/events' - -export function useEvents() { - const { state: events, isLoading, error: asyncError, execute: refresh } = useAsyncState( - fetchEvents, - [] as Event[], - { - immediate: true, - resetOnExecute: false, - } - ) - - const error = computed(() => { - if (asyncError.value) { - return { - message: asyncError.value instanceof Error - ? asyncError.value.message - : 'An error occurred while fetching events' - } - } - return null - }) - - const sortedEvents = computed(() => { - return [...events.value].sort((a, b) => - new Date(b.time).getTime() - new Date(a.time).getTime() - ) - }) - - const upcomingEvents = computed(() => { - const now = new Date() - return sortedEvents.value.filter(event => - new Date(event.event_start_date) > now - ) - }) - - const pastEvents = computed(() => { - const now = new Date() - return sortedEvents.value.filter(event => - new Date(event.event_end_date) < now - ) - }) - - return { - events: sortedEvents, - upcomingEvents, - pastEvents, - isLoading, - error, - refresh, - } -} diff --git a/src/composables/useNostrChat.ts b/src/composables/useNostrChat.ts deleted file mode 100644 index b1372b8..0000000 --- a/src/composables/useNostrChat.ts +++ /dev/null @@ -1,928 +0,0 @@ -import { ref, computed, readonly } from 'vue' - -import { nip04, finalizeEvent, type EventTemplate } from 'nostr-tools' -import { hexToBytes } from '@/lib/utils/crypto' -import { getAuthToken } from '@/lib/config/lnbits' -import { config } from '@/lib/config' -import { relayHubComposable } from './useRelayHub' -import { useAuth } from './useAuth' - -// Types -export interface ChatMessage { - id: string - content: string - created_at: number - sent: boolean - pubkey: string -} - -export interface NostrRelayConfig { - url: string - read?: boolean - write?: boolean -} - -// Add notification system for unread messages -interface UnreadMessageData { - lastReadTimestamp: number - unreadCount: number - processedMessageIds: Set // Track which messages we've already counted as unread -} - -const UNREAD_MESSAGES_KEY = 'nostr-chat-unread-messages' - -// Get unread message data for a peer -const getUnreadData = (peerPubkey: string): UnreadMessageData => { - try { - const stored = localStorage.getItem(`${UNREAD_MESSAGES_KEY}-${peerPubkey}`) - if (stored) { - const data = JSON.parse(stored) - // Convert the array back to a Set for processedMessageIds - return { - ...data, - processedMessageIds: new Set(data.processedMessageIds || []) - } - } - return { lastReadTimestamp: 0, unreadCount: 0, processedMessageIds: new Set() } - } catch (error) { - console.warn('Failed to load unread data for peer:', peerPubkey, error) - return { lastReadTimestamp: 0, unreadCount: 0, processedMessageIds: new Set() } - } -} - -// Save unread message data for a peer -const saveUnreadData = (peerPubkey: string, data: UnreadMessageData): void => { - try { - // Convert Set to array for localStorage serialization - const serializableData = { - ...data, - processedMessageIds: Array.from(data.processedMessageIds) - } - localStorage.setItem(`${UNREAD_MESSAGES_KEY}-${peerPubkey}`, JSON.stringify(serializableData)) - } catch (error) { - console.warn('Failed to save unread data for peer:', peerPubkey, error) - } -} - -export function useNostrChat() { - // Use the centralized relay hub - const relayHub = relayHubComposable - - // Use the main authentication system - const auth = useAuth() - - // State - const messages = ref>(new Map()) - const processedMessageIds = ref(new Set()) - const onMessageAdded = ref<((peerPubkey: string) => void) | null>(null) - - // Reactive unread counts - const unreadCounts = ref>(new Map()) - - // Track latest message timestamp for each peer (for sorting) - const latestMessageTimestamps = ref>(new Map()) - - // Track peers globally - const peers = ref([]) - - // Track malformed message IDs to prevent repeated processing attempts - const malformedMessageIds = ref(new Set()) - - // Mark a message as malformed to prevent future processing attempts - const markMessageAsMalformed = (eventId: string) => { - malformedMessageIds.value.add(eventId) - // Also mark as processed to prevent retries - processedMessageIds.value.add(eventId) - } - - // Clean up old malformed messages (call this periodically) - const cleanupMalformedMessages = () => { - // const now = Math.floor(Date.now() / 1000) - // const maxAge = 24 * 60 * 60 // 24 hours - - // Clear old malformed message IDs to free memory - // This is a simple cleanup - in production you might want more sophisticated tracking - if (malformedMessageIds.value.size > 1000) { - console.log('Cleaning up malformed message tracking (clearing all)') - malformedMessageIds.value.clear() - } - } - - // Set up periodic cleanup - let cleanupInterval: number | null = null - - // Clean up resources - const cleanup = () => { - if (cleanupInterval) { - clearInterval(cleanupInterval) - cleanupInterval = null - console.log('Cleaned up malformed message tracking interval') - } - } - - // Manually clear all malformed message tracking - const clearAllMalformedMessages = () => { - const count = malformedMessageIds.value.size - malformedMessageIds.value.clear() - console.log(`Cleared ${count} malformed message IDs from tracking`) - } - - // Get statistics about malformed messages - const getMalformedMessageStats = () => { - return { - totalMalformed: malformedMessageIds.value.size, - totalProcessed: processedMessageIds.value.size, - malformedIds: Array.from(malformedMessageIds.value).slice(0, 10) // First 10 for debugging - } - } - - // Computed - use relay hub's connection status and auth system - const isConnected = computed(() => relayHub.isConnected.value) - - // Get current user from auth system - const currentUser = computed(() => { - const user = auth.currentUser.value - if (!user) { - return null - } - - // Check if the user has a pubkey field - if (!user.pubkey) { - return null - } - - // Check if the user has a prvkey field - if (!user.prvkey) { - return null - } - - // Use the actual user data - assume prvkey and pubkey contain real Nostr keys - return { - pubkey: user.pubkey, - prvkey: user.prvkey - } - }) - - // Check if user is authenticated (has LNBits login) - const isAuthenticated = computed(() => { - return auth.currentUser.value !== null - }) - - // Check if user has complete Nostr keypair - const hasNostrKeys = computed(() => { - const user = currentUser.value - return user && user.pubkey && user.prvkey - }) - - // Get Nostr key status for debugging - const getNostrKeyStatus = () => { - const user = auth.currentUser.value - if (!user) { - return { hasUser: false, hasPubkey: false, hasPrvkey: false, message: 'No user logged in' } - } - - return { - hasUser: true, - hasPubkey: !!user.pubkey, - hasPrvkey: !!user.prvkey, - message: user.pubkey && user.prvkey ? 'User has complete Nostr keypair' : 'User missing Nostr keys', - pubkey: user.pubkey - } - } - - // Get unread count for a peer - const getUnreadCount = (peerPubkey: string): number => { - return unreadCounts.value.get(peerPubkey) || 0 - } - - // Get all unread counts - const getAllUnreadCounts = (): Map => { - return new Map(unreadCounts.value) - } - - // Get total unread count across all peers - const getTotalUnreadCount = (): number => { - let total = 0 - for (const count of unreadCounts.value.values()) { - total += count - } - return total - } - - // Get latest message timestamp for a peer - const getLatestMessageTimestamp = (peerPubkey: string): number => { - return latestMessageTimestamps.value.get(peerPubkey) || 0 - } - - // Get all latest message timestamps - const getAllLatestMessageTimestamps = (): Map => { - return new Map(latestMessageTimestamps.value) - } - - // Update latest message timestamp for a peer - const updateLatestMessageTimestamp = (peerPubkey: string, timestamp: number): void => { - const current = latestMessageTimestamps.value.get(peerPubkey) || 0 - if (timestamp > current) { - latestMessageTimestamps.value.set(peerPubkey, timestamp) - } - } - - // Update unread count for a peer - const updateUnreadCount = (peerPubkey: string, count: number): void => { - if (count > 0) { - unreadCounts.value.set(peerPubkey, count) - } else { - unreadCounts.value.delete(peerPubkey) - } - // Force reactivity - unreadCounts.value = new Map(unreadCounts.value) - - // Save to localStorage - const unreadData = getUnreadData(peerPubkey) - unreadData.unreadCount = count - saveUnreadData(peerPubkey, unreadData) - } - - // Mark messages as read for a peer - const markMessagesAsRead = (peerPubkey: string): void => { - const currentTimestamp = Math.floor(Date.now() / 1000) - - // Update last read timestamp, reset unread count, and clear processed message IDs - const updatedData: UnreadMessageData = { - lastReadTimestamp: currentTimestamp, - unreadCount: 0, - processedMessageIds: new Set() // Clear processed messages when marking as read - } - - saveUnreadData(peerPubkey, updatedData) - updateUnreadCount(peerPubkey, 0) - - // Also clear any processed message IDs from the global set that might be from this peer - // This helps prevent duplicate message issues - } - - // Load unread counts from localStorage - const loadUnreadCounts = (): void => { - try { - const keys = Object.keys(localStorage).filter(key => - key.startsWith(`${UNREAD_MESSAGES_KEY}-`) - ) - - - - for (const key of keys) { - const peerPubkey = key.replace(`${UNREAD_MESSAGES_KEY}-`, '') - const unreadData = getUnreadData(peerPubkey) - - // Recalculate unread count based on actual messages and lastReadTimestamp - const peerMessages = messages.value.get(peerPubkey) || [] - let actualUnreadCount = 0 - - for (const message of peerMessages) { - // Only count messages not sent by us and created after last read timestamp - if (!message.sent && message.created_at > unreadData.lastReadTimestamp) { - actualUnreadCount++ - } - } - - // Update the stored count to match reality - if (actualUnreadCount !== unreadData.unreadCount) { - unreadData.unreadCount = actualUnreadCount - saveUnreadData(peerPubkey, unreadData) - } - - - - if (actualUnreadCount > 0) { - unreadCounts.value.set(peerPubkey, actualUnreadCount) - } - } - } catch (error) { - console.warn('Failed to load unread counts:', error) - } - } - - // Initialize unread counts on startup - loadUnreadCounts() - - // Clear unread count for a peer - // const clearUnreadCount = (peerPubkey: string): void => { - // unreadCounts.value.delete(peerPubkey) - // - // // Clear from localStorage - // const unreadData = getUnreadData(peerPubkey) - // unreadData.unreadCount = 0 - // saveUnreadData(peerPubkey, unreadData) - // } - - // Clear all unread counts - const clearAllUnreadCounts = (): void => { - unreadCounts.value.clear() - - // Clear from localStorage for all peers - for (const [peerPubkey] of messages.value) { - const unreadData = getUnreadData(peerPubkey) - unreadData.unreadCount = 0 - saveUnreadData(peerPubkey, unreadData) - } - - // Also clear from localStorage for all stored keys - try { - const keys = Object.keys(localStorage).filter(key => - key.startsWith(`${UNREAD_MESSAGES_KEY}-`) - ) - - for (const key of keys) { - const peerPubkey = key.replace(`${UNREAD_MESSAGES_KEY}-`, '') - const unreadData = getUnreadData(peerPubkey) - unreadData.unreadCount = 0 - saveUnreadData(peerPubkey, unreadData) - } - } catch (error) { - console.warn('Failed to clear unread counts from localStorage:', error) - } - } - - // Clear processed message IDs for a peer - const clearProcessedMessageIds = (peerPubkey: string): void => { - const unreadData = getUnreadData(peerPubkey) - unreadData.processedMessageIds.clear() - saveUnreadData(peerPubkey, unreadData) - } - - // Debug unread data for a peer - const debugUnreadData = (peerPubkey: string): void => { - // Function kept for potential future debugging - getUnreadData(peerPubkey) - } - - // Get relay configuration - const getRelays = (): NostrRelayConfig[] => { - return config.nostr.relays.map(url => ({ - url, - read: true, - write: true - })) - } - - // Connect using the relay hub - const connect = async () => { - try { - // The relay hub should already be initialized by the app - if (!relayHub.isConnected.value) { - await relayHub.connect() - } - - // Set up periodic cleanup of malformed messages - if (!cleanupInterval) { - cleanupInterval = setInterval(cleanupMalformedMessages, 5 * 60 * 1000) as unknown as number // Every 5 minutes - console.log('Set up periodic cleanup of malformed messages') - } - - } catch (error) { - console.error('Failed to connect to relays:', error) - throw error - } - } - - // Disconnect using the relay hub - const disconnect = () => { - // Note: We don't disconnect the relay hub here as other components might be using it - // The relay hub will be managed at the app level - - } - - // Load current user from LNBits - // const loadCurrentUser = async () => { - // try { - // // Get current user from LNBits API using the auth endpoint - // const authToken = getAuthToken() - // if (!authToken) { - // throw new Error('No authentication token found') - // } - - // const API_BASE_URL = config.api.baseUrl || 'http://localhost:5006' - // const response = await fetch(`${API_BASE_URL}/api/v1/auth/nostr/me`, { - // headers: { - // 'Authorization': `Bearer ${authToken}`, - // 'Content-Type': 'application/json' - // } - // }) - - // console.log('API Response status:', response.status) - // console.log('API Response headers:', response.headers) - - // const responseText = await response.text() - // console.log('API Response text:', responseText) - - // if (response.ok) { - // try { - // const user = JSON.parse(responseText) - // currentUser.value = { - // pubkey: user.pubkey, - // prvkey: user.prvkey - // } - // } catch (parseError) { - // console.error('JSON Parse Error:', parseError) - // console.error('Response was:', responseText) - // throw new Error('Invalid JSON response from API') - // } - // } else { - // console.error('API Error:', response.status, responseText) - // throw new Error(`Failed to load current user: ${response.status}`) - // } - // } catch (error) { - // console.error('Failed to load current user:', error) - // throw error - // } - // } - - // Subscribe to a specific peer for messages - const subscribeToPeer = async (peerPubkey: string) => { - if (!currentUser.value) { - console.warn('Cannot subscribe to peer: no user logged in') - return - } - - if (!currentUser.value.pubkey) { - console.warn('Cannot subscribe to peer: no public key available') - return - } - - // Check if we have a pool and are connected - if (!relayHub.isConnected.value) { - console.warn('Not connected to relays - attempting to connect...') - await connect() - } - - if (!relayHub.isConnected.value) { - throw new Error('Failed to initialize Nostr pool') - } - - try { - // Subscribe to direct messages (kind 4) from this peer - const filter = { - kinds: [4], - '#p': [currentUser.value.pubkey], // Messages where we are the recipient - authors: [peerPubkey] // Messages from this specific peer - } - - - - // Use the relay hub to subscribe - const unsubscribe = relayHub.subscribe({ - id: `peer-${peerPubkey}`, - filters: [filter], - onEvent: (event) => { - handleIncomingMessage(event, peerPubkey) - } - }) - - - - return unsubscribe - } catch (error) { - console.error('Failed to subscribe to peer:', error) - throw error - } - } - - // Subscribe to a peer for notifications only (without loading full message history) - const subscribeToPeerForNotifications = async (peerPubkey: string) => { - if (!currentUser.value) { - console.warn('No user logged in - cannot subscribe to peer notifications') - return null - } - - // Check if we have a pool and are connected - if (!relayHub.isConnected.value) { - console.warn('Not connected to relays - attempting to connect...') - await connect() - } - - if (!relayHub.isConnected.value) { - throw new Error('Failed to initialize Nostr pool') - } - - const myPubkey = currentUser.value.pubkey - - // Subscribe to new messages only (no historical messages) - const relayConfigs = getRelays() - - const filters = [ - { - kinds: [4], - authors: [peerPubkey], - '#p': [myPubkey] - }, - { - kinds: [4], - authors: [myPubkey], - '#p': [peerPubkey] - } - ] - - const unsubscribe = relayHub.subscribe({ - id: `notifications-${peerPubkey}-${Date.now()}`, - filters, - relays: relayConfigs.map(r => r.url), - onEvent: (event: any) => { - handleIncomingMessage(event, peerPubkey) - }, - onEose: () => { - // Notification subscription closed - } - }) - - return unsubscribe - } - - - - // Handle incoming message - const handleIncomingMessage = async (event: any, peerPubkey: string) => { - if (!currentUser.value || !currentUser.value.prvkey) { - console.warn('Cannot decrypt message: no private key available') - return - } - - // Check if we've already processed this message to prevent duplicates - if (processedMessageIds.value.has(event.id)) { - return - } - - // Check if this message was previously identified as malformed - if (malformedMessageIds.value.has(event.id)) { - console.log('Skipping previously identified malformed message:', event.id) - return - } - - try { - // For NIP-04 direct messages, always use peerPubkey as the second argument - // This is the public key of the other party in the conversation - const isSentByMe = event.pubkey === currentUser.value.pubkey - - // Check for malformed messages before attempting decryption - if (typeof event.content !== 'string' || event.content.length === 0) { - console.warn('Skipping message with invalid content format:', { - eventId: event.id, - contentType: typeof event.content, - contentLength: event.content?.length - }) - return - } - - // Check for our old placeholder encryption format - if (event.content.includes('[ENCRYPTED]') && event.content.includes('[ENCRYPTED]')) { - console.warn('Skipping message with old placeholder encryption format:', { - eventId: event.id, - content: event.content.substring(0, 100) + '...' - }) - return - } - - // Check for other common malformed patterns - if (event.content.startsWith('[') || event.content.includes('ENCRYPTED')) { - console.warn('Skipping message with suspicious encryption format:', { - eventId: event.id, - content: event.content.substring(0, 100) + '...' - }) - return - } - - const decryptedContent = await nip04.decrypt( - currentUser.value.prvkey, - peerPubkey, // Always use peerPubkey for shared secret derivation - event.content - ) - - - - // Create chat message - const message: ChatMessage = { - id: event.id, - content: decryptedContent, - created_at: event.created_at, - sent: isSentByMe, - pubkey: event.pubkey - } - - // Add to messages - if (!messages.value.has(peerPubkey)) { - messages.value.set(peerPubkey, []) - } - messages.value.get(peerPubkey)!.push(message) - - // Mark as unread if not sent by us AND created after last read timestamp - if (!isSentByMe) { - const unreadData = getUnreadData(peerPubkey) - - // Only count as unread if message was created after last read timestamp - if (event.created_at > unreadData.lastReadTimestamp) { - // Increment the unread count for this peer - const currentCount = unreadCounts.value.get(peerPubkey) || 0 - const newCount = currentCount + 1 - unreadCounts.value.set(peerPubkey, newCount) - - // Force reactivity - unreadCounts.value = new Map(unreadCounts.value) - - // Save to localStorage - unreadData.unreadCount = newCount - saveUnreadData(peerPubkey, unreadData) - } - } - - // Update latest message timestamp - updateLatestMessageTimestamp(peerPubkey, event.created_at) - - // Mark this message as processed to prevent duplicates - processedMessageIds.value.add(event.id) - - // Trigger callback if set - if (onMessageAdded.value) { - onMessageAdded.value(peerPubkey) - } - } catch (error) { - // Provide more specific error handling for different types of failures - const errorMessage = error instanceof Error ? error.message : 'Unknown error' - - // Check for specific error patterns that indicate malformed messages - if (errorMessage.includes('join.decode') || errorMessage.includes('input should be string')) { - console.warn('Skipping malformed message (invalid NIP-04 format):', { - eventId: event.id, - pubkey: event.pubkey, - error: errorMessage, - contentPreview: typeof event.content === 'string' ? event.content.substring(0, 100) + '...' : 'Invalid content type' - }) - markMessageAsMalformed(event.id) - return - } - - if (errorMessage.includes('Invalid byte sequence') || errorMessage.includes('hex string')) { - console.warn('Skipping message with invalid hex encoding:', { - eventId: event.id, - pubkey: event.pubkey, - error: errorMessage - }) - markMessageAsMalformed(event.id) - return - } - - // For other decryption errors, log with more context - console.error('Failed to decrypt message:', { - eventId: event.id, - pubkey: event.pubkey, - error: errorMessage, - contentType: typeof event.content, - contentLength: event.content?.length, - contentPreview: typeof event.content === 'string' ? event.content.substring(0, 100) + '...' : 'Invalid content type' - }) - } - } - - // Send message to a peer - const sendMessage = async (peerPubkey: string, content: string) => { - if (!currentUser.value) { - throw new Error('No user logged in - please authenticate first') - } - - // Check if we have the required Nostr keypair - if (!currentUser.value.prvkey) { - throw new Error('Nostr private key not available. Please ensure your LNBits account has Nostr keys configured.') - } - - // Check if we have a pool and are connected - if (!relayHub.isConnected.value) { - console.warn('Not connected to relays - attempting to connect...') - await connect() - } - - if (!relayHub.isConnected.value) { - throw new Error('Failed to initialize Nostr pool') - } - - try { - // Validate keys before encryption - if (!currentUser.value.prvkey || !peerPubkey) { - throw new Error('Missing private key or peer public key') - } - - // Ensure keys are in correct hex format (64 characters for private key, 64 characters for public key) - const privateKey = currentUser.value.prvkey.startsWith('0x') - ? currentUser.value.prvkey.slice(2) - : currentUser.value.prvkey - - const publicKey = peerPubkey.startsWith('0x') - ? peerPubkey.slice(2) - : peerPubkey - - if (privateKey.length !== 64) { - throw new Error(`Invalid private key length: ${privateKey.length} (expected 64)`) - } - - if (publicKey.length !== 64) { - throw new Error(`Invalid public key length: ${publicKey.length} (expected 64)`) - } - - // Validate hex format - const hexRegex = /^[0-9a-fA-F]+$/ - if (!hexRegex.test(privateKey)) { - throw new Error(`Invalid private key format: contains non-hex characters`) - } - - if (!hexRegex.test(publicKey)) { - throw new Error(`Invalid public key format: contains non-hex characters`) - } - - // Encrypt the message - let encryptedContent: string - try { - encryptedContent = await nip04.encrypt( - privateKey, - publicKey, - content - ) - } catch (encryptError) { - console.error('Encryption failed:', encryptError) - throw new Error(`Encryption failed: ${encryptError instanceof Error ? encryptError.message : String(encryptError)}`) - } - - // Create the event template - const eventTemplate: EventTemplate = { - kind: 4, - created_at: Math.floor(Date.now() / 1000), - tags: [['p', peerPubkey]], - content: encryptedContent - } - - // Finalize the event (sign it) - const event = finalizeEvent(eventTemplate, hexToBytes(privateKey)) - - // Publish to relays using the relay hub - await relayHub.publishEvent(event) - - // Add message to local state - const message: ChatMessage = { - id: event.id, - content, - created_at: event.created_at, - sent: true, - pubkey: currentUser.value.pubkey - } - - // Add to processed IDs to prevent duplicate processing - processedMessageIds.value.add(event.id) - - if (!messages.value.has(peerPubkey)) { - messages.value.set(peerPubkey, []) - } - - messages.value.get(peerPubkey)!.push(message) - - // Sort messages by timestamp - messages.value.get(peerPubkey)!.sort((a, b) => a.created_at - b.created_at) - - // Force reactivity by triggering a change - messages.value = new Map(messages.value) - - // Update latest message timestamp for this peer (for sorting) - updateLatestMessageTimestamp(peerPubkey, message.created_at) - - // Trigger callback if set - if (onMessageAdded.value) { - onMessageAdded.value(peerPubkey) - } - - } catch (error) { - console.error('Failed to send message:', error) - throw error - } - } - - // Get messages for a specific peer - const getMessages = (peerPubkey: string): ChatMessage[] => { - return messages.value.get(peerPubkey) || [] - } - - // Clear messages for a specific peer - const clearMessages = (peerPubkey: string) => { - messages.value.delete(peerPubkey) - } - - // Load peers from API - const loadPeers = async () => { - try { - const authToken = getAuthToken() - if (!authToken) { - throw new Error('No authentication token found') - } - - const API_BASE_URL = config.api.baseUrl || 'http://localhost:5006' - const response = await fetch(`${API_BASE_URL}/api/v1/auth/nostr/pubkeys`, { - headers: { - 'Authorization': `Bearer ${authToken}`, - 'Content-Type': 'application/json' - } - }) - - if (!response.ok) { - throw new Error(`Failed to load peers: ${response.status}`) - } - - const data = await response.json() - const loadedPeers = data.map((peer: any) => ({ - user_id: peer.user_id, - username: peer.username, - pubkey: peer.pubkey - })) - - // Store peers in the singleton state - peers.value = loadedPeers - - - return loadedPeers - - } catch (error) { - console.error('Failed to load peers:', error) - throw error - } - } - - // Subscribe to all peers for notifications (without loading full message history) - const subscribeToAllPeersForNotifications = async (peers: any[]) => { - if (!peers.length) { - return - } - - // Wait for connection to be established - if (!relayHub.isConnected.value) { - // Wait a bit for connection to establish - await new Promise(resolve => setTimeout(resolve, 1000)) - - if (!relayHub.isConnected.value) { - console.warn('Still not connected, skipping peer subscriptions') - return - } - } - - // Subscribe to each peer for notifications - for (const peer of peers) { - try { - await subscribeToPeerForNotifications(peer.pubkey) - } catch (error) { - console.error(`Failed to subscribe to peer ${peer.username} (${peer.pubkey}):`, error) - } - } - } - - return { - // State - isConnected: readonly(isConnected), - messages: readonly(messages), - isLoggedIn: readonly(isAuthenticated), - peers: readonly(peers), - - // Reactive computed properties - totalUnreadCount: computed(() => getTotalUnreadCount()), - - // Methods - connect, - disconnect, - subscribeToPeer, - subscribeToPeerForNotifications, - sendMessage, - getMessages, - clearMessages, - onMessageAdded, - - // Notification methods - markMessagesAsRead, - getUnreadCount, - getAllUnreadCounts, - getTotalUnreadCount, - clearAllUnreadCounts, - clearProcessedMessageIds, - debugUnreadData, - getUnreadData, - - // Timestamp methods (for sorting) - getLatestMessageTimestamp, - getAllLatestMessageTimestamps, - - // Peer management methods - loadPeers, - subscribeToAllPeersForNotifications, - currentUser, - hasNostrKeys, - getNostrKeyStatus, - markMessageAsMalformed, - cleanupMalformedMessages, - clearAllMalformedMessages, // Add the new function to the return object - cleanup, // Add the cleanup function to the return object - getMalformedMessageStats // Add the new function to the return object - } -} - -// Export singleton instance for global state -export const nostrChat = useNostrChat() \ No newline at end of file diff --git a/src/composables/useNostrclientHub.ts b/src/composables/useNostrclientHub.ts deleted file mode 100644 index d12debb..0000000 --- a/src/composables/useNostrclientHub.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { ref, computed, onUnmounted, readonly } from 'vue' -import { nostrclientHub, type SubscriptionConfig } from '../lib/nostr/nostrclientHub' - -export function useNostrclientHub() { - // Reactive state - const isConnected = ref(false) - const isConnecting = ref(false) - const connectionStatus = ref<'connecting' | 'connected' | 'disconnected' | 'error'>('disconnected') - const error = ref(null) - const activeSubscriptions = ref>(new Set()) - - // Reactive counts - const totalSubscriptionCount = ref(0) - const subscriptionDetails = ref>([]) - - // Computed properties - const connectionHealth = computed(() => { - return isConnected.value ? 100 : 0 - }) - - // Initialize nostrclient hub - const initialize = async (): Promise => { - try { - connectionStatus.value = 'connecting' - error.value = null - - console.log('🔧 NostrclientHub: Initializing...') - await nostrclientHub.initialize() - console.log('🔧 NostrclientHub: Initialization successful') - - // Set up event listeners - setupEventListeners() - - connectionStatus.value = 'connected' - isConnected.value = true - console.log('🔧 NostrclientHub: Connection status set to connected') - - } catch (err) { - const errorObj = err instanceof Error ? err : new Error('Failed to initialize NostrclientHub') - error.value = errorObj - connectionStatus.value = 'error' - isConnected.value = false - console.error('🔧 NostrclientHub: Failed to initialize:', errorObj) - throw errorObj - } - } - - // Connect to nostrclient - const connect = async (): Promise => { - try { - connectionStatus.value = 'connecting' - error.value = null - - await nostrclientHub.connect() - - connectionStatus.value = 'connected' - isConnected.value = true - } catch (err) { - const errorObj = err instanceof Error ? err : new Error('Failed to connect') - error.value = errorObj - connectionStatus.value = 'error' - isConnected.value = false - throw errorObj - } - } - - // Disconnect from nostrclient - const disconnect = (): void => { - nostrclientHub.disconnect() - connectionStatus.value = 'disconnected' - isConnected.value = false - error.value = null - } - - // Subscribe to events - const subscribe = (config: SubscriptionConfig): (() => void) => { - try { - const unsubscribe = nostrclientHub.subscribe(config) - activeSubscriptions.value.add(config.id) - - // Update reactive state - totalSubscriptionCount.value = nostrclientHub.totalSubscriptionCount - subscriptionDetails.value = nostrclientHub.subscriptionDetails - - return () => { - unsubscribe() - activeSubscriptions.value.delete(config.id) - totalSubscriptionCount.value = nostrclientHub.totalSubscriptionCount - subscriptionDetails.value = nostrclientHub.subscriptionDetails - } - } catch (err) { - console.error('Failed to subscribe:', err) - throw err - } - } - - // Publish an event - const publishEvent = async (event: any): Promise => { - try { - await nostrclientHub.publishEvent(event) - } catch (err) { - console.error('Failed to publish event:', err) - throw err - } - } - - // Query events - const queryEvents = async (filters: any[]): Promise => { - try { - return await nostrclientHub.queryEvents(filters) - } catch (err) { - console.error('Failed to query events:', err) - throw err - } - } - - // Set up event listeners - const setupEventListeners = () => { - nostrclientHub.on('connected', () => { - isConnected.value = true - isConnecting.value = false - connectionStatus.value = 'connected' - error.value = null - }) - - nostrclientHub.on('disconnected', () => { - isConnected.value = false - isConnecting.value = false - connectionStatus.value = 'disconnected' - }) - - nostrclientHub.on('error', (err: any) => { - error.value = err - connectionStatus.value = 'error' - }) - - nostrclientHub.on('connectionError', (err: any) => { - error.value = err - connectionStatus.value = 'error' - }) - - nostrclientHub.on('maxReconnectionAttemptsReached', () => { - error.value = new Error('Max reconnection attempts reached') - connectionStatus.value = 'error' - }) - - nostrclientHub.on('event', ({ subscriptionId, event }: any) => { - console.log('Received event for subscription:', subscriptionId, event.id) - }) - - nostrclientHub.on('eose', ({ subscriptionId }: any) => { - console.log('EOSE received for subscription:', subscriptionId) - }) - - nostrclientHub.on('notice', ({ message }: any) => { - console.log('Notice from nostrclient:', message) - }) - - nostrclientHub.on('eventPublished', ({ eventId }: any) => { - console.log('Event published successfully:', eventId) - }) - } - - // Clean up event listeners - const cleanup = () => { - nostrclientHub.removeAllListeners() - } - - // Auto-cleanup on unmount - onUnmounted(() => { - cleanup() - }) - - return { - // State - isConnected: readonly(isConnected), - isConnecting: readonly(isConnecting), - connectionStatus: readonly(connectionStatus), - error: readonly(error), - activeSubscriptions: readonly(activeSubscriptions), - totalSubscriptionCount: readonly(totalSubscriptionCount), - subscriptionDetails: readonly(subscriptionDetails), - - // Computed - connectionHealth: readonly(connectionHealth), - - // Methods - initialize, - connect, - disconnect, - subscribe, - publishEvent, - queryEvents, - - // Internal - cleanup - } -} diff --git a/src/composables/useOrderEvents.ts b/src/composables/useOrderEvents.ts deleted file mode 100644 index e8c2a4f..0000000 --- a/src/composables/useOrderEvents.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { ref, computed } from 'vue' -import { nip04 } from 'nostr-tools' -import { relayHubComposable } from './useRelayHub' -import { useAuth } from './useAuth' -import { useMarketStore } from '@/stores/market' -import { decode } from 'light-bolt11-decoder' - -// Nostrmarket Order interfaces based on the actual implementation - -// Nostrmarket Order interfaces based on the actual implementation -interface OrderItem { - product_id: string - quantity: number -} - -interface OrderContact { - nostr?: string - phone?: string - email?: string -} - -// Direct message types from nostrmarket -enum DirectMessageType { - PLAIN_TEXT = -1, - CUSTOMER_ORDER = 0, - PAYMENT_REQUEST = 1, - ORDER_PAID_OR_SHIPPED = 2 -} - -// Event types for nostrmarket protocol -interface CustomerOrderEvent { - type: DirectMessageType.CUSTOMER_ORDER - id: string - items: OrderItem[] - contact?: OrderContact - shipping_id: string - message?: string -} - -interface PaymentRequestEvent { - type: DirectMessageType.PAYMENT_REQUEST - id: string - message?: string - payment_options: Array<{ - type: string - link: string - }> -} - -interface OrderStatusEvent { - type: DirectMessageType.ORDER_PAID_OR_SHIPPED - id: string - message?: string - paid?: boolean - shipped?: boolean -} - -// Helper function to extract expiry from bolt11 invoice -function extractExpiryFromBolt11(bolt11String: string): string | undefined { - try { - const decoded = decode(bolt11String) - console.log('Decoded bolt11 invoice:', { - amount: decoded.sections.find(section => section.name === 'amount')?.value, - expiry: decoded.expiry, - timestamp: decoded.sections.find(section => section.name === 'timestamp')?.value - }) - - // Calculate expiry date from timestamp + expiry seconds - const timestamp = decoded.sections.find(section => section.name === 'timestamp')?.value as number - const expirySeconds = decoded.expiry as number - - if (timestamp && expirySeconds) { - const expiryDate = new Date((timestamp + expirySeconds) * 1000) - return expiryDate.toISOString() - } - - return undefined - } catch (error) { - console.warn('Failed to extract expiry from bolt11:', error) - return undefined - } -} - -export function useOrderEvents() { - const relayHub = relayHubComposable - const auth = useAuth() - const marketStore = useMarketStore() - - // State - const isSubscribed = ref(false) - const lastEventTimestamp = ref(0) - const processedEventIds = ref(new Set()) - const subscriptionId = ref(null) - - // Computed - const currentUserPubkey = computed(() => auth.currentUser?.value?.pubkey) - const isReady = computed(() => { - const isAuth = auth.isAuthenticated - const isConnected = relayHub.isConnected.value - const hasPubkey = !!currentUserPubkey.value - - return isAuth && isConnected && hasPubkey - }) - - // Subscribe to order events - const subscribeToOrderEvents = async () => { - if (!isReady.value || isSubscribed.value) { - return - } - - try { - // Subscribe to direct messages (kind 4) that contain order information - const filters = [ - { - kinds: [4], // NIP-04 encrypted direct messages - '#p': [currentUserPubkey.value].filter(Boolean) as string[], - since: lastEventTimestamp.value - } - ] - - relayHub.subscribe({ - id: 'order-events', - filters, - onEvent: handleOrderEvent, - onEose: () => { - console.log('Order events subscription ended') - } - }) - - subscriptionId.value = 'order-events' - isSubscribed.value = true - console.log('Successfully subscribed to order events') - - } catch (error) { - console.error('Failed to subscribe to order events:', error) - } - } - - // Handle incoming order events - const handleOrderEvent = async (event: any) => { - if (processedEventIds.value.has(event.id)) { - return - } - - processedEventIds.value.add(event.id) - lastEventTimestamp.value = Math.max(lastEventTimestamp.value, event.created_at) - - try { - // Decrypt the message content - const decryptedContent = await nip04.decrypt( - auth.currentUser.value?.prvkey || '', - event.pubkey, - event.content - ) - - // Parse the JSON content - const jsonData = JSON.parse(decryptedContent) - - // Handle different message types - switch (jsonData.type) { - case DirectMessageType.CUSTOMER_ORDER: - await handleCustomerOrder(jsonData as CustomerOrderEvent, event.pubkey) - break - case DirectMessageType.PAYMENT_REQUEST: - await handlePaymentRequest(jsonData as PaymentRequestEvent, event.pubkey) - break - case DirectMessageType.ORDER_PAID_OR_SHIPPED: - await handleOrderStatusUpdate(jsonData as OrderStatusEvent, event.pubkey) - break - default: - console.log('Unknown message type:', jsonData.type) - } - } catch (error) { - console.error('Error processing order event:', error) - } - } - - // Handle customer order (type 0) - const handleCustomerOrder = async (orderData: CustomerOrderEvent, _senderPubkey: string) => { - console.log('Received customer order:', orderData) - - // Create a basic order object from the event data - const order = { - id: orderData.id, - type: DirectMessageType.CUSTOMER_ORDER, - items: orderData.items, - contact: orderData.contact, - shipping_id: orderData.shipping_id, - message: orderData.message, - createdAt: Date.now(), - updatedAt: Date.now() - } - - // Store the order in our local state - // Note: We're not using the complex Order interface from market store - // Instead, we're using the simple nostrmarket format - console.log('Processed customer order:', order) - } - - // Handle payment request (type 1) - const handlePaymentRequest = async (paymentData: PaymentRequestEvent, _senderPubkey: string) => { - console.log('Received payment request:', paymentData) - - // Find the lightning payment option - const lightningOption = paymentData.payment_options?.find(opt => opt.type === 'ln') - if (lightningOption) { - console.log('Lightning payment request:', lightningOption.link) - - // Find the existing order by ID - const existingOrder = marketStore.orders[paymentData.id] - if (existingOrder) { - console.log('Found existing order, updating with payment request:', existingOrder.id) - - // Try to extract actual expiry from bolt11 - const actualExpiry = extractExpiryFromBolt11(lightningOption.link) - - // Create lightning invoice object - const lightningInvoice = { - checking_id: '', // Will be extracted from bolt11 if needed - payment_hash: '', // Will be extracted from bolt11 if needed - wallet_id: '', // Not available from payment request - amount: existingOrder.total, - fee: 0, // Not available from payment request - bolt11: lightningOption.link, - status: 'pending', - memo: paymentData.message || 'Payment for order', - created_at: new Date().toISOString(), - expiry: actualExpiry // Use actual expiry from bolt11 decoding - } - - // Update the order with the lightning invoice - marketStore.updateOrder(existingOrder.id, { - lightningInvoice, - status: 'pending', - paymentRequest: lightningOption.link, - updatedAt: Date.now() - }) - - console.log('Order updated with payment request:', existingOrder.id) - } else { - console.warn('Order not found for payment request:', paymentData.id) - } - } - } - - // Handle order status update (type 2) - const handleOrderStatusUpdate = async (statusData: OrderStatusEvent, _senderPubkey: string) => { - console.log('Received order status update:', statusData) - - // Update order status in local state - if (statusData.paid !== undefined) { - console.log(`Order ${statusData.id} payment status: ${statusData.paid}`) - } - if (statusData.shipped !== undefined) { - console.log(`Order ${statusData.id} shipping status: ${statusData.shipped}`) - } - } - - // Unsubscribe from order events - const unsubscribeFromOrderEvents = () => { - if (subscriptionId.value) { - relayHub.cleanup() - subscriptionId.value = null - } - isSubscribed.value = false - console.log('Unsubscribed from order events') - } - - // Watch for ready state changes - const watchReadyState = () => { - if (isReady.value && !isSubscribed.value) { - subscribeToOrderEvents() - } else if (!isReady.value && isSubscribed.value) { - unsubscribeFromOrderEvents() - } - } - - // Watch for authentication changes - const watchAuthChanges = () => { - if (auth.isAuthenticated && relayHub.isConnected.value) { - subscribeToOrderEvents() - } else { - unsubscribeFromOrderEvents() - } - } - - // Initialize subscription when ready - const initialize = () => { - if (isReady.value) { - subscribeToOrderEvents() - } - } - - // Cleanup - const cleanup = () => { - unsubscribeFromOrderEvents() - processedEventIds.value.clear() - } - - return { - // State - isSubscribed: computed(() => isSubscribed.value), - isReady: computed(() => isReady.value), - lastEventTimestamp: computed(() => lastEventTimestamp.value), - - // Methods - subscribeToOrderEvents, - unsubscribeFromOrderEvents, - initialize, - cleanup, - watchReadyState, - watchAuthChanges - } -} diff --git a/src/composables/useRelayHub.ts b/src/composables/useRelayHub.ts deleted file mode 100644 index 055e659..0000000 --- a/src/composables/useRelayHub.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { ref, computed, onMounted, onUnmounted, readonly } from 'vue' -import { relayHub, type SubscriptionConfig, type RelayStatus } from '../lib/nostr/relayHub' -import { config } from '../lib/config' - -export function useRelayHub() { - // Reactive state - const isConnected = ref(false) - const connectionStatus = ref<'connecting' | 'connected' | 'disconnected' | 'error'>('disconnected') - const relayStatuses = ref([]) - const error = ref(null) - const activeSubscriptions = ref>(new Set()) - - // Reactive relay counts - these will be updated when relayHub state changes - const connectedRelayCount = ref(0) - const totalRelayCount = ref(0) - const totalSubscriptionCount = ref(0) - - // Reactive subscription details - const subscriptionDetails = ref>([]) - - // Computed properties - const connectionHealth = computed(() => { - if (totalRelayCount.value === 0) return 0 - return (connectedRelayCount.value / totalRelayCount.value) * 100 - }) - - // Initialize relay hub - const initialize = async (): Promise => { - try { - connectionStatus.value = 'connecting' - error.value = null - - // Get relay URLs from config - const relayUrls = config.market.supportedRelays - console.log('🔧 RelayHub: Initializing with relay URLs:', relayUrls) - - if (!relayUrls || relayUrls.length === 0) { - throw new Error('No relay URLs configured') - } - - // Initialize the relay hub - console.log('🔧 RelayHub: Calling relayHub.initialize...') - await relayHub.initialize(relayUrls) - console.log('🔧 RelayHub: Initialization successful') - - // Set up event listeners - setupEventListeners() - - connectionStatus.value = 'connected' - isConnected.value = true - console.log('🔧 RelayHub: Connection status set to connected') - - - } catch (err) { - const errorObj = err instanceof Error ? err : new Error('Failed to initialize RelayHub') - error.value = errorObj - connectionStatus.value = 'error' - isConnected.value = false - console.error('🔧 RelayHub: Failed to initialize RelayHub:', errorObj) - throw errorObj - } - } - - // Connect to relays - const connect = async (): Promise => { - try { - if (!relayHub.isInitialized) { - await initialize() - return - } - - connectionStatus.value = 'connecting' - error.value = null - - await relayHub.connect() - - connectionStatus.value = 'connected' - isConnected.value = true - } catch (err) { - const errorObj = err instanceof Error ? err : new Error('Failed to connect') - error.value = errorObj - connectionStatus.value = 'error' - isConnected.value = false - throw errorObj - } - } - - // Disconnect from relays - const disconnect = (): void => { - relayHub.disconnect() - connectionStatus.value = 'disconnected' - isConnected.value = false - error.value = null - } - - // Subscribe to events - const subscribe = (config: SubscriptionConfig): (() => void) => { - try { - const unsubscribe = relayHub.subscribe(config) - activeSubscriptions.value.add(config.id) - - // Return enhanced unsubscribe function - return () => { - unsubscribe() - activeSubscriptions.value.delete(config.id) - } - } catch (err) { - const errorObj = err instanceof Error ? err : new Error('Failed to subscribe') - error.value = errorObj - throw errorObj - } - } - - // Publish an event - const publishEvent = async (event: any): Promise<{ success: number; total: number }> => { - try { - return await relayHub.publishEvent(event) - } catch (err) { - const errorObj = err instanceof Error ? err : new Error('Failed to publish event') - error.value = errorObj - throw errorObj - } - } - - // Query events (one-time fetch) - const queryEvents = async (filters: any[], relays?: string[]): Promise => { - try { - return await relayHub.queryEvents(filters, relays) - } catch (err) { - const errorObj = err instanceof Error ? err : new Error('Failed to query events') - error.value = errorObj - throw errorObj - } - } - - // Get relay status - const getRelayStatus = (url: string): RelayStatus | undefined => { - return relayStatuses.value.find(status => status.url === url) - } - - // Set up event listeners for relay hub events - const setupEventListeners = (): void => { - relayHub.on('connected', (count: number) => { - - isConnected.value = true - connectionStatus.value = 'connected' - error.value = null - connectedRelayCount.value = count - totalRelayCount.value = relayHub.totalRelayCount - totalSubscriptionCount.value = relayHub.totalSubscriptionCount - }) - - relayHub.on('disconnected', () => { - - isConnected.value = false - connectionStatus.value = 'disconnected' - error.value = null - connectedRelayCount.value = 0 - totalSubscriptionCount.value = 0 - }) - - relayHub.on('connectionError', (err: Error) => { - console.error('Connection error:', err) - error.value = err - connectionStatus.value = 'error' - isConnected.value = false - connectedRelayCount.value = 0 - }) - - relayHub.on('allRelaysDisconnected', () => { - console.warn('All relays disconnected') - isConnected.value = false - connectionStatus.value = 'disconnected' - connectedRelayCount.value = 0 - }) - - relayHub.on('partialDisconnection', ({ connected, total }: { connected: number; total: number }) => { - console.warn(`Partial disconnection: ${connected}/${total} relays connected`) - isConnected.value = connected > 0 - connectionStatus.value = connected > 0 ? 'connected' : 'disconnected' - connectedRelayCount.value = connected - totalRelayCount.value = total - }) - - relayHub.on('maxReconnectAttemptsReached', () => { - console.error('Max reconnection attempts reached') - connectionStatus.value = 'error' - isConnected.value = false - error.value = new Error('Max reconnection attempts reached') - connectedRelayCount.value = 0 - }) - - relayHub.on('networkOffline', () => { - console.log('Network went offline') - connectionStatus.value = 'disconnected' - isConnected.value = false - connectedRelayCount.value = 0 - }) - - // Subscription events - relayHub.on('subscriptionCreated', ({ count }: { id: string; count: number }) => { - - totalSubscriptionCount.value = count - }) - - relayHub.on('subscriptionRemoved', ({ count }: { id: string; count: number }) => { - - totalSubscriptionCount.value = count - }) - - // Update relay statuses periodically - const updateRelayStatuses = () => { - relayStatuses.value = relayHub.relayStatuses - // Also update the reactive counts to keep them in sync - connectedRelayCount.value = relayHub.connectedRelayCount - totalRelayCount.value = relayHub.totalRelayCount - totalSubscriptionCount.value = relayHub.totalSubscriptionCount - subscriptionDetails.value = relayHub.subscriptionDetails - } - - // Update immediately and then every 10 seconds - updateRelayStatuses() - const statusInterval = setInterval(updateRelayStatuses, 10000) - - // Cleanup interval on unmount - onUnmounted(() => { - clearInterval(statusInterval) - }) - } - - // Cleanup function - const cleanup = (): void => { - // Close all active subscriptions - activeSubscriptions.value.forEach(subId => { - relayHub.unsubscribe(subId) - }) - activeSubscriptions.value.clear() - } - - // Auto-initialize on mount if config is available - onMounted(async () => { - try { - if (config.nostr.relays && config.nostr.relays.length > 0) { - await initialize() - } - } catch (err) { - console.warn('Auto-initialization failed:', err) - } - }) - - // Cleanup on unmount - onUnmounted(() => { - cleanup() - }) - - return { - // State - isConnected: readonly(isConnected), - connectionStatus: readonly(connectionStatus), - relayStatuses: readonly(relayStatuses), - error: readonly(error), - activeSubscriptions: readonly(activeSubscriptions), - connectedRelayCount: readonly(connectedRelayCount), - totalRelayCount: readonly(totalRelayCount), - totalSubscriptionCount: readonly(totalSubscriptionCount), - subscriptionDetails: readonly(subscriptionDetails), - connectionHealth: readonly(connectionHealth), - - // Methods - initialize, - connect, - disconnect, - subscribe, - publishEvent, - queryEvents, - getRelayStatus, - getConnectionHealth: connectionHealth, - cleanup - } -} - -// Export singleton instance for global state -export const relayHubComposable = useRelayHub() diff --git a/src/composables/useTicketPurchase.ts b/src/composables/useTicketPurchase.ts deleted file mode 100644 index 269b26c..0000000 --- a/src/composables/useTicketPurchase.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { ref, computed, onUnmounted } from 'vue' -import { purchaseTicket, checkPaymentStatus, payInvoiceWithWallet } from '@/lib/api/events' -import { useAuth } from './useAuth' -import { toast } from 'vue-sonner' - -export function useTicketPurchase() { - const { isAuthenticated, currentUser } = useAuth() - - // State - const isLoading = ref(false) - const error = ref(null) - const paymentHash = ref(null) - const paymentRequest = ref(null) - const qrCode = ref(null) - const isPaymentPending = ref(false) - const isPayingWithWallet = ref(false) - - // Ticket QR code state - const ticketQRCode = ref(null) - const purchasedTicketId = ref(null) - const showTicketQR = ref(false) - - // Computed properties - const canPurchase = computed(() => isAuthenticated.value && currentUser.value) - const userDisplay = computed(() => { - if (!currentUser.value) return null - return { - name: currentUser.value.username || currentUser.value.id, - shortId: currentUser.value.id.slice(0, 8) - } - }) - - const userWallets = computed(() => currentUser.value?.wallets || []) - const hasWalletWithBalance = computed(() => - userWallets.value.some((wallet: any) => wallet.balance_msat > 0) - ) - - // Generate QR code for Lightning payment - async function generateQRCode(bolt11: string) { - try { - const qrcode = await import('qrcode') - const dataUrl = await qrcode.toDataURL(bolt11, { - width: 256, - margin: 2, - color: { - dark: '#000000', - light: '#FFFFFF' - } - }) - qrCode.value = dataUrl - } catch (err) { - console.error('Error generating QR code:', err) - error.value = 'Failed to generate QR code' - } - } - - // Generate QR code for ticket - async function generateTicketQRCode(ticketId: string) { - try { - const qrcode = await import('qrcode') - const ticketUrl = `ticket://${ticketId}` - const dataUrl = await qrcode.toDataURL(ticketUrl, { - width: 128, - margin: 2, - color: { - dark: '#000000', - light: '#FFFFFF' - } - }) - ticketQRCode.value = dataUrl - return dataUrl - } catch (error) { - console.error('Error generating ticket QR code:', error) - return null - } - } - - // Pay with wallet - async function payWithWallet(paymentRequest: string) { - const walletWithBalance = userWallets.value.find((wallet: any) => wallet.balance_msat > 0) - - if (!walletWithBalance) { - throw new Error('No wallet with sufficient balance found') - } - - try { - await payInvoiceWithWallet(paymentRequest, walletWithBalance.id, walletWithBalance.adminkey) - return true - } catch (error) { - console.error('Wallet payment failed:', error) - throw error - } - } - - // Purchase ticket for event - async function purchaseTicketForEvent(eventId: string) { - if (!canPurchase.value) { - throw new Error('User must be authenticated to purchase tickets') - } - - isLoading.value = true - error.value = null - paymentHash.value = null - paymentRequest.value = null - qrCode.value = null - ticketQRCode.value = null - purchasedTicketId.value = null - showTicketQR.value = false - - try { - // Get the invoice - const invoice = await purchaseTicket(eventId) - paymentHash.value = invoice.payment_hash - paymentRequest.value = invoice.payment_request - - // Generate QR code for payment - await generateQRCode(invoice.payment_request) - - // Try to pay with wallet if available - if (hasWalletWithBalance.value) { - isPayingWithWallet.value = true - try { - await payWithWallet(invoice.payment_request) - // If wallet payment succeeds, proceed to check payment status - await startPaymentStatusCheck(eventId, invoice.payment_hash) - } catch (walletError) { - // If wallet payment fails, fall back to manual payment - console.log('Wallet payment failed, falling back to manual payment:', walletError) - isPayingWithWallet.value = false - await startPaymentStatusCheck(eventId, invoice.payment_hash) - } - } else { - // No wallet balance, proceed with manual payment - await startPaymentStatusCheck(eventId, invoice.payment_hash) - } - } catch (err) { - error.value = err instanceof Error ? err.message : 'Failed to purchase ticket' - console.error('Error purchasing ticket:', err) - } finally { - isLoading.value = false - } - } - - // Start payment status check - async function startPaymentStatusCheck(eventId: string, hash: string) { - isPaymentPending.value = true - let checkInterval: number | null = null - - const checkPayment = async () => { - try { - const result = await checkPaymentStatus(eventId, hash) - - if (result.paid) { - isPaymentPending.value = false - if (checkInterval) { - clearInterval(checkInterval) - } - - // Generate ticket QR code - if (result.ticket_id) { - purchasedTicketId.value = result.ticket_id - await generateTicketQRCode(result.ticket_id) - showTicketQR.value = true - } - - toast.success('Ticket purchased successfully!') - } - } catch (err) { - console.error('Error checking payment status:', err) - } - } - - // Check immediately - await checkPayment() - - // Then check every 2 seconds - checkInterval = setInterval(checkPayment, 2000) as unknown as number - } - - // Stop payment status check - function stopPaymentStatusCheck() { - isPaymentPending.value = false - } - - // Reset payment state - function resetPaymentState() { - isLoading.value = false - error.value = null - paymentHash.value = null - paymentRequest.value = null - qrCode.value = null - isPaymentPending.value = false - isPayingWithWallet.value = false - ticketQRCode.value = null - purchasedTicketId.value = null - showTicketQR.value = false - } - - // Open Lightning wallet - function handleOpenLightningWallet() { - if (paymentRequest.value) { - window.open(`lightning:${paymentRequest.value}`, '_blank') - } - } - - // Cleanup function - function cleanup() { - stopPaymentStatusCheck() - } - - // Lifecycle - onUnmounted(() => { - cleanup() - }) - - return { - // State - isLoading, - error, - paymentHash, - paymentRequest, - qrCode, - isPaymentPending, - isPayingWithWallet, - ticketQRCode, - purchasedTicketId, - showTicketQR, - - // Computed - canPurchase, - userDisplay, - userWallets, - hasWalletWithBalance, - - // Actions - purchaseTicketForEvent, - handleOpenLightningWallet, - resetPaymentState, - cleanup, - generateTicketQRCode - } -} \ No newline at end of file diff --git a/src/composables/useUserTickets.ts b/src/composables/useUserTickets.ts deleted file mode 100644 index fd8e21b..0000000 --- a/src/composables/useUserTickets.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { computed } from 'vue' -import { useAsyncState } from '@vueuse/core' -import type { Ticket } from '@/lib/types/event' -import { fetchUserTickets } from '@/lib/api/events' -import { useAuth } from './useAuth' - -interface GroupedTickets { - eventId: string - tickets: Ticket[] - paidCount: number - pendingCount: number - registeredCount: number -} - -export function useUserTickets() { - const { isAuthenticated, currentUser } = useAuth() - - const { state: tickets, isLoading, error: asyncError, execute: refresh } = useAsyncState( - async () => { - if (!isAuthenticated.value || !currentUser.value) { - return [] - } - return await fetchUserTickets(currentUser.value.id) - }, - [] as Ticket[], - { - immediate: false, - resetOnExecute: false, - } - ) - - const error = computed(() => { - if (asyncError.value) { - return { - message: asyncError.value instanceof Error - ? asyncError.value.message - : 'An error occurred while fetching tickets' - } - } - return null - }) - - const sortedTickets = computed(() => { - return [...tickets.value].sort((a, b) => - new Date(b.time).getTime() - new Date(a.time).getTime() - ) - }) - - const paidTickets = computed(() => { - return sortedTickets.value.filter(ticket => ticket.paid) - }) - - const pendingTickets = computed(() => { - return sortedTickets.value.filter(ticket => !ticket.paid) - }) - - const registeredTickets = computed(() => { - return sortedTickets.value.filter(ticket => ticket.registered) - }) - - const unregisteredTickets = computed(() => { - return sortedTickets.value.filter(ticket => ticket.paid && !ticket.registered) - }) - - // Group tickets by event - const groupedTickets = computed(() => { - const groups = new Map() - - sortedTickets.value.forEach(ticket => { - if (!groups.has(ticket.event)) { - groups.set(ticket.event, { - eventId: ticket.event, - tickets: [], - paidCount: 0, - pendingCount: 0, - registeredCount: 0 - }) - } - - const group = groups.get(ticket.event)! - group.tickets.push(ticket) - - if (ticket.paid) { - group.paidCount++ - } else { - group.pendingCount++ - } - - if (ticket.registered) { - group.registeredCount++ - } - }) - - // Convert to array and sort by most recent ticket in each group - return Array.from(groups.values()).sort((a, b) => { - const aLatest = Math.max(...a.tickets.map(t => new Date(t.time).getTime())) - const bLatest = Math.max(...b.tickets.map(t => new Date(t.time).getTime())) - return bLatest - aLatest - }) - }) - - // Load tickets when authenticated - const loadTickets = async () => { - if (isAuthenticated.value && currentUser.value) { - await refresh() - } - } - - return { - // State - tickets: sortedTickets, - paidTickets, - pendingTickets, - registeredTickets, - unregisteredTickets, - groupedTickets, - isLoading, - error, - - // Actions - refresh: loadTickets, - } -} \ No newline at end of file diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts index b91b382..713b2f8 100644 --- a/src/lib/config/index.ts +++ b/src/lib/config/index.ts @@ -18,7 +18,6 @@ interface PushConfig { interface MarketConfig { defaultNaddr: string - supportedRelays: string[] lightningEnabled: boolean defaultCurrency: string } @@ -65,14 +64,6 @@ export const config: AppConfig = { }, market: { defaultNaddr: import.meta.env.VITE_MARKET_NADDR || '', - supportedRelays: parseJsonEnv(import.meta.env.VITE_MARKET_RELAYS, [ - 'ws://127.0.0.1:7777', - 'wss://relay.damus.io', - 'wss://relay.snort.social', - 'wss://nostr-pub.wellorder.net', - 'wss://nostr.zebedee.cloud', - 'wss://nostr.walletofsatoshi.com' - ]), lightningEnabled: Boolean(import.meta.env.VITE_LIGHTNING_ENABLED), defaultCurrency: import.meta.env.VITE_MARKET_DEFAULT_CURRENCY || 'sat' }, @@ -119,7 +110,8 @@ export const configUtils = { }, getMarketRelays: (): string[] => { - return config.market.supportedRelays + // Market now uses the same relays as the main Nostr configuration + return config.nostr.relays } } diff --git a/src/modules/chat/components/ChatComponent.vue b/src/modules/chat/components/ChatComponent.vue index 189ddd3..31d51a1 100644 --- a/src/modules/chat/components/ChatComponent.vue +++ b/src/modules/chat/components/ChatComponent.vue @@ -375,7 +375,7 @@ import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import { ScrollArea } from '@/components/ui/scroll-area' import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar' -import { nostrChat } from '@/composables/useNostrChat' +import { useChat } from '../composables/useChat' import { useFuzzySearch } from '@/composables/useFuzzySearch' @@ -386,8 +386,11 @@ interface Peer { pubkey: string } +// Initialize chat composable +const chat = useChat() + // State -const peers = computed(() => nostrChat.peers.value) +const peers = computed(() => chat.peers.value) const selectedPeer = ref(null) const messageInput = ref('') @@ -400,20 +403,30 @@ const scrollTarget = ref(null) // Mobile detection const isMobile = ref(false) -// Nostr chat composable (singleton) -const { - isConnected, - messages, - connect, - disconnect, - subscribeToPeer, - sendMessage: sendNostrMessage, - onMessageAdded, - markMessagesAsRead, - getUnreadCount, - totalUnreadCount, - getLatestMessageTimestamp -} = nostrChat +// Get methods and state from chat composable +// Note: The modular chat service handles connection and peer management automatically +const isConnected = computed(() => true) // Chat service manages connection +const messages = ref(new Map()) // Local messages map for compatibility +const totalUnreadCount = computed(() => chat.totalUnreadCount.value) + +// Adapter functions for compatibility with existing code +const connect = async () => {} // Connection handled by chat service +const disconnect = () => {} // Handled by chat service +const subscribeToPeer = async (peer: string) => {} // Handled by chat service +const sendNostrMessage = async (peer: string, content: string) => { + chat.selectPeer(peer) + await chat.sendMessage(content) +} +const onMessageAdded = (callback: Function) => {} // Event handling via chat service +const markMessagesAsRead = (peer: string) => chat.markAsRead(peer) +const getUnreadCount = (peer: string) => { + const peerData = chat.peers.value.find(p => p.pubkey === peer) + return peerData?.unreadCount || 0 +} +const getLatestMessageTimestamp = (peer: string) => { + const msgs = messages.value.get(peer) || [] + return msgs.length > 0 ? msgs[msgs.length - 1].created_at : 0 +} // Computed const currentMessages = computed(() => { @@ -498,7 +511,7 @@ const goBackToPeers = () => { const refreshPeers = async () => { isLoading.value = true try { - await nostrChat.loadPeers() + // Peers are loaded automatically by the chat service } catch (error) { console.error('Failed to refresh peers:', error) } finally { @@ -601,7 +614,7 @@ onMounted(async () => { // If no peers loaded, load them if (peers.value.length === 0) { - await nostrChat.loadPeers() + // Peers are loaded automatically by the chat service } }) diff --git a/src/modules/events/components/PurchaseTicketDialog.vue b/src/modules/events/components/PurchaseTicketDialog.vue index 2ec7657..4255ee5 100644 --- a/src/modules/events/components/PurchaseTicketDialog.vue +++ b/src/modules/events/components/PurchaseTicketDialog.vue @@ -4,7 +4,7 @@ import { onUnmounted } from 'vue' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' -import { useTicketPurchase } from '@/composables/useTicketPurchase' +import { useTicketPurchase } from '../composables/useTicketPurchase' import { useAuth } from '@/composables/useAuth' import { User, Wallet, CreditCard, Zap, Ticket } from 'lucide-vue-next' import { formatEventPrice, formatWalletBalance } from '@/lib/utils/formatting' diff --git a/src/modules/market/components/MarketSettings.vue b/src/modules/market/components/MarketSettings.vue index eb2c1bf..44ade18 100644 --- a/src/modules/market/components/MarketSettings.vue +++ b/src/modules/market/components/MarketSettings.vue @@ -201,13 +201,14 @@