Refactor authentication architecture to eliminate dual auth complexity
This major refactor consolidates the authentication system to use a single source of truth, eliminating timing issues and architectural complexity that was causing chat and payment functionality problems. Key Changes: • Remove old global useAuth composable and replace with useAuthService wrapper • Update all 25+ files to use consistent auth pattern via dependency injection • Eliminate dual auth detection workarounds from services (ChatService, PaymentService, etc.) • Fix TypeScript errors and add proper Uint8Array conversion for Nostr private keys • Consolidate auth state management to AuthService as single source of truth Benefits: • Resolves chat peer loading and message subscription timing issues • Fixes wallet detection problems for Lightning payments • Eliminates race conditions between global and injected auth • Maintains API compatibility while improving architecture • Reduces code complexity and improves maintainability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5633aa154b
commit
4feb5459cc
27 changed files with 210 additions and 518 deletions
|
|
@ -5,8 +5,6 @@ import { nip04, finalizeEvent, type Event, type EventTemplate } from 'nostr-tool
|
|||
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 = {
|
||||
|
|
@ -14,7 +12,6 @@ export class ChatService extends BaseService {
|
|||
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())
|
||||
|
|
@ -24,80 +21,59 @@ export class ChatService extends BaseService {
|
|||
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')
|
||||
// Removed dual auth import
|
||||
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
|
||||
authServicePubkey: hasAuthService ? hasAuthService.substring(0, 10) : 'none',
|
||||
})
|
||||
|
||||
if (!hasAuthService && !hasGlobalAuth) {
|
||||
if (!this.authService?.user?.value) {
|
||||
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) {
|
||||
// Removed dual auth import
|
||||
if (this.authService?.user?.value?.pubkey) {
|
||||
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
|
||||
*/
|
||||
|
|
@ -106,68 +82,52 @@ export class ChatService extends BaseService {
|
|||
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,
|
||||
|
|
@ -175,61 +135,48 @@ export class ChatService extends BaseService {
|
|||
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(),
|
||||
|
|
@ -237,7 +184,6 @@ export class ChatService extends BaseService {
|
|||
processedMessageIds: new Set()
|
||||
}
|
||||
this.saveUnreadData(peerPubkey, unreadData)
|
||||
|
||||
eventBus.emit('chat:unread-count-changed', {
|
||||
peerPubkey,
|
||||
count: 0,
|
||||
|
|
@ -245,61 +191,46 @@ export class ChatService extends BaseService {
|
|||
}, '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
|
||||
|
||||
// Removed dual auth import
|
||||
const hasAuth = this.authService?.user?.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)) {
|
||||
// Removed dual auth import
|
||||
const userPubkey = this.authService?.user?.value?.pubkey
|
||||
const userPrivkey = this.authService?.user?.value?.prvkey
|
||||
if (!this.relayHub || (!this.authService?.user?.value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!this.relayHub.isConnected) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
relayHub: this.relayHub,
|
||||
authService: this.authService || auth,
|
||||
authService: this.authService,
|
||||
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,
|
||||
|
|
@ -307,10 +238,9 @@ export class ChatService extends BaseService {
|
|||
tags: [['p', peerPubkey]],
|
||||
content: encryptedContent
|
||||
}
|
||||
|
||||
// Finalize the event with signature
|
||||
const signedEvent = finalizeEvent(eventTemplate, userPrivkey)
|
||||
|
||||
// Finalize the event with signature
|
||||
const privkeyBytes = this.hexToUint8Array(userPrivkey)
|
||||
const signedEvent = finalizeEvent(eventTemplate, privkeyBytes)
|
||||
// Create local message for immediate display
|
||||
const message: ChatMessage = {
|
||||
id: signedEvent.id,
|
||||
|
|
@ -319,36 +249,40 @@ export class ChatService extends BaseService {
|
|||
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
|
||||
|
||||
/**
|
||||
* Convert hex string to Uint8Array (browser-compatible)
|
||||
*/
|
||||
private hexToUint8Array(hex: string): Uint8Array {
|
||||
const bytes = new Uint8Array(hex.length / 2)
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
@ -356,20 +290,17 @@ export class ChatService extends BaseService {
|
|||
}, '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,
|
||||
|
|
@ -377,7 +308,6 @@ export class ChatService extends BaseService {
|
|||
}
|
||||
this.storageService.setUserData(`chat-unread-messages-${peerPubkey}`, serializable)
|
||||
}
|
||||
|
||||
// Load peers from API
|
||||
async loadPeersFromAPI(): Promise<void> {
|
||||
try {
|
||||
|
|
@ -386,38 +316,31 @@ export class ChatService extends BaseService {
|
|||
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)}`,
|
||||
|
|
@ -426,18 +349,14 @@ export class ChatService extends BaseService {
|
|||
}
|
||||
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
|
||||
|
|
@ -445,7 +364,6 @@ export class ChatService extends BaseService {
|
|||
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')
|
||||
|
|
@ -456,41 +374,32 @@ export class ChatService extends BaseService {
|
|||
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)) {
|
||||
// Removed dual auth import
|
||||
// const hasAuthService = this.authService?.user?.value?.pubkey
|
||||
const userPubkey = this.authService?.user?.value?.pubkey
|
||||
const userPrivkey = this.authService?.user?.value?.prvkey
|
||||
if (!this.relayHub || (!this.authService?.user?.value)) {
|
||||
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([
|
||||
|
|
@ -501,7 +410,6 @@ export class ChatService extends BaseService {
|
|||
limit: 100
|
||||
}
|
||||
])
|
||||
|
||||
const sentEvents = await this.relayHub.queryEvents([
|
||||
{
|
||||
kinds: [4],
|
||||
|
|
@ -510,12 +418,9 @@ export class ChatService extends BaseService {
|
|||
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 {
|
||||
|
|
@ -523,12 +428,9 @@ export class ChatService extends BaseService {
|
|||
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,
|
||||
|
|
@ -537,46 +439,36 @@ export class ChatService extends BaseService {
|
|||
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
|
||||
|
||||
// Removed dual auth import
|
||||
// const hasAuthService = this.authService?.user?.value?.pubkey
|
||||
const userPubkey = this.authService?.user?.value?.pubkey
|
||||
this.debug('Setup message subscription auth check:', {
|
||||
hasAuthService: !!hasAuthService,
|
||||
hasGlobalAuth: !!hasGlobalAuth,
|
||||
hasAuthService: !!this.authService?.user?.value,
|
||||
hasRelayHub: !!this.relayHub,
|
||||
relayHubConnected: this.relayHub?.isConnected,
|
||||
userPubkey: userPubkey ? userPubkey.substring(0, 10) + '...' : null
|
||||
userPubkey: userPubkey ? userPubkey.substring(0, 10) : 'none',
|
||||
})
|
||||
|
||||
if (!this.relayHub || (!hasAuthService && !hasGlobalAuth)) {
|
||||
if (!this.relayHub || (!this.authService?.user?.value)) {
|
||||
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
|
||||
|
|
@ -588,13 +480,11 @@ export class ChatService extends BaseService {
|
|||
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',
|
||||
|
|
@ -609,23 +499,19 @@ export class ChatService extends BaseService {
|
|||
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
|
||||
*/
|
||||
|
|
@ -634,42 +520,34 @@ export class ChatService extends BaseService {
|
|||
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
|
||||
*/
|
||||
|
|
@ -678,39 +556,30 @@ export class ChatService extends BaseService {
|
|||
// 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
|
||||
|
||||
// Removed dual auth import
|
||||
// const hasAuthService = this.authService?.user?.value?.pubkey
|
||||
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 {
|
||||
|
|
@ -718,7 +587,6 @@ export class ChatService extends BaseService {
|
|||
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)
|
||||
|
|
@ -730,12 +598,10 @@ export class ChatService extends BaseService {
|
|||
} 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)
|
||||
|
|
@ -749,14 +615,12 @@ export class ChatService extends BaseService {
|
|||
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,
|
||||
|
|
@ -765,37 +629,27 @@ export class ChatService extends BaseService {
|
|||
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
|
||||
// Removed dual auth import
|
||||
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
|
||||
|
|
@ -812,17 +666,14 @@ export class ChatService extends BaseService {
|
|||
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)
|
||||
*/
|
||||
|
|
@ -832,26 +683,21 @@ export class ChatService extends BaseService {
|
|||
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
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue