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