From 5eb46e96c3aadb3b70ba31598250f397e9ead3a5 Mon Sep 17 00:00:00 2001 From: padreug Date: Sat, 15 Feb 2025 00:26:11 +0100 Subject: [PATCH] refactor: improve nostr connection and message handling - Add WebSocket manager class for better connection handling - Split message handling into separate store - Add encryption service class - Create chat composable for reusable chat logic - Add error handling service - Add connection status indicators throughout app - Add message persistence service - Improve subscription reliability with EOSE handling - Add connection state management - Hide status text on mobile for better space usage These changes improve code organization, reliability, and user experience by: - Better separation of concerns - More robust error handling - Clearer connection status feedback - Improved message persistence - More maintainable WebSocket management - Better mobile responsiveness Breaking changes: - Message handling moved to separate store - WebSocket connections now managed through NostrWebSocketManager - Encryption now handled through NostrEncryption service --- dev-dist/sw.js | 2 +- src/components/ConnectionStatus.vue | 21 +++++++++++++++++ src/components/SupportChat.vue | 28 +++++++++-------------- src/components/layout/Navbar.vue | 3 +++ src/composables/useChat.ts | 23 +++++++++++++++++++ src/lib/encryption.ts | 9 ++++++++ src/lib/error.ts | 14 ++++++++++++ src/lib/storage.ts | 22 ++++++++++++++++++ src/lib/websocket.ts | 35 +++++++++++++++++++++++++++++ src/stores/messages.ts | 21 +++++++++++++++++ src/stores/nostr.ts | 17 +++++++------- 11 files changed, 169 insertions(+), 26 deletions(-) create mode 100644 src/components/ConnectionStatus.vue create mode 100644 src/composables/useChat.ts create mode 100644 src/lib/encryption.ts create mode 100644 src/lib/error.ts create mode 100644 src/lib/storage.ts create mode 100644 src/lib/websocket.ts create mode 100644 src/stores/messages.ts diff --git a/dev-dist/sw.js b/dev-dist/sw.js index 40d995c..f0d0468 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.36o4mscev7" + "revision": "0.qrl00u05iuo" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/src/components/ConnectionStatus.vue b/src/components/ConnectionStatus.vue new file mode 100644 index 0000000..6d76609 --- /dev/null +++ b/src/components/ConnectionStatus.vue @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/src/components/SupportChat.vue b/src/components/SupportChat.vue index 2e873d1..ebd956c 100644 --- a/src/components/SupportChat.vue +++ b/src/components/SupportChat.vue @@ -10,6 +10,7 @@ import { ScrollArea } from '@/components/ui/scroll-area' import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Send } from 'lucide-vue-next' import MessageBubble from '@/components/ui/message-bubble/MessageBubble.vue' +import ConnectionStatus from '@/components/ConnectionStatus.vue' const nostrStore = useNostrStore() const input = ref('') @@ -69,7 +70,7 @@ watch(() => nostrStore.currentMessages.length, () => { onMounted(async () => { try { if (!SUPPORT_NPUB) return - + const supportPubkeyHex = npubToHex(SUPPORT_NPUB) nostrStore.activeChat = supportPubkeyHex @@ -177,8 +178,7 @@ const getMessageGroupClasses = (sent: boolean) => { @@ -218,16 +218,10 @@ const getMessageGroupClasses = (sent: boolean) => {
- +
@@ -236,8 +230,7 @@ const getMessageGroupClasses = (sent: boolean) => { - +
{
@@ -398,4 +392,4 @@ a:active { .scrollarea-viewport { height: 100%; } - \ No newline at end of file + diff --git a/src/components/layout/Navbar.vue b/src/components/layout/Navbar.vue index d929b67..bb3a831 100644 --- a/src/components/layout/Navbar.vue +++ b/src/components/layout/Navbar.vue @@ -9,6 +9,7 @@ import { useRouter } from 'vue-router' import LogoutDialog from '@/components/ui/logout-dialog/LogoutDialog.vue' import Login from '@/components/Login.vue' import { Dialog, DialogContent } from '@/components/ui/dialog' +import ConnectionStatus from '@/components/ConnectionStatus.vue' const { t, locale } = useI18n() const { theme, setTheme } = useTheme() @@ -84,6 +85,8 @@ const openLogin = () => { {{ locale === 'en' ? 'πŸ‡ͺπŸ‡Έ ES' : 'πŸ‡ΊπŸ‡Έ EN' }} + + diff --git a/src/composables/useChat.ts b/src/composables/useChat.ts new file mode 100644 index 0000000..2284122 --- /dev/null +++ b/src/composables/useChat.ts @@ -0,0 +1,23 @@ +export function useChat(pubkey: string) { + const messageStore = useMessageStore() + const nostrStore = useNostrStore() + + const messages = computed(() => + messageStore.messages.get(pubkey) || [] + ) + + const sendMessage = async (content: string) => { + if (!content.trim()) return + await nostrStore.sendMessage(pubkey, content) + } + + const loadHistory = async () => { + await nostrStore.subscribeToMessages() + } + + return { + messages, + sendMessage, + loadHistory + } +} \ No newline at end of file diff --git a/src/lib/encryption.ts b/src/lib/encryption.ts new file mode 100644 index 0000000..275bcc3 --- /dev/null +++ b/src/lib/encryption.ts @@ -0,0 +1,9 @@ +export class NostrEncryption { + static async encrypt(privkey: string, pubkey: string, content: string) { + return await window.NostrTools.nip04.encrypt(privkey, pubkey, content) + } + + static async decrypt(privkey: string, pubkey: string, content: string) { + return await window.NostrTools.nip04.decrypt(privkey, pubkey, content) + } +} \ No newline at end of file diff --git a/src/lib/error.ts b/src/lib/error.ts new file mode 100644 index 0000000..802947c --- /dev/null +++ b/src/lib/error.ts @@ -0,0 +1,14 @@ +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' + } +} \ No newline at end of file diff --git a/src/lib/storage.ts b/src/lib/storage.ts new file mode 100644 index 0000000..5c9bf40 --- /dev/null +++ b/src/lib/storage.ts @@ -0,0 +1,22 @@ +export class MessageStorage { + static saveMessages(pubkey: string, messages: DirectMessage[]) { + try { + localStorage.setItem( + `messages_${pubkey}`, + JSON.stringify(messages) + ) + } catch (err) { + console.error('Failed to save messages:', err) + } + } + + static loadMessages(pubkey: string): DirectMessage[] { + try { + const stored = localStorage.getItem(`messages_${pubkey}`) + return stored ? JSON.parse(stored) : [] + } catch (err) { + console.error('Failed to load messages:', err) + return [] + } + } +} \ No newline at end of file diff --git a/src/lib/websocket.ts b/src/lib/websocket.ts new file mode 100644 index 0000000..111f827 --- /dev/null +++ b/src/lib/websocket.ts @@ -0,0 +1,35 @@ +// 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) + + const relay = window.NostrTools.relayInit(url) + try { + await relay.connect() + this.connections.set(url, relay) + this.subscriptions.set(url, []) + return relay + } catch (err) { + console.error(`Failed to connect to ${url}:`, err) + return null + } + } + + addSubscription(url: string, sub: any) { + const subs = this.subscriptions.get(url) || [] + subs.push(sub) + this.subscriptions.set(url, subs) + } + + cleanup() { + for (const [url, subs] of this.subscriptions.entries()) { + subs.forEach(sub => sub.unsub?.()) + this.connections.get(url)?.close() + } + this.connections.clear() + this.subscriptions.clear() + } +} \ No newline at end of file diff --git a/src/stores/messages.ts b/src/stores/messages.ts new file mode 100644 index 0000000..83e4d6d --- /dev/null +++ b/src/stores/messages.ts @@ -0,0 +1,21 @@ +// Separate message handling into its own store +export const useMessageStore = defineStore('messages', () => { + const messages = ref>(new Map()) + const processedIds = ref(new Set()) + + const addMessage = async (pubkey: string, message: DirectMessage) => { + if (processedIds.value.has(message.id)) return + + processedIds.value.add(message.id) + const userMessages = messages.value.get(pubkey) || [] + messages.value.set(pubkey, [...userMessages, message].sort((a, b) => + a.created_at - b.created_at + )) + } + + return { + messages, + processedIds, + addMessage + } +}) \ No newline at end of file diff --git a/src/stores/nostr.ts b/src/stores/nostr.ts index 64e6a90..888811b 100644 --- a/src/stores/nostr.ts +++ b/src/stores/nostr.ts @@ -48,23 +48,23 @@ async function withTimeout(promise: Promise, timeoutMs: number = 10000): P ]) } +// Add to state +const connectionStatus = ref<'connected' | 'connecting' | 'disconnected'>('disconnected') + +// Update in connect function async function connectToRelay(url: string) { + connectionStatus.value = 'connecting' console.log(`Attempting to connect to relay: ${url}`) const relay = window.NostrTools.relayInit(url) try { console.log(`Initializing connection to ${url}...`) await withTimeout(relay.connect()) console.log(`Successfully connected to ${url}`) + connectionStatus.value = 'connected' return relay } catch (err) { console.error(`Failed to connect to ${url}:`, err) - if (err instanceof Error) { - console.error('Error details:', { - message: err.message, - name: err.name, - stack: err.stack - }) - } + connectionStatus.value = 'disconnected' return null } } @@ -519,6 +519,7 @@ export const useNostrStore = defineStore('nostr', () => { sendMessage, subscribeToMessages, unsubscribeFromMessages, - loadProfiles + loadProfiles, + connectionStatus, } }) \ No newline at end of file