mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 05:39:05 +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 { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
interface Props extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onScroll'> {
|
||||||
right?: boolean
|
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 [isScrolling, setIsScrolling] = useState(false)
|
||||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
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)
|
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(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
timeoutRef.current && clearTimeout(timeoutRef.current)
|
timeoutRef.current && clearTimeout(timeoutRef.current)
|
||||||
throttledHandleScroll.cancel()
|
throttledInternalScrollHandler.cancel()
|
||||||
}
|
}
|
||||||
}, [throttledHandleScroll])
|
}, [throttledInternalScrollHandler])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container {...props} isScrolling={isScrolling} onScroll={throttledHandleScroll} ref={ref}>
|
<Container
|
||||||
{props.children}
|
{...htmlProps} // Pass other HTML attributes
|
||||||
|
isScrolling={isScrolling}
|
||||||
|
onScroll={combinedOnScroll} // Use the combined handler
|
||||||
|
ref={passedRef}>
|
||||||
|
{children}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,9 @@ export default function useScrollPosition(key: string) {
|
|||||||
|
|
||||||
const handleScroll = throttle(() => {
|
const handleScroll = throttle(() => {
|
||||||
const position = containerRef.current?.scrollTop ?? 0
|
const position = containerRef.current?.scrollTop ?? 0
|
||||||
window.keyv.set(scrollKey, position)
|
window.requestAnimationFrame(() => {
|
||||||
|
window.keyv.set(scrollKey, position)
|
||||||
|
})
|
||||||
}, 100)
|
}, 100)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import Scrollbar from '@renderer/components/Scrollbar'
|
|||||||
import { LOAD_MORE_COUNT } from '@renderer/config/constant'
|
import { LOAD_MORE_COUNT } from '@renderer/config/constant'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useMessageOperations, useTopicMessages } from '@renderer/hooks/useMessageOperations'
|
import { useMessageOperations, useTopicMessages } from '@renderer/hooks/useMessageOperations'
|
||||||
|
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||||
import { autoRenameTopic, getTopic } from '@renderer/hooks/useTopic'
|
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 { getMainTextContent } from '@renderer/utils/messageUtils/find'
|
||||||
import { isTextLikeBlock } from '@renderer/utils/messageUtils/is'
|
import { isTextLikeBlock } from '@renderer/utils/messageUtils/is'
|
||||||
import { last } from 'lodash'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -45,12 +46,14 @@ interface MessagesProps {
|
|||||||
onFirstUpdate?(): void
|
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 { t } = useTranslation()
|
||||||
const { showPrompt, showTopics, topicPosition, showAssistants, messageNavigation } = useSettings()
|
const { showPrompt, showTopics, topicPosition, showAssistants, messageNavigation } = useSettings()
|
||||||
const { updateTopic, addTopic } = useAssistant(assistant.id)
|
const { updateTopic, addTopic } = useAssistant(assistant.id)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
|
||||||
const [displayMessages, setDisplayMessages] = useState<Message[]>([])
|
const [displayMessages, setDisplayMessages] = useState<Message[]>([])
|
||||||
const [hasMore, setHasMore] = useState(false)
|
const [hasMore, setHasMore] = useState(false)
|
||||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||||
@ -77,16 +80,16 @@ const Messages: FC<MessagesProps> = ({ assistant, topic, setActiveTopic, onCompo
|
|||||||
}, [showAssistants, showTopics, topicPosition])
|
}, [showAssistants, showTopics, topicPosition])
|
||||||
|
|
||||||
const scrollToBottom = useCallback(() => {
|
const scrollToBottom = useCallback(() => {
|
||||||
if (containerRef.current) {
|
if (scrollContainerRef.current) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (containerRef.current) {
|
if (scrollContainerRef.current) {
|
||||||
containerRef.current.scrollTo({
|
scrollContainerRef.current.scrollTo({
|
||||||
top: containerRef.current.scrollHeight
|
top: scrollContainerRef.current.scrollHeight
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [])
|
}, [scrollContainerRef])
|
||||||
|
|
||||||
const clearTopic = useCallback(
|
const clearTopic = useCallback(
|
||||||
async (data: Topic) => {
|
async (data: Topic) => {
|
||||||
@ -120,14 +123,14 @@ const Messages: FC<MessagesProps> = ({ assistant, topic, setActiveTopic, onCompo
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
EventEmitter.on(EVENT_NAMES.COPY_TOPIC_IMAGE, async () => {
|
EventEmitter.on(EVENT_NAMES.COPY_TOPIC_IMAGE, async () => {
|
||||||
await captureScrollableDivAsBlob(containerRef, async (blob) => {
|
await captureScrollableDivAsBlob(scrollContainerRef, async (blob) => {
|
||||||
if (blob) {
|
if (blob) {
|
||||||
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })])
|
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
EventEmitter.on(EVENT_NAMES.EXPORT_TOPIC_IMAGE, async () => {
|
EventEmitter.on(EVENT_NAMES.EXPORT_TOPIC_IMAGE, async () => {
|
||||||
const imageData = await captureScrollableDivAsDataURL(containerRef)
|
const imageData = await captureScrollableDivAsDataURL(scrollContainerRef)
|
||||||
if (imageData) {
|
if (imageData) {
|
||||||
window.api.file.saveImage(removeSpecialCharactersForFileName(topic.name), imageData)
|
window.api.file.saveImage(removeSpecialCharactersForFileName(topic.name), imageData)
|
||||||
}
|
}
|
||||||
@ -261,7 +264,8 @@ const Messages: FC<MessagesProps> = ({ assistant, topic, setActiveTopic, onCompo
|
|||||||
id="messages"
|
id="messages"
|
||||||
style={{ maxWidth, paddingTop: showPrompt ? 10 : 0 }}
|
style={{ maxWidth, paddingTop: showPrompt ? 10 : 0 }}
|
||||||
key={assistant.id}
|
key={assistant.id}
|
||||||
ref={containerRef}
|
ref={scrollContainerRef}
|
||||||
|
onScroll={handleScrollPosition}
|
||||||
$right={topicPosition === 'left'}>
|
$right={topicPosition === 'left'}>
|
||||||
<NarrowLayout style={{ display: 'flex', flexDirection: 'column-reverse' }}>
|
<NarrowLayout style={{ display: 'flex', flexDirection: 'column-reverse' }}>
|
||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user