diff --git a/docs/VisibilityService-Integration.md b/docs/VisibilityService-Integration.md index c43ad1d..b188c16 100644 --- a/docs/VisibilityService-Integration.md +++ b/docs/VisibilityService-Integration.md @@ -424,6 +424,219 @@ it('should handle app resume correctly', async () => { }) ``` +## WebSocket Connection Recovery Examples + +### Real-World Chat Message Recovery + +**Scenario**: User gets a phone call, returns to app 5 minutes later. Chat messages arrived while away. + +```typescript +export class ChatService extends BaseService { + private async handleAppResume(): Promise { + // Step 1: Check if subscription still exists + if (!this.subscriptionUnsubscriber) { + this.debug('Chat subscription lost during backgrounding - recreating') + this.setupMessageSubscription() + } + + // Step 2: Sync missed messages from all chat peers + await this.syncMissedMessages() + } + + private async syncMissedMessages(): Promise { + const peers = Array.from(this.peers.value.values()) + + for (const peer of peers) { + try { + // Get messages from last hour for this peer + const recentEvents = await this.relayHub.queryEvents([ + { + kinds: [4], // Encrypted DMs + authors: [peer.pubkey], + '#p': [this.getUserPubkey()], + since: Math.floor(Date.now() / 1000) - 3600, + limit: 20 + } + ]) + + // Process each recovered message + for (const event of recentEvents) { + await this.processIncomingMessage(event) + } + + this.debug(`Recovered ${recentEvents.length} messages from ${peer.pubkey.slice(0, 8)}`) + + } catch (error) { + console.warn(`Failed to recover messages from peer ${peer.pubkey.slice(0, 8)}:`, error) + } + } + } +} +``` + +### Nostr Relay Connection Recovery + +**Scenario**: Nostr relays disconnected due to mobile browser suspension. Subscriptions need restoration. + +```typescript +export class RelayHub extends BaseService { + private async handleResume(): Promise { + // Step 1: Check which relays are still connected + const disconnectedRelays = this.checkDisconnectedRelays() + + if (disconnectedRelays.length > 0) { + this.debug(`Found ${disconnectedRelays.length} disconnected relays`) + await this.reconnectToRelays(disconnectedRelays) + } + + // Step 2: Restore all subscriptions on recovered relays + await this.restoreSubscriptions() + + this.emit('connectionRecovered', { + reconnectedRelays: disconnectedRelays.length, + restoredSubscriptions: this.subscriptions.size + }) + } + + private async restoreSubscriptions(): Promise { + if (this.subscriptions.size === 0) return + + this.debug(`Restoring ${this.subscriptions.size} subscriptions`) + + for (const [id, config] of this.subscriptions) { + try { + // Recreate subscription on available relays + const subscription = this.pool.subscribeMany( + this.getAvailableRelays(), + config.filters, + { + onevent: (event) => this.emit('event', { subscriptionId: id, event }), + oneose: () => this.emit('eose', { subscriptionId: id }) + } + ) + + this.activeSubscriptions.set(id, subscription) + this.debug(`✅ Restored subscription: ${id}`) + + } catch (error) { + this.debug(`❌ Failed to restore subscription ${id}:`, error) + } + } + } +} +``` + +### WebSocket Service Recovery + +**Scenario**: Custom WebSocket service (like nostrclient-hub) needs reconnection after suspension. + +```typescript +export class WebSocketService extends BaseService { + private ws: WebSocket | null = null + private subscriptions = new Map() + + private async handleAppResume(): Promise { + // Step 1: Check WebSocket connection state + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + this.debug('WebSocket connection lost, reconnecting...') + await this.reconnect() + } + + // Step 2: Resubscribe to all active subscriptions + if (this.ws?.readyState === WebSocket.OPEN) { + await this.resubscribeAll() + } + } + + private async reconnect(): Promise { + if (this.ws) { + this.ws.close() + } + + return new Promise((resolve, reject) => { + this.ws = new WebSocket(this.config.url) + + this.ws.onopen = () => { + this.debug('WebSocket reconnected successfully') + resolve() + } + + this.ws.onerror = (error) => { + this.debug('WebSocket reconnection failed:', error) + reject(error) + } + + this.ws.onmessage = (message) => { + this.handleMessage(JSON.parse(message.data)) + } + }) + } + + private async resubscribeAll(): Promise { + for (const [id, config] of this.subscriptions) { + const subscribeMessage = JSON.stringify(['REQ', id, ...config.filters]) + this.ws?.send(subscribeMessage) + this.debug(`Resubscribed to: ${id}`) + } + } +} +``` + +## Debugging Connection Issues + +### Enable Debug Logging + +```typescript +// In browser console or service initialization +localStorage.setItem('debug', 'VisibilityService,RelayHub,ChatService') + +// Or programmatically in your service +protected async onInitialize(): Promise { + if (import.meta.env.DEV) { + this.visibilityService.on('debug', (message, data) => { + console.log(`[VisibilityService] ${message}`, data) + }) + } +} +``` + +### Check Connection Status + +```javascript +// In browser console +// Check visibility state +console.log('Visibility state:', visibilityService.getState()) + +// Check relay connections +console.log('Relay status:', relayHub.getConnectionStatus()) + +// Check active subscriptions +console.log('Active subscriptions:', relayHub.subscriptionDetails) + +// Force connection check +await visibilityService.forceConnectionCheck() +``` + +### Monitor Recovery Events + +```typescript +export class MyService extends BaseService { + protected async onInitialize(): Promise { + // Listen for visibility events + this.visibilityService.on('visibilityChanged', (isVisible) => { + console.log('App visibility changed:', isVisible) + }) + + // Listen for reconnection events + this.relayHub.on('connectionRecovered', (data) => { + console.log('Connections recovered:', data) + }) + } +} +``` + --- -This integration guide provides everything a module developer needs to add visibility management to their services. The patterns are battle-tested and optimize for both user experience and device battery life. \ No newline at end of file +This integration guide provides everything a module developer needs to add visibility management to their services. The patterns are battle-tested and optimize for both user experience and device battery life. + +**Key takeaway**: Mobile browsers WILL suspend WebSocket connections when apps lose focus. Integrating with VisibilityService ensures your real-time features work reliably across all platforms and usage patterns. \ No newline at end of file diff --git a/docs/VisibilityService.md b/docs/VisibilityService.md index 9afc945..2eefe46 100644 --- a/docs/VisibilityService.md +++ b/docs/VisibilityService.md @@ -423,49 +423,194 @@ export class RelayHub extends BaseService { } ``` -### Chat Service Integration +### Chat Service Integration - WebSocket Connection Recovery + +**Real-World Scenario**: User receives a WhatsApp notification, switches to WhatsApp for 2 minutes, then returns to the Nostr chat app. The WebSocket connection was suspended by the mobile browser. ```typescript export class ChatService extends BaseService { - private messageQueue: Message[] = [] - private connectionRetryCount = 0 + protected readonly metadata = { + name: 'ChatService', + version: '1.0.0', + dependencies: ['RelayHub', 'AuthService', 'VisibilityService'] + } + + private subscriptionUnsubscriber?: () => void + private visibilityUnsubscribe?: () => void - private async handleResume(): Promise { - // Reset retry count on successful resume - this.connectionRetryCount = 0 + protected async onInitialize(): Promise { + // Set up chat subscription and register with visibility service + await this.initializeMessageHandling() + this.registerWithVisibilityService() - // Check if we missed any messages while away + this.debug('Chat service fully initialized!') + } + + private registerWithVisibilityService(): void { + if (!this.visibilityService) return + + this.visibilityUnsubscribe = this.visibilityService.registerService( + this.metadata.name, + async () => this.handleAppResume(), + async () => this.handleAppPause() + ) + } + + /** + * STEP 1: App becomes visible again + * VisibilityService detects visibility change and calls this method + */ + private async handleAppResume(): Promise { + this.debug('App resumed - checking chat connections') + + // Check if our chat subscription is still active + if (!this.subscriptionUnsubscriber) { + this.debug('Chat subscription lost, re-establishing...') + this.setupMessageSubscription() // Recreate subscription + } + + // Sync any messages missed while app was hidden await this.syncMissedMessages() - - // Process any queued messages - await this.processMessageQueue() - - // Resume real-time message monitoring - this.startMessageMonitoring() - } - - private async handlePause(): Promise { - // Queue outgoing messages instead of sending immediately - this.enableMessageQueueing() - - // Stop real-time monitoring - this.stopMessageMonitoring() - - // Save current conversation state - await this.saveConversationState() } + /** + * STEP 2: Sync missed messages from the time we were away + */ private async syncMissedMessages(): Promise { - const lastSeenTimestamp = this.getLastSeenTimestamp() - const missedMessages = await this.fetchMessagesSince(lastSeenTimestamp) + try { + const peers = Array.from(this.peers.value.values()) + const syncPromises = peers.map(peer => this.loadRecentMessagesForPeer(peer.pubkey)) + + await Promise.allSettled(syncPromises) + this.debug('Missed messages sync completed') + + } catch (error) { + console.warn('Failed to sync missed messages:', error) + } + } + + /** + * STEP 3: Load recent messages for each chat peer + */ + private async loadRecentMessagesForPeer(peerPubkey: string): Promise { + const userPubkey = this.authService?.user?.value?.pubkey + if (!userPubkey || !this.relayHub) return + + try { + // Get messages from the last hour (while we were away) + const oneHourAgo = Math.floor(Date.now() / 1000) - 3600 + + const events = await this.relayHub.queryEvents([ + { + kinds: [4], // Encrypted DMs + authors: [peerPubkey], + '#p': [userPubkey], + since: oneHourAgo, + limit: 10 + }, + { + kinds: [4], // Encrypted DMs from us to them + authors: [userPubkey], + '#p': [peerPubkey], + since: oneHourAgo, + limit: 10 + } + ]) + + // Process any new messages we missed + for (const event of events) { + await this.processIncomingMessage(event) + } + + } catch (error) { + this.debug(`Failed to load recent messages for peer ${peerPubkey.slice(0, 8)}:`, error) + } + } + + /** + * STEP 4: Process messages (decrypt, filter market messages, add to chat) + */ + private async processIncomingMessage(event: any): Promise { + try { + const userPubkey = this.authService?.user?.value?.pubkey + const userPrivkey = this.authService?.user?.value?.prvkey + + if (!userPubkey || !userPrivkey) return + + const senderPubkey = event.pubkey + const decryptedContent = await nip04.decrypt(userPrivkey, senderPubkey, event.content) + + // Check if this is a market order (type 1 or 2) + let isMarketMessage = false + try { + const parsedContent = JSON.parse(decryptedContent) + if (parsedContent.type === 1 || parsedContent.type === 2) { + isMarketMessage = true + if (this.marketMessageHandler) { + await this.marketMessageHandler(event) + } + } + } catch (e) { + // Not JSON, treat as regular chat message + } + + // Add to chat if it's not a market message + if (!isMarketMessage) { + const message: ChatMessage = { + id: event.id, + content: decryptedContent, + created_at: event.created_at, + sent: false, + pubkey: senderPubkey + } + + this.addPeer(senderPubkey) // Ensure peer exists + this.addMessage(senderPubkey, message) // Add to chat history + + console.log('💬 Recovered missed message from:', senderPubkey.slice(0, 8)) + } + + } catch (error) { + console.error('Failed to process recovered message:', error) + } + } + + /** + * Battery-conscious pause behavior + */ + private async handleAppPause(): Promise { + this.debug('App paused - chat subscription maintained for quick resume') - for (const message of missedMessages) { - this.processMessage(message) + // Don't immediately unsubscribe - RelayHub will handle connection management + // This allows for quick resume without full subscription recreation overhead + } + + /** + * Cleanup when service is disposed + */ + protected async onDispose(): Promise { + if (this.visibilityUnsubscribe) { + this.visibilityUnsubscribe() + } + + if (this.subscriptionUnsubscriber) { + this.subscriptionUnsubscriber() } } } ``` +**What happens in this example:** + +1. **🔍 Detection**: VisibilityService detects app became visible after 2+ minutes +2. **🔌 Connection Check**: ChatService checks if its subscription is still active +3. **📥 Message Recovery**: Queries for missed messages from all chat peers in the last hour +4. **🔓 Decryption**: Decrypts and processes each missed message +5. **📱 UI Update**: New messages appear in chat history as if they were never missed +6. **⚡ Real-time Resume**: Chat subscription is fully restored for new incoming messages + +**The user experience**: Seamless. Messages that arrived while the app was backgrounded appear instantly when the app regains focus. + ### Custom Service Example ```typescript @@ -569,6 +714,74 @@ private async handleAppResume(): Promise { } ``` +#### WebSocket Connection Issues + +```typescript +// Problem: WebSocket connections not recovering after app backgrounding +// Common on mobile browsers (iOS Safari, Chrome mobile) + +// ❌ Incorrect - not integrating with VisibilityService +export class MyRealtimeService extends BaseService { + private ws: WebSocket | null = null + + protected async onInitialize(): Promise { + this.ws = new WebSocket('wss://example.com') + // Missing: visibility service registration + } +} + +// ✅ Correct - proper WebSocket recovery integration +export class MyRealtimeService extends BaseService { + protected readonly metadata = { + name: 'MyRealtimeService', + dependencies: ['VisibilityService'] + } + + private ws: WebSocket | null = null + private visibilityUnsubscribe?: () => void + + protected async onInitialize(): Promise { + await this.connect() + this.registerWithVisibilityService() // ✅ Essential for mobile + } + + private registerWithVisibilityService(): void { + if (!this.visibilityService) return + + this.visibilityUnsubscribe = this.visibilityService.registerService( + this.metadata.name, + async () => this.handleAppResume(), + async () => this.handleAppPause() + ) + } + + private async handleAppResume(): Promise { + // Check WebSocket connection health + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + this.debug('WebSocket disconnected, reconnecting...') + await this.connect() + } + + // Restore any lost subscriptions + await this.restoreSubscriptions() + } + + private async handleAppPause(): Promise { + // Don't immediately close WebSocket + // Mobile browsers may suspend it anyway + this.debug('App paused - WebSocket will be checked on resume') + } +} +``` + +**Mobile WebSocket Behavior:** +- **iOS Safari**: Suspends WebSocket connections after ~30 seconds in background +- **Chrome Mobile**: May suspend connections when memory is needed +- **Desktop**: Generally maintains connections but may timeout after extended periods +- **PWA Standalone**: Better connection persistence but still subject to system limitations + +**Solution**: Always integrate WebSocket services with VisibilityService for automatic recovery. + #### Memory Leaks ```typescript diff --git a/src/modules/base/index.ts b/src/modules/base/index.ts index b42cbd3..56f15b1 100644 --- a/src/modules/base/index.ts +++ b/src/modules/base/index.ts @@ -56,6 +56,10 @@ export const baseModule: ModulePlugin = { waitForDependencies: false, // VisibilityService has no dependencies maxRetries: 1 }) + await nostrclientHub.initialize({ + waitForDependencies: true, // NostrClientHub depends on VisibilityService + maxRetries: 3 + }) console.log('✅ Base module installed successfully') }, @@ -63,9 +67,12 @@ export const baseModule: ModulePlugin = { async uninstall() { console.log('🗑️ Uninstalling base module...') - // Cleanup Nostr connections - relayHub.disconnect() - nostrclientHub.disconnect?.() + // Cleanup services + await relayHub.dispose() + await nostrclientHub.dispose() + await auth.dispose() + await paymentService.dispose() + await visibilityService.dispose() console.log('✅ Base module uninstalled') }, diff --git a/src/modules/base/nostr/nostrclient-hub.ts b/src/modules/base/nostr/nostrclient-hub.ts index 0db1f6a..80e7f37 100644 --- a/src/modules/base/nostr/nostrclient-hub.ts +++ b/src/modules/base/nostr/nostrclient-hub.ts @@ -1,30 +1,6 @@ import type { Filter, Event } from 'nostr-tools' +import { BaseService } from '@/core/base/BaseService' -// Simple EventEmitter for browser compatibility -class EventEmitter { - private events: { [key: string]: Function[] } = {} - - on(event: string, listener: Function) { - if (!this.events[event]) { - this.events[event] = [] - } - this.events[event].push(listener) - } - - emit(event: string, ...args: any[]) { - if (this.events[event]) { - this.events[event].forEach(listener => listener(...args)) - } - } - - removeAllListeners(event?: string) { - if (event) { - delete this.events[event] - } else { - this.events = {} - } - } -} export interface NostrclientConfig { url: string @@ -46,7 +22,18 @@ export interface RelayStatus { error?: string } -export class NostrclientHub extends EventEmitter { +export class NostrclientHub extends BaseService { + // Service metadata + protected readonly metadata = { + name: 'NostrclientHub', + version: '1.0.0', + dependencies: ['VisibilityService'] + } + + // EventEmitter functionality + private events: { [key: string]: Function[] } = {} + + // Service state private ws: WebSocket | null = null private config: NostrclientConfig private subscriptions: Map = new Map() @@ -54,6 +41,7 @@ export class NostrclientHub extends EventEmitter { private reconnectAttempts = 0 private readonly maxReconnectAttempts = 5 private readonly reconnectDelay = 5000 + private visibilityUnsubscribe?: () => void // Connection state private _isConnected = false @@ -64,6 +52,42 @@ export class NostrclientHub extends EventEmitter { this.config = config } + // EventEmitter methods + on(event: string, listener: Function) { + if (!this.events[event]) { + this.events[event] = [] + } + this.events[event].push(listener) + } + + emit(event: string, ...args: any[]) { + if (this.events[event]) { + this.events[event].forEach(listener => listener(...args)) + } + } + + removeAllListeners(event?: string) { + if (event) { + delete this.events[event] + } else { + this.events = {} + } + } + + /** + * Service-specific initialization (called by BaseService) + */ + protected async onInitialize(): Promise { + // Connect to WebSocket + console.log('🔧 NostrclientHub: Initializing connection to', this.config.url) + await this.connect() + + // Register with visibility service + this.registerWithVisibilityService() + + this.debug('NostrclientHub initialized') + } + get isConnected(): boolean { return this._isConnected } @@ -83,13 +107,6 @@ export class NostrclientHub extends EventEmitter { })) } - /** - * Initialize and connect to nostrclient WebSocket - */ - async initialize(): Promise { - console.log('🔧 NostrclientHub: Initializing connection to', this.config.url) - await this.connect() - } /** * Connect to the nostrclient WebSocket @@ -351,6 +368,69 @@ export class NostrclientHub extends EventEmitter { await this.connect() }, delay) as unknown as number } + + /** + * Register with VisibilityService for connection management + */ + private registerWithVisibilityService(): void { + if (!this.visibilityService) { + this.debug('VisibilityService not available') + return + } + + this.visibilityUnsubscribe = this.visibilityService.registerService( + this.metadata.name, + async () => this.handleAppResume(), + async () => this.handleAppPause() + ) + + this.debug('Registered with VisibilityService') + } + + /** + * Handle app resuming from visibility change + */ + private async handleAppResume(): Promise { + this.debug('App resumed - checking nostrclient WebSocket connection') + + // Check if we need to reconnect + if (!this.isConnected && !this._isConnecting) { + this.debug('WebSocket disconnected, attempting to reconnect...') + await this.connect() + } else if (this.isConnected) { + // Connection is alive, resubscribe to ensure all subscriptions are active + this.resubscribeAll() + } + } + + /** + * Handle app pausing from visibility change + */ + private async handleAppPause(): Promise { + this.debug('App paused - WebSocket connection will be maintained for quick resume') + + // Don't immediately disconnect - WebSocket will be checked on resume + // This allows for quick resume without full reconnection overhead + } + + /** + * Cleanup when service is disposed (overrides BaseService) + */ + protected async onDispose(): Promise { + // Unregister from visibility service + if (this.visibilityUnsubscribe) { + this.visibilityUnsubscribe() + this.visibilityUnsubscribe = undefined + } + + // Disconnect WebSocket + this.disconnect() + + // Clear all event listeners + this.removeAllListeners() + + this.debug('NostrclientHub disposed') + } } // Export singleton instance diff --git a/src/modules/chat/services/chat-service.ts b/src/modules/chat/services/chat-service.ts index 4457a8d..7c7bf71 100644 --- a/src/modules/chat/services/chat-service.ts +++ b/src/modules/chat/services/chat-service.ts @@ -14,7 +14,7 @@ export class ChatService extends BaseService { protected readonly metadata = { name: 'ChatService', version: '1.0.0', - dependencies: ['RelayHub', 'AuthService'] + dependencies: ['RelayHub', 'AuthService', 'VisibilityService'] } // Service-specific state @@ -23,6 +23,7 @@ export class ChatService extends BaseService { private config: ChatConfig private subscriptionUnsubscriber?: () => void private marketMessageHandler?: (event: any) => Promise + private visibilityUnsubscribe?: () => void constructor(config: ChatConfig) { super() @@ -71,6 +72,9 @@ export class ChatService extends BaseService { // Initialize message handling (subscription + history loading) await this.initializeMessageHandling() + // Register with visibility service + this.registerWithVisibilityService() + this.debug('Chat service fully initialized and ready!') } @@ -491,7 +495,6 @@ export class ChatService extends BaseService { } const userPubkey = this.authService.user.value.pubkey - const userPrivkey = this.authService.user.value.prvkey // Subscribe to encrypted direct messages (kind 4) addressed to this user this.subscriptionUnsubscriber = this.relayHub.subscribe({ @@ -503,61 +506,12 @@ export class ChatService extends BaseService { } ], onEvent: async (event: Event) => { - try { - // Find the sender's pubkey from the event - const senderPubkey = event.pubkey - - // Skip our own messages - if (senderPubkey === userPubkey) { - return - } - - // Decrypt the message - const decryptedContent = await nip04.decrypt(userPrivkey, senderPubkey, event.content) - - // Check if this is a market-related message (JSON with type field) - let isMarketMessage = false - try { - const parsedContent = JSON.parse(decryptedContent) - if (parsedContent && typeof parsedContent.type === 'number' && (parsedContent.type === 1 || parsedContent.type === 2)) { - // This is a market message (payment request type 1 or status update type 2) - isMarketMessage = true - console.log('🛒 Forwarding market message to market handler:', parsedContent.type) - - // Forward to market handler - if (this.marketMessageHandler) { - await this.marketMessageHandler(event) - } else { - console.warn('Market message handler not available, message will be treated as chat') - } - } - } catch (e) { - // Not JSON or not a market message, treat as regular chat - } - - // Only process as chat message if it's not a market message - if (!isMarketMessage) { - // Create a chat message - const message: ChatMessage = { - id: event.id, - content: decryptedContent, - created_at: event.created_at, - sent: false, - pubkey: senderPubkey - } - - // Ensure we have a peer record for the sender - this.addPeer(senderPubkey) - - // Add the message - this.addMessage(senderPubkey, message) - - console.log('Received encrypted chat message from:', senderPubkey.slice(0, 8)) - } - - } catch (error) { - console.error('Failed to decrypt incoming message:', error) + // Skip our own messages + if (event.pubkey === userPubkey) { + return } + + await this.processIncomingMessage(event) }, onEose: () => { console.log('Chat message subscription EOSE received') @@ -571,10 +525,178 @@ export class ChatService extends BaseService { } } + /** + * Register with VisibilityService for connection management + */ + private registerWithVisibilityService(): void { + if (!this.visibilityService) { + this.debug('VisibilityService not available') + return + } + + this.visibilityUnsubscribe = this.visibilityService.registerService( + this.metadata.name, + async () => this.handleAppResume(), + async () => this.handleAppPause() + ) + + this.debug('Registered with VisibilityService') + } + + /** + * Handle app resuming from visibility change + */ + private async handleAppResume(): Promise { + this.debug('App resumed - checking chat connections') + + // Check if subscription is still active + if (!this.subscriptionUnsubscriber) { + this.debug('Chat subscription lost, re-establishing...') + this.setupMessageSubscription() + } + + // Check if we need to sync missed messages + await this.syncMissedMessages() + } + + /** + * Handle app pausing from visibility change + */ + private async handleAppPause(): Promise { + this.debug('App paused - chat subscription will be maintained for quick resume') + + // Don't immediately unsubscribe - let RelayHub handle connection management + // Subscriptions will be restored automatically on resume if needed + } + + /** + * Sync any messages that might have been missed while app was hidden + */ + private async syncMissedMessages(): Promise { + try { + // For each peer, try to load recent messages + const peers = Array.from(this.peers.value.values()) + const syncPromises = peers.map(peer => this.loadRecentMessagesForPeer(peer.pubkey)) + + await Promise.allSettled(syncPromises) + this.debug('Missed messages sync completed') + + } catch (error) { + console.warn('Failed to sync missed messages:', error) + } + } + + /** + * Process an incoming message event + */ + private async processIncomingMessage(event: any): Promise { + try { + const userPubkey = this.authService?.user?.value?.pubkey + const userPrivkey = this.authService?.user?.value?.prvkey + + if (!userPubkey || !userPrivkey) { + console.warn('Cannot process message: user not authenticated') + return + } + + // Get sender pubkey from event + const senderPubkey = event.pubkey + + // Decrypt the message content + const decryptedContent = await nip04.decrypt(userPrivkey, senderPubkey, event.content) + + // Check if this is a market-related message + let isMarketMessage = false + try { + const parsedContent = JSON.parse(decryptedContent) + if (parsedContent.type === 1 || parsedContent.type === 2) { + // This is a market order message + isMarketMessage = true + + // Forward to market handler + if (this.marketMessageHandler) { + await this.marketMessageHandler(event) + } else { + console.warn('Market message handler not available, message will be treated as chat') + } + } + } catch (e) { + // Not JSON or not a market message, treat as regular chat + } + + // Only process as chat message if it's not a market message + if (!isMarketMessage) { + // Create a chat message + const message: ChatMessage = { + id: event.id, + content: decryptedContent, + created_at: event.created_at, + sent: false, + pubkey: senderPubkey + } + + // Ensure we have a peer record for the sender + this.addPeer(senderPubkey) + + // Add the message + this.addMessage(senderPubkey, message) + + console.log('Received encrypted chat message from:', senderPubkey.slice(0, 8)) + } + + } catch (error) { + console.error('Failed to process incoming message:', error) + } + } + + /** + * Load recent messages for a specific peer + */ + private async loadRecentMessagesForPeer(peerPubkey: string): Promise { + const userPubkey = this.authService?.user?.value?.pubkey + if (!userPubkey || !this.relayHub) return + + try { + // Get last 10 messages from the last hour for this peer + const oneHourAgo = Math.floor(Date.now() / 1000) - 3600 + + const events = await this.relayHub.queryEvents([ + { + kinds: [4], // Encrypted DMs + authors: [peerPubkey], + '#p': [userPubkey], + since: oneHourAgo, + limit: 10 + }, + { + kinds: [4], // Encrypted DMs + authors: [userPubkey], + '#p': [peerPubkey], + since: oneHourAgo, + limit: 10 + } + ]) + + // Process any new messages + for (const event of events) { + await this.processIncomingMessage(event) + } + + } catch (error) { + this.debug(`Failed to load recent messages for peer ${peerPubkey.slice(0, 8)}:`, error) + } + } + /** * Cleanup when service is disposed (overrides BaseService) */ protected async onDispose(): Promise { + // Unregister from visibility service + if (this.visibilityUnsubscribe) { + this.visibilityUnsubscribe() + this.visibilityUnsubscribe = undefined + } + // Unsubscribe from message subscription if (this.subscriptionUnsubscriber) { this.subscriptionUnsubscriber()