fix(ai-daily): can not show entry paper click the daily entry link

- Introduced EntryModalPreview for displaying entry content in a modal.
- Added EntryMoreActions for handling additional actions related to entries.
- Implemented EntryToastPreview to show a toast notification for entries.
- Updated usePeekModal hook to integrate new components and streamline entry detail fetching.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-07-29 01:08:34 +08:00
parent c17f741bcd
commit 2b1fc18813
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
4 changed files with 239 additions and 227 deletions

View File

@ -0,0 +1,11 @@
import { Paper } from "~/components/ui/paper"
import { EntryContent } from "~/modules/entry-content/components/entry-content"
export const EntryModalPreview = ({ entryId }: { entryId: string }) => (
<Paper className="p-0 !pt-16 empty:hidden">
<EntryContent
className="h-auto [&_#entry-action-header-bar]:!bg-transparent"
entryId={entryId}
/>
</Paper>
)

View File

@ -0,0 +1,70 @@
import { RootPortal } from "@follow/components/ui/portal/index.js"
import type { FC } from "react"
import { useMemo } from "react"
import { MenuItemText } from "~/atoms/context-menu"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu/dropdown-menu"
import { useSortedEntryActions } from "~/hooks/biz/useEntryActions"
import { getRouteParams } from "~/hooks/biz/useRouteParams"
import { COMMAND_ID } from "~/modules/command/commands/id"
import { hasCommand } from "~/modules/command/hooks/use-command"
import { CommandDropdownMenuItem } from "~/modules/entry-content/actions/more-actions"
export const EntryMoreActions: FC<{ entryId: string }> = ({ entryId }) => {
const { view } = getRouteParams()
const { moreAction, mainAction } = useSortedEntryActions({ entryId, view })
const actionConfigs = useMemo(
() =>
[...moreAction, ...mainAction].filter(
(action) => action instanceof MenuItemText && hasCommand(action.id),
),
[moreAction, mainAction],
)
const availableActions = useMemo(
() =>
actionConfigs.filter(
(item) => item instanceof MenuItemText && item.id !== COMMAND_ID.settings.customizeToolbar,
),
[actionConfigs],
)
const extraAction = useMemo(
() =>
actionConfigs.filter(
(item) => item instanceof MenuItemText && item.id === COMMAND_ID.settings.customizeToolbar,
),
[actionConfigs],
)
if (availableActions.length === 0 && extraAction.length === 0) {
return null
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<i className="i-mgc-more-1-cute-re" />
</DropdownMenuTrigger>
<RootPortal>
<DropdownMenuContent alignOffset={20} sideOffset={30}>
{availableActions.map((config) =>
config instanceof MenuItemText ? (
<CommandDropdownMenuItem
key={config.id}
commandId={config.id}
onClick={config.click!}
active={config.active}
/>
) : null,
)}
</DropdownMenuContent>
</RootPortal>
</DropdownMenu>
)
}

View File

