190 lines
5.1 KiB
TypeScript
Raw Normal View History

2026-03-17 00:03:08 +00:00
// Pinia store for mesh networking state (Meshcore LoRa)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { rpcClient } from '@/api/rpc-client'
export interface MeshStatus {
enabled: boolean
device_type: string
device_path: string | null
device_connected: boolean
firmware_version: string | null
self_node_id: number | null
self_advert_name: string | null
peer_count: number
channel_name: string
messages_sent: number
messages_received: number
detected_devices?: string[]
}
export interface MeshPeer {
contact_id: number
advert_name: string
did: string | null
pubkey_hex: string | null
rssi: number | null
snr: number | null
last_heard: string
hops: number
}
export interface MeshChannel {
index: number
name: string
has_secret: boolean
}
export interface MeshMessage {
id: number
direction: 'sent' | 'received'
peer_contact_id: number
peer_name: string | null
plaintext: string
timestamp: string
delivered: boolean
encrypted: boolean
}
export const useMeshStore = defineStore('mesh', () => {
const status = ref<MeshStatus | null>(null)
const peers = ref<MeshPeer[]>([])
const messages = ref<MeshMessage[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const sending = ref(false)
// Track unread message counts per peer (contact_id -> count)
const unreadCounts = ref<Record<number, number>>({})
// Currently viewing chat for this contact_id (clears unread)
const viewingChatId = ref<number | null>(null)
// Total unread count for nav badge
const totalUnread = computed(() =>
Object.values(unreadCounts.value).reduce((a, b) => a + b, 0)
)
async function fetchStatus() {
try {
loading.value = true
error.value = null
const res = await rpcClient.call<MeshStatus>({ method: 'mesh.status' })
status.value = res
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : 'Failed to fetch mesh status'
} finally {
loading.value = false
}
}
async function fetchPeers() {
try {
const res = await rpcClient.call<{ peers: MeshPeer[]; count: number }>({
method: 'mesh.peers',
})
peers.value = res.peers
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : 'Failed to fetch mesh peers'
}
}
async function fetchMessages(limit?: number) {
try {
const res = await rpcClient.call<{ messages: MeshMessage[]; count: number }>({
method: 'mesh.messages',
params: limit ? { limit } : {},
})
// Detect new incoming messages and increment unread counts
const newMsgs = res.messages.filter(
m => m.direction === 'received' && !messages.value.some(existing => existing.id === m.id)
)
for (const msg of newMsgs) {
// Don't count as unread if we're currently viewing that chat
if (msg.peer_contact_id !== viewingChatId.value) {
unreadCounts.value[msg.peer_contact_id] = (unreadCounts.value[msg.peer_contact_id] || 0) + 1
}
}
messages.value = res.messages
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : 'Failed to fetch mesh messages'
}
}
function markChatRead(contactId: number) {
viewingChatId.value = contactId
delete unreadCounts.value[contactId]
}
function clearViewingChat() {
viewingChatId.value = null
}
async function sendMessage(contactId: number, message: string) {
try {
sending.value = true
error.value = null
const res = await rpcClient.call<{ sent: boolean; message_id: number; encrypted: boolean }>({
method: 'mesh.send',
params: { contact_id: contactId, message: message.trim() },
})
// Refresh messages after sending
if (res.sent) {
await fetchMessages()
}
return res
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : 'Failed to send mesh message'
throw err
} finally {
sending.value = false
}
}
async function broadcastIdentity() {
try {
error.value = null
await rpcClient.call<{ broadcast: boolean }>({ method: 'mesh.broadcast' })
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : 'Failed to broadcast identity'
throw err
}
}
async function configure(config: Partial<MeshStatus>) {
try {
error.value = null
await rpcClient.call<{ configured: boolean }>({
method: 'mesh.configure',
params: config,
})
await fetchStatus()
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : 'Failed to configure mesh'
throw err
}
}
async function refreshAll() {
await Promise.all([fetchStatus(), fetchPeers(), fetchMessages()])
}
return {
status,
peers,
messages,
loading,
error,
sending,
unreadCounts,
totalUnread,
fetchStatus,
fetchPeers,
fetchMessages,
sendMessage,
broadcastIdentity,
configure,
refreshAll,
markChatRead,
clearViewingChat,
}
})