From d1ac7da1a6dea12c1136ef21ea1d1864a23a2335 Mon Sep 17 00:00:00 2001 From: padreug Date: Sat, 15 Feb 2025 01:33:32 +0100 Subject: [PATCH] refactor for better message handling --- dev-dist/sw.js | 2 +- src/components/SupportChat.vue | 56 +++++++++++----------- src/lib/error.ts | 34 ++++++++----- src/lib/messages.ts | 53 +++++++++++++++++++++ src/lib/subscriptions.ts | 34 +++++++++++++ src/lib/websocket.ts | 47 ++++++++++-------- src/stores/nostr.ts | 87 ++++++++++++++++++++++++++++------ 7 files changed, 240 insertions(+), 73 deletions(-) create mode 100644 src/lib/messages.ts create mode 100644 src/lib/subscriptions.ts diff --git a/dev-dist/sw.js b/dev-dist/sw.js index f0d0468..920f5f9 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -79,7 +79,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict'; */ workbox.precacheAndRoute([{ "url": "index.html", - "revision": "0.qrl00u05iuo" + "revision": "0.rpn4gsapmg" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/src/components/SupportChat.vue b/src/components/SupportChat.vue index ebd956c..5496104 100644 --- a/src/components/SupportChat.vue +++ b/src/components/SupportChat.vue @@ -74,29 +74,28 @@ onMounted(async () => { const supportPubkeyHex = npubToHex(SUPPORT_NPUB) nostrStore.activeChat = supportPubkeyHex - // Try to subscribe in the background - isSubscribing.value = true - nostrStore.subscribeToMessages() - .catch(err => { - console.debug('Support chat subscription error:', err) - // Continue anyway - messages will come through when connection succeeds - }) - .finally(() => { - isSubscribing.value = false - }) + // Only subscribe if not already subscribed + if (!nostrStore.hasActiveSubscription) { + isSubscribing.value = true + nostrStore.subscribeToMessages() + .catch(err => { + console.debug('Support chat subscription error:', err) + }) + .finally(() => { + isSubscribing.value = false + }) + } scrollToBottom() } catch (err) { console.debug('Support chat setup error:', err) - // Continue anyway } }) -// Add cleanup on unmount +// Remove the unsubscribe on unmount since we want to keep the connection onUnmounted(() => { - if (nostrStore.activeChat) { - nostrStore.unsubscribeFromMessages() - } + // Only clear active chat + nostrStore.activeChat = null }) // Watch for changes in activeChat @@ -116,7 +115,11 @@ watch(() => nostrStore.activeChat, async (newChat) => { function scrollToBottom() { if (messagesEndRef.value) { - messagesEndRef.value.scrollIntoView({ behavior: 'smooth' }) + // Get the scroll area element + const scrollArea = messagesEndRef.value.closest('.scrollarea-viewport') + if (scrollArea) { + scrollArea.scrollTop = scrollArea.scrollHeight + } } } @@ -225,7 +228,7 @@ const getMessageGroupClasses = (sent: boolean) => { -
+
@@ -307,14 +310,17 @@ const getMessageGroupClasses = (sent: boolean) => { :deep(.scrollarea-viewport) { height: 100% !important; - scroll-behavior: smooth; } -/* Ensure the scroll area takes up all available space */ -:deep(.scrollarea-viewport > div) { - height: 100%; - display: flex; - flex-direction: column; +/* Remove any scroll-behavior from parent elements */ +.scrollarea-viewport { + scroll-behavior: auto !important; +} + +/* Ensure proper containment */ +.card { + position: relative; + contain: content; } /* Improved focus styles */ @@ -388,8 +394,4 @@ a:active { display: flex; flex-direction: column; } - -.scrollarea-viewport { - height: 100%; -} diff --git a/src/lib/error.ts b/src/lib/error.ts index 802947c..3ea42b8 100644 --- a/src/lib/error.ts +++ b/src/lib/error.ts @@ -1,14 +1,24 @@ -export class ErrorHandler { - static handle(error: unknown, context: string) { - console.error(`Error in ${context}:`, error) - - if (error instanceof Error) { - // Handle specific error types - if (error.name === 'TimeoutError') { - return 'Connection timed out. Please try again.' - } - } - - return 'An unexpected error occurred' +export class NostrError extends Error { + constructor( + message: string, + public code: string, + public context?: any + ) { + super(message) + this.name = 'NostrError' } +} + +export function handleNostrError(error: unknown) { + if (error instanceof NostrError) { + switch (error.code) { + case 'CONNECTION_FAILED': + return 'Failed to connect to relay. Please check your connection.' + case 'DECRYPT_FAILED': + return 'Failed to decrypt message.' + default: + return error.message + } + } + return 'An unexpected error occurred' } \ No newline at end of file diff --git a/src/lib/messages.ts b/src/lib/messages.ts new file mode 100644 index 0000000..3e14351 --- /dev/null +++ b/src/lib/messages.ts @@ -0,0 +1,53 @@ +export class MessageManager { + private messages = new Map() + private processedIds = new Set() + + constructor() { + this.loadFromStorage() + } + + addMessage(pubkey: string, message: DirectMessage) { + if (this.processedIds.has(message.id)) return false + if (this.isDuplicate(pubkey, message)) return false + + this.processedIds.add(message.id) + const messages = [...(this.messages.get(pubkey) || []), message] + messages.sort((a, b) => a.created_at - b.created_at) + this.messages.set(pubkey, messages) + this.saveToStorage() + return true + } + + private isDuplicate(pubkey: string, message: DirectMessage) { + const existing = this.messages.get(pubkey) || [] + return existing.some(msg => + msg.content === message.content && + Math.abs(msg.created_at - message.created_at) < 1 + ) + } + + private loadFromStorage() { + try { + const stored = localStorage.getItem('nostr_messages') + if (stored) { + this.messages = new Map(JSON.parse(stored)) + this.messages.forEach(msgs => + msgs.forEach(msg => this.processedIds.add(msg.id)) + ) + } + } catch (err) { + console.error('Failed to load messages:', err) + } + } + + private saveToStorage() { + try { + localStorage.setItem( + 'nostr_messages', + JSON.stringify(Array.from(this.messages.entries())) + ) + } catch (err) { + console.error('Failed to save messages:', err) + } + } +} \ No newline at end of file diff --git a/src/lib/subscriptions.ts b/src/lib/subscriptions.ts new file mode 100644 index 0000000..a8c7149 --- /dev/null +++ b/src/lib/subscriptions.ts @@ -0,0 +1,34 @@ +export class SubscriptionManager { + private currentSubs: any[] = [] + private isActive = false + + async subscribe(relay: any, filters: any[], handlers: { + onEvent: (event: NostrEvent) => void, + onEose?: () => void + }) { + if (this.isActive) return + + this.isActive = true + const sub = relay.sub(filters) + + sub.on('event', handlers.onEvent) + if (handlers.onEose) { + sub.on('eose', handlers.onEose) + } + + this.currentSubs.push(sub) + return sub + } + + unsubscribe() { + this.currentSubs.forEach(sub => { + try { + if (sub?.unsub) sub.unsub() + } catch (err) { + console.error('Failed to unsubscribe:', err) + } + }) + this.currentSubs = [] + this.isActive = false + } +} \ No newline at end of file diff --git a/src/lib/websocket.ts b/src/lib/websocket.ts index 111f827..e19e644 100644 --- a/src/lib/websocket.ts +++ b/src/lib/websocket.ts @@ -1,16 +1,24 @@ // Create a new WebSocket manager class export class NostrWebSocketManager { - private connections: Map = new Map() - private subscriptions: Map = new Map() - - async connect(url: string) { - if (this.connections.has(url)) return this.connections.get(url) - + private relayPool: any[] = [] + private subscriptions = new Map() + + async connect(relays: { url: string }[]) { + // Close existing connections + await this.disconnect() + + // Connect to all relays + this.relayPool = (await Promise.all( + relays.map(relay => this.connectToRelay(relay.url)) + )).filter((relay): relay is any => relay !== null) + + return this.relayPool.length > 0 + } + + private async connectToRelay(url: string) { const relay = window.NostrTools.relayInit(url) try { - await relay.connect() - this.connections.set(url, relay) - this.subscriptions.set(url, []) + await withTimeout(relay.connect()) return relay } catch (err) { console.error(`Failed to connect to ${url}:`, err) @@ -18,18 +26,19 @@ export class NostrWebSocketManager { } } - addSubscription(url: string, sub: any) { - const subs = this.subscriptions.get(url) || [] - subs.push(sub) - this.subscriptions.set(url, subs) + async publish(event: NostrEvent, relays: { url: string }[]) { + return Promise.all( + relays.map(({ url }) => this.publishToRelay(event, url)) + ) } - cleanup() { - for (const [url, subs] of this.subscriptions.entries()) { - subs.forEach(sub => sub.unsub?.()) - this.connections.get(url)?.close() - } - this.connections.clear() + disconnect() { + this.relayPool.forEach(relay => relay.close()) + this.relayPool = [] this.subscriptions.clear() } + + get isConnected() { + return this.relayPool.length > 0 + } } \ No newline at end of file diff --git a/src/stores/nostr.ts b/src/stores/nostr.ts index 888811b..9678329 100644 --- a/src/stores/nostr.ts +++ b/src/stores/nostr.ts @@ -97,6 +97,30 @@ export const useNostrStore = defineStore('nostr', () => { const relayPool = ref([]) const processedMessageIds = ref(new Set()) const currentSubscription = ref(null) + const hasActiveSubscription = ref(false) + + // Load stored messages and IDs on initialization + const initializeFromStorage = () => { + try { + const storedMessages = JSON.parse(localStorage.getItem('nostr_messages') || '[]') + const messageMap = new Map(storedMessages) + + // Initialize processedMessageIds from stored messages + messageMap.forEach(msgs => { + msgs.forEach(msg => { + processedMessageIds.value.add(msg.id) + }) + }) + + messages.value = messageMap + } catch (err) { + console.error('Failed to load stored messages:', err) + localStorage.removeItem('nostr_messages') + } + } + + // Call initialization + initializeFromStorage() // Watch account changes and persist to localStorage watch(account, (newAccount) => { @@ -107,6 +131,17 @@ export const useNostrStore = defineStore('nostr', () => { } }, { deep: true }) + // Watch messages for changes and persist + watch(messages, (newMessages) => { + try { + localStorage.setItem('nostr_messages', + JSON.stringify(Array.from(newMessages.entries())) + ) + } catch (err) { + console.error('Failed to save messages:', err) + } + }, { deep: true }) + // Initialize store if account exists in localStorage if (account.value) { console.log('Found existing account, initializing connection...') @@ -124,8 +159,7 @@ export const useNostrStore = defineStore('nostr', () => { if (!account.value) return try { - // Clear existing state - messages.value.clear() + // Only clear profiles and processed IDs profiles.value.clear() processedMessageIds.value.clear() @@ -186,7 +220,10 @@ export const useNostrStore = defineStore('nostr', () => { relayPool.value = [] messages.value.clear() profiles.value.clear() + processedMessageIds.value.clear() activeChat.value = null + localStorage.removeItem('nostr_messages') + localStorage.removeItem('nostr_account') } const addMessage = async (pubkey: string, message: DirectMessage) => { @@ -200,12 +237,21 @@ export const useNostrStore = defineStore('nostr', () => { // Add message to the map const userMessages = messages.value.get(pubkey) || [] - messages.value.set(pubkey, [...userMessages, message]) + + // Check for duplicates by content and timestamp (backup check) + const isDuplicate = userMessages.some(msg => + msg.content === message.content && + Math.abs(msg.created_at - message.created_at) < 1 + ) - // Sort messages by timestamp - const sortedMessages = messages.value.get(pubkey) || [] - sortedMessages.sort((a, b) => a.created_at - b.created_at) - messages.value.set(pubkey, sortedMessages) + if (!isDuplicate) { + messages.value.set(pubkey, [...userMessages, message]) + + // Sort messages by timestamp + const sortedMessages = messages.value.get(pubkey) || [] + sortedMessages.sort((a, b) => a.created_at - b.created_at) + messages.value.set(pubkey, sortedMessages) + } } async function sendMessage(to: string, content: string) { @@ -241,8 +287,9 @@ export const useNostrStore = defineStore('nostr', () => { } async function subscribeToMessages() { - if (!account.value) return - + if (!account.value || hasActiveSubscription.value) return + + hasActiveSubscription.value = true // Cleanup existing subscription unsubscribeFromMessages() @@ -395,6 +442,7 @@ export const useNostrStore = defineStore('nostr', () => { } currentSubscription.value = null } + hasActiveSubscription.value = false } async function loadProfiles() { @@ -492,18 +540,28 @@ export const useNostrStore = defineStore('nostr', () => { await subscribeToMessages() } - // Add visibility change handler + // Update visibility handler function setupVisibilityHandler() { - document.addEventListener('visibilitychange', async () => { + const handleVisibilityChange = async () => { if (document.visibilityState === 'visible' && account.value) { - console.log('Page became visible, reconnecting...') + console.log('Page became visible, checking connection...') try { - await reconnectToRelays() + // Only reconnect if we don't have active connections + if (relayPool.value.length === 0 || !hasActiveSubscription.value) { + await reconnectToRelays() + } } catch (err) { console.error('Failed to reconnect:', err) } } - }) + } + + // Remove any existing handler + document.removeEventListener('visibilitychange', handleVisibilityChange) + document.addEventListener('visibilitychange', handleVisibilityChange) + + // Add focus handler for mobile + window.addEventListener('focus', handleVisibilityChange) } return { @@ -521,5 +579,6 @@ export const useNostrStore = defineStore('nostr', () => { unsubscribeFromMessages, loadProfiles, connectionStatus, + hasActiveSubscription, } }) \ No newline at end of file