diff --git a/conversations.php b/conversations.php index 79ce46a..3e0cc1b 100644 --- a/conversations.php +++ b/conversations.php @@ -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 = `
@@ -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 = `
En respuesta a: ${window.escapeHtml(previewText)}
`; } @@ -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', {