| import { useRecoilValue } from 'recoil'; | |
| import { Constants } from 'librechat-data-provider'; | |
| import { useState, useRef, useCallback, useEffect } from 'react'; | |
| import type { TMessage } from 'librechat-data-provider'; | |
| import { useMessagesConversation, useMessagesSubmission } from '~/Providers'; | |
| import useScrollToRef from '~/hooks/useScrollToRef'; | |
| import store from '~/store'; | |
| const threshold = 0.85; | |
| const debounceRate = 150; | |
| export default function useMessageScrolling(messagesTree?: TMessage[] | null) { | |
| const autoScroll = useRecoilValue(store.autoScroll); | |
| const scrollableRef = useRef<HTMLDivElement | null>(null); | |
| const messagesEndRef = useRef<HTMLDivElement | null>(null); | |
| const [showScrollButton, setShowScrollButton] = useState(false); | |
| const { conversation, conversationId } = useMessagesConversation(); | |
| const { setAbortScroll, isSubmitting, abortScroll } = useMessagesSubmission(); | |
| const timeoutIdRef = useRef<NodeJS.Timeout>(); | |
| const debouncedSetShowScrollButton = useCallback((value: boolean) => { | |
| clearTimeout(timeoutIdRef.current); | |
| timeoutIdRef.current = setTimeout(() => { | |
| setShowScrollButton(value); | |
| }, debounceRate); | |
| }, []); | |
| useEffect(() => { | |
| if (!messagesEndRef.current || !scrollableRef.current) { | |
| return; | |
| } | |
| const observer = new IntersectionObserver( | |
| ([entry]) => { | |
| debouncedSetShowScrollButton(!entry.isIntersecting); | |
| }, | |
| { root: scrollableRef.current, threshold }, | |
| ); | |
| observer.observe(messagesEndRef.current); | |
| return () => { | |
| observer.disconnect(); | |
| clearTimeout(timeoutIdRef.current); | |
| }; | |
| }, [messagesEndRef, scrollableRef, debouncedSetShowScrollButton]); | |
| const debouncedHandleScroll = useCallback(() => { | |
| if (messagesEndRef.current && scrollableRef.current) { | |
| const observer = new IntersectionObserver( | |
| ([entry]) => { | |
| debouncedSetShowScrollButton(!entry.isIntersecting); | |
| }, | |
| { root: scrollableRef.current, threshold }, | |
| ); | |
| observer.observe(messagesEndRef.current); | |
| return () => observer.disconnect(); | |
| } | |
| }, [debouncedSetShowScrollButton]); | |
| const scrollCallback = () => debouncedSetShowScrollButton(false); | |
| const { scrollToRef: scrollToBottom, handleSmoothToRef } = useScrollToRef({ | |
| targetRef: messagesEndRef, | |
| callback: scrollCallback, | |
| smoothCallback: () => { | |
| scrollCallback(); | |
| setAbortScroll(false); | |
| }, | |
| }); | |
| useEffect(() => { | |
| if (!messagesTree || messagesTree.length === 0) { | |
| return; | |
| } | |
| if (!messagesEndRef.current || !scrollableRef.current) { | |
| return; | |
| } | |
| if (isSubmitting && scrollToBottom && abortScroll !== true) { | |
| scrollToBottom(); | |
| } | |
| return () => { | |
| if (abortScroll === true) { | |
| scrollToBottom && scrollToBottom.cancel(); | |
| } | |
| }; | |
| }, [isSubmitting, messagesTree, scrollToBottom, abortScroll]); | |
| useEffect(() => { | |
| if (!messagesEndRef.current || !scrollableRef.current) { | |
| return; | |
| } | |
| if (scrollToBottom && autoScroll && conversationId !== Constants.NEW_CONVO) { | |
| scrollToBottom(); | |
| } | |
| }, [autoScroll, conversationId, scrollToBottom]); | |
| return { | |
| conversation, | |
| scrollableRef, | |
| messagesEndRef, | |
| scrollToBottom, | |
| showScrollButton, | |
| handleSmoothToRef, | |
| debouncedHandleScroll, | |
| }; | |
| } | |