Update conversations.php
This commit is contained in:
+117
-59
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user