up
This commit is contained in:
+128
-37
@@ -88,8 +88,21 @@ if (!isset($_SESSION['user_id'])) {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 6px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Scrollbar personalizado estilo WhatsApp (fino y discreto) */
|
||||
.conversation-list::-webkit-scrollbar { width: 4px; }
|
||||
.conversation-list::-webkit-scrollbar-track { background: transparent; }
|
||||
.conversation-list::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); border-radius: 4px; }
|
||||
.conversation-list::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.28); }
|
||||
|
||||
/* Buscador mejorado */
|
||||
.search-wrapper { position: relative; }
|
||||
.search-spinner { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); display: none; color: #999; font-size: 13px; }
|
||||
.search-clear { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); display: none; background: none; border: none; padding: 0; color: #999; cursor: pointer; font-size: 14px; line-height: 1; }
|
||||
.search-clear:hover { color: #333; }
|
||||
|
||||
.conversation-item {
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid #f1f5f8;
|
||||
@@ -847,11 +860,13 @@ if (!isset($_SESSION['user_id'])) {
|
||||
</div>
|
||||
|
||||
<div class="sidebar-search">
|
||||
<div class="input-group">
|
||||
<div class="input-group search-wrapper" style="position:relative;">
|
||||
<span class="input-group-text bg-transparent border-0">
|
||||
<i class="fas fa-search text-muted"></i>
|
||||
<i class="fas fa-search text-muted" id="search-icon"></i>
|
||||
</span>
|
||||
<input type="text" class="form-control border-0" placeholder="Buscar conversaciones..." id="search-input">
|
||||
<input type="text" class="form-control border-0" placeholder="Buscar por nombre, teléfono o mensaje..." id="search-input" autocomplete="off" style="padding-right:28px;">
|
||||
<span class="search-spinner" id="search-spinner"><i class="fas fa-circle-notch fa-spin"></i></span>
|
||||
<button class="search-clear" id="search-clear" title="Limpiar búsqueda">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1797,13 +1812,34 @@ if (!isset($_SESSION['user_id'])) {
|
||||
setupEventListeners() {
|
||||
// Búsqueda de conversaciones con debounce
|
||||
let searchTimeout;
|
||||
document.getElementById('search-input').addEventListener('input', (e) => {
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchClear = document.getElementById('search-clear');
|
||||
const searchSpinner = document.getElementById('search-spinner');
|
||||
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
this.searchConversations(e.target.value);
|
||||
}, 500); // Esperar 500ms después de que el usuario deje de escribir
|
||||
const val = e.target.value;
|
||||
// Mostrar/ocultar botón X
|
||||
if (searchClear) searchClear.style.display = val ? 'block' : 'none';
|
||||
// Mostrar spinner mientras espera debounce
|
||||
if (searchSpinner && val) searchSpinner.style.display = 'block';
|
||||
searchTimeout = setTimeout(async () => {
|
||||
if (searchSpinner) searchSpinner.style.display = 'none';
|
||||
await this.searchConversations(val);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
if (searchClear) {
|
||||
searchClear.addEventListener('click', () => {
|
||||
searchInput.value = '';
|
||||
searchClear.style.display = 'none';
|
||||
if (searchSpinner) searchSpinner.style.display = 'none';
|
||||
clearTimeout(searchTimeout);
|
||||
this.searchConversations('');
|
||||
searchInput.focus();
|
||||
});
|
||||
}
|
||||
|
||||
// Envío de mensajes
|
||||
document.getElementById('send-btn').addEventListener('click', () => {
|
||||
this.sendMessage();
|
||||
@@ -2555,7 +2591,7 @@ if (!isset($_SESSION['user_id'])) {
|
||||
console.error('❌ No se encontró el elemento #conversation-list en el DOM');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
console.log('✅ Container encontrado:', container);
|
||||
console.log('🔍 Filtro activo:', this.conversationFilter);
|
||||
|
||||
@@ -2604,6 +2640,10 @@ if (!isset($_SESSION['user_id'])) {
|
||||
} else {
|
||||
preview = `${mediaIcon} ${conv.last_message || 'Archivo'}`;
|
||||
}
|
||||
} else if (conv.matched_content) {
|
||||
// Mostrar snippet del mensaje que coincidió en la búsqueda
|
||||
const snippet = this.truncateText(conv.matched_content, 55);
|
||||
preview = `<span style="color:#888;font-size:11px;">💬 </span>${escapeHtml(snippet)}`;
|
||||
} else {
|
||||
preview = this.truncateText(conv.last_message || 'Sin mensajes', 50);
|
||||
}
|
||||
@@ -2652,30 +2692,32 @@ if (!isset($_SESSION['user_id'])) {
|
||||
try {
|
||||
const prevScrollTop = container.scrollTop;
|
||||
const prevScrollHeight = container.scrollHeight;
|
||||
const wasNearTop = prevScrollTop < 60;
|
||||
|
||||
console.log('🔄 Actualizando innerHTML del container...');
|
||||
console.log('📏 Scroll antes - Top:', prevScrollTop, 'Height:', prevScrollHeight);
|
||||
// Only reset to top if literally at position 0 (or within one pixel)
|
||||
const wasAtTop = prevScrollTop <= 4;
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
console.log('✅ DOM actualizado');
|
||||
console.log('📊 Elementos en DOM:', container.children.length);
|
||||
|
||||
// Forzar repaint del DOM
|
||||
void container.offsetHeight;
|
||||
|
||||
void container.offsetHeight; // forzar repaint
|
||||
|
||||
const newScrollHeight = container.scrollHeight;
|
||||
const scrollDelta = newScrollHeight - prevScrollHeight;
|
||||
|
||||
console.log('📏 Scroll después - Height:', newScrollHeight, 'Delta:', scrollDelta);
|
||||
|
||||
if (wasNearTop) {
|
||||
// keep at top
|
||||
if (wasAtTop) {
|
||||
container.scrollTop = 0;
|
||||
} else {
|
||||
// preserve visual offset (avoid jumping) whether the user is scrolling or not
|
||||
container.scrollTop = Math.max(0, prevScrollTop + scrollDelta);
|
||||
// Preservar posición visual exacta compensando cambio de altura
|
||||
const restoredPos = Math.max(0, prevScrollTop + scrollDelta);
|
||||
container.scrollTop = restoredPos;
|
||||
// Si el ítem activo está ahora fuera de vista (más de 2 alturas de pantalla),
|
||||
// acercarlo de forma suave sin regresar al tope
|
||||
const activeItem = container.querySelector('.conversation-item.active');
|
||||
if (activeItem) {
|
||||
const rect = activeItem.getBoundingClientRect();
|
||||
const listRect = container.getBoundingClientRect();
|
||||
const isVisible = rect.top >= listRect.top && rect.bottom <= listRect.bottom;
|
||||
if (!isVisible) {
|
||||
activeItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ renderConversations COMPLETADO');
|
||||
@@ -2683,7 +2725,6 @@ if (!isset($_SESSION['user_id'])) {
|
||||
// fallback to naive replace if anything failed
|
||||
console.warn('renderConversations: scroll preservation failed', e);
|
||||
container.innerHTML = html;
|
||||
// Forzar repaint
|
||||
void container.offsetHeight;
|
||||
console.log('⚠️ renderConversations COMPLETADO con fallback');
|
||||
}
|
||||
@@ -5854,22 +5895,28 @@ if (!isset($_SESSION['user_id'])) {
|
||||
thumb = '';
|
||||
}
|
||||
|
||||
// Si no hay URL de media, mostrar placeholder con botón de descarga
|
||||
// Si no hay URL de media, mostrar placeholder con botón de descarga inmediata
|
||||
if (!full && mediaType !== 'text') {
|
||||
const retryUrl = mediaUrl && /^\d+$/.test(mediaUrl)
|
||||
? `${baseUrl}/api/version/media-url.php?id=${encodeURIComponent(mediaUrl)}`
|
||||
: (mediaUrlExternal ? `${baseUrl}/${mediaUrlExternal.replace(/^\//, '')}` : '');
|
||||
const retryBtn = retryUrl
|
||||
? `<a href="${escapeHtml(retryUrl)}" target="_blank" class="btn btn-sm btn-outline-success mt-1" onclick="event.stopPropagation(); setTimeout(()=>location.reload(), 3000);"><i class="fas fa-download"></i> Descargar ahora</a>`
|
||||
: '';
|
||||
const iconMap = {image: 'fa-image', video: 'fa-video', audio: 'fa-microphone', document: 'fa-file', sticker: 'fa-sticky-note'};
|
||||
const icon = iconMap[mediaType] || 'fa-file';
|
||||
const labelMap = {image: 'imagen', video: 'video', audio: 'audio', document: 'documento', sticker: 'sticker'};
|
||||
const label = labelMap[mediaType] || mediaType;
|
||||
const btnId = `dl-btn-${messageId}`;
|
||||
const statusId = `dl-status-${messageId}`;
|
||||
// Botón descarga on-demand
|
||||
const hasId = messageId || (mediaUrl && /^\d+$/.test(mediaUrl));
|
||||
const downloadBtn = hasId ? `
|
||||
<button id="${escapeHtml(btnId)}" class="btn btn-sm btn-success mt-2" style="border-radius:20px; font-size:11px; padding:3px 12px;"
|
||||
onclick="window.forceDownloadMedia(${escapeHtml(String(messageId))}, '${escapeHtml(btnId)}', '${escapeHtml(statusId)}'); return false;">
|
||||
<i class='fas fa-download'></i> Descargar ahora
|
||||
</button>
|
||||
<div id="${escapeHtml(statusId)}" style="font-size:11px; color:#aaa; margin-top:4px;"></div>
|
||||
` : '';
|
||||
return `
|
||||
<div class="message-media" style="background:rgba(0,0,0,0.04); border-radius:8px; padding:12px; text-align:center; min-width:180px;">
|
||||
<i class="fas ${icon}" style="font-size:28px; color:#999; margin-bottom:6px;"></i>
|
||||
<div style="font-size:12px; color:#888;">Descargando ${escapeHtml(mediaType)}...</div>
|
||||
<div style="font-size:11px; color:#aaa; margin-top:2px;">En cola de descarga</div>
|
||||
${retryBtn}
|
||||
<i class="fas ${icon}" style="font-size:28px; color:#999; margin-bottom:6px; display:block;"></i>
|
||||
<div style="font-size:12px; color:#888;">Pendiente de descarga (${escapeHtml(label)})</div>
|
||||
${downloadBtn}
|
||||
</div>
|
||||
${caption ? `<div>${escapeHtml(caption)}</div>` : ''}
|
||||
`;
|
||||
@@ -6044,6 +6091,50 @@ if (!isset($_SESSION['user_id'])) {
|
||||
|
||||
window.formatMessageContent = formatMessageContent;
|
||||
|
||||
/**
|
||||
* Descarga inmediata on-demand de un archivo multimedia pendiente.
|
||||
* Se llama desde el botón del placeholder de media pendiente.
|
||||
*/
|
||||
window.forceDownloadMedia = async function(messageId, btnId, statusId) {
|
||||
const btn = document.getElementById(btnId);
|
||||
const statusEl = document.getElementById(statusId);
|
||||
if (!btn) return;
|
||||
|
||||
// Mostrar estado de carga
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Descargando...';
|
||||
if (statusEl) statusEl.textContent = 'Solicitando descarga...';
|
||||
|
||||
try {
|
||||
const resp = await fetch('api/force_download_media.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message_id: messageId })
|
||||
});
|
||||
const data = await resp.json();
|
||||
|
||||
if (data.success) {
|
||||
if (statusEl) statusEl.innerHTML = '<span style="color:#25d366">✓ Descargado. Recargando...</span>';
|
||||
// Recargar los mensajes después de 1 segundo para mostrar el archivo
|
||||
setTimeout(() => {
|
||||
if (window.chatApp && window.chatApp.currentUserId) {
|
||||
window.chatApp.loadMessages(window.chatApp.currentUserId, true, true);
|
||||
}
|
||||
}, 1200);
|
||||
} else {
|
||||
const msg = data.message || data.error || 'No se pudo descargar ahora';
|
||||
if (statusEl) statusEl.innerHTML = '<span style="color:#e74c3c">⚠️ ' + escapeHtml(msg) + '</span>';
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-redo"></i> Reintentar';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('forceDownloadMedia error', err);
|
||||
if (statusEl) statusEl.innerHTML = '<span style="color:#e74c3c">⚠️ Error de conexión</span>';
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-redo"></i> Reintentar';
|
||||
}
|
||||
};
|
||||
|
||||
// Delegate clicks on interactive buttons/list items to send quick replies
|
||||
document.addEventListener('click', (ev) => {
|
||||
const btn = ev.target.closest('.wa-interactive-btn');
|
||||
|
||||
Reference in New Issue
Block a user