From 878ad81a7ddd12b98992cdfc7e729cc2ca71f118 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 21 Oct 2025 15:10:20 +0800 Subject: [PATCH] fix: align entry details column layout with ai panel states --- .../hooks/biz/useShowEntryDetailsColumn.ts | 17 ++ .../AIEnhancedTimelineLayout.tsx | 190 +++++++++++++----- .../entry-column/components/DateItem.tsx | 5 +- .../components/layouts/ArticleLayout.tsx | 5 +- 4 files changed, 166 insertions(+), 51 deletions(-) create mode 100644 apps/desktop/layer/renderer/src/hooks/biz/useShowEntryDetailsColumn.ts diff --git a/apps/desktop/layer/renderer/src/hooks/biz/useShowEntryDetailsColumn.ts b/apps/desktop/layer/renderer/src/hooks/biz/useShowEntryDetailsColumn.ts new file mode 100644 index 000000000..498ea5618 --- /dev/null +++ b/apps/desktop/layer/renderer/src/hooks/biz/useShowEntryDetailsColumn.ts @@ -0,0 +1,17 @@ +import { FeedViewType } from "@follow/constants" + +import { AIChatPanelStyle, useAIChatPanelStyle, useAIPanelVisibility } from "~/atoms/settings/ai" +import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams" + +export const useShowEntryDetailsColumn = () => { + const { view } = useRouteParamsSelector((s) => ({ + view: s.view, + })) + const aiPanelStyle = useAIChatPanelStyle() + const isAIPanelVisible = useAIPanelVisibility() + + return ( + view === FeedViewType.Articles && + (aiPanelStyle === AIChatPanelStyle.Floating || !isAIPanelVisible) + ) +} diff --git a/apps/desktop/layer/renderer/src/modules/app-layout/ai-enhanced-timeline/AIEnhancedTimelineLayout.tsx b/apps/desktop/layer/renderer/src/modules/app-layout/ai-enhanced-timeline/AIEnhancedTimelineLayout.tsx index 125085b71..33052f048 100644 --- a/apps/desktop/layer/renderer/src/modules/app-layout/ai-enhanced-timeline/AIEnhancedTimelineLayout.tsx +++ b/apps/desktop/layer/renderer/src/modules/app-layout/ai-enhanced-timeline/AIEnhancedTimelineLayout.tsx @@ -3,20 +3,23 @@ import { PanelSplitter } from "@follow/components/ui/divider/index.js" import { FeedViewType } from "@follow/constants" import { defaultUISettings } from "@follow/shared/settings/defaults" import { cn } from "@follow/utils" +import { isSafari } from "@follow/utils/utils" import { AnimatePresence } from "motion/react" import { memo, startTransition, useCallback, useEffect, useMemo, useRef } from "react" import { useResizable } from "react-resizable-layout" import { AIChatPanelStyle, useAIChatPanelStyle, useAIPanelVisibility } from "~/atoms/settings/ai" import { useGeneralSettingKey } from "~/atoms/settings/general" -import { getUISettings, setUISetting } from "~/atoms/settings/ui" +import { getUISettings, setUISetting, useUISettingKey } from "~/atoms/settings/ui" import { setSubscriptionColumnApronNode, useSubscriptionEntryPlaneVisible } from "~/atoms/sidebar" import { m } from "~/components/common/Motion" import { ROUTE_ENTRY_PENDING } from "~/constants" import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams" +import { useShowEntryDetailsColumn } from "~/hooks/biz/useShowEntryDetailsColumn" import { AIChatRoot } from "~/modules/ai-chat/components/layouts/AIChatRoot" import { AIChatLayout } from "~/modules/app-layout/ai/AIChatLayout" import { AIIndicator } from "~/modules/app-layout/ai/AISplineButton" +import { EntryContentPlaceholder } from "~/modules/app-layout/entry-content/EntryContentPlaceholder" import { EntryColumn } from "~/modules/entry-column" import { EntryPlaneToolbar } from "~/modules/entry-column/components/EntryPlaneToolbar" import { EntrySubscriptionList } from "~/modules/entry-column/EntrySubscriptionList" @@ -64,6 +67,40 @@ const AIEnhancedTimelineLayoutImpl = () => { entryId: s.entryId, })) const realEntryId = entryId === ROUTE_ENTRY_PENDING ? "" : entryId + const showEntryDetailsColumn = useShowEntryDetailsColumn() + + const layoutContainerRef = useRef(null) + const feedColumnWidth = useUISettingKey("feedColWidth") + + // Timeline column resizable configuration (used when entry details column is visible) + const entryColumnInitialWidth = useMemo(() => getUISettings().entryColWidth, []) + const timelineMaxWidth = useMemo(() => { + if (typeof window === "undefined") return 600 + return Math.max((window.innerWidth - feedColumnWidth) / 2, 600) + }, [feedColumnWidth]) + const timelineStartDragPosition = useRef(0) + const { + position: timelineColumnWidth, + separatorProps: timelineSeparatorProps, + isDragging: isTimelineDragging, + separatorCursor: timelineSeparatorCursor, + setPosition: setTimelineColumnWidth, + } = useResizable({ + axis: "x", + min: isSafari() ? 356 : 300, + max: timelineMaxWidth, + initial: entryColumnInitialWidth, + containerRef: layoutContainerRef as React.RefObject, + onResizeStart({ position }) { + timelineStartDragPosition.current = position + }, + onResizeEnd({ position }) { + if (position === timelineStartDragPosition.current) return + setUISetting("entryColWidth", position) + // TODO: Remove this after useMeasure can get bounds in time + window.dispatchEvent(new Event("resize")) + }, + }) // AI chat resizable panel configuration const isAllView = view === FeedViewType.All @@ -88,19 +125,25 @@ const AIEnhancedTimelineLayoutImpl = () => { // Calculate initial width depending on view const initialAiWidth = useMemo(() => resolvePreferredWidth(), [resolvePreferredWidth]) - const startDragPosition = useRef(0) + const aiPanelStartDragPosition = useRef(0) - const { position, separatorProps, isDragging, separatorCursor, setPosition } = useResizable({ + const { + position: aiPanelWidth, + separatorProps: aiSeparatorProps, + isDragging: isAiPanelDragging, + separatorCursor: aiSeparatorCursor, + setPosition: setAiPanelWidth, + } = useResizable({ axis: "x", min: minWidth, max: maxWidth, initial: initialAiWidth, reverse: true, onResizeStart({ position }) { - startDragPosition.current = position + aiPanelStartDragPosition.current = position }, onResizeEnd({ position }) { - if (position === startDragPosition.current) return + if (position === aiPanelStartDragPosition.current) return // Persist per-view width setUISetting(isAllView ? "aiColWidthAll" : "aiColWidth", position) // TODO: Remove this after useMeasure can get bounds in time @@ -111,54 +154,102 @@ const AIEnhancedTimelineLayoutImpl = () => { // When view changes, switch to the saved width for that view useEffect(() => { const width = resolvePreferredWidth() - setPosition(width) + setAiPanelWidth(width) // Trigger layout update window.dispatchEvent(new Event("resize")) }, [resolvePreferredWidth]) const showCompactTimelineColumn = useGeneralSettingKey("showCompactTimelineInSub") return ( -
-
+
+
- {/* Entry list - always rendered to prevent animation */} - - - {realEntryId && ( - - - +
- {/* Entry content overlay with exit animation */} - - {realEntryId && ( -
- - - + style={showEntryDetailsColumn ? { width: timelineColumnWidth } : undefined} + > +
+ {/* Entry list - always rendered to prevent animation */} + + {!showEntryDetailsColumn && ( + <> + + {realEntryId && ( + + + + )} + + {/* Entry content overlay with exit animation */} + + {realEntryId && ( +
+ + + +
+ )} +
+ + )} +
+
+ + {showEntryDetailsColumn && ( + <> +
+ { + setUISetting("entryColWidth", defaultUISettings.entryColWidth) + setTimelineColumnWidth(defaultUISettings.entryColWidth) + window.dispatchEvent(new Event("resize")) + }} + />
- )} -
+
+ {realEntryId ? ( + <> +
+ +
+
+
+ +
+
+ + ) : ( +
+ +
+ )} +
+ + )}
@@ -167,22 +258,25 @@ const AIEnhancedTimelineLayoutImpl = () => { {aiSidebarVisible && ( <> { const resetWidth = isAllView ? getHalfScreenWidth() : clampWidth(defaultUISettings.aiColWidth) setUISetting(isAllView ? "aiColWidthAll" : "aiColWidth", resetWidth) - setPosition(resetWidth) + setAiPanelWidth(resetWidth) window.dispatchEvent(new Event("resize")) }} /> diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/components/DateItem.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/components/DateItem.tsx index da1a93010..851bf08c1 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/components/DateItem.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/components/DateItem.tsx @@ -13,6 +13,7 @@ import { RelativeDay } from "~/components/ui/datetime" import { IconScaleTransition } from "~/components/ux/transition/icon" import { useFeature } from "~/hooks/biz/useFeature" import { getRouteParams, useRouteParams } from "~/hooks/biz/useRouteParams" +import { useShowEntryDetailsColumn } from "~/hooks/biz/useShowEntryDetailsColumn" import { markAllByRoute } from "../hooks/useMarkAll" @@ -43,7 +44,9 @@ const useParseDate = (date: string) => const dateItemclassName = tw`relative flex items-center text-sm lg:text-base gap-1 px-4 font-bold text-text h-7` export const DateItem = memo(({ date, view, isSticky }: DateItemProps) => { const aiEnabled = useFeature("ai") - if (view === FeedViewType.SocialMedia || aiEnabled) { + const showEntryDetailsColumn = useShowEntryDetailsColumn() + + if (view === FeedViewType.SocialMedia || (aiEnabled && !showEntryDetailsColumn)) { return } return diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/ArticleLayout.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/ArticleLayout.tsx index c221718b0..e1efd80b9 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/ArticleLayout.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/layouts/ArticleLayout.tsx @@ -7,7 +7,7 @@ import { cn } from "@follow/utils" import { ErrorBoundary } from "@sentry/react" import { useCallback, useMemo, useRef, useState } from "react" -import { AIChatPanelStyle, useAIChatPanelStyle } from "~/atoms/settings/ai" +import { AIChatPanelStyle, useAIChatPanelStyle, useAIPanelVisibility } from "~/atoms/settings/ai" import { useUISettingKey } from "~/atoms/settings/ui" import { ShadowDOM } from "~/components/common/ShadowDOM" import type { TocRef } from "~/components/ui/markdown/components/Toc" @@ -66,8 +66,9 @@ export const ArticleLayout: React.FC = ({ }, [removeBlock]) const aiChatPanelStyle = useAIChatPanelStyle() + const isAIPanelVisible = useAIPanelVisibility() - const shouldShowAISummary = aiChatPanelStyle === AIChatPanelStyle.Floating + const shouldShowAISummary = aiChatPanelStyle === AIChatPanelStyle.Floating || !isAIPanelVisible if (!entry) return null return (