Update conversations.php
This commit is contained in:
+117
-59
@@ -815,7 +815,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-replies-wrapper" style="position:relative;">
|
<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;">
|
<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 -->
|
<!-- Quick replies buttons inserted dynamically -->
|
||||||
</div>
|
</div>
|
||||||
@@ -2149,12 +2149,12 @@
|
|||||||
this.hasMoreMessages = hasMore;
|
this.hasMoreMessages = hasMore;
|
||||||
if (earliest) this.earliestMessage = earliest;
|
if (earliest) this.earliestMessage = earliest;
|
||||||
|
|
||||||
// Renderizar y ajustar scroll
|
// Renderizar incrementalmente para evitar parpadeos y preservar reproducción de media
|
||||||
this.renderconversations();
|
// 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;
|
||||||
// Replace media rendering using shared helper for consistency
|
this.renderMessagesIncremental(initial, newlyFetched);
|
||||||
// (renderconversations will include the HTML from renderMediaMessage in content)
|
|
||||||
|
|
||||||
|
// Replace media rendering uses window.renderMediaMessage inside element creation/update
|
||||||
|
|
||||||
if (initial) {
|
if (initial) {
|
||||||
this.scrollToBottom();
|
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');
|
const container = document.getElementById('chat-conversations');
|
||||||
|
if (!container) return;
|
||||||
if (this.conversations.length === 0) {
|
|
||||||
container.innerHTML = `
|
if (initial) {
|
||||||
<div class="text-center p-4">
|
container.innerHTML = '';
|
||||||
<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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const html = this.conversations.map(msg => {
|
// Map existing nodes
|
||||||
const time = new Date(msg.created_at).toLocaleTimeString('es-ES', {
|
const existing = new Map();
|
||||||
hour: '2-digit',
|
Array.from(container.querySelectorAll('[data-message-id]')).forEach(el => {
|
||||||
minute: '2-digit'
|
existing.set(String(el.dataset.messageId), el);
|
||||||
});
|
});
|
||||||
|
|
||||||
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]');
|
|
||||||
|
|
||||||
// Mostrar preview si es reply/context
|
const prependEls = []; // elements to insert at top if we loaded older messages
|
||||||
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>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mostrar reacción si existe
|
const wasNearBottom = (container.scrollHeight - container.scrollTop - container.clientHeight) < 150;
|
||||||
let reactionHtml = '';
|
|
||||||
if (msg.reaction_emoji) {
|
for (let i = 0; i < this.conversations.length; i++) {
|
||||||
reactionHtml = `<div class="reaction-badge">${msg.reaction_emoji}</div>`;
|
const msg = this.conversations[i];
|
||||||
}
|
const mid = String(msg.message_id || msg.id || ('local_' + i));
|
||||||
|
const existingEl = existing.get(mid);
|
||||||
return `
|
|
||||||
<div class="message ${msg.direction}" data-message-id="${escapeHtml(msg.message_id || msg.id || '')}">
|
if (existingEl) {
|
||||||
<div class="message-bubble">
|
// update in place
|
||||||
${replyHtml}
|
try {
|
||||||
<div class="message-content">${content}</div>
|
const rp = existingEl.querySelector('.reply-preview');
|
||||||
${reactionHtml}
|
if (msg.reply_to_message_id) {
|
||||||
<div class="message-actions mt-1">
|
const previewSrc = (this.conversations.find(m => m.message_id == msg.reply_to_message_id) || {}).content || ('Mensaje ' + msg.reply_to_message_id);
|
||||||
<button class="btn btn-sm btn-link" onclick="chat.promptReply('${msg.message_id}')" title="Responder"><i class="fas fa-reply"></i></button>
|
if (rp) rp.textContent = 'En respuesta a: ' + previewSrc.substring(0,140);
|
||||||
<button class="btn btn-sm btn-link" onclick="chat.openReactionPicker(event, '${msg.message_id}')" title="Reaccionar"><i class="far fa-grin"></i></button>
|
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>
|
||||||
<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) {
|
getStatusIcon(status) {
|
||||||
@@ -2421,7 +2479,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.conversations.push(msg);
|
this.conversations.push(msg);
|
||||||
this.renderconversations();
|
this.renderMessagesIncremental();
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user