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
This commit is contained in:
jwcrystal 2025-05-19 16:33:06 +08:00 committed by GitHub
parent e3f5999362
commit 0c32ac1262
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 40 additions and 20 deletions

View File

@ -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<HTMLDivElement> {
interface Props extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onScroll'> {
right?: boolean
ref?: any
ref?: React.RefObject<HTMLDivElement | null>
onScroll?: () => void // Custom onScroll prop for useScrollPosition's handleScroll
}
const Scrollbar: FC<Props> = ({ ref, ...props }: Props & { ref?: React.RefObject<HTMLDivElement | null> }) => {
const Scrollbar: FC<Props> = ({ ref: passedRef, children, onScroll: externalOnScroll, ...htmlProps }) => {
const [isScrolling, setIsScrolling] = useState(false)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
@ -21,18 +22,31 @@ const Scrollbar: FC<Props> = ({ 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 (
<Container {...props} isScrolling={isScrolling} onScroll={throttledHandleScroll} ref={ref}>
{props.children}
<Container
{...htmlProps} // Pass other HTML attributes
isScrolling={isScrolling}
onScroll={combinedOnScroll} // Use the combined handler
ref={passedRef}>
{children}
</Container>
)
}

View File

@ -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(() => {

View File

@ -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<MessagesProps> = ({ assistant, topic, setActiveTopic, onComponentUpdate, onFirstUpdate }) => {
const Messages: React.FC<MessagesProps> = ({ 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<HTMLDivElement>(null)
const [displayMessages, setDisplayMessages] = useState<Message[]>([])
const [hasMore, setHasMore] = useState(false)
const [isLoadingMore, setIsLoadingMore] = useState(false)
@ -77,16 +80,16 @@ const Messages: FC<MessagesProps> = ({ 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<MessagesProps> = ({ 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<MessagesProps> = ({ assistant, topic, setActiveTopic, onCompo
id="messages"
style={{ maxWidth, paddingTop: showPrompt ? 10 : 0 }}
key={assistant.id}
ref={containerRef}
ref={scrollContainerRef}
onScroll={handleScrollPosition}
$right={topicPosition === 'left'}>
<NarrowLayout style={{ display: 'flex', flexDirection: 'column-reverse' }}>
<InfiniteScroll