Folo/apps/renderer/src/modules/entry-column/components/DateItem.tsx

201 lines
5.6 KiB
TypeScript

import { ActionButton } from "@follow/components/ui/button/index.js"
import { useScrollViewElement } from "@follow/components/ui/scroll-area/hooks.js"
import { FeedViewType } from "@follow/constants"
import { stopPropagation } from "@follow/utils/dom"
import { cn } from "@follow/utils/utils"
import { throttle } from "es-toolkit/compat"
import { m } from "framer-motion"
import type { FC, PropsWithChildren } from "react"
import {
cloneElement,
memo,
useCallback,
useId,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react"
import { Trans } from "react-i18next"
import { useDebounceCallback } from "usehooks-ts"
import { SafeFragment } from "~/components/common/Fragment"
import { RelativeDay } from "~/components/ui/datetime"
import { IconScaleTransition } from "~/components/ux/transition/icon"
import { useRouteParams } from "~/hooks/biz/useRouteParams"
import { isListSubscription } from "~/store/subscription"
import { useMarkAllByRoute } from "../hooks/useMarkAll"
const useParseDate = (date: string) =>
useMemo(() => {
const dateObj = new Date(date)
return {
dateObj,
startOfDay: new Date(dateObj.setHours(0, 0, 0, 0)).getTime(),
endOfDay: new Date(dateObj.setHours(23, 59, 59, 999)).getTime(),
}
}, [date])
const useSticky = () => {
const $scroller = useScrollViewElement()
const itemRef = useRef<HTMLDivElement>(null)
const [isSticky, setIsSticky] = useState(false)
useLayoutEffect(() => {
const $ = itemRef.current?.parentElement
if (!$) return
const handler = throttle((e: HTMLElementEventMap["scroll"]) => {
if ((e.target as HTMLElement).scrollTop < 10) {
setIsSticky(false)
return
}
const isSticky = $.offsetTop <= 0
setIsSticky(isSticky)
}, 16)
$scroller?.addEventListener("scroll", handler)
return () => {
$scroller?.removeEventListener("scroll", handler)
}
}, [$scroller])
return { isSticky, itemRef }
}
export const DateItem = memo(({ date, view }: { date: string; view: FeedViewType }) => {
const className = cn(
"lg:pt-2",
`relative -mx-2 flex items-center gap-1 bg-background px-4 text-base font-bold text-zinc-800 dark:text-neutral-400`,
)
if (view === FeedViewType.SocialMedia) {
return <SocialMediaDateItem date={date} className={className} />
}
return <UniversalDateItem date={date} className={className} />
})
const UniversalDateItem = ({ date, className }: { date: string; className?: string }) => {
const { startOfDay, endOfDay, dateObj } = useParseDate(date)
return (
<DateItemInner className={className} date={dateObj} startTime={startOfDay} endTime={endOfDay} />
)
}
const DateItemInner: FC<{
date: Date
startTime: number
endTime: number
className?: string
Wrapper?: FC<PropsWithChildren>
}> = ({ date, endTime, startTime, className, Wrapper }) => {
const rid = useId()
const RelativeElement = useMemo(
() => (
<m.span key="b" layout layoutId={rid}>
<RelativeDay date={date} />
</m.span>
),
[date, rid],
)
const handleMarkAllAsRead = useMarkAllByRoute({
startTime,
endTime,
})
const { isSticky, itemRef } = useSticky()
const [confirmMark, setConfirmMark] = useState(false)
const removeConfirm = useDebounceCallback(
() => {
setConfirmMark(false)
},
1000,
{
leading: false,
},
)
const timerRef = useRef<any>()
const W = Wrapper ?? SafeFragment
const { feedId } = useRouteParams()
const isList = isListSubscription(feedId)
const tooltipId = useRef(Math.random().toString())
return (
<div
className={cn(className, isSticky && "border-b")}
ref={itemRef}
onClick={stopPropagation}
onMouseEnter={removeConfirm.cancel}
onMouseLeave={removeConfirm}
>
<W>
<ActionButton
tooltip={
<span>
<Trans
i18nKey="mark_all_read_button.mark_as_read"
components={{
which: useMemo(
() => cloneElement(RelativeElement, { layoutId: tooltipId.current }),
[RelativeElement],
),
}}
/>
</span>
}
onClick={() => {
if (confirmMark) {
clearTimeout(timerRef.current)
handleMarkAllAsRead()
setConfirmMark(false)
} else {
setConfirmMark(true)
}
}}
className={cn("size-7 text-base", isList && "pointer-events-none opacity-0")}
>
<IconScaleTransition
icon1="i-mgc-check-filled text-green-600"
icon2="i-mgc-check-circle-cute-re"
status={!confirmMark ? "done" : "init"}
/>
</ActionButton>
{confirmMark ? (
<div className="animate-mask-in" key="a">
<Trans
i18nKey="mark_all_read_button.confirm_mark_all"
components={{
which: <>{RelativeElement}</>,
}}
/>
</div>
) : (
RelativeElement
)}
</W>
</div>
)
}
const SocialMediaDateItem = ({ date, className }: { date: string; className?: string }) => {
const { startOfDay, endOfDay, dateObj } = useParseDate(date)
return (
<DateItemInner
// @ts-expect-error
Wrapper={useCallback(
({ children }) => (
<div className="m-auto flex w-[645px] gap-3 pl-5 text-lg">{children}</div>
),
[],
)}
className={className}
date={dateObj}
startTime={startOfDay}
endTime={endOfDay}
/>
)
}