mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-05 12:29:44 +08:00
fix: up-down button does not hide properly in some cases (#10693)
* fix: simplify navigation button auto-hide logic Remove complex state management (isNearButtons, resetHideTimer) and rely directly on isInTriggerArea to control button visibility. This fixes the issue where buttons don't properly auto-hide by using mouse position detection instead of fragile state tracking. - Simplify showNavigation to just show and clear timers - Remove resetHideTimer function and use showNavigation directly - Simplify handleNavigationMouseLeave to always schedule hide after 500ms - Update all button handlers to call showNavigation() instead of resetHideTimer() - Rely on mouse enter/leave events to control visibility state * refactor(ChatNavigation): replace native setTimeout with custom useTimer hook Use custom useTimer hook for better timer management and cleanup --------- Co-authored-by: icarus <eurfelux@gmail.com>
This commit is contained in:
parent
f5a1d3f8d0
commit
c5ce0b763b
@ -7,6 +7,7 @@ import {
|
|||||||
VerticalAlignTopOutlined
|
VerticalAlignTopOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { useTimer } from '@renderer/hooks/useTimer'
|
||||||
import { RootState } from '@renderer/store'
|
import { RootState } from '@renderer/store'
|
||||||
// import { selectCurrentTopicId } from '@renderer/store/newMessage'
|
// import { selectCurrentTopicId } from '@renderer/store/newMessage'
|
||||||
import { Button, Drawer, Tooltip } from 'antd'
|
import { Button, Drawer, Tooltip } from 'antd'
|
||||||
@ -38,58 +39,60 @@ interface ChatNavigationProps {
|
|||||||
const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [isVisible, setIsVisible] = useState(false)
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
const [isNearButtons, setIsNearButtons] = useState(false)
|
const timerKey = 'hide'
|
||||||
const hideTimerRef = useRef<NodeJS.Timeout>(undefined)
|
const { setTimeoutTimer, clearTimeoutTimer } = useTimer()
|
||||||
const [showChatHistory, setShowChatHistory] = useState(false)
|
const [showChatHistory, setShowChatHistory] = useState(false)
|
||||||
const [manuallyClosedUntil, setManuallyClosedUntil] = useState<number | null>(null)
|
const [manuallyClosedUntil, setManuallyClosedUntil] = useState<number | null>(null)
|
||||||
const currentTopicId = useSelector((state: RootState) => state.messages.currentTopicId)
|
const currentTopicId = useSelector((state: RootState) => state.messages.currentTopicId)
|
||||||
const lastMoveTime = useRef(0)
|
const lastMoveTime = useRef(0)
|
||||||
|
const isHoveringNavigationRef = useRef(false)
|
||||||
|
const isPointerInTriggerAreaRef = useRef(false)
|
||||||
const { topicPosition, showTopics } = useSettings()
|
const { topicPosition, showTopics } = useSettings()
|
||||||
const showRightTopics = topicPosition === 'right' && showTopics
|
const showRightTopics = topicPosition === 'right' && showTopics
|
||||||
|
|
||||||
// Reset hide timer and make buttons visible
|
const clearHideTimer = useCallback(() => {
|
||||||
const resetHideTimer = useCallback(() => {
|
clearTimeoutTimer(timerKey)
|
||||||
setIsVisible(true)
|
}, [clearTimeoutTimer])
|
||||||
|
|
||||||
// Only set a hide timer if cursor is not near the buttons
|
const scheduleHide = useCallback(
|
||||||
if (!isNearButtons) {
|
(delay: number) => {
|
||||||
clearTimeout(hideTimerRef.current)
|
setTimeoutTimer(
|
||||||
hideTimerRef.current = setTimeout(() => {
|
timerKey,
|
||||||
setIsVisible(false)
|
() => {
|
||||||
}, 1500)
|
setIsVisible(false)
|
||||||
}
|
},
|
||||||
}, [isNearButtons])
|
delay
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[setTimeoutTimer]
|
||||||
|
)
|
||||||
|
|
||||||
// Handle mouse entering button area
|
const showNavigation = useCallback(() => {
|
||||||
const handleMouseEnter = useCallback(() => {
|
|
||||||
if (manuallyClosedUntil && Date.now() < manuallyClosedUntil) {
|
if (manuallyClosedUntil && Date.now() < manuallyClosedUntil) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsNearButtons(true)
|
|
||||||
setIsVisible(true)
|
setIsVisible(true)
|
||||||
|
clearHideTimer()
|
||||||
|
}, [clearHideTimer, manuallyClosedUntil])
|
||||||
|
|
||||||
// Clear any existing hide timer
|
// Handle mouse entering button area
|
||||||
clearTimeout(hideTimerRef.current)
|
const handleNavigationMouseEnter = useCallback(() => {
|
||||||
}, [manuallyClosedUntil])
|
if (manuallyClosedUntil && Date.now() < manuallyClosedUntil) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isHoveringNavigationRef.current = true
|
||||||
|
showNavigation()
|
||||||
|
}, [manuallyClosedUntil, showNavigation])
|
||||||
|
|
||||||
// Handle mouse leaving button area
|
// Handle mouse leaving button area
|
||||||
const handleMouseLeave = useCallback(() => {
|
const handleNavigationMouseLeave = useCallback(() => {
|
||||||
setIsNearButtons(false)
|
isHoveringNavigationRef.current = false
|
||||||
|
scheduleHide(500)
|
||||||
// Set a timer to hide the buttons
|
}, [scheduleHide])
|
||||||
hideTimerRef.current = setTimeout(() => {
|
|
||||||
setIsVisible(false)
|
|
||||||
}, 500)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(hideTimerRef.current)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleChatHistoryClick = () => {
|
const handleChatHistoryClick = () => {
|
||||||
setShowChatHistory(true)
|
setShowChatHistory(true)
|
||||||
resetHideTimer()
|
showNavigation()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDrawerClose = () => {
|
const handleDrawerClose = () => {
|
||||||
@ -173,22 +176,25 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
|||||||
// 修改 handleCloseChatNavigation 函数
|
// 修改 handleCloseChatNavigation 函数
|
||||||
const handleCloseChatNavigation = () => {
|
const handleCloseChatNavigation = () => {
|
||||||
setIsVisible(false)
|
setIsVisible(false)
|
||||||
|
isHoveringNavigationRef.current = false
|
||||||
|
isPointerInTriggerAreaRef.current = false
|
||||||
|
clearHideTimer()
|
||||||
// 设置手动关闭状态,1分钟内不响应鼠标靠近事件
|
// 设置手动关闭状态,1分钟内不响应鼠标靠近事件
|
||||||
setManuallyClosedUntil(Date.now() + 60000) // 60000毫秒 = 1分钟
|
setManuallyClosedUntil(Date.now() + 60000) // 60000毫秒 = 1分钟
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleScrollToTop = () => {
|
const handleScrollToTop = () => {
|
||||||
resetHideTimer()
|
showNavigation()
|
||||||
scrollToTop()
|
scrollToTop()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleScrollToBottom = () => {
|
const handleScrollToBottom = () => {
|
||||||
resetHideTimer()
|
showNavigation()
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNextMessage = () => {
|
const handleNextMessage = () => {
|
||||||
resetHideTimer()
|
showNavigation()
|
||||||
const userMessages = findUserMessages()
|
const userMessages = findUserMessages()
|
||||||
const assistantMessages = findAssistantMessages()
|
const assistantMessages = findAssistantMessages()
|
||||||
|
|
||||||
@ -215,7 +221,7 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePrevMessage = () => {
|
const handlePrevMessage = () => {
|
||||||
resetHideTimer()
|
showNavigation()
|
||||||
const userMessages = findUserMessages()
|
const userMessages = findUserMessages()
|
||||||
const assistantMessages = findAssistantMessages()
|
const assistantMessages = findAssistantMessages()
|
||||||
if (userMessages.length === 0 && assistantMessages.length === 0) {
|
if (userMessages.length === 0 && assistantMessages.length === 0) {
|
||||||
@ -249,9 +255,9 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
|||||||
|
|
||||||
// Handle scroll events on the container
|
// Handle scroll events on the container
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
// Only show buttons when scrolling if cursor is near the button area
|
// Only show buttons when scrolling if cursor is in trigger area or hovering navigation
|
||||||
if (isNearButtons) {
|
if (isPointerInTriggerAreaRef.current || isHoveringNavigationRef.current) {
|
||||||
resetHideTimer()
|
showNavigation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,50 +296,48 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
|||||||
e.clientX < rightPosition + triggerWidth + RIGHT_GAP &&
|
e.clientX < rightPosition + triggerWidth + RIGHT_GAP &&
|
||||||
e.clientY > topPosition &&
|
e.clientY > topPosition &&
|
||||||
e.clientY < topPosition + height
|
e.clientY < topPosition + height
|
||||||
|
// Update proximity state based on mouse position
|
||||||
// Update state based on mouse position
|
if (isInTriggerArea) {
|
||||||
if (isInTriggerArea && !isNearButtons) {
|
if (!isPointerInTriggerAreaRef.current) {
|
||||||
handleMouseEnter()
|
isPointerInTriggerAreaRef.current = true
|
||||||
} else if (!isInTriggerArea && isNearButtons) {
|
showNavigation()
|
||||||
// Only trigger mouse leave when not in the navigation area
|
}
|
||||||
// This ensures we don't leave when hovering over the actual buttons
|
} else if (isPointerInTriggerAreaRef.current) {
|
||||||
handleMouseLeave()
|
isPointerInTriggerAreaRef.current = false
|
||||||
|
if (!isHoveringNavigationRef.current) {
|
||||||
|
scheduleHide(500)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use passive: true for better scroll performance
|
// Use passive: true for better scroll performance
|
||||||
container.addEventListener('scroll', handleScroll, { passive: true })
|
container.addEventListener('scroll', handleScroll, { passive: true })
|
||||||
|
|
||||||
if (messagesContainer) {
|
// Track pointer position globally so we still detect exits after leaving the chat area
|
||||||
// Listen to the messages container (but with global coordinates)
|
window.addEventListener('mousemove', handleMouseMove)
|
||||||
messagesContainer.addEventListener('mousemove', handleMouseMove)
|
const handleMessagesMouseLeave = () => {
|
||||||
} else {
|
if (!isHoveringNavigationRef.current) {
|
||||||
window.addEventListener('mousemove', handleMouseMove)
|
isPointerInTriggerAreaRef.current = false
|
||||||
|
scheduleHide(500)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
messagesContainer?.addEventListener('mouseleave', handleMessagesMouseLeave)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
container.removeEventListener('scroll', handleScroll)
|
container.removeEventListener('scroll', handleScroll)
|
||||||
if (messagesContainer) {
|
window.removeEventListener('mousemove', handleMouseMove)
|
||||||
messagesContainer.removeEventListener('mousemove', handleMouseMove)
|
messagesContainer?.removeEventListener('mouseleave', handleMessagesMouseLeave)
|
||||||
} else {
|
clearHideTimer()
|
||||||
window.removeEventListener('mousemove', handleMouseMove)
|
|
||||||
}
|
|
||||||
clearTimeout(hideTimerRef.current)
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [containerId, showRightTopics, manuallyClosedUntil, scheduleHide, showNavigation, clearHideTimer])
|
||||||
containerId,
|
|
||||||
resetHideTimer,
|
|
||||||
isNearButtons,
|
|
||||||
handleMouseEnter,
|
|
||||||
handleMouseLeave,
|
|
||||||
showRightTopics,
|
|
||||||
manuallyClosedUntil
|
|
||||||
])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavigationContainer $isVisible={isVisible} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
<NavigationContainer
|
||||||
<ButtonGroup>
|
$isVisible={isVisible}
|
||||||
|
onMouseEnter={handleNavigationMouseEnter}
|
||||||
|
onMouseLeave={handleNavigationMouseLeave}>
|
||||||
|
<ButtonGroup $isVisible={isVisible}>
|
||||||
<Tooltip title={t('chat.navigation.close')} placement="left" mouseEnterDelay={0.5}>
|
<Tooltip title={t('chat.navigation.close')} placement="left" mouseEnterDelay={0.5}>
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
type="text"
|
type="text"
|
||||||
@ -418,7 +422,7 @@ const NavigationContainer = styled.div<NavigationContainerProps>`
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
right: ${RIGHT_GAP}px;
|
right: ${RIGHT_GAP}px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%) translateX(${(props) => (props.$isVisible ? 0 : '100%')});
|
transform: translateY(-50%) translateX(${(props) => (props.$isVisible ? '0' : '32px')});
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
opacity: ${(props) => (props.$isVisible ? 1 : 0)};
|
opacity: ${(props) => (props.$isVisible ? 1 : 0)};
|
||||||
transition:
|
transition:
|
||||||
@ -427,15 +431,22 @@ const NavigationContainer = styled.div<NavigationContainerProps>`
|
|||||||
pointer-events: ${(props) => (props.$isVisible ? 'auto' : 'none')};
|
pointer-events: ${(props) => (props.$isVisible ? 'auto' : 'none')};
|
||||||
`
|
`
|
||||||
|
|
||||||
const ButtonGroup = styled.div`
|
interface ButtonGroupProps {
|
||||||
|
$isVisible: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonGroup = styled.div<ButtonGroupProps>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: ${(props) => (props.$isVisible ? 'blur(8px)' : 'blur(0px)')};
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
transition:
|
||||||
|
backdrop-filter 0.25s ease-in-out,
|
||||||
|
background 0.25s ease-in-out;
|
||||||
`
|
`
|
||||||
|
|
||||||
const NavigationButton = styled(Button)`
|
const NavigationButton = styled(Button)`
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user