From e57d4c9f9a8b8c81932a26d94b542c9b61ead2f9 Mon Sep 17 00:00:00 2001 From: jwcrystal <121911854+jwcrystal@users.noreply.github.com> Date: Mon, 19 May 2025 16:33:06 +0800 Subject: [PATCH] refactor(Scrollbar): Optimize scroll handling logic to support external scroll events (#6047) * refactor(Scrollbar): Optimize scroll handling logic to support external scroll events - Refactor `onScroll` logic to support external scroll events - Integrate with `useScrollPosition` hook for better scroll state management - Memorize the scoll state for better user experience - Fix type definition for `ref` attribute - Remove unnecessary `ref` type overrides - Improve component compatibility and maintainability * perf(useScrollPosition): Optimize scroll position updates using requestAnimationFrame - Wrap the `window.keyv.set` call in `requestAnimationFrame` to reduce unnecessary performance overhead and improve responsiveness during scrolling. * fix(Messages): Remove unused FC imports and add onComponentUpdate and onFirstUpdate properties --- .../src/components/Scrollbar/index.tsx | 30 ++++++++++++++----- src/renderer/src/hooks/useScrollPosition.ts | 4 ++- .../src/pages/home/Messages/Messages.tsx | 26 +++++++++------- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/renderer/src/components/Scrollbar/index.tsx b/src/renderer/src/components/Scrollbar/index.tsx index 670e37895a..857a8404e2 100644 --- a/src/renderer/src/components/Scrollbar/index.tsx +++ b/src/renderer/src/components/Scrollbar/index.tsx @@ -2,12 +2,13 @@ import { throttle } from 'lodash' import { FC, useCallback, useEffect, useRef, useState } from 'react' import styled from 'styled-components' -interface Props extends React.HTMLAttributes { +interface Props extends Omit, 'onScroll'> { right?: boolean - ref?: any + ref?: React.RefObject + onScroll?: () => void // Custom onScroll prop for useScrollPosition's handleScroll } -const Scrollbar: FC = ({ ref, ...props }: Props & { ref?: React.RefObject }) => { +const Scrollbar: FC = ({ ref: passedRef, children, onScroll: externalOnScroll, ...htmlProps }) => { const [isScrolling, setIsScrolling] = useState(false) const timeoutRef = useRef(null) @@ -21,18 +22,31 @@ const Scrollbar: FC = ({ ref, ...props }: Props & { ref?: React.RefObject timeoutRef.current = setTimeout(() => setIsScrolling(false), 1500) }, []) - const throttledHandleScroll = throttle(handleScroll, 200) + const throttledInternalScrollHandler = throttle(handleScroll, 200) + + // Combined scroll handler + const combinedOnScroll = useCallback(() => { + // Event is available if needed by internal handler + throttledInternalScrollHandler() // Call internal logic + if (externalOnScroll) { + externalOnScroll() // Call external logic (from useScrollPosition) + } + }, [throttledInternalScrollHandler, externalOnScroll]) useEffect(() => { return () => { timeoutRef.current && clearTimeout(timeoutRef.current) - throttledHandleScroll.cancel() + throttledInternalScrollHandler.cancel() } - }, [throttledHandleScroll]) + }, [throttledInternalScrollHandler]) return ( - - {props.children} + + {children} ) } diff --git a/src/renderer/src/hooks/useScrollPosition.ts b/src/renderer/src/hooks/useScrollPosition.ts index ddcd54028f..b3f4b5d512 100644 --- a/src/renderer/src/hooks/useScrollPosition.ts +++ b/src/renderer/src/hooks/useScrollPosition.ts @@ -7,7 +7,9 @@ export default function useScrollPosition(key: string) { const handleScroll = throttle(() => { const position = containerRef.current?.scrollTop ?? 0 - window.keyv.set(scrollKey, position) + window.requestAnimationFrame(() => { + window.keyv.set(scrollKey, position) + }) }, 100) useEffect(() => { diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index 678e848e22..58ee978fe2 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -3,6 +3,7 @@ import Scrollbar from '@renderer/components/Scrollbar' import { LOAD_MORE_COUNT } from '@renderer/config/constant' import { useAssistant } from '@renderer/hooks/useAssistant' import { useMessageOperations, useTopicMessages } from '@renderer/hooks/useMessageOperations' +import useScrollPosition from '@renderer/hooks/useScrollPosition' import { useSettings } from '@renderer/hooks/useSettings' import { useShortcut } from '@renderer/hooks/useShortcuts' import { autoRenameTopic, getTopic } from '@renderer/hooks/useTopic' @@ -26,7 +27,7 @@ import { updateCodeBlock } from '@renderer/utils/markdown' import { getMainTextContent } from '@renderer/utils/messageUtils/find' import { isTextLikeBlock } from '@renderer/utils/messageUtils/is' import { last } from 'lodash' -import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import InfiniteScroll from 'react-infinite-scroll-component' import styled from 'styled-components' @@ -45,12 +46,14 @@ interface MessagesProps { onFirstUpdate?(): void } -const Messages: FC = ({ assistant, topic, setActiveTopic, onComponentUpdate, onFirstUpdate }) => { +const Messages: React.FC = ({ assistant, topic, setActiveTopic, onComponentUpdate, onFirstUpdate }) => { + const { containerRef: scrollContainerRef, handleScroll: handleScrollPosition } = useScrollPosition( + `topic-${topic.id}` + ) const { t } = useTranslation() const { showPrompt, showTopics, topicPosition, showAssistants, messageNavigation } = useSettings() const { updateTopic, addTopic } = useAssistant(assistant.id) const dispatch = useAppDispatch() - const containerRef = useRef(null) const [displayMessages, setDisplayMessages] = useState([]) const [hasMore, setHasMore] = useState(false) const [isLoadingMore, setIsLoadingMore] = useState(false) @@ -77,16 +80,16 @@ const Messages: FC = ({ assistant, topic, setActiveTopic, onCompo }, [showAssistants, showTopics, topicPosition]) const scrollToBottom = useCallback(() => { - if (containerRef.current) { + if (scrollContainerRef.current) { requestAnimationFrame(() => { - if (containerRef.current) { - containerRef.current.scrollTo({ - top: containerRef.current.scrollHeight + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollTo({ + top: scrollContainerRef.current.scrollHeight }) } }) } - }, []) + }, [scrollContainerRef]) const clearTopic = useCallback( async (data: Topic) => { @@ -120,14 +123,14 @@ const Messages: FC = ({ assistant, topic, setActiveTopic, onCompo }) }), EventEmitter.on(EVENT_NAMES.COPY_TOPIC_IMAGE, async () => { - await captureScrollableDivAsBlob(containerRef, async (blob) => { + await captureScrollableDivAsBlob(scrollContainerRef, async (blob) => { if (blob) { await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]) } }) }), EventEmitter.on(EVENT_NAMES.EXPORT_TOPIC_IMAGE, async () => { - const imageData = await captureScrollableDivAsDataURL(containerRef) + const imageData = await captureScrollableDivAsDataURL(scrollContainerRef) if (imageData) { window.api.file.saveImage(removeSpecialCharactersForFileName(topic.name), imageData) } @@ -261,7 +264,8 @@ const Messages: FC = ({ assistant, topic, setActiveTopic, onCompo id="messages" style={{ maxWidth, paddingTop: showPrompt ? 10 : 0 }} key={assistant.id} - ref={containerRef} + ref={scrollContainerRef} + onScroll={handleScrollPosition} $right={topicPosition === 'left'}>