diff --git a/conversations.php b/conversations.php index f247cc9..f8be997 100644 --- a/conversations.php +++ b/conversations.php @@ -772,6 +772,10 @@ this.conversations = []; // Track shown notifications to avoid duplicates from polling this._shownNotifications = new Set(); + // Track notifications the user dismissed/read locally so they don't reappear + this._dismissedNotifications = new Set(); + // Map of pending ack notifications to retry marking as read (nid => notification) + this._pendingAck = new Map(); // Message pagination / loading state this.messageLimit = 50; this.loadingMessages = false; @@ -793,6 +797,8 @@ this.setupEventListeners(); this.setupAutoRefresh(); this.setupNotificationPolling(); + // start background retry loop to ack dismissed notifications on server + this.setupNotificationAckRetry && this.setupNotificationAckRetry(); } setupNotificationPolling() { @@ -820,6 +826,48 @@ } } + // Try to mark a notification as read on the server. Returns true on success. + async ackNotification(notification) { + if (!notification || !notification.id) return true; // nothing to ack on server + try { + const resp = await fetch('api/mark_notification_read.php', { + method: 'POST', + headers: {'Content-Type':'application/json'}, + body: JSON.stringify({ id: notification.id }) + }); + if (!resp.ok) { + console.warn('ackNotification: server responded with', resp.status); + return false; + } + const j = await resp.json().catch(() => null); + return !!(j && j.success) || resp.ok; + } catch (e) { + console.warn('ackNotification failed', e); + return false; + } + } + + setupNotificationAckRetry() { + if (this._ackInterval) return; + this._ackInterval = setInterval(async () => { + if (this._pendingAck.size === 0) return; + for (const [nid, notification] of Array.from(this._pendingAck.entries())) { + try { + const ok = await this.ackNotification(notification); + if (ok) { + this._pendingAck.delete(nid); + this._dismissedNotifications.add(nid); + console.debug('Ack retry succeeded for', nid); + } else { + console.debug('Ack retry still failing for', nid); + } + } catch (e) { + console.warn('Ack retry error for', nid, e); + } + } + }, 30000); // every 30s + } + async showNotificationToast(notification) { // Create or reuse container const containerId = 'notification-toasts'; @@ -860,7 +908,7 @@ const actions = document.createElement('div'); actions.className = 'nt-actions'; - const removeToast = () => { + const removeToastLocal = () => { if (toast.parentNode) toast.remove(); if (this._shownNotifications.has(nid)) this._shownNotifications.delete(nid); }; @@ -868,6 +916,33 @@ const openBtn = document.createElement('button'); openBtn.className = notification.cool ? 'btn btn-sm btn-light' : 'btn btn-sm btn-primary'; openBtn.textContent = 'Abrir'; + openBtn.onclick = async () => { + // Try ack on server; if fails, schedule retry. Always mark as dismissed locally so it won't reappear. + try { + const ok = await this.ackNotification(notification); + if (ok) { + this._dismissedNotifications.add(nid); + this._pendingAck.delete(nid); + } else { + this._pendingAck.set(nid, notification); + } + } catch (e) { this._pendingAck.set(nid, notification); } + removeToastLocal(); + // navigate to conv if present + let data = {}; + try { data = notification.data ? JSON.parse(notification.data) : {}; } catch(e) {} + const userId = data.user_id || notification.user_id; + if (userId) { + const conv = this.conversations.find(c => c.user_id == userId); + if (conv) { + this.openConversation(conv.user_id, conv.name, conv.phone_number); + } else { + this.openConversation(userId, 'Usuario', ''); + } + } + }; + openBtn.className = notification.cool ? 'btn btn-sm btn-light' : 'btn btn-sm btn-primary'; + openBtn.textContent = 'Abrir'; openBtn.onclick = async () => { try { await fetch('api/mark_notification_read.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({id: notification.id})}); @@ -892,9 +967,15 @@ dismiss.title = 'Descartar'; dismiss.onclick = async () => { try { - await fetch('api/mark_notification_read.php', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({id: notification.id})}); - } catch(e) { console.warn('mark_notification_read failed', e); } - removeToast(); + const ok = await this.ackNotification(notification); + if (ok) { + this._dismissedNotifications.add(nid); + this._pendingAck.delete(nid); + } else { + this._pendingAck.set(nid, notification); + } + } catch(e) { this._pendingAck.set(nid, notification); } + removeToastLocal(); }; actions.appendChild(openBtn); @@ -908,7 +989,21 @@ let timeout = notification.duration ? Number(notification.duration) : (notification.cool ? 15000 : 8000); if (!isFinite(timeout) || timeout <= 0) timeout = (notification.cool ? 15000 : 8000); timeout = Math.max(timeout, _minToastDuration); - setTimeout(removeToast, timeout); + setTimeout(() => { + // on automatic timeout, attempt ack and schedule retry if needed + (async () => { + try { + const ok = await this.ackNotification(notification); + if (ok) { + this._dismissedNotifications.add(nid); + this._pendingAck.delete(nid); + } else { + this._pendingAck.set(nid, notification); + } + } catch (e) { this._pendingAck.set(nid, notification); } + removeToastLocal(); + })(); + }, timeout); } setupEventListeners() {