diff --git a/conversations.php b/conversations.php
index 00b39c3..14c792a 100644
--- a/conversations.php
+++ b/conversations.php
@@ -815,7 +815,7 @@
-
Respuestas
+
⚡️
@@ -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();
}