fix: scroll chat usa Livewire.hook oficial + debounce para Android

MutationObserver disparaba scroll repetido durante render de Livewire
bloqueando el touch scroll en Android. Ahora usa Livewire.hook('message.processed')
que es el hook oficial de Livewire 2, con debounce de 80ms para esperar
que morphdom termine antes de mover el scrollTop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro
2026-07-19 22:12:51 +00:00
co-authored by Claude Sonnet 4.6
parent d77eabc272
commit 7cc696e04a
@@ -605,39 +605,58 @@
<script> <script>
(function () { (function () {
function initChatScroll() { var container, badge, badgeBtn, countEl;
const container = document.getElementById('pub-messages'); var userScrolledUp = false;
const badge = document.getElementById('new-msg-badge'); var newMsgCount = 0;
const badgeBtn = document.getElementById('new-msg-badge-btn'); var debounceTimer = null;
const countEl = document.getElementById('new-msg-count');
function atBottom() {
return container.scrollHeight - container.scrollTop - container.clientHeight < 120;
}
function scrollToBottom() {
container.scrollTop = container.scrollHeight;
}
function showBadge(n) {
if (!badge || !countEl) return;
countEl.textContent = n;
badge.classList.remove('hidden');
badge.classList.add('flex');
}
function hideBadge() {
if (!badge) return;
badge.classList.add('hidden');
badge.classList.remove('flex');
newMsgCount = 0;
}
function onUpdate() {
if (!userScrolledUp) {
scrollToBottom();
} else {
newMsgCount++;
showBadge(newMsgCount);
}
}
function debouncedUpdate() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(onUpdate, 80);
}
document.addEventListener('livewire:load', function () {
container = document.getElementById('pub-messages');
badge = document.getElementById('new-msg-badge');
badgeBtn = document.getElementById('new-msg-badge-btn');
countEl = document.getElementById('new-msg-count');
if (!container) return; if (!container) return;
let userScrolledUp = false; // Scroll inicial
let newMsgCount = 0; scrollToBottom();
let lastScrollHeight = container.scrollHeight;
function atBottom() {
return container.scrollHeight - container.scrollTop - container.clientHeight < 120;
}
function scrollToBottom() {
container.scrollTop = container.scrollHeight;
}
function showBadge(n) {
if (!badge || !countEl) return;
countEl.textContent = n;
badge.classList.remove('hidden');
badge.classList.add('flex');
}
function hideBadge() {
if (!badge) return;
badge.classList.add('hidden');
badge.classList.remove('flex');
newMsgCount = 0;
}
// Click en badge baja al fondo
if (badgeBtn) { if (badgeBtn) {
badgeBtn.addEventListener('click', function () { badgeBtn.addEventListener('click', function () {
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' }); container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
@@ -645,6 +664,7 @@
}); });
} }
// Detectar scroll manual del usuario
container.addEventListener('scroll', function () { container.addEventListener('scroll', function () {
if (atBottom()) { if (atBottom()) {
userScrolledUp = false; userScrolledUp = false;
@@ -654,32 +674,10 @@
} }
}, { passive: true }); }, { passive: true });
// Scroll inicial al fondo // Hook oficial Livewire 2: se ejecuta después de cada update del componente
scrollToBottom(); Livewire.hook('message.processed', function (message, component) {
debouncedUpdate();
// MutationObserver: detecta cuando Livewire agrega/modifica nodos en el contenedor
const observer = new MutationObserver(function () {
const newHeight = container.scrollHeight;
if (newHeight === lastScrollHeight) return;
lastScrollHeight = newHeight;
if (!userScrolledUp) {
scrollToBottom();
} else {
newMsgCount++;
showBadge(newMsgCount);
}
}); });
});
observer.observe(container, { childList: true, subtree: true });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initChatScroll);
} else {
initChatScroll();
}
document.addEventListener('livewire:load', initChatScroll);
})(); })();
</script> </script>