+127
-46
@@ -2416,6 +2416,8 @@
|
|||||||
|
|
||||||
const prependEls = []; // elements to insert at top if we loaded older messages
|
const prependEls = []; // elements to insert at top if we loaded older messages
|
||||||
|
|
||||||
|
// calculate near-bottom late to decide scrolling after DOM updates (avoids stale value)
|
||||||
|
|
||||||
for (let i = 0; i < this.conversations.length; i++) {
|
for (let i = 0; i < this.conversations.length; i++) {
|
||||||
const msg = this.conversations[i];
|
const msg = this.conversations[i];
|
||||||
const mid = String(msg.message_id || msg.id || ('local_' + i));
|
const mid = String(msg.message_id || msg.id || ('local_' + i));
|
||||||
@@ -2431,57 +2433,134 @@
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renderizar imagen si corresponde
|
if (existingEl) {
|
||||||
let content = '';
|
// update in place
|
||||||
if (msg.media_type === 'image' || msg.message_type === 'image' || msg.type === 'image') {
|
try {
|
||||||
const imageUrl = msg.media_url || msg.image_url || msg.url || msg.local_thumb || msg.local_file || msg.media_url_external || msg.content;
|
const rp = existingEl.querySelector('.reply-preview');
|
||||||
const caption = msg.caption || msg.message_text || msg.content || '';
|
if (msg.reply_to_message_id) {
|
||||||
content = `<img src="${imageUrl}" alt="Imagen recibida" style="max-width:220px; border-radius:8px; margin-bottom:6px; display:block;">`;
|
const previewSrc = (this.conversations.find(m => m.message_id == msg.reply_to_message_id) || {}).content || ('Mensaje ' + msg.reply_to_message_id);
|
||||||
if (caption) content += `<div style="margin-top:4px;">${caption}</div>`;
|
if (rp) rp.textContent = 'En respuesta a: ' + previewSrc.substring(0,140);
|
||||||
} else if (msg.media_type === 'video' || msg.message_type === 'video' || msg.type === 'video') {
|
else {
|
||||||
const videoUrl = msg.media_url || msg.url || msg.local_file || msg.media_url_external;
|
const div = document.createElement('div'); div.className = 'reply-preview'; div.textContent = 'En respuesta a: ' + previewSrc.substring(0,140); existingEl.insertBefore(div, existingEl.firstChild);
|
||||||
const caption = msg.caption || msg.message_text || '';
|
}
|
||||||
content = `<video controls style="max-width:220px; border-radius:8px; margin-bottom:6px; display:block;"><source src="${videoUrl}"></video>`;
|
} else if (rp) rp.remove();
|
||||||
if (caption) content += `<div style="margin-top:4px;">${caption}</div>`;
|
|
||||||
} else if (msg.media_type === 'audio' || msg.message_type === 'audio' || msg.type === 'audio') {
|
const body = existingEl.querySelector('.message-content');
|
||||||
const audioUrl = msg.media_url || msg.url || msg.local_file || msg.media_url_external;
|
if (body) {
|
||||||
content = `<audio controls style="width:220px; border-radius:8px; margin-bottom:6px; display:block;"><source src="${audioUrl}"></audio>`;
|
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 media url unchanged, keep existing element (preserve playback)
|
||||||
|
if (existingMedia && newMediaUrl && existingSrc && String(existingSrc).includes(newMediaUrl)) {
|
||||||
|
// nothing to change
|
||||||
|
} else if (existingMedia && newMediaUrl && (!existingSrc || !String(existingSrc).includes(newMediaUrl))) {
|
||||||
|
// Replace src but preserve playback state for audio/video
|
||||||
|
try {
|
||||||
|
const tag = existingMedia.tagName && existingMedia.tagName.toLowerCase();
|
||||||
|
if (tag === 'audio' || tag === 'video') {
|
||||||
|
const currentTime = existingMedia.currentTime || 0;
|
||||||
|
const wasPaused = existingMedia.paused;
|
||||||
|
existingMedia.src = newMediaUrl;
|
||||||
|
existingMedia.addEventListener('loadedmetadata', () => {
|
||||||
|
try {
|
||||||
|
if (typeof existingMedia.duration === 'number' && !isNaN(existingMedia.duration)) {
|
||||||
|
existingMedia.currentTime = Math.min(currentTime, existingMedia.duration || currentTime);
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
try { if (!wasPaused) existingMedia.play().catch(()=>{}); } catch(e){}
|
||||||
|
}, { once: true });
|
||||||
|
} else {
|
||||||
|
// image or other: just replace src
|
||||||
|
try { existingMedia.src = newMediaUrl; } catch(e) { body.innerHTML = window.renderMediaMessage ? window.renderMediaMessage(msg) : window.escapeHtml(msg.content || '[Mensaje]'); }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('preserve media update failed', e);
|
||||||
|
body.innerHTML = window.renderMediaMessage ? window.renderMediaMessage(msg) : window.escapeHtml(msg.content || '[Mensaje]');
|
||||||
|
}
|
||||||
|
} else if (!existingMedia && newMediaUrl) {
|
||||||
|
// No existing media element, render new media block without touching other parts
|
||||||
|
try {
|
||||||
|
body.innerHTML = window.renderMediaMessage ? window.renderMediaMessage(msg) : window.escapeHtml(msg.content || '[Mensaje]');
|
||||||
|
// If after inserting media/text the body appears empty, replace with placeholder
|
||||||
|
if ((body.textContent || '').trim() === '') {
|
||||||
|
body.innerHTML = '<em>(Mensaje sin texto)</em>';
|
||||||
|
console.warn('Post-update: media/text produced empty body, set placeholder for', msg && (msg.id || msg.message_id));
|
||||||
|
}
|
||||||
|
} catch(e) { /* ignore */ }
|
||||||
|
} else if (existingMedia && !newMediaUrl) {
|
||||||
|
// Media removed in new message: replace body with text/content
|
||||||
|
try { body.innerHTML = window.escapeHtml(msg.content || '[Mensaje]'); } catch(e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
content = window.formatMessageContent ? window.formatMessageContent(msg.content || '') : window.escapeHtml(msg.content || '[Mensaje vacío]');
|
// create new element
|
||||||
}
|
try {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'message ' + (msg.direction || 'incoming');
|
||||||
|
div.dataset.messageId = mid;
|
||||||
|
|
||||||
let replyHtml = '';
|
let replyHtml = '';
|
||||||
if (msg.reply_to_message_id) {
|
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 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);
|
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>`;
|
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);
|
||||||
|
let content = mediaPresent
|
||||||
|
? (window.renderMediaMessage ? window.renderMediaMessage(msg) : (window.formatMessageContent ? window.formatMessageContent(msg.content || '[Mensaje]') : window.escapeHtml(msg.content || '[Mensaje]')))
|
||||||
|
: (window.formatMessageContent ? window.formatMessageContent(msg.content || '') : window.escapeHtml(msg.content || ''));
|
||||||
|
|
||||||
const time = msg.created_at ? new Date(msg.created_at).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' }) : '';
|
// If after formatting the content is empty (spaces or nothing), show a clearer placeholder
|
||||||
const statusIcon = this.getStatusIcon(msg.status);
|
try {
|
||||||
const reactionHtml = msg.reaction_emoji ? `<div class="reaction-badge">${msg.reaction_emoji}</div>` : '';
|
const tmp = (content || '').replace(/<[^>]*>/g, '').trim(); // strip tags and check
|
||||||
|
if (!tmp) {
|
||||||
|
console.warn('Rendered message content empty, replacing with placeholder', mid, msg.id, msg.created_at);
|
||||||
|
content = '<em>(Mensaje sin texto)</em>';
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
|
||||||
const div = document.createElement('div');
|
const time = msg.created_at ? new Date(msg.created_at).toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' }) : '';
|
||||||
div.className = 'message ' + (msg.direction || 'incoming');
|
const statusIcon = this.getStatusIcon(msg.status);
|
||||||
div.dataset.messageId = mid;
|
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>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// For prepended older messages, collect to insert at top later
|
div.innerHTML = `
|
||||||
if (prependCount && i < prependCount) {
|
<div class="message-bubble">
|
||||||
prependEls.push(div);
|
${replyHtml}
|
||||||
} else {
|
<div class="message-content">${content}</div>
|
||||||
container.appendChild(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>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2502,6 +2581,8 @@
|
|||||||
if (shouldScroll) {
|
if (shouldScroll) {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mostrar / quitar hint "No hay más mensajes anteriores"
|
// Mostrar / quitar hint "No hay más mensajes anteriores"
|
||||||
|
|||||||
Reference in New Issue
Block a user