webapp/src/composables/useNostrChat.ts
padreug ea5a2380f1 feat: Add market integration roadmap to NOSTR architecture documentation
- Introduce a comprehensive roadmap for integrating nostr-market-app purchasing functionality into the web-app.
- Outline key components of the shopping cart system, checkout process, and order management.
- Detail phased implementation strategy, including enhanced user experience and advanced features.
- Include security, performance, and testing considerations to ensure robust integration.

feat: Enhance market store with new order and cart management features

- Introduce new interfaces for Order, OrderItem, ContactInfo, and ShippingZone to support enhanced order management.
- Update Stall and Product interfaces to include currency and shipping details.
- Implement a comprehensive shopping cart system with stall-specific carts, including methods for adding, removing, and updating items.
- Add payment-related interfaces and methods for managing payment requests and statuses.
- Enhance filter options to include in-stock status and payment methods, improving product filtering capabilities.
- Refactor computed properties and methods for better cart management and checkout processes.

feat: Implement shopping cart functionality with new components and routing

- Add ShoppingCart, CartItem, and CartSummary components to manage cart items and display summaries.
- Introduce Cart.vue page to serve as the main shopping cart interface, integrating cart and summary components.
- Update Navbar.vue to include a cart icon with item count, enhancing user navigation.
- Implement cart management features in the market store, including item addition, quantity updates, and removal.
- Establish routing for the cart page, ensuring seamless navigation for users.
- Enhance ProductCard.vue to support adding items to the cart directly from the product listing.

feat: Update cart and checkout functionality with improved navigation and button labels

- Change "Proceed to Checkout" button text to dynamic "Place Order" based on context in CartSummary.vue.
- Update "Continue Shopping" button to "Back to Cart" in CartSummary.vue for clearer navigation.
- Modify routing for checkout to include stall ID in ShoppingCart.vue, enhancing checkout process.
- Simplify Cart.vue by removing CartSummary component and focusing on ShoppingCart display.
- Add new route for checkout with stall ID in router configuration for better handling of checkout flows.

feat: Enhance cart and checkout components with improved shipping address handling

- Update CartSummary.vue to use readonly types for cart items and shipping zones, ensuring immutability.
- Modify Checkout.vue to conditionally display the shipping address field based on the selected shipping zone's requirements for physical shipping.
- Add a digital delivery note for products that do not require a shipping address.
- Introduce a computed property to determine if a shipping address is required, improving validation logic during checkout.
- Update market store to include a new property for shipping zones indicating if physical shipping is required.

feat: Implement order placement functionality in checkout process

- Add a "Place Order" button in Checkout.vue that triggers the order placement process.
- Introduce loading state during order placement to enhance user experience.
- Implement createAndPlaceOrder method in market store to handle order creation and status updates.
- Include error handling for order placement failures, providing user feedback on errors.
- Update checkout logic to validate shipping zone and contact information before proceeding.

feat: Add Order History page and update Navbar for order tracking

- Introduce a new OrderHistory.vue page to display users' past orders with filtering and sorting options.
- Update Navbar.vue to include an "Order History" option with a badge showing the count of orders.
- Implement computed properties for order count and enhance user navigation experience.

feat: Integrate Nostr functionality for order management and user notifications

- Add NostrExtensionGuide component to inform users about the required Nostr extension for order transmission.
- Implement useNostrOrders composable to manage Nostr connection, event creation, and order sending.
- Update Checkout.vue to display Nostr connection status and provide feedback on order transmission.
- Enhance OrderHistory.vue to show Nostr transmission status and details for each order.
- Modify market store to handle Nostr event details and errors during order placement, ensuring local fallback.
- Introduce types for Nostr events to improve type safety and integration with the existing order management system.

refactor: Update Nostr relay configuration to use environment variable

- Change DEFAULT_RELAYS to dynamically retrieve relay URLs from the VITE_MARKET_RELAYS environment variable.
- Add error handling to ensure relays are configured before establishing a connection.
- Modify createBlankEvent function to return a more precise type.
- Update event signing process to ensure the event ID is generated correctly before signing.

refactor: useAuth switch Enhance Nostr order management with authentication checks

- Integrate user authentication checks to ensure Nostr features are only accessible to authenticated users.
- Replace direct window.nostr calls with auth store methods for retrieving public and private keys.
- Implement a helper function for signing events and mock encryption for order content.
- Remove obsolete Nostr type definitions to streamline the codebase.

feat: Enhance Checkout.vue with Nostr processing feedback and cleanup

- Update the checkout button to disable based on order placement state.
- Simplify order placement feedback by removing unnecessary Nostr processing checks.
- Introduce a new visual indicator for Nostr order processing status.
- Refactor computed properties for better clarity and efficiency in shipping zone handling.

refactor: Streamline Nostr order handling and integrate buyer public key retrieval

- Remove redundant Nostr relay tag from order event creation in useNostrOrders.
- Update Checkout.vue to retrieve the buyer's public key from the auth store, enhancing order placement logic.
- Modify createAndPlaceOrder method in market store to accept an optional Nostr orders instance for improved flexibility in order processing.

refactor: Remove Nostr-related components and streamline order processing

- Delete NostrExtensionGuide.vue and associated type definitions to simplify the codebase.
- Remove unused useNostr.ts file and related logic from useNostrOrders.ts.
- Update order handling in market store to directly integrate Nostr publishing without relying on external components.
- Enhance Checkout.vue and Cart.vue to reflect changes in Nostr integration and provide clearer order status feedback.

feat: Enhance Nostr chat functionality with malformed message handling

- Introduce tracking for malformed message IDs to prevent repeated processing attempts.
- Implement functions to mark messages as malformed, clean up old entries, and retrieve statistics on malformed messages.
- Add periodic cleanup of malformed messages to manage memory usage.
- Enhance message processing logic to skip previously identified malformed messages and provide detailed error handling for decryption failures.
- Update the return object to include new functions for managing malformed messages.

ZZ feat: Implement Lightning invoice management in market store

- Add functionality to create and manage Lightning invoices for orders.
- Introduce payment monitoring and status updates for invoices.
- Implement payment confirmation messaging via Nostr upon successful payment.
- Enhance order interface to include new fields for Lightning invoice details and payment status.

ZZ feat: Enhance OrderHistory.vue with payment status indicators and invoice management

- Add visual indicators for payment status, including 'Paid' and
'Payment Pending' badges.
- Implement expandable payment display for orders with Lightning
invoices.
- Introduce functionality to toggle payment display and generate
Lightning invoices.
- Update order status messaging to reflect payment requirements and
invoice generation status.

ZZ feat: Enhance OrderHistory.vue with payment status indicators and invoice management

- Add visual indicators for payment status, including 'Paid' and
'Payment Pending' badges.
- Implement expandable payment display for orders with Lightning
invoices.
- Introduce functionality to toggle payment display and generate
Lightning invoices.
- Update order status messaging to reflect payment requirements and
invoice generation status.

feat: Implement order event handling in useOrderEvents composable

- Introduce useOrderEvents composable to manage subscription and processing of order-related events.
- Define order event types and interfaces for better type safety and clarity.
- Implement methods to handle payment requests, order status updates, and invoice generation.
- Enhance OrderHistory.vue to display order event subscription status and last update timestamp.
- Update market store to include order update functionality for better integration with order events.

FIX: Build errors

refactor: Update component styles and improve UI consistency across market pages

- Replace various color classes with updated design tokens for better consistency.
- Change background colors of components to align with the new design system.
- Update text colors to enhance readability and maintain a cohesive look.
- Refactor class names in CartItem.vue, CartSummary.vue, DashboardOverview.vue, and other components to use the new color scheme.
- Ensure all components reflect the updated design guidelines for a unified user experience.

refactor: Remove Order History references from Navbar component

- Eliminate order count computation and related UI elements from the Navbar.
- Streamline the Navbar by removing the Order History button and badge.
- Maintain existing functionality for other menu items, ensuring a cleaner user interface.

feat: Implement QR code generation and download functionality in PaymentDisplay component

- Add QR code generation for payment requests using the qrcode library.
- Enhance UI to display loading states and error messages during QR code generation.
- Introduce a download button for users to save the generated QR code.
- Implement logic to regenerate QR code when the invoice changes.

refactor: Replace useRelayHub with relayHubComposable across components

- Update imports in multiple components and composables to use the new relayHubComposable for better consistency and maintainability.
- Enhance OrderHistory.vue with debug information for development, displaying key states related to orders, authentication, and relay hub connectivity.
- Remove unnecessary reconnect button from RelayHubStatus.vue to streamline user interactions.
- Improve logging in useOrderEvents for better debugging and monitoring of order event subscriptions.

refactor: Update OrderHistory.vue styles for improved UI consistency

- Replace color classes with updated design tokens for better alignment with the new design system.
- Enhance readability by adjusting text colors and background styles for payment status indicators.
- Ensure a cohesive look across the component by standardizing class names and styles.

refactor: Update component styles for improved UI consistency across checkout pages

- Replace color classes with updated design tokens for better alignment with the new design system.
- Enhance readability by adjusting text colors and background styles in CartSummary.vue, PaymentDisplay.vue, Checkout.vue, and OrderHistory.vue.
- Standardize class names and styles to ensure a cohesive look across all components.

feat: Implement invoice generation and Nostr integration in MerchantStore component

- Add functionality to generate Lightning invoices for orders and send them to customers via Nostr.
- Introduce a new sendInvoiceToCustomer method to update order details and publish invoice information.
- Enhance order event handling in useOrderEvents to update existing orders with new invoice data.
- Improve error handling and logging for invoice generation and sending processes.

feat: Enhance MerchantStore and PaymentDisplay components for improved invoice handling

- Add wallet indicator in MerchantStore to display the selected wallet name during pending orders.
- Implement temporary fixes for missing buyer and seller public keys when generating invoices.
- Update invoice generation logic to utilize the first available wallet and improve error handling.
- Modify PaymentDisplay to use the new bolt11 field for payment requests and enhance date formatting.
- Refactor order event handling to ensure accurate updates and invoice management across components.

feat: Enhance order event processing in useOrderEvents composable

- Refactor processOrderEvent to handle incoming Nostr market order events with improved validation and logging.
- Implement logic to update existing orders or create new ones based on event data, ensuring accurate order management.
- Add detailed console logging for better debugging and tracking of order events and their statuses.
- Ensure compatibility with market order structure and invoice details for seamless integration with payment processing.

feat: Enhance order management with localStorage persistence

- Update createOrder method to optionally accept an order ID from events, improving order tracking.
- Convert items from readonly to mutable for better manipulation.
- Implement localStorage persistence for orders, ensuring data is saved and loaded across sessions.
- Add methods to save and load orders from localStorage, enhancing user experience and data reliability.

feat: Update invoice creation to support additional metadata and nostrmarket compatibility

- Modify createInvoice method to accept an optional extra parameter for additional metadata.
- Change invoice tag to 'nostrmarket' for improved compatibility with Nostr market.
- Include merchant and buyer public keys in the invoice data for better integration.
- Update invoice creation in market store to utilize new parameters for enhanced functionality.

feat: Enhance order and invoice handling for Nostr market compatibility

- Add originalOrderId to order events for tracking Nostr order IDs.
- Update invoice creation to utilize original Nostr order ID when generating invoices.
- Improve logging for invoice requests to LNBits, providing better visibility into the data being sent.
- Ensure compatibility with nostrmarket by adjusting order ID handling in the market store.

fix: Refine invoice creation logic for Nostr market compatibility

- Adjust order ID handling in invoice creation to prioritize originalOrderId for better compatibility with nostrmarket.
- Enhance logging to provide clearer insights into the order ID being used during invoice generation.

feat: Integrate nostrmarket service for order publishing and merchant catalog management

- Implement functionality to publish orders via the nostrmarket protocol, replacing the previous Nostr integration.
- Add methods to publish merchant catalogs, including stalls and products, to nostrmarket with event ID tracking.
- Enhance order interface to include nostrEventId for better integration with nostrmarket.
- Improve error handling and logging for nostrmarket publishing processes.

refactor: Simplify order creation logic in useOrderEvents and update contact structure in nostrmarketService

- Streamline order creation by using event.id and defaulting to 'unknown' for stallId.
- Update contact structure to include address and message, removing optional email and phone fields for clarity.
- Ensure compatibility with new order data structure for improved integration with nostrmarket.

feat: Add bech32 to hex conversion utility and integrate into nostrmarketService

- Implement a new utility function to convert bech32 keys to hex format, enhancing key handling.
- Update nostrmarketService to utilize the new conversion function for user public and private keys.
- Modify contact structure to include additional fields for improved order information management.

feat: Add nostrclient configuration to AppConfig for enhanced Nostr integration

- Introduce a new nostrclient property in AppConfig to manage Nostr client settings.
- Include url and enabled fields to configure the Nostr client connection dynamically.
- Ensure compatibility with environment variables for flexible deployment configurations.

feat: Introduce comprehensive order management and fulfillment documentation

- Add ORDER_MANAGEMENT_FULFILLMENT.md to detail the complete order lifecycle, including order states, data models, and merchant/customer interfaces.
- Implement test scripts for verifying order and payment request formats in test-nostrmarket-format.js.
- Create PaymentRequestDialog.vue for handling payment requests with dynamic options and QR code generation.
- Enhance useOrderEvents.ts to process nostrmarket protocol messages for order management.
- Update nostrmarketService.ts to handle payment requests and order status updates, ensuring seamless integration with the marketplace.
- Integrate payment request dialog in Market.vue and manage its state in the market store.

refactor: Remove obsolete test script for nostrmarket order format

- Delete test-nostrmarket-format.js as it is no longer needed for verifying order and payment request formats.
- Update PaymentRequestDialog.vue to enhance UI components and integrate QR code generation for payment requests.
- Refactor payment handling and notification logic to utilize toast notifications instead of Quasar's notify system.

feat: Enhance OrderHistory component with payment request handling and QR code generation

- Add UI elements to display payment request status and options in OrderHistory.vue.
- Implement functions to copy payment requests, open Lightning wallets, and download QR codes.
- Update nostrmarketService to generate QR codes for payment requests and manage order statuses effectively.
- Remove obsolete PaymentRequestDialog integration from Market.vue for a cleaner UI.

feat: Add debug information and toast notifications in OrderHistory component

- Introduce debug info display for payment requests and hashes in OrderHistory.vue.
- Implement toast notifications for actions like copying payment requests, opening wallets, and downloading QR codes.
- Enhance error handling with user feedback for various order-related actions.
- Remove obsolete payment request dialog methods from market store for cleaner code.

feat: Revamp CartItem and ShoppingCart components for improved layout and functionality

- Enhance CartItem.vue with responsive design for desktop and mobile views, including better organization of product details, price, quantity controls, and remove button.
- Update ShoppingCart.vue to separate desktop and mobile layouts, improving the user experience with clearer action buttons and cart summary display.
- Implement consistent styling and layout adjustments for better visual coherence across different screen sizes.
2025-09-04 17:51:30 +02:00

928 lines
No EOL
28 KiB
TypeScript

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: NodeJS.Timeout | 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) // 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()