feat(desktop): implement smooth scrolling functionality

- Added a smooth scrolling feature to enhance user experience when navigating through content.
- Replaced the previous spring scroll implementation with a custom smooth scroll function that adapts duration based on scroll distance.
- Updated scroll event handlers to utilize the new smooth scrolling method for both upward and downward scrolling actions.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-05-26 17:04:38 +08:00
parent 26c6853d7d
commit 6c73ae53a1
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
1 changed files with 84 additions and 19 deletions

View File

@ -12,13 +12,12 @@ import { useTitle } from "@follow/hooks"
import type { FeedModel, InboxModel } from "@follow/models/types"
import { nextFrame, stopPropagation } from "@follow/utils/dom"
import { EventBus } from "@follow/utils/event-bus"
import { springScrollTo } from "@follow/utils/scroller"
import { cn, combineCleanupFunctions } from "@follow/utils/utils"
import { ErrorBoundary } from "@sentry/react"
import type { JSAnimation, Variants } from "motion/react"
import { AnimatePresence, m, useAnimationControls } from "motion/react"
import * as React from "react"
import { useEffect, useMemo, useRef, useState } from "react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useEntryIsInReadability } from "~/atoms/readability"
import { useIsZenMode, useUISettingKey } from "~/atoms/settings/ui"
@ -348,6 +347,64 @@ const RegisterCommands = ({
const { highlightBoundary } = useFocusActions()
// Smooth scroll implementation similar to Vimium
const smoothScrollTo = useCallback(
(targetScrollTop: number, element: HTMLDivElement) => {
// Stop any existing animation
if (scrollAnimationRef.current) {
scrollAnimationRef.current.stop()
}
const startScrollTop = element.scrollTop
const distance = targetScrollTop - startScrollTop
// If distance is very small, just set it directly
if (Math.abs(distance) < 1) {
element.scrollTop = targetScrollTop
scrollAnimationRef.current = null
return
}
// Adaptive duration based on distance - shorter for small distances, longer for large ones
const baseDuration = 150
const maxDuration = 300
const duration = Math.min(maxDuration, baseDuration + Math.abs(distance) * 0.5)
const startTime = performance.now()
// Easing function similar to Vimium's smooth scrolling - ease out cubic for natural feel
const easeOutCubic = (t: number): number => {
return 1 - Math.pow(1 - t, 3)
}
let animationId: number
const animateScroll = (currentTime: number) => {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
const easedProgress = easeOutCubic(progress)
const currentScrollTop = startScrollTop + distance * easedProgress
element.scrollTop = currentScrollTop
if (progress < 1) {
animationId = requestAnimationFrame(animateScroll)
scrollAnimationRef.current = {
stop: () => {
cancelAnimationFrame(animationId)
scrollAnimationRef.current = null
},
} as any
} else {
scrollAnimationRef.current = null
}
}
animationId = requestAnimationFrame(animateScroll)
},
[scrollAnimationRef],
)
useEffect(() => {
const checkScrollBottom = ($scroller: HTMLDivElement) => {
const currentScroll = $scroller.scrollTop
@ -357,7 +414,7 @@ const RegisterCommands = ({
EventBus.dispatch(COMMAND_ID.timeline.switchToNext)
setShowKeepScrollingPanel(false)
isAlreadyScrolledBottomRef.current = false
springScrollTo(0, $scroller)
smoothScrollTo(0, $scroller)
return
}
@ -384,29 +441,37 @@ const RegisterCommands = ({
},
cleanupScrollAnimation,
EventBus.subscribe(COMMAND_ID.entryRender.scrollUp, () => {
const currentScroll = scrollerRef.current?.scrollTop
const delta = window.innerHeight
const $scroller = scrollerRef.current
if (!$scroller) return
if (typeof currentScroll === "number" && delta) {
cleanupScrollAnimation()
scrollAnimationRef.current = springScrollTo(currentScroll - delta, scrollerRef.current!)
}
checkScrollBottom(scrollerRef.current!)
const currentScroll = $scroller.scrollTop
// Smart scroll distance: larger viewports get larger scroll distances
// But cap it at a reasonable maximum for very large screens
const viewportHeight = $scroller.clientHeight
const delta = Math.min(Math.max(120, viewportHeight * 0.25), 250)
cleanupScrollAnimation()
const targetScroll = Math.max(0, currentScroll - delta)
smoothScrollTo(targetScroll, $scroller)
checkScrollBottom($scroller)
}),
EventBus.subscribe(COMMAND_ID.entryRender.scrollDown, () => {
const $scroller = scrollerRef.current
if (!$scroller) {
return
}
if (!$scroller) return
const currentScroll = $scroller.scrollTop
const delta = window.innerHeight
// Smart scroll distance: larger viewports get larger scroll distances
// But cap it at a reasonable maximum for very large screens
const viewportHeight = $scroller.clientHeight
const delta = Math.min(Math.max(120, viewportHeight * 0.25), 250)
if (typeof currentScroll === "number" && delta) {
cleanupScrollAnimation()
scrollAnimationRef.current = springScrollTo(currentScroll + delta, $scroller)
}
cleanupScrollAnimation()
const targetScroll = Math.min(
$scroller.scrollHeight - $scroller.clientHeight,
currentScroll + delta,
)
smoothScrollTo(targetScroll, $scroller)
checkScrollBottom($scroller)
}),
EventBus.subscribe(
@ -424,7 +489,7 @@ const RegisterCommands = ({
},
),
)
}, [highlightBoundary, scrollAnimationRef, scrollerRef])
}, [highlightBoundary, scrollAnimationRef, scrollerRef, smoothScrollTo])
return (
<AnimatePresence>