From bbb493b544acd0ea4165633826612d0c2e914895 Mon Sep 17 00:00:00 2001 From: Adam French Date: Wed, 25 Mar 2026 03:06:27 +0000 Subject: [PATCH] Improve Chat scroll-to-bottom reliability with ResizeObserver Replace ad-hoc nextTick and media load handlers with a ResizeObserver on an inner content wrapper, which fires after layout for all content changes (new messages, image/video loads, window resize). Add scroll position tracking so auto-scroll only triggers when user is near bottom, and conditionally show the Bottom button only when scrolled up. Co-Authored-By: Claude Opus 4.6 --- nginx/vue/src/components/util/Chat.vue | 130 +++++++++++++++++-------- 1 file changed, 89 insertions(+), 41 deletions(-) diff --git a/nginx/vue/src/components/util/Chat.vue b/nginx/vue/src/components/util/Chat.vue index f859684..28c9e1b 100644 --- a/nginx/vue/src/components/util/Chat.vue +++ b/nginx/vue/src/components/util/Chat.vue @@ -11,21 +11,47 @@ const authStore = useAuthStore(); const messages = computed(() => messagesStore.messages); const messageInput = ref(""); const messagesContainer = ref(null); +const messagesInner = ref(null); const fileInput = ref(null); +const isNearBottom = ref(true); +const SCROLL_THRESHOLD = 100; +let resizeObserver = null; + function scrollToBottom() { - nextTick(() => { - if (messagesContainer.value) { - messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight; - } - }); + if (messagesContainer.value) { + messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight; + } } -watch(messages, scrollToBottom, { deep: true }); +function scrollToBottomIfNear() { + if (isNearBottom.value) { + scrollToBottom(); + } +} + +function onScroll() { + if (!messagesContainer.value) return; + const { scrollHeight, scrollTop, clientHeight } = messagesContainer.value; + isNearBottom.value = scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD; +} + +function goToBottom() { + isNearBottom.value = true; + scrollToBottom(); +} + +watch( + () => messages.value.length, + () => { + nextTick(scrollToBottomIfNear); + }, +); function sendMessage() { const text = messageInput.value.trim(); if (!text) return; + isNearBottom.value = true; messagesStore.sendMessage(text); messageInput.value = ""; } @@ -33,6 +59,7 @@ function sendMessage() { async function onFileSelected(e) { const file = e.target.files[0]; if (!file) return; + isNearBottom.value = true; await messagesStore.uploadAndSendFile(file); fileInput.value.value = ""; } @@ -73,9 +100,30 @@ function parseMessageParts(text) { onMounted(() => { messagesStore.connect(); + + if (messagesContainer.value) { + messagesContainer.value.addEventListener("scroll", onScroll, { passive: true }); + } + + if (messagesInner.value) { + resizeObserver = new ResizeObserver(scrollToBottomIfNear); + resizeObserver.observe(messagesInner.value); + } + + scrollToBottom(); }); + onUnmounted(() => { messagesStore.disconnect(); + + if (messagesContainer.value) { + messagesContainer.value.removeEventListener("scroll", onScroll); + } + + if (resizeObserver) { + resizeObserver.disconnect(); + resizeObserver = null; + } }); @@ -83,41 +131,41 @@ onUnmounted(() => {
Chat
-

- {{ message.authorId }}: - + +

+
@@ -135,7 +183,7 @@ onUnmounted(() => { @click="fileInput.click()" >Attach - +