Update conversations.php

This commit is contained in:
Lizandro Guarnizo
2026-01-24 00:09:35 -05:00
parent 754f6da6c7
commit 81500d8ce7
+207 -105
View File
@@ -556,20 +556,59 @@
}
@media (max-width: 768px) {
/* Fullscreen sidebar that can slide in/out */
.chat-container { padding: 0; gap: 0; }
.chat-sidebar {
width: 100%;
position: absolute;
z-index: 1000;
position: fixed;
top: 0;
left: 0;
z-index: 1100;
height: 100vh;
transform: translateX(0);
transition: transform 240ms ease;
box-shadow: none;
}
/* Hidden by default on mobile. .open will show it */
.chat-sidebar.mobile-hidden { transform: translateX(-110%); }
.chat-main {
width: 100%;
margin-left: 0;
display: flex;
flex-direction: column;
height: 100vh;
}
.chat-sidebar.show-chat {
transform: translateX(-100%);
/* Header adjustment */
.chat-header { padding: 10px 12px; gap: 8px; }
.chat-header-avatar { width: 38px; height: 38px; }
/* Fix input to bottom to emulate mobile chat apps */
.chat-input {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 10px;
background: #fff;
z-index: 1200;
border-top: 1px solid #e9eef2;
}
/* Give chat area bottom padding so messages are not hidden under input */
.chat-conversations { padding-bottom: 120px; }
/* Compact avatars and message bubble width */
.conversation-avatar { width: 40px; height: 40px; }
.message-bubble { max-width: 85%; font-size: 15px; }
/* Quick replies panel adapt */
.quick-replies-panel { left: 8px; right: 8px; bottom: 70px; max-height: 160px; }
/* Show a small back button in header to open sidebar */
#sidebar-toggle { display: inline-flex; }
}
.reply-preview {
@@ -652,6 +691,7 @@
<div id="chat-area" style="display: none; height: 100%; flex-direction: column;">
<div class="chat-header">
<button id="sidebar-toggle" class="btn btn-sm btn-outline-light d-md-none me-2" style="display:none; border-radius:8px; padding:6px 8px;" title="Volver a conversaciones"><i class="fas fa-bars"></i></button>
<div class="chat-header-avatar" id="chat-avatar">
<i class="fas fa-user"></i>
</div>
@@ -966,25 +1006,6 @@
}
}
};
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})});
} catch(e) { console.warn('mark_notification_read failed', e); }
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', '');
}
}
removeToast();
};
const dismiss = document.createElement('button');
dismiss.className = 'btn btn-sm btn-outline-secondary';
@@ -1100,6 +1121,27 @@
});
}
// Mobile: sidebar toggle
const sidebar = document.querySelector('.chat-sidebar');
const sidebarToggle = document.getElementById('sidebar-toggle');
if (sidebarToggle && sidebar) {
sidebarToggle.addEventListener('click', (ev) => {
ev.preventDefault();
// toggle mobile-hidden class
sidebar.classList.toggle('mobile-hidden');
});
// ensure sidebar hidden by default on small screens
const applyInitialMobile = () => {
if (window.innerWidth <= 768) {
sidebar.classList.add('mobile-hidden');
} else {
sidebar.classList.remove('mobile-hidden');
}
};
applyInitialMobile();
window.addEventListener('resize', applyInitialMobile);
}
const stopRecBtn = document.getElementById('stop-recording');
if (stopRecBtn) stopRecBtn.addEventListener('click', () => this.stopRecording());
const cancelRecBtn = document.getElementById('cancel-recording');
@@ -1415,6 +1457,12 @@
const releaseBtn = document.getElementById('release-hold-btn');
const markUnreadBtn = document.getElementById('mark-unread-btn');
// On mobile, hide the sidebar when opening a conversation so chat occupies full screen
const sidebar = document.querySelector('.chat-sidebar');
if (sidebar && window.innerWidth <= 768) {
sidebar.classList.add('mobile-hidden');
}
if (markUnreadBtn) {
markUnreadBtn.style.display = 'inline-block';
markUnreadBtn.onclick = async () => {
@@ -2353,84 +2401,66 @@
}
renderMediaMessage(message) {
if (!message.media_url) return '';
const mediaType = message.message_type;
const mediaUrl = message.media_url;
const caption = message.content;
if (!message) return '';
const mediaType = message.message_type || message.media_type || 'text';
const mediaUrl = message.media_url || message.media_url_external || '';
const caption = message.content || '';
const localFile = message.local_file || '';
const localThumb = message.local_thumb || '';
const thumb = localThumb ? (`/${localThumb}`) : (localFile ? (`/${localFile}`) : (message.media_url_external || mediaUrl));
let full = '';
if (localFile) {
full = `/api/version/media-url.php?local=${encodeURIComponent(localFile)}`;
} else if (message.media_url_external) {
if (message.media_url_external.indexOf('api/get_media.php?id=') !== -1) {
const parts = message.media_url_external.split('id=');
const mid = parts[1] ? decodeURIComponent(parts[1]) : '';
full = `/api/version/media-url.php?id=${encodeURIComponent(mid)}`;
} else if (/^https?:\/\//i.test(message.media_url_external)) {
full = message.media_url_external;
} else {
full = `/api/version/media-url.php?url=${encodeURIComponent(message.media_url_external)}`;
}
} else {
full = mediaUrl || '';
}
switch (mediaType) {
case 'image':
// Usar miniatura local si existe, al abrir expandir con el archivo local completo o con media-url redirect
const thumb = msg.local_thumb ? (`/${msg.local_thumb}`) : (msg.local_file ? (`/${msg.local_file}`) : (msg.media_url_external || mediaUrl));
let full = '';
if (msg.local_file) {
full = `/api/version/media-url.php?local=${encodeURIComponent(msg.local_file)}`;
} else if (msg.media_url_external) {
// Si viene una ruta del tipo api/get_media.php?id=XXX extraer id
if (msg.media_url_external.indexOf('api/get_media.php?id=') !== -1) {
const parts = msg.media_url_external.split('id=');
const mid = parts[1] ? decodeURIComponent(parts[1]) : '';
full = `/api/version/media-url.php?id=${encodeURIComponent(mid)}`;
} else if (/^https?:\/\//i.test(msg.media_url_external)) {
// URL directa
full = msg.media_url_external;
} else {
// fallback: pasar como url
full = `/api/version/media-url.php?url=${encodeURIComponent(msg.media_url_external)}`;
}
} else {
full = mediaUrl || '';
}
return `
<div class="message-media">
<a href="#" onclick="openImageLightbox(${JSON.stringify(full)}); return false;" title="Abrir imagen">
<img src="${thumb}" alt="Imagen" style="cursor:zoom-in">
<a href="#" data-full="${escapeHtml(full)}" data-type="image" data-caption="${escapeHtml(caption)}" onclick="openMediaLightbox(this.dataset.full, this.dataset.type, this.dataset.caption); return false;" title="Abrir imagen">
<img src="${escapeHtml(thumb)}" alt="Imagen" style="cursor:zoom-in">
</a>
</div>
${caption ? `<div>${caption}</div>` : ''}
${caption ? `<div>${escapeHtml(caption)}</div>` : ''}
`;
case 'video':
let videoSrc = '';
if (msg.local_file) {
videoSrc = `/${msg.local_file}`;
} else if (msg.media_url_external) {
if (msg.media_url_external.indexOf('api/get_media.php?id=') !== -1) {
const parts = msg.media_url_external.split('id=');
const mid = parts[1] ? decodeURIComponent(parts[1]) : '';
videoSrc = `/api/version/media-url.php?id=${encodeURIComponent(mid)}`;
} else if (/^https?:\/\//i.test(msg.media_url_external)) {
videoSrc = msg.media_url_external;
} else {
videoSrc = `/api/version/media-url.php?url=${encodeURIComponent(msg.media_url_external)}`;
}
} else {
videoSrc = mediaUrl;
}
return `
<div class="message-media">
<video controls>
<source src="${videoSrc}" type="video/mp4">
Tu navegador no soporta video.
</video>
<a href="#" data-full="${escapeHtml(full)}" data-type="video" data-caption="${escapeHtml(caption)}" onclick="openMediaLightbox(this.dataset.full, this.dataset.type, this.dataset.caption); return false;" title="Abrir video">
<div style="position:relative; display:inline-block;">
<img src="${escapeHtml(thumb)}" alt="Video" style="cursor:zoom-in; max-width:100%; border-radius:6px;">
<div style="position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); font-size:28px; color:white; text-shadow:0 1px 6px rgba(0,0,0,0.6);"><i class="fas fa-play-circle"></i></div>
</div>
</a>
</div>
${caption ? `<div>${caption}</div>` : ''}
${caption ? `<div>${escapeHtml(caption)}</div>` : ''}
`;
case 'audio':
case 'audio': {
let audioSrc = '';
if (msg.local_file) {
audioSrc = `/${msg.local_file}`;
} else if (msg.media_url_external) {
if (msg.media_url_external.indexOf('api/get_media.php?id=') !== -1) {
const parts = msg.media_url_external.split('id=');
if (localFile) {
audioSrc = `/${localFile}`;
} else if (message.media_url_external) {
if (message.media_url_external.indexOf('api/get_media.php?id=') !== -1) {
const parts = message.media_url_external.split('id=');
const mid = parts[1] ? decodeURIComponent(parts[1]) : '';
audioSrc = `/api/version/media-url.php?id=${encodeURIComponent(mid)}`;
} else if (/^https?:\/\//i.test(msg.media_url_external)) {
audioSrc = msg.media_url_external;
} else if (/^https?:\/\//i.test(message.media_url_external)) {
audioSrc = message.media_url_external;
} else {
audioSrc = `/api/version/media-url.php?url=${encodeURIComponent(msg.media_url_external)}`;
audioSrc = `/api/version/media-url.php?url=${encodeURIComponent(message.media_url_external)}`;
}
} else {
audioSrc = mediaUrl;
@@ -2438,27 +2468,25 @@
return `
<div class="message-media">
<audio controls>
<source src="${audioSrc}" type="audio/mpeg">
<source src="${escapeHtml(audioSrc)}" type="audio/mpeg">
Tu navegador no soporta audio.
</audio>
</div>
`;
case 'document':
// Para documentos no descargamos automáticamente; abrir la URL externa (o la redirección por media id) para descargar
}
case 'document': {
let docUrl = '';
if (msg.local_file) {
docUrl = `/api/version/media-url.php?local=${encodeURIComponent(msg.local_file)}&download=1`;
} else if (msg.media_url_external) {
if (msg.media_url_external.indexOf('api/get_media.php?id=') !== -1) {
const parts = msg.media_url_external.split('id=');
if (localFile) {
docUrl = `/api/version/media-url.php?local=${encodeURIComponent(localFile)}&download=1`;
} else if (message.media_url_external) {
if (message.media_url_external.indexOf('api/get_media.php?id=') !== -1) {
const parts = message.media_url_external.split('id=');
const mid = parts[1] ? decodeURIComponent(parts[1]) : '';
docUrl = `/api/version/media-url.php?id=${encodeURIComponent(mid)}&download=1`;
} else if (/^https?:\/\//i.test(msg.media_url_external)) {
docUrl = `/api/version/media-url.php?url=${encodeURIComponent(msg.media_url_external)}&download=1`;
} else if (/^https?:\/\//i.test(message.media_url_external)) {
docUrl = `/api/version/media-url.php?url=${encodeURIComponent(message.media_url_external)}&download=1`;
} else {
// fallback
docUrl = `/api/version/media-url.php?url=${encodeURIComponent(msg.media_url_external)}&download=1`;
docUrl = `/api/version/media-url.php?url=${encodeURIComponent(message.media_url_external)}&download=1`;
}
}
if (!docUrl) {
@@ -2466,7 +2494,7 @@
<div class="message-document disabled">
<i class="fas fa-file-pdf"></i>
<div class="document-info">
<div class="document-name">${caption || 'Documento'}</div>
<div class="document-name">${escapeHtml(caption || 'Documento')}</div>
<div class="document-size text-muted">No disponible para descargar</div>
</div>
</div>
@@ -2476,17 +2504,17 @@
<div class="message-document">
<i class="fas fa-file-pdf"></i>
<div class="document-info">
<div class="document-name">${caption || 'Documento'}</div>
<div class="document-name">${escapeHtml(caption || 'Documento')}</div>
<div class="document-size">
<a href="${docUrl}" target="_blank" rel="noopener noreferrer" ${docUrl && docUrl.indexOf('download=1') !== -1 ? 'download' : ''}>Descargar</a>
<a href="${escapeHtml(docUrl)}" target="_blank" rel="noopener noreferrer" ${docUrl && docUrl.indexOf('download=1') !== -1 ? 'download' : ''}>Descargar</a>
</div>
</div>
<i class="fas fa-download"></i>
</div>
`;
}
default:
return caption || '';
return escapeHtml(caption) || '';
}
}
}
@@ -2498,6 +2526,63 @@
}
}
// Helper para iconos de archivos (copiado de chat_window)
function getFileIcon(filename) {
if (!filename) return 'fas fa-file text-muted';
const ext = String(filename).split('.').pop().toLowerCase();
const iconMap = {
'pdf': 'fas fa-file-pdf text-danger',
'doc': 'fas fa-file-word text-primary',
'docx': 'fas fa-file-word text-primary',
'xls': 'fas fa-file-excel text-success',
'xlsx': 'fas fa-file-excel text-success',
'ppt': 'fas fa-file-powerpoint text-warning',
'pptx': 'fas fa-file-powerpoint text-warning',
'zip': 'fas fa-file-archive text-secondary',
'rar': 'fas fa-file-archive text-secondary',
'txt': 'fas fa-file-alt text-muted',
'csv': 'fas fa-file-csv text-success'
};
return iconMap[ext] || 'fas fa-file text-muted';
}
// Media lightbox helper (image/video with download)
function openMediaLightbox(url, type = 'image', caption = '') {
if (!url) {
showAlert && showAlert('Media no disponible', 'warning');
return;
}
const body = document.getElementById('imageLightboxBody');
const cap = document.getElementById('imageLightboxCaption');
const downloadBtn = document.getElementById('imageLightboxDownload');
if (!body || !cap) return;
// clear existing content
body.innerHTML = '';
// Decide rendering by type or by URL extension
const isVideo = (type === 'video') || /\.(mp4|webm|ogg)(\?|$)/i.test(url);
if (isVideo) {
body.innerHTML = `<video controls class="w-100" style="border-radius:10px;"><source src="${escapeHtml(url)}" type="video/mp4">Tu navegador no soporta video.</video>`;
} else {
body.innerHTML = `<img src="${escapeHtml(url)}" class="img-fluid w-100" style="border-radius:10px;">`;
}
cap.textContent = caption || '';
if (downloadBtn) {
downloadBtn.href = url;
downloadBtn.style.display = 'inline-block';
}
const modalEl = document.getElementById('imageLightboxModal');
const bsModal = new bootstrap.Modal(modalEl);
bsModal.show();
}
// backward compatibility wrapper
function openImageLightbox(url, caption = '') {
openMediaLightbox(url, 'image', caption);
}
// Inicializar cuando la página cargue
let chat;
document.addEventListener('DOMContentLoaded', () => {
@@ -2505,5 +2590,22 @@
window.chatApp = chat; // Exponer globalmente para funciones auxiliares
});
</script>
<!-- Lightbox Modal -->
<div class="modal fade" id="imageLightboxModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content bg-transparent border-0">
<div class="modal-body p-0 position-relative">
<button type="button" class="btn-close btn-close-white position-absolute top-0 end-0 m-3" data-bs-dismiss="modal" aria-label="Cerrar" style="z-index:10;"></button>
<div id="imageLightboxBody" style="max-height:75vh; overflow:auto;"></div>
<div id="imageLightboxCaption" class="mt-2 text-center text-white small"></div>
<div class="position-absolute bottom-0 end-0 m-3">
<a id="imageLightboxDownload" class="btn btn-sm btn-light" href="#" target="_blank" download style="display:none;"><i class="fas fa-download"></i> Descargar</a>
</div>
</div>
</div>
</div>
</div>
</body>
</html>