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([{
"url": "index.html",
"revision": "0.qrl00u05iuo"
"revision": "0.rpn4gsapmg"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View file

@ -74,29 +74,28 @@ onMounted(async () => {
const supportPubkeyHex = npubToHex(SUPPORT_NPUB)
nostrStore.activeChat = supportPubkeyHex
// Try to subscribe in the background
// Only subscribe if not already subscribed
if (!nostrStore.hasActiveSubscription) {
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
})
}
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) => {
</div>
</div>
</template>
<div ref="messagesEndRef" />
<div ref="messagesEndRef" class="h-px" />
</div>
</ScrollArea>
</CardContent>
@ -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%;
}
</style>

View file

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

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
export class NostrWebSocketManager {
private connections: Map<string, any> = new Map()
private subscriptions: Map<string, any[]> = new Map()
private relayPool: any[] = []
private subscriptions = new Map<string, any>()
async connect(url: string) {
if (this.connections.has(url)) return this.connections.get(url)
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
}
}

View file

@ -97,6 +97,30 @@ export const useNostrStore = defineStore('nostr', () => {
const relayPool = ref<any[]>([])
const processedMessageIds = ref(new Set<string>())
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, (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,6 +237,14 @@ export const useNostrStore = defineStore('nostr', () => {
// Add message to the map
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])
// Sort messages by timestamp
@ -207,6 +252,7 @@ export const useNostrStore = defineStore('nostr', () => {
sortedMessages.sort((a, b) => a.created_at - b.created_at)
messages.value.set(pubkey, sortedMessages)
}
}
async function sendMessage(to: string, content: string) {
if (!account.value) return
@ -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 {
// 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,
}
})