@ -0,0 +1,152 @@
import { Spring } from "@follow/components/constants/spring.js"
import { useIsEntryStarred } from "@follow/store/collection/hooks"
import { useEntry, usePrefetchEntryDetail } from "@follow/store/entry/hooks"
import { useFeedById } from "@follow/store/feed/hooks"
import { nextFrame, stopPropagation } from "@follow/utils/dom"
import { cn } from "@follow/utils/utils"
import type { Variant } from "motion/react"
import { m, useAnimationControls } from "motion/react"
import { useEffect } from "react"
import { RelativeTime } from "~/components/ui/datetime"
import { usePreviewMedia } from "~/components/ui/media/hooks"
import { Media } from "~/components/ui/media/Media"
import { StarIcon } from "~/modules/entry-column/star-icon"
import type { FeedIconEntry } from "~/modules/feed/feed-icon"
import { FeedIcon } from "~/modules/feed/feed-icon"
const variants: Record<string, Variant> = {
enter: {
x: 0,
opacity: 1,
},
initial: {
x: 700,
opacity: 0.9,
},
exit: {
x: 750,
opacity: 0,
},
}
export const EntryToastPreview = ({ entryId }: { entryId: string }) => {
usePrefetchEntryDetail(entryId)
const entry = useEntry(entryId, (state) => {
const { feedId } = state
const { author, authorAvatar, description, publishedAt } = state
const media = state.media || []
const firstPhotoUrl = media.find((a) => a.type === "photo")?.url
const iconEntry: FeedIconEntry = {
firstPhotoUrl,
authorAvatar,
}
return {
author,
description,
feedId,
iconEntry,
media,
publishedAt,
}
})
const isInCollection = useIsEntryStarred(entryId)
const feed = useFeedById(entry?.feedId)
const controller = useAnimationControls()
const isDisplay = !!entry && !!feed
useEffect(() => {
if (isDisplay) {
nextFrame(() => controller.start("enter"))
}
}, [controller, isDisplay])
const previewMedia = usePreviewMedia()
if (!isDisplay) return null
return (
<m.div
tabIndex={-1}
initial="initial"
animate={controller}
onPointerDown={stopPropagation}
onPointerDownCapture={stopPropagation}
variants={variants}
onWheel={stopPropagation}
transition={Spring.presets.snappy}
exit="exit"
layout="size"
className={cn(
"shadow-perfect bg-theme-background relative flex flex-col items-center rounded-xl border p-8",
"mr-4 mt-4 max-h-[500px] w-[60ch] max-w-full overflow-auto",
)}
>
<div className="flex w-full gap-3">
<FeedIcon
fallback
className="mask-squircle mask"
feed={feed}
entry={entry.iconEntry}
size={36}
/>
<div className="flex min-w-0 grow flex-col">
<div className="w-[calc(100%-10rem)] space-x-1">
<span className="font-semibold">{entry.author}</span>
<span className="text-zinc-500">·</span>
<span className="text-zinc-500">
<RelativeTime date={entry.publishedAt} />
</span>
</div>
<div
className={cn(
"relative mt-0.5 whitespace-pre-line text-base",
isInCollection && "pr-5",
)}
>
<div
className={cn(
"rounded-xl p-3 align-middle text-[15px]",
"rounded-tl-none bg-zinc-600/5 dark:bg-zinc-500/20",
"mt-1 -translate-x-3",
"break-words",
)}
>
{entry.description}
{!!entry.media?.length && (
<div className="mt-1 flex w-full gap-2 overflow-x-auto">
{entry.media.map((media, i, mediaList) => (
<Media
key={media.url}
src={media.url}
type={media.type}
previewImageUrl={media.preview_image_url}
className="size-28 shrink-0 cursor-zoom-in"
loading="lazy"
proxy={{
width: 224,
height: 224,
}}
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
previewMedia(mediaList, i)
}}
/>
))}
</div>
)}
</div>
{isInCollection && <StarIcon />}
</div>
{/* End right column */}
</div>
</div>
</m.div>
)
}

View File

@ -1,37 +1,13 @@
import { Spring } from "@follow/components/constants/spring.js"
import { RootPortal } from "@follow/components/ui/portal/index.js"
import { useIsEntryStarred } from "@follow/store/collection/hooks"
import { useEntry, usePrefetchEntryDetail } from "@follow/store/entry/hooks"
import { useFeedById } from "@follow/store/feed/hooks"
import { nextFrame, stopPropagation } from "@follow/utils/dom"
import { cn } from "@follow/utils/utils"
import type { Variant } from "motion/react"
import { m, useAnimationControls } from "motion/react"
import type { FC } from "react"
import { useCallback, useEffect, useMemo } from "react"
import { useCallback } from "react"
import { MenuItemText } from "~/atoms/context-menu"
import { RelativeTime } from "~/components/ui/datetime"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu/dropdown-menu"
import { usePreviewMedia } from "~/components/ui/media/hooks"
import { Media } from "~/components/ui/media/Media"
import { PeekModal } from "~/components/ui/modal/inspire/PeekModal"
import { PlainModal } from "~/components/ui/modal/stacked/custom-modal"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { Paper } from "~/components/ui/paper"
import { useSortedEntryActions } from "~/hooks/biz/useEntryActions"
import { EntryModalPreview } from "~/components/ui/peek-modal/EntryModalPreview"
import { EntryMoreActions } from "~/components/ui/peek-modal/EntryMoreActions"
import { EntryToastPreview } from "~/components/ui/peek-modal/EntryToastPreview"
import { getRouteParams } from "~/hooks/biz/useRouteParams"
import { COMMAND_ID } from "~/modules/command/commands/id"
import { hasCommand } from "~/modules/command/hooks/use-command"
import { StarIcon } from "~/modules/entry-column/star-icon"
import { CommandDropdownMenuItem } from "~/modules/entry-content/actions/more-actions"
import { EntryContent } from "~/modules/entry-content/components/entry-content"
import type { FeedIconEntry } from "~/modules/feed/feed-icon"
import { FeedIcon } from "~/modules/feed/feed-icon"
export const usePeekModal = () => {
const { present } = useModalStack()
@ -59,7 +35,9 @@ export const usePeekModal = () => {
"relative mx-auto mt-[10vh] scrollbar-none max-w-full overflow-auto px-2 lg:max-w-[65rem] lg:p-0",
CustomModalComponent: ({ children }) => {
usePrefetchEntryDetail(entryId)
const feedId = useEntry(entryId, (state) => state.feedId)
if (!feedId) return null
return (
@ -85,202 +63,3 @@ export const usePeekModal = () => {
[present],
)
}
const variants: Record<string, Variant> = {
enter: {
x: 0,
opacity: 1,
},
initial: {
x: 700,
opacity: 0.9,
},
exit: {
x: 750,
opacity: 0,
},
}
const EntryToastPreview = ({ entryId }: { entryId: string }) => {
usePrefetchEntryDetail(entryId)
const entry = useEntry(entryId, (state) => {
const { feedId } = state
const { author, authorAvatar, description, publishedAt } = state
const media = state.media || []
const firstPhotoUrl = media.find((a) => a.type === "photo")?.url
const iconEntry: FeedIconEntry = {
firstPhotoUrl,
authorAvatar,
}
return {
author,
description,
feedId,
iconEntry,
media,
publishedAt,
}
})
const isInCollection = useIsEntryStarred(entryId)
const feed = useFeedById(entry?.feedId)
const controller = useAnimationControls()
const isDisplay = !!entry && !!feed
useEffect(() => {
if (isDisplay) {
nextFrame(() => controller.start("enter"))
}
}, [controller, isDisplay])
const previewMedia = usePreviewMedia()
if (!isDisplay) return null
return (
<m.div
tabIndex={-1}
initial="initial"
animate={controller}
onPointerDown={stopPropagation}
onPointerDownCapture={stopPropagation}
variants={variants}
onWheel={stopPropagation}
transition={Spring.presets.snappy}
exit="exit"
layout="size"
className={cn(
"shadow-perfect bg-theme-background relative flex flex-col items-center rounded-xl border p-8",
"mr-4 mt-4 max-h-[500px] w-[60ch] max-w-full overflow-auto",
)}
>
<div className="flex w-full gap-3">
<FeedIcon
fallback
className="mask-squircle mask"
feed={feed}
entry={entry.iconEntry}
size={36}
/>
<div className="flex min-w-0 grow flex-col">
<div className="w-[calc(100%-10rem)] space-x-1">
<span className="font-semibold">{entry.author}</span>
<span className="text-zinc-500">·</span>
<span className="text-zinc-500">
<RelativeTime date={entry.publishedAt} />
</span>
</div>
<div
className={cn(
"relative mt-0.5 whitespace-pre-line text-base",
isInCollection && "pr-5",
)}
>
<div
className={cn(
"rounded-xl p-3 align-middle text-[15px]",
"rounded-tl-none bg-zinc-600/5 dark:bg-zinc-500/20",
"mt-1 -translate-x-3",
"break-words",
)}
>
{entry.description}
{!!entry.media?.length && (
<div className="mt-1 flex w-full gap-2 overflow-x-auto">
{entry.media.map((media, i, mediaList) => (
<Media
key={media.url}
src={media.url}
type={media.type}
previewImageUrl={media.preview_image_url}
className="size-28 shrink-0 cursor-zoom-in"
loading="lazy"
proxy={{
width: 224,
height: 224,
}}
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
previewMedia(mediaList, i)
}}
/>
))}
</div>
)}
</div>
{isInCollection && <StarIcon />}
</div>
{/* End right column */}
</div>
</div>
</m.div>
)
}
const EntryModalPreview = ({ entryId }: { entryId: string }) => (
<Paper className="p-0 !pt-16 empty:hidden">
<EntryContent
className="h-auto [&_#entry-action-header-bar]:!bg-transparent"
entryId={entryId}
/>
</Paper>
)
const EntryMoreActions: FC<{ entryId: string }> = ({ entryId }) => {
const { view } = getRouteParams()
const { moreAction, mainAction } = useSortedEntryActions({ entryId, view })
const actionConfigs = useMemo(
() =>
[...moreAction, ...mainAction].filter(
(action) => action instanceof MenuItemText && hasCommand(action.id),
),
[moreAction, mainAction],
)
const availableActions = useMemo(
() =>
actionConfigs.filter(
(item) => item instanceof MenuItemText && item.id !== COMMAND_ID.settings.customizeToolbar,
),
[actionConfigs],
)
const extraAction = useMemo(
() =>
actionConfigs.filter(
(item) => item instanceof MenuItemText && item.id === COMMAND_ID.settings.customizeToolbar,
),
[actionConfigs],
)
if (availableActions.length === 0 && extraAction.length === 0) {
return null
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<i className="i-mgc-more-1-cute-re" />
</DropdownMenuTrigger>
<RootPortal>
<DropdownMenuContent alignOffset={20} sideOffset={30}>
{availableActions.map((config) =>
config instanceof MenuItemText ? (
<CommandDropdownMenuItem
key={config.id}
commandId={config.id}
onClick={config.click!}
active={config.active}
/>
) : null,
)}
</DropdownMenuContent>
</RootPortal>
</DropdownMenu>
)
}