|
|
|
|
@@ -37,18 +37,34 @@ let pollInterval: ReturnType<typeof setInterval> | null = null
|
|
|
|
|
// The Public channel (always available on Meshcore)
|
|
|
|
|
const publicChannel = { index: 0, name: 'Public' }
|
|
|
|
|
|
|
|
|
|
// Channel contact_id convention: matches backend u32::MAX - channel_index
|
|
|
|
|
function channelContactId(channelIndex: number): number {
|
|
|
|
|
return 4294967295 - channelIndex // u32::MAX - index
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Archipelago Channel — Tor-based messaging to all federated/peered nodes
|
|
|
|
|
const archChannelActive = ref(false)
|
|
|
|
|
const archMessages = ref<Array<{ from_pubkey: string; message: string; timestamp: string }>>([])
|
|
|
|
|
const archMessages = ref<Array<{ from_pubkey: string; from_name?: string; message: string; timestamp: string; direction?: string }>>([])
|
|
|
|
|
const archUnread = ref(0)
|
|
|
|
|
let archPollInterval: ReturnType<typeof setInterval> | null = null
|
|
|
|
|
// Federation node name cache: pubkey -> node name
|
|
|
|
|
const fedNodeNames = ref<Record<string, string>>({})
|
|
|
|
|
|
|
|
|
|
function openArchChannel() {
|
|
|
|
|
async function openArchChannel() {
|
|
|
|
|
activeChatPeer.value = null
|
|
|
|
|
activeChatChannel.value = null
|
|
|
|
|
archChannelActive.value = true
|
|
|
|
|
archUnread.value = 0
|
|
|
|
|
mobileShowChat.value = true
|
|
|
|
|
// Load federation node names for resolving pubkeys to names
|
|
|
|
|
try {
|
|
|
|
|
const res = await rpcClient.federationListNodes()
|
|
|
|
|
const names: Record<string, string> = {}
|
|
|
|
|
for (const node of res.nodes) {
|
|
|
|
|
if (node.pubkey) names[node.pubkey] = node.name || node.did.slice(0, 12) + '...'
|
|
|
|
|
}
|
|
|
|
|
fedNodeNames.value = names
|
|
|
|
|
} catch { /* non-fatal */ }
|
|
|
|
|
loadArchMessages()
|
|
|
|
|
if (!archPollInterval) {
|
|
|
|
|
archPollInterval = setInterval(loadArchMessages, 15000)
|
|
|
|
|
@@ -58,7 +74,18 @@ function openArchChannel() {
|
|
|
|
|
async function loadArchMessages() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await rpcClient.getReceivedMessages()
|
|
|
|
|
archMessages.value = res.messages || []
|
|
|
|
|
const newMessages = res.messages || []
|
|
|
|
|
// Track unread: count new received messages since last load
|
|
|
|
|
if (archMessages.value.length > 0 && !archChannelActive.value) {
|
|
|
|
|
const newReceived = newMessages.filter(
|
|
|
|
|
m => m.direction !== 'sent' && m.from_pubkey !== 'me'
|
|
|
|
|
&& !archMessages.value.some(existing =>
|
|
|
|
|
existing.from_pubkey === m.from_pubkey && existing.timestamp === m.timestamp
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
archUnread.value += newReceived.length
|
|
|
|
|
}
|
|
|
|
|
archMessages.value = newMessages
|
|
|
|
|
} catch { /* silent */ }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -142,6 +169,11 @@ async function handleToggleOffGrid() {
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
window.addEventListener('resize', handleResize)
|
|
|
|
|
await Promise.all([mesh.refreshAll(), transport.fetchStatus()])
|
|
|
|
|
// Start background polling for Archipelago (Tor) messages so unread count works
|
|
|
|
|
loadArchMessages()
|
|
|
|
|
if (!archPollInterval) {
|
|
|
|
|
archPollInterval = setInterval(loadArchMessages, 15000)
|
|
|
|
|
}
|
|
|
|
|
pollInterval = setInterval(() => {
|
|
|
|
|
mesh.fetchStatus()
|
|
|
|
|
mesh.fetchPeers()
|
|
|
|
|
@@ -154,6 +186,7 @@ onMounted(async () => {
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
window.removeEventListener('resize', handleResize)
|
|
|
|
|
if (pollInterval) clearInterval(pollInterval)
|
|
|
|
|
if (archPollInterval) { clearInterval(archPollInterval); archPollInterval = null }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Active chat name for the header
|
|
|
|
|
@@ -177,11 +210,21 @@ const hasActiveChat = computed(() => !!activeChatPeer.value || !!activeChatChann
|
|
|
|
|
const chatMessages = computed(() => {
|
|
|
|
|
if (archChannelActive.value) {
|
|
|
|
|
return archMessages.value.map((m, i) => {
|
|
|
|
|
const isSent = (m as Record<string, unknown>).direction === 'sent' || m.from_pubkey === 'me'
|
|
|
|
|
const isSent = m.direction === 'sent' || m.from_pubkey === 'me'
|
|
|
|
|
let peerName = 'Unknown'
|
|
|
|
|
if (isSent) {
|
|
|
|
|
peerName = 'You'
|
|
|
|
|
} else if (m.from_name) {
|
|
|
|
|
peerName = m.from_name
|
|
|
|
|
} else if (fedNodeNames.value[m.from_pubkey]) {
|
|
|
|
|
peerName = fedNodeNames.value[m.from_pubkey]
|
|
|
|
|
} else {
|
|
|
|
|
peerName = m.from_pubkey.slice(0, 12) + '...'
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
id: i,
|
|
|
|
|
peer_contact_id: -99,
|
|
|
|
|
peer_name: isSent ? 'You' : (m.from_pubkey.slice(0, 12) + '...'),
|
|
|
|
|
peer_name: peerName,
|
|
|
|
|
direction: (isSent ? 'sent' : 'received') as 'sent' | 'received',
|
|
|
|
|
plaintext: m.message,
|
|
|
|
|
timestamp: m.timestamp,
|
|
|
|
|
@@ -193,7 +236,7 @@ const chatMessages = computed(() => {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if (activeChatChannel.value) {
|
|
|
|
|
const chanId = -(activeChatChannel.value.index + 1)
|
|
|
|
|
const chanId = channelContactId(activeChatChannel.value.index)
|
|
|
|
|
return mesh.messages.filter(m => m.peer_contact_id === chanId)
|
|
|
|
|
}
|
|
|
|
|
if (activeChatPeer.value) {
|
|
|
|
|
@@ -236,6 +279,7 @@ function openChannelChat(channel: { index: number; name: string }) {
|
|
|
|
|
messageText.value = ''
|
|
|
|
|
activeTab.value = 'chat'
|
|
|
|
|
mobileShowChat.value = true
|
|
|
|
|
mesh.markChatRead(channelContactId(channel.index))
|
|
|
|
|
nextTick(() => scrollChatToBottom())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -254,12 +298,18 @@ async function handleSendMessage() {
|
|
|
|
|
nextTick(() => scrollChatToBottom())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (!activeChatPeer.value || !messageText.value.trim()) return
|
|
|
|
|
if (!messageText.value.trim()) return
|
|
|
|
|
sendError.value = ''
|
|
|
|
|
try {
|
|
|
|
|
await mesh.sendMessage(activeChatPeer.value.contact_id, messageText.value)
|
|
|
|
|
messageText.value = ''
|
|
|
|
|
nextTick(() => scrollChatToBottom())
|
|
|
|
|
if (activeChatChannel.value) {
|
|
|
|
|
await mesh.sendChannelMessage(activeChatChannel.value.index, messageText.value)
|
|
|
|
|
messageText.value = ''
|
|
|
|
|
nextTick(() => scrollChatToBottom())
|
|
|
|
|
} else if (activeChatPeer.value) {
|
|
|
|
|
await mesh.sendMessage(activeChatPeer.value.contact_id, messageText.value)
|
|
|
|
|
messageText.value = ''
|
|
|
|
|
nextTick(() => scrollChatToBottom())
|
|
|
|
|
}
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
sendError.value = err instanceof Error ? err.message : 'Send failed'
|
|
|
|
|
}
|
|
|
|
|
@@ -467,6 +517,7 @@ function truncatePubkey(hex: string | null): string {
|
|
|
|
|
<div class="mesh-peer-name">Public</div>
|
|
|
|
|
<div class="mesh-peer-sub">Mesh radio</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span v-if="mesh.unreadCounts[channelContactId(0)]" class="ml-auto text-[10px] px-1.5 py-0.5 rounded-full bg-orange-500/30 text-orange-300">{{ mesh.unreadCounts[channelContactId(0)] }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
v-for="peer in sortedPeers" :key="peer.contact_id"
|
|
|
|
|
|