this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Scroll only if the user is at the bottom of the container and remove visible bottom gap from chat messages stack

+60 -6
+60 -6
apps/web/src/components/chat-messages.tsx
··· 1 - import { useEffect, useRef } from "react"; 1 + import { useEffect, useLayoutEffect, useRef } from "react"; 2 2 import type { ChatMessage as ChatMessageType } from "#/lib/types"; 3 3 import { ChatMessage } from "./chat-message"; 4 4 import { Stack } from "./layout"; ··· 7 7 messages: ChatMessageType[]; 8 8 } 9 9 10 + const SCROLL_THRESHOLD_PX = 2; 11 + 12 + function getDistanceFromBottom(container: HTMLDivElement) { 13 + return container.scrollHeight - container.scrollTop - container.clientHeight; 14 + } 15 + 16 + function scrollToBottom(container: HTMLDivElement) { 17 + container.scrollTo({ 18 + top: container.scrollHeight, 19 + behavior: "instant", 20 + }); 21 + } 22 + 10 23 export function ChatMessages({ messages }: ChatMessageProps) { 11 - const endRef = useRef<HTMLDivElement>(null); 24 + const containerRef = useRef<HTMLDivElement>(null); 25 + const hasInitializedScrollRef = useRef(false); 26 + const shouldStickToBottomRef = useRef(true); 27 + const previousMessagesRef = useRef(messages); 12 28 13 29 useEffect(() => { 14 - if (messages.length === 0) return; 15 - endRef.current?.scrollIntoView({ block: "end", behavior: "instant" }); 30 + const container = containerRef.current; 31 + if (!container) return; 32 + 33 + function updateShouldStickToBottom() { 34 + shouldStickToBottomRef.current = 35 + getDistanceFromBottom(container) <= SCROLL_THRESHOLD_PX; 36 + } 37 + 38 + updateShouldStickToBottom(); 39 + container.addEventListener("scroll", updateShouldStickToBottom); 40 + 41 + return () => { 42 + container.removeEventListener("scroll", updateShouldStickToBottom); 43 + }; 44 + }, []); 45 + 46 + useLayoutEffect(() => { 47 + const container = containerRef.current; 48 + if (!container || messages.length === 0) return; 49 + 50 + const previousMessages = previousMessagesRef.current; 51 + const previousLastMessage = previousMessages.at(-1); 52 + const lastMessage = messages.at(-1); 53 + const messageWasAppended = messages.length > previousMessages.length; 54 + const shouldForceScroll = 55 + !hasInitializedScrollRef.current || 56 + (messageWasAppended && 57 + (lastMessage?.role === "user" || 58 + (previousLastMessage?.role === "user" && 59 + lastMessage?.role === "assistant"))); 60 + 61 + if (!hasInitializedScrollRef.current) { 62 + hasInitializedScrollRef.current = true; 63 + } 64 + 65 + if (shouldForceScroll || shouldStickToBottomRef.current) { 66 + scrollToBottom(container); 67 + shouldStickToBottomRef.current = true; 68 + } 69 + 70 + previousMessagesRef.current = messages; 16 71 }, [messages]); 17 72 18 73 return ( 19 - <Stack className="no-scrollbar flex-1 overflow-y-auto"> 74 + <Stack ref={containerRef} className="no-scrollbar flex-1 overflow-y-auto"> 20 75 {messages.map((message) => ( 21 76 <ChatMessage 22 77 key={message.id} ··· 24 79 text={message.text} 25 80 /> 26 81 ))} 27 - <div ref={endRef} /> 28 82 </Stack> 29 83 ); 30 84 }