Skip to content

Commit db7b133

Browse files
authored
fix: improve autoscroll UX (#128)
1 parent ac21d13 commit db7b133

File tree

1 file changed

+23
-17
lines changed

1 file changed

+23
-17
lines changed

chat/src/components/message-list.tsx

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import React, {useLayoutEffect, useRef, useEffect, useCallback, useMemo} from "react";
3+
import React, {useLayoutEffect, useRef, useEffect, useCallback, useMemo, useState} from "react";
44

55
interface Message {
66
role: string;
@@ -24,18 +24,20 @@ interface ProcessedMessageProps {
2424
}
2525

2626
export default function MessageList({messages}: MessageListProps) {
27-
const scrollAreaRef = useRef<HTMLDivElement>(null);
27+
const [scrollAreaRef, setScrollAreaRef] = useState<HTMLDivElement | null>(null);
2828

2929
// Track if user is at bottom - default to true for initial scroll
3030
const isAtBottomRef = useRef(true);
3131
// Track the last known scroll height to detect new content
3232
const lastScrollHeightRef = useRef(0);
33+
// Track if we're currently doing a programmatic scroll
34+
const isProgrammaticScrollRef = useRef(false);
3335

3436
const checkIfAtBottom = useCallback(() => {
35-
if (!scrollAreaRef.current) return false;
36-
const { scrollTop, scrollHeight, clientHeight } = scrollAreaRef.current;
37+
if (!scrollAreaRef) return false;
38+
const { scrollTop, scrollHeight, clientHeight } = scrollAreaRef;
3739
return scrollTop + clientHeight >= scrollHeight - 10; // 10px tolerance
38-
}, []);
40+
}, [scrollAreaRef]);
3941

4042
// Track Ctrl (Windows/Linux) or Cmd (Mac) key state
4143
// This is so that underline is only visible when hover + cmd/ctrl
@@ -60,26 +62,30 @@ export default function MessageList({messages}: MessageListProps) {
6062

6163
// Update isAtBottom on scroll
6264
useEffect(() => {
63-
const scrollContainer = scrollAreaRef.current;
64-
if (!scrollContainer) return;
65+
if (!scrollAreaRef) return;
6566

6667
const handleScroll = () => {
68+
if (isProgrammaticScrollRef.current) return;
6769
isAtBottomRef.current = checkIfAtBottom();
6870
};
6971

7072
// Initial check
7173
handleScroll();
7274

73-
scrollContainer.addEventListener("scroll", handleScroll);
74-
return () => scrollContainer.removeEventListener("scroll", handleScroll);
75-
}, [checkIfAtBottom]);
75+
scrollAreaRef.addEventListener("scroll", handleScroll);
76+
scrollAreaRef.addEventListener("scrollend", () => isProgrammaticScrollRef.current = false);
77+
return () => {
78+
scrollAreaRef.removeEventListener("scroll", handleScroll)
79+
scrollAreaRef.removeEventListener("scrollend", () => isProgrammaticScrollRef.current = false);
80+
81+
};
82+
}, [checkIfAtBottom, scrollAreaRef]);
7683

7784
// Handle auto-scrolling when messages change
7885
useLayoutEffect(() => {
79-
if (!scrollAreaRef.current) return;
86+
if (!scrollAreaRef) return;
8087

81-
const scrollContainer = scrollAreaRef.current;
82-
const currentScrollHeight = scrollContainer.scrollHeight;
88+
const currentScrollHeight = scrollAreaRef.scrollHeight;
8389

8490
// Check if this is new content (scroll height increased)
8591
const hasNewContent = currentScrollHeight > lastScrollHeightRef.current;
@@ -95,7 +101,8 @@ export default function MessageList({messages}: MessageListProps) {
95101
hasNewContent &&
96102
(isFirstRender || isAtBottomRef.current || isNewUserMessage)
97103
) {
98-
scrollContainer.scrollTo({
104+
isProgrammaticScrollRef.current = true;
105+
scrollAreaRef.scrollTo({
99106
top: currentScrollHeight,
100107
behavior: isFirstRender ? "instant" : "smooth",
101108
});
@@ -105,7 +112,7 @@ export default function MessageList({messages}: MessageListProps) {
105112

106113
// Update the last known scroll height
107114
lastScrollHeightRef.current = currentScrollHeight;
108-
}, [messages]);
115+
}, [messages, scrollAreaRef]);
109116

110117
// If no messages, show a placeholder
111118
if (messages.length === 0) {
@@ -117,7 +124,7 @@ export default function MessageList({messages}: MessageListProps) {
117124
}
118125

119126
return (
120-
<div className="overflow-y-auto flex-1" ref={scrollAreaRef}>
127+
<div className="overflow-y-auto flex-1" ref={setScrollAreaRef}>
121128
<div
122129
className="p-4 flex flex-col gap-4 max-w-4xl mx-auto transition-all duration-300 ease-in-out min-h-0">
123130
{messages.map((message, index) => (
@@ -191,7 +198,6 @@ const ProcessedMessage = React.memo(function ProcessedMessage({
191198

192199
const linkedContent = useMemo(() => {
193200
return messageContent.split(urlRegex).map((content, idx) => {
194-
console.log(content)
195201
if (urlRegex.test(content)) {
196202
return (
197203
<a

0 commit comments

Comments
 (0)