Update conversations.php

This commit is contained in:
Lizandro Guarnizo
2026-01-24 01:13:21 -05:00
parent 7da2a0e46f
commit d0232c9c21
+117 -59
View File
@@ -815,7 +815,7 @@
</select>
</div>
<div class="quick-replies-wrapper" style="position:relative;">
<button id="quick-replies-toggle" class="btn btn-sm btn-outline-secondary" title="Respuestas rápidas" aria-expanded="false">Respuestas</button>
<button id="quick-replies-toggle" class="btn btn-sm btn-outline-secondary" title="Respuestas rápidas" aria-expanded="false">⚡️</button>
<div id="quick-replies-panel" class="quick-replies-panel" style="display:none; position:absolute; bottom:48px; left:0; z-index:1200; min-width:220px; max-width:420px;">
<!-- Quick replies buttons inserted dynamically -->
</div>
@@ -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 = `
<div class="text-center p-4">
<i class="far fa-comment fa-3x text-muted mb-3"></i>
<p class="text-muted">No hay mensajes en esta conversación</p>
</div>
`;
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 = `<div class="reply-preview">En respuesta a: ${previewText}</div>`;
}
const prependEls = []; // elements to insert at top if we loaded older messages
// Mostrar reacción si existe
let reactionHtml = '';
if (msg.reaction_emoji) {
reactionHtml = `<div class="reaction-badge">${msg.reaction_emoji}</div>`;
}
return `
<div class="message ${msg.direction}" data-message-id="${escapeHtml(msg.message_id || msg.id || '')}">
<div class="message-bubble">
${replyHtml}
<div class="message-content">${content}</div>
${reactionHtml}
<div class="message-actions mt-1">
<button class="btn btn-sm btn-link" onclick="chat.promptReply('${msg.message_id}')" title="Responder"><i class="fas fa-reply"></i></button>
<button class="btn btn-sm btn-link" onclick="chat.openReactionPicker(event, '${msg.message_id}')" title="Reaccionar"><i class="far fa-grin"></i></button>
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 = `<div class="reply-preview">En respuesta a: ${window.escapeHtml(previewText)}</div>`;
}
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 ? `<div class="reaction-badge">${msg.reaction_emoji}</div>` : '';
div.innerHTML = `
<div class="message-bubble">
${replyHtml}
<div class="message-content">${content}</div>
${reactionHtml}
<div class="message-actions mt-1">
<button class="btn btn-sm btn-link" onclick="chat.promptReply('${window.escapeHtml(msg.message_id || msg.id || '')}')" title="Responder"><i class="fas fa-reply"></i></button>
<button class="btn btn-sm btn-link" onclick="chat.openReactionPicker(event, '${window.escapeHtml(msg.message_id || msg.id || '')}')" title="Reaccionar"><i class="far fa-grin"></i></button>
</div>
<div class="message-time">${time} ${msg.direction === 'outgoing' ? `<span class="message-status">${statusIcon}</span>` : ''}</div>
</div>
<div class="message-time">
${time}
${msg.direction === 'outgoing' ? `<span class="message-status">${statusIcon}</span>` : ''}
</div>
</div>
</div>
`;
}).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();
}