Refactor chat and market modules for improved integration and maintainability
- Remove deprecated Nostr chat and relay hub components, transitioning to a modular chat service for better encapsulation. - Update App.vue and Navbar.vue to utilize the new chat module, enhancing user experience with automatic peer management. - Simplify event handling and connection logic in ChatComponent.vue, ensuring compatibility with the new chat service architecture. - Adjust market settings and order history components to reflect changes in the chat module, improving overall coherence in the application structure. - Clean up unused imports and streamline configuration access for better performance and maintainability.
This commit is contained in:
parent
63de083909
commit
17c07c37a0
17 changed files with 63 additions and 2222 deletions
38
src/App.vue
38
src/App.vue
|
|
@ -7,9 +7,7 @@ import LoginDialog from '@/components/auth/LoginDialog.vue'
|
||||||
import { Toaster } from '@/components/ui/sonner'
|
import { Toaster } from '@/components/ui/sonner'
|
||||||
import 'vue-sonner/style.css'
|
import 'vue-sonner/style.css'
|
||||||
import { useMarketPreloader } from '@/composables/useMarketPreloader'
|
import { useMarketPreloader } from '@/composables/useMarketPreloader'
|
||||||
import { nostrChat } from '@/composables/useNostrChat'
|
|
||||||
import { auth } from '@/composables/useAuth'
|
import { auth } from '@/composables/useAuth'
|
||||||
import { relayHubComposable } from '@/composables/useRelayHub'
|
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
@ -18,8 +16,7 @@ const showLoginDialog = ref(false)
|
||||||
// Initialize preloader
|
// Initialize preloader
|
||||||
const marketPreloader = useMarketPreloader()
|
const marketPreloader = useMarketPreloader()
|
||||||
|
|
||||||
// Initialize relay hub
|
// Relay hub initialization is now handled by the base module
|
||||||
const relayHub = relayHubComposable
|
|
||||||
|
|
||||||
// Hide navbar on login page
|
// Hide navbar on login page
|
||||||
const showNavbar = computed(() => {
|
const showNavbar = computed(() => {
|
||||||
|
|
@ -33,18 +30,7 @@ async function handleLoginSuccess() {
|
||||||
// Trigger preloading after successful login
|
// Trigger preloading after successful login
|
||||||
marketPreloader.preloadMarket()
|
marketPreloader.preloadMarket()
|
||||||
|
|
||||||
// Connect to chat
|
// Chat initialization is now handled by the chat module
|
||||||
if (!nostrChat.isConnected.value) {
|
|
||||||
try {
|
|
||||||
await nostrChat.connect()
|
|
||||||
|
|
||||||
// Load peers and subscribe to all for notifications
|
|
||||||
const peers = await nostrChat.loadPeers()
|
|
||||||
await nostrChat.subscribeToAllPeersForNotifications(peers)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize chat:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
@ -55,12 +41,7 @@ onMounted(async () => {
|
||||||
console.error('Failed to initialize authentication:', error)
|
console.error('Failed to initialize authentication:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize relay hub
|
// Relay hub initialization is handled by the base module
|
||||||
try {
|
|
||||||
await relayHub.initialize()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize relay hub:', error)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch for authentication changes and trigger preloading
|
// Watch for authentication changes and trigger preloading
|
||||||
|
|
@ -70,18 +51,7 @@ watch(() => auth.isAuthenticated.value, async (isAuthenticated) => {
|
||||||
console.log('User authenticated, triggering market preload...')
|
console.log('User authenticated, triggering market preload...')
|
||||||
marketPreloader.preloadMarket()
|
marketPreloader.preloadMarket()
|
||||||
}
|
}
|
||||||
if (!nostrChat.isConnected.value) {
|
// Chat connection is now handled by the chat module automatically
|
||||||
console.log('User authenticated, connecting to chat...')
|
|
||||||
try {
|
|
||||||
await nostrChat.connect()
|
|
||||||
|
|
||||||
// Load peers and subscribe to all for notifications
|
|
||||||
const peers = await nostrChat.loadPeers()
|
|
||||||
await nostrChat.subscribeToAllPeersForNotifications(peers)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize chat:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { LogoutConfirmDialog } from '@/components/ui/LogoutConfirmDialog'
|
||||||
import { auth } from '@/composables/useAuth'
|
import { auth } from '@/composables/useAuth'
|
||||||
import { useMarketPreloader } from '@/composables/useMarketPreloader'
|
import { useMarketPreloader } from '@/composables/useMarketPreloader'
|
||||||
import { useMarketStore } from '@/stores/market'
|
import { useMarketStore } from '@/stores/market'
|
||||||
import { nostrChat } from '@/composables/useNostrChat'
|
import { tryInjectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
import { useModularNavigation } from '@/composables/useModularNavigation'
|
import { useModularNavigation } from '@/composables/useModularNavigation'
|
||||||
|
|
||||||
interface NavigationItem {
|
interface NavigationItem {
|
||||||
|
|
@ -45,9 +45,12 @@ const totalBalance = computed(() => {
|
||||||
}, 0)
|
}, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Try to get chat service from DI (may not be available if chat module not loaded)
|
||||||
|
const chatService = tryInjectService(SERVICE_TOKENS.CHAT_SERVICE)
|
||||||
|
|
||||||
// Compute total unread messages (reactive)
|
// Compute total unread messages (reactive)
|
||||||
const totalUnreadMessages = computed(() => {
|
const totalUnreadMessages = computed(() => {
|
||||||
return nostrChat.totalUnreadCount.value
|
return chatService?.totalUnreadCount?.value || 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// Compute cart item count
|
// Compute cart item count
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
import { computed } from 'vue'
|
|
||||||
import { useAsyncState } from '@vueuse/core'
|
|
||||||
import type { Event } from '@/lib/types/event'
|
|
||||||
import { fetchEvents } from '@/lib/api/events'
|
|
||||||
|
|
||||||
export function useEvents() {
|
|
||||||
const { state: events, isLoading, error: asyncError, execute: refresh } = useAsyncState(
|
|
||||||
fetchEvents,
|
|
||||||
[] as Event[],
|
|
||||||
{
|
|
||||||
immediate: true,
|
|
||||||
resetOnExecute: false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const error = computed(() => {
|
|
||||||
if (asyncError.value) {
|
|
||||||
return {
|
|
||||||
message: asyncError.value instanceof Error
|
|
||||||
? asyncError.value.message
|
|
||||||
: 'An error occurred while fetching events'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
const sortedEvents = computed(() => {
|
|
||||||
return [...events.value].sort((a, b) =>
|
|
||||||
new Date(b.time).getTime() - new Date(a.time).getTime()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const upcomingEvents = computed(() => {
|
|
||||||
const now = new Date()
|
|
||||||
return sortedEvents.value.filter(event =>
|
|
||||||
new Date(event.event_start_date) > now
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const pastEvents = computed(() => {
|
|
||||||
const now = new Date()
|
|
||||||
return sortedEvents.value.filter(event =>
|
|
||||||
new Date(event.event_end_date) < now
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
events: sortedEvents,
|
|
||||||
upcomingEvents,
|
|
||||||
pastEvents,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
refresh,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,928 +0,0 @@
|
||||||
import { ref, computed, readonly } from 'vue'
|
|
||||||
|
|
||||||
import { nip04, finalizeEvent, type EventTemplate } from 'nostr-tools'
|
|
||||||
import { hexToBytes } from '@/lib/utils/crypto'
|
|
||||||
import { getAuthToken } from '@/lib/config/lnbits'
|
|
||||||
import { config } from '@/lib/config'
|
|
||||||
import { relayHubComposable } from './useRelayHub'
|
|
||||||
import { useAuth } from './useAuth'
|
|
||||||
|
|
||||||
// Types
|
|
||||||
export interface ChatMessage {
|
|
||||||
id: string
|
|
||||||
content: string
|
|
||||||
created_at: number
|
|
||||||
sent: boolean
|
|
||||||
pubkey: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NostrRelayConfig {
|
|
||||||
url: string
|
|
||||||
read?: boolean
|
|
||||||
write?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add notification system for unread messages
|
|
||||||
interface UnreadMessageData {
|
|
||||||
lastReadTimestamp: number
|
|
||||||
unreadCount: number
|
|
||||||
processedMessageIds: Set<string> // Track which messages we've already counted as unread
|
|
||||||
}
|
|
||||||
|
|
||||||
const UNREAD_MESSAGES_KEY = 'nostr-chat-unread-messages'
|
|
||||||
|
|
||||||
// Get unread message data for a peer
|
|
||||||
const getUnreadData = (peerPubkey: string): UnreadMessageData => {
|
|
||||||
try {
|
|
||||||
const stored = localStorage.getItem(`${UNREAD_MESSAGES_KEY}-${peerPubkey}`)
|
|
||||||
if (stored) {
|
|
||||||
const data = JSON.parse(stored)
|
|
||||||
// Convert the array back to a Set for processedMessageIds
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
processedMessageIds: new Set(data.processedMessageIds || [])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { lastReadTimestamp: 0, unreadCount: 0, processedMessageIds: new Set() }
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to load unread data for peer:', peerPubkey, error)
|
|
||||||
return { lastReadTimestamp: 0, unreadCount: 0, processedMessageIds: new Set() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save unread message data for a peer
|
|
||||||
const saveUnreadData = (peerPubkey: string, data: UnreadMessageData): void => {
|
|
||||||
try {
|
|
||||||
// Convert Set to array for localStorage serialization
|
|
||||||
const serializableData = {
|
|
||||||
...data,
|
|
||||||
processedMessageIds: Array.from(data.processedMessageIds)
|
|
||||||
}
|
|
||||||
localStorage.setItem(`${UNREAD_MESSAGES_KEY}-${peerPubkey}`, JSON.stringify(serializableData))
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to save unread data for peer:', peerPubkey, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useNostrChat() {
|
|
||||||
// Use the centralized relay hub
|
|
||||||
const relayHub = relayHubComposable
|
|
||||||
|
|
||||||
// Use the main authentication system
|
|
||||||
const auth = useAuth()
|
|
||||||
|
|
||||||
// State
|
|
||||||
const messages = ref<Map<string, ChatMessage[]>>(new Map())
|
|
||||||
const processedMessageIds = ref(new Set<string>())
|
|
||||||
const onMessageAdded = ref<((peerPubkey: string) => void) | null>(null)
|
|
||||||
|
|
||||||
// Reactive unread counts
|
|
||||||
const unreadCounts = ref<Map<string, number>>(new Map())
|
|
||||||
|
|
||||||
// Track latest message timestamp for each peer (for sorting)
|
|
||||||
const latestMessageTimestamps = ref<Map<string, number>>(new Map())
|
|
||||||
|
|
||||||
// Track peers globally
|
|
||||||
const peers = ref<any[]>([])
|
|
||||||
|
|
||||||
// Track malformed message IDs to prevent repeated processing attempts
|
|
||||||
const malformedMessageIds = ref(new Set<string>())
|
|
||||||
|
|
||||||
// Mark a message as malformed to prevent future processing attempts
|
|
||||||
const markMessageAsMalformed = (eventId: string) => {
|
|
||||||
malformedMessageIds.value.add(eventId)
|
|
||||||
// Also mark as processed to prevent retries
|
|
||||||
processedMessageIds.value.add(eventId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up old malformed messages (call this periodically)
|
|
||||||
const cleanupMalformedMessages = () => {
|
|
||||||
// const now = Math.floor(Date.now() / 1000)
|
|
||||||
// const maxAge = 24 * 60 * 60 // 24 hours
|
|
||||||
|
|
||||||
// Clear old malformed message IDs to free memory
|
|
||||||
// This is a simple cleanup - in production you might want more sophisticated tracking
|
|
||||||
if (malformedMessageIds.value.size > 1000) {
|
|
||||||
console.log('Cleaning up malformed message tracking (clearing all)')
|
|
||||||
malformedMessageIds.value.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up periodic cleanup
|
|
||||||
let cleanupInterval: number | null = null
|
|
||||||
|
|
||||||
// Clean up resources
|
|
||||||
const cleanup = () => {
|
|
||||||
if (cleanupInterval) {
|
|
||||||
clearInterval(cleanupInterval)
|
|
||||||
cleanupInterval = null
|
|
||||||
console.log('Cleaned up malformed message tracking interval')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manually clear all malformed message tracking
|
|
||||||
const clearAllMalformedMessages = () => {
|
|
||||||
const count = malformedMessageIds.value.size
|
|
||||||
malformedMessageIds.value.clear()
|
|
||||||
console.log(`Cleared ${count} malformed message IDs from tracking`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get statistics about malformed messages
|
|
||||||
const getMalformedMessageStats = () => {
|
|
||||||
return {
|
|
||||||
totalMalformed: malformedMessageIds.value.size,
|
|
||||||
totalProcessed: processedMessageIds.value.size,
|
|
||||||
malformedIds: Array.from(malformedMessageIds.value).slice(0, 10) // First 10 for debugging
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Computed - use relay hub's connection status and auth system
|
|
||||||
const isConnected = computed(() => relayHub.isConnected.value)
|
|
||||||
|
|
||||||
// Get current user from auth system
|
|
||||||
const currentUser = computed(() => {
|
|
||||||
const user = auth.currentUser.value
|
|
||||||
if (!user) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the user has a pubkey field
|
|
||||||
if (!user.pubkey) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the user has a prvkey field
|
|
||||||
if (!user.prvkey) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the actual user data - assume prvkey and pubkey contain real Nostr keys
|
|
||||||
return {
|
|
||||||
pubkey: user.pubkey,
|
|
||||||
prvkey: user.prvkey
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check if user is authenticated (has LNBits login)
|
|
||||||
const isAuthenticated = computed(() => {
|
|
||||||
return auth.currentUser.value !== null
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check if user has complete Nostr keypair
|
|
||||||
const hasNostrKeys = computed(() => {
|
|
||||||
const user = currentUser.value
|
|
||||||
return user && user.pubkey && user.prvkey
|
|
||||||
})
|
|
||||||
|
|
||||||
// Get Nostr key status for debugging
|
|
||||||
const getNostrKeyStatus = () => {
|
|
||||||
const user = auth.currentUser.value
|
|
||||||
if (!user) {
|
|
||||||
return { hasUser: false, hasPubkey: false, hasPrvkey: false, message: 'No user logged in' }
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasUser: true,
|
|
||||||
hasPubkey: !!user.pubkey,
|
|
||||||
hasPrvkey: !!user.prvkey,
|
|
||||||
message: user.pubkey && user.prvkey ? 'User has complete Nostr keypair' : 'User missing Nostr keys',
|
|
||||||
pubkey: user.pubkey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get unread count for a peer
|
|
||||||
const getUnreadCount = (peerPubkey: string): number => {
|
|
||||||
return unreadCounts.value.get(peerPubkey) || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all unread counts
|
|
||||||
const getAllUnreadCounts = (): Map<string, number> => {
|
|
||||||
return new Map(unreadCounts.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get total unread count across all peers
|
|
||||||
const getTotalUnreadCount = (): number => {
|
|
||||||
let total = 0
|
|
||||||
for (const count of unreadCounts.value.values()) {
|
|
||||||
total += count
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get latest message timestamp for a peer
|
|
||||||
const getLatestMessageTimestamp = (peerPubkey: string): number => {
|
|
||||||
return latestMessageTimestamps.value.get(peerPubkey) || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all latest message timestamps
|
|
||||||
const getAllLatestMessageTimestamps = (): Map<string, number> => {
|
|
||||||
return new Map(latestMessageTimestamps.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update latest message timestamp for a peer
|
|
||||||
const updateLatestMessageTimestamp = (peerPubkey: string, timestamp: number): void => {
|
|
||||||
const current = latestMessageTimestamps.value.get(peerPubkey) || 0
|
|
||||||
if (timestamp > current) {
|
|
||||||
latestMessageTimestamps.value.set(peerPubkey, timestamp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update unread count for a peer
|
|
||||||
const updateUnreadCount = (peerPubkey: string, count: number): void => {
|
|
||||||
if (count > 0) {
|
|
||||||
unreadCounts.value.set(peerPubkey, count)
|
|
||||||
} else {
|
|
||||||
unreadCounts.value.delete(peerPubkey)
|
|
||||||
}
|
|
||||||
// Force reactivity
|
|
||||||
unreadCounts.value = new Map(unreadCounts.value)
|
|
||||||
|
|
||||||
// Save to localStorage
|
|
||||||
const unreadData = getUnreadData(peerPubkey)
|
|
||||||
unreadData.unreadCount = count
|
|
||||||
saveUnreadData(peerPubkey, unreadData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark messages as read for a peer
|
|
||||||
const markMessagesAsRead = (peerPubkey: string): void => {
|
|
||||||
const currentTimestamp = Math.floor(Date.now() / 1000)
|
|
||||||
|
|
||||||
// Update last read timestamp, reset unread count, and clear processed message IDs
|
|
||||||
const updatedData: UnreadMessageData = {
|
|
||||||
lastReadTimestamp: currentTimestamp,
|
|
||||||
unreadCount: 0,
|
|
||||||
processedMessageIds: new Set() // Clear processed messages when marking as read
|
|
||||||
}
|
|
||||||
|
|
||||||
saveUnreadData(peerPubkey, updatedData)
|
|
||||||
updateUnreadCount(peerPubkey, 0)
|
|
||||||
|
|
||||||
// Also clear any processed message IDs from the global set that might be from this peer
|
|
||||||
// This helps prevent duplicate message issues
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load unread counts from localStorage
|
|
||||||
const loadUnreadCounts = (): void => {
|
|
||||||
try {
|
|
||||||
const keys = Object.keys(localStorage).filter(key =>
|
|
||||||
key.startsWith(`${UNREAD_MESSAGES_KEY}-`)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
for (const key of keys) {
|
|
||||||
const peerPubkey = key.replace(`${UNREAD_MESSAGES_KEY}-`, '')
|
|
||||||
const unreadData = getUnreadData(peerPubkey)
|
|
||||||
|
|
||||||
// Recalculate unread count based on actual messages and lastReadTimestamp
|
|
||||||
const peerMessages = messages.value.get(peerPubkey) || []
|
|
||||||
let actualUnreadCount = 0
|
|
||||||
|
|
||||||
for (const message of peerMessages) {
|
|
||||||
// Only count messages not sent by us and created after last read timestamp
|
|
||||||
if (!message.sent && message.created_at > unreadData.lastReadTimestamp) {
|
|
||||||
actualUnreadCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the stored count to match reality
|
|
||||||
if (actualUnreadCount !== unreadData.unreadCount) {
|
|
||||||
unreadData.unreadCount = actualUnreadCount
|
|
||||||
saveUnreadData(peerPubkey, unreadData)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (actualUnreadCount > 0) {
|
|
||||||
unreadCounts.value.set(peerPubkey, actualUnreadCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to load unread counts:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize unread counts on startup
|
|
||||||
loadUnreadCounts()
|
|
||||||
|
|
||||||
// Clear unread count for a peer
|
|
||||||
// const clearUnreadCount = (peerPubkey: string): void => {
|
|
||||||
// unreadCounts.value.delete(peerPubkey)
|
|
||||||
//
|
|
||||||
// // Clear from localStorage
|
|
||||||
// const unreadData = getUnreadData(peerPubkey)
|
|
||||||
// unreadData.unreadCount = 0
|
|
||||||
// saveUnreadData(peerPubkey, unreadData)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Clear all unread counts
|
|
||||||
const clearAllUnreadCounts = (): void => {
|
|
||||||
unreadCounts.value.clear()
|
|
||||||
|
|
||||||
// Clear from localStorage for all peers
|
|
||||||
for (const [peerPubkey] of messages.value) {
|
|
||||||
const unreadData = getUnreadData(peerPubkey)
|
|
||||||
unreadData.unreadCount = 0
|
|
||||||
saveUnreadData(peerPubkey, unreadData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also clear from localStorage for all stored keys
|
|
||||||
try {
|
|
||||||
const keys = Object.keys(localStorage).filter(key =>
|
|
||||||
key.startsWith(`${UNREAD_MESSAGES_KEY}-`)
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const key of keys) {
|
|
||||||
const peerPubkey = key.replace(`${UNREAD_MESSAGES_KEY}-`, '')
|
|
||||||
const unreadData = getUnreadData(peerPubkey)
|
|
||||||
unreadData.unreadCount = 0
|
|
||||||
saveUnreadData(peerPubkey, unreadData)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to clear unread counts from localStorage:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear processed message IDs for a peer
|
|
||||||
const clearProcessedMessageIds = (peerPubkey: string): void => {
|
|
||||||
const unreadData = getUnreadData(peerPubkey)
|
|
||||||
unreadData.processedMessageIds.clear()
|
|
||||||
saveUnreadData(peerPubkey, unreadData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug unread data for a peer
|
|
||||||
const debugUnreadData = (peerPubkey: string): void => {
|
|
||||||
// Function kept for potential future debugging
|
|
||||||
getUnreadData(peerPubkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get relay configuration
|
|
||||||
const getRelays = (): NostrRelayConfig[] => {
|
|
||||||
return config.nostr.relays.map(url => ({
|
|
||||||
url,
|
|
||||||
read: true,
|
|
||||||
write: true
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect using the relay hub
|
|
||||||
const connect = async () => {
|
|
||||||
try {
|
|
||||||
// The relay hub should already be initialized by the app
|
|
||||||
if (!relayHub.isConnected.value) {
|
|
||||||
await relayHub.connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up periodic cleanup of malformed messages
|
|
||||||
if (!cleanupInterval) {
|
|
||||||
cleanupInterval = setInterval(cleanupMalformedMessages, 5 * 60 * 1000) as unknown as number // Every 5 minutes
|
|
||||||
console.log('Set up periodic cleanup of malformed messages')
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to connect to relays:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disconnect using the relay hub
|
|
||||||
const disconnect = () => {
|
|
||||||
// Note: We don't disconnect the relay hub here as other components might be using it
|
|
||||||
// The relay hub will be managed at the app level
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load current user from LNBits
|
|
||||||
// const loadCurrentUser = async () => {
|
|
||||||
// try {
|
|
||||||
// // Get current user from LNBits API using the auth endpoint
|
|
||||||
// const authToken = getAuthToken()
|
|
||||||
// if (!authToken) {
|
|
||||||
// throw new Error('No authentication token found')
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const API_BASE_URL = config.api.baseUrl || 'http://localhost:5006'
|
|
||||||
// const response = await fetch(`${API_BASE_URL}/api/v1/auth/nostr/me`, {
|
|
||||||
// headers: {
|
|
||||||
// 'Authorization': `Bearer ${authToken}`,
|
|
||||||
// 'Content-Type': 'application/json'
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
// console.log('API Response status:', response.status)
|
|
||||||
// console.log('API Response headers:', response.headers)
|
|
||||||
|
|
||||||
// const responseText = await response.text()
|
|
||||||
// console.log('API Response text:', responseText)
|
|
||||||
|
|
||||||
// if (response.ok) {
|
|
||||||
// try {
|
|
||||||
// const user = JSON.parse(responseText)
|
|
||||||
// currentUser.value = {
|
|
||||||
// pubkey: user.pubkey,
|
|
||||||
// prvkey: user.prvkey
|
|
||||||
// }
|
|
||||||
// } catch (parseError) {
|
|
||||||
// console.error('JSON Parse Error:', parseError)
|
|
||||||
// console.error('Response was:', responseText)
|
|
||||||
// throw new Error('Invalid JSON response from API')
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// console.error('API Error:', response.status, responseText)
|
|
||||||
// throw new Error(`Failed to load current user: ${response.status}`)
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Failed to load current user:', error)
|
|
||||||
// throw error
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Subscribe to a specific peer for messages
|
|
||||||
const subscribeToPeer = async (peerPubkey: string) => {
|
|
||||||
if (!currentUser.value) {
|
|
||||||
console.warn('Cannot subscribe to peer: no user logged in')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentUser.value.pubkey) {
|
|
||||||
console.warn('Cannot subscribe to peer: no public key available')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have a pool and are connected
|
|
||||||
if (!relayHub.isConnected.value) {
|
|
||||||
console.warn('Not connected to relays - attempting to connect...')
|
|
||||||
await connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!relayHub.isConnected.value) {
|
|
||||||
throw new Error('Failed to initialize Nostr pool')
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Subscribe to direct messages (kind 4) from this peer
|
|
||||||
const filter = {
|
|
||||||
kinds: [4],
|
|
||||||
'#p': [currentUser.value.pubkey], // Messages where we are the recipient
|
|
||||||
authors: [peerPubkey] // Messages from this specific peer
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Use the relay hub to subscribe
|
|
||||||
const unsubscribe = relayHub.subscribe({
|
|
||||||
id: `peer-${peerPubkey}`,
|
|
||||||
filters: [filter],
|
|
||||||
onEvent: (event) => {
|
|
||||||
handleIncomingMessage(event, peerPubkey)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return unsubscribe
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to subscribe to peer:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe to a peer for notifications only (without loading full message history)
|
|
||||||
const subscribeToPeerForNotifications = async (peerPubkey: string) => {
|
|
||||||
if (!currentUser.value) {
|
|
||||||
console.warn('No user logged in - cannot subscribe to peer notifications')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have a pool and are connected
|
|
||||||
if (!relayHub.isConnected.value) {
|
|
||||||
console.warn('Not connected to relays - attempting to connect...')
|
|
||||||
await connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!relayHub.isConnected.value) {
|
|
||||||
throw new Error('Failed to initialize Nostr pool')
|
|
||||||
}
|
|
||||||
|
|
||||||
const myPubkey = currentUser.value.pubkey
|
|
||||||
|
|
||||||
// Subscribe to new messages only (no historical messages)
|
|
||||||
const relayConfigs = getRelays()
|
|
||||||
|
|
||||||
const filters = [
|
|
||||||
{
|
|
||||||
kinds: [4],
|
|
||||||
authors: [peerPubkey],
|
|
||||||
'#p': [myPubkey]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kinds: [4],
|
|
||||||
authors: [myPubkey],
|
|
||||||
'#p': [peerPubkey]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const unsubscribe = relayHub.subscribe({
|
|
||||||
id: `notifications-${peerPubkey}-${Date.now()}`,
|
|
||||||
filters,
|
|
||||||
relays: relayConfigs.map(r => r.url),
|
|
||||||
onEvent: (event: any) => {
|
|
||||||
handleIncomingMessage(event, peerPubkey)
|
|
||||||
},
|
|
||||||
onEose: () => {
|
|
||||||
// Notification subscription closed
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return unsubscribe
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Handle incoming message
|
|
||||||
const handleIncomingMessage = async (event: any, peerPubkey: string) => {
|
|
||||||
if (!currentUser.value || !currentUser.value.prvkey) {
|
|
||||||
console.warn('Cannot decrypt message: no private key available')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we've already processed this message to prevent duplicates
|
|
||||||
if (processedMessageIds.value.has(event.id)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this message was previously identified as malformed
|
|
||||||
if (malformedMessageIds.value.has(event.id)) {
|
|
||||||
console.log('Skipping previously identified malformed message:', event.id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// For NIP-04 direct messages, always use peerPubkey as the second argument
|
|
||||||
// This is the public key of the other party in the conversation
|
|
||||||
const isSentByMe = event.pubkey === currentUser.value.pubkey
|
|
||||||
|
|
||||||
// Check for malformed messages before attempting decryption
|
|
||||||
if (typeof event.content !== 'string' || event.content.length === 0) {
|
|
||||||
console.warn('Skipping message with invalid content format:', {
|
|
||||||
eventId: event.id,
|
|
||||||
contentType: typeof event.content,
|
|
||||||
contentLength: event.content?.length
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for our old placeholder encryption format
|
|
||||||
if (event.content.includes('[ENCRYPTED]') && event.content.includes('[ENCRYPTED]')) {
|
|
||||||
console.warn('Skipping message with old placeholder encryption format:', {
|
|
||||||
eventId: event.id,
|
|
||||||
content: event.content.substring(0, 100) + '...'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for other common malformed patterns
|
|
||||||
if (event.content.startsWith('[') || event.content.includes('ENCRYPTED')) {
|
|
||||||
console.warn('Skipping message with suspicious encryption format:', {
|
|
||||||
eventId: event.id,
|
|
||||||
content: event.content.substring(0, 100) + '...'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const decryptedContent = await nip04.decrypt(
|
|
||||||
currentUser.value.prvkey,
|
|
||||||
peerPubkey, // Always use peerPubkey for shared secret derivation
|
|
||||||
event.content
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Create chat message
|
|
||||||
const message: ChatMessage = {
|
|
||||||
id: event.id,
|
|
||||||
content: decryptedContent,
|
|
||||||
created_at: event.created_at,
|
|
||||||
sent: isSentByMe,
|
|
||||||
pubkey: event.pubkey
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to messages
|
|
||||||
if (!messages.value.has(peerPubkey)) {
|
|
||||||
messages.value.set(peerPubkey, [])
|
|
||||||
}
|
|
||||||
messages.value.get(peerPubkey)!.push(message)
|
|
||||||
|
|
||||||
// Mark as unread if not sent by us AND created after last read timestamp
|
|
||||||
if (!isSentByMe) {
|
|
||||||
const unreadData = getUnreadData(peerPubkey)
|
|
||||||
|
|
||||||
// Only count as unread if message was created after last read timestamp
|
|
||||||
if (event.created_at > unreadData.lastReadTimestamp) {
|
|
||||||
// Increment the unread count for this peer
|
|
||||||
const currentCount = unreadCounts.value.get(peerPubkey) || 0
|
|
||||||
const newCount = currentCount + 1
|
|
||||||
unreadCounts.value.set(peerPubkey, newCount)
|
|
||||||
|
|
||||||
// Force reactivity
|
|
||||||
unreadCounts.value = new Map(unreadCounts.value)
|
|
||||||
|
|
||||||
// Save to localStorage
|
|
||||||
unreadData.unreadCount = newCount
|
|
||||||
saveUnreadData(peerPubkey, unreadData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update latest message timestamp
|
|
||||||
updateLatestMessageTimestamp(peerPubkey, event.created_at)
|
|
||||||
|
|
||||||
// Mark this message as processed to prevent duplicates
|
|
||||||
processedMessageIds.value.add(event.id)
|
|
||||||
|
|
||||||
// Trigger callback if set
|
|
||||||
if (onMessageAdded.value) {
|
|
||||||
onMessageAdded.value(peerPubkey)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Provide more specific error handling for different types of failures
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
|
||||||
|
|
||||||
// Check for specific error patterns that indicate malformed messages
|
|
||||||
if (errorMessage.includes('join.decode') || errorMessage.includes('input should be string')) {
|
|
||||||
console.warn('Skipping malformed message (invalid NIP-04 format):', {
|
|
||||||
eventId: event.id,
|
|
||||||
pubkey: event.pubkey,
|
|
||||||
error: errorMessage,
|
|
||||||
contentPreview: typeof event.content === 'string' ? event.content.substring(0, 100) + '...' : 'Invalid content type'
|
|
||||||
})
|
|
||||||
markMessageAsMalformed(event.id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorMessage.includes('Invalid byte sequence') || errorMessage.includes('hex string')) {
|
|
||||||
console.warn('Skipping message with invalid hex encoding:', {
|
|
||||||
eventId: event.id,
|
|
||||||
pubkey: event.pubkey,
|
|
||||||
error: errorMessage
|
|
||||||
})
|
|
||||||
markMessageAsMalformed(event.id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// For other decryption errors, log with more context
|
|
||||||
console.error('Failed to decrypt message:', {
|
|
||||||
eventId: event.id,
|
|
||||||
pubkey: event.pubkey,
|
|
||||||
error: errorMessage,
|
|
||||||
contentType: typeof event.content,
|
|
||||||
contentLength: event.content?.length,
|
|
||||||
contentPreview: typeof event.content === 'string' ? event.content.substring(0, 100) + '...' : 'Invalid content type'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send message to a peer
|
|
||||||
const sendMessage = async (peerPubkey: string, content: string) => {
|
|
||||||
if (!currentUser.value) {
|
|
||||||
throw new Error('No user logged in - please authenticate first')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have the required Nostr keypair
|
|
||||||
if (!currentUser.value.prvkey) {
|
|
||||||
throw new Error('Nostr private key not available. Please ensure your LNBits account has Nostr keys configured.')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have a pool and are connected
|
|
||||||
if (!relayHub.isConnected.value) {
|
|
||||||
console.warn('Not connected to relays - attempting to connect...')
|
|
||||||
await connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!relayHub.isConnected.value) {
|
|
||||||
throw new Error('Failed to initialize Nostr pool')
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate keys before encryption
|
|
||||||
if (!currentUser.value.prvkey || !peerPubkey) {
|
|
||||||
throw new Error('Missing private key or peer public key')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure keys are in correct hex format (64 characters for private key, 64 characters for public key)
|
|
||||||
const privateKey = currentUser.value.prvkey.startsWith('0x')
|
|
||||||
? currentUser.value.prvkey.slice(2)
|
|
||||||
: currentUser.value.prvkey
|
|
||||||
|
|
||||||
const publicKey = peerPubkey.startsWith('0x')
|
|
||||||
? peerPubkey.slice(2)
|
|
||||||
: peerPubkey
|
|
||||||
|
|
||||||
if (privateKey.length !== 64) {
|
|
||||||
throw new Error(`Invalid private key length: ${privateKey.length} (expected 64)`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (publicKey.length !== 64) {
|
|
||||||
throw new Error(`Invalid public key length: ${publicKey.length} (expected 64)`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate hex format
|
|
||||||
const hexRegex = /^[0-9a-fA-F]+$/
|
|
||||||
if (!hexRegex.test(privateKey)) {
|
|
||||||
throw new Error(`Invalid private key format: contains non-hex characters`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hexRegex.test(publicKey)) {
|
|
||||||
throw new Error(`Invalid public key format: contains non-hex characters`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt the message
|
|
||||||
let encryptedContent: string
|
|
||||||
try {
|
|
||||||
encryptedContent = await nip04.encrypt(
|
|
||||||
privateKey,
|
|
||||||
publicKey,
|
|
||||||
content
|
|
||||||
)
|
|
||||||
} catch (encryptError) {
|
|
||||||
console.error('Encryption failed:', encryptError)
|
|
||||||
throw new Error(`Encryption failed: ${encryptError instanceof Error ? encryptError.message : String(encryptError)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the event template
|
|
||||||
const eventTemplate: EventTemplate = {
|
|
||||||
kind: 4,
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
tags: [['p', peerPubkey]],
|
|
||||||
content: encryptedContent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finalize the event (sign it)
|
|
||||||
const event = finalizeEvent(eventTemplate, hexToBytes(privateKey))
|
|
||||||
|
|
||||||
// Publish to relays using the relay hub
|
|
||||||
await relayHub.publishEvent(event)
|
|
||||||
|
|
||||||
// Add message to local state
|
|
||||||
const message: ChatMessage = {
|
|
||||||
id: event.id,
|
|
||||||
content,
|
|
||||||
created_at: event.created_at,
|
|
||||||
sent: true,
|
|
||||||
pubkey: currentUser.value.pubkey
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to processed IDs to prevent duplicate processing
|
|
||||||
processedMessageIds.value.add(event.id)
|
|
||||||
|
|
||||||
if (!messages.value.has(peerPubkey)) {
|
|
||||||
messages.value.set(peerPubkey, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.value.get(peerPubkey)!.push(message)
|
|
||||||
|
|
||||||
// Sort messages by timestamp
|
|
||||||
messages.value.get(peerPubkey)!.sort((a, b) => a.created_at - b.created_at)
|
|
||||||
|
|
||||||
// Force reactivity by triggering a change
|
|
||||||
messages.value = new Map(messages.value)
|
|
||||||
|
|
||||||
// Update latest message timestamp for this peer (for sorting)
|
|
||||||
updateLatestMessageTimestamp(peerPubkey, message.created_at)
|
|
||||||
|
|
||||||
// Trigger callback if set
|
|
||||||
if (onMessageAdded.value) {
|
|
||||||
onMessageAdded.value(peerPubkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to send message:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get messages for a specific peer
|
|
||||||
const getMessages = (peerPubkey: string): ChatMessage[] => {
|
|
||||||
return messages.value.get(peerPubkey) || []
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear messages for a specific peer
|
|
||||||
const clearMessages = (peerPubkey: string) => {
|
|
||||||
messages.value.delete(peerPubkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load peers from API
|
|
||||||
const loadPeers = async () => {
|
|
||||||
try {
|
|
||||||
const authToken = getAuthToken()
|
|
||||||
if (!authToken) {
|
|
||||||
throw new Error('No authentication token found')
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
throw new Error(`Failed to load peers: ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
const loadedPeers = data.map((peer: any) => ({
|
|
||||||
user_id: peer.user_id,
|
|
||||||
username: peer.username,
|
|
||||||
pubkey: peer.pubkey
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Store peers in the singleton state
|
|
||||||
peers.value = loadedPeers
|
|
||||||
|
|
||||||
|
|
||||||
return loadedPeers
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load peers:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe to all peers for notifications (without loading full message history)
|
|
||||||
const subscribeToAllPeersForNotifications = async (peers: any[]) => {
|
|
||||||
if (!peers.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for connection to be established
|
|
||||||
if (!relayHub.isConnected.value) {
|
|
||||||
// Wait a bit for connection to establish
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
||||||
|
|
||||||
if (!relayHub.isConnected.value) {
|
|
||||||
console.warn('Still not connected, skipping peer subscriptions')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe to each peer for notifications
|
|
||||||
for (const peer of peers) {
|
|
||||||
try {
|
|
||||||
await subscribeToPeerForNotifications(peer.pubkey)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to subscribe to peer ${peer.username} (${peer.pubkey}):`, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
// State
|
|
||||||
isConnected: readonly(isConnected),
|
|
||||||
messages: readonly(messages),
|
|
||||||
isLoggedIn: readonly(isAuthenticated),
|
|
||||||
peers: readonly(peers),
|
|
||||||
|
|
||||||
// Reactive computed properties
|
|
||||||
totalUnreadCount: computed(() => getTotalUnreadCount()),
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
connect,
|
|
||||||
disconnect,
|
|
||||||
subscribeToPeer,
|
|
||||||
subscribeToPeerForNotifications,
|
|
||||||
sendMessage,
|
|
||||||
getMessages,
|
|
||||||
clearMessages,
|
|
||||||
onMessageAdded,
|
|
||||||
|
|
||||||
// Notification methods
|
|
||||||
markMessagesAsRead,
|
|
||||||
getUnreadCount,
|
|
||||||
getAllUnreadCounts,
|
|
||||||
getTotalUnreadCount,
|
|
||||||
clearAllUnreadCounts,
|
|
||||||
clearProcessedMessageIds,
|
|
||||||
debugUnreadData,
|
|
||||||
getUnreadData,
|
|
||||||
|
|
||||||
// Timestamp methods (for sorting)
|
|
||||||
getLatestMessageTimestamp,
|
|
||||||
getAllLatestMessageTimestamps,
|
|
||||||
|
|
||||||
// Peer management methods
|
|
||||||
loadPeers,
|
|
||||||
subscribeToAllPeersForNotifications,
|
|
||||||
currentUser,
|
|
||||||
hasNostrKeys,
|
|
||||||
getNostrKeyStatus,
|
|
||||||
markMessageAsMalformed,
|
|
||||||
cleanupMalformedMessages,
|
|
||||||
clearAllMalformedMessages, // Add the new function to the return object
|
|
||||||
cleanup, // Add the cleanup function to the return object
|
|
||||||
getMalformedMessageStats // Add the new function to the return object
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export singleton instance for global state
|
|
||||||
export const nostrChat = useNostrChat()
|
|
||||||
|
|
@ -1,198 +0,0 @@
|
||||||
import { ref, computed, onUnmounted, readonly } from 'vue'
|
|
||||||
import { nostrclientHub, type SubscriptionConfig } from '../lib/nostr/nostrclientHub'
|
|
||||||
|
|
||||||
export function useNostrclientHub() {
|
|
||||||
// Reactive state
|
|
||||||
const isConnected = ref(false)
|
|
||||||
const isConnecting = ref(false)
|
|
||||||
const connectionStatus = ref<'connecting' | 'connected' | 'disconnected' | 'error'>('disconnected')
|
|
||||||
const error = ref<Error | null>(null)
|
|
||||||
const activeSubscriptions = ref<Set<string>>(new Set())
|
|
||||||
|
|
||||||
// Reactive counts
|
|
||||||
const totalSubscriptionCount = ref(0)
|
|
||||||
const subscriptionDetails = ref<Array<{ id: string; filters: any[] }>>([])
|
|
||||||
|
|
||||||
// Computed properties
|
|
||||||
const connectionHealth = computed(() => {
|
|
||||||
return isConnected.value ? 100 : 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// Initialize nostrclient hub
|
|
||||||
const initialize = async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
connectionStatus.value = 'connecting'
|
|
||||||
error.value = null
|
|
||||||
|
|
||||||
console.log('🔧 NostrclientHub: Initializing...')
|
|
||||||
await nostrclientHub.initialize()
|
|
||||||
console.log('🔧 NostrclientHub: Initialization successful')
|
|
||||||
|
|
||||||
// Set up event listeners
|
|
||||||
setupEventListeners()
|
|
||||||
|
|
||||||
connectionStatus.value = 'connected'
|
|
||||||
isConnected.value = true
|
|
||||||
console.log('🔧 NostrclientHub: Connection status set to connected')
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
const errorObj = err instanceof Error ? err : new Error('Failed to initialize NostrclientHub')
|
|
||||||
error.value = errorObj
|
|
||||||
connectionStatus.value = 'error'
|
|
||||||
isConnected.value = false
|
|
||||||
console.error('🔧 NostrclientHub: Failed to initialize:', errorObj)
|
|
||||||
throw errorObj
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to nostrclient
|
|
||||||
const connect = async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
connectionStatus.value = 'connecting'
|
|
||||||
error.value = null
|
|
||||||
|
|
||||||
await nostrclientHub.connect()
|
|
||||||
|
|
||||||
connectionStatus.value = 'connected'
|
|
||||||
isConnected.value = true
|
|
||||||
} catch (err) {
|
|
||||||
const errorObj = err instanceof Error ? err : new Error('Failed to connect')
|
|
||||||
error.value = errorObj
|
|
||||||
connectionStatus.value = 'error'
|
|
||||||
isConnected.value = false
|
|
||||||
throw errorObj
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disconnect from nostrclient
|
|
||||||
const disconnect = (): void => {
|
|
||||||
nostrclientHub.disconnect()
|
|
||||||
connectionStatus.value = 'disconnected'
|
|
||||||
isConnected.value = false
|
|
||||||
error.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe to events
|
|
||||||
const subscribe = (config: SubscriptionConfig): (() => void) => {
|
|
||||||
try {
|
|
||||||
const unsubscribe = nostrclientHub.subscribe(config)
|
|
||||||
activeSubscriptions.value.add(config.id)
|
|
||||||
|
|
||||||
// Update reactive state
|
|
||||||
totalSubscriptionCount.value = nostrclientHub.totalSubscriptionCount
|
|
||||||
subscriptionDetails.value = nostrclientHub.subscriptionDetails
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribe()
|
|
||||||
activeSubscriptions.value.delete(config.id)
|
|
||||||
totalSubscriptionCount.value = nostrclientHub.totalSubscriptionCount
|
|
||||||
subscriptionDetails.value = nostrclientHub.subscriptionDetails
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to subscribe:', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish an event
|
|
||||||
const publishEvent = async (event: any): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await nostrclientHub.publishEvent(event)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to publish event:', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query events
|
|
||||||
const queryEvents = async (filters: any[]): Promise<any[]> => {
|
|
||||||
try {
|
|
||||||
return await nostrclientHub.queryEvents(filters)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to query events:', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up event listeners
|
|
||||||
const setupEventListeners = () => {
|
|
||||||
nostrclientHub.on('connected', () => {
|
|
||||||
isConnected.value = true
|
|
||||||
isConnecting.value = false
|
|
||||||
connectionStatus.value = 'connected'
|
|
||||||
error.value = null
|
|
||||||
})
|
|
||||||
|
|
||||||
nostrclientHub.on('disconnected', () => {
|
|
||||||
isConnected.value = false
|
|
||||||
isConnecting.value = false
|
|
||||||
connectionStatus.value = 'disconnected'
|
|
||||||
})
|
|
||||||
|
|
||||||
nostrclientHub.on('error', (err: any) => {
|
|
||||||
error.value = err
|
|
||||||
connectionStatus.value = 'error'
|
|
||||||
})
|
|
||||||
|
|
||||||
nostrclientHub.on('connectionError', (err: any) => {
|
|
||||||
error.value = err
|
|
||||||
connectionStatus.value = 'error'
|
|
||||||
})
|
|
||||||
|
|
||||||
nostrclientHub.on('maxReconnectionAttemptsReached', () => {
|
|
||||||
error.value = new Error('Max reconnection attempts reached')
|
|
||||||
connectionStatus.value = 'error'
|
|
||||||
})
|
|
||||||
|
|
||||||
nostrclientHub.on('event', ({ subscriptionId, event }: any) => {
|
|
||||||
console.log('Received event for subscription:', subscriptionId, event.id)
|
|
||||||
})
|
|
||||||
|
|
||||||
nostrclientHub.on('eose', ({ subscriptionId }: any) => {
|
|
||||||
console.log('EOSE received for subscription:', subscriptionId)
|
|
||||||
})
|
|
||||||
|
|
||||||
nostrclientHub.on('notice', ({ message }: any) => {
|
|
||||||
console.log('Notice from nostrclient:', message)
|
|
||||||
})
|
|
||||||
|
|
||||||
nostrclientHub.on('eventPublished', ({ eventId }: any) => {
|
|
||||||
console.log('Event published successfully:', eventId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up event listeners
|
|
||||||
const cleanup = () => {
|
|
||||||
nostrclientHub.removeAllListeners()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-cleanup on unmount
|
|
||||||
onUnmounted(() => {
|
|
||||||
cleanup()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
// State
|
|
||||||
isConnected: readonly(isConnected),
|
|
||||||
isConnecting: readonly(isConnecting),
|
|
||||||
connectionStatus: readonly(connectionStatus),
|
|
||||||
error: readonly(error),
|
|
||||||
activeSubscriptions: readonly(activeSubscriptions),
|
|
||||||
totalSubscriptionCount: readonly(totalSubscriptionCount),
|
|
||||||
subscriptionDetails: readonly(subscriptionDetails),
|
|
||||||
|
|
||||||
// Computed
|
|
||||||
connectionHealth: readonly(connectionHealth),
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
initialize,
|
|
||||||
connect,
|
|
||||||
disconnect,
|
|
||||||
subscribe,
|
|
||||||
publishEvent,
|
|
||||||
queryEvents,
|
|
||||||
|
|
||||||
// Internal
|
|
||||||
cleanup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,314 +0,0 @@
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import { nip04 } from 'nostr-tools'
|
|
||||||
import { relayHubComposable } from './useRelayHub'
|
|
||||||
import { useAuth } from './useAuth'
|
|
||||||
import { useMarketStore } from '@/stores/market'
|
|
||||||
import { decode } from 'light-bolt11-decoder'
|
|
||||||
|
|
||||||
// Nostrmarket Order interfaces based on the actual implementation
|
|
||||||
|
|
||||||
// Nostrmarket Order interfaces based on the actual implementation
|
|
||||||
interface OrderItem {
|
|
||||||
product_id: string
|
|
||||||
quantity: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OrderContact {
|
|
||||||
nostr?: string
|
|
||||||
phone?: string
|
|
||||||
email?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Direct message types from nostrmarket
|
|
||||||
enum DirectMessageType {
|
|
||||||
PLAIN_TEXT = -1,
|
|
||||||
CUSTOMER_ORDER = 0,
|
|
||||||
PAYMENT_REQUEST = 1,
|
|
||||||
ORDER_PAID_OR_SHIPPED = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event types for nostrmarket protocol
|
|
||||||
interface CustomerOrderEvent {
|
|
||||||
type: DirectMessageType.CUSTOMER_ORDER
|
|
||||||
id: string
|
|
||||||
items: OrderItem[]
|
|
||||||
contact?: OrderContact
|
|
||||||
shipping_id: string
|
|
||||||
message?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PaymentRequestEvent {
|
|
||||||
type: DirectMessageType.PAYMENT_REQUEST
|
|
||||||
id: string
|
|
||||||
message?: string
|
|
||||||
payment_options: Array<{
|
|
||||||
type: string
|
|
||||||
link: string
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OrderStatusEvent {
|
|
||||||
type: DirectMessageType.ORDER_PAID_OR_SHIPPED
|
|
||||||
id: string
|
|
||||||
message?: string
|
|
||||||
paid?: boolean
|
|
||||||
shipped?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to extract expiry from bolt11 invoice
|
|
||||||
function extractExpiryFromBolt11(bolt11String: string): string | undefined {
|
|
||||||
try {
|
|
||||||
const decoded = decode(bolt11String)
|
|
||||||
console.log('Decoded bolt11 invoice:', {
|
|
||||||
amount: decoded.sections.find(section => section.name === 'amount')?.value,
|
|
||||||
expiry: decoded.expiry,
|
|
||||||
timestamp: decoded.sections.find(section => section.name === 'timestamp')?.value
|
|
||||||
})
|
|
||||||
|
|
||||||
// Calculate expiry date from timestamp + expiry seconds
|
|
||||||
const timestamp = decoded.sections.find(section => section.name === 'timestamp')?.value as number
|
|
||||||
const expirySeconds = decoded.expiry as number
|
|
||||||
|
|
||||||
if (timestamp && expirySeconds) {
|
|
||||||
const expiryDate = new Date((timestamp + expirySeconds) * 1000)
|
|
||||||
return expiryDate.toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to extract expiry from bolt11:', error)
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useOrderEvents() {
|
|
||||||
const relayHub = relayHubComposable
|
|
||||||
const auth = useAuth()
|
|
||||||
const marketStore = useMarketStore()
|
|
||||||
|
|
||||||
// State
|
|
||||||
const isSubscribed = ref(false)
|
|
||||||
const lastEventTimestamp = ref(0)
|
|
||||||
const processedEventIds = ref(new Set<string>())
|
|
||||||
const subscriptionId = ref<string | null>(null)
|
|
||||||
|
|
||||||
// Computed
|
|
||||||
const currentUserPubkey = computed(() => auth.currentUser?.value?.pubkey)
|
|
||||||
const isReady = computed(() => {
|
|
||||||
const isAuth = auth.isAuthenticated
|
|
||||||
const isConnected = relayHub.isConnected.value
|
|
||||||
const hasPubkey = !!currentUserPubkey.value
|
|
||||||
|
|
||||||
return isAuth && isConnected && hasPubkey
|
|
||||||
})
|
|
||||||
|
|
||||||
// Subscribe to order events
|
|
||||||
const subscribeToOrderEvents = async () => {
|
|
||||||
if (!isReady.value || isSubscribed.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Subscribe to direct messages (kind 4) that contain order information
|
|
||||||
const filters = [
|
|
||||||
{
|
|
||||||
kinds: [4], // NIP-04 encrypted direct messages
|
|
||||||
'#p': [currentUserPubkey.value].filter(Boolean) as string[],
|
|
||||||
since: lastEventTimestamp.value
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
relayHub.subscribe({
|
|
||||||
id: 'order-events',
|
|
||||||
filters,
|
|
||||||
onEvent: handleOrderEvent,
|
|
||||||
onEose: () => {
|
|
||||||
console.log('Order events subscription ended')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
subscriptionId.value = 'order-events'
|
|
||||||
isSubscribed.value = true
|
|
||||||
console.log('Successfully subscribed to order events')
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to subscribe to order events:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle incoming order events
|
|
||||||
const handleOrderEvent = async (event: any) => {
|
|
||||||
if (processedEventIds.value.has(event.id)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
processedEventIds.value.add(event.id)
|
|
||||||
lastEventTimestamp.value = Math.max(lastEventTimestamp.value, event.created_at)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Decrypt the message content
|
|
||||||
const decryptedContent = await nip04.decrypt(
|
|
||||||
auth.currentUser.value?.prvkey || '',
|
|
||||||
event.pubkey,
|
|
||||||
event.content
|
|
||||||
)
|
|
||||||
|
|
||||||
// Parse the JSON content
|
|
||||||
const jsonData = JSON.parse(decryptedContent)
|
|
||||||
|
|
||||||
// Handle different message types
|
|
||||||
switch (jsonData.type) {
|
|
||||||
case DirectMessageType.CUSTOMER_ORDER:
|
|
||||||
await handleCustomerOrder(jsonData as CustomerOrderEvent, event.pubkey)
|
|
||||||
break
|
|
||||||
case DirectMessageType.PAYMENT_REQUEST:
|
|
||||||
await handlePaymentRequest(jsonData as PaymentRequestEvent, event.pubkey)
|
|
||||||
break
|
|
||||||
case DirectMessageType.ORDER_PAID_OR_SHIPPED:
|
|
||||||
await handleOrderStatusUpdate(jsonData as OrderStatusEvent, event.pubkey)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
console.log('Unknown message type:', jsonData.type)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing order event:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle customer order (type 0)
|
|
||||||
const handleCustomerOrder = async (orderData: CustomerOrderEvent, _senderPubkey: string) => {
|
|
||||||
console.log('Received customer order:', orderData)
|
|
||||||
|
|
||||||
// Create a basic order object from the event data
|
|
||||||
const order = {
|
|
||||||
id: orderData.id,
|
|
||||||
type: DirectMessageType.CUSTOMER_ORDER,
|
|
||||||
items: orderData.items,
|
|
||||||
contact: orderData.contact,
|
|
||||||
shipping_id: orderData.shipping_id,
|
|
||||||
message: orderData.message,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
updatedAt: Date.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the order in our local state
|
|
||||||
// Note: We're not using the complex Order interface from market store
|
|
||||||
// Instead, we're using the simple nostrmarket format
|
|
||||||
console.log('Processed customer order:', order)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle payment request (type 1)
|
|
||||||
const handlePaymentRequest = async (paymentData: PaymentRequestEvent, _senderPubkey: string) => {
|
|
||||||
console.log('Received payment request:', paymentData)
|
|
||||||
|
|
||||||
// Find the lightning payment option
|
|
||||||
const lightningOption = paymentData.payment_options?.find(opt => opt.type === 'ln')
|
|
||||||
if (lightningOption) {
|
|
||||||
console.log('Lightning payment request:', lightningOption.link)
|
|
||||||
|
|
||||||
// Find the existing order by ID
|
|
||||||
const existingOrder = marketStore.orders[paymentData.id]
|
|
||||||
if (existingOrder) {
|
|
||||||
console.log('Found existing order, updating with payment request:', existingOrder.id)
|
|
||||||
|
|
||||||
// Try to extract actual expiry from bolt11
|
|
||||||
const actualExpiry = extractExpiryFromBolt11(lightningOption.link)
|
|
||||||
|
|
||||||
// Create lightning invoice object
|
|
||||||
const lightningInvoice = {
|
|
||||||
checking_id: '', // Will be extracted from bolt11 if needed
|
|
||||||
payment_hash: '', // Will be extracted from bolt11 if needed
|
|
||||||
wallet_id: '', // Not available from payment request
|
|
||||||
amount: existingOrder.total,
|
|
||||||
fee: 0, // Not available from payment request
|
|
||||||
bolt11: lightningOption.link,
|
|
||||||
status: 'pending',
|
|
||||||
memo: paymentData.message || 'Payment for order',
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
expiry: actualExpiry // Use actual expiry from bolt11 decoding
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the order with the lightning invoice
|
|
||||||
marketStore.updateOrder(existingOrder.id, {
|
|
||||||
lightningInvoice,
|
|
||||||
status: 'pending',
|
|
||||||
paymentRequest: lightningOption.link,
|
|
||||||
updatedAt: Date.now()
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('Order updated with payment request:', existingOrder.id)
|
|
||||||
} else {
|
|
||||||
console.warn('Order not found for payment request:', paymentData.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle order status update (type 2)
|
|
||||||
const handleOrderStatusUpdate = async (statusData: OrderStatusEvent, _senderPubkey: string) => {
|
|
||||||
console.log('Received order status update:', statusData)
|
|
||||||
|
|
||||||
// Update order status in local state
|
|
||||||
if (statusData.paid !== undefined) {
|
|
||||||
console.log(`Order ${statusData.id} payment status: ${statusData.paid}`)
|
|
||||||
}
|
|
||||||
if (statusData.shipped !== undefined) {
|
|
||||||
console.log(`Order ${statusData.id} shipping status: ${statusData.shipped}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unsubscribe from order events
|
|
||||||
const unsubscribeFromOrderEvents = () => {
|
|
||||||
if (subscriptionId.value) {
|
|
||||||
relayHub.cleanup()
|
|
||||||
subscriptionId.value = null
|
|
||||||
}
|
|
||||||
isSubscribed.value = false
|
|
||||||
console.log('Unsubscribed from order events')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch for ready state changes
|
|
||||||
const watchReadyState = () => {
|
|
||||||
if (isReady.value && !isSubscribed.value) {
|
|
||||||
subscribeToOrderEvents()
|
|
||||||
} else if (!isReady.value && isSubscribed.value) {
|
|
||||||
unsubscribeFromOrderEvents()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch for authentication changes
|
|
||||||
const watchAuthChanges = () => {
|
|
||||||
if (auth.isAuthenticated && relayHub.isConnected.value) {
|
|
||||||
subscribeToOrderEvents()
|
|
||||||
} else {
|
|
||||||
unsubscribeFromOrderEvents()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize subscription when ready
|
|
||||||
const initialize = () => {
|
|
||||||
if (isReady.value) {
|
|
||||||
subscribeToOrderEvents()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
const cleanup = () => {
|
|
||||||
unsubscribeFromOrderEvents()
|
|
||||||
processedEventIds.value.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
// State
|
|
||||||
isSubscribed: computed(() => isSubscribed.value),
|
|
||||||
isReady: computed(() => isReady.value),
|
|
||||||
lastEventTimestamp: computed(() => lastEventTimestamp.value),
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
subscribeToOrderEvents,
|
|
||||||
unsubscribeFromOrderEvents,
|
|
||||||
initialize,
|
|
||||||
cleanup,
|
|
||||||
watchReadyState,
|
|
||||||
watchAuthChanges
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,283 +0,0 @@
|
||||||
import { ref, computed, onMounted, onUnmounted, readonly } from 'vue'
|
|
||||||
import { relayHub, type SubscriptionConfig, type RelayStatus } from '../lib/nostr/relayHub'
|
|
||||||
import { config } from '../lib/config'
|
|
||||||
|
|
||||||
export function useRelayHub() {
|
|
||||||
// Reactive state
|
|
||||||
const isConnected = ref(false)
|
|
||||||
const connectionStatus = ref<'connecting' | 'connected' | 'disconnected' | 'error'>('disconnected')
|
|
||||||
const relayStatuses = ref<RelayStatus[]>([])
|
|
||||||
const error = ref<Error | null>(null)
|
|
||||||
const activeSubscriptions = ref<Set<string>>(new Set())
|
|
||||||
|
|
||||||
// Reactive relay counts - these will be updated when relayHub state changes
|
|
||||||
const connectedRelayCount = ref(0)
|
|
||||||
const totalRelayCount = ref(0)
|
|
||||||
const totalSubscriptionCount = ref(0)
|
|
||||||
|
|
||||||
// Reactive subscription details
|
|
||||||
const subscriptionDetails = ref<Array<{ id: string; filters: any[]; relays?: string[] }>>([])
|
|
||||||
|
|
||||||
// Computed properties
|
|
||||||
const connectionHealth = computed(() => {
|
|
||||||
if (totalRelayCount.value === 0) return 0
|
|
||||||
return (connectedRelayCount.value / totalRelayCount.value) * 100
|
|
||||||
})
|
|
||||||
|
|
||||||
// Initialize relay hub
|
|
||||||
const initialize = async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
connectionStatus.value = 'connecting'
|
|
||||||
error.value = null
|
|
||||||
|
|
||||||
// Get relay URLs from config
|
|
||||||
const relayUrls = config.market.supportedRelays
|
|
||||||
console.log('🔧 RelayHub: Initializing with relay URLs:', relayUrls)
|
|
||||||
|
|
||||||
if (!relayUrls || relayUrls.length === 0) {
|
|
||||||
throw new Error('No relay URLs configured')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the relay hub
|
|
||||||
console.log('🔧 RelayHub: Calling relayHub.initialize...')
|
|
||||||
await relayHub.initialize(relayUrls)
|
|
||||||
console.log('🔧 RelayHub: Initialization successful')
|
|
||||||
|
|
||||||
// Set up event listeners
|
|
||||||
setupEventListeners()
|
|
||||||
|
|
||||||
connectionStatus.value = 'connected'
|
|
||||||
isConnected.value = true
|
|
||||||
console.log('🔧 RelayHub: Connection status set to connected')
|
|
||||||
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
const errorObj = err instanceof Error ? err : new Error('Failed to initialize RelayHub')
|
|
||||||
error.value = errorObj
|
|
||||||
connectionStatus.value = 'error'
|
|
||||||
isConnected.value = false
|
|
||||||
console.error('🔧 RelayHub: Failed to initialize RelayHub:', errorObj)
|
|
||||||
throw errorObj
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to relays
|
|
||||||
const connect = async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
if (!relayHub.isInitialized) {
|
|
||||||
await initialize()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
connectionStatus.value = 'connecting'
|
|
||||||
error.value = null
|
|
||||||
|
|
||||||
await relayHub.connect()
|
|
||||||
|
|
||||||
connectionStatus.value = 'connected'
|
|
||||||
isConnected.value = true
|
|
||||||
} catch (err) {
|
|
||||||
const errorObj = err instanceof Error ? err : new Error('Failed to connect')
|
|
||||||
error.value = errorObj
|
|
||||||
connectionStatus.value = 'error'
|
|
||||||
isConnected.value = false
|
|
||||||
throw errorObj
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disconnect from relays
|
|
||||||
const disconnect = (): void => {
|
|
||||||
relayHub.disconnect()
|
|
||||||
connectionStatus.value = 'disconnected'
|
|
||||||
isConnected.value = false
|
|
||||||
error.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe to events
|
|
||||||
const subscribe = (config: SubscriptionConfig): (() => void) => {
|
|
||||||
try {
|
|
||||||
const unsubscribe = relayHub.subscribe(config)
|
|
||||||
activeSubscriptions.value.add(config.id)
|
|
||||||
|
|
||||||
// Return enhanced unsubscribe function
|
|
||||||
return () => {
|
|
||||||
unsubscribe()
|
|
||||||
activeSubscriptions.value.delete(config.id)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
const errorObj = err instanceof Error ? err : new Error('Failed to subscribe')
|
|
||||||
error.value = errorObj
|
|
||||||
throw errorObj
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish an event
|
|
||||||
const publishEvent = async (event: any): Promise<{ success: number; total: number }> => {
|
|
||||||
try {
|
|
||||||
return await relayHub.publishEvent(event)
|
|
||||||
} catch (err) {
|
|
||||||
const errorObj = err instanceof Error ? err : new Error('Failed to publish event')
|
|
||||||
error.value = errorObj
|
|
||||||
throw errorObj
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query events (one-time fetch)
|
|
||||||
const queryEvents = async (filters: any[], relays?: string[]): Promise<any[]> => {
|
|
||||||
try {
|
|
||||||
return await relayHub.queryEvents(filters, relays)
|
|
||||||
} catch (err) {
|
|
||||||
const errorObj = err instanceof Error ? err : new Error('Failed to query events')
|
|
||||||
error.value = errorObj
|
|
||||||
throw errorObj
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get relay status
|
|
||||||
const getRelayStatus = (url: string): RelayStatus | undefined => {
|
|
||||||
return relayStatuses.value.find(status => status.url === url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up event listeners for relay hub events
|
|
||||||
const setupEventListeners = (): void => {
|
|
||||||
relayHub.on('connected', (count: number) => {
|
|
||||||
|
|
||||||
isConnected.value = true
|
|
||||||
connectionStatus.value = 'connected'
|
|
||||||
error.value = null
|
|
||||||
connectedRelayCount.value = count
|
|
||||||
totalRelayCount.value = relayHub.totalRelayCount
|
|
||||||
totalSubscriptionCount.value = relayHub.totalSubscriptionCount
|
|
||||||
})
|
|
||||||
|
|
||||||
relayHub.on('disconnected', () => {
|
|
||||||
|
|
||||||
isConnected.value = false
|
|
||||||
connectionStatus.value = 'disconnected'
|
|
||||||
error.value = null
|
|
||||||
connectedRelayCount.value = 0
|
|
||||||
totalSubscriptionCount.value = 0
|
|
||||||
})
|
|
||||||
|
|
||||||
relayHub.on('connectionError', (err: Error) => {
|
|
||||||
console.error('Connection error:', err)
|
|
||||||
error.value = err
|
|
||||||
connectionStatus.value = 'error'
|
|
||||||
isConnected.value = false
|
|
||||||
connectedRelayCount.value = 0
|
|
||||||
})
|
|
||||||
|
|
||||||
relayHub.on('allRelaysDisconnected', () => {
|
|
||||||
console.warn('All relays disconnected')
|
|
||||||
isConnected.value = false
|
|
||||||
connectionStatus.value = 'disconnected'
|
|
||||||
connectedRelayCount.value = 0
|
|
||||||
})
|
|
||||||
|
|
||||||
relayHub.on('partialDisconnection', ({ connected, total }: { connected: number; total: number }) => {
|
|
||||||
console.warn(`Partial disconnection: ${connected}/${total} relays connected`)
|
|
||||||
isConnected.value = connected > 0
|
|
||||||
connectionStatus.value = connected > 0 ? 'connected' : 'disconnected'
|
|
||||||
connectedRelayCount.value = connected
|
|
||||||
totalRelayCount.value = total
|
|
||||||
})
|
|
||||||
|
|
||||||
relayHub.on('maxReconnectAttemptsReached', () => {
|
|
||||||
console.error('Max reconnection attempts reached')
|
|
||||||
connectionStatus.value = 'error'
|
|
||||||
isConnected.value = false
|
|
||||||
error.value = new Error('Max reconnection attempts reached')
|
|
||||||
connectedRelayCount.value = 0
|
|
||||||
})
|
|
||||||
|
|
||||||
relayHub.on('networkOffline', () => {
|
|
||||||
console.log('Network went offline')
|
|
||||||
connectionStatus.value = 'disconnected'
|
|
||||||
isConnected.value = false
|
|
||||||
connectedRelayCount.value = 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// Subscription events
|
|
||||||
relayHub.on('subscriptionCreated', ({ count }: { id: string; count: number }) => {
|
|
||||||
|
|
||||||
totalSubscriptionCount.value = count
|
|
||||||
})
|
|
||||||
|
|
||||||
relayHub.on('subscriptionRemoved', ({ count }: { id: string; count: number }) => {
|
|
||||||
|
|
||||||
totalSubscriptionCount.value = count
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update relay statuses periodically
|
|
||||||
const updateRelayStatuses = () => {
|
|
||||||
relayStatuses.value = relayHub.relayStatuses
|
|
||||||
// Also update the reactive counts to keep them in sync
|
|
||||||
connectedRelayCount.value = relayHub.connectedRelayCount
|
|
||||||
totalRelayCount.value = relayHub.totalRelayCount
|
|
||||||
totalSubscriptionCount.value = relayHub.totalSubscriptionCount
|
|
||||||
subscriptionDetails.value = relayHub.subscriptionDetails
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update immediately and then every 10 seconds
|
|
||||||
updateRelayStatuses()
|
|
||||||
const statusInterval = setInterval(updateRelayStatuses, 10000)
|
|
||||||
|
|
||||||
// Cleanup interval on unmount
|
|
||||||
onUnmounted(() => {
|
|
||||||
clearInterval(statusInterval)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup function
|
|
||||||
const cleanup = (): void => {
|
|
||||||
// Close all active subscriptions
|
|
||||||
activeSubscriptions.value.forEach(subId => {
|
|
||||||
relayHub.unsubscribe(subId)
|
|
||||||
})
|
|
||||||
activeSubscriptions.value.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-initialize on mount if config is available
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
if (config.nostr.relays && config.nostr.relays.length > 0) {
|
|
||||||
await initialize()
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('Auto-initialization failed:', err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Cleanup on unmount
|
|
||||||
onUnmounted(() => {
|
|
||||||
cleanup()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
// State
|
|
||||||
isConnected: readonly(isConnected),
|
|
||||||
connectionStatus: readonly(connectionStatus),
|
|
||||||
relayStatuses: readonly(relayStatuses),
|
|
||||||
error: readonly(error),
|
|
||||||
activeSubscriptions: readonly(activeSubscriptions),
|
|
||||||
connectedRelayCount: readonly(connectedRelayCount),
|
|
||||||
totalRelayCount: readonly(totalRelayCount),
|
|
||||||
totalSubscriptionCount: readonly(totalSubscriptionCount),
|
|
||||||
subscriptionDetails: readonly(subscriptionDetails),
|
|
||||||
connectionHealth: readonly(connectionHealth),
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
initialize,
|
|
||||||
connect,
|
|
||||||
disconnect,
|
|
||||||
subscribe,
|
|
||||||
publishEvent,
|
|
||||||
queryEvents,
|
|
||||||
getRelayStatus,
|
|
||||||
getConnectionHealth: connectionHealth,
|
|
||||||
cleanup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export singleton instance for global state
|
|
||||||
export const relayHubComposable = useRelayHub()
|
|
||||||
|
|
@ -1,242 +0,0 @@
|
||||||
import { ref, computed, onUnmounted } from 'vue'
|
|
||||||
import { purchaseTicket, checkPaymentStatus, payInvoiceWithWallet } from '@/lib/api/events'
|
|
||||||
import { useAuth } from './useAuth'
|
|
||||||
import { toast } from 'vue-sonner'
|
|
||||||
|
|
||||||
export function useTicketPurchase() {
|
|
||||||
const { isAuthenticated, currentUser } = useAuth()
|
|
||||||
|
|
||||||
// State
|
|
||||||
const isLoading = ref(false)
|
|
||||||
const error = ref<string | null>(null)
|
|
||||||
const paymentHash = ref<string | null>(null)
|
|
||||||
const paymentRequest = ref<string | null>(null)
|
|
||||||
const qrCode = ref<string | null>(null)
|
|
||||||
const isPaymentPending = ref(false)
|
|
||||||
const isPayingWithWallet = ref(false)
|
|
||||||
|
|
||||||
// Ticket QR code state
|
|
||||||
const ticketQRCode = ref<string | null>(null)
|
|
||||||
const purchasedTicketId = ref<string | null>(null)
|
|
||||||
const showTicketQR = ref(false)
|
|
||||||
|
|
||||||
// Computed properties
|
|
||||||
const canPurchase = computed(() => isAuthenticated.value && currentUser.value)
|
|
||||||
const userDisplay = computed(() => {
|
|
||||||
if (!currentUser.value) return null
|
|
||||||
return {
|
|
||||||
name: currentUser.value.username || currentUser.value.id,
|
|
||||||
shortId: currentUser.value.id.slice(0, 8)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const userWallets = computed(() => currentUser.value?.wallets || [])
|
|
||||||
const hasWalletWithBalance = computed(() =>
|
|
||||||
userWallets.value.some((wallet: any) => wallet.balance_msat > 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Generate QR code for Lightning payment
|
|
||||||
async function generateQRCode(bolt11: string) {
|
|
||||||
try {
|
|
||||||
const qrcode = await import('qrcode')
|
|
||||||
const dataUrl = await qrcode.toDataURL(bolt11, {
|
|
||||||
width: 256,
|
|
||||||
margin: 2,
|
|
||||||
color: {
|
|
||||||
dark: '#000000',
|
|
||||||
light: '#FFFFFF'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
qrCode.value = dataUrl
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error generating QR code:', err)
|
|
||||||
error.value = 'Failed to generate QR code'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate QR code for ticket
|
|
||||||
async function generateTicketQRCode(ticketId: string) {
|
|
||||||
try {
|
|
||||||
const qrcode = await import('qrcode')
|
|
||||||
const ticketUrl = `ticket://${ticketId}`
|
|
||||||
const dataUrl = await qrcode.toDataURL(ticketUrl, {
|
|
||||||
width: 128,
|
|
||||||
margin: 2,
|
|
||||||
color: {
|
|
||||||
dark: '#000000',
|
|
||||||
light: '#FFFFFF'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
ticketQRCode.value = dataUrl
|
|
||||||
return dataUrl
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating ticket QR code:', error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pay with wallet
|
|
||||||
async function payWithWallet(paymentRequest: string) {
|
|
||||||
const walletWithBalance = userWallets.value.find((wallet: any) => wallet.balance_msat > 0)
|
|
||||||
|
|
||||||
if (!walletWithBalance) {
|
|
||||||
throw new Error('No wallet with sufficient balance found')
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await payInvoiceWithWallet(paymentRequest, walletWithBalance.id, walletWithBalance.adminkey)
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Wallet payment failed:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Purchase ticket for event
|
|
||||||
async function purchaseTicketForEvent(eventId: string) {
|
|
||||||
if (!canPurchase.value) {
|
|
||||||
throw new Error('User must be authenticated to purchase tickets')
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true
|
|
||||||
error.value = null
|
|
||||||
paymentHash.value = null
|
|
||||||
paymentRequest.value = null
|
|
||||||
qrCode.value = null
|
|
||||||
ticketQRCode.value = null
|
|
||||||
purchasedTicketId.value = null
|
|
||||||
showTicketQR.value = false
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get the invoice
|
|
||||||
const invoice = await purchaseTicket(eventId)
|
|
||||||
paymentHash.value = invoice.payment_hash
|
|
||||||
paymentRequest.value = invoice.payment_request
|
|
||||||
|
|
||||||
// Generate QR code for payment
|
|
||||||
await generateQRCode(invoice.payment_request)
|
|
||||||
|
|
||||||
// Try to pay with wallet if available
|
|
||||||
if (hasWalletWithBalance.value) {
|
|
||||||
isPayingWithWallet.value = true
|
|
||||||
try {
|
|
||||||
await payWithWallet(invoice.payment_request)
|
|
||||||
// If wallet payment succeeds, proceed to check payment status
|
|
||||||
await startPaymentStatusCheck(eventId, invoice.payment_hash)
|
|
||||||
} catch (walletError) {
|
|
||||||
// If wallet payment fails, fall back to manual payment
|
|
||||||
console.log('Wallet payment failed, falling back to manual payment:', walletError)
|
|
||||||
isPayingWithWallet.value = false
|
|
||||||
await startPaymentStatusCheck(eventId, invoice.payment_hash)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No wallet balance, proceed with manual payment
|
|
||||||
await startPaymentStatusCheck(eventId, invoice.payment_hash)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
error.value = err instanceof Error ? err.message : 'Failed to purchase ticket'
|
|
||||||
console.error('Error purchasing ticket:', err)
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start payment status check
|
|
||||||
async function startPaymentStatusCheck(eventId: string, hash: string) {
|
|
||||||
isPaymentPending.value = true
|
|
||||||
let checkInterval: number | null = null
|
|
||||||
|
|
||||||
const checkPayment = async () => {
|
|
||||||
try {
|
|
||||||
const result = await checkPaymentStatus(eventId, hash)
|
|
||||||
|
|
||||||
if (result.paid) {
|
|
||||||
isPaymentPending.value = false
|
|
||||||
if (checkInterval) {
|
|
||||||
clearInterval(checkInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate ticket QR code
|
|
||||||
if (result.ticket_id) {
|
|
||||||
purchasedTicketId.value = result.ticket_id
|
|
||||||
await generateTicketQRCode(result.ticket_id)
|
|
||||||
showTicketQR.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success('Ticket purchased successfully!')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error checking payment status:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check immediately
|
|
||||||
await checkPayment()
|
|
||||||
|
|
||||||
// Then check every 2 seconds
|
|
||||||
checkInterval = setInterval(checkPayment, 2000) as unknown as number
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop payment status check
|
|
||||||
function stopPaymentStatusCheck() {
|
|
||||||
isPaymentPending.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset payment state
|
|
||||||
function resetPaymentState() {
|
|
||||||
isLoading.value = false
|
|
||||||
error.value = null
|
|
||||||
paymentHash.value = null
|
|
||||||
paymentRequest.value = null
|
|
||||||
qrCode.value = null
|
|
||||||
isPaymentPending.value = false
|
|
||||||
isPayingWithWallet.value = false
|
|
||||||
ticketQRCode.value = null
|
|
||||||
purchasedTicketId.value = null
|
|
||||||
showTicketQR.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open Lightning wallet
|
|
||||||
function handleOpenLightningWallet() {
|
|
||||||
if (paymentRequest.value) {
|
|
||||||
window.open(`lightning:${paymentRequest.value}`, '_blank')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup function
|
|
||||||
function cleanup() {
|
|
||||||
stopPaymentStatusCheck()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
onUnmounted(() => {
|
|
||||||
cleanup()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
// State
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
paymentHash,
|
|
||||||
paymentRequest,
|
|
||||||
qrCode,
|
|
||||||
isPaymentPending,
|
|
||||||
isPayingWithWallet,
|
|
||||||
ticketQRCode,
|
|
||||||
purchasedTicketId,
|
|
||||||
showTicketQR,
|
|
||||||
|
|
||||||
// Computed
|
|
||||||
canPurchase,
|
|
||||||
userDisplay,
|
|
||||||
userWallets,
|
|
||||||
hasWalletWithBalance,
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
purchaseTicketForEvent,
|
|
||||||
handleOpenLightningWallet,
|
|
||||||
resetPaymentState,
|
|
||||||
cleanup,
|
|
||||||
generateTicketQRCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
import { computed } from 'vue'
|
|
||||||
import { useAsyncState } from '@vueuse/core'
|
|
||||||
import type { Ticket } from '@/lib/types/event'
|
|
||||||
import { fetchUserTickets } from '@/lib/api/events'
|
|
||||||
import { useAuth } from './useAuth'
|
|
||||||
|
|
||||||
interface GroupedTickets {
|
|
||||||
eventId: string
|
|
||||||
tickets: Ticket[]
|
|
||||||
paidCount: number
|
|
||||||
pendingCount: number
|
|
||||||
registeredCount: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUserTickets() {
|
|
||||||
const { isAuthenticated, currentUser } = useAuth()
|
|
||||||
|
|
||||||
const { state: tickets, isLoading, error: asyncError, execute: refresh } = useAsyncState(
|
|
||||||
async () => {
|
|
||||||
if (!isAuthenticated.value || !currentUser.value) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return await fetchUserTickets(currentUser.value.id)
|
|
||||||
},
|
|
||||||
[] as Ticket[],
|
|
||||||
{
|
|
||||||
immediate: false,
|
|
||||||
resetOnExecute: false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const error = computed(() => {
|
|
||||||
if (asyncError.value) {
|
|
||||||
return {
|
|
||||||
message: asyncError.value instanceof Error
|
|
||||||
? asyncError.value.message
|
|
||||||
: 'An error occurred while fetching tickets'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
const sortedTickets = computed(() => {
|
|
||||||
return [...tickets.value].sort((a, b) =>
|
|
||||||
new Date(b.time).getTime() - new Date(a.time).getTime()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const paidTickets = computed(() => {
|
|
||||||
return sortedTickets.value.filter(ticket => ticket.paid)
|
|
||||||
})
|
|
||||||
|
|
||||||
const pendingTickets = computed(() => {
|
|
||||||
return sortedTickets.value.filter(ticket => !ticket.paid)
|
|
||||||
})
|
|
||||||
|
|
||||||
const registeredTickets = computed(() => {
|
|
||||||
return sortedTickets.value.filter(ticket => ticket.registered)
|
|
||||||
})
|
|
||||||
|
|
||||||
const unregisteredTickets = computed(() => {
|
|
||||||
return sortedTickets.value.filter(ticket => ticket.paid && !ticket.registered)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Group tickets by event
|
|
||||||
const groupedTickets = computed(() => {
|
|
||||||
const groups = new Map<string, GroupedTickets>()
|
|
||||||
|
|
||||||
sortedTickets.value.forEach(ticket => {
|
|
||||||
if (!groups.has(ticket.event)) {
|
|
||||||
groups.set(ticket.event, {
|
|
||||||
eventId: ticket.event,
|
|
||||||
tickets: [],
|
|
||||||
paidCount: 0,
|
|
||||||
pendingCount: 0,
|
|
||||||
registeredCount: 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const group = groups.get(ticket.event)!
|
|
||||||
group.tickets.push(ticket)
|
|
||||||
|
|
||||||
if (ticket.paid) {
|
|
||||||
group.paidCount++
|
|
||||||
} else {
|
|
||||||
group.pendingCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ticket.registered) {
|
|
||||||
group.registeredCount++
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Convert to array and sort by most recent ticket in each group
|
|
||||||
return Array.from(groups.values()).sort((a, b) => {
|
|
||||||
const aLatest = Math.max(...a.tickets.map(t => new Date(t.time).getTime()))
|
|
||||||
const bLatest = Math.max(...b.tickets.map(t => new Date(t.time).getTime()))
|
|
||||||
return bLatest - aLatest
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Load tickets when authenticated
|
|
||||||
const loadTickets = async () => {
|
|
||||||
if (isAuthenticated.value && currentUser.value) {
|
|
||||||
await refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
// State
|
|
||||||
tickets: sortedTickets,
|
|
||||||
paidTickets,
|
|
||||||
pendingTickets,
|
|
||||||
registeredTickets,
|
|
||||||
unregisteredTickets,
|
|
||||||
groupedTickets,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
refresh: loadTickets,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -18,7 +18,6 @@ interface PushConfig {
|
||||||
|
|
||||||
interface MarketConfig {
|
interface MarketConfig {
|
||||||
defaultNaddr: string
|
defaultNaddr: string
|
||||||
supportedRelays: string[]
|
|
||||||
lightningEnabled: boolean
|
lightningEnabled: boolean
|
||||||
defaultCurrency: string
|
defaultCurrency: string
|
||||||
}
|
}
|
||||||
|
|
@ -65,14 +64,6 @@ export const config: AppConfig = {
|
||||||
},
|
},
|
||||||
market: {
|
market: {
|
||||||
defaultNaddr: import.meta.env.VITE_MARKET_NADDR || '',
|
defaultNaddr: import.meta.env.VITE_MARKET_NADDR || '',
|
||||||
supportedRelays: parseJsonEnv(import.meta.env.VITE_MARKET_RELAYS, [
|
|
||||||
'ws://127.0.0.1:7777',
|
|
||||||
'wss://relay.damus.io',
|
|
||||||
'wss://relay.snort.social',
|
|
||||||
'wss://nostr-pub.wellorder.net',
|
|
||||||
'wss://nostr.zebedee.cloud',
|
|
||||||
'wss://nostr.walletofsatoshi.com'
|
|
||||||
]),
|
|
||||||
lightningEnabled: Boolean(import.meta.env.VITE_LIGHTNING_ENABLED),
|
lightningEnabled: Boolean(import.meta.env.VITE_LIGHTNING_ENABLED),
|
||||||
defaultCurrency: import.meta.env.VITE_MARKET_DEFAULT_CURRENCY || 'sat'
|
defaultCurrency: import.meta.env.VITE_MARKET_DEFAULT_CURRENCY || 'sat'
|
||||||
},
|
},
|
||||||
|
|
@ -119,7 +110,8 @@ export const configUtils = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getMarketRelays: (): string[] => {
|
getMarketRelays: (): string[] => {
|
||||||
return config.market.supportedRelays
|
// Market now uses the same relays as the main Nostr configuration
|
||||||
|
return config.nostr.relays
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -375,7 +375,7 @@ import { Input } from '@/components/ui/input'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||||
import { nostrChat } from '@/composables/useNostrChat'
|
import { useChat } from '../composables/useChat'
|
||||||
|
|
||||||
import { useFuzzySearch } from '@/composables/useFuzzySearch'
|
import { useFuzzySearch } from '@/composables/useFuzzySearch'
|
||||||
|
|
||||||
|
|
@ -386,8 +386,11 @@ interface Peer {
|
||||||
pubkey: string
|
pubkey: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize chat composable
|
||||||
|
const chat = useChat()
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const peers = computed(() => nostrChat.peers.value)
|
const peers = computed(() => chat.peers.value)
|
||||||
const selectedPeer = ref<Peer | null>(null)
|
const selectedPeer = ref<Peer | null>(null)
|
||||||
const messageInput = ref('')
|
const messageInput = ref('')
|
||||||
|
|
||||||
|
|
@ -400,20 +403,30 @@ const scrollTarget = ref<HTMLElement | null>(null)
|
||||||
// Mobile detection
|
// Mobile detection
|
||||||
const isMobile = ref(false)
|
const isMobile = ref(false)
|
||||||
|
|
||||||
// Nostr chat composable (singleton)
|
// Get methods and state from chat composable
|
||||||
const {
|
// Note: The modular chat service handles connection and peer management automatically
|
||||||
isConnected,
|
const isConnected = computed(() => true) // Chat service manages connection
|
||||||
messages,
|
const messages = ref(new Map()) // Local messages map for compatibility
|
||||||
connect,
|
const totalUnreadCount = computed(() => chat.totalUnreadCount.value)
|
||||||
disconnect,
|
|
||||||
subscribeToPeer,
|
// Adapter functions for compatibility with existing code
|
||||||
sendMessage: sendNostrMessage,
|
const connect = async () => {} // Connection handled by chat service
|
||||||
onMessageAdded,
|
const disconnect = () => {} // Handled by chat service
|
||||||
markMessagesAsRead,
|
const subscribeToPeer = async (peer: string) => {} // Handled by chat service
|
||||||
getUnreadCount,
|
const sendNostrMessage = async (peer: string, content: string) => {
|
||||||
totalUnreadCount,
|
chat.selectPeer(peer)
|
||||||
getLatestMessageTimestamp
|
await chat.sendMessage(content)
|
||||||
} = nostrChat
|
}
|
||||||
|
const onMessageAdded = (callback: Function) => {} // Event handling via chat service
|
||||||
|
const markMessagesAsRead = (peer: string) => chat.markAsRead(peer)
|
||||||
|
const getUnreadCount = (peer: string) => {
|
||||||
|
const peerData = chat.peers.value.find(p => p.pubkey === peer)
|
||||||
|
return peerData?.unreadCount || 0
|
||||||
|
}
|
||||||
|
const getLatestMessageTimestamp = (peer: string) => {
|
||||||
|
const msgs = messages.value.get(peer) || []
|
||||||
|
return msgs.length > 0 ? msgs[msgs.length - 1].created_at : 0
|
||||||
|
}
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const currentMessages = computed(() => {
|
const currentMessages = computed(() => {
|
||||||
|
|
@ -498,7 +511,7 @@ const goBackToPeers = () => {
|
||||||
const refreshPeers = async () => {
|
const refreshPeers = async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
try {
|
try {
|
||||||
await nostrChat.loadPeers()
|
// Peers are loaded automatically by the chat service
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to refresh peers:', error)
|
console.error('Failed to refresh peers:', error)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -601,7 +614,7 @@ onMounted(async () => {
|
||||||
|
|
||||||
// If no peers loaded, load them
|
// If no peers loaded, load them
|
||||||
if (peers.value.length === 0) {
|
if (peers.value.length === 0) {
|
||||||
await nostrChat.loadPeers()
|
// Peers are loaded automatically by the chat service
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { onUnmounted } from 'vue'
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { useTicketPurchase } from '@/composables/useTicketPurchase'
|
import { useTicketPurchase } from '../composables/useTicketPurchase'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { User, Wallet, CreditCard, Zap, Ticket } from 'lucide-vue-next'
|
import { User, Wallet, CreditCard, Zap, Ticket } from 'lucide-vue-next'
|
||||||
import { formatEventPrice, formatWalletBalance } from '@/lib/utils/formatting'
|
import { formatEventPrice, formatWalletBalance } from '@/lib/utils/formatting'
|
||||||
|
|
|
||||||
|
|
@ -201,13 +201,14 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useOrderEvents } from '@/composables/useOrderEvents'
|
// import { useOrderEvents } from '@/composables/useOrderEvents' // TODO: Move to market module
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Plus, X } from 'lucide-vue-next'
|
import { Plus, X } from 'lucide-vue-next'
|
||||||
|
|
||||||
// const marketStore = useMarketStore()
|
// const marketStore = useMarketStore()
|
||||||
const orderEvents = useOrderEvents()
|
// const orderEvents = useOrderEvents() // TODO: Move to market module
|
||||||
|
const orderEvents = { isSubscribed: ref(false) } // Temporary mock
|
||||||
|
|
||||||
// Local state
|
// Local state
|
||||||
const activeSettingsTab = ref('store')
|
const activeSettingsTab = ref('store')
|
||||||
|
|
|
||||||
|
|
@ -231,8 +231,8 @@
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useMarketStore } from '@/stores/market'
|
import { useMarketStore } from '@/stores/market'
|
||||||
import { useOrderEvents } from '@/composables/useOrderEvents'
|
// import { useOrderEvents } from '@/composables/useOrderEvents' // TODO: Move to market module
|
||||||
import { relayHubComposable } from '@/composables/useRelayHub'
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
import { auth } from '@/composables/useAuth'
|
import { auth } from '@/composables/useAuth'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
|
@ -242,8 +242,9 @@ import type { OrderStatus } from '@/stores/market'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const marketStore = useMarketStore()
|
const marketStore = useMarketStore()
|
||||||
const relayHub = relayHubComposable
|
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
||||||
const orderEvents = useOrderEvents()
|
// const orderEvents = useOrderEvents() // TODO: Move to market module
|
||||||
|
const orderEvents = { isSubscribed: ref(false), subscribeToOrderEvents: () => {}, cleanup: () => {} } // Temporary mock
|
||||||
|
|
||||||
// Local state
|
// Local state
|
||||||
const statusFilter = ref('')
|
const statusFilter = ref('')
|
||||||
|
|
|
||||||
|
|
@ -463,14 +463,17 @@ export function useMarket() {
|
||||||
// Connect to market
|
// Connect to market
|
||||||
const connectToMarket = async () => {
|
const connectToMarket = async () => {
|
||||||
try {
|
try {
|
||||||
// Connect to market
|
// Use existing relay hub connection (should already be connected by base module)
|
||||||
|
|
||||||
// Connect to relay hub
|
|
||||||
await relayHub.connect()
|
|
||||||
isConnected.value = relayHub.isConnected.value
|
isConnected.value = relayHub.isConnected.value
|
||||||
|
|
||||||
if (!isConnected.value) {
|
if (!isConnected.value) {
|
||||||
throw new Error('Failed to connect to Nostr relays')
|
console.warn('RelayHub not connected, attempting to connect...')
|
||||||
|
await relayHub.connect()
|
||||||
|
isConnected.value = relayHub.isConnected.value
|
||||||
|
|
||||||
|
if (!isConnected.value) {
|
||||||
|
throw new Error('Failed to connect to Nostr relays')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Market connected successfully
|
// Market connected successfully
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,14 @@ import { Button } from '@/components/ui/button'
|
||||||
import { formatDistanceToNow } from 'date-fns'
|
import { formatDistanceToNow } from 'date-fns'
|
||||||
import { Megaphone, RefreshCw, AlertCircle } from 'lucide-vue-next'
|
import { Megaphone, RefreshCw, AlertCircle } from 'lucide-vue-next'
|
||||||
import { config, configUtils } from '@/lib/config'
|
import { config, configUtils } from '@/lib/config'
|
||||||
import { relayHubComposable } from '@/composables/useRelayHub'
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
relays?: string[]
|
relays?: string[]
|
||||||
feedType?: 'all' | 'announcements' | 'events' | 'general'
|
feedType?: 'all' | 'announcements' | 'events' | 'general'
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const relayHub = relayHubComposable
|
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
const notes = ref<any[]>([])
|
const notes = ref<any[]>([])
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import NostrFeed from '@/components/nostr/NostrFeed.vue'
|
// NostrFeed is now registered globally by the nostr-feed module
|
||||||
|
// No need to import it directly - use the modular version
|
||||||
import NotificationPermission from '@/components/notifications/NotificationPermission.vue'
|
import NotificationPermission from '@/components/notifications/NotificationPermission.vue'
|
||||||
import PWAInstallPrompt from '@/components/pwa/PWAInstallPrompt.vue'
|
import PWAInstallPrompt from '@/components/pwa/PWAInstallPrompt.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue