From ff41763d76d8cf7e37843adba34f0199c261e7d7 Mon Sep 17 00:00:00 2001 From: Alois Date: Sat, 13 Jun 2026 12:10:34 +0200 Subject: [PATCH] (feat): add smooth scrolling --- packages/chat/src/components/message.tsx | 18 +++++++- packages/chat/src/screen.tsx | 55 +++++++++++++++++------- 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/packages/chat/src/components/message.tsx b/packages/chat/src/components/message.tsx index dc785a6..51705ff 100644 --- a/packages/chat/src/components/message.tsx +++ b/packages/chat/src/components/message.tsx @@ -29,8 +29,24 @@ function MessageComponent({ user: User | null; }) { const actuallyFailed = message.failed && message.message_state === "awaiting"; + const [hasFadedIn, setHasFadedIn] = useState(false); + const opacityClass = !hasFadedIn + ? "opacity-0" + : actuallyFailed || message.message_state === "awaiting" + ? "opacity-50" + : "opacity-100"; const [isValidURL, setIsValidURL] = useState(false); + useEffect(() => { + const timeout = window.setTimeout(() => { + setHasFadedIn(true); + }, 100); + + return () => { + window.clearTimeout(timeout); + }; + }, []); + useEffect(() => { try { if (message.content.split(" ").length > 1) throw new Error(); @@ -45,7 +61,7 @@ function MessageComponent({ return (
(null); + const smoothScrollTargetRef = React.useRef(0); const [messageUsers, setMessageUsers] = React.useState< Record @@ -356,17 +357,52 @@ export default function Screen() { return; } + smoothScrollTargetRef.current = element.scrollTop; + + const animateScroll = () => { + const distance = smoothScrollTargetRef.current - element.scrollTop; + if (Math.abs(distance) < 0.5) { + element.scrollTop = smoothScrollTargetRef.current; + smoothScrollFrameRef.current = null; + handleContainerScroll(); + return; + } + + element.scrollTop += distance * 0.35; + handleContainerScroll(); + smoothScrollFrameRef.current = requestAnimationFrame(animateScroll); + }; + const handleWheel = (event: WheelEvent) => { event.preventDefault(); event.stopPropagation(); - element.scrollTop -= event.deltaY; - handleContainerScroll(); + + if (smoothScrollFrameRef.current === null) { + smoothScrollTargetRef.current = element.scrollTop; + } + + const maxScrollTop = Math.max( + 0, + element.scrollHeight - element.clientHeight, + ); + smoothScrollTargetRef.current = Math.min( + Math.max(0, smoothScrollTargetRef.current - event.deltaY), + maxScrollTop, + ); + + if (smoothScrollFrameRef.current === null) { + smoothScrollFrameRef.current = requestAnimationFrame(animateScroll); + } }; element.addEventListener("wheel", handleWheel, { passive: false }); return () => { element.removeEventListener("wheel", handleWheel); + if (smoothScrollFrameRef.current !== null) { + cancelAnimationFrame(smoothScrollFrameRef.current); + smoothScrollFrameRef.current = null; + } }; }, [handleContainerScroll]); @@ -391,19 +427,6 @@ export default function Screen() { }} onScroll={handleContainerScroll} > - {messagesQuery.isPending && ( -
-

- - Loading... -

-
- )} - {messagesQuery.isFetchingNextPage && ( -
- -
- )}