mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 21:01:32 +08:00
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:
parent
e3f5999362
commit
0c32ac1262
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user