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>
(function () {
function initChatScroll() {
const container = document.getElementById('pub-messages');
const badge = document.getElementById('new-msg-badge');
const badgeBtn = document.getElementById('new-msg-badge-btn');
const countEl = document.getElementById('new-msg-count');
var container, badge, badgeBtn, countEl;
var userScrolledUp = false;
var newMsgCount = 0;
var debounceTimer = null;
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;
let userScrolledUp = false;
let newMsgCount = 0;
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;
}
// Scroll inicial
scrollToBottom();
// Click en badge baja al fondo
if (badgeBtn) {
badgeBtn.addEventListener('click', function () {
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
@@ -645,6 +664,7 @@
});
}
// Detectar scroll manual del usuario
container.addEventListener('scroll', function () {
if (atBottom()) {
userScrolledUp = false;
@@ -654,32 +674,10 @@
}
}, { passive: true });
// Scroll inicial al fondo
scrollToBottom();
// 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);
}
// Hook oficial Livewire 2: se ejecuta después de cada update del componente
Livewire.hook('message.processed', function (message, component) {
debouncedUpdate();
});
observer.observe(container, { childList: true, subtree: true });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initChatScroll);
} else {
initChatScroll();
}
document.addEventListener('livewire:load', initChatScroll);
});
})();
</script>