mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 18:00:15 +01:00
refactor: Consolidate Message Scrolling & other Logic to Custom Hooks 🔄 (#1257)
* refactor: remove unnecessary drilling/invoking of ScrollToBottom - feat: useMessageScrolling: consolidates all scrolling logic to hook - feat: useMessageHelpers: creates message utilities and consolidates logic from UI component * fix: ensure automatic scrolling is triggered by messagesTree re-render and is throttled
This commit is contained in:
parent
ebd23f7295
commit
4674a54c70
12 changed files with 208 additions and 169 deletions
2
client/src/hooks/Messages/index.ts
Normal file
2
client/src/hooks/Messages/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as useMessageHelpers } from './useMessageHelpers';
|
||||
export { default as useMessageScrolling } from './useMessageScrolling';
|
||||
83
client/src/hooks/Messages/useMessageHelpers.ts
Normal file
83
client/src/hooks/Messages/useMessageHelpers.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { useEffect } from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import type { TMessageProps } from '~/common';
|
||||
import Icon from '~/components/Endpoints/Icon';
|
||||
import { useChatContext } from '~/Providers';
|
||||
|
||||
export default function useMessageHelpers(props: TMessageProps) {
|
||||
const { message, currentEditId, setCurrentEditId } = props;
|
||||
|
||||
const {
|
||||
ask,
|
||||
regenerate,
|
||||
isSubmitting,
|
||||
conversation,
|
||||
latestMessage,
|
||||
setAbortScroll,
|
||||
handleContinue,
|
||||
setLatestMessage,
|
||||
} = useChatContext();
|
||||
|
||||
const { text, children, messageId = null, isCreatedByUser } = message ?? {};
|
||||
const edit = messageId === currentEditId;
|
||||
const isLast = !children?.length;
|
||||
|
||||
useEffect(() => {
|
||||
if (!message) {
|
||||
return;
|
||||
} else if (isLast) {
|
||||
setLatestMessage({ ...message });
|
||||
}
|
||||
}, [isLast, message, setLatestMessage]);
|
||||
|
||||
const enterEdit = (cancel?: boolean) =>
|
||||
setCurrentEditId && setCurrentEditId(cancel ? -1 : messageId);
|
||||
|
||||
const handleScroll = () => {
|
||||
if (isSubmitting) {
|
||||
setAbortScroll(true);
|
||||
} else {
|
||||
setAbortScroll(false);
|
||||
}
|
||||
};
|
||||
|
||||
const icon = Icon({
|
||||
...conversation,
|
||||
...(message as TMessage),
|
||||
model: message?.model ?? conversation?.model,
|
||||
size: 28.8,
|
||||
});
|
||||
|
||||
const regenerateMessage = () => {
|
||||
if ((isSubmitting && isCreatedByUser) || !message) {
|
||||
return;
|
||||
}
|
||||
|
||||
regenerate(message);
|
||||
};
|
||||
|
||||
const copyToClipboard = (setIsCopied: React.Dispatch<React.SetStateAction<boolean>>) => {
|
||||
setIsCopied(true);
|
||||
copy(text ?? '');
|
||||
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
return {
|
||||
ask,
|
||||
icon,
|
||||
edit,
|
||||
isLast,
|
||||
enterEdit,
|
||||
conversation,
|
||||
isSubmitting,
|
||||
handleScroll,
|
||||
latestMessage,
|
||||
handleContinue,
|
||||
copyToClipboard,
|
||||
regenerateMessage,
|
||||
};
|
||||
}
|
||||
83
client/src/hooks/Messages/useMessageScrolling.ts
Normal file
83
client/src/hooks/Messages/useMessageScrolling.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { useRecoilValue } from 'recoil';
|
||||
import { useLayoutEffect, useState, useRef, useCallback, useEffect } from 'react';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import useScrollToRef from '../useScrollToRef';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import store from '~/store';
|
||||
|
||||
export default function useMessageScrolling(messagesTree?: TMessage[] | null) {
|
||||
const autoScroll = useRecoilValue(store.autoScroll);
|
||||
|
||||
const timeoutIdRef = useRef<NodeJS.Timeout>();
|
||||
const scrollableRef = useRef<HTMLDivElement | null>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement | null>(null);
|
||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||
const { conversation, setAbortScroll, isSubmitting, abortScroll } = useChatContext();
|
||||
const { conversationId } = conversation ?? {};
|
||||
|
||||
const checkIfAtBottom = useCallback(() => {
|
||||
if (!scrollableRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current;
|
||||
const diff = Math.abs(scrollHeight - scrollTop);
|
||||
const percent = Math.abs(clientHeight - diff) / clientHeight;
|
||||
const hasScrollbar = scrollHeight > clientHeight && percent >= 0.15;
|
||||
setShowScrollButton(hasScrollbar);
|
||||
}, [scrollableRef]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const scrollableElement = scrollableRef.current;
|
||||
if (!scrollableElement) {
|
||||
return;
|
||||
}
|
||||
const timeoutId = setTimeout(checkIfAtBottom, 650);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [checkIfAtBottom]);
|
||||
|
||||
const debouncedHandleScroll = useCallback(() => {
|
||||
clearTimeout(timeoutIdRef.current);
|
||||
timeoutIdRef.current = setTimeout(checkIfAtBottom, 100);
|
||||
}, [checkIfAtBottom]);
|
||||
|
||||
const scrollCallback = () => setShowScrollButton(false);
|
||||
|
||||
const { scrollToRef: scrollToBottom, handleSmoothToRef } = useScrollToRef({
|
||||
targetRef: messagesEndRef,
|
||||
callback: scrollCallback,
|
||||
smoothCallback: () => {
|
||||
scrollCallback();
|
||||
setAbortScroll(false);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!messagesTree) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSubmitting && scrollToBottom && !abortScroll) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}, [isSubmitting, messagesTree, scrollToBottom, abortScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollToBottom && autoScroll && conversationId !== 'new') {
|
||||
scrollToBottom();
|
||||
}
|
||||
}, [autoScroll, conversationId, scrollToBottom]);
|
||||
|
||||
return {
|
||||
conversation,
|
||||
scrollableRef,
|
||||
messagesEndRef,
|
||||
scrollToBottom,
|
||||
showScrollButton,
|
||||
handleSmoothToRef,
|
||||
debouncedHandleScroll,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
export * from './Messages';
|
||||
|
||||
export * from './AuthContext';
|
||||
export * from './ThemeContext';
|
||||
export * from './ScreenshotContext';
|
||||
|
|
|
|||
|
|
@ -8,29 +8,22 @@ type TUseScrollToRef = {
|
|||
};
|
||||
|
||||
export default function useScrollToRef({ targetRef, callback, smoothCallback }: TUseScrollToRef) {
|
||||
const logAndScroll = (behavior: 'instant' | 'smooth', callbackFn: () => void) => {
|
||||
// Debugging:
|
||||
// console.log(`Scrolling with behavior: ${behavior}, Time: ${new Date().toISOString()}`);
|
||||
targetRef.current?.scrollIntoView({ behavior });
|
||||
callbackFn();
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const scrollToRef = useCallback(
|
||||
throttle(
|
||||
() => {
|
||||
targetRef.current?.scrollIntoView({ behavior: 'instant' });
|
||||
callback();
|
||||
},
|
||||
450,
|
||||
{ leading: true },
|
||||
),
|
||||
throttle(() => logAndScroll('instant', callback), 450, { leading: true }),
|
||||
[targetRef],
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const scrollToRefSmooth = useCallback(
|
||||
throttle(
|
||||
() => {
|
||||
targetRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
smoothCallback();
|
||||
},
|
||||
750,
|
||||
{ leading: true },
|
||||
),
|
||||
throttle(() => logAndScroll('smooth', smoothCallback), 750, { leading: true }),
|
||||
[targetRef],
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue