Implement LNbits integration in AuthService and enhance ChatComponent for improved user experience
- Refactor AuthService to integrate LNbits authentication, including fetching user data from the API and handling token validation. - Update ChatComponent to reflect changes in peer management, replacing user_id with pubkey and username with name for better clarity. - Enhance connection status indicators in ChatComponent for improved user feedback during chat initialization.
This commit is contained in:
parent
d33d2abf8a
commit
daa9656680
4 changed files with 421 additions and 129 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
// Copy the existing auth logic into a service class
|
// Auth service for LNbits integration
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { eventBus } from '@/core/event-bus'
|
import { eventBus } from '@/core/event-bus'
|
||||||
|
import { getAuthToken } from '@/lib/config/lnbits'
|
||||||
|
import { config } from '@/lib/config'
|
||||||
|
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
public isAuthenticated = ref(false)
|
public isAuthenticated = ref(false)
|
||||||
|
|
@ -10,33 +12,64 @@ export class AuthService {
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
console.log('🔑 Initializing auth service...')
|
console.log('🔑 Initializing auth service...')
|
||||||
|
|
||||||
// Check for existing auth state
|
// Check for existing auth state and fetch user data
|
||||||
this.checkAuth()
|
await this.checkAuth()
|
||||||
|
|
||||||
if (this.isAuthenticated.value) {
|
if (this.isAuthenticated.value) {
|
||||||
eventBus.emit('auth:login', { user: this.user.value }, 'auth-service')
|
eventBus.emit('auth:login', { user: this.user.value }, 'auth-service')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkAuth(): boolean {
|
async checkAuth(): Promise<boolean> {
|
||||||
// Implement your existing auth check logic here
|
const authToken = getAuthToken()
|
||||||
// For now, we'll use a simple localStorage check
|
|
||||||
const authData = localStorage.getItem('auth')
|
|
||||||
if (authData) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(authData)
|
|
||||||
this.isAuthenticated.value = true
|
|
||||||
this.user.value = parsed
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Invalid auth data in localStorage:', error)
|
|
||||||
this.logout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isAuthenticated.value = false
|
if (!authToken) {
|
||||||
this.user.value = null
|
console.log('🔑 No auth token found - user needs to login')
|
||||||
return false
|
this.isAuthenticated.value = false
|
||||||
|
this.user.value = null
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch current user data from API
|
||||||
|
try {
|
||||||
|
this.isLoading.value = true
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
console.log('🔑 Auth token invalid - user needs to login')
|
||||||
|
this.logout()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
console.warn(`🔑 Failed to fetch user data: ${response.status} - authentication may not be properly configured`)
|
||||||
|
this.isAuthenticated.value = false
|
||||||
|
this.user.value = null
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = await response.json()
|
||||||
|
|
||||||
|
this.user.value = userData
|
||||||
|
this.isAuthenticated.value = true
|
||||||
|
|
||||||
|
console.log('🔑 User authenticated:', userData.username || userData.id, userData.pubkey?.slice(0, 8))
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('🔑 Authentication check failed:', error)
|
||||||
|
this.isAuthenticated.value = false
|
||||||
|
this.user.value = null
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
this.isLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(credentials: any): Promise<void> {
|
async login(credentials: any): Promise<void> {
|
||||||
|
|
@ -71,8 +104,8 @@ export class AuthService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async refresh(): Promise<void> {
|
async refresh(): Promise<void> {
|
||||||
// Implement token refresh logic if needed
|
// Re-fetch user data from API
|
||||||
console.log('Refreshing auth token...')
|
await this.checkAuth()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<h2 class="text-lg font-semibold">Chat</h2>
|
<h2 class="text-lg font-semibold">Chat</h2>
|
||||||
<Badge v-if="isConnected" variant="default" class="text-xs">
|
<Badge v-if="isConnected" variant="default" class="text-xs">
|
||||||
Connected
|
Ready
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge v-else variant="secondary" class="text-xs">
|
<Badge v-else variant="secondary" class="text-xs">
|
||||||
Disconnected
|
Initializing...
|
||||||
</Badge>
|
</Badge>
|
||||||
<!-- Total unread count -->
|
<!-- Total unread count -->
|
||||||
<Badge v-if="totalUnreadCount > 0" variant="secondary" class="text-xs">
|
<Badge v-if="totalUnreadCount > 0" variant="secondary" class="text-xs">
|
||||||
|
|
@ -68,11 +68,11 @@
|
||||||
<!-- Peer list -->
|
<!-- Peer list -->
|
||||||
<div
|
<div
|
||||||
v-for="peer in filteredPeers"
|
v-for="peer in filteredPeers"
|
||||||
:key="peer.user_id"
|
:key="peer.pubkey"
|
||||||
@click="selectPeer(peer)"
|
@click="selectPeer(peer)"
|
||||||
:class="[
|
:class="[
|
||||||
'flex items-center space-x-3 p-3 rounded-lg cursor-pointer transition-colors touch-manipulation relative',
|
'flex items-center space-x-3 p-3 rounded-lg cursor-pointer transition-colors touch-manipulation relative',
|
||||||
selectedPeer?.user_id === peer.user_id
|
selectedPeer?.pubkey === peer.pubkey
|
||||||
? 'bg-primary text-primary-foreground'
|
? 'bg-primary text-primary-foreground'
|
||||||
: 'hover:bg-muted active:bg-muted/80'
|
: 'hover:bg-muted active:bg-muted/80'
|
||||||
]"
|
]"
|
||||||
|
|
@ -83,7 +83,7 @@
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="text-sm font-medium truncate">
|
<p class="text-sm font-medium truncate">
|
||||||
{{ peer.username || 'Unknown User' }}
|
{{ peer.name || 'Unknown User' }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-muted-foreground truncate">
|
<p class="text-xs text-muted-foreground truncate">
|
||||||
{{ formatPubkey(peer.pubkey) }}
|
{{ formatPubkey(peer.pubkey) }}
|
||||||
|
|
@ -119,7 +119,7 @@
|
||||||
<AvatarFallback>{{ selectedPeer ? getPeerInitials(selectedPeer) : 'U' }}</AvatarFallback>
|
<AvatarFallback>{{ selectedPeer ? getPeerInitials(selectedPeer) : 'U' }}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-medium">{{ selectedPeer?.username || 'Unknown User' }}</h3>
|
<h3 class="font-medium">{{ selectedPeer?.name || 'Unknown User' }}</h3>
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
{{ selectedPeer ? formatPubkey(selectedPeer.pubkey) : '' }}
|
{{ selectedPeer ? formatPubkey(selectedPeer.pubkey) : '' }}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -127,10 +127,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<Badge v-if="isConnected" variant="default" class="text-xs">
|
<Badge v-if="isConnected" variant="default" class="text-xs">
|
||||||
Connected
|
Ready
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge v-else variant="secondary" class="text-xs">
|
<Badge v-else variant="secondary" class="text-xs">
|
||||||
Disconnected
|
Initializing...
|
||||||
</Badge>
|
</Badge>
|
||||||
<!-- Unread count for current peer -->
|
<!-- Unread count for current peer -->
|
||||||
<Badge v-if="selectedPeer && getUnreadCount(selectedPeer.pubkey) > 0" variant="secondary" class="text-xs">
|
<Badge v-if="selectedPeer && getUnreadCount(selectedPeer.pubkey) > 0" variant="secondary" class="text-xs">
|
||||||
|
|
@ -192,10 +192,10 @@
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<h2 class="text-lg font-semibold">Chat</h2>
|
<h2 class="text-lg font-semibold">Chat</h2>
|
||||||
<Badge v-if="isConnected" variant="default" class="text-xs">
|
<Badge v-if="isConnected" variant="default" class="text-xs">
|
||||||
Connected
|
Ready
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge v-else variant="secondary" class="text-xs">
|
<Badge v-else variant="secondary" class="text-xs">
|
||||||
Disconnected
|
Initializing...
|
||||||
</Badge>
|
</Badge>
|
||||||
<!-- Total unread count -->
|
<!-- Total unread count -->
|
||||||
<Badge v-if="totalUnreadCount > 0" variant="secondary" class="text-xs">
|
<Badge v-if="totalUnreadCount > 0" variant="secondary" class="text-xs">
|
||||||
|
|
@ -255,11 +255,11 @@
|
||||||
<!-- Peer list -->
|
<!-- Peer list -->
|
||||||
<div
|
<div
|
||||||
v-for="peer in filteredPeers"
|
v-for="peer in filteredPeers"
|
||||||
:key="peer.user_id"
|
:key="peer.pubkey"
|
||||||
@click="selectPeer(peer)"
|
@click="selectPeer(peer)"
|
||||||
:class="[
|
:class="[
|
||||||
'flex items-center space-x-3 p-3 rounded-lg cursor-pointer transition-colors relative',
|
'flex items-center space-x-3 p-3 rounded-lg cursor-pointer transition-colors relative',
|
||||||
selectedPeer?.user_id === peer.user_id
|
selectedPeer?.pubkey === peer.pubkey
|
||||||
? 'bg-primary text-primary-foreground'
|
? 'bg-primary text-primary-foreground'
|
||||||
: 'hover:bg-muted'
|
: 'hover:bg-muted'
|
||||||
]"
|
]"
|
||||||
|
|
@ -270,7 +270,7 @@
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="text-sm font-medium truncate">
|
<p class="text-sm font-medium truncate">
|
||||||
{{ peer.username || 'Unknown User' }}
|
{{ peer.name || 'Unknown User' }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-muted-foreground truncate">
|
<p class="text-xs text-muted-foreground truncate">
|
||||||
{{ formatPubkey(peer.pubkey) }}
|
{{ formatPubkey(peer.pubkey) }}
|
||||||
|
|
@ -297,7 +297,7 @@
|
||||||
<AvatarFallback>{{ getPeerInitials(selectedPeer) }}</AvatarFallback>
|
<AvatarFallback>{{ getPeerInitials(selectedPeer) }}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-medium">{{ selectedPeer.username || 'Unknown User' }}</h3>
|
<h3 class="font-medium">{{ selectedPeer.name || 'Unknown User' }}</h3>
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
{{ formatPubkey(selectedPeer.pubkey) }}
|
{{ formatPubkey(selectedPeer.pubkey) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -380,18 +380,14 @@ import { useChat } from '../composables/useChat'
|
||||||
import { useFuzzySearch } from '@/composables/useFuzzySearch'
|
import { useFuzzySearch } from '@/composables/useFuzzySearch'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
interface Peer {
|
import type { ChatPeer } from '../types'
|
||||||
user_id: string
|
|
||||||
username: string
|
|
||||||
pubkey: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize chat composable
|
// Initialize chat composable
|
||||||
const chat = useChat()
|
const chat = useChat()
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const peers = computed(() => chat.peers.value)
|
const peers = computed(() => chat.peers.value)
|
||||||
const selectedPeer = ref<Peer | null>(null)
|
const selectedPeer = ref<ChatPeer | null>(null)
|
||||||
const messageInput = ref('')
|
const messageInput = ref('')
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
|
@ -405,45 +401,23 @@ const isMobile = ref(false)
|
||||||
|
|
||||||
// Get methods and state from chat composable
|
// Get methods and state from chat composable
|
||||||
// Note: The modular chat service handles connection and peer management automatically
|
// Note: The modular chat service handles connection and peer management automatically
|
||||||
const isConnected = computed(() => true) // Chat service manages connection
|
const isConnected = computed(() => chat.isReady.value) // Use chat service ready state
|
||||||
const messages = ref(new Map()) // Local messages map for compatibility
|
|
||||||
const totalUnreadCount = computed(() => chat.totalUnreadCount.value)
|
const totalUnreadCount = computed(() => chat.totalUnreadCount.value)
|
||||||
|
|
||||||
// Adapter functions for compatibility with existing code
|
// Helper functions
|
||||||
const connect = async () => {} // Connection handled by chat service
|
|
||||||
const disconnect = () => {} // Handled by chat service
|
|
||||||
const subscribeToPeer = async (peer: string) => {} // Handled by chat service
|
|
||||||
const sendNostrMessage = async (peer: string, content: string) => {
|
|
||||||
chat.selectPeer(peer)
|
|
||||||
await chat.sendMessage(content)
|
|
||||||
}
|
|
||||||
const onMessageAdded = (callback: Function) => {} // Event handling via chat service
|
|
||||||
const markMessagesAsRead = (peer: string) => chat.markAsRead(peer)
|
|
||||||
const getUnreadCount = (peer: string) => {
|
const getUnreadCount = (peer: string) => {
|
||||||
const peerData = chat.peers.value.find(p => p.pubkey === peer)
|
const peerData = chat.peers.value.find(p => p.pubkey === peer)
|
||||||
return peerData?.unreadCount || 0
|
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(() => {
|
||||||
if (!selectedPeer.value) return []
|
return chat.currentMessages.value
|
||||||
const peerMessages = messages.value.get(selectedPeer.value.pubkey) || []
|
|
||||||
|
|
||||||
// Sort messages by timestamp (oldest first) to ensure chronological order
|
|
||||||
const sortedMessages = [...peerMessages].sort((a, b) => a.created_at - b.created_at)
|
|
||||||
|
|
||||||
return sortedMessages
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Sort peers by latest message timestamp (newest first) and unread status
|
// Sort peers by unread count and name
|
||||||
const sortedPeers = computed(() => {
|
const sortedPeers = computed(() => {
|
||||||
const sorted = [...peers.value].sort((a, b) => {
|
const sorted = [...peers.value].sort((a, b) => {
|
||||||
const aTimestamp = getLatestMessageTimestamp(a.pubkey)
|
|
||||||
const bTimestamp = getLatestMessageTimestamp(b.pubkey)
|
|
||||||
const aUnreadCount = getUnreadCount(a.pubkey)
|
const aUnreadCount = getUnreadCount(a.pubkey)
|
||||||
const bUnreadCount = getUnreadCount(b.pubkey)
|
const bUnreadCount = getUnreadCount(b.pubkey)
|
||||||
|
|
||||||
|
|
@ -451,13 +425,8 @@ const sortedPeers = computed(() => {
|
||||||
if (aUnreadCount > 0 && bUnreadCount === 0) return -1
|
if (aUnreadCount > 0 && bUnreadCount === 0) return -1
|
||||||
if (aUnreadCount === 0 && bUnreadCount > 0) return 1
|
if (aUnreadCount === 0 && bUnreadCount > 0) return 1
|
||||||
|
|
||||||
// Then, sort by latest message timestamp (newest first)
|
// Finally, sort alphabetically by name for peers with same unread status
|
||||||
if (aTimestamp !== bTimestamp) {
|
return (a.name || '').localeCompare(b.name || '')
|
||||||
return bTimestamp - aTimestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, sort alphabetically by username for peers with same timestamp
|
|
||||||
return (a.username || '').localeCompare(b.username || '')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return sorted
|
return sorted
|
||||||
|
|
@ -475,7 +444,7 @@ const {
|
||||||
} = useFuzzySearch(sortedPeers, {
|
} = useFuzzySearch(sortedPeers, {
|
||||||
fuseOptions: {
|
fuseOptions: {
|
||||||
keys: [
|
keys: [
|
||||||
{ name: 'username', weight: 0.7 }, // Username has higher weight for better UX
|
{ name: 'name', weight: 0.7 }, // Name has higher weight for better UX
|
||||||
{ name: 'pubkey', weight: 0.3 } // Pubkey has lower weight but still searchable
|
{ name: 'pubkey', weight: 0.3 } // Pubkey has lower weight but still searchable
|
||||||
],
|
],
|
||||||
threshold: 0.3, // Fuzzy matching threshold (0.0 = perfect, 1.0 = match anything)
|
threshold: 0.3, // Fuzzy matching threshold (0.0 = perfect, 1.0 = match anything)
|
||||||
|
|
@ -509,31 +478,21 @@ const goBackToPeers = () => {
|
||||||
|
|
||||||
|
|
||||||
const refreshPeers = async () => {
|
const refreshPeers = async () => {
|
||||||
isLoading.value = true
|
await chat.refreshPeers()
|
||||||
try {
|
|
||||||
// Peers are loaded automatically by the chat service
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to refresh peers:', error)
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectPeer = async (peer: Peer) => {
|
const selectPeer = async (peer: ChatPeer) => {
|
||||||
selectedPeer.value = peer
|
selectedPeer.value = peer
|
||||||
messageInput.value = ''
|
messageInput.value = ''
|
||||||
|
|
||||||
// Mark messages as read for this peer
|
// Use the modular chat service
|
||||||
markMessagesAsRead(peer.pubkey)
|
chat.selectPeer(peer.pubkey)
|
||||||
|
|
||||||
// On mobile, show chat view
|
// On mobile, show chat view
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
showChat.value = true
|
showChat.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to messages from this peer
|
|
||||||
await subscribeToPeer(peer.pubkey)
|
|
||||||
|
|
||||||
// Scroll to bottom to show latest messages when selecting a peer
|
// Scroll to bottom to show latest messages when selecting a peer
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|
@ -544,7 +503,7 @@ const sendMessage = async () => {
|
||||||
if (!selectedPeer.value || !messageInput.value.trim()) return
|
if (!selectedPeer.value || !messageInput.value.trim()) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendNostrMessage(selectedPeer.value.pubkey, messageInput.value)
|
await chat.sendMessage(messageInput.value)
|
||||||
messageInput.value = ''
|
messageInput.value = ''
|
||||||
|
|
||||||
// Scroll to bottom
|
// Scroll to bottom
|
||||||
|
|
@ -579,14 +538,14 @@ const formatTime = (timestamp: number) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPeerAvatar = (_peer: Peer) => {
|
const getPeerAvatar = (_peer: ChatPeer) => {
|
||||||
// You can implement avatar logic here
|
// You can implement avatar logic here
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPeerInitials = (peer: Peer) => {
|
const getPeerInitials = (peer: ChatPeer) => {
|
||||||
if (peer.username) {
|
if (peer.name) {
|
||||||
return peer.username.slice(0, 2).toUpperCase()
|
return peer.name.slice(0, 2).toUpperCase()
|
||||||
}
|
}
|
||||||
return peer.pubkey.slice(0, 2).toUpperCase()
|
return peer.pubkey.slice(0, 2).toUpperCase()
|
||||||
}
|
}
|
||||||
|
|
@ -594,33 +553,13 @@ const getPeerInitials = (peer: Peer) => {
|
||||||
|
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
onMounted(async () => {
|
onMounted(() => {
|
||||||
checkMobile()
|
checkMobile()
|
||||||
window.addEventListener('resize', checkMobile)
|
window.addEventListener('resize', checkMobile)
|
||||||
|
|
||||||
// Set up message callback
|
|
||||||
onMessageAdded.value = (peerPubkey: string) => {
|
|
||||||
if (selectedPeer.value && selectedPeer.value.pubkey === peerPubkey) {
|
|
||||||
nextTick(() => {
|
|
||||||
scrollToBottom()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not connected, connect
|
|
||||||
if (!isConnected.value) {
|
|
||||||
await connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no peers loaded, load them
|
|
||||||
if (peers.value.length === 0) {
|
|
||||||
// Peers are loaded automatically by the chat service
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', checkMobile)
|
window.removeEventListener('resize', checkMobile)
|
||||||
disconnect()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch for connection state changes
|
// Watch for connection state changes
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export function useChat() {
|
||||||
// Computed properties
|
// Computed properties
|
||||||
const peers = computed(() => chatService.allPeers.value)
|
const peers = computed(() => chatService.allPeers.value)
|
||||||
const totalUnreadCount = computed(() => chatService.totalUnreadCount.value)
|
const totalUnreadCount = computed(() => chatService.totalUnreadCount.value)
|
||||||
|
const isReady = computed(() => chatService.isReady.value)
|
||||||
|
|
||||||
const currentMessages = computed(() => {
|
const currentMessages = computed(() => {
|
||||||
return selectedPeer.value ? chatService.getMessages(selectedPeer.value) : []
|
return selectedPeer.value ? chatService.getMessages(selectedPeer.value) : []
|
||||||
|
|
@ -61,6 +62,19 @@ export function useChat() {
|
||||||
chatService.markAsRead(peerPubkey)
|
chatService.markAsRead(peerPubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshPeers = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
await chatService.refreshPeers()
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err instanceof Error ? err.message : 'Failed to refresh peers'
|
||||||
|
console.error('Failed to refresh peers:', err)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// State
|
// State
|
||||||
selectedPeer,
|
selectedPeer,
|
||||||
|
|
@ -70,6 +84,7 @@ export function useChat() {
|
||||||
// Computed
|
// Computed
|
||||||
peers,
|
peers,
|
||||||
totalUnreadCount,
|
totalUnreadCount,
|
||||||
|
isReady,
|
||||||
currentMessages,
|
currentMessages,
|
||||||
currentPeer,
|
currentPeer,
|
||||||
|
|
||||||
|
|
@ -77,6 +92,7 @@ export function useChat() {
|
||||||
selectPeer,
|
selectPeer,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
addPeer,
|
addPeer,
|
||||||
markAsRead
|
markAsRead,
|
||||||
|
refreshPeers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { eventBus } from '@/core/event-bus'
|
import { eventBus } from '@/core/event-bus'
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
|
import { nip04, getEventHash, finalizeEvent, type Event, type EventTemplate } from 'nostr-tools'
|
||||||
import type { ChatMessage, ChatPeer, UnreadMessageData, ChatConfig } from '../types'
|
import type { ChatMessage, ChatPeer, UnreadMessageData, ChatConfig } from '../types'
|
||||||
|
import { getAuthToken } from '@/lib/config/lnbits'
|
||||||
|
import { config } from '@/lib/config'
|
||||||
|
|
||||||
const UNREAD_MESSAGES_KEY = 'nostr-chat-unread-messages'
|
const UNREAD_MESSAGES_KEY = 'nostr-chat-unread-messages'
|
||||||
const PEERS_KEY = 'nostr-chat-peers'
|
const PEERS_KEY = 'nostr-chat-peers'
|
||||||
|
|
@ -10,10 +13,67 @@ export class ChatService {
|
||||||
private messages = ref<Map<string, ChatMessage[]>>(new Map())
|
private messages = ref<Map<string, ChatMessage[]>>(new Map())
|
||||||
private peers = ref<Map<string, ChatPeer>>(new Map())
|
private peers = ref<Map<string, ChatPeer>>(new Map())
|
||||||
private config: ChatConfig
|
private config: ChatConfig
|
||||||
|
private subscriptionUnsubscriber?: () => void
|
||||||
|
private isInitialized = ref(false)
|
||||||
|
|
||||||
constructor(config: ChatConfig) {
|
constructor(config: ChatConfig) {
|
||||||
this.config = config
|
this.config = config
|
||||||
this.loadPeersFromStorage()
|
this.loadPeersFromStorage()
|
||||||
|
|
||||||
|
// Defer initialization until services are available
|
||||||
|
this.deferredInitialization()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer initialization until services are ready
|
||||||
|
private deferredInitialization(): void {
|
||||||
|
// Try initialization immediately
|
||||||
|
this.tryInitialization()
|
||||||
|
|
||||||
|
// Also listen for auth events to re-initialize when user logs in
|
||||||
|
eventBus.on('auth:login', () => {
|
||||||
|
console.log('💬 Auth login detected, initializing chat...')
|
||||||
|
this.tryInitialization()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to initialize services if they're available
|
||||||
|
private async tryInitialization(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any
|
||||||
|
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any
|
||||||
|
|
||||||
|
if (!relayHub || !authService?.user?.value?.pubkey) {
|
||||||
|
console.log('💬 Services not ready yet, will retry when auth completes...')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('💬 Services ready, initializing chat functionality...')
|
||||||
|
|
||||||
|
// Load peers from API
|
||||||
|
await this.loadPeersFromAPI().catch(error => {
|
||||||
|
console.warn('Failed to load peers from API:', error)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initialize message handling (subscription + history loading)
|
||||||
|
await this.initializeMessageHandling()
|
||||||
|
|
||||||
|
// Mark as initialized
|
||||||
|
this.isInitialized.value = true
|
||||||
|
console.log('💬 Chat service fully initialized and ready!')
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('💬 Failed to initialize chat:', error)
|
||||||
|
this.isInitialized.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize message handling (subscription + history loading)
|
||||||
|
async initializeMessageHandling(): Promise<void> {
|
||||||
|
// Set up real-time subscription
|
||||||
|
this.setupMessageSubscription()
|
||||||
|
|
||||||
|
// Load message history for known peers
|
||||||
|
await this.loadMessageHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
|
|
@ -28,6 +88,10 @@ export class ChatService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isReady() {
|
||||||
|
return computed(() => this.isInitialized.value)
|
||||||
|
}
|
||||||
|
|
||||||
// Get messages for a specific peer
|
// Get messages for a specific peer
|
||||||
getMessages(peerPubkey: string): ChatMessage[] {
|
getMessages(peerPubkey: string): ChatMessage[] {
|
||||||
return this.messages.value.get(peerPubkey) || []
|
return this.messages.value.get(peerPubkey) || []
|
||||||
|
|
@ -120,31 +184,70 @@ export class ChatService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh peers from API
|
||||||
|
async refreshPeers(): Promise<void> {
|
||||||
|
return this.loadPeersFromAPI()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if services are available for messaging
|
||||||
|
private checkServicesAvailable(): { relayHub: any; authService: any } | null {
|
||||||
|
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any
|
||||||
|
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any
|
||||||
|
|
||||||
|
if (!relayHub || !authService?.user?.value?.prvkey) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!relayHub.isConnected) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return { relayHub, authService }
|
||||||
|
}
|
||||||
|
|
||||||
// Send a message
|
// Send a message
|
||||||
async sendMessage(peerPubkey: string, content: string): Promise<void> {
|
async sendMessage(peerPubkey: string, content: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
const services = this.checkServicesAvailable()
|
||||||
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any
|
|
||||||
|
|
||||||
if (!relayHub || !authService?.user?.value?.privkey) {
|
if (!services) {
|
||||||
throw new Error('Required services not available')
|
throw new Error('Chat services not ready. Please wait for connection to establish.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { relayHub, authService } = services
|
||||||
|
|
||||||
|
const userPrivkey = authService.user.value.prvkey
|
||||||
|
const userPubkey = authService.user.value.pubkey
|
||||||
|
|
||||||
|
// Encrypt the message using NIP-04
|
||||||
|
const encryptedContent = await nip04.encrypt(userPrivkey, peerPubkey, content)
|
||||||
|
|
||||||
|
// Create Nostr event for the encrypted message (kind 4 = encrypted direct message)
|
||||||
|
const eventTemplate: EventTemplate = {
|
||||||
|
kind: 4,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [['p', peerPubkey]],
|
||||||
|
content: encryptedContent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create message
|
// Finalize the event with signature
|
||||||
|
const signedEvent = finalizeEvent(eventTemplate, userPrivkey)
|
||||||
|
|
||||||
|
// Create local message for immediate display
|
||||||
const message: ChatMessage = {
|
const message: ChatMessage = {
|
||||||
id: crypto.randomUUID(),
|
id: signedEvent.id,
|
||||||
content,
|
content,
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: signedEvent.created_at,
|
||||||
sent: true,
|
sent: true,
|
||||||
pubkey: authService.user.value.pubkey
|
pubkey: userPubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to local messages immediately
|
// Add to local messages immediately
|
||||||
this.addMessage(peerPubkey, message)
|
this.addMessage(peerPubkey, message)
|
||||||
|
|
||||||
// TODO: Implement actual Nostr message sending
|
// Publish to Nostr relays
|
||||||
// This would involve encrypting the message and publishing to relays
|
const result = await relayHub.publishEvent(signedEvent)
|
||||||
console.log('Sending message:', { peerPubkey, content })
|
console.log('Message published to relays:', { success: result.success, total: result.total })
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to send message:', error)
|
console.error('Failed to send message:', error)
|
||||||
|
|
@ -209,6 +312,52 @@ export class ChatService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load peers from API
|
||||||
|
async loadPeersFromAPI(): Promise<void> {
|
||||||
|
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()
|
||||||
|
|
||||||
|
// Clear existing peers and load from API
|
||||||
|
this.peers.value.clear()
|
||||||
|
|
||||||
|
data.forEach((peer: any) => {
|
||||||
|
const chatPeer: ChatPeer = {
|
||||||
|
pubkey: peer.pubkey,
|
||||||
|
name: peer.username || `User ${peer.pubkey.slice(0, 8)}`,
|
||||||
|
unreadCount: 0,
|
||||||
|
lastSeen: Date.now()
|
||||||
|
}
|
||||||
|
this.peers.value.set(peer.pubkey, chatPeer)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Save to storage
|
||||||
|
this.savePeersToStorage()
|
||||||
|
|
||||||
|
console.log(`Loaded ${data.length} peers from API`)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load peers from API:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private loadPeersFromStorage(): void {
|
private loadPeersFromStorage(): void {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(PEERS_KEY)
|
const stored = localStorage.getItem(PEERS_KEY)
|
||||||
|
|
@ -232,8 +381,163 @@ export class ChatService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load message history for known peers
|
||||||
|
private async loadMessageHistory(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any
|
||||||
|
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any
|
||||||
|
|
||||||
|
if (!relayHub || !authService?.user?.value?.pubkey) {
|
||||||
|
console.warn('Cannot load message history: missing services')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const userPubkey = authService.user.value.pubkey
|
||||||
|
const userPrivkey = authService.user.value.prvkey
|
||||||
|
const peerPubkeys = Array.from(this.peers.value.keys())
|
||||||
|
|
||||||
|
if (peerPubkeys.length === 0) {
|
||||||
|
console.log('No peers to load message history for')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Loading message history for', peerPubkeys.length, 'peers')
|
||||||
|
|
||||||
|
// Query historical messages (kind 4) to/from known peers
|
||||||
|
const events = await relayHub.queryEvents([
|
||||||
|
{
|
||||||
|
kinds: [4],
|
||||||
|
authors: [userPubkey, ...peerPubkeys], // Messages from us or peers
|
||||||
|
'#p': [userPubkey], // Messages tagged with our pubkey
|
||||||
|
limit: 100 // Limit to last 100 messages per conversation
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('Found', events.length, 'historical messages')
|
||||||
|
|
||||||
|
// Process historical messages
|
||||||
|
for (const event of events) {
|
||||||
|
try {
|
||||||
|
const isFromUs = event.pubkey === userPubkey
|
||||||
|
const peerPubkey = isFromUs
|
||||||
|
? event.tags.find(tag => tag[0] === 'p')?.[1] // Get recipient from tag
|
||||||
|
: event.pubkey // Sender is the peer
|
||||||
|
|
||||||
|
if (!peerPubkey || peerPubkey === userPubkey) continue
|
||||||
|
|
||||||
|
// Decrypt the message
|
||||||
|
const decryptedContent = await nip04.decrypt(userPrivkey, peerPubkey, event.content)
|
||||||
|
|
||||||
|
// Create a chat message
|
||||||
|
const message: ChatMessage = {
|
||||||
|
id: event.id,
|
||||||
|
content: decryptedContent,
|
||||||
|
created_at: event.created_at,
|
||||||
|
sent: isFromUs,
|
||||||
|
pubkey: event.pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the message (will avoid duplicates)
|
||||||
|
this.addMessage(peerPubkey, message)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to decrypt historical message:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Message history loaded successfully')
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load message history:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup subscription for incoming messages
|
||||||
|
private setupMessageSubscription(): void {
|
||||||
|
try {
|
||||||
|
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any
|
||||||
|
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any
|
||||||
|
|
||||||
|
if (!relayHub || !authService?.user?.value?.pubkey) {
|
||||||
|
console.warn('💬 Cannot setup message subscription: missing services')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!relayHub.isConnected) {
|
||||||
|
console.warn('💬 RelayHub not connected, waiting for connection...')
|
||||||
|
// Listen for connection event
|
||||||
|
relayHub.on('connected', () => {
|
||||||
|
console.log('💬 RelayHub connected, setting up message subscription...')
|
||||||
|
this.setupMessageSubscription()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const userPubkey = authService.user.value.pubkey
|
||||||
|
const userPrivkey = authService.user.value.prvkey
|
||||||
|
|
||||||
|
// Subscribe to encrypted direct messages (kind 4) addressed to this user
|
||||||
|
this.subscriptionUnsubscriber = relayHub.subscribe({
|
||||||
|
id: 'chat-messages',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
kinds: [4], // Encrypted direct messages
|
||||||
|
'#p': [userPubkey] // Messages tagged with our pubkey
|
||||||
|
}
|
||||||
|
],
|
||||||
|
onEvent: async (event: Event) => {
|
||||||
|
try {
|
||||||
|
// Find the sender's pubkey from the event
|
||||||
|
const senderPubkey = event.pubkey
|
||||||
|
|
||||||
|
// Skip our own messages
|
||||||
|
if (senderPubkey === userPubkey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt the message
|
||||||
|
const decryptedContent = await nip04.decrypt(userPrivkey, senderPubkey, event.content)
|
||||||
|
|
||||||
|
// Create a chat message
|
||||||
|
const message: ChatMessage = {
|
||||||
|
id: event.id,
|
||||||
|
content: decryptedContent,
|
||||||
|
created_at: event.created_at,
|
||||||
|
sent: false,
|
||||||
|
pubkey: senderPubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have a peer record for the sender
|
||||||
|
this.addPeer(senderPubkey)
|
||||||
|
|
||||||
|
// Add the message
|
||||||
|
this.addMessage(senderPubkey, message)
|
||||||
|
|
||||||
|
console.log('Received encrypted message from:', senderPubkey.slice(0, 8))
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to decrypt incoming message:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEose: () => {
|
||||||
|
console.log('Chat message subscription EOSE received')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Chat message subscription set up successfully')
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to setup message subscription:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
|
// Unsubscribe from message subscription
|
||||||
|
if (this.subscriptionUnsubscriber) {
|
||||||
|
this.subscriptionUnsubscriber()
|
||||||
|
}
|
||||||
|
|
||||||
this.messages.value.clear()
|
this.messages.value.clear()
|
||||||
this.peers.value.clear()
|
this.peers.value.clear()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue