feat: Integrate Nostr chat preloader for improved chat data handling
- Introduce a new composable, useNostrChatPreloader, to manage chat data preloading, including peer loading and subscription for notifications. - Update ChatComponent to utilize the preloader, ensuring chat data is ready before connecting. - Enhance Navbar to display unread message counts with notification badges for better user experience. - Refactor App.vue to trigger both market and chat preloading upon successful login, streamlining the user experience.
This commit is contained in:
parent
b0101915c7
commit
855a003962
4 changed files with 233 additions and 133 deletions
17
src/App.vue
17
src/App.vue
|
|
@ -8,13 +8,15 @@ import { Toaster } from '@/components/ui/sonner'
|
|||
import 'vue-sonner/style.css'
|
||||
import { auth } from '@/composables/useAuth'
|
||||
import { useMarketPreloader } from '@/composables/useMarketPreloader'
|
||||
import { useNostrChatPreloader } from '@/composables/useNostrChatPreloader'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const route = useRoute()
|
||||
const showLoginDialog = ref(false)
|
||||
|
||||
// Initialize market preloader
|
||||
// Initialize preloaders
|
||||
const marketPreloader = useMarketPreloader()
|
||||
const chatPreloader = useNostrChatPreloader()
|
||||
|
||||
// Hide navbar on login page
|
||||
const showNavbar = computed(() => {
|
||||
|
|
@ -25,8 +27,9 @@ function handleLoginSuccess() {
|
|||
showLoginDialog.value = false
|
||||
toast.success('Welcome back!')
|
||||
|
||||
// Trigger market preloading after successful login
|
||||
// Trigger preloading after successful login
|
||||
marketPreloader.preloadMarket()
|
||||
chatPreloader.preloadChat()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
|
@ -38,12 +41,18 @@ onMounted(async () => {
|
|||
}
|
||||
})
|
||||
|
||||
// Watch for authentication changes and trigger market preloading
|
||||
// Watch for authentication changes and trigger preloading
|
||||
watch(() => auth.isAuthenticated.value, (isAuthenticated) => {
|
||||
if (isAuthenticated && !marketPreloader.isPreloaded.value) {
|
||||
if (isAuthenticated) {
|
||||
if (!marketPreloader.isPreloaded.value) {
|
||||
console.log('User authenticated, triggering market preload...')
|
||||
marketPreloader.preloadMarket()
|
||||
}
|
||||
if (!chatPreloader.isPreloaded.value) {
|
||||
console.log('User authenticated, triggering chat preload...')
|
||||
chatPreloader.preloadChat()
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import ProfileDialog from '@/components/auth/ProfileDialog.vue'
|
|||
import CurrencyDisplay from '@/components/ui/CurrencyDisplay.vue'
|
||||
import { auth } from '@/composables/useAuth'
|
||||
import { useMarketPreloader } from '@/composables/useMarketPreloader'
|
||||
import { useNostrChatPreloader } from '@/composables/useNostrChatPreloader'
|
||||
|
||||
interface NavigationItem {
|
||||
name: string
|
||||
|
|
@ -26,6 +27,7 @@ const isOpen = ref(false)
|
|||
const showLoginDialog = ref(false)
|
||||
const showProfileDialog = ref(false)
|
||||
const marketPreloader = useMarketPreloader()
|
||||
const chatPreloader = useNostrChatPreloader()
|
||||
|
||||
const navigation = computed<NavigationItem[]>(() => [
|
||||
{ name: t('nav.home'), href: '/' },
|
||||
|
|
@ -43,6 +45,11 @@ const totalBalance = computed(() => {
|
|||
}, 0)
|
||||
})
|
||||
|
||||
// Compute total unread messages
|
||||
const totalUnreadMessages = computed(() => {
|
||||
return chatPreloader.getTotalUnreadCount()
|
||||
})
|
||||
|
||||
const toggleMenu = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
|
|
@ -87,13 +94,20 @@ const handleLogout = async () => {
|
|||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex gap-4 lg:gap-6 xl:gap-8">
|
||||
<router-link v-for="item in navigation" :key="item.name" :to="item.href"
|
||||
class="text-muted-foreground hover:text-foreground transition-colors flex items-center gap-2 text-sm lg:text-base xl:text-lg"
|
||||
class="text-muted-foreground hover:text-foreground transition-colors flex items-center gap-2 text-sm lg:text-base xl:text-lg relative"
|
||||
:class="{
|
||||
'text-foreground': $route.path === item.href
|
||||
}">
|
||||
<!-- Chat icon -->
|
||||
<MessageSquare v-if="item.href === '/chat'" class="h-4 w-4" />
|
||||
{{ item.name }}
|
||||
<!-- Chat icon with notification badge -->
|
||||
<div v-if="item.href === '/chat'" class="relative">
|
||||
<MessageSquare class="h-4 w-4" />
|
||||
<Badge v-if="totalUnreadMessages > 0"
|
||||
class="absolute -top-2 -right-2 h-3.5 w-4 text-xs bg-red-500 text-white border-0 p-0 flex items-center justify-center">
|
||||
{{ totalUnreadMessages > 99 ? '99+' : totalUnreadMessages }}
|
||||
</Badge>
|
||||
</div>
|
||||
<span v-else>{{ item.name }}</span>
|
||||
<span v-if="item.href === '/chat'">{{ item.name }}</span>
|
||||
<!-- Market preloading indicator -->
|
||||
<div v-if="item.href === '/market' && marketPreloader.isPreloading"
|
||||
class="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
|
||||
|
|
@ -226,12 +240,18 @@ const handleLogout = async () => {
|
|||
</div>
|
||||
|
||||
<router-link v-for="item in navigation" :key="item.name" :to="item.href"
|
||||
class="flex items-center gap-2 px-3 py-2 text-base font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||
class="flex items-center gap-2 px-3 py-2 text-base font-medium text-muted-foreground hover:text-foreground transition-colors relative"
|
||||
:class="{
|
||||
'text-foreground': $route.path === item.href
|
||||
}" @click="isOpen = false">
|
||||
<!-- Chat icon -->
|
||||
<MessageSquare v-if="item.href === '/chat'" class="h-4 w-4" />
|
||||
<!-- Chat icon with notification badge -->
|
||||
<div v-if="item.href === '/chat'" class="relative">
|
||||
<MessageSquare class="h-4 w-4" />
|
||||
<Badge v-if="totalUnreadMessages > 0"
|
||||
class="absolute -top-2 -right-2 h-3.5 w-4 text-xs bg-red-500 text-white border-0 p-0 flex items-center justify-center">
|
||||
{{ totalUnreadMessages > 99 ? '99+' : totalUnreadMessages }}
|
||||
</Badge>
|
||||
</div>
|
||||
{{ item.name }}
|
||||
<!-- Market preloading indicator -->
|
||||
<div v-if="item.href === '/market' && marketPreloader.isPreloading"
|
||||
|
|
|
|||
|
|
@ -376,9 +376,8 @@ import { Badge } from '@/components/ui/badge'
|
|||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { useNostrChat } from '@/composables/useNostrChat'
|
||||
import { useNostrChatPreloader } from '@/composables/useNostrChatPreloader'
|
||||
import { useFuzzySearch } from '@/composables/useFuzzySearch'
|
||||
import { getAuthToken } from '@/lib/config/lnbits'
|
||||
import { config } from '@/lib/config'
|
||||
|
||||
// Types
|
||||
interface Peer {
|
||||
|
|
@ -387,12 +386,15 @@ interface Peer {
|
|||
pubkey: string
|
||||
}
|
||||
|
||||
// Initialize preloader and chat
|
||||
const chatPreloader = useNostrChatPreloader()
|
||||
|
||||
// State
|
||||
const peers = ref<Peer[]>([])
|
||||
const peers = computed(() => chatPreloader.peers.value)
|
||||
const selectedPeer = ref<Peer | null>(null)
|
||||
const messageInput = ref('')
|
||||
|
||||
const isLoading = ref(false)
|
||||
const isLoading = computed(() => chatPreloader.isPreloading.value)
|
||||
const showChat = ref(false)
|
||||
const messagesScrollArea = ref<HTMLElement | null>(null)
|
||||
const messagesContainer = ref<HTMLElement | null>(null)
|
||||
|
|
@ -408,7 +410,6 @@ const {
|
|||
connect,
|
||||
disconnect,
|
||||
subscribeToPeer,
|
||||
subscribeToPeerForNotifications,
|
||||
sendMessage: sendNostrMessage,
|
||||
onMessageAdded,
|
||||
markMessagesAsRead,
|
||||
|
|
@ -499,106 +500,12 @@ const goBackToPeers = () => {
|
|||
}
|
||||
|
||||
// Methods
|
||||
const loadPeers = async () => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const authToken = getAuthToken()
|
||||
if (!authToken) {
|
||||
console.warn('No authentication token found - cannot load peers')
|
||||
return
|
||||
}
|
||||
|
||||
const API_BASE_URL = config.api.baseUrl || 'http://localhost:5006'
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/auth/nostr/pubkeys`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Peers API Response status:', response.status)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error('Peers API Error:', response.status, errorText)
|
||||
throw new Error(`Failed to load peers: ${response.status}`)
|
||||
}
|
||||
|
||||
const responseText = await response.text()
|
||||
console.log('Peers API Response text:', responseText)
|
||||
|
||||
try {
|
||||
const data = JSON.parse(responseText)
|
||||
peers.value = data.map((peer: any) => ({
|
||||
user_id: peer.user_id,
|
||||
username: peer.username,
|
||||
pubkey: peer.pubkey
|
||||
}))
|
||||
|
||||
console.log(`Loaded ${peers.value.length} peers`)
|
||||
|
||||
// Note: Subscriptions will be handled by the isConnected watcher
|
||||
|
||||
} catch (parseError) {
|
||||
console.error('JSON Parse Error for peers:', parseError)
|
||||
console.error('Response was:', responseText)
|
||||
throw new Error('Invalid JSON response from peers API')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load peers:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to all peers for notifications (without loading full message history)
|
||||
const subscribeToAllPeers = async () => {
|
||||
if (!peers.value.length) {
|
||||
console.log('No peers to subscribe to')
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for connection to be established
|
||||
if (!isConnected.value) {
|
||||
console.log('Waiting for connection to be established before subscribing to peers')
|
||||
// Wait a bit for connection to establish
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
if (!isConnected.value) {
|
||||
console.warn('Still not connected, skipping peer subscriptions')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Subscribing to ${peers.value.length} peers for notifications`)
|
||||
console.log('Peers to subscribe to:', peers.value.map(p => ({ pubkey: p.pubkey, username: p.username })))
|
||||
|
||||
let successCount = 0
|
||||
let errorCount = 0
|
||||
|
||||
for (const peer of peers.value) {
|
||||
try {
|
||||
console.log(`Attempting to subscribe to peer: ${peer.pubkey} (${peer.username})`)
|
||||
// Subscribe to peer for notifications only (don't load full history)
|
||||
const subscription = await subscribeToPeerForNotifications(peer.pubkey)
|
||||
if (subscription) {
|
||||
console.log(`Successfully subscribed to notifications for peer: ${peer.pubkey}`)
|
||||
successCount++
|
||||
} else {
|
||||
console.warn(`Failed to create subscription for peer: ${peer.pubkey}`)
|
||||
errorCount++
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to subscribe to peer ${peer.pubkey}:`, error)
|
||||
errorCount++
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Subscription summary: ${successCount} successful, ${errorCount} failed`)
|
||||
}
|
||||
|
||||
const refreshPeers = () => {
|
||||
loadPeers()
|
||||
const refreshPeers = async () => {
|
||||
console.log('Refreshing peers and chat data...')
|
||||
await chatPreloader.preloadChat()
|
||||
}
|
||||
|
||||
const selectPeer = async (peer: Peer) => {
|
||||
|
|
@ -693,21 +600,22 @@ onMounted(async () => {
|
|||
}
|
||||
}
|
||||
|
||||
console.log('Starting connection and peer loading...')
|
||||
await connect()
|
||||
console.log('Connection established, loading peers...')
|
||||
await loadPeers()
|
||||
console.log('Peers loaded, checking if we should subscribe...')
|
||||
console.log('Chat component mounted - checking if preloader has data...')
|
||||
|
||||
// If we're connected and have peers, subscribe to them
|
||||
if (isConnected.value && peers.value.length > 0) {
|
||||
console.log('Connection and peers ready, subscribing to all peers for notifications')
|
||||
await subscribeToAllPeers()
|
||||
// If chat is already preloaded, we're good to go
|
||||
if (chatPreloader.isPreloaded.value) {
|
||||
console.log('Chat data was preloaded, connecting to chat...')
|
||||
await connect()
|
||||
console.log('Chat connected successfully')
|
||||
} else if (!chatPreloader.isPreloading.value) {
|
||||
// If not preloaded and not currently preloading, trigger preload
|
||||
console.log('Chat data not preloaded, triggering preload...')
|
||||
await chatPreloader.preloadChat()
|
||||
await connect()
|
||||
} else {
|
||||
console.log('Not ready to subscribe yet:', {
|
||||
isConnected: isConnected.value,
|
||||
peerCount: peers.value.length
|
||||
})
|
||||
// Currently preloading, just connect
|
||||
console.log('Chat is currently preloading, just connecting...')
|
||||
await connect()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -716,13 +624,10 @@ onUnmounted(() => {
|
|||
disconnect()
|
||||
})
|
||||
|
||||
// Watch for connection state changes and subscribe to peers when connected
|
||||
// Watch for connection state changes
|
||||
watch(isConnected, async (connected, prevConnected) => {
|
||||
console.log('Connection state changed:', { connected, prevConnected, peerCount: peers.value.length })
|
||||
if (connected && peers.value.length > 0 && !prevConnected) {
|
||||
console.log('Connection established and peers available, subscribing to peers for notifications')
|
||||
await subscribeToAllPeers()
|
||||
}
|
||||
// Note: Peer subscriptions are handled by the preloader
|
||||
})
|
||||
|
||||
// Watch for new messages and scroll to bottom
|
||||
|
|
|
|||
166
src/composables/useNostrChatPreloader.ts
Normal file
166
src/composables/useNostrChatPreloader.ts
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import { ref, readonly } from 'vue'
|
||||
import { useNostrChat } from './useNostrChat'
|
||||
import { getAuthToken } from '@/lib/config/lnbits'
|
||||
import { config } from '@/lib/config'
|
||||
|
||||
export interface Peer {
|
||||
user_id: string
|
||||
username: string
|
||||
pubkey: string
|
||||
}
|
||||
|
||||
export function useNostrChatPreloader() {
|
||||
const isPreloading = ref(false)
|
||||
const isPreloaded = ref(false)
|
||||
const preloadError = ref<string | null>(null)
|
||||
const peers = ref<Peer[]>([])
|
||||
|
||||
const chat = useNostrChat()
|
||||
|
||||
const preloadChat = async () => {
|
||||
// Don't preload if already done or currently preloading
|
||||
if (isPreloaded.value || isPreloading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isPreloading.value = true
|
||||
preloadError.value = null
|
||||
|
||||
console.log('Preloading chat data...')
|
||||
|
||||
// Connect to chat
|
||||
await chat.connect()
|
||||
|
||||
// Load peers
|
||||
await loadPeers()
|
||||
|
||||
// Subscribe to all peers for notifications (without loading full history)
|
||||
if (peers.value.length > 0) {
|
||||
console.log(`Subscribing to ${peers.value.length} peers for notifications`)
|
||||
await subscribeToAllPeersForNotifications()
|
||||
}
|
||||
|
||||
isPreloaded.value = true
|
||||
console.log('Chat data preloaded successfully')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to preload chat:', error)
|
||||
preloadError.value = error instanceof Error ? error.message : 'Failed to preload chat'
|
||||
// Don't throw error, let the UI handle it gracefully
|
||||
} finally {
|
||||
isPreloading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadPeers = async () => {
|
||||
try {
|
||||
const authToken = getAuthToken()
|
||||
if (!authToken) {
|
||||
console.warn('No authentication token found - cannot load peers')
|
||||
return
|
||||
}
|
||||
|
||||
const API_BASE_URL = config.api.baseUrl || 'http://localhost:5006'
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/auth/nostr/pubkeys`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error('Peers API Error:', response.status, errorText)
|
||||
throw new Error(`Failed to load peers: ${response.status}`)
|
||||
}
|
||||
|
||||
const responseText = await response.text()
|
||||
|
||||
try {
|
||||
const data = JSON.parse(responseText)
|
||||
peers.value = data.map((peer: any) => ({
|
||||
user_id: peer.user_id,
|
||||
username: peer.username,
|
||||
pubkey: peer.pubkey
|
||||
}))
|
||||
|
||||
console.log(`Loaded ${peers.value.length} peers for chat preloader`)
|
||||
|
||||
} catch (parseError) {
|
||||
console.error('JSON Parse Error for peers:', parseError)
|
||||
throw new Error('Invalid JSON response from peers API')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load peers in preloader:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to all peers for notifications (without loading full message history)
|
||||
const subscribeToAllPeersForNotifications = async () => {
|
||||
if (!peers.value.length) {
|
||||
console.log('No peers to subscribe to')
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for connection to be established
|
||||
if (!chat.isConnected.value) {
|
||||
console.log('Waiting for connection to be established before subscribing to peers')
|
||||
// Wait a bit for connection to establish
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
if (!chat.isConnected.value) {
|
||||
console.warn('Still not connected, skipping peer subscriptions')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Subscribing to ${peers.value.length} peers for notifications`)
|
||||
|
||||
let successCount = 0
|
||||
let errorCount = 0
|
||||
|
||||
for (const peer of peers.value) {
|
||||
try {
|
||||
console.log(`Attempting to subscribe to peer: ${peer.pubkey} (${peer.username})`)
|
||||
// Subscribe to peer for notifications only (don't load full history)
|
||||
const subscription = await chat.subscribeToPeerForNotifications(peer.pubkey)
|
||||
if (subscription) {
|
||||
console.log(`Successfully subscribed to notifications for peer: ${peer.pubkey}`)
|
||||
successCount++
|
||||
} else {
|
||||
console.warn(`Failed to create subscription for peer: ${peer.pubkey}`)
|
||||
errorCount++
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to subscribe to peer ${peer.pubkey}:`, error)
|
||||
errorCount++
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Chat preloader subscription summary: ${successCount} successful, ${errorCount} failed`)
|
||||
}
|
||||
|
||||
const resetPreload = () => {
|
||||
isPreloaded.value = false
|
||||
preloadError.value = null
|
||||
peers.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
isPreloading: readonly(isPreloading),
|
||||
isPreloaded: readonly(isPreloaded),
|
||||
preloadError: readonly(preloadError),
|
||||
peers: readonly(peers),
|
||||
preloadChat,
|
||||
resetPreload,
|
||||
|
||||
// Expose chat composable methods for global access
|
||||
getTotalUnreadCount: chat.getTotalUnreadCount,
|
||||
getUnreadCount: chat.getUnreadCount,
|
||||
getAllUnreadCounts: chat.getAllUnreadCounts,
|
||||
markMessagesAsRead: chat.markMessagesAsRead,
|
||||
clearAllUnreadCounts: chat.clearAllUnreadCounts
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue