diff --git a/app/device_manager.py b/app/device_manager.py
index d2f9649..fdea069 100644
--- a/app/device_manager.py
+++ b/app/device_manager.py
@@ -395,6 +395,9 @@ class DeviceManager:
if self.socketio:
self.socketio.emit('ack', {
'expected_ack': expected_ack,
+ 'snr': data.get('snr'),
+ 'rssi': data.get('rssi'),
+ 'route_type': data.get('route_type', ''),
}, namespace='/chat')
except Exception as e:
diff --git a/app/static/js/dm.js b/app/static/js/dm.js
index 51ad209..e926262 100644
--- a/app/static/js/dm.js
+++ b/app/static/js/dm.js
@@ -11,6 +11,7 @@ let contactsList = []; // List of all contacts from device
let dmLastSeenTimestamps = {};
let autoRefreshInterval = null;
let lastMessageTimestamp = 0; // Track latest message timestamp for smart refresh
+let chatSocket = null; // SocketIO connection to /chat namespace
/**
* Get display-friendly name (truncate full pubkeys to short prefix)
@@ -21,6 +22,72 @@ function displayName(name) {
return name;
}
+/**
+ * Connect to SocketIO /chat namespace for real-time DM and ACK updates
+ */
+function connectChatSocket() {
+ if (typeof io === 'undefined') {
+ console.warn('SocketIO not available, falling back to polling only');
+ return;
+ }
+
+ const wsUrl = window.location.origin;
+ chatSocket = io(wsUrl + '/chat', {
+ transports: ['websocket', 'polling'],
+ reconnection: true,
+ reconnectionDelay: 2000,
+ reconnectionDelayMax: 10000,
+ });
+
+ chatSocket.on('connect', () => {
+ console.log('DM: SocketIO connected to /chat');
+ updateStatus('connected');
+ });
+
+ chatSocket.on('disconnect', () => {
+ console.log('DM: SocketIO disconnected');
+ });
+
+ // Real-time new DM message
+ chatSocket.on('new_message', (data) => {
+ if (data.type !== 'dm') return;
+
+ const msgPubkey = data.contact_pubkey || '';
+ const currentPubkey = currentConversationId ? currentConversationId.replace('pk_', '') : '';
+
+ if (currentPubkey && msgPubkey.startsWith(currentPubkey)) {
+ // Message is for current conversation — reload messages
+ loadMessages();
+ } else {
+ // Message is for a different conversation — refresh conversation list
+ loadConversations();
+ }
+ });
+
+ // Real-time ACK delivery confirmation
+ chatSocket.on('ack', (data) => {
+ if (!data.expected_ack) return;
+
+ // Find message with matching expected_ack in DOM and update status
+ const msgElements = document.querySelectorAll('#dmMessagesList .dm-message.own');
+ msgElements.forEach(el => {
+ const statusEl = el.querySelector(`.dm-status[data-ack="${data.expected_ack}"]`);
+ if (statusEl) {
+ statusEl.className = 'bi bi-check2 dm-status delivered';
+ const tooltip = [];
+ if (data.snr != null) tooltip.push(`SNR: ${data.snr}`);
+ if (data.route_type) tooltip.push(`Route: ${data.route_type}`);
+ statusEl.title = tooltip.length > 0 ? tooltip.join(', ') : 'Delivered';
+ }
+ });
+ });
+
+ // Real-time device status
+ chatSocket.on('device_status', (data) => {
+ updateStatus(data.connected ? 'connected' : 'disconnected');
+ });
+}
+
// Initialize on page load
document.addEventListener('DOMContentLoaded', async function() {
console.log('DM page initialized');
@@ -70,7 +137,10 @@ document.addEventListener('DOMContentLoaded', async function() {
// Load auto-retry config
loadAutoRetryConfig();
- // Setup auto-refresh
+ // Connect SocketIO for real-time updates
+ connectChatSocket();
+
+ // Setup auto-refresh (fallback, 60s interval since SocketIO handles primary updates)
setupAutoRefresh();
});
@@ -440,18 +510,19 @@ function displayMessages(messages) {
// Status icon for own messages
let statusIcon = '';
if (msg.is_own) {
+ const ackAttr = msg.expected_ack ? ` data-ack="${msg.expected_ack}"` : '';
if (msg.status === 'delivered') {
let title = 'Delivered';
if (msg.delivery_snr !== null && msg.delivery_snr !== undefined) {
title += `, SNR: ${msg.delivery_snr.toFixed(1)} dB`;
}
if (msg.delivery_route) title += ` (${msg.delivery_route})`;
- statusIcon = ``;
+ statusIcon = ``;
} else if (msg.status === 'pending') {
- statusIcon = '';
+ statusIcon = ``;
} else {
// No ACK received — show clickable "?" with explanation
- statusIcon = ``;
+ statusIcon = ``;
}
}
@@ -529,22 +600,9 @@ async function sendMessage() {
updateCharCounter();
showNotification('Message sent', 'success');
- // Reload messages to show sent message + ACK delivery status
- // Extended delays to cover auto-retry window (retries happen in background)
- const ackRefreshDelays = [2000, 8000, 20000, 40000, 60000];
- let ackRefreshIdx = 0;
- const scheduleAckRefresh = () => {
- if (ackRefreshIdx >= ackRefreshDelays.length) return;
- const delay = ackRefreshDelays[ackRefreshIdx++];
- setTimeout(async () => {
- await loadMessages();
- const ownMsgs = document.querySelectorAll('#dmMessagesList .dm-message.own');
- const lastOwn = ownMsgs.length > 0 ? ownMsgs[ownMsgs.length - 1] : null;
- const delivered = lastOwn && lastOwn.querySelector('.dm-status.delivered');
- if (!delivered) scheduleAckRefresh();
- }, delay);
- };
- scheduleAckRefresh();
+ // Reload messages once to show sent message
+ // ACK delivery updates arrive via SocketIO in real-time
+ await loadMessages();
} else {
showNotification('Failed to send: ' + data.error, 'danger');
}
@@ -562,7 +620,7 @@ async function sendMessage() {
* Only refreshes UI when new messages arrive
*/
function setupAutoRefresh() {
- const checkInterval = 10000; // 10 seconds
+ const checkInterval = 60000; // 60 seconds (SocketIO handles real-time updates)
autoRefreshInterval = setInterval(async () => {
// Reload conversations to update unread indicators
diff --git a/app/templates/dm.html b/app/templates/dm.html
index 16c3811..970f1f1 100644
--- a/app/templates/dm.html
+++ b/app/templates/dm.html
@@ -186,6 +186,9 @@
+
+
+