This commit is contained in:
Lizandro Guarnizo
2026-01-29 11:40:04 -05:00
parent 1e936be593
commit 44423adb32
8 changed files with 146 additions and 145 deletions
+116 -75
View File
@@ -826,10 +826,6 @@ if (!isset($_SESSION['user_id'])) {
.unread-badge { margin-left: 8px; font-size: 12px; }
</style>
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#10b981">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
</head>
<body>
<div class="chat-container">
@@ -839,7 +835,7 @@ if (!isset($_SESSION['user_id'])) {
<div>
<h5 class="mb-0">💬 Conversaciones
<span class="badge bg-warning text-dark ms-2" style="font-size: 9px; padding: 2px 6px; animation: pulse 2s infinite;">
v1.2.1-<?php echo substr(time(), -4); ?>
v1.2.2-<?php echo substr(time(), -4); ?>
</span>
</h5>
</div>
@@ -910,8 +906,6 @@ if (!isset($_SESSION['user_id'])) {
</div>
<!-- Botón actualizar mensajes -->
<button class="btn btn-sm btn-outline-light" id="refresh-messages-btn" title="Actualizar mensajes"><i class="fas fa-sync-alt"></i></button>
<!-- PWA install button for conversations view -->
<button id="pwa-install-btn" class="btn btn-sm btn-outline-primary d-none" style="display:none;">Instalar</button>
<button class="btn btn-sm btn-outline-secondary" id="mark-unread-btn" title="Marcar conversación como no leída" style="display:none;">Marcar no leído</button>
<button class="btn btn-sm btn-outline-danger" id="delete-conversation-btn" title="Eliminar conversación" style="display:none;"><i class="fas fa-trash"></i></button>
</div>
@@ -1218,23 +1212,35 @@ if (!isset($_SESSION['user_id'])) {
if (this.currentUserId && String(this.currentUserId) === String(userId)) {
console.log('♻️ Mensaje para conversación ACTIVA');
// SOLUCIÓN: Siempre usar staged messages + indicador
// Esto evita el error que borra la pantalla al intentar recargar automáticamente
if (!this._stagedMessages) this._stagedMessages = [];
if (!this._stagedMessageIds) this._stagedMessageIds = new Set();
// Verificar si el usuario está cerca del final del chat
const container = document.getElementById('chat-conversations');
const nearBottom = container ? ((container.scrollHeight - container.scrollTop - container.clientHeight) < 150) : true;
// Crear key única para evitar duplicados
const msgKey = data.message_id || data.id || `${data.created_at}_${data.content?.substring(0,20)}`;
if (!this._stagedMessageIds.has(msgKey)) {
this._stagedMessageIds.add(msgKey);
this._stagedMessages.push(data);
console.log('💾 Mensaje guardado en staging. Total:', this._stagedMessages.length);
// Mostrar indicador de nuevos mensajes
this.showNewMessagesIndicator(this._stagedMessages.length);
if (nearBottom && !this._userScrolling) {
// Usuario está en el final: recargar mensajes automáticamente
console.log('🔄 Usuario cerca del final, recargando mensajes...');
this.loadMessages(this.currentUserId, true, true).catch(err => {
console.error('Error recargando mensajes:', err);
});
} else {
console.log('⏭️ Mensaje duplicado, omitiendo');
// Usuario está leyendo historial: usar staged messages
console.log('📜 Usuario leyendo historial, guardando en staging...');
if (!this._stagedMessages) this._stagedMessages = [];
if (!this._stagedMessageIds) this._stagedMessageIds = new Set();
// Crear key única para evitar duplicados
const msgKey = data.message_id || data.id || `${data.created_at}_${data.content?.substring(0,20)}`;
if (!this._stagedMessageIds.has(msgKey)) {
this._stagedMessageIds.add(msgKey);
this._stagedMessages.push(data);
console.log('💾 Mensaje guardado en staging. Total:', this._stagedMessages.length);
// Mostrar indicador de nuevos mensajes
this.showNewMessagesIndicator(this._stagedMessages.length);
} else {
console.log('⏭️ Mensaje duplicado, omitiendo');
}
}
} else {
console.log('📋 Mensaje para OTRA conversación, solo actualizando lista');
@@ -1380,7 +1386,7 @@ if (!isset($_SESSION['user_id'])) {
*/
updateConversationInList(data) {
try {
console.log('📝 Actualizando conversación en lista:', data);
console.log('📝 updateConversationInList INICIADO:', data);
// Asegurar que conversations esté inicializado
if (!this.conversations || !Array.isArray(this.conversations)) {
@@ -1396,6 +1402,8 @@ if (!isset($_SESSION['user_id'])) {
return;
}
console.log('👤 User ID extraído:', userId);
// Si no viene el mensaje, recargar la lista completa para obtener datos actualizados
if (!data.message && !data.content && !data.text && !data.last_message) {
console.log('⚠️ Evento SSE sin contenido de mensaje, recargando lista completa...');
@@ -1412,11 +1420,14 @@ if (!isset($_SESSION['user_id'])) {
const message = data.message || data.content || data.text || data.last_message || 'Nuevo mensaje';
const timestamp = data.timestamp || data.created_at || new Date().toISOString();
console.log('💬 Mensaje extraído:', message);
console.log('⏰ Timestamp:', timestamp);
// Buscar la conversación en el array
const existingIndex = this.conversations.findIndex(c => String(c.user_id) === String(userId));
if (existingIndex !== -1) {
console.log('✅ Conversación encontrada, actualizando...');
console.log('✅ Conversación encontrada en índice:', existingIndex);
// Actualizar conversación existente
const conv = this.conversations[existingIndex];
conv.last_message = message;
@@ -1425,14 +1436,17 @@ if (!isset($_SESSION['user_id'])) {
// Solo incrementar unread_count si no es la conversación activa
if (String(this.currentUserId) !== String(userId)) {
conv.unread_count = (conv.unread_count || 0) + 1;
console.log('📬 unread_count incrementado a:', conv.unread_count);
} else {
// Si es la conversación activa, mantener unread_count en 0
conv.unread_count = 0;
console.log('✅ Conversación activa, unread_count = 0');
}
// Mover al inicio de la lista
this.conversations.splice(existingIndex, 1);
this.conversations.unshift(conv);
console.log('⬆️ Conversación movida al inicio');
} else {
console.log(' Conversación no existe, agregando nueva...');
// Nueva conversación, agregar al inicio
@@ -1444,22 +1458,25 @@ if (!isset($_SESSION['user_id'])) {
last_time: timestamp,
unread_count: String(this.currentUserId) !== String(userId) ? 1 : 0
});
console.log('✅ Nueva conversación agregada');
}
// Re-renderizar solo la lista de conversaciones
console.log('🔄 Re-renderizando lista de conversaciones...');
console.log('📊 Total conversaciones:', this.conversations.length);
console.log('🔍 Filtro activo:', this.conversationFilter);
console.log('🔍 this.conversations:', this.conversations);
if (typeof this.renderConversations === 'function') {
console.log('✅ Llamando a renderConversations()...');
this.renderConversations();
console.log('✅ renderConversations() ejecutado correctamente');
} else {
console.error('❌ renderConversations no es una función!');
}
} catch (error) {
console.error('❌ Error actualizando conversación en lista:', error);
console.error('❌ Error updateConversationInList:', error);
console.error('Stack trace:', error.stack);
}
}
@@ -2110,49 +2127,6 @@ if (!isset($_SESSION['user_id'])) {
};
applyInitialMobile();
window.addEventListener('resize', applyInitialMobile);
// PWA support (install prompt + SW)
let _deferredPWA = null;
const _pwaBtnConv = document.getElementById('pwa-install-btn');
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
_deferredPWA = e;
if (_pwaBtnConv) { _pwaBtnConv.style.display = 'inline-block'; _pwaBtnConv.classList.remove('d-none'); }
});
if (_pwaBtnConv) {
_pwaBtnConv.addEventListener('click', async () => {
if (!_deferredPWA) return;
_deferredPWA.prompt();
const choice = await _deferredPWA.userChoice;
console.debug('PWA install (conv):', choice);
if (choice && choice.outcome === 'accepted') _pwaBtnConv.style.display = 'none';
_deferredPWA = null;
});
}
window.addEventListener('appinstalled', () => console.debug('PWA installed (conversations)'));
if ('serviceWorker' in navigator) {
(async () => {
try {
// Check if the service-worker script is actually reachable to avoid noisy 404 warnings
const resp = await fetch('/service-worker.js', { method: 'GET', cache: 'no-store' });
if (resp && resp.ok) {
navigator.serviceWorker.register('/service-worker.js')
.then(() => console.debug('SW reg (conv)'))
.catch(e => console.warn('SW reg failed', e));
} else {
console.debug('Service worker not registered: script not found (status ' + (resp && resp.status) + ')');
// Remove manifest link if it's missing to avoid extra 404s
try {
const link = document.querySelector('link[rel="manifest"]');
if (link && (!resp || resp.status === 404)) link.parentNode && link.parentNode.removeChild(link);
} catch(e) {}
}
} catch (e) {
// Network or fetch failed — don't spam console with errors
console.debug('Service worker check failed:', e);
}
})();
}
}
const stopRecBtn = document.getElementById('stop-recording');
@@ -2524,7 +2498,9 @@ if (!isset($_SESSION['user_id'])) {
renderConversations() {
console.log('🎨 Renderizando conversaciones:', this.conversations.length);
console.log('🎨 renderConversations INICIADO');
console.log('📊 Total conversaciones:', this.conversations?.length || 0);
const container = document.getElementById('conversation-list');
if (!container) {
@@ -2631,12 +2607,20 @@ if (!isset($_SESSION['user_id'])) {
const wasNearTop = prevScrollTop < 60;
console.log('🔄 Actualizando innerHTML del container...');
console.log('📏 Scroll antes - Top:', prevScrollTop, 'Height:', prevScrollHeight);
container.innerHTML = html;
console.log('✅ DOM actualizado con', this.conversations.length, 'conversaciones');
console.log('✅ DOM actualizado');
console.log('📊 Elementos en DOM:', container.children.length);
// Forzar repaint del DOM
void container.offsetHeight;
const newScrollHeight = container.scrollHeight;
const scrollDelta = newScrollHeight - prevScrollHeight;
console.log('📏 Scroll después - Height:', newScrollHeight, 'Delta:', scrollDelta);
if (wasNearTop) {
// keep at top
@@ -2645,10 +2629,15 @@ if (!isset($_SESSION['user_id'])) {
// preserve visual offset (avoid jumping) whether the user is scrolling or not
container.scrollTop = Math.max(0, prevScrollTop + scrollDelta);
}
console.log('✅ renderConversations COMPLETADO');
} catch (e) {
// 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');
}
}
@@ -4046,9 +4035,40 @@ if (!isset($_SESSION['user_id'])) {
this.startRecording = async function() {
try {
// Verificar que el navegador soporta MediaDevices
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error('Tu navegador no soporta grabación de audio');
}
// Suspend periodic reload while recording
this._suspendAutoRefresh = true;
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// Intentar obtener acceso al micrófono con diferentes configuraciones
let stream = null;
try {
// Intento 1: Configuración básica sin especificar dispositivo
stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
} catch (err1) {
console.warn('Intento 1 falló, probando configuración más simple:', err1.message);
try {
// Intento 2: Configuración minimalista
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
} catch (err2) {
console.error('Todos los intentos fallaron');
throw err2;
}
}
if (!stream) {
throw new Error('No se pudo obtener acceso al micrófono');
}
this._mediaRecorder = new MediaRecorder(stream);
const chunks = [];
this._mediaRecorder.ondataavailable = (e) => { if (e.data && e.data.size) chunks.push(e.data); };
@@ -4114,8 +4134,29 @@ if (!isset($_SESSION['user_id'])) {
update();
this._recordingInterval = setInterval(update, 1000);
} catch (err) {
console.error('startRecording error', err);
alert('No se pudo acceder al micrófono: ' + (err.message||err));
console.error('startRecording error', err.name, err.message);
// Mensajes de error más específicos
let errorMsg = 'No se pudo acceder al micrófono';
if (err.name === 'NotFoundError') {
errorMsg = 'No se encontró ningún micrófono. Conecta un micrófono e intenta de nuevo.';
} else if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
errorMsg = 'Permiso denegado. Permite el acceso al micrófono en la configuración del navegador.';
} else if (err.name === 'NotReadableError') {
errorMsg = 'El micrófono está siendo usado por otra aplicación.';
} else if (err.message) {
errorMsg += ': ' + err.message;
}
alert(errorMsg);
// Limpiar estado
this._suspendAutoRefresh = false;
const ind = document.getElementById('recording-indicator');
if (ind) ind.style.display = 'none';
const micEl = document.getElementById('mic-btn');
if (micEl) micEl.classList.remove('recording', 'recording-cancel');
}
};