diff --git a/conversations.php b/conversations.php index 00b39c3..14c792a 100644 --- a/conversations.php +++ b/conversations.php @@ -815,7 +815,7 @@
- + @@ -2149,12 +2149,12 @@ this.hasMoreMessages = hasMore; if (earliest) this.earliestMessage = earliest; - // Renderizar y ajustar scroll - this.renderconversations(); - - // Replace media rendering using shared helper for consistency - // (renderconversations will include the HTML from renderMediaMessage in content) + // 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; + this.renderMessagesIncremental(initial, newlyFetched); + // Replace media rendering uses window.renderMediaMessage inside element creation/update if (initial) { this.scrollToBottom(); @@ -2196,65 +2196,123 @@ } } - renderconversations() { + // Incremental rendering to avoid DOM replacement that interrupts media playback or scroll + renderMessagesIncremental(initial = false, prependCount = 0) { const container = document.getElementById('chat-conversations'); - - if (this.conversations.length === 0) { - container.innerHTML = ` -
- -

No hay mensajes en esta conversación

-
- `; - return; + if (!container) return; + + if (initial) { + container.innerHTML = ''; } - const html = this.conversations.map(msg => { - const time = new Date(msg.created_at).toLocaleTimeString('es-ES', { - hour: '2-digit', - minute: '2-digit' - }); - - const statusIcon = this.getStatusIcon(msg.status); - - // Renderizar contenido (texto o multimedia) usando helper común - const mediaPresent = (msg.local_thumb || msg.local_file || msg.media_url_external || msg.media_url); - const content = mediaPresent ? (window.renderMediaMessage ? window.renderMediaMessage(msg) : (msg.content || '[Mensaje]')) : (msg.content || '[Mensaje vacío]'); + // Map existing nodes + const existing = new Map(); + Array.from(container.querySelectorAll('[data-message-id]')).forEach(el => { + existing.set(String(el.dataset.messageId), el); + }); - // Mostrar preview si es reply/context - let replyHtml = ''; - if (msg.reply_to_message_id) { - const target = this.conversations.find(m => m.message_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: ${previewText}
`; - } + const prependEls = []; // elements to insert at top if we loaded older messages - // Mostrar reacción si existe - let reactionHtml = ''; - if (msg.reaction_emoji) { - reactionHtml = `
${msg.reaction_emoji}
`; - } - - return ` -
-
- ${replyHtml} -
${content}
- ${reactionHtml} -
- - + const wasNearBottom = (container.scrollHeight - container.scrollTop - container.clientHeight) < 150; + + for (let i = 0; i < this.conversations.length; i++) { + const msg = this.conversations[i]; + const mid = String(msg.message_id || msg.id || ('local_' + i)); + const existingEl = existing.get(mid); + + if (existingEl) { + // update in place + 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); + 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); + } + } else if (rp) rp.remove(); + + const body = existingEl.querySelector('.message-content'); + if (body) { + const mediaPresent = (msg.local_thumb || msg.local_file || msg.media_url_external || msg.media_url); + const existingMedia = body.querySelector('audio,video,img'); + let existingSrc = null; + if (existingMedia) existingSrc = existingMedia.getAttribute('src') || existingMedia.getAttribute('data-src'); + const newMediaUrl = mediaPresent ? (msg.local_thumb || msg.local_file || msg.media_url_external || msg.media_url) : null; + if (!(existingSrc && newMediaUrl && String(existingSrc).includes(newMediaUrl))) { + const newContent = mediaPresent ? (window.renderMediaMessage ? window.renderMediaMessage(msg) : (msg.content || '[Mensaje]')) : (msg.content || '[Mensaje vacío]'); + body.innerHTML = newContent; + } + } + + const timeEl = existingEl.querySelector('.message-time'); + if (timeEl) timeEl.textContent = msg.created_at ? new Date(msg.created_at).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' }) : ''; + const statusEl = existingEl.querySelector('.message-status'); + if (statusEl) statusEl.innerHTML = this.getStatusIcon(msg.status); + + const reactBadge = existingEl.querySelector('.reaction-badge'); + if (msg.reaction_emoji) { + if (reactBadge) reactBadge.textContent = msg.reaction_emoji; else { + const b = document.createElement('div'); b.className = 'reaction-badge'; b.textContent = msg.reaction_emoji; const bubble = existingEl.querySelector('.message-bubble') || existingEl; bubble.insertBefore(b, bubble.querySelector('.message-actions')); + } + } else if (reactBadge) reactBadge.remove(); + + } catch (e) { console.warn('update message failed', e); } + existing.delete(mid); + } else { + // create new element + try { + const div = document.createElement('div'); + div.className = 'message ' + (msg.direction || 'incoming'); + div.dataset.messageId = mid; + + 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 previewText = target ? (target.content || target.message_text || '').substring(0,140) : ('Mensaje ' + msg.reply_to_message_id); + replyHtml = `
En respuesta a: ${window.escapeHtml(previewText)}
`; + } + const mediaPresent = (msg.local_thumb || msg.local_file || msg.media_url_external || msg.media_url); + const content = mediaPresent ? (window.renderMediaMessage ? window.renderMediaMessage(msg) : (window.escapeHtml(msg.content || '[Mensaje]'))) : window.escapeHtml(msg.content || '[Mensaje vacío]'); + const time = msg.created_at ? new Date(msg.created_at).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' }) : ''; + const statusIcon = this.getStatusIcon(msg.status); + const reactionHtml = msg.reaction_emoji ? `
${msg.reaction_emoji}
` : ''; + + div.innerHTML = ` +
+ ${replyHtml} +
${content}
+ ${reactionHtml} +
+ + +
+
${time} ${msg.direction === 'outgoing' ? `${statusIcon}` : ''}
-
- ${time} - ${msg.direction === 'outgoing' ? `${statusIcon}` : ''} -
-
-
- `; - }).join(''); + `; - container.innerHTML = html; + // For prepended older messages, collect to insert at top later + if (prependCount && i < prependCount) { + prependEls.push(div); + } else { + container.appendChild(div); + } + } catch (e) { console.warn('create message failed', e); } + } + } + + // insert prepended elements (keep order) + if (prependEls.length) { + const frag = document.createDocumentFragment(); + prependEls.forEach(e => frag.appendChild(e)); + container.insertBefore(frag, container.firstChild); + } + + // remove any remaining old elements that weren't updated + existing.forEach((el) => el.remove()); + + // If user was near bottom before update, keep it at bottom to follow conversation + if (wasNearBottom) this.scrollToBottom(); } getStatusIcon(status) { @@ -2421,7 +2479,7 @@ } this.conversations.push(msg); - this.renderconversations(); + this.renderMessagesIncremental(); this.scrollToBottom(); }