Update conversations.php
This commit is contained in:
+118
-3
@@ -255,6 +255,17 @@
|
|||||||
white-space: -o-pre-wrap;
|
white-space: -o-pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* WhatsApp-like inline formatting and interactive messages */
|
||||||
|
.wa-interactive { margin-top: 8px; border: 1px solid rgba(0,0,0,0.06); padding: 8px; border-radius: 8px; background: #fff; }
|
||||||
|
.wa-interactive .wa-title { font-weight:700; margin-bottom:6px; }
|
||||||
|
.wa-interactive .wa-buttons { display:flex; gap:6px; flex-wrap:wrap; }
|
||||||
|
.wa-interactive .wa-buttons .wa-interactive-btn { white-space:nowrap; }
|
||||||
|
.wa-interactive .wa-list { margin-top:6px; }
|
||||||
|
.wa-interactive .wa-list-item { padding:8px; border-radius:6px; cursor:pointer; border:1px solid transparent; }
|
||||||
|
.wa-interactive .wa-list-item:hover { background:#f6f8fb; border-color:rgba(0,0,0,0.04); }
|
||||||
|
.wa-code { background:#0b0b0b; color:#fff; padding:8px; border-radius:6px; font-family: monospace; white-space: pre-wrap; }
|
||||||
|
.wa-inline-code { background:#f4f4f4; padding:2px 6px; border-radius:4px; font-family: monospace; }
|
||||||
.message-bubble:hover {
|
.message-bubble:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 8px 20px rgba(2,6,23,0.06);
|
box-shadow: 0 8px 20px rgba(2,6,23,0.06);
|
||||||
@@ -818,11 +829,11 @@
|
|||||||
<option value="text">Texto</option>
|
<option value="text">Texto</option>
|
||||||
<option value="template">Plantilla</option>
|
<option value="template">Plantilla</option>
|
||||||
</select> -->
|
</select> -->
|
||||||
<div id="templateSelectContainer" style="display:none; margin-right:8px;">
|
<!-- <div id="templateSelectContainer" style="display:none; margin-right:8px;">
|
||||||
<select id="templateSelect" class="form-select form-select-sm">
|
<select id="templateSelect" class="form-select form-select-sm">
|
||||||
<option value="">Seleccionar plantilla...</option>
|
<option value="">Seleccionar plantilla...</option>
|
||||||
</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">⚡️</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;">
|
||||||
@@ -2328,7 +2339,9 @@
|
|||||||
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);
|
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 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 || '[Mensaje vacío]') : 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 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 statusIcon = this.getStatusIcon(msg.status);
|
||||||
const reactionHtml = msg.reaction_emoji ? `<div class="reaction-badge">${msg.reaction_emoji}</div>` : '';
|
const reactionHtml = msg.reaction_emoji ? `<div class="reaction-badge">${msg.reaction_emoji}</div>` : '';
|
||||||
@@ -3181,6 +3194,108 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper para renderizar WhatsApp-like formatted text and interactive messages
|
||||||
|
function renderInteractiveMessage(obj) {
|
||||||
|
try {
|
||||||
|
const interactive = obj.interactive || obj;
|
||||||
|
const t = interactive.type || (interactive.header && 'button');
|
||||||
|
if (t === 'button' || interactive.button) {
|
||||||
|
const title = escapeHtml(interactive.title || interactive.body || '');
|
||||||
|
const buttons = interactive.buttons || interactive.button || [];
|
||||||
|
const html = [`<div class="wa-interactive">`, title ? `<div class="wa-title">${title}</div>` : ''];
|
||||||
|
html.push(`<div class="wa-buttons">`);
|
||||||
|
buttons.forEach(b => {
|
||||||
|
const label = escapeHtml(b.title || b.text || b.body || b.label || '');
|
||||||
|
html.push(`<button class="btn btn-sm btn-outline-primary wa-interactive-btn" data-value="${label}">${label}</button>`);
|
||||||
|
});
|
||||||
|
html.push(`</div></div>`);
|
||||||
|
return html.join('');
|
||||||
|
}
|
||||||
|
if (t === 'list' || interactive.sections) {
|
||||||
|
const title = escapeHtml(interactive.title || interactive.header || '');
|
||||||
|
const sections = interactive.sections || [];
|
||||||
|
const html = [`<div class="wa-interactive">`, title ? `<div class="wa-title">${title}</div>` : ''];
|
||||||
|
html.push(`<div class="wa-list">`);
|
||||||
|
sections.forEach(s => {
|
||||||
|
const secTitle = escapeHtml(s.title || '');
|
||||||
|
if (secTitle) html.push(`<div class="wa-section-title">${secTitle}</div>`);
|
||||||
|
(s.rows || []).forEach(r => {
|
||||||
|
const rowTitle = escapeHtml(r.title || r.name || r.id || '');
|
||||||
|
const rowDesc = escapeHtml(r.description || '');
|
||||||
|
html.push(`<div class="wa-list-item" data-value="${rowTitle}"><div class="wa-list-name">${rowTitle}</div>${rowDesc?`<div class="wa-list-desc small text-muted">${rowDesc}</div>`:''}</div>`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
html.push(`</div></div>`);
|
||||||
|
return html.join('');
|
||||||
|
}
|
||||||
|
} catch (e) { console.warn('renderInteractiveMessage failed', e); }
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMessageInlineCode(s) {
|
||||||
|
return s.replace(/`([^`]+)`/g, (m, g1) => `<span class="wa-inline-code">${escapeHtml(g1)}</span>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMessageContent(text) {
|
||||||
|
if (!text && text !== 0) return '';
|
||||||
|
try {
|
||||||
|
// If looks like JSON with interactive payload, render interactive UI
|
||||||
|
const t = String(text).trim();
|
||||||
|
if ((t.startsWith('{') && t.endsWith('}')) || (t.startsWith('[') && t.endsWith(']'))) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(t);
|
||||||
|
if (parsed && (parsed.interactive || parsed.type === 'interactive')) {
|
||||||
|
return renderInteractiveMessage(parsed.interactive || parsed);
|
||||||
|
}
|
||||||
|
} catch (e) { /* not json */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape HTML first
|
||||||
|
let s = escapeHtml(String(text));
|
||||||
|
|
||||||
|
// Code block (triple backticks) -> <pre>
|
||||||
|
s = s.replace(/```([\s\S]*?)```/g, (m, g1) => `<div class="wa-code">${escapeHtml(g1)}</div>`);
|
||||||
|
// Inline code
|
||||||
|
s = formatMessageInlineCode(s);
|
||||||
|
// Bold *text*
|
||||||
|
s = s.replace(/\*([^*]+)\*/g, '<strong>$1</strong>');
|
||||||
|
// Italic _text_
|
||||||
|
s = s.replace(/_([^_]+)_/g, '<em>$1</em>');
|
||||||
|
// Strikethrough ~text~
|
||||||
|
s = s.replace(/~([^~]+)~/g, '<del>$1</del>');
|
||||||
|
|
||||||
|
// Auto-link URLs
|
||||||
|
s = s.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||||
|
|
||||||
|
return s;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('formatMessageContent failed', e);
|
||||||
|
return escapeHtml(String(text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.formatMessageContent = formatMessageContent;
|
||||||
|
|
||||||
|
// Delegate clicks on interactive buttons/list items to send quick replies
|
||||||
|
document.addEventListener('click', (ev) => {
|
||||||
|
const btn = ev.target.closest('.wa-interactive-btn');
|
||||||
|
if (btn) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const v = btn.dataset.value;
|
||||||
|
if (v && window.chatApp && typeof window.chatApp.sendQuickReply === 'function') {
|
||||||
|
window.chatApp.sendQuickReply(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const item = ev.target.closest('.wa-list-item');
|
||||||
|
if (item) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const v = item.dataset.value;
|
||||||
|
if (v && window.chatApp && typeof window.chatApp.sendQuickReply === 'function') {
|
||||||
|
window.chatApp.sendQuickReply(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Helper para iconos de archivos (copiado de chat_window)
|
// Helper para iconos de archivos (copiado de chat_window)
|
||||||
function getFileIcon(filename) {
|
function getFileIcon(filename) {
|
||||||
if (!filename) return 'fas fa-file text-muted';
|
if (!filename) return 'fas fa-file text-muted';
|
||||||
|
|||||||
Reference in New Issue
Block a user