Update conversations.php

This commit is contained in:
Lizandro Guarnizo
2026-01-25 23:39:42 -05:00
parent 7b5916ea9b
commit 5ff209cbe5
+235 -16
View File
@@ -564,6 +564,22 @@
.notification-toast.urgent { border-left: 4px solid #e74c3c; }
/* Visual destacado para notificaciones de tipo "attention" (ej. usuario subió documentos) */
.notification-toast.attention {
background: linear-gradient(90deg, #fff7e6, #fff3e0);
border: 1px solid rgba(255,165,0,0.18);
color: #333;
box-shadow: 0 8px 24px rgba(255,165,0,0.06);
min-width: 240px;
}
.notification-toast.attention .nt-icon {
font-size: 18px;
margin-right: 8px;
transform: translateY(-1px);
color: #ff9900;
}
.notification-toast.attention .nt-actions .btn { min-width: 70px; }
.message-template {
background: #fffacd;
border: 1px solid #f0e68c;
@@ -572,6 +588,31 @@
margin-bottom: 10px;
font-size: 13px;
}
.message-template.system {
background: linear-gradient(90deg,#fff4e6,#fffaf0);
border: 1px solid rgba(255,165,0,0.18);
color: #5c3a00;
font-weight: 600;
display: flex;
gap: 8px;
align-items: center;
animation: systemPop 320ms ease;
}
.message-template.system .mt-icon { font-size:16px; margin-right:6px; }
@keyframes systemPop { from { transform: translateY(6px); opacity:0 } to { transform: translateY(0); opacity:1 } }
/* Highlight animation for newly inserted system messages */
.message-template.system.system-highlight {
box-shadow: 0 10px 30px rgba(255,165,0,0.12);
border-color: rgba(255,165,0,0.28);
transform-origin: center left;
animation: pulseHighlight 1s ease both;
}
@keyframes pulseHighlight {
0% { transform: translateY(6px) scale(.995); opacity: 0.95; }
50% { transform: translateY(0) scale(1.01); opacity: 1; }
100% { transform: translateY(0) scale(1); opacity: 1; }
}
.unread-count {
background: var(--whatsapp-green);
@@ -1066,12 +1107,20 @@
const toast = document.createElement('div');
// compact by default
toast.className = 'notification-toast small';
// Detect special "attention" notifications (e.g. user sent documents or similar system events)
const isAttention = (
// explicit attention flags
notification.type === 'attention' || notification.system === 'attention' || notification.level === 'attention' || notification.attention ||
// known event types/tags for user-sent documents
notification.type === 'usersentdocuments' || notification.tag === 'usersentdocuments' || notification.system === 'usersentdocuments'
);
if (notification.cool || notification.type === 'cool') toast.classList.add('cool');
if (notification.level === 'urgent' || notification.urgent) toast.classList.add('urgent');
if (isAttention) toast.classList.add('attention');
const iconSpan = document.createElement('span');
iconSpan.className = 'nt-icon';
iconSpan.textContent = notification.icon || (notification.cool ? '✨' : (notification.level === 'urgent' ? '⚠️' : '🔔'));
iconSpan.textContent = notification.icon || (isAttention ? '📎' : (notification.cool ? '✨' : (notification.level === 'urgent' ? '⚠️' : '🔔')));
toast.appendChild(iconSpan);
const msg = document.createElement('div');
@@ -1090,8 +1139,8 @@
};
const openBtn = document.createElement('button');
openBtn.className = notification.cool ? 'btn btn-sm btn-light' : 'btn btn-sm btn-primary';
openBtn.textContent = 'Abrir';
openBtn.className = notification.cool ? 'btn btn-sm btn-light' : (isAttention ? 'btn btn-sm btn-warning' : 'btn btn-sm btn-primary');
openBtn.textContent = (notification.open_label && notification.open_label !== 'Ver') ? notification.open_label : 'Abrir';
openBtn.onclick = async () => {
// Try ack on server; if fails, schedule retry. Always mark as dismissed locally so it won't reappear.
try {
@@ -1104,11 +1153,14 @@
}
} catch (e) { this._pendingAck.set(nid, notification); }
removeToastLocal();
// navigate to conv if present
// navigate to conv or open media 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 fileUrl = data.file_url || data.url || null;
if (fileUrl && this.currentUserId && String(this.currentUserId) === String(userId)) {
try { openMediaLightbox(fileUrl, data.file_name || ''); } catch (e) { console.warn('openMediaLightbox failed', e); }
} else if (userId) {
const conv = this.conversations.find(c => c.user_id == userId);
if (conv) {
this.openConversation(conv.user_id, conv.name, conv.phone_number);
@@ -1116,7 +1168,7 @@
this.openConversation(userId, 'Usuario', '');
}
}
};
};
const dismiss = document.createElement('button');
dismiss.className = 'btn btn-sm btn-outline-secondary';
@@ -1141,6 +1193,15 @@
container.appendChild(toast);
// If attention notification and related to current open conversation, insert a system message into the chat view
try {
const dataObj = notification.data ? (typeof notification.data === 'string' ? JSON.parse(notification.data) : notification.data) : {};
const userId = dataObj.user_id || notification.user_id;
if (isAttention && userId && this.currentUserId && String(this.currentUserId) === String(userId) && typeof this.showSystemNotificationInChat === 'function') {
this.showSystemNotificationInChat(notification);
}
} catch (e) { console.warn('showNotificationToast -> showSystemNotificationInChat failed', e); }
// Auto remove (shorter for compact notifications) with minimum enforced
const _minToastDuration = 3000;
let timeout = notification.duration ? Number(notification.duration) : (notification.cool ? 15000 : 8000);
@@ -1163,6 +1224,82 @@
}, timeout);
}
showSystemNotificationInChat(notification) {
try {
const data = notification.data ? (typeof notification.data === 'string' ? JSON.parse(notification.data) : notification.data) : {};
const userId = data.user_id || notification.user_id;
// Only show in currently open conversation
if (!userId || String(this.currentUserId) !== String(userId)) return;
const container = document.getElementById('chat-conversations');
if (!container) return;
const el = document.createElement('div');
el.className = 'message incoming';
el.dataset.notificationId = String(notification.id || '');
const bubble = document.createElement('div');
bubble.className = 'message-bubble message-template system';
// Friendly title and message (prefer explicit fields if present)
const title = notification.title || data.title || 'Documentos recibidos';
const msg = notification.message || data.text || data.message || (data.summary || 'El usuario envió documentos.');
// File info (if available)
const fileName = data.file_name || data.filename || data.file || null;
const fileUrl = data.file_url || data.url || null;
const fileSize = data.file_size || data.size || null;
// Build inner HTML
const parts = [];
parts.push(`<span class="mt-icon">📎</span>`);
parts.push(`<div style="flex:1">`);
parts.push(`<div style="font-weight:700; margin-bottom:6px">${escapeHtml(String(title))}</div>`);
parts.push(`<div style="font-size:13px; color:#444">${escapeHtml(String(msg))}</div>`);
if (fileName) {
parts.push(`<div style="margin-top:8px; display:flex; gap:8px; align-items:center;">
<i class="${getFileIcon(fileName)}"></i>
<div style="display:flex; flex-direction:column;">
<div class="document-name">${escapeHtml(fileName)}</div>
<div class="document-size text-muted" style="font-size:12px">${fileSize ? this.formatFileSize(Number(fileSize)) : ''}</div>
</div>
</div>`);
}
parts.push(`</div>`);
// Actions
parts.push(`<div style="margin-left:8px; display:flex; gap:6px; align-items:center">`);
if (fileUrl) {
parts.push(`<a href="${escapeHtml(fileUrl)}" target="_blank" class="btn btn-sm btn-outline-primary">Abrir</a>`);
parts.push(`<button class="btn btn-sm btn-primary download-doc-btn">Descargar</button>`);
}
parts.push(`</div>`);
bubble.innerHTML = parts.join('');
el.appendChild(bubble);
container.appendChild(el);
this.scrollToBottom();
// Attach handlers
const downloadBtn = bubble.querySelector('.download-doc-btn');
if (downloadBtn) downloadBtn.addEventListener('click', () => {
if (fileUrl) {
const a = document.createElement('a');
a.href = fileUrl;
a.download = fileName || '';
document.body.appendChild(a);
a.click();
a.remove();
}
});
} catch (e) {
console.warn('showSystemNotificationInChat failed', e);
}
}
setupEventListeners() {
// Búsqueda de conversaciones
document.getElementById('search-input').addEventListener('input', (e) => {
@@ -2602,21 +2739,75 @@
replyHtml = `<div class="reply-preview">En respuesta a: ${window.escapeHtml(previewText)}</div>`;
}
const mediaPresent = (msg.local_thumb || msg.local_file || msg.media_url_external || msg.media_url);
let content = mediaPresent
? (window.renderMediaMessage ? window.renderMediaMessage(msg) : (window.formatMessageContent ? window.formatMessageContent(msg.content || '[Mensaje]') : window.escapeHtml(msg.content || '[Mensaje]')))
: (window.formatMessageContent ? window.formatMessageContent(msg.content || '') : window.escapeHtml(msg.content || ''));
// If after formatting the content is empty (spaces or nothing), show a clearer placeholder.
// But allow pure media-only content (images/audio/video) to render instead of being treated as empty.
// Try to render system-style messages when the message content is a JSON payload
let content = '';
let isSystemJson = false;
try {
const tmp = (content || '').replace(/<[^>]*>/g, '').trim(); // strip tags and check
const hasMediaTag = /<(img|audio|video|source|a[^>]*download|div\s+class=["']?message-media)/i.test(content || '');
if (!tmp && !hasMediaTag) {
console.warn('Rendered message content empty, replacing with placeholder', mid, msg.id, msg.created_at);
content = '<em>(Mensaje sin texto)</em>';
const raw = (typeof msg.content === 'string') ? msg.content.trim() : '';
if (!mediaPresent && raw && (raw.startsWith('{') || raw.startsWith('['))) {
try {
const parsed = JSON.parse(raw);
const isSys = parsed && (parsed.type === 'usersentdocuments' || parsed.system === 'usersentdocuments' || parsed.type === 'attention' || parsed.system === 'attention' || parsed.attention);
if (isSys) {
isSystemJson = true;
const title = parsed.title || parsed.message || parsed.text || parsed.summary || 'Documentos recibidos';
const msgText = parsed.message || parsed.text || parsed.summary || '';
const fileName = parsed.file_name || parsed.filename || parsed.file || null;
const fileUrl = parsed.file_url || parsed.url || null;
const fileSize = parsed.file_size || parsed.size || null;
const parts = [];
parts.push(`<div class="message-template system" style="display:flex; gap:8px; align-items:center">`);
parts.push(`<span class="mt-icon">📎</span>`);
parts.push(`<div style="flex:1">`);
parts.push(`<div style="font-weight:700; margin-bottom:6px">${escapeHtml(String(title))}</div>`);
if (msgText) parts.push(`<div style="font-size:13px; color:#444">${escapeHtml(String(msgText))}</div>`);
if (fileName) {
parts.push(`<div style="margin-top:8px; display:flex; gap:8px; align-items:center;">
<i class="${getFileIcon(fileName)}"></i>
<div style="display:flex; flex-direction:column;">
<div class="document-name">${escapeHtml(fileName)}</div>
<div class="document-size text-muted" style="font-size:12px">${fileSize ? this.formatFileSize(Number(fileSize)) : ''}</div>
</div>
</div>`);
}
parts.push(`</div>`);
parts.push(`<div style="margin-left:8px; display:flex; gap:6px; align-items:center">`);
if (fileUrl) {
parts.push(`<a href="${escapeHtml(fileUrl)}" target="_blank" class="btn btn-sm btn-outline-primary">Abrir</a>`);
parts.push(`<button class="btn btn-sm btn-primary download-doc-btn">Descargar</button>`);
}
parts.push(`</div>`);
parts.push(`</div>`);
content = parts.join('');
}
} catch (e) {
// not JSON or parse error — fall back to normal rendering
}
}
} catch (e) { /* ignore */ }
if (!isSystemJson) {
content = mediaPresent
? (window.renderMediaMessage ? window.renderMediaMessage(msg) : (window.formatMessageContent ? window.formatMessageContent(msg.content || '[Mensaje]') : window.escapeHtml(msg.content || '[Mensaje]')))
: (window.formatMessageContent ? window.formatMessageContent(msg.content || '') : window.escapeHtml(msg.content || ''));
// If after formatting the content is empty (spaces or nothing), show a clearer placeholder.
// But allow pure media-only content (images/audio/video) to render instead of being treated as empty.
try {
const tmp = (content || '').replace(/<[^>]*>/g, '').trim(); // strip tags and check
const hasMediaTag = /<(img|audio|video|source|a[^>]*download|div\s+class=["']?message-media)/i.test(content || '');
if (!tmp && !hasMediaTag) {
console.warn('Rendered message content empty, replacing with placeholder', mid, msg.id, msg.created_at);
content = '<em>(Mensaje sin texto)</em>';
}
} catch (e) { /* ignore */ }
} else {
// If it is the system JSON, we may want to highlight it briefly after insertion later
}
const time = msg.created_at ? new Date(msg.created_at).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' }) : '';
const statusIcon = this.getStatusIcon(msg.status);
const reactionHtml = msg.reaction_emoji ? `<div class="reaction-badge">${msg.reaction_emoji}</div>` : '';
@@ -2640,6 +2831,34 @@
} else {
container.appendChild(div);
}
// Attach handlers for system-like messages rendered from JSON (view/download buttons + highlight)
try {
const raw = (typeof msg.content === 'string') ? msg.content.trim() : '';
if (raw && (raw.startsWith('{') || raw.startsWith('['))) {
const parsed = JSON.parse(raw);
const isSys = parsed && (parsed.type === 'usersentdocuments' || parsed.system === 'usersentdocuments' || parsed.type === 'attention' || parsed.system === 'attention' || parsed.attention);
if (isSys) {
const bubble = div.querySelector('.message-template.system') || div.querySelector('.message-template');
if (bubble) {
const viewBtn = bubble.querySelector('.view-docs-btn');
const downloadBtn = bubble.querySelector('.download-doc-btn');
const fileUrl = parsed.file_url || parsed.url || null;
const userId = parsed.user_id || parsed.data && parsed.data.user_id || null;
if (downloadBtn) downloadBtn.addEventListener('click', () => {
if (fileUrl) {
const a = document.createElement('a'); a.href = fileUrl; a.download = parsed.file_name || ''; document.body.appendChild(a); a.click(); a.remove();
}
});
// brief visual highlight
bubble.classList.add('system-highlight');
setTimeout(() => bubble.classList.remove('system-highlight'), 4200);
}
}
}
} catch (e) { /* ignore */ }
} catch (e) { console.warn('create message failed', e); }
}
}