From 60ea682fba5faf0c59bf4aee8aa37892e580b26a Mon Sep 17 00:00:00 2001 From: Innei Date: Fri, 15 Aug 2025 21:37:52 +0800 Subject: [PATCH] feat(entry-content): introduce navigation hints constants and refactor hint logic - Added constants for navigation hints, including icons and text, to improve maintainability and clarity. - Refactored the EntryNavigationHandler and useEntryNavigationHints hook to utilize these constants, enhancing the user experience with clearer guidance. - Updated styling for hint buttons to improve visual consistency across components. These changes collectively streamline the navigation hint implementation, providing a more intuitive interaction model for users. Signed-off-by: Innei --- .../entry-content/EntryContent.ai.tsx | 16 +-- .../entry-content/EntryContent.legacy.tsx | 16 ++- .../EntryScrollingAndNavigationHandler.tsx | 2 +- .../constants/navigation-hints.ts | 40 +++++++ .../hooks/useEntryNavigationHints.ts | 101 +++++++++++++----- 5 files changed, 131 insertions(+), 44 deletions(-) create mode 100644 apps/desktop/layer/renderer/src/modules/entry-content/constants/navigation-hints.ts diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.ai.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.ai.tsx index 1d6b0dec3..02c5a22ba 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.ai.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.ai.tsx @@ -36,6 +36,7 @@ import { useCommandHotkey } from "~/modules/command/hooks/use-register-hotkey" import { useWheelGestureClose } from "~/modules/entry-column/hooks/useWheelGestureClose" import { ApplyEntryActions } from "../../ApplyEntryActions" +import { NAVIGATION_HINTS_ICONS, NAVIGATION_HINTS_TEXT } from "../../constants/navigation-hints" import { useEntryContent } from "../../hooks" import { useEntryNavigationHints } from "../../hooks/useEntryNavigationHints" import { AIEntryHeader } from "../entry-header" @@ -278,7 +279,6 @@ const EntryNavigationHandler = ({ entryId }: { entryId: string }) => { } = useEntryNavigationHints({ enabled: when && !!entryId, entryId, - scrollThreshold: 100, }) const isZenMode = useIsZenMode() @@ -323,7 +323,7 @@ const EntryNavigationHandler = ({ entryId }: { entryId: string }) => { className={clsx( "group pointer-events-auto flex items-center gap-2", "rounded-full border px-3.5 py-2", - "border-border/40 bg-material-ultra-thin/70 shadow-[0_1px_2px_rgba(0,0,0,0.06),0_8px_24px_rgba(0,0,0,0.08)]", + "border-border/40 bg-material-ultra-thick shadow-[0_1px_2px_rgba(0,0,0,0.06),0_8px_24px_rgba(0,0,0,0.08)]", "hover:bg-material-thin/70 hover:border-border/60 active:scale-[0.98]", "backdrop-blur-background", )} @@ -339,24 +339,24 @@ const EntryNavigationHandler = ({ entryId }: { entryId: string }) => { {/* First entry hint */} {showFirstEntryHint && renderHintButton( - tw`i-mgc-up-cute-re`, - "Scroll up or click left-top back button to exit", + NAVIGATION_HINTS_ICONS.ARROW_UP, + NAVIGATION_HINTS_TEXT.SCROLL_UP_EXIT, "top", )} {/* Scroll threshold hint or wheel gesture hint */} {(showScrollThresholdHint || showScrollHint) && renderHintButton( - tw`i-mingcute-arrow-left-up-line`, - "Scroll up or click left-top back button to exit", + NAVIGATION_HINTS_ICONS.ARROW_LEFT_UP, + NAVIGATION_HINTS_TEXT.SCROLL_UP_EXIT, "top", )} {/* Bottom hint */} {showBottomHint && renderHintButton( - tw`i-mingcute-arrow-to-down-line`, - "Press ESC or click left-top back button to exit", + NAVIGATION_HINTS_ICONS.ARROW_TO_DOWN, + NAVIGATION_HINTS_TEXT.ESC_EXIT, "bottom", )} diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.legacy.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.legacy.tsx index dceda958d..85a444157 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.legacy.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.legacy.tsx @@ -37,6 +37,7 @@ import { WrappedElementProvider } from "~/providers/wrapped-element-provider" import { AISummary } from "../../AISummary" import { ApplyEntryActions } from "../../ApplyEntryActions" +import { NAVIGATION_HINTS_ICONS, NAVIGATION_HINTS_TEXT } from "../../constants/navigation-hints" import { useEntryContent, useEntryMediaInfo } from "../../hooks" import { useEntryNavigationHints } from "../../hooks/useEntryNavigationHints" import { EntryHeader } from "../entry-header" @@ -330,7 +331,6 @@ const EntryNavigationHandler = ({ entryId }: { entryId: string }) => { const { showFirstEntryHint, showScrollHint, showBottomHint } = useEntryNavigationHints({ enabled: when && !!entryId, entryId, - scrollThreshold: 100, }) // Render hint button with different states @@ -367,26 +367,22 @@ const EntryNavigationHandler = ({ entryId }: { entryId: string }) => { {/* First entry hint */} {showFirstEntryHint && renderHintButton( - "i-mgc-arrow-up-cute-re", - "Scroll up or click left-top back button to exit", + NAVIGATION_HINTS_ICONS.ARROW_UP, + NAVIGATION_HINTS_TEXT.SCROLL_UP_EXIT, "top", )} {/* Scroll threshold hint */} {showScrollHint && renderHintButton( - "i-mgc-arrow-up-cute-re", - "Scroll up or click left-top back button to exit", + NAVIGATION_HINTS_ICONS.ARROW_UP, + NAVIGATION_HINTS_TEXT.SCROLL_UP_EXIT, "top", )} {/* Bottom hint */} {showBottomHint && - renderHintButton( - "i-mgc-close-cute-re", - "Press ESC or click left-top back button to exit", - "bottom", - )} + renderHintButton(NAVIGATION_HINTS_ICONS.CLOSE, NAVIGATION_HINTS_TEXT.ESC_EXIT, "bottom")} ) } diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryScrollingAndNavigationHandler.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryScrollingAndNavigationHandler.tsx index 5eb25b644..e8572150a 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryScrollingAndNavigationHandler.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryScrollingAndNavigationHandler.tsx @@ -154,7 +154,7 @@ export const EntryScrollingAndNavigationHandler = ({ "pointer-events-none absolute !right-1/2 z-40 !translate-x-1/2", "bottom-12", "backdrop-blur-background rounded-full border px-3.5 py-2", - "border-border/40 bg-material-ultra-thin/70 shadow-[0_1px_2px_rgba(0,0,0,0.06),0_8px_24px_rgba(0,0,0,0.08)]", + "border-border/40 bg-material-ultra-thick shadow-[0_1px_2px_rgba(0,0,0,0.06),0_8px_24px_rgba(0,0,0,0.08)]", "hover:bg-material-thin/70 hover:border-border/60 active:scale-[0.98]", )} > diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/constants/navigation-hints.ts b/apps/desktop/layer/renderer/src/modules/entry-content/constants/navigation-hints.ts new file mode 100644 index 000000000..3264f4c5b --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/entry-content/constants/navigation-hints.ts @@ -0,0 +1,40 @@ +/** + * Constants for entry navigation hints behavior + */ +export const NAVIGATION_HINTS_CONSTANTS = { + /** Default scroll threshold to trigger scroll hint (px) */ + DEFAULT_SCROLL_THRESHOLD: 100, + + /** Delay before showing first entry hint (ms) */ + FIRST_HINT_DELAY: 500, + + /** Duration to show hints before auto-hiding (ms) */ + HINT_DISPLAY_DURATION: 3000, + + /** Distance from bottom to trigger bottom hint (px) */ + BOTTOM_THRESHOLD: 50, + + /** Distance from bottom to hide bottom hint when scrolling up (px) */ + BOTTOM_HIDE_THRESHOLD: 100, + + /** Throttle interval for scroll handler (ms) */ + SCROLL_THROTTLE_INTERVAL: 100, +} as const + +/** + * Text constants for navigation hints + */ +export const NAVIGATION_HINTS_TEXT = { + SCROLL_UP_EXIT: "Scroll up or click left-top back button to exit", + ESC_EXIT: "Press ESC or click left-top back button to exit", +} as const + +/** + * Icon constants for navigation hints + */ +export const NAVIGATION_HINTS_ICONS = { + ARROW_UP: "i-mgc-up-cute-re", + ARROW_LEFT_UP: "i-mingcute-arrow-left-up-line", + ARROW_TO_DOWN: "i-mingcute-arrow-to-down-line", + CLOSE: "i-mgc-close-cute-re", +} as const diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/hooks/useEntryNavigationHints.ts b/apps/desktop/layer/renderer/src/modules/entry-content/hooks/useEntryNavigationHints.ts index 0b540c323..7fd3e7483 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/hooks/useEntryNavigationHints.ts +++ b/apps/desktop/layer/renderer/src/modules/entry-content/hooks/useEntryNavigationHints.ts @@ -1,14 +1,16 @@ import { useScrollViewElement } from "@follow/components/ui/scroll-area/hooks.js" import { throttle } from "es-toolkit" -import { useEffect, useRef, useState } from "react" +import { startTransition, useEffect, useRef, useState } from "react" import { useEventCallback } from "usehooks-ts" +import { NAVIGATION_HINTS_CONSTANTS } from "../constants/navigation-hints" + interface UseEntryNavigationHintsOptions { /** Whether hints are enabled */ enabled: boolean /** Entry ID to track changes */ entryId?: string - /** Scroll threshold to show hint (default: 100px) */ + /** Scroll threshold to show hint */ scrollThreshold?: number } @@ -28,7 +30,7 @@ interface UseEntryNavigationHintsReturn { export const useEntryNavigationHints = ({ enabled, entryId, - scrollThreshold = 100, + scrollThreshold = NAVIGATION_HINTS_CONSTANTS.DEFAULT_SCROLL_THRESHOLD, }: UseEntryNavigationHintsOptions): UseEntryNavigationHintsReturn => { const $scrollElement = useScrollViewElement() @@ -45,6 +47,8 @@ export const useEntryNavigationHints = ({ const firstHintTimerRef = useRef>(void 0) const scrollHintTimerRef = useRef>(void 0) const bottomHintTimerRef = useRef>(void 0) + const lastScrollTopRef = useRef(0) + const scrollDirectionRef = useRef<"up" | "down" | "none">("none") // Reset hints when entry changes useEffect(() => { @@ -53,30 +57,38 @@ export const useEntryNavigationHints = ({ hasShownFirstHintRef.current = false hasShownScrollHintRef.current = false hasShownBottomHintRef.current = false + lastScrollTopRef.current = 0 + scrollDirectionRef.current = "none" // Clear existing timers if (firstHintTimerRef.current) clearTimeout(firstHintTimerRef.current) if (scrollHintTimerRef.current) clearTimeout(scrollHintTimerRef.current) if (bottomHintTimerRef.current) clearTimeout(bottomHintTimerRef.current) - // Reset all hint states - setShowFirstEntryHint(false) - setShowScrollHint(false) - setShowBottomHint(false) + // Reset all hint states with low priority + startTransition(() => { + setShowFirstEntryHint(false) + setShowScrollHint(false) + setShowBottomHint(false) + }) if (enabled) { // Show first entry hint after a brief delay firstHintTimerRef.current = setTimeout(() => { if (!hasShownFirstHintRef.current) { - setShowFirstEntryHint(true) + startTransition(() => { + setShowFirstEntryHint(true) + }) hasShownFirstHintRef.current = true - // Hide after 3 seconds + // Hide after configured duration firstHintTimerRef.current = setTimeout(() => { - setShowFirstEntryHint(false) - }, 3000) + startTransition(() => { + setShowFirstEntryHint(false) + }) + }, NAVIGATION_HINTS_CONSTANTS.HINT_DISPLAY_DURATION) } - }, 500) // Small delay to allow content to load + }, NAVIGATION_HINTS_CONSTANTS.FIRST_HINT_DELAY) // Small delay to allow content to load } } }, [entryId, enabled]) @@ -91,35 +103,74 @@ export const useEntryNavigationHints = ({ const { clientHeight } = $scrollElement const scrollBottom = scrollHeight - clientHeight - scrollTop - // Check if scrolled past threshold - if (scrollTop > scrollThreshold && !hasShownScrollHintRef.current) { + // Detect scroll direction + const lastScrollTop = lastScrollTopRef.current + if (scrollTop > lastScrollTop) { + scrollDirectionRef.current = "down" + } else if (scrollTop < lastScrollTop) { + scrollDirectionRef.current = "up" + } + lastScrollTopRef.current = scrollTop + + // Check if scrolled past threshold and scrolling up + if ( + scrollTop > scrollThreshold && + !hasShownScrollHintRef.current && + scrollDirectionRef.current === "up" + ) { hasShownScrollHintRef.current = true - setShowScrollHint(true) + startTransition(() => { + setShowScrollHint(true) + }) // Clear previous timer if (scrollHintTimerRef.current) clearTimeout(scrollHintTimerRef.current) - // Hide after 3 seconds + // Hide after configured duration scrollHintTimerRef.current = setTimeout(() => { - setShowScrollHint(false) - }, 3000) + startTransition(() => { + setShowScrollHint(false) + }) + }, NAVIGATION_HINTS_CONSTANTS.HINT_DISPLAY_DURATION) } - // Check if at bottom (within 50px threshold) - if (scrollBottom <= 50 && !hasShownBottomHintRef.current) { + // Check if at bottom (within configured threshold) + if ( + scrollBottom <= NAVIGATION_HINTS_CONSTANTS.BOTTOM_THRESHOLD && + !hasShownBottomHintRef.current + ) { hasShownBottomHintRef.current = true - setShowBottomHint(true) + startTransition(() => { + setShowBottomHint(true) + }) // Clear previous timer if (bottomHintTimerRef.current) clearTimeout(bottomHintTimerRef.current) - // Hide after 3 seconds + // Hide after configured duration bottomHintTimerRef.current = setTimeout(() => { - setShowBottomHint(false) + startTransition(() => { + setShowBottomHint(false) + }) hasShownBottomHintRef.current = false - }, 3000) + }, NAVIGATION_HINTS_CONSTANTS.HINT_DISPLAY_DURATION) } - }, 100), + + // Hide bottom hint if user scrolls up from bottom + if ( + scrollBottom > NAVIGATION_HINTS_CONSTANTS.BOTTOM_HIDE_THRESHOLD && + hasShownBottomHintRef.current && + scrollDirectionRef.current === "up" + ) { + // Clear timer if exists + if (bottomHintTimerRef.current) clearTimeout(bottomHintTimerRef.current) + + startTransition(() => { + setShowBottomHint(false) + }) + hasShownBottomHintRef.current = false + } + }, NAVIGATION_HINTS_CONSTANTS.SCROLL_THROTTLE_INTERVAL), ) // Attach scroll listener