mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
⏬ feat: Optimize Scroll Handling with Intersection Observer (#3564)
* ⏬ refactor(ScrollToBottom): use Intersection Observer for efficient scroll handling
* chore: imports, remove debug console
This commit is contained in:
parent
6879de0bf1
commit
c2a79aee1b
2 changed files with 54 additions and 27 deletions
|
|
@ -1,50 +1,63 @@
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { useLayoutEffect, useState, useRef, useCallback, useEffect } from 'react';
|
import { Constants } from 'librechat-data-provider';
|
||||||
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||||
import type { TMessage } from 'librechat-data-provider';
|
import type { TMessage } from 'librechat-data-provider';
|
||||||
import useScrollToRef from '../useScrollToRef';
|
import useScrollToRef from '~/hooks/useScrollToRef';
|
||||||
import { useChatContext } from '~/Providers';
|
import { useChatContext } from '~/Providers';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function useMessageScrolling(messagesTree?: TMessage[] | null) {
|
export default function useMessageScrolling(messagesTree?: TMessage[] | null) {
|
||||||
const autoScroll = useRecoilValue(store.autoScroll);
|
const autoScroll = useRecoilValue(store.autoScroll);
|
||||||
|
|
||||||
const timeoutIdRef = useRef<NodeJS.Timeout>();
|
|
||||||
const scrollableRef = useRef<HTMLDivElement | null>(null);
|
const scrollableRef = useRef<HTMLDivElement | null>(null);
|
||||||
const messagesEndRef = useRef<HTMLDivElement | null>(null);
|
const messagesEndRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||||
const { conversation, setAbortScroll, isSubmitting, abortScroll } = useChatContext();
|
const { conversation, setAbortScroll, isSubmitting, abortScroll } = useChatContext();
|
||||||
const { conversationId } = conversation ?? {};
|
const { conversationId } = conversation ?? {};
|
||||||
|
|
||||||
const checkIfAtBottom = useCallback(() => {
|
const timeoutIdRef = useRef<NodeJS.Timeout>();
|
||||||
if (!scrollableRef.current) {
|
|
||||||
|
const debouncedSetShowScrollButton = useCallback((value: boolean) => {
|
||||||
|
clearTimeout(timeoutIdRef.current);
|
||||||
|
timeoutIdRef.current = setTimeout(() => {
|
||||||
|
setShowScrollButton(value);
|
||||||
|
}, 150);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!messagesEndRef.current || !scrollableRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current;
|
const observer = new IntersectionObserver(
|
||||||
const diff = Math.abs(scrollHeight - scrollTop);
|
([entry]) => {
|
||||||
const percent = Math.abs(clientHeight - diff) / clientHeight;
|
debouncedSetShowScrollButton(!entry.isIntersecting);
|
||||||
const hasScrollbar = scrollHeight > clientHeight && percent >= 0.15;
|
},
|
||||||
setShowScrollButton(hasScrollbar);
|
{ root: scrollableRef.current, threshold: 0.1 },
|
||||||
}, [scrollableRef]);
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
observer.observe(messagesEndRef.current);
|
||||||
const scrollableElement = scrollableRef.current;
|
|
||||||
if (!scrollableElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const timeoutId = setTimeout(checkIfAtBottom, 650);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(timeoutId);
|
observer.disconnect();
|
||||||
|
clearTimeout(timeoutIdRef.current);
|
||||||
};
|
};
|
||||||
}, [checkIfAtBottom]);
|
}, [messagesEndRef, scrollableRef, debouncedSetShowScrollButton]);
|
||||||
|
|
||||||
const debouncedHandleScroll = useCallback(() => {
|
const debouncedHandleScroll = useCallback(() => {
|
||||||
clearTimeout(timeoutIdRef.current);
|
if (messagesEndRef.current && scrollableRef.current) {
|
||||||
timeoutIdRef.current = setTimeout(checkIfAtBottom, 100);
|
const observer = new IntersectionObserver(
|
||||||
}, [checkIfAtBottom]);
|
([entry]) => {
|
||||||
|
debouncedSetShowScrollButton(!entry.isIntersecting);
|
||||||
|
},
|
||||||
|
{ root: scrollableRef.current, threshold: 0.1 },
|
||||||
|
);
|
||||||
|
observer.observe(messagesEndRef.current);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}
|
||||||
|
}, [debouncedSetShowScrollButton]);
|
||||||
|
|
||||||
const scrollCallback = () => setShowScrollButton(false);
|
const scrollCallback = () => debouncedSetShowScrollButton(false);
|
||||||
|
|
||||||
const { scrollToRef: scrollToBottom, handleSmoothToRef } = useScrollToRef({
|
const { scrollToRef: scrollToBottom, handleSmoothToRef } = useScrollToRef({
|
||||||
targetRef: messagesEndRef,
|
targetRef: messagesEndRef,
|
||||||
|
|
@ -66,13 +79,13 @@ export default function useMessageScrolling(messagesTree?: TMessage[] | null) {
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (abortScroll) {
|
if (abortScroll) {
|
||||||
scrollToBottom && scrollToBottom?.cancel();
|
scrollToBottom && scrollToBottom.cancel();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [isSubmitting, messagesTree, scrollToBottom, abortScroll]);
|
}, [isSubmitting, messagesTree, scrollToBottom, abortScroll]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scrollToBottom && autoScroll && conversationId !== 'new') {
|
if (scrollToBottom && autoScroll && conversationId !== Constants.NEW_CONVO) {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
}, [autoScroll, conversationId, scrollToBottom]);
|
}, [autoScroll, conversationId, scrollToBottom]);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,21 @@ type TUseScrollToRef = {
|
||||||
smoothCallback: () => void;
|
smoothCallback: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function useScrollToRef({ targetRef, callback, smoothCallback }: TUseScrollToRef) {
|
type ThrottledFunction = (() => void) & {
|
||||||
|
cancel: () => void;
|
||||||
|
flush: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ScrollToRefReturn = {
|
||||||
|
scrollToRef?: ThrottledFunction;
|
||||||
|
handleSmoothToRef: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useScrollToRef({
|
||||||
|
targetRef,
|
||||||
|
callback,
|
||||||
|
smoothCallback,
|
||||||
|
}: TUseScrollToRef): ScrollToRefReturn {
|
||||||
const logAndScroll = (behavior: 'instant' | 'smooth', callbackFn: () => void) => {
|
const logAndScroll = (behavior: 'instant' | 'smooth', callbackFn: () => void) => {
|
||||||
// Debugging:
|
// Debugging:
|
||||||
// console.log(`Scrolling with behavior: ${behavior}, Time: ${new Date().toISOString()}`);
|
// console.log(`Scrolling with behavior: ${behavior}, Time: ${new Date().toISOString()}`);
|
||||||
|
|
@ -17,7 +31,7 @@ export default function useScrollToRef({ targetRef, callback, smoothCallback }:
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const scrollToRef = useCallback(
|
const scrollToRef = useCallback(
|
||||||
throttle(() => logAndScroll('instant', callback), 250, { leading: true }),
|
throttle(() => logAndScroll('instant', callback), 145, { leading: true }),
|
||||||
[targetRef],
|
[targetRef],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue