Update conversations.php
This commit is contained in:
+235
-16
@@ -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); }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user