fix: feed column resize perf
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
11577dd740
commit
a09f55af08
|
|
@ -42,6 +42,52 @@ export const MarkAllReadWithOverlay = forwardRef<
|
|||
useOnClickOutside({ current: popoverRef }, () => {
|
||||
setShow(false)
|
||||
})
|
||||
const renderPopup = () => {
|
||||
const $parent = containerRef.current!
|
||||
const rect = $parent.getBoundingClientRect()
|
||||
const paddingLeft = $parent.offsetLeft
|
||||
return (
|
||||
<RootPortal to={$parent}>
|
||||
<m.div
|
||||
ref={setPopoverRef}
|
||||
initial={{ y: -70 }}
|
||||
animate={{ y: 0 }}
|
||||
exit={{ y: -70 }}
|
||||
transition={{ type: "spring", damping: 20, stiffness: 300 }}
|
||||
className="shadow-modal fixed z-50 bg-theme-modal-background-opaque shadow"
|
||||
style={{
|
||||
left: -paddingLeft,
|
||||
top: rect.top,
|
||||
width: rect.width,
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full translate-x-[-2px] items-center justify-between gap-3 !py-3 pl-6 pr-3 [&_button]:text-xs">
|
||||
<span className="center gap-[calc(0.5rem+2px)]">
|
||||
<i className="i-mgc-check-circle-cute-re" />
|
||||
<span className="text-sm font-bold">
|
||||
Mark
|
||||
<span> </span>
|
||||
{which}
|
||||
<span> </span>
|
||||
as read?
|
||||
</span>
|
||||
</span>
|
||||
<div className="space-x-4">
|
||||
<IconButton
|
||||
icon={<i className="i-mgc-check-filled" />}
|
||||
onClick={() => {
|
||||
handleMarkAllAsRead()
|
||||
setShow(false)
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</m.div>
|
||||
</RootPortal>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
<ActionButton
|
||||
|
|
@ -64,49 +110,7 @@ export const MarkAllReadWithOverlay = forwardRef<
|
|||
<i className="i-mgc-check-circle-cute-re" />
|
||||
</ActionButton>
|
||||
|
||||
<AnimatePresence>
|
||||
{show && (
|
||||
<RootPortal>
|
||||
<m.div
|
||||
ref={setPopoverRef}
|
||||
initial={{ y: -70 }}
|
||||
animate={{ y: 0 }}
|
||||
exit={{ y: -70 }}
|
||||
transition={{ type: "spring", damping: 20, stiffness: 300 }}
|
||||
className="shadow-modal fixed z-50 bg-theme-modal-background-opaque shadow"
|
||||
style={{
|
||||
left: containerRef.current?.getBoundingClientRect().left,
|
||||
top: containerRef.current?.getBoundingClientRect().top,
|
||||
width: containerRef.current?.getBoundingClientRect().width,
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full translate-x-[-2px] items-center justify-between gap-3 !py-3 pl-6 pr-3 [&_button]:text-xs">
|
||||
<span className="center gap-[calc(0.5rem+2px)]">
|
||||
<i className="i-mgc-check-circle-cute-re" />
|
||||
<span className="text-sm font-bold">
|
||||
Mark
|
||||
<span> </span>
|
||||
{which}
|
||||
<span> </span>
|
||||
as read?
|
||||
</span>
|
||||
</span>
|
||||
<div className="space-x-4">
|
||||
<IconButton
|
||||
icon={<i className="i-mgc-check-filled" />}
|
||||
onClick={() => {
|
||||
handleMarkAllAsRead()
|
||||
setShow(false)
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</m.div>
|
||||
</RootPortal>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<AnimatePresence>{show && renderPopup()}</AnimatePresence>
|
||||
</Fragment>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
/* eslint-disable @eslint-react/hooks-extra/no-direct-set-state-in-use-layout-effect */
|
||||
import { getReadonlyRoute } from "@renderer/atoms/route"
|
||||
import { useUISettingKey } from "@renderer/atoms/settings/ui"
|
||||
import {
|
||||
useUISettingKey,
|
||||
} from "@renderer/atoms/settings/ui"
|
||||
import { useSidebarActiveView } from "@renderer/atoms/sidebar"
|
||||
import { ActionButton } from "@renderer/components/ui/button"
|
||||
import { HotKeyScopeMap, views } from "@renderer/constants"
|
||||
|
|
@ -8,21 +11,23 @@ import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry"
|
|||
import { useReduceMotion } from "@renderer/hooks/biz/useReduceMotion"
|
||||
import { getRouteParams } from "@renderer/hooks/biz/useRouteParams"
|
||||
import { useAuthQuery } from "@renderer/hooks/common"
|
||||
import { nextFrame, stopPropagation } from "@renderer/lib/dom"
|
||||
import { stopPropagation } from "@renderer/lib/dom"
|
||||
import { Routes } from "@renderer/lib/enum"
|
||||
import { jotaiStore } from "@renderer/lib/jotai"
|
||||
import { clamp, cn } from "@renderer/lib/utils"
|
||||
import { Queries } from "@renderer/queries"
|
||||
import { useSubscriptionStore } from "@renderer/store/subscription"
|
||||
import { useFeedUnreadStore } from "@renderer/store/unread"
|
||||
import { useSubscribeElectronEvent } from "@shared/event"
|
||||
import { useWheel } from "@use-gesture/react"
|
||||
import type { MotionValue } from "framer-motion"
|
||||
import { m, useSpring } from "framer-motion"
|
||||
import { atom, useAtomValue } from "jotai"
|
||||
import { AnimatePresence, m } from "framer-motion"
|
||||
import { Lethargy } from "lethargy"
|
||||
import type { PropsWithChildren } from "react"
|
||||
import { useCallback, useLayoutEffect, useRef } from "react"
|
||||
import type { FC, PropsWithChildren } from "react"
|
||||
import {
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import { isHotkeyPressed, useHotkeys } from "react-hotkeys-hook"
|
||||
|
||||
import { WindowUnderBlur } from "../../components/ui/background"
|
||||
|
|
@ -64,15 +69,11 @@ const useUnreadByView = () => {
|
|||
return totalUnread
|
||||
}
|
||||
|
||||
const carouselWidthAtom = atom(256)
|
||||
export function FeedColumn({ children }: PropsWithChildren) {
|
||||
const carouselRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [active, setActive_] = useSidebarActiveView()
|
||||
const spring = useSpring(0, {
|
||||
stiffness: 700,
|
||||
damping: 40,
|
||||
})
|
||||
|
||||
const navigateBackHome = useBackHome(active)
|
||||
const setActive: typeof setActive_ = useCallback(
|
||||
(args) => {
|
||||
|
|
@ -83,7 +84,7 @@ export function FeedColumn({ children }: PropsWithChildren) {
|
|||
navigateBackHome(nextActive)
|
||||
}
|
||||
},
|
||||
[active, navigateBackHome, spring],
|
||||
[active, navigateBackHome],
|
||||
)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
|
@ -93,18 +94,6 @@ export function FeedColumn({ children }: PropsWithChildren) {
|
|||
}
|
||||
}, [setActive_])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const handler = () => {
|
||||
spring.jump(-active * jotaiStore.get(carouselWidthAtom))
|
||||
}
|
||||
const dispose = jotaiStore.sub(carouselWidthAtom, handler)
|
||||
|
||||
spring.set(-active * jotaiStore.get(carouselWidthAtom))
|
||||
return () => {
|
||||
dispose()
|
||||
}
|
||||
}, [active, spring])
|
||||
|
||||
useHotkeys(
|
||||
shortcuts.feeds.switchBetweenViews.key,
|
||||
(e) => {
|
||||
|
|
@ -147,22 +136,6 @@ export function FeedColumn({ children }: PropsWithChildren) {
|
|||
},
|
||||
)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const $carousel = carouselRef.current
|
||||
if (!$carousel) return
|
||||
|
||||
const handler = () => {
|
||||
const width = $carousel.clientWidth
|
||||
|
||||
jotaiStore.set(carouselWidthAtom, width)
|
||||
}
|
||||
handler()
|
||||
new ResizeObserver(handler).observe($carousel)
|
||||
return () => {
|
||||
new ResizeObserver(handler).disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const unreadByView = useUnreadByView()
|
||||
|
||||
const showSidebarUnreadCount = useUISettingKey("sidebarShowUnreadCount")
|
||||
|
|
@ -212,14 +185,11 @@ export function FeedColumn({ children }: PropsWithChildren) {
|
|||
))}
|
||||
</div>
|
||||
<div className="relative size-full overflow-hidden" ref={carouselRef}>
|
||||
<SwipeWrapper active={active} spring={spring}>
|
||||
<SwipeWrapper active={active}>
|
||||
{views.map((item, index) => (
|
||||
<section
|
||||
key={item.name}
|
||||
className="absolute h-full w-[var(--fo-feed-col-w)] shrink-0 snap-center"
|
||||
style={{
|
||||
left: `${index * 100}%`,
|
||||
}}
|
||||
className="h-full w-[var(--fo-feed-col-w)] shrink-0 snap-center"
|
||||
>
|
||||
{active === index && (
|
||||
<FeedList
|
||||
|
|
@ -237,52 +207,78 @@ export function FeedColumn({ children }: PropsWithChildren) {
|
|||
)
|
||||
}
|
||||
|
||||
const SwipeWrapper: Component<{
|
||||
const SwipeWrapper: FC<{
|
||||
active: number
|
||||
spring: MotionValue<number>
|
||||
}> = ({ children, active, spring }) => {
|
||||
children: React.JSX.Element[]
|
||||
}> = ({ children, active }) => {
|
||||
const reduceMotion = useReduceMotion()
|
||||
|
||||
const carouselWidth = useAtomValue(carouselWidthAtom)
|
||||
const feedColumnWidth = useUISettingKey("feedColWidth")
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// useLayoutEffect(() => {
|
||||
// const $container = containerRef.current;
|
||||
// if (!$container) return;
|
||||
|
||||
// const x = -active * feedColumnWidth;
|
||||
// // NOTE: To fix the misalignment of the browser's layout, use display to re-render it.
|
||||
// if (x !== $container.getBoundingClientRect().x) {
|
||||
// $container.style.display = "none";
|
||||
|
||||
// nextFrame(() => {
|
||||
// $container.style.display = "";
|
||||
// });
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
const prevActiveIndexRef = useRef(-1)
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
|
||||
const [direction, setDirection] = useState<"left" | "right">("right")
|
||||
const [currentAnimtedActive, setCurrentAnimatedActive] = useState(active)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const $container = containerRef.current
|
||||
if (!$container) return
|
||||
|
||||
const x = -active * carouselWidth
|
||||
// NOTE: To fix the misalignment of the browser's layout, use display to re-render it.
|
||||
if (x !== $container.getBoundingClientRect().x) {
|
||||
$container.style.display = "none"
|
||||
|
||||
nextFrame(() => {
|
||||
$container.style.display = ""
|
||||
})
|
||||
const prevActiveIndex = prevActiveIndexRef.current
|
||||
if (prevActiveIndex !== active) {
|
||||
if (prevActiveIndex < active) {
|
||||
setDirection("right")
|
||||
} else {
|
||||
setDirection("left")
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
setCurrentAnimatedActive(active)
|
||||
if (prevActiveIndexRef.current !== -1) {
|
||||
setIsReady(true)
|
||||
}
|
||||
prevActiveIndexRef.current = active
|
||||
}, [active])
|
||||
|
||||
if (reduceMotion) {
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
transform: `translateX(${-active * carouselWidth}px)`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
return <div ref={containerRef}>{children}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<m.div
|
||||
ref={containerRef}
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
x: spring,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</m.div>
|
||||
<AnimatePresence mode="popLayout">
|
||||
<m.div
|
||||
key={currentAnimtedActive}
|
||||
initial={
|
||||
isReady ?
|
||||
{
|
||||
x: direction === "right" ? -feedColumnWidth : feedColumnWidth,
|
||||
} :
|
||||
true
|
||||
}
|
||||
animate={{ x: 0 }}
|
||||
exit={{
|
||||
x: direction === "right" ? feedColumnWidth : -feedColumnWidth,
|
||||
}}
|
||||
transition={{
|
||||
x: { type: "spring", stiffness: 700, damping: 40 },
|
||||
}}
|
||||
ref={containerRef}
|
||||
>
|
||||
{children[currentAnimtedActive]}
|
||||
</m.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,13 +194,14 @@ const FeedResponsiveResizerContainer = ({
|
|||
<div
|
||||
className={cn(
|
||||
"shrink-0 overflow-hidden",
|
||||
"absolute inset-y-0 z-10 duration-200",
|
||||
"absolute inset-y-0 z-10",
|
||||
feedColumnTempShow &&
|
||||
!feedColumnShow &&
|
||||
"shadow-drawer-right z-[12] border-r",
|
||||
!feedColumnShow && !feedColumnTempShow ?
|
||||
"-translate-x-full delay-200" :
|
||||
"",
|
||||
!isDragging ? "duration-200" : "",
|
||||
)}
|
||||
style={{
|
||||
"width": `${position}px`,
|
||||
|
|
|
|||
Loading…
Reference in New Issue