refactor for better message handling

This commit is contained in:
padreug 2025-02-15 01:33:32 +01:00
parent 5eb46e96c3
commit d1ac7da1a6
7 changed files with 240 additions and 73 deletions

View file

@ -79,7 +79,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
*/ */
workbox.precacheAndRoute([{ workbox.precacheAndRoute([{
"url": "index.html", "url": "index.html",
"revision": "0.qrl00u05iuo" "revision": "0.rpn4gsapmg"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View file

@ -74,29 +74,28 @@ onMounted(async () => {
const supportPubkeyHex = npubToHex(SUPPORT_NPUB) const supportPubkeyHex = npubToHex(SUPPORT_NPUB)
nostrStore.activeChat = supportPubkeyHex nostrStore.activeChat = supportPubkeyHex
// Try to subscribe in the background // Only subscribe if not already subscribed
if (!nostrStore.hasActiveSubscription) {
isSubscribing.value = true isSubscribing.value = true
nostrStore.subscribeToMessages() nostrStore.subscribeToMessages()
.catch(err => { .catch(err => {
console.debug('Support chat subscription error:', err) console.debug('Support chat subscription error:', err)
// Continue anyway - messages will come through when connection succeeds
}) })
.finally(() => { .finally(() => {
isSubscribing.value = false isSubscribing.value = false
}) })
}
scrollToBottom() scrollToBottom()
} catch (err) { } catch (err) {
console.debug('Support chat setup error:', 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(() => { onUnmounted(() => {
if (nostrStore.activeChat) { // Only clear active chat
nostrStore.unsubscribeFromMessages() nostrStore.activeChat = null
}
}) })
// Watch for changes in activeChat // Watch for changes in activeChat
@ -116,7 +115,11 @@ watch(() => nostrStore.activeChat, async (newChat) => {
function scrollToBottom() { function scrollToBottom() {
if (messagesEndRef.value) { 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) => {
</div> </div>
</div> </div>
</template> </template>
<div ref="messagesEndRef" /> <div ref="messagesEndRef" class="h-px" />
</div> </div>
</ScrollArea> </ScrollArea>
</CardContent> </CardContent>
@ -307,14 +310,17 @@ const getMessageGroupClasses = (sent: boolean) => {
:deep(.scrollarea-viewport) { :deep(.scrollarea-viewport) {
height: 100% !important; height: 100% !important;
scroll-behavior: smooth;
} }
/* Ensure the scroll area takes up all available space */ /* Remove any scroll-behavior from parent elements */
:deep(.scrollarea-viewport > div) { .scrollarea-viewport {
height: 100%; scroll-behavior: auto !important;
display: flex; }
flex-direction: column;
/* Ensure proper containment */
.card {
position: relative;
contain: content;
} }
/* Improved focus styles */ /* Improved focus styles */
@ -388,8 +394,4 @@ a:active {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.scrollarea-viewport {
height: 100%;
}
</style> </style>

View file

@ -1,14 +1,24 @@
export class ErrorHandler { export class NostrError extends Error {
static handle(error: unknown, context: string) { constructor(
console.error(`Error in ${context}:`, error) message: string,
public code: string,
if (error instanceof Error) { public context?: any
// Handle specific error types ) {
if (error.name === 'TimeoutError') { super(message)
return 'Connection timed out. Please try again.' this.name = 'NostrError'
}
}
return 'An unexpected error occurred'
} }
} }
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'
}

53
src/lib/messages.ts Normal file
View file

@ -0,0 +1,53 @@
export class MessageManager {
private messages = new Map<string, DirectMessage[]>()
private processedIds = new Set<string>()
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)
}
}
}

34
src/lib/subscriptions.ts Normal file
View file

@ -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
}
}

View file

@ -1,16 +1,24 @@
// Create a new WebSocket manager class // Create a new WebSocket manager class
export class NostrWebSocketManager { export class NostrWebSocketManager {
private connections: Map<string, any> = new Map() private relayPool: any[] = []
private subscriptions: Map<string, any[]> = new Map() private subscriptions = new Map<string, any>()
async connect(url: string) { async connect(relays: { url: string }[]) {
if (this.connections.has(url)) return this.connections.get(url) // 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) const relay = window.NostrTools.relayInit(url)
try { try {
await relay.connect() await withTimeout(relay.connect())
this.connections.set(url, relay)
this.subscriptions.set(url, [])
return relay return relay
} catch (err) { } catch (err) {
console.error(`Failed to connect to ${url}:`, err) console.error(`Failed to connect to ${url}:`, err)
@ -18,18 +26,19 @@ export class NostrWebSocketManager {
} }
} }
addSubscription(url: string, sub: any) { async publish(event: NostrEvent, relays: { url: string }[]) {
const subs = this.subscriptions.get(url) || [] return Promise.all(
subs.push(sub) relays.map(({ url }) => this.publishToRelay(event, url))
this.subscriptions.set(url, subs) )
} }
cleanup() { disconnect() {
for (const [url, subs] of this.subscriptions.entries()) { this.relayPool.forEach(relay => relay.close())
subs.forEach(sub => sub.unsub?.()) this.relayPool = []
this.connections.get(url)?.close()
}
this.connections.clear()
this.subscriptions.clear() this.subscriptions.clear()
} }
get isConnected() {
return this.relayPool.length > 0
}
} }

View file

@ -97,6 +97,30 @@ export const useNostrStore = defineStore('nostr', () => {
const relayPool = ref<any[]>([]) const relayPool = ref<any[]>([])
const processedMessageIds = ref(new Set<string>()) const processedMessageIds = ref(new Set<string>())
const currentSubscription = ref<any | null>(null) const currentSubscription = ref<any | null>(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 changes and persist to localStorage
watch(account, (newAccount) => { watch(account, (newAccount) => {
@ -107,6 +131,17 @@ export const useNostrStore = defineStore('nostr', () => {
} }
}, { deep: true }) }, { 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 // Initialize store if account exists in localStorage
if (account.value) { if (account.value) {
console.log('Found existing account, initializing connection...') console.log('Found existing account, initializing connection...')
@ -124,8 +159,7 @@ export const useNostrStore = defineStore('nostr', () => {
if (!account.value) return if (!account.value) return
try { try {
// Clear existing state // Only clear profiles and processed IDs
messages.value.clear()
profiles.value.clear() profiles.value.clear()
processedMessageIds.value.clear() processedMessageIds.value.clear()
@ -186,7 +220,10 @@ export const useNostrStore = defineStore('nostr', () => {
relayPool.value = [] relayPool.value = []
messages.value.clear() messages.value.clear()
profiles.value.clear() profiles.value.clear()
processedMessageIds.value.clear()
activeChat.value = null activeChat.value = null
localStorage.removeItem('nostr_messages')
localStorage.removeItem('nostr_account')
} }
const addMessage = async (pubkey: string, message: DirectMessage) => { const addMessage = async (pubkey: string, message: DirectMessage) => {
@ -200,6 +237,14 @@ export const useNostrStore = defineStore('nostr', () => {
// Add message to the map // Add message to the map
const userMessages = messages.value.get(pubkey) || [] const userMessages = messages.value.get(pubkey) || []
// 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
)
if (!isDuplicate) {
messages.value.set(pubkey, [...userMessages, message]) messages.value.set(pubkey, [...userMessages, message])
// Sort messages by timestamp // Sort messages by timestamp
@ -207,6 +252,7 @@ export const useNostrStore = defineStore('nostr', () => {
sortedMessages.sort((a, b) => a.created_at - b.created_at) sortedMessages.sort((a, b) => a.created_at - b.created_at)
messages.value.set(pubkey, sortedMessages) messages.value.set(pubkey, sortedMessages)
} }
}
async function sendMessage(to: string, content: string) { async function sendMessage(to: string, content: string) {
if (!account.value) return if (!account.value) return
@ -241,8 +287,9 @@ export const useNostrStore = defineStore('nostr', () => {
} }
async function subscribeToMessages() { async function subscribeToMessages() {
if (!account.value) return if (!account.value || hasActiveSubscription.value) return
hasActiveSubscription.value = true
// Cleanup existing subscription // Cleanup existing subscription
unsubscribeFromMessages() unsubscribeFromMessages()
@ -395,6 +442,7 @@ export const useNostrStore = defineStore('nostr', () => {
} }
currentSubscription.value = null currentSubscription.value = null
} }
hasActiveSubscription.value = false
} }
async function loadProfiles() { async function loadProfiles() {
@ -492,18 +540,28 @@ export const useNostrStore = defineStore('nostr', () => {
await subscribeToMessages() await subscribeToMessages()
} }
// Add visibility change handler // Update visibility handler
function setupVisibilityHandler() { function setupVisibilityHandler() {
document.addEventListener('visibilitychange', async () => { const handleVisibilityChange = async () => {
if (document.visibilityState === 'visible' && account.value) { if (document.visibilityState === 'visible' && account.value) {
console.log('Page became visible, reconnecting...') console.log('Page became visible, checking connection...')
try { try {
// Only reconnect if we don't have active connections
if (relayPool.value.length === 0 || !hasActiveSubscription.value) {
await reconnectToRelays() await reconnectToRelays()
}
} catch (err) { } catch (err) {
console.error('Failed to reconnect:', 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 { return {
@ -521,5 +579,6 @@ export const useNostrStore = defineStore('nostr', () => {
unsubscribeFromMessages, unsubscribeFromMessages,
loadProfiles, loadProfiles,
connectionStatus, connectionStatus,
hasActiveSubscription,
} }
}) })