- Updated ChatService to forward market messages to the chat interface, ensuring they are displayed alongside regular chat messages. - Improved message formatting for payment requests and order status updates, providing clearer user notifications. - Deprecated the previous order updates subscription method in useMarket, redirecting functionality to the chat service for better integration. These changes improve the user experience by consolidating message handling and enhancing clarity in communication.
861 lines
No EOL
28 KiB
TypeScript
861 lines
No EOL
28 KiB
TypeScript
import { ref, computed } from 'vue'
|
|
import { eventBus } from '@/core/event-bus'
|
|
import { BaseService } from '@/core/base/BaseService'
|
|
import { nip04, finalizeEvent, type Event, type EventTemplate } from 'nostr-tools'
|
|
import type { ChatMessage, ChatPeer, UnreadMessageData, ChatConfig } from '../types'
|
|
import { getAuthToken } from '@/lib/config/lnbits'
|
|
import { config } from '@/lib/config'
|
|
|
|
|
|
export class ChatService extends BaseService {
|
|
// Service metadata
|
|
protected readonly metadata = {
|
|
name: 'ChatService',
|
|
version: '1.0.0',
|
|
dependencies: ['RelayHub', 'AuthService', 'VisibilityService', 'StorageService']
|
|
}
|
|
|
|
// Service-specific state
|
|
private messages = ref<Map<string, ChatMessage[]>>(new Map())
|
|
private peers = ref<Map<string, ChatPeer>>(new Map())
|
|
private config: ChatConfig
|
|
private subscriptionUnsubscriber?: () => void
|
|
private marketMessageHandler?: (event: any) => Promise<void>
|
|
private visibilityUnsubscribe?: () => void
|
|
private isFullyInitialized = false
|
|
private authCheckInterval?: ReturnType<typeof setInterval>
|
|
|
|
constructor(config: ChatConfig) {
|
|
super()
|
|
this.config = config
|
|
this.loadPeersFromStorage()
|
|
}
|
|
|
|
// Register market message handler for forwarding market-related DMs
|
|
setMarketMessageHandler(handler: (event: any) => Promise<void>) {
|
|
this.marketMessageHandler = handler
|
|
}
|
|
|
|
/**
|
|
* Service-specific initialization (called by BaseService)
|
|
*/
|
|
protected async onInitialize(): Promise<void> {
|
|
this.debug('Chat service onInitialize called')
|
|
|
|
// Check both injected auth service AND global auth composable
|
|
const { auth } = await import('@/composables/useAuth')
|
|
const hasAuthService = this.authService?.user?.value?.pubkey
|
|
const hasGlobalAuth = auth.currentUser.value?.pubkey
|
|
|
|
this.debug('Auth detection:', {
|
|
hasAuthService: !!hasAuthService,
|
|
hasGlobalAuth: !!hasGlobalAuth,
|
|
authServicePubkey: hasAuthService ? hasAuthService.substring(0, 10) + '...' : null,
|
|
globalAuthPubkey: hasGlobalAuth ? hasGlobalAuth.substring(0, 10) + '...' : null
|
|
})
|
|
|
|
if (!hasAuthService && !hasGlobalAuth) {
|
|
this.debug('User not authenticated yet, deferring full initialization with periodic check')
|
|
|
|
// Listen for auth events to complete initialization when user logs in
|
|
const unsubscribe = eventBus.on('auth:login', async () => {
|
|
this.debug('Auth login detected, completing chat initialization...')
|
|
unsubscribe()
|
|
|
|
if (this.authCheckInterval) {
|
|
clearInterval(this.authCheckInterval)
|
|
this.authCheckInterval = undefined
|
|
}
|
|
|
|
// Re-inject dependencies and complete initialization
|
|
await this.waitForDependencies()
|
|
await this.completeInitialization()
|
|
})
|
|
|
|
// Also check periodically in case we missed the auth event
|
|
this.authCheckInterval = setInterval(async () => {
|
|
const { auth } = await import('@/composables/useAuth')
|
|
const hasAuthService = this.authService?.user?.value?.pubkey
|
|
const hasGlobalAuth = auth.currentUser.value?.pubkey
|
|
|
|
if (hasAuthService || hasGlobalAuth) {
|
|
this.debug('Auth detected via periodic check, completing initialization')
|
|
|
|
if (this.authCheckInterval) {
|
|
clearInterval(this.authCheckInterval)
|
|
this.authCheckInterval = undefined
|
|
}
|
|
|
|
unsubscribe()
|
|
await this.waitForDependencies()
|
|
await this.completeInitialization()
|
|
}
|
|
}, 2000) // Check every 2 seconds
|
|
|
|
return
|
|
}
|
|
|
|
await this.completeInitialization()
|
|
}
|
|
|
|
/**
|
|
* Complete the initialization once all dependencies are available
|
|
*/
|
|
private async completeInitialization(): Promise<void> {
|
|
if (this.isFullyInitialized) {
|
|
this.debug('Chat service already fully initialized, skipping')
|
|
return
|
|
}
|
|
|
|
this.debug('Completing chat service initialization...')
|
|
|
|
// Load peers from storage first
|
|
this.loadPeersFromStorage()
|
|
|
|
// Load peers from API
|
|
await this.loadPeersFromAPI().catch(error => {
|
|
console.warn('Failed to load peers from API:', error)
|
|
})
|
|
|
|
// Initialize message handling (subscription + history loading)
|
|
await this.initializeMessageHandling()
|
|
|
|
// Register with visibility service
|
|
this.registerWithVisibilityService()
|
|
|
|
this.isFullyInitialized = true
|
|
this.debug('Chat service fully initialized and ready!')
|
|
}
|
|
|
|
private isFullyInitialized = false
|
|
|
|
// Initialize message handling (subscription + history loading)
|
|
async initializeMessageHandling(): Promise<void> {
|
|
// Set up real-time subscription
|
|
await this.setupMessageSubscription()
|
|
|
|
// Load message history for known peers
|
|
await this.loadMessageHistory()
|
|
}
|
|
|
|
// Computed properties
|
|
get allPeers() {
|
|
return computed(() => Array.from(this.peers.value.values()))
|
|
}
|
|
|
|
get totalUnreadCount() {
|
|
return computed(() => {
|
|
return Array.from(this.peers.value.values())
|
|
.reduce((total, peer) => total + peer.unreadCount, 0)
|
|
})
|
|
}
|
|
|
|
get isReady() {
|
|
return this.isInitialized
|
|
}
|
|
|
|
// Get messages for a specific peer
|
|
getMessages(peerPubkey: string): ChatMessage[] {
|
|
return this.messages.value.get(peerPubkey) || []
|
|
}
|
|
|
|
// Get peer by pubkey
|
|
getPeer(pubkey: string): ChatPeer | undefined {
|
|
return this.peers.value.get(pubkey)
|
|
}
|
|
|
|
// Add or update a peer
|
|
addPeer(pubkey: string, name?: string): ChatPeer {
|
|
let peer = this.peers.value.get(pubkey)
|
|
|
|
if (!peer) {
|
|
peer = {
|
|
pubkey,
|
|
name: name || `User ${pubkey.slice(0, 8)}`,
|
|
unreadCount: 0,
|
|
lastSeen: Date.now()
|
|
}
|
|
|
|
this.peers.value.set(pubkey, peer)
|
|
this.savePeersToStorage()
|
|
|
|
eventBus.emit('chat:peer-added', { peer }, 'chat-service')
|
|
} else if (name && name !== peer.name) {
|
|
peer.name = name
|
|
this.savePeersToStorage()
|
|
}
|
|
|
|
return peer
|
|
}
|
|
|
|
// Add a message
|
|
addMessage(peerPubkey: string, message: ChatMessage): void {
|
|
if (!this.messages.value.has(peerPubkey)) {
|
|
this.messages.value.set(peerPubkey, [])
|
|
}
|
|
|
|
const peerMessages = this.messages.value.get(peerPubkey)!
|
|
|
|
// Avoid duplicates
|
|
if (!peerMessages.some(m => m.id === message.id)) {
|
|
peerMessages.push(message)
|
|
|
|
// Sort by timestamp
|
|
peerMessages.sort((a, b) => a.created_at - b.created_at)
|
|
|
|
// Limit message count
|
|
if (peerMessages.length > this.config.maxMessages) {
|
|
peerMessages.splice(0, peerMessages.length - this.config.maxMessages)
|
|
}
|
|
|
|
// Update peer info
|
|
const peer = this.addPeer(peerPubkey)
|
|
peer.lastMessage = message
|
|
peer.lastSeen = Date.now()
|
|
|
|
// Update unread count if message is not sent by us
|
|
if (!message.sent) {
|
|
this.updateUnreadCount(peerPubkey, message)
|
|
}
|
|
|
|
// Emit events
|
|
const eventType = message.sent ? 'chat:message-sent' : 'chat:message-received'
|
|
eventBus.emit(eventType, { message, peerPubkey }, 'chat-service')
|
|
}
|
|
}
|
|
|
|
// Mark messages as read for a peer
|
|
markAsRead(peerPubkey: string): void {
|
|
const peer = this.peers.value.get(peerPubkey)
|
|
if (peer && peer.unreadCount > 0) {
|
|
peer.unreadCount = 0
|
|
|
|
// Save unread state
|
|
const unreadData: UnreadMessageData = {
|
|
lastReadTimestamp: Date.now(),
|
|
unreadCount: 0,
|
|
processedMessageIds: new Set()
|
|
}
|
|
this.saveUnreadData(peerPubkey, unreadData)
|
|
|
|
eventBus.emit('chat:unread-count-changed', {
|
|
peerPubkey,
|
|
count: 0,
|
|
totalUnread: this.totalUnreadCount.value
|
|
}, 'chat-service')
|
|
}
|
|
}
|
|
|
|
// Refresh peers from API
|
|
async refreshPeers(): Promise<void> {
|
|
// Check if we should trigger full initialization
|
|
const { auth } = await import('@/composables/useAuth')
|
|
const hasAuth = this.authService?.user?.value?.pubkey || auth.currentUser.value?.pubkey
|
|
|
|
if (!this.isFullyInitialized && hasAuth) {
|
|
console.log('💬 Refresh peers triggered full initialization')
|
|
await this.completeInitialization()
|
|
}
|
|
|
|
return this.loadPeersFromAPI()
|
|
}
|
|
|
|
// Check if services are available for messaging
|
|
private async checkServicesAvailable(): Promise<{ relayHub: any; authService: any; userPubkey: string; userPrivkey: string } | null> {
|
|
// Check both injected auth service AND global auth composable
|
|
const { auth } = await import('@/composables/useAuth')
|
|
const hasAuthService = this.authService?.user?.value?.prvkey
|
|
const hasGlobalAuth = auth.currentUser.value?.prvkey
|
|
|
|
const userPubkey = hasAuthService ? this.authService.user.value.pubkey : auth.currentUser.value?.pubkey
|
|
const userPrivkey = hasAuthService ? this.authService.user.value.prvkey : auth.currentUser.value?.prvkey
|
|
|
|
if (!this.relayHub || (!hasAuthService && !hasGlobalAuth)) {
|
|
return null
|
|
}
|
|
|
|
if (!this.relayHub.isConnected) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
relayHub: this.relayHub,
|
|
authService: this.authService || auth,
|
|
userPubkey: userPubkey!,
|
|
userPrivkey: userPrivkey!
|
|
}
|
|
}
|
|
|
|
// Send a message
|
|
async sendMessage(peerPubkey: string, content: string): Promise<void> {
|
|
try {
|
|
const services = await this.checkServicesAvailable()
|
|
|
|
if (!services) {
|
|
throw new Error('Chat services not ready. Please wait for connection to establish.')
|
|
}
|
|
|
|
const { relayHub, userPrivkey, userPubkey } = services
|
|
|
|
// Encrypt the message using NIP-04
|
|
const encryptedContent = await nip04.encrypt(userPrivkey, peerPubkey, content)
|
|
|
|
// Create Nostr event for the encrypted message (kind 4 = encrypted direct message)
|
|
const eventTemplate: EventTemplate = {
|
|
kind: 4,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [['p', peerPubkey]],
|
|
content: encryptedContent
|
|
}
|
|
|
|
// Finalize the event with signature
|
|
const signedEvent = finalizeEvent(eventTemplate, userPrivkey)
|
|
|
|
// Create local message for immediate display
|
|
const message: ChatMessage = {
|
|
id: signedEvent.id,
|
|
content,
|
|
created_at: signedEvent.created_at,
|
|
sent: true,
|
|
pubkey: userPubkey
|
|
}
|
|
|
|
// Add to local messages immediately
|
|
this.addMessage(peerPubkey, message)
|
|
|
|
// Publish to Nostr relays
|
|
const result = await relayHub.publishEvent(signedEvent)
|
|
console.log('Message published to relays:', { success: result.success, total: result.total })
|
|
|
|
} catch (error) {
|
|
console.error('Failed to send message:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// Private methods
|
|
private updateUnreadCount(peerPubkey: string, message: ChatMessage): void {
|
|
const unreadData = this.getUnreadData(peerPubkey)
|
|
|
|
if (!unreadData.processedMessageIds.has(message.id)) {
|
|
unreadData.processedMessageIds.add(message.id)
|
|
unreadData.unreadCount++
|
|
|
|
const peer = this.peers.value.get(peerPubkey)
|
|
if (peer) {
|
|
peer.unreadCount = unreadData.unreadCount
|
|
this.savePeersToStorage()
|
|
}
|
|
|
|
this.saveUnreadData(peerPubkey, unreadData)
|
|
|
|
eventBus.emit('chat:unread-count-changed', {
|
|
peerPubkey,
|
|
count: unreadData.unreadCount,
|
|
totalUnread: this.totalUnreadCount.value
|
|
}, 'chat-service')
|
|
}
|
|
}
|
|
|
|
private getUnreadData(peerPubkey: string): UnreadMessageData {
|
|
const data = this.storageService.getUserData(`chat-unread-messages-${peerPubkey}`, {
|
|
lastReadTimestamp: 0,
|
|
unreadCount: 0,
|
|
processedMessageIds: []
|
|
})
|
|
|
|
return {
|
|
...data,
|
|
processedMessageIds: new Set(data.processedMessageIds || [])
|
|
}
|
|
}
|
|
|
|
private saveUnreadData(peerPubkey: string, data: UnreadMessageData): void {
|
|
const serializable = {
|
|
...data,
|
|
processedMessageIds: Array.from(data.processedMessageIds)
|
|
}
|
|
this.storageService.setUserData(`chat-unread-messages-${peerPubkey}`, serializable)
|
|
}
|
|
|
|
// Load peers from API
|
|
async loadPeersFromAPI(): Promise<void> {
|
|
try {
|
|
const authToken = getAuthToken()
|
|
if (!authToken) {
|
|
console.warn('💬 No authentication token found for loading peers from API')
|
|
throw new Error('No authentication token found')
|
|
}
|
|
|
|
const API_BASE_URL = config.api.baseUrl || 'http://localhost:5006'
|
|
console.log('💬 Loading peers from API:', `${API_BASE_URL}/api/v1/auth/nostr/pubkeys`)
|
|
|
|
const response = await fetch(`${API_BASE_URL}/api/v1/auth/nostr/pubkeys`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${authToken}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text()
|
|
console.error('💬 API response error:', response.status, errorText)
|
|
throw new Error(`Failed to load peers: ${response.status} - ${errorText}`)
|
|
}
|
|
|
|
const data = await response.json()
|
|
console.log('💬 API returned', data?.length || 0, 'peers')
|
|
|
|
if (!Array.isArray(data)) {
|
|
console.warn('💬 Invalid API response format - expected array, got:', typeof data)
|
|
return
|
|
}
|
|
|
|
// Don't clear existing peers - merge instead
|
|
data.forEach((peer: any) => {
|
|
if (!peer.pubkey) {
|
|
console.warn('💬 Skipping peer without pubkey:', peer)
|
|
return
|
|
}
|
|
|
|
const chatPeer: ChatPeer = {
|
|
pubkey: peer.pubkey,
|
|
name: peer.username || `User ${peer.pubkey.slice(0, 8)}`,
|
|
unreadCount: 0,
|
|
lastSeen: Date.now()
|
|
}
|
|
this.peers.value.set(peer.pubkey, chatPeer)
|
|
})
|
|
|
|
// Save to storage
|
|
this.savePeersToStorage()
|
|
|
|
console.log(`✅ Loaded ${data.length} peers from API, total peers now: ${this.peers.value.size}`)
|
|
|
|
} catch (error) {
|
|
console.error('❌ Failed to load peers from API:', error)
|
|
// Don't re-throw - peers from storage are still available
|
|
}
|
|
}
|
|
|
|
private loadPeersFromStorage(): void {
|
|
// Skip loading peers in constructor as StorageService may not be available yet
|
|
// This will be called later during initialization when dependencies are ready
|
|
if (!this.isInitialized.value || !this.storageService) {
|
|
this.debug('Skipping peer loading from storage - not initialized or storage unavailable')
|
|
return
|
|
}
|
|
|
|
try {
|
|
const peersArray = this.storageService.getUserData('chat-peers', []) as ChatPeer[]
|
|
console.log('💬 Loading', peersArray.length, 'peers from storage')
|
|
peersArray.forEach(peer => {
|
|
this.peers.value.set(peer.pubkey, peer)
|
|
})
|
|
} catch (error) {
|
|
console.warn('💬 Failed to load peers from storage:', error)
|
|
}
|
|
}
|
|
|
|
private savePeersToStorage(): void {
|
|
const peersArray = Array.from(this.peers.value.values())
|
|
this.storageService.setUserData('chat-peers', peersArray)
|
|
}
|
|
|
|
// Load message history for known peers
|
|
private async loadMessageHistory(): Promise<void> {
|
|
try {
|
|
// Check both injected auth service AND global auth composable
|
|
const { auth } = await import('@/composables/useAuth')
|
|
const hasAuthService = this.authService?.user?.value?.pubkey
|
|
const hasGlobalAuth = auth.currentUser.value?.pubkey
|
|
|
|
const userPubkey = hasAuthService ? this.authService.user.value.pubkey : auth.currentUser.value?.pubkey
|
|
const userPrivkey = hasAuthService ? this.authService.user.value.prvkey : auth.currentUser.value?.prvkey
|
|
|
|
if (!this.relayHub || (!hasAuthService && !hasGlobalAuth)) {
|
|
console.warn('Cannot load message history: missing services')
|
|
return
|
|
}
|
|
|
|
if (!userPubkey || !userPrivkey) {
|
|
console.warn('Cannot load message history: missing user keys')
|
|
return
|
|
}
|
|
const peerPubkeys = Array.from(this.peers.value.keys())
|
|
|
|
if (peerPubkeys.length === 0) {
|
|
console.log('No peers to load message history for')
|
|
return
|
|
}
|
|
|
|
console.log('Loading message history for', peerPubkeys.length, 'peers')
|
|
|
|
// Query historical messages (kind 4) to/from known peers
|
|
// We need separate queries for sent vs received messages due to different tagging
|
|
const receivedEvents = await this.relayHub.queryEvents([
|
|
{
|
|
kinds: [4],
|
|
authors: peerPubkeys, // Messages from peers
|
|
'#p': [userPubkey], // Messages tagged to us
|
|
limit: 100
|
|
}
|
|
])
|
|
|
|
const sentEvents = await this.relayHub.queryEvents([
|
|
{
|
|
kinds: [4],
|
|
authors: [userPubkey], // Messages from us
|
|
'#p': peerPubkeys, // Messages tagged to peers
|
|
limit: 100
|
|
}
|
|
])
|
|
|
|
const events = [...receivedEvents, ...sentEvents]
|
|
.sort((a, b) => a.created_at - b.created_at) // Sort by timestamp
|
|
|
|
console.log('Found', events.length, 'historical messages:', receivedEvents.length, 'received,', sentEvents.length, 'sent')
|
|
|
|
// Process historical messages
|
|
for (const event of events) {
|
|
try {
|
|
const isFromUs = event.pubkey === userPubkey
|
|
const peerPubkey = isFromUs
|
|
? event.tags.find((tag: string[]) => tag[0] === 'p')?.[1] // Get recipient from tag
|
|
: event.pubkey // Sender is the peer
|
|
|
|
if (!peerPubkey || peerPubkey === userPubkey) continue
|
|
|
|
// Decrypt the message
|
|
const decryptedContent = await nip04.decrypt(userPrivkey, peerPubkey, event.content)
|
|
|
|
// Create a chat message
|
|
const message: ChatMessage = {
|
|
id: event.id,
|
|
content: decryptedContent,
|
|
created_at: event.created_at,
|
|
sent: isFromUs,
|
|
pubkey: event.pubkey
|
|
}
|
|
|
|
// Add the message (will avoid duplicates)
|
|
this.addMessage(peerPubkey, message)
|
|
|
|
} catch (error) {
|
|
console.error('Failed to decrypt historical message:', error)
|
|
}
|
|
}
|
|
|
|
console.log('Message history loaded successfully')
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load message history:', error)
|
|
}
|
|
}
|
|
|
|
// Setup subscription for incoming messages
|
|
private async setupMessageSubscription(): Promise<void> {
|
|
try {
|
|
// Check both injected auth service AND global auth composable
|
|
const { auth } = await import('@/composables/useAuth')
|
|
const hasAuthService = this.authService?.user?.value?.pubkey
|
|
const hasGlobalAuth = auth.currentUser.value?.pubkey
|
|
const userPubkey = hasAuthService ? this.authService.user.value.pubkey : auth.currentUser.value?.pubkey
|
|
|
|
this.debug('Setup message subscription auth check:', {
|
|
hasAuthService: !!hasAuthService,
|
|
hasGlobalAuth: !!hasGlobalAuth,
|
|
hasRelayHub: !!this.relayHub,
|
|
relayHubConnected: this.relayHub?.isConnected,
|
|
userPubkey: userPubkey ? userPubkey.substring(0, 10) + '...' : null
|
|
})
|
|
|
|
if (!this.relayHub || (!hasAuthService && !hasGlobalAuth)) {
|
|
console.warn('💬 Cannot setup message subscription: missing services')
|
|
// Retry after 2 seconds
|
|
setTimeout(() => this.setupMessageSubscription(), 2000)
|
|
return
|
|
}
|
|
|
|
if (!this.relayHub.isConnected) {
|
|
console.warn('💬 RelayHub not connected, waiting for connection...')
|
|
// Listen for connection event
|
|
this.relayHub.on('connected', () => {
|
|
console.log('💬 RelayHub connected, setting up message subscription...')
|
|
this.setupMessageSubscription()
|
|
})
|
|
// Also retry after timeout in case event is missed
|
|
setTimeout(() => this.setupMessageSubscription(), 5000)
|
|
return
|
|
}
|
|
|
|
if (!userPubkey) {
|
|
console.warn('💬 No user pubkey available for subscription')
|
|
setTimeout(() => this.setupMessageSubscription(), 2000)
|
|
return
|
|
}
|
|
|
|
// Subscribe to encrypted direct messages (kind 4) addressed to this user
|
|
this.subscriptionUnsubscriber = this.relayHub.subscribe({
|
|
id: 'chat-messages',
|
|
filters: [
|
|
{
|
|
kinds: [4], // Encrypted direct messages
|
|
'#p': [userPubkey] // Messages tagged with our pubkey
|
|
}
|
|
],
|
|
onEvent: async (event: Event) => {
|
|
// Skip our own messages
|
|
if (event.pubkey === userPubkey) {
|
|
return
|
|
}
|
|
|
|
await this.processIncomingMessage(event)
|
|
},
|
|
onEose: () => {
|
|
console.log('💬 Chat message subscription EOSE received')
|
|
}
|
|
})
|
|
|
|
console.log('💬 Chat message subscription set up successfully for pubkey:', userPubkey.substring(0, 10) + '...')
|
|
|
|
} catch (error) {
|
|
console.error('💬 Failed to setup message subscription:', error)
|
|
// Retry after delay
|
|
setTimeout(() => this.setupMessageSubscription(), 3000)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
try {
|
|
// Check both injected auth service AND global auth composable
|
|
const { auth } = await import('@/composables/useAuth')
|
|
const hasAuthService = this.authService?.user?.value?.pubkey
|
|
const hasGlobalAuth = auth.currentUser.value?.pubkey
|
|
|
|
const userPubkey = hasAuthService ? this.authService.user.value.pubkey : auth.currentUser.value?.pubkey
|
|
const userPrivkey = hasAuthService ? this.authService.user.value.prvkey : auth.currentUser.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)
|
|
console.log('💬 Market message forwarded to market handler and will also be added to chat')
|
|
} 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
|
|
}
|
|
|
|
// Process as chat message regardless (market messages should also appear in chat)
|
|
{
|
|
// Format the content for display based on whether it's a market message
|
|
let displayContent = decryptedContent
|
|
|
|
if (isMarketMessage) {
|
|
try {
|
|
const parsedContent = JSON.parse(decryptedContent)
|
|
if (parsedContent.type === 1) {
|
|
// Payment request
|
|
displayContent = `💰 Payment Request for Order ${parsedContent.id}\n${parsedContent.message || 'Please pay to proceed with your order'}`
|
|
} else if (parsedContent.type === 2) {
|
|
// Order status update
|
|
const status = []
|
|
if (parsedContent.paid === true) status.push('✅ Paid')
|
|
else if (parsedContent.paid === false) status.push('⏳ Payment Pending')
|
|
if (parsedContent.shipped === true) status.push('📦 Shipped')
|
|
else if (parsedContent.shipped === false) status.push('🔄 Processing')
|
|
|
|
displayContent = `📋 Order Update: ${parsedContent.id}\n${status.join(' | ')}\n${parsedContent.message || ''}`
|
|
}
|
|
} catch (e) {
|
|
// Fallback to raw content if parsing fails
|
|
}
|
|
}
|
|
|
|
// Create a chat message
|
|
const message: ChatMessage = {
|
|
id: event.id,
|
|
content: displayContent,
|
|
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<void> {
|
|
// Check both injected auth service AND global auth composable
|
|
const { auth } = await import('@/composables/useAuth')
|
|
const hasAuthService = this.authService?.user?.value?.pubkey
|
|
const hasGlobalAuth = auth.currentUser.value?.pubkey
|
|
|
|
const userPubkey = hasAuthService ? this.authService.user.value.pubkey : auth.currentUser.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<void> {
|
|
// Clear auth check interval
|
|
if (this.authCheckInterval) {
|
|
clearInterval(this.authCheckInterval)
|
|
this.authCheckInterval = undefined
|
|
}
|
|
|
|
// Unregister from visibility service
|
|
if (this.visibilityUnsubscribe) {
|
|
this.visibilityUnsubscribe()
|
|
this.visibilityUnsubscribe = undefined
|
|
}
|
|
|
|
// Unsubscribe from message subscription
|
|
if (this.subscriptionUnsubscriber) {
|
|
this.subscriptionUnsubscriber()
|
|
this.subscriptionUnsubscriber = undefined
|
|
}
|
|
|
|
this.messages.value.clear()
|
|
this.peers.value.clear()
|
|
this.isFullyInitialized = false
|
|
|
|
this.debug('Chat service disposed')
|
|
}
|
|
|
|
/**
|
|
* Legacy destroy method for backward compatibility
|
|
*/
|
|
destroy(): void {
|
|
this.dispose()
|
|
}
|
|
} |