refactor for better message handling
This commit is contained in:
parent
5eb46e96c3
commit
d1ac7da1a6
7 changed files with 240 additions and 73 deletions
|
|
@ -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"), {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
isSubscribing.value = true
|
if (!nostrStore.hasActiveSubscription) {
|
||||||
nostrStore.subscribeToMessages()
|
isSubscribing.value = true
|
||||||
.catch(err => {
|
nostrStore.subscribeToMessages()
|
||||||
console.debug('Support chat subscription error:', err)
|
.catch(err => {
|
||||||
// Continue anyway - messages will come through when connection succeeds
|
console.debug('Support chat subscription error:', err)
|
||||||
})
|
})
|
||||||
.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>
|
||||||
|
|
|
||||||
|
|
@ -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
53
src/lib/messages.ts
Normal 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
34
src/lib/subscriptions.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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,12 +237,21 @@ 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) || []
|
||||||
messages.value.set(pubkey, [...userMessages, message])
|
|
||||||
|
|
||||||
// Sort messages by timestamp
|
// Check for duplicates by content and timestamp (backup check)
|
||||||
const sortedMessages = messages.value.get(pubkey) || []
|
const isDuplicate = userMessages.some(msg =>
|
||||||
sortedMessages.sort((a, b) => a.created_at - b.created_at)
|
msg.content === message.content &&
|
||||||
messages.value.set(pubkey, sortedMessages)
|
Math.abs(msg.created_at - message.created_at) < 1
|
||||||
|
)
|
||||||
|
|
||||||
|
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) {
|
async function sendMessage(to: string, content: string) {
|
||||||
|
|
@ -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 {
|
||||||
await reconnectToRelays()
|
// Only reconnect if we don't have active connections
|
||||||
|
if (relayPool.value.length === 0 || !hasActiveSubscription.value) {
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue