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() {
this.currentConversationId = 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
this._shownNotifications = new Set();
// 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)) {
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);
const conv = this.conversationsList.find(c => c.user_id == userId);
if (conv) {
// Prefetch messages then open conversation using preloaded data to avoid double-fetch
try {
@@ -1480,7 +1481,7 @@
const j = await resp.json();
if (j && j.success) {
// 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;
this.updateMessageReactionInView && this.updateMessageReactionInView(mid, emoji);
} else {
@@ -1944,12 +1945,12 @@
for (const m of messages) {
try {
// 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 (nearBottomNow && !this._userScrolling) {
// append directly and mark for render
this.conversations.push(m);
this.currentMessages.push(m);
appended++;
} else {
// stage for later (user is reading history)
@@ -2072,9 +2073,9 @@
}
if (append) {
this.conversations = this.conversations.concat(items);
this.conversationsList = this.conversationsList.concat(items);
} else {
this.conversations = items;
this.conversationsList = items;
}
this.hasMoreConversations = hasMore;
@@ -2103,7 +2104,7 @@
renderConversations() {
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';
container.innerHTML = `
<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
const html = this.conversations.map(conv => {
const html = this.conversationsList.map(conv => {
const isActive = conv.user_id == this.currentUserId ? 'active' : '';
const time = this.formatTime(conv.last_time);
const preview = this.truncateText(conv.last_message || 'Sin mensajes', 50);
@@ -2149,7 +2150,7 @@
// Detectar nuevas notificaciones: comparar unread counts previos
if (!this._prevConversations) this._prevConversations = {};
this.conversations.forEach(c => {
this.conversationsList.forEach(c => {
const prev = this._prevConversations[c.user_id] || { unread_count: 0 };
if (c.unread_count > prev.unread_count && c.user_id != this.currentUserId) {
// nueva notificación
@@ -2250,7 +2251,7 @@
}
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;
const el = document.getElementById('chat-phone');
return el ? el.textContent.trim() : null;
@@ -2296,7 +2297,7 @@
document.querySelector(`[data-user-id="${userId}"]`)?.classList.add('active');
// 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) {
const btn = document.getElementById('bot-toggle');
const holdIndicator = document.getElementById('hold-indicator');
@@ -2413,7 +2414,7 @@
attendStatus.textContent = '';
// 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
this.getUserState(userId).then(serverState => {
@@ -2434,7 +2435,7 @@
const toggleAttend = async () => {
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
const serverState = await this.getUserState(userId, true);
@@ -2728,9 +2729,10 @@
this._fullLoadInProgress = true;
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 prevScrollTop = container ? container.scrollTop : 0;
const wasAtBottom = container ? ((container.scrollHeight - container.scrollTop - container.clientHeight) < 150) : true;
// indicador top cuando cargamos anteriores
let topLoader = null;
@@ -2812,7 +2814,7 @@
}
// 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) {}
// 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) {}
@@ -2824,13 +2826,13 @@
if (initial) {
// 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 {
// Prepend sólo si hay mensajes nuevos
if (Array.isArray(messages) && messages.length > 0) {
// Evitar introducir duplicados que ya existen en la vista actual
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;
messages = messages.filter(m => {
const k = this.messageKey(m);
@@ -2848,7 +2850,7 @@
} catch (e) { console.warn('duplicate filtering failed', e); }
// Prepend mensajes antiguos
this.conversations = messages.concat(this.conversations || []);
this.currentMessages = messages.concat(this.currentMessages || []);
} else {
// 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
try {
if (Array.isArray(this.conversations) && this.conversations.length > 0) {
const last = this.conversations[this.conversations.length - 1];
if (Array.isArray(this.currentMessages) && this.currentMessages.length > 0) {
const last = this.currentMessages[this.currentMessages.length - 1];
if (last && last.created_at) this._latestMessage = last.created_at;
}
} catch(e) { /* ignore */ }
// Improved dedupe: pick the most "readable" message when duplicates exist
try {
if (!this.conversations || this.conversations.length === 0) {
if (!this.currentMessages || this.currentMessages.length === 0) {
// Nothing to dedupe
} else {
const best = new Map(); // key => { index, score }
@@ -2897,8 +2899,8 @@
return s;
};
for (let i = this.conversations.length - 1; i >= 0; i--) {
const m = this.conversations[i];
for (let i = this.currentMessages.length - 1; i >= 0; i--) {
const m = this.currentMessages[i];
if (!m) continue;
const mid = m.message_id || m.id || null;
let key = null;
@@ -2931,7 +2933,7 @@
// remove from highest index down
for (let j = uniq.length - 1; j >= 0; 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) );
}
}
@@ -2953,7 +2955,7 @@
// 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
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);
} else if (initial) {
// Si es carga inicial y no tenemos mensajes, mostrar una vista vacía
@@ -2967,8 +2969,17 @@
this._fullLoadInProgress = false;
if (initial) {
// Scroll to bottom only if the caller requested following behavior
if (follow) { this.scrollToBottom(); }
// Scroll to bottom SOLO si: (1) follow fue solicitado Y (2) el usuario estaba en el fondo O (3) es el primer mensaje
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)
try {
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
try {
const before = this.conversations ? this.conversations.length : 0;
if (Array.isArray(this.conversations) && this.conversations.length > 0) {
this.conversations = this.conversations.filter(m => !this.isEmptyMessage(m));
const removed = before - this.conversations.length;
const before = this.currentMessages ? this.currentMessages.length : 0;
if (Array.isArray(this.currentMessages) && this.currentMessages.length > 0) {
this.currentMessages = this.currentMessages.filter(m => !this.isEmptyMessage(m));
const removed = before - this.currentMessages.length;
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); }
@@ -3106,8 +3117,8 @@
// calculate near-bottom late to decide scrolling after DOM updates (avoids stale value)
for (let i = 0; i < this.conversations.length; i++) {
const msg = this.conversations[i];
for (let i = 0; i < this.currentMessages.length; i++) {
const msg = this.currentMessages[i];
const mid = String(msg.message_id || msg.id || ('local_' + i));
const existingEl = existing.get(mid);
@@ -3126,7 +3137,7 @@
try {
const rp = existingEl.querySelector('.reply-preview');
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);
else {
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 */ }
// 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)) {
const sep = document.createElement('div');
sep.className = 'date-sep';
@@ -3266,7 +3277,7 @@
let replyHtml = '';
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);
replyHtml = `<div class="reply-preview">En respuesta a: ${window.escapeHtml(previewText)}</div>`;
}
@@ -3748,7 +3759,7 @@
// Append staged messages to conversation model
const toApplyCount = this._stagedMessages.length;
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
this._stagedMessages = [];
@@ -3760,7 +3771,7 @@
this.scrollToBottom();
// 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) {
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 (templateName === 'contacto_nuevo') {
// 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();
if (!resolvedName) {
const nameEl = document.getElementById('chat-name-text') || document.getElementById('chat-name');
@@ -3982,7 +3993,7 @@
msg.message_type = options.message_type;
}
this.conversations.push(msg);
this.currentMessages.push(msg);
this.renderMessagesIncremental();
this.scrollToBottom();
}
@@ -3992,7 +4003,7 @@
const preview = document.getElementById('reply-preview');
const previewText = document.getElementById('reply-preview-text');
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 text = m ? (m.content || m.message_text || (m.caption || '[Mensaje]')) : ('Mensaje ' + messageId);
@@ -4317,11 +4328,10 @@
}
// 2. Enviar mensaje con el archivo
const user = this.conversations.find(c => c.user_id === this.currentUserId);
const phone = user ? user.phone_number : null;
const phone = this.getConversationPhone(this.currentUserId);
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', {