feat(chat): separadores de fecha estilo WhatsApp

Muestra "Hoy", "Ayer" o la fecha entre grupos de mensajes de días distintos.
El timestamp del bubble ahora solo muestra la hora.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-24 09:58:08 -05:00
co-authored by Claude Sonnet 4.6
parent 42662c2ade
commit dc5fd6e272
+62 -8
View File
@@ -152,6 +152,11 @@ html, body { height: 100%; overflow: hidden; font-family: -apple-system, BlinkMa
.msg-time {
font-size: .68rem; color: #aaa; text-align: right; margin-top: 3px;
}
.date-sep {
align-self: center; background: #e9ecef; color: #6c757d;
font-size: .73rem; font-weight: 600; padding: 3px 12px;
border-radius: 99px; margin: 10px 0; user-select: none;
}
.msg-type-badge {
font-size: .7rem; color: #888; font-style: italic;
}
@@ -743,6 +748,8 @@ async function pollMessages() {
}
// ── Render mensajes ───────────────────────────────────────────────────────────
let _lastRenderedDay = null;
function renderMessages(msgs, prepend) {
const area = document.getElementById('messagesArea');
area.innerHTML = '';
@@ -755,7 +762,16 @@ function renderMessages(msgs, prepend) {
loadBtn.innerHTML = '<i class="fas fa-chevron-up"></i> Ver mensajes anteriores';
area.appendChild(loadBtn);
msgs.forEach(m => area.appendChild(buildBubble(m)));
let lastDay = null;
msgs.forEach(m => {
const day = m.created_at ? msgDay(m.created_at) : null;
if (day && day !== lastDay) {
area.appendChild(buildDateSep(dateLabel(m.created_at)));
lastDay = day;
}
area.appendChild(buildBubble(m));
});
_lastRenderedDay = lastDay;
area.scrollTop = area.scrollHeight;
}
@@ -763,15 +779,39 @@ function prependMessages(msgs) {
const area = document.getElementById('messagesArea');
const oldTop = area.scrollHeight - area.scrollTop;
const loadBtn = document.getElementById('loadMoreBtn');
msgs.forEach(m => area.insertBefore(buildBubble(m), loadBtn.nextSibling));
// Día del primer mensaje que ya estaba en pantalla (para no duplicar separador)
const firstBubble = loadBtn.nextSibling;
const firstExistingDay = firstBubble?.dataset?.msgId
? msgDay(msgs[msgs.length - 1]?.created_at || '') // comparar con el último del lote nuevo
: null;
let lastDay = null;
let insertRef = loadBtn.nextSibling;
msgs.forEach(m => {
const day = m.created_at ? msgDay(m.created_at) : null;
if (day && day !== lastDay) {
area.insertBefore(buildDateSep(dateLabel(m.created_at)), insertRef);
lastDay = day;
}
area.insertBefore(buildBubble(m), insertRef);
insertRef = area.querySelector(`[data-msg-id="${m.id}"]`)?.nextSibling || insertRef;
});
// Eliminar separador duplicado si el último día del lote coincide con el primero ya visible
const seps = area.querySelectorAll('.date-sep');
for (let i = 0; i < seps.length - 1; i++) {
if (seps[i].textContent === seps[i+1].textContent) seps[i+1].remove();
}
if (!state.hasMore) loadBtn.style.display = 'none';
area.scrollTop = area.scrollHeight - oldTop;
}
function appendMessage(msg) {
const area = document.getElementById('messagesArea');
const bubble = buildBubble(msg);
area.appendChild(bubble);
const area = document.getElementById('messagesArea');
const day = msg.created_at ? msgDay(msg.created_at) : null;
if (day && day !== _lastRenderedDay) {
area.appendChild(buildDateSep(dateLabel(msg.created_at)));
_lastRenderedDay = day;
}
area.appendChild(buildBubble(msg));
area.scrollTop = area.scrollHeight;
}
@@ -1415,6 +1455,22 @@ function escHtml(str) {
return String(str ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function nl2br(str) { return str.replace(/\n/g, '<br>'); }
function dateLabel(ts) {
const d = new Date(ts.replace(' ', 'T'));
const now = new Date();
const yesterday = new Date(now); yesterday.setDate(now.getDate() - 1);
if (d.toDateString() === now.toDateString()) return 'Hoy';
if (d.toDateString() === yesterday.toDateString()) return 'Ayer';
return d.toLocaleDateString('es-CO', { weekday:'short', day:'2-digit', month:'short' });
}
function buildDateSep(label) {
const el = document.createElement('div');
el.className = 'date-sep';
el.textContent = label;
return el;
}
function msgDay(ts) { return new Date(ts.replace(' ', 'T')).toDateString(); }
function formatTime(ts) {
const d = new Date(ts.replace(' ', 'T'));
const now = new Date();
@@ -1425,9 +1481,7 @@ function formatTime(ts) {
}
function formatTimeFull(ts) {
const d = new Date(ts.replace(' ', 'T'));
const t = d.toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit' });
if (d.toDateString() === new Date().toDateString()) return t;
return d.toLocaleDateString('es-CO', { day: '2-digit', month: '2-digit' }) + ' ' + t;
return d.toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit' });
}
</script>
</body>