- Add pollMensajes() so wire:poll no longer force-scrolls to bottom - MutationObserver detects new DOM nodes: auto-scrolls only if user is already at the bottom (within 80px threshold) - Floating '↓ N mensaje nuevo' badge appears when new bot messages arrive while user is scrolled up; tap badge to jump to bottom - scroll-chat event (emitted on button clicks/sends) still forces scroll since the user just took an action Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
213 lines
7.3 KiB
PHP
213 lines
7.3 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
<meta name="color-scheme" content="dark">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
<meta name="apple-mobile-web-app-title" content="Chat">
|
|
<title>Chat en vivo | {{ config('app.name') }}</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; }
|
|
|
|
html, body {
|
|
margin: 0; padding: 0;
|
|
font-family: 'Inter', sans-serif;
|
|
-webkit-tap-highlight-color: transparent;
|
|
overscroll-behavior: none;
|
|
}
|
|
|
|
/* Móvil: pantalla completa */
|
|
body {
|
|
background: #111b21;
|
|
height: 100vh;
|
|
height: 100dvh;
|
|
height: -webkit-fill-available;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Desktop: fondo con gradiente + chat centrado como frame */
|
|
@media (min-width: 640px) {
|
|
body {
|
|
background: #0d1117;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
}
|
|
}
|
|
|
|
/* El contenedor del chat */
|
|
#chat-root {
|
|
width: 100%;
|
|
height: 100vh;
|
|
height: 100dvh;
|
|
height: -webkit-fill-available;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: #111b21;
|
|
overflow: hidden;
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
#chat-root {
|
|
width: 420px;
|
|
height: 90vh;
|
|
height: 90dvh;
|
|
max-height: 820px;
|
|
border-radius: 20px;
|
|
box-shadow: 0 24px 64px rgba(0,0,0,.6);
|
|
overflow: hidden;
|
|
}
|
|
}
|
|
|
|
/* Evita zoom en inputs iOS */
|
|
input, textarea, select { font-size: 16px !important; }
|
|
|
|
/* Scroll suave nativo */
|
|
.ios-scroll {
|
|
-webkit-overflow-scrolling: touch;
|
|
overscroll-behavior-y: contain;
|
|
}
|
|
|
|
/* Elimina appearance por defecto iOS */
|
|
input[type="tel"], input[type="text"],
|
|
input[type="email"], textarea {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
button {
|
|
-webkit-appearance: none;
|
|
touch-action: manipulation;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Safe areas (solo afectan en iOS con viewport-fit=cover) */
|
|
.safe-top { padding-top: env(safe-area-inset-top, 0px); }
|
|
.safe-bottom { padding-bottom: env(safe-area-inset-bottom, 0px); }
|
|
.safe-left { padding-left: env(safe-area-inset-left, 0px); }
|
|
.safe-right { padding-right: env(safe-area-inset-right, 0px); }
|
|
</style>
|
|
@livewireStyles
|
|
</head>
|
|
<body>
|
|
|
|
<livewire:chat.public-chat />
|
|
|
|
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
@livewireScripts
|
|
|
|
<script>
|
|
(function () {
|
|
const BOTTOM_THRESHOLD = 80; // px del fondo = "está al fondo"
|
|
let isAtBottom = true;
|
|
let newMsgCount = 0;
|
|
let lastMsgCount = 0;
|
|
let msgsEl = null;
|
|
let observer = null;
|
|
|
|
/* ── Helpers ── */
|
|
function countMsgs() {
|
|
return msgsEl ? msgsEl.querySelectorAll(':scope > div').length : 0;
|
|
}
|
|
function checkBottom() {
|
|
if (!msgsEl) return true;
|
|
return (msgsEl.scrollHeight - msgsEl.scrollTop - msgsEl.clientHeight) <= BOTTOM_THRESHOLD;
|
|
}
|
|
function scrollBottom(smooth) {
|
|
if (msgsEl) msgsEl.scrollTo({ top: msgsEl.scrollHeight, behavior: smooth ? 'smooth' : 'instant' });
|
|
}
|
|
function updateBadge() {
|
|
const badge = document.getElementById('new-msg-badge');
|
|
const count = document.getElementById('new-msg-count');
|
|
if (!badge) return;
|
|
if (newMsgCount > 0 && !isAtBottom) {
|
|
if (count) count.textContent = newMsgCount;
|
|
badge.classList.remove('hidden');
|
|
} else {
|
|
badge.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
/* ── Inicializar ── */
|
|
function init() {
|
|
msgsEl = document.getElementById('pub-messages');
|
|
if (!msgsEl) return;
|
|
|
|
lastMsgCount = countMsgs();
|
|
|
|
// Rastrear posición de scroll
|
|
msgsEl.addEventListener('scroll', () => {
|
|
isAtBottom = checkBottom();
|
|
if (isAtBottom) {
|
|
newMsgCount = 0;
|
|
updateBadge();
|
|
}
|
|
}, { passive: true });
|
|
|
|
// Detectar mensajes nuevos sin hacer scroll invasivo
|
|
if (observer) observer.disconnect();
|
|
observer = new MutationObserver(() => {
|
|
const current = countMsgs();
|
|
const diff = current - lastMsgCount;
|
|
lastMsgCount = current;
|
|
if (diff <= 0) return;
|
|
|
|
if (isAtBottom) {
|
|
scrollBottom(false);
|
|
} else {
|
|
newMsgCount += diff;
|
|
updateBadge();
|
|
}
|
|
});
|
|
observer.observe(msgsEl, { childList: true });
|
|
|
|
// Badge: clic → bajar
|
|
const btn = document.getElementById('new-msg-badge-btn');
|
|
if (btn && !btn._scrollBound) {
|
|
btn._scrollBound = true;
|
|
btn.addEventListener('click', () => {
|
|
newMsgCount = 0;
|
|
isAtBottom = true;
|
|
scrollBottom(true);
|
|
updateBadge();
|
|
});
|
|
}
|
|
}
|
|
|
|
/* ── scroll-chat: emitido por PHP al enviar/click botón ── */
|
|
window.addEventListener('scroll-chat', () => {
|
|
// El usuario acaba de interactuar → siempre bajar
|
|
isAtBottom = true;
|
|
newMsgCount = 0;
|
|
updateBadge();
|
|
scrollBottom(false);
|
|
});
|
|
|
|
/* ── Ajuste de altura cuando aparece el teclado en móvil ── */
|
|
function initViewport() {
|
|
const root = document.getElementById('chat-root');
|
|
if (!root || !window.visualViewport) return;
|
|
const update = () => {
|
|
if (window.innerWidth < 640) root.style.height = window.visualViewport.height + 'px';
|
|
if (isAtBottom && msgsEl) scrollBottom(false);
|
|
};
|
|
window.visualViewport.addEventListener('resize', update);
|
|
update();
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { init(); initViewport(); });
|
|
document.addEventListener('livewire:load', () => { init(); initViewport(); });
|
|
// Re-init después de cada actualización de Livewire (por si el DOM se reinicia)
|
|
document.addEventListener('livewire:update', () => { setTimeout(init, 50); });
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|