Update conversations.php
This commit is contained in:
+235
-16
@@ -564,6 +564,22 @@
|
|||||||
|
|
||||||
.notification-toast.urgent { border-left: 4px solid #e74c3c; }
|
.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 {
|
.message-template {
|
||||||
background: #fffacd;
|
background: #fffacd;
|
||||||
border: 1px solid #f0e68c;
|
border: 1px solid #f0e68c;
|
||||||
@@ -572,6 +588,31 @@
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-size: 13px;
|
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 {
|
.unread-count {
|
||||||
background: var(--whatsapp-green);
|
background: var(--whatsapp-green);
|
||||||
@@ -1066,12 +1107,20 @@
|
|||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
// compact by default
|
// compact by default
|
||||||
toast.className = 'notification-toast small';
|
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.cool || notification.type === 'cool') toast.classList.add('cool');
|
||||||
if (notification.level === 'urgent' || notification.urgent) toast.classList.add('urgent');
|
if (notification.level === 'urgent' || notification.urgent) toast.classList.add('urgent');
|
||||||
|
if (isAttention) toast.classList.add('attention');
|
||||||
|
|
||||||
const iconSpan = document.createElement('span');
|
const iconSpan = document.createElement('span');
|
||||||
iconSpan.className = 'nt-icon';
|
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);
|
toast.appendChild(iconSpan);
|
||||||
|
|
||||||
const msg = document.createElement('div');
|
const msg = document.createElement('div');
|
||||||
@@ -1090,8 +1139,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openBtn = document.createElement('button');
|
const openBtn = document.createElement('button');
|
||||||
openBtn.className = notification.cool ? 'btn btn-sm btn-light' : 'btn btn-sm btn-primary';
|
openBtn.className = notification.cool ? 'btn btn-sm btn-light' : (isAttention ? 'btn btn-sm btn-warning' : 'btn btn-sm btn-primary');
|
||||||
openBtn.textContent = 'Abrir';
|
openBtn.textContent = (notification.open_label && notification.open_label !== 'Ver') ? notification.open_label : 'Abrir';
|
||||||
openBtn.onclick = async () => {
|
openBtn.onclick = async () => {
|
||||||
// Try ack on server; if fails, schedule retry. Always mark as dismissed locally so it won't reappear.
|
// Try ack on server; if fails, schedule retry. Always mark as dismissed locally so it won't reappear.
|
||||||
try {
|
try {
|
||||||
@@ -1104,11 +1153,14 @@
|
|||||||
}
|
}
|
||||||
} catch (e) { this._pendingAck.set(nid, notification); }
|
} catch (e) { this._pendingAck.set(nid, notification); }
|
||||||
removeToastLocal();
|
removeToastLocal();
|
||||||
// navigate to conv if present
|
// navigate to conv or open media if present
|
||||||
let data = {};
|
let data = {};
|
||||||
try { data = notification.data ? JSON.parse(notification.data) : {}; } catch(e) {}
|
try { data = notification.data ? JSON.parse(notification.data) : {}; } catch(e) {}
|
||||||
const userId = data.user_id || notification.user_id;
|
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);
|
const conv = this.conversations.find(c => c.user_id == userId);
|
||||||
if (conv) {
|
if (conv) {
|
||||||
this.openConversation(conv.user_id, conv.name, conv.phone_number);
|
this.openConversation(conv.user_id, conv.name, conv.phone_number);
|
||||||
@@ -1116,7 +1168,7 @@
|
|||||||
this.openConversation(userId, 'Usuario', '');
|
this.openConversation(userId, 'Usuario', '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const dismiss = document.createElement('button');
|
const dismiss = document.createElement('button');
|
||||||
dismiss.className = 'btn btn-sm btn-outline-secondary';
|
dismiss.className = 'btn btn-sm btn-outline-secondary';
|
||||||
@@ -1141,6 +1193,15 @@
|
|||||||
|
|
||||||
container.appendChild(toast);
|
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
|
// Auto remove (shorter for compact notifications) with minimum enforced
|
||||||
const _minToastDuration = 3000;
|
const _minToastDuration = 3000;
|
||||||
let timeout = notification.duration ? Number(notification.duration) : (notification.cool ? 15000 : 8000);
|
let timeout = notification.duration ? Number(notification.duration) : (notification.cool ? 15000 : 8000);
|
||||||
@@ -1163,6 +1224,82 @@
|
|||||||
}, timeout);
|
}, 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() {
|
setupEventListeners() {
|
||||||
// Búsqueda de conversaciones
|
// Búsqueda de conversaciones
|
||||||
document.getElementById('search-input').addEventListener('input', (e) => {
|
document.getElementById('search-input').addEventListener('input', (e) => {
|
||||||
@@ -2602,21 +2739,75 @@
|
|||||||
replyHtml = `<div class="reply-preview">En respuesta a: ${window.escapeHtml(previewText)}</div>`;
|
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);
|
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.
|
// Try to render system-style messages when the message content is a JSON payload
|
||||||
// But allow pure media-only content (images/audio/video) to render instead of being treated as empty.
|
let content = '';
|
||||||
|
let isSystemJson = false;
|
||||||
try {
|
try {
|
||||||
const tmp = (content || '').replace(/<[^>]*>/g, '').trim(); // strip tags and check
|
const raw = (typeof msg.content === 'string') ? msg.content.trim() : '';
|
||||||
const hasMediaTag = /<(img|audio|video|source|a[^>]*download|div\s+class=["']?message-media)/i.test(content || '');
|
if (!mediaPresent && raw && (raw.startsWith('{') || raw.startsWith('['))) {
|
||||||
if (!tmp && !hasMediaTag) {
|
try {
|
||||||
console.warn('Rendered message content empty, replacing with placeholder', mid, msg.id, msg.created_at);
|
const parsed = JSON.parse(raw);
|
||||||
content = '<em>(Mensaje sin texto)</em>';
|
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 */ }
|
} 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 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 statusIcon = this.getStatusIcon(msg.status);
|
||||||
const reactionHtml = msg.reaction_emoji ? `<div class="reaction-badge">${msg.reaction_emoji}</div>` : '';
|
const reactionHtml = msg.reaction_emoji ? `<div class="reaction-badge">${msg.reaction_emoji}</div>` : '';
|
||||||
@@ -2640,6 +2831,34 @@
|
|||||||
} else {
|
} else {
|
||||||
container.appendChild(div);
|
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); }
|
} catch (e) { console.warn('create message failed', e); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user