Update conversations.php

This commit is contained in:
Lizandro Guarnizo
2026-01-27 11:06:32 -05:00
parent 46f33b652b
commit b4335f0cb6
+55 -45
View File
@@ -994,7 +994,8 @@
constructor() { constructor() {
this.currentConversationId = null; this.currentConversationId = null;
this.currentUserId = null; this.currentUserId = null;
this.conversations = []; this.conversationsList = []; // Lista de usuarios/conversaciones en el sidebar
this.currentMessages = []; // Mensajes de la conversación activa
// Track shown notifications to avoid duplicates from polling // Track shown notifications to avoid duplicates from polling
this._shownNotifications = new Set(); this._shownNotifications = new Set();
// Track notifications the user dismissed/read locally so they don't reappear // Track notifications the user dismissed/read locally so they don't reappear
@@ -1213,7 +1214,7 @@
if (fileUrl && this.currentUserId && String(this.currentUserId) === String(userId)) { if (fileUrl && this.currentUserId && String(this.currentUserId) === String(userId)) {
try { openMediaLightbox(fileUrl, data.file_name || ''); } catch (e) { console.warn('openMediaLightbox failed', e); } try { openMediaLightbox(fileUrl, data.file_name || ''); } catch (e) { console.warn('openMediaLightbox failed', e); }
} else if (userId) { } else if (userId) {
const conv = this.conversations.find(c => c.user_id == userId); const conv = this.conversationsList.find(c => c.user_id == userId);
if (conv) { if (conv) {
// Prefetch messages then open conversation using preloaded data to avoid double-fetch // Prefetch messages then open conversation using preloaded data to avoid double-fetch
try { try {
@@ -1480,7 +1481,7 @@
const j = await resp.json(); const j = await resp.json();
if (j && j.success) { if (j && j.success) {
// update local model and UI // update local model and UI
const m = this.conversations.find(x => x.message_id == mid || x.id == mid); const m = this.currentMessages.find(x => x.message_id == mid || x.id == mid);
if (m) m.reaction_emoji = emoji; if (m) m.reaction_emoji = emoji;
this.updateMessageReactionInView && this.updateMessageReactionInView(mid, emoji); this.updateMessageReactionInView && this.updateMessageReactionInView(mid, emoji);
} else { } else {
@@ -1944,12 +1945,12 @@
for (const m of messages) { for (const m of messages) {
try { try {
// skip duplicates // skip duplicates
const exists = this.conversations && this.conversations.some(x => (x.message_id && m.message_id && String(x.message_id) === String(m.message_id)) || (x.id && m.id && Number(x.id) === Number(m.id))); const exists = this.currentMessages && this.currentMessages.some(x => (x.message_id && m.message_id && String(x.message_id) === String(m.message_id)) || (x.id && m.id && Number(x.id) === Number(m.id)));
if (exists) continue; if (exists) continue;
if (nearBottomNow && !this._userScrolling) { if (nearBottomNow && !this._userScrolling) {
// append directly and mark for render // append directly and mark for render
this.conversations.push(m); this.currentMessages.push(m);
appended++; appended++;
} else { } else {
// stage for later (user is reading history) // stage for later (user is reading history)
@@ -2072,9 +2073,9 @@
} }
if (append) { if (append) {
this.conversations = this.conversations.concat(items); this.conversationsList = this.conversationsList.concat(items);
} else { } else {
this.conversations = items; this.conversationsList = items;
} }
this.hasMoreConversations = hasMore; this.hasMoreConversations = hasMore;
@@ -2103,7 +2104,7 @@
renderConversations() { renderConversations() {
const container = document.getElementById('conversation-list'); const container = document.getElementById('conversation-list');
if (this.conversations.length === 0) { if (this.conversationsList.length === 0) {
const emptyMsg = this.conversationFilter === 'unread' ? 'No hay conversaciones no leídas' : 'No hay conversaciones aún'; const emptyMsg = this.conversationFilter === 'unread' ? 'No hay conversaciones no leídas' : 'No hay conversaciones aún';
container.innerHTML = ` container.innerHTML = `
<div class="text-center p-4"> <div class="text-center p-4">
@@ -2115,7 +2116,7 @@
} }
// Ahora el backend devuelve por usuario: last_message, last_time, unread_count y avatar_url // Ahora el backend devuelve por usuario: last_message, last_time, unread_count y avatar_url
const html = this.conversations.map(conv => { const html = this.conversationsList.map(conv => {
const isActive = conv.user_id == this.currentUserId ? 'active' : ''; const isActive = conv.user_id == this.currentUserId ? 'active' : '';
const time = this.formatTime(conv.last_time); const time = this.formatTime(conv.last_time);
const preview = this.truncateText(conv.last_message || 'Sin mensajes', 50); const preview = this.truncateText(conv.last_message || 'Sin mensajes', 50);
@@ -2149,7 +2150,7 @@
// Detectar nuevas notificaciones: comparar unread counts previos // Detectar nuevas notificaciones: comparar unread counts previos
if (!this._prevConversations) this._prevConversations = {}; if (!this._prevConversations) this._prevConversations = {};
this.conversations.forEach(c => { this.conversationsList.forEach(c => {
const prev = this._prevConversations[c.user_id] || { unread_count: 0 }; const prev = this._prevConversations[c.user_id] || { unread_count: 0 };
if (c.unread_count > prev.unread_count && c.user_id != this.currentUserId) { if (c.unread_count > prev.unread_count && c.user_id != this.currentUserId) {
// nueva notificación // nueva notificación
@@ -2250,7 +2251,7 @@
} }
getConversationPhone(userId) { getConversationPhone(userId) {
const conv = this.conversations.find(c => c.user_id === userId); const conv = this.conversationsList.find(c => c.user_id === userId);
if (conv) return conv.phone_number || conv.phone || conv.user_phone || null; if (conv) return conv.phone_number || conv.phone || conv.user_phone || null;
const el = document.getElementById('chat-phone'); const el = document.getElementById('chat-phone');
return el ? el.textContent.trim() : null; return el ? el.textContent.trim() : null;
@@ -2296,7 +2297,7 @@
document.querySelector(`[data-user-id="${userId}"]`)?.classList.add('active'); document.querySelector(`[data-user-id="${userId}"]`)?.classList.add('active');
// Actualizar estado del toggle del bot según datos de la conversación // Actualizar estado del toggle del bot según datos de la conversación
const conv = this.conversations.find(c => c.user_id === userId); const conv = this.conversationsList.find(c => c.user_id === userId);
if (conv) { if (conv) {
const btn = document.getElementById('bot-toggle'); const btn = document.getElementById('bot-toggle');
const holdIndicator = document.getElementById('hold-indicator'); const holdIndicator = document.getElementById('hold-indicator');
@@ -2413,7 +2414,7 @@
attendStatus.textContent = ''; attendStatus.textContent = '';
// refresh conv reference // refresh conv reference
const c = this.conversations.find(x => x.user_id === userId) || conv; const c = this.conversationsList.find(x => x.user_id === userId) || conv;
// Update only the in-chat status message based on server/local state // Update only the in-chat status message based on server/local state
this.getUserState(userId).then(serverState => { this.getUserState(userId).then(serverState => {
@@ -2434,7 +2435,7 @@
const toggleAttend = async () => { const toggleAttend = async () => {
try { try {
const c = this.conversations.find(x => x.user_id === userId) || conv; const c = this.conversationsList.find(x => x.user_id === userId) || conv;
// Preflight: obtener estado canonico del servidor con cache corta // Preflight: obtener estado canonico del servidor con cache corta
const serverState = await this.getUserState(userId, true); const serverState = await this.getUserState(userId, true);
@@ -2728,9 +2729,10 @@
this._fullLoadInProgress = true; this._fullLoadInProgress = true;
const container = document.getElementById('chat-conversations'); const container = document.getElementById('chat-conversations');
// si cargamos más antiguos, preservar scroll // Guardar posición de scroll y determinar si el usuario estaba en el fondo
let prevScrollHeight = container ? container.scrollHeight : 0; let prevScrollHeight = container ? container.scrollHeight : 0;
let prevScrollTop = container ? container.scrollTop : 0; let prevScrollTop = container ? container.scrollTop : 0;
const wasAtBottom = container ? ((container.scrollHeight - container.scrollTop - container.clientHeight) < 150) : true;
// indicador top cuando cargamos anteriores // indicador top cuando cargamos anteriores
let topLoader = null; let topLoader = null;
@@ -2812,7 +2814,7 @@
} }
// Si es carga inicial y el servidor devolvió vacío, pero ya teníamos mensajes, preservarlos. // Si es carga inicial y el servidor devolvió vacío, pero ya teníamos mensajes, preservarlos.
if (initial && Array.isArray(messages) && messages.length === 0 && Array.isArray(this.conversations) && this.conversations.length > 0) { if (initial && Array.isArray(messages) && messages.length === 0 && Array.isArray(this.currentMessages) && this.currentMessages.length > 0) {
try { if (topLoader && topLoader.parentNode) topLoader.remove(); } catch(e) {} try { if (topLoader && topLoader.parentNode) topLoader.remove(); } catch(e) {}
// Ensure container visibility is restored if it was hidden during render // Ensure container visibility is restored if it was hidden during render
try { if (container && container.dataset && container.dataset.renderHidden) { container.style.visibility = 'visible'; delete container.dataset.renderHidden; } } catch(e) {} try { if (container && container.dataset && container.dataset.renderHidden) { container.style.visibility = 'visible'; delete container.dataset.renderHidden; } } catch(e) {}
@@ -2824,13 +2826,13 @@
if (initial) { if (initial) {
// Reemplazar sólo en carga inicial válida (puede estar vacía si no hay nada en DB) // Reemplazar sólo en carga inicial válida (puede estar vacía si no hay nada en DB)
this.conversations = messages; this.currentMessages = messages;
} else { } else {
// Prepend sólo si hay mensajes nuevos // Prepend sólo si hay mensajes nuevos
if (Array.isArray(messages) && messages.length > 0) { if (Array.isArray(messages) && messages.length > 0) {
// Evitar introducir duplicados que ya existen en la vista actual // Evitar introducir duplicados que ya existen en la vista actual
try { try {
const existingKeys = new Set((this.conversations || []).map(m => this.messageKey(m))); const existingKeys = new Set((this.currentMessages || []).map(m => this.messageKey(m)));
const beforeLen = messages.length; const beforeLen = messages.length;
messages = messages.filter(m => { messages = messages.filter(m => {
const k = this.messageKey(m); const k = this.messageKey(m);
@@ -2848,7 +2850,7 @@
} catch (e) { console.warn('duplicate filtering failed', e); } } catch (e) { console.warn('duplicate filtering failed', e); }
// Prepend mensajes antiguos // Prepend mensajes antiguos
this.conversations = messages.concat(this.conversations || []); this.currentMessages = messages.concat(this.currentMessages || []);
} else { } else {
// No hay mensajes nuevos para añadir; nada que hacer // No hay mensajes nuevos para añadir; nada que hacer
} }
@@ -2860,15 +2862,15 @@
// Update last/latest message timestamp so we can poll deltas later // Update last/latest message timestamp so we can poll deltas later
try { try {
if (Array.isArray(this.conversations) && this.conversations.length > 0) { if (Array.isArray(this.currentMessages) && this.currentMessages.length > 0) {
const last = this.conversations[this.conversations.length - 1]; const last = this.currentMessages[this.currentMessages.length - 1];
if (last && last.created_at) this._latestMessage = last.created_at; if (last && last.created_at) this._latestMessage = last.created_at;
} }
} catch(e) { /* ignore */ } } catch(e) { /* ignore */ }
// Improved dedupe: pick the most "readable" message when duplicates exist // Improved dedupe: pick the most "readable" message when duplicates exist
try { try {
if (!this.conversations || this.conversations.length === 0) { if (!this.currentMessages || this.currentMessages.length === 0) {
// Nothing to dedupe // Nothing to dedupe
} else { } else {
const best = new Map(); // key => { index, score } const best = new Map(); // key => { index, score }
@@ -2897,8 +2899,8 @@
return s; return s;
}; };
for (let i = this.conversations.length - 1; i >= 0; i--) { for (let i = this.currentMessages.length - 1; i >= 0; i--) {
const m = this.conversations[i]; const m = this.currentMessages[i];
if (!m) continue; if (!m) continue;
const mid = m.message_id || m.id || null; const mid = m.message_id || m.id || null;
let key = null; let key = null;
@@ -2931,7 +2933,7 @@
// remove from highest index down // remove from highest index down
for (let j = uniq.length - 1; j >= 0; j--) { for (let j = uniq.length - 1; j >= 0; j--) {
const idx = uniq[j]; const idx = uniq[j];
const removed = this.conversations.splice(idx, 1)[0]; const removed = this.currentMessages.splice(idx, 1)[0];
console.debug('Dedupe removed message at index', idx, 'removed=', removed && (removed.message_id || removed.id || removed.content || removed.media_url) ); console.debug('Dedupe removed message at index', idx, 'removed=', removed && (removed.message_id || removed.id || removed.content || removed.media_url) );
} }
} }
@@ -2953,7 +2955,7 @@
// Renderizar incrementalmente para evitar parpadeos y preservar reproducción de media // Renderizar incrementalmente para evitar parpadeos y preservar reproducción de media
// Si cargamos paginación (initial=false), pasar la cantidad de mensajes recién obtenidos para insertarlos al inicio // Si cargamos paginación (initial=false), pasar la cantidad de mensajes recién obtenidos para insertarlos al inicio
const newlyFetched = (!initial && Array.isArray(messages)) ? messages.length : 0; const newlyFetched = (!initial && Array.isArray(messages)) ? messages.length : 0;
if (this.conversations && this.conversations.length > 0) { if (this.currentMessages && this.currentMessages.length > 0) {
this.renderMessagesIncremental(initial, newlyFetched, follow); this.renderMessagesIncremental(initial, newlyFetched, follow);
} else if (initial) { } else if (initial) {
// Si es carga inicial y no tenemos mensajes, mostrar una vista vacía // Si es carga inicial y no tenemos mensajes, mostrar una vista vacía
@@ -2967,8 +2969,17 @@
this._fullLoadInProgress = false; this._fullLoadInProgress = false;
if (initial) { if (initial) {
// Scroll to bottom only if the caller requested following behavior // Scroll to bottom SOLO si: (1) follow fue solicitado Y (2) el usuario estaba en el fondo O (3) es el primer mensaje
if (follow) { this.scrollToBottom(); } const shouldScrollToBottom = follow && (wasAtBottom || this.currentMessages.length === messages.length);
if (shouldScrollToBottom) {
this.scrollToBottom();
} else {
// Preservar la posición relativa del scroll
if (container && prevScrollHeight > 0) {
const scrollPercentage = prevScrollTop / prevScrollHeight;
container.scrollTop = container.scrollHeight * scrollPercentage;
}
}
// Re-show the container (we hid it before rendering to avoid jump-to-top) // Re-show the container (we hid it before rendering to avoid jump-to-top)
try { try {
if (container && container.dataset && container.dataset.renderHidden) { if (container && container.dataset && container.dataset.renderHidden) {
@@ -3043,10 +3054,10 @@
// Safety: remove any empty text-only messages that may have slipped into the model // Safety: remove any empty text-only messages that may have slipped into the model
try { try {
const before = this.conversations ? this.conversations.length : 0; const before = this.currentMessages ? this.currentMessages.length : 0;
if (Array.isArray(this.conversations) && this.conversations.length > 0) { if (Array.isArray(this.currentMessages) && this.currentMessages.length > 0) {
this.conversations = this.conversations.filter(m => !this.isEmptyMessage(m)); this.currentMessages = this.currentMessages.filter(m => !this.isEmptyMessage(m));
const removed = before - this.conversations.length; const removed = before - this.currentMessages.length;
if (removed > 0 && console && console.info) console.info('Removed', removed, 'empty messages before render'); if (removed > 0 && console && console.info) console.info('Removed', removed, 'empty messages before render');
} }
} catch (e) { console.warn('Failed to trim empty messages before render', e); } } catch (e) { console.warn('Failed to trim empty messages before render', e); }
@@ -3106,8 +3117,8 @@
// calculate near-bottom late to decide scrolling after DOM updates (avoids stale value) // calculate near-bottom late to decide scrolling after DOM updates (avoids stale value)
for (let i = 0; i < this.conversations.length; i++) { for (let i = 0; i < this.currentMessages.length; i++) {
const msg = this.conversations[i]; const msg = this.currentMessages[i];
const mid = String(msg.message_id || msg.id || ('local_' + i)); const mid = String(msg.message_id || msg.id || ('local_' + i));
const existingEl = existing.get(mid); const existingEl = existing.get(mid);
@@ -3126,7 +3137,7 @@
try { try {
const rp = existingEl.querySelector('.reply-preview'); const rp = existingEl.querySelector('.reply-preview');
if (msg.reply_to_message_id) { if (msg.reply_to_message_id) {
const previewSrc = (this.conversations.find(m => m.message_id == msg.reply_to_message_id) || {}).content || ('Mensaje ' + msg.reply_to_message_id); const previewSrc = (this.currentMessages.find(m => m.message_id == msg.reply_to_message_id) || {}).content || ('Mensaje ' + msg.reply_to_message_id);
if (rp) rp.textContent = 'En respuesta a: ' + previewSrc.substring(0,140); if (rp) rp.textContent = 'En respuesta a: ' + previewSrc.substring(0,140);
else { else {
const div = document.createElement('div'); div.className = 'reply-preview'; div.textContent = 'En respuesta a: ' + previewSrc.substring(0,140); existingEl.insertBefore(div, existingEl.firstChild); const div = document.createElement('div'); div.className = 'reply-preview'; div.textContent = 'En respuesta a: ' + previewSrc.substring(0,140); existingEl.insertBefore(div, existingEl.firstChild);
@@ -3249,7 +3260,7 @@
} catch (e) { /* ignore staging errors */ } } catch (e) { /* ignore staging errors */ }
// insert date separator if this message is the first of a new day // insert date separator if this message is the first of a new day
const prev = (i>0) ? this.conversations[i-1] : null; const prev = (i>0) ? this.currentMessages[i-1] : null;
if (!prev || !sameDay(prev.created_at, msg.created_at)) { if (!prev || !sameDay(prev.created_at, msg.created_at)) {
const sep = document.createElement('div'); const sep = document.createElement('div');
sep.className = 'date-sep'; sep.className = 'date-sep';
@@ -3266,7 +3277,7 @@
let replyHtml = ''; let replyHtml = '';
if (msg.reply_to_message_id) { if (msg.reply_to_message_id) {
const target = this.conversations.find(m => m.message_id == msg.reply_to_message_id || m.id == msg.reply_to_message_id); const target = this.currentMessages.find(m => m.message_id == msg.reply_to_message_id || m.id == msg.reply_to_message_id);
const previewText = target ? (target.content || target.message_text || '').substring(0,140) : ('Mensaje ' + msg.reply_to_message_id); const previewText = target ? (target.content || target.message_text || '').substring(0,140) : ('Mensaje ' + msg.reply_to_message_id);
replyHtml = `<div class="reply-preview">En respuesta a: ${window.escapeHtml(previewText)}</div>`; replyHtml = `<div class="reply-preview">En respuesta a: ${window.escapeHtml(previewText)}</div>`;
} }
@@ -3748,7 +3759,7 @@
// Append staged messages to conversation model // Append staged messages to conversation model
const toApplyCount = this._stagedMessages.length; const toApplyCount = this._stagedMessages.length;
console.debug('applyStagedMessages: applying', toApplyCount, 'messages'); console.debug('applyStagedMessages: applying', toApplyCount, 'messages');
this._stagedMessages.forEach(m => { this.conversations.push(m); }); this._stagedMessages.forEach(m => { this.currentMessages.push(m); });
// clear staged storage // clear staged storage
this._stagedMessages = []; this._stagedMessages = [];
@@ -3760,7 +3771,7 @@
this.scrollToBottom(); this.scrollToBottom();
// Update latest timestamp // Update latest timestamp
try { const last = this.conversations[this.conversations.length - 1]; if (last && last.created_at) this._latestMessage = last.created_at; } catch(e){} try { const last = this.currentMessages[this.currentMessages.length - 1]; if (last && last.created_at) this._latestMessage = last.created_at; } catch(e){}
} catch (e) { } catch (e) {
console.warn('applyStagedMessages failed', e); console.warn('applyStagedMessages failed', e);
@@ -3901,7 +3912,7 @@
// If the template expects a named parameter `name`, provide it using user name or phone // If the template expects a named parameter `name`, provide it using user name or phone
if (templateName === 'contacto_nuevo') { if (templateName === 'contacto_nuevo') {
// Try to find conversation info // Try to find conversation info
let conv = this.conversations.find(c => c.user_id === this.currentUserId) || {}; let conv = this.conversationsList.find(c => c.user_id === this.currentUserId) || {};
let resolvedName = (conv.name || conv.full_name || '').trim(); let resolvedName = (conv.name || conv.full_name || '').trim();
if (!resolvedName) { if (!resolvedName) {
const nameEl = document.getElementById('chat-name-text') || document.getElementById('chat-name'); const nameEl = document.getElementById('chat-name-text') || document.getElementById('chat-name');
@@ -3982,7 +3993,7 @@
msg.message_type = options.message_type; msg.message_type = options.message_type;
} }
this.conversations.push(msg); this.currentMessages.push(msg);
this.renderMessagesIncremental(); this.renderMessagesIncremental();
this.scrollToBottom(); this.scrollToBottom();
} }
@@ -3992,7 +4003,7 @@
const preview = document.getElementById('reply-preview'); const preview = document.getElementById('reply-preview');
const previewText = document.getElementById('reply-preview-text'); const previewText = document.getElementById('reply-preview-text');
const cancelBtn = document.getElementById('cancel-reply-btn'); const cancelBtn = document.getElementById('cancel-reply-btn');
let msg = this.conversations.find(m => m.message_id == messageId || m.id == messageId); let msg = this.currentMessages.find(m => m.message_id == messageId || m.id == messageId);
const setPreviewFromMsg = (m) => { const setPreviewFromMsg = (m) => {
const text = m ? (m.content || m.message_text || (m.caption || '[Mensaje]')) : ('Mensaje ' + messageId); const text = m ? (m.content || m.message_text || (m.caption || '[Mensaje]')) : ('Mensaje ' + messageId);
@@ -4317,11 +4328,10 @@
} }
// 2. Enviar mensaje con el archivo // 2. Enviar mensaje con el archivo
const user = this.conversations.find(c => c.user_id === this.currentUserId); const phone = this.getConversationPhone(this.currentUserId);
const phone = user ? user.phone_number : null;
if (!phone) { if (!phone) {
throw new Error('No se encontró el número de teléfono'); throw new Error('No se encontró el número de teléfono del usuario');
} }
const sendResponse = await fetch('api/send_media_message.php', { const sendResponse = await fetch('api/send_media_message.php', {