From 699cbfaefca804448acc9f03deb45a71e802dad3 Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 13 Aug 2025 01:46:19 +0800 Subject: [PATCH] feat(ui): enhance debug feature management and chat ai entry content layout - Updated the debug feature atom to support feature overrides and added a new hook for setting debug feature values. - Integrated debug feature toggles into the EnvironmentIndicator component, allowing users to enable or disable features dynamically. - Refactored the useFeature hook to accommodate the new debug feature logic. - Improved the EntryLayoutContent and AIEntryLayout components to handle feature visibility based on debug settings. - Introduced a new AIEntryHeader component for better separation of concerns in entry header rendering. These changes collectively enhance the configurability and usability of the debug features, providing users with better control over feature management in the application. Signed-off-by: Innei --- .../layer/renderer/src/atoms/debug-feature.ts | 14 ++- .../renderer/src/hooks/biz/useFeature.ts | 7 +- .../entry-content/EntryLayoutContent.tsx | 4 +- .../app-layout/entry-content/index.tsx | 2 +- .../src/modules/app/EnvironmentIndicator.tsx | 84 ++++++++++--- .../modules/entry-column/AIEntryLayout.tsx | 116 +----------------- .../src/modules/entry-column/item.tsx | 10 +- .../templates/grid-item-template.tsx | 6 +- .../components/EntryTimelineSidebar.tsx | 6 +- .../entry-content/EntryContent.ai.tsx | 59 +++++---- .../EntryScrollingAndNavigationHandler.tsx | 75 +++++------ .../components/entry-header/AIEntryHeader.tsx | 18 +++ .../components/entry-header/EntryHeader.tsx | 85 ++----------- .../components/entry-header/index.ts | 1 + .../entry-header/internal/BaseEntryHeader.tsx | 74 +++++++++++ .../internal/EntryHeaderActionsContainer.tsx | 30 +++++ .../entry-header/internal/EntryHeaderMeta.tsx | 37 ++++++ .../internal/EntryHeaderReadHistory.tsx | 37 ++++++ .../[timelineId]/[feedId]/[entryId]/index.tsx | 2 +- packages/internal/store/src/entry/hooks.ts | 4 +- 20 files changed, 383 insertions(+), 288 deletions(-) create mode 100644 apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/AIEntryHeader.tsx create mode 100644 apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/BaseEntryHeader.tsx create mode 100644 apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderActionsContainer.tsx create mode 100644 apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderMeta.tsx create mode 100644 apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderReadHistory.tsx diff --git a/apps/desktop/layer/renderer/src/atoms/debug-feature.ts b/apps/desktop/layer/renderer/src/atoms/debug-feature.ts index 0e80022ab..74bd5eb6f 100644 --- a/apps/desktop/layer/renderer/src/atoms/debug-feature.ts +++ b/apps/desktop/layer/renderer/src/atoms/debug-feature.ts @@ -2,6 +2,14 @@ import { createAtomHooks } from "@follow/utils/jotai" import { getStorageNS } from "@follow/utils/ns" import { atomWithStorage } from "jotai/utils" -export const [, , useDebugFeatureValue, getDebugFeatureValue, ,] = createAtomHooks( - atomWithStorage(getStorageNS("debug-feature"), {}), -) +// Shape: { __override?: boolean, [featureKey: string]: boolean } +export const [ + , + , + useDebugFeatureValue, + useSetDebugFeatureValue, + getDebugFeatureValue, + setDebugFeatureValue, +] = createAtomHooks(atomWithStorage>(getStorageNS("debug-feature"), {})) + +export { useDebugFeatureValue as useDebugFeatures } diff --git a/apps/desktop/layer/renderer/src/hooks/biz/useFeature.ts b/apps/desktop/layer/renderer/src/hooks/biz/useFeature.ts index aa73267b1..58e4580c7 100644 --- a/apps/desktop/layer/renderer/src/hooks/biz/useFeature.ts +++ b/apps/desktop/layer/renderer/src/hooks/biz/useFeature.ts @@ -5,8 +5,9 @@ import { featureConfigMap } from "~/lib/features" export const useFeature = (feature: keyof typeof featureConfigMap) => { const debugFeatureValue = useDebugFeatureValue() const serverConfigs = useServerConfigs() - const isEnabled = - (featureConfigMap[feature] && serverConfigs?.[featureConfigMap[feature]]) || - debugFeatureValue[feature] + const override = !!(debugFeatureValue as any).__override + const isEnabled = override + ? !!(debugFeatureValue as any)[feature] + : !!(featureConfigMap[feature] && serverConfigs?.[featureConfigMap[feature]]) return isEnabled } diff --git a/apps/desktop/layer/renderer/src/modules/app-layout/entry-content/EntryLayoutContent.tsx b/apps/desktop/layer/renderer/src/modules/app-layout/entry-content/EntryLayoutContent.tsx index 9fedc0d12..12656ee09 100644 --- a/apps/desktop/layer/renderer/src/modules/app-layout/entry-content/EntryLayoutContent.tsx +++ b/apps/desktop/layer/renderer/src/modules/app-layout/entry-content/EntryLayoutContent.tsx @@ -92,7 +92,7 @@ export const EntryLayoutContentWithAI = () => { export const EntryLayoutContent = () => { const aiEnabled = useFeature("ai") if (aiEnabled) { - return + return null } return } @@ -175,6 +175,6 @@ const EntryGridContainer: FC< ) } else { - return
{children}
+ return
{children}
} } diff --git a/apps/desktop/layer/renderer/src/modules/app-layout/entry-content/index.tsx b/apps/desktop/layer/renderer/src/modules/app-layout/entry-content/index.tsx index bc18a3854..ef640226b 100644 --- a/apps/desktop/layer/renderer/src/modules/app-layout/entry-content/index.tsx +++ b/apps/desktop/layer/renderer/src/modules/app-layout/entry-content/index.tsx @@ -1 +1 @@ -export { EntryLayoutContent as RightContentLayout } from "./EntryLayoutContent" +export { EntryLayoutContent } from "./EntryLayoutContent" diff --git a/apps/desktop/layer/renderer/src/modules/app/EnvironmentIndicator.tsx b/apps/desktop/layer/renderer/src/modules/app/EnvironmentIndicator.tsx index 324e38228..c5c4fb9d1 100644 --- a/apps/desktop/layer/renderer/src/modules/app/EnvironmentIndicator.tsx +++ b/apps/desktop/layer/renderer/src/modules/app/EnvironmentIndicator.tsx @@ -1,5 +1,6 @@ /* eslint-disable @eslint-react/dom/no-missing-iframe-sandbox */ import { Button } from "@follow/components/ui/button/index.js" +import { Switch } from "@follow/components/ui/switch/index.jsx" import { Tooltip, TooltipContent, @@ -11,11 +12,74 @@ import { DEV, MODE } from "@follow/shared/constants" import { env } from "@follow/shared/env.desktop" import { useUserRole } from "@follow/store/user/hooks" +import { useDebugFeatureValue, useSetDebugFeatureValue } from "~/atoms/debug-feature" import { PlainModal } from "~/components/ui/modal/stacked/custom-modal" import { useModalStack } from "~/components/ui/modal/stacked/hooks" +import { featureConfigMap } from "~/lib/features" import { DebugRegistry } from "../debug/registry" +const EnvironmentDebugModalContent = () => { + const actionMap = DebugRegistry.getAll() + const debugValues = useDebugFeatureValue() as Record + const setDebugValues = useSetDebugFeatureValue() + + const overrideEnabled = !!debugValues.__override + + const handleToggleOverride = (checked: boolean) => { + setDebugValues((prev) => ({ ...(prev as Record), __override: checked })) + } + + const handleToggleFeature = (key: string, checked: boolean) => { + setDebugValues((prev) => ({ ...(prev as Record), [key]: checked })) + } + + const featureKeys = Object.keys(featureConfigMap) + + return ( +
+
+
+
Debug override features
+ +
+

+ When enabled, the switches below override server feature flags locally. +

+
+
+ {featureKeys.map((key) => ( +
+ {key} + handleToggleFeature(key, v)} + disabled={!overrideEnabled} + /> +
+ ))} +
+
+
+ +
+
Debug actions
+
+ {Object.entries(actionMap).map(([key, action]) => ( +
+ {key} + +
+ ))} +
+
+
+ ) +} + export const EnvironmentIndicator = () => { const role = useUserRole() const { present } = useModalStack() @@ -29,27 +93,9 @@ export const EnvironmentIndicator = () => { onClick={() => { if (!DEV) return - const actionMap = DebugRegistry.getAll() - present({ title: "Debug Actions", - content: () => { - return ( -
- {Object.entries(actionMap).map(([key, action]) => { - return ( -
- {key} - -
- ) - })} -
- ) - }, + content: EnvironmentDebugModalContent, }) }} > diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/AIEntryLayout.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/AIEntryLayout.tsx index c7d244329..756b7f72f 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/AIEntryLayout.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/AIEntryLayout.tsx @@ -1,17 +1,15 @@ import { Spring } from "@follow/components/constants/spring.js" -import { Button } from "@follow/components/ui/button/index.js" import { PanelSplitter } from "@follow/components/ui/divider/index.js" import { defaultUISettings } from "@follow/shared/settings/defaults" import { cn } from "@follow/utils" import { AnimatePresence, m } from "motion/react" -import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { useMemo, useRef } from "react" import { useResizable } from "react-resizable-layout" import { useParams } from "react-router" import { AIChatPanelStyle, useAIChatPanelStyle } from "~/atoms/settings/ai" import { getUISettings, setUISetting } from "~/atoms/settings/ui" import { ROUTE_ENTRY_PENDING } from "~/constants" -import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry" import { AIChatLayout } from "~/modules/app-layout/ai/AIChatLayout" import { EntryContent } from "~/modules/entry-content/components/entry-content" import { AppLayoutGridContainerProvider } from "~/providers/app-grid-layout-container-provider" @@ -21,95 +19,11 @@ import { EntryColumn } from "./index" const AIEntryLayoutImpl = () => { const { entryId } = useParams() - const navigate = useNavigateEntry() + const panelStyle = useAIChatPanelStyle() const realEntryId = entryId === ROUTE_ENTRY_PENDING ? "" : entryId - // Swipe/scroll to close functionality - const entryContentRef = useRef(null) - const accumulatedDelta = useRef(0) - const isScrollingAtTop = useRef(false) - const [showScrollHint, setShowScrollHint] = useState(false) - - const handleCloseGesture = useCallback(() => { - navigate({ entryId: null }) - }, [navigate]) - - const handleWheel = useCallback( - (e: WheelEvent) => { - if (!realEntryId || !entryContentRef.current) return - - // Find the actual scroll viewport element with correct Radix UI attribute - const entryContentElement = entryContentRef.current.querySelector( - "[data-radix-scroll-area-viewport]", - ) as HTMLElement - const scrollElement = entryContentElement || entryContentRef.current - - // Check if we're at the top of the content - const scrollTop = scrollElement?.scrollTop || 0 - isScrollingAtTop.current = scrollTop === 0 - setShowScrollHint(scrollTop === 0) - - // Handle trackpad/mouse wheel: upward scroll (deltaY < 0) or downward swipe gesture - // On macOS trackpad, natural scrolling makes upward finger movement negative deltaY - if (e.deltaY < 0 && isScrollingAtTop.current) { - e.preventDefault() - accumulatedDelta.current += Math.abs(e.deltaY) - - // Close when accumulated scroll exceeds threshold (150px for trackpad sensitivity) - if (accumulatedDelta.current > 1000) { - handleCloseGesture() - accumulatedDelta.current = 0 - } - } else { - // Reset accumulation when scrolling down or not at top - accumulatedDelta.current = 0 - } - }, - [realEntryId, handleCloseGesture], - ) - - useEffect(() => { - if (!realEntryId || !entryContentRef.current) return - - const element = entryContentRef.current - - // Find the scroll area viewport element with correct Radix UI attribute - const scrollViewport = element.querySelector("[data-radix-scroll-area-viewport]") as HTMLElement - - // Add wheel event listener to both the main container and scroll viewport - // This ensures the gesture works in both header area and scrollable content - const elementsToListen: HTMLElement[] = [element] - if (scrollViewport) { - elementsToListen.push(scrollViewport) - } - - elementsToListen.forEach((el) => { - el.addEventListener("wheel", handleWheel, { passive: false }) - }) - - // Initial scroll position check for hint visibility - const initialCheckScrollPosition = () => { - const scrollTop = scrollViewport?.scrollTop || element.scrollTop || 0 - setShowScrollHint(scrollTop === 0) - } - - // Check initial position - initialCheckScrollPosition() - - // Add scroll listener for hint visibility - const scrollElement = scrollViewport || element - scrollElement.addEventListener("scroll", initialCheckScrollPosition, { passive: true }) - - return () => { - elementsToListen.forEach((el) => { - el.removeEventListener("wheel", handleWheel) - }) - scrollElement.removeEventListener("scroll", initialCheckScrollPosition) - } - }, [realEntryId, handleWheel]) - // AI chat resizable panel configuration const aiColWidth = useMemo(() => getUISettings().aiColWidth, []) const startDragPosition = useRef(0) @@ -139,38 +53,16 @@ const AIEntryLayoutImpl = () => { {/* Entry content overlay with exit animation */} - + {realEntryId && ( - {/* Scroll hint indicator */} -
- -
- +
)}
diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/item.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/item.tsx index 8c45f2359..649053ac5 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/item.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/item.tsx @@ -1,5 +1,5 @@ import { FeedViewType } from "@follow/constants" -import { useEntry } from "@follow/store/entry/hooks" +import { useHasEntry } from "@follow/store/entry/hooks" import { useEntryTranslation, usePrefetchEntryTranslation } from "@follow/store/translation/hooks" import type { FC } from "react" import { memo } from "react" @@ -47,9 +47,9 @@ const EntryItemImpl = memo(function EntryItemImpl({ }) export const EntryItem: FC = memo(({ entryId, view }) => { - const entry = useEntry(entryId, () => ({})) + const hasEntry = useHasEntry(entryId) - if (!entry) return null + if (!hasEntry) return null return }) @@ -63,9 +63,9 @@ export const EntryVirtualListItem = ({ React.DetailedHTMLProps, HTMLDivElement> & { ref?: React.Ref }) => { - const entry = useEntry(entryId, () => ({})) + const hasEntry = useHasEntry(entryId) - if (!entry) return
+ if (!hasEntry) return
return (
diff --git a/apps/desktop/layer/renderer/src/modules/entry-column/templates/grid-item-template.tsx b/apps/desktop/layer/renderer/src/modules/entry-column/templates/grid-item-template.tsx index 3d41c89f1..50426dd73 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-column/templates/grid-item-template.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-column/templates/grid-item-template.tsx @@ -1,6 +1,6 @@ import { TitleMarquee } from "@follow/components/ui/marquee/index.jsx" import { useIsEntryStarred } from "@follow/store/collection/hooks" -import { useEntry } from "@follow/store/entry/hooks" +import { useEntry, useHasEntry } from "@follow/store/entry/hooks" import { useFeedById } from "@follow/store/feed/hooks" import { cn } from "@follow/utils/utils" import dayjs from "dayjs" @@ -21,9 +21,9 @@ interface GridItemProps extends UniversalItemProps { } export function GridItem(props: GridItemProps) { const { entryId, entryPreview, wrapperClassName, children, translation } = props - const entry = useEntry(entryId, () => ({})) + const hasEntry = useHasEntry(entryId) - if (!entry) return null + if (!hasEntry) return null return (
{children} diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryTimelineSidebar.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryTimelineSidebar.tsx index 0b320c8f8..34a99b345 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryTimelineSidebar.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/EntryTimelineSidebar.tsx @@ -17,15 +17,15 @@ export const EntryTimelineSidebar = ({ entryId }: { entryId: string }) => { return null } - return + return } -const Timeline = ({ entryId }: { entryId: string }) => { +export const EntryTimeline = ({ entryId }: { entryId: string }) => { const entryIds = useGetEntryIdInRange(entryId, [5, 5]) return ( diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.ai.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.ai.tsx index 4b032bb57..14d28369f 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.ai.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryContent.ai.tsx @@ -1,3 +1,4 @@ +import { Spring } from "@follow/components/constants/spring.js" import { MotionButtonBase } from "@follow/components/ui/button/index.js" import { RootPortal } from "@follow/components/ui/portal/index.js" import { ScrollArea } from "@follow/components/ui/scroll-area/index.js" @@ -10,8 +11,9 @@ import { useIsInbox } from "@follow/store/inbox/hooks" import { thenable } from "@follow/utils" import { stopPropagation } from "@follow/utils/dom" import { EventBus } from "@follow/utils/event-bus" -import { clsx, cn } from "@follow/utils/utils" +import { cn } from "@follow/utils/utils" import type { JSAnimation } from "motion/react" +import { m, useAnimationControls } from "motion/react" import * as React from "react" import { memo, useEffect, useRef, useState } from "react" @@ -28,8 +30,8 @@ import { COMMAND_ID } from "~/modules/command/commands/id" import { ApplyEntryActions } from "../../ApplyEntryActions" import { useEntryContent } from "../../hooks" -import { EntryHeader } from "../entry-header" -import { EntryTimelineSidebar } from "../EntryTimelineSidebar" +import { AIEntryHeader } from "../entry-header" +import { EntryTimeline } from "../EntryTimelineSidebar" import { getEntryContentLayout } from "../layouts" import { SourceContentPanel } from "../SourceContentView" import { EntryCommandShortcutRegister } from "./EntryCommandShortcutRegister" @@ -38,6 +40,11 @@ import { EntryNoContent } from "./EntryNoContent" import { EntryScrollingAndNavigationHandler } from "./EntryScrollingAndNavigationHandler.js" import type { EntryContentProps } from "./types" +const contentVariants = { + initial: { opacity: 0, y: 30 }, + animate: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: 30 }, +} const EntryContentImpl: Component = ({ entryId, noMedia, @@ -65,7 +72,6 @@ const EntryContentImpl: Component = ({ const scrollerRef = useRef(null) const safeUrl = useFeedSafeUrl(entryId) - const isInPeekModal = useInPeekModal() const isZenMode = useIsZenMode() const [panelPortalElement, setPanelPortalElement] = useState(null) @@ -89,18 +95,30 @@ const EntryContentImpl: Component = ({ removeBlock(BlockSliceAction.SPECIAL_TYPES.mainEntry) } }, [addOrUpdateBlock, entryId, removeBlock]) + const animationController = useAnimationControls() + + useEffect(() => { + animationController.set(contentVariants.exit) + animationController.start(contentVariants.animate) + return () => { + animationController.stop() + } + }, [animationController, entryId]) return ( -
+ - {!isInPeekModal && ( - - )} +
= ({ scrollerRef={scrollerRef} /> - + {/* Indicator for the entry */}
- {!isZenMode && isInHasTimelineView && !isInPeekModal && ( + {!isZenMode && isInHasTimelineView && ( <> -
+
= ({
-
+
{ @@ -147,10 +165,7 @@ const EntryContentImpl: Component = ({
@@ -187,7 +202,7 @@ const EntryContentImpl: Component = ({ {/* {!isInPeekModal && } */} -
+ ) } export const EntryContent = memo(EntryContentImpl) diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryScrollingAndNavigationHandler.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryScrollingAndNavigationHandler.tsx index a1bec41f9..2cbfd8794 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryScrollingAndNavigationHandler.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-content/EntryScrollingAndNavigationHandler.tsx @@ -2,14 +2,16 @@ import { useFocusActions, useGlobalFocusableScopeSelector, } from "@follow/components/common/Focusable/index.js" +import { Spring } from "@follow/components/constants/spring.js" import { useSmoothScroll } from "@follow/hooks" import { nextFrame } from "@follow/utils/dom" import { EventBus } from "@follow/utils/event-bus" -import { cn, combineCleanupFunctions } from "@follow/utils/utils" +import { clsx, combineCleanupFunctions } from "@follow/utils/utils" import type { JSAnimation } from "motion/react" import { AnimatePresence, m } from "motion/react" import * as React from "react" import { useEffect, useRef, useState } from "react" +import { useEventCallback } from "usehooks-ts" import { FocusablePresets } from "~/components/common/Focusable" import { COMMAND_ID } from "~/modules/command/commands/id" @@ -56,16 +58,21 @@ export const EntryScrollingAndNavigationHandler = ({ const { highlightBoundary } = useFocusActions() const smoothScrollTo = useSmoothScroll() + const navigateToNext = useEventCallback(() => { + EventBus.dispatch(COMMAND_ID.timeline.switchToNext) + setShowKeepScrollingPanel(false) + isAlreadyScrolledBottomRef.current = false + if (scrollerRef.current) { + smoothScrollTo(0, scrollerRef.current) + } + }) useEffect(() => { const checkScrollBottom = ($scroller: HTMLDivElement) => { const currentScroll = $scroller.scrollTop const { scrollHeight, clientHeight } = $scroller if (isAlreadyScrolledBottomRef.current) { - EventBus.dispatch(COMMAND_ID.timeline.switchToNext) - setShowKeepScrollingPanel(false) - isAlreadyScrolledBottomRef.current = false - smoothScrollTo(0, $scroller) + navigateToNext() return } @@ -140,42 +147,38 @@ export const EntryScrollingAndNavigationHandler = ({ }, ), ) - }, [highlightBoundary, scrollAnimationRef, scrollerRef, smoothScrollTo]) + }, [highlightBoundary, navigateToNext, scrollAnimationRef, scrollerRef, smoothScrollTo]) return ( {showKeepScrollingPanel && ( - - Already scrolled to the bottom. -
- Keep pressing to jump to the next article -
+ + + )}
) } - -const FloatPanel: React.FC<{ children: React.ReactNode; side: "bottom" | "top" }> = ({ - children, - side, -}) => ( - - {children} - -) diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/AIEntryHeader.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/AIEntryHeader.tsx new file mode 100644 index 000000000..9e069c7c4 --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/AIEntryHeader.tsx @@ -0,0 +1,18 @@ +import { memo } from "react" + +import { BaseEntryHeader } from "./internal/BaseEntryHeader" +import type { EntryHeaderProps } from "./types" + +function EntryHeaderImpl({ view, entryId, className, compact }: EntryHeaderProps) { + return ( + + ) +} + +export const AIEntryHeader = memo(EntryHeaderImpl) diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/EntryHeader.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/EntryHeader.tsx index 488a003cf..3b405557b 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/EntryHeader.tsx +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/EntryHeader.tsx @@ -1,86 +1,17 @@ -import { views } from "@follow/constants" -import { useEntry } from "@follow/store/entry/hooks" -import { cn } from "@follow/utils/utils" -import { AnimatePresence, m } from "motion/react" import { memo } from "react" -import { useUISettingKey } from "~/atoms/settings/ui" -import { useFeature } from "~/hooks/biz/useFeature" - -import { EntryHeaderActions } from "../../actions/header-actions" -import { MoreActions } from "../../actions/more-actions" -import { useEntryContentScrollToTop, useEntryTitleMeta } from "../../atoms" -import { EntryReadHistory } from "../entry-read-history" +import { BaseEntryHeader } from "./internal/BaseEntryHeader" import type { EntryHeaderProps } from "./types" function EntryHeaderImpl({ view, entryId, className, compact }: EntryHeaderProps) { - const entry = useEntry(entryId, () => ({})) - const entryTitleMeta = useEntryTitleMeta() - const isAtTop = useEntryContentScrollToTop() - - const hideRecentReader = useUISettingKey("hideRecentReader") - - const shouldShowMeta = !isAtTop && !!entryTitleMeta?.title - - const aiEnabled = useFeature("ai") - const isWide = views[view]?.wideMode || aiEnabled - - if (!entry) return null - return ( -
- {!hideRecentReader && ( -
- -
- )} -
-
- - {shouldShowMeta && ( - - - {entryTitleMeta.title} - - - - {entryTitleMeta.description} - - - )} - -
- - {!isWide && ( -
- - -
- )} -
-
+ ) } diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/index.ts b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/index.ts index ea1eaaa54..d9b65cd9e 100644 --- a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/index.ts +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/index.ts @@ -1,2 +1,3 @@ +export * from "./AIEntryHeader" export * from "./EntryHeader" export * from "./types" diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/BaseEntryHeader.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/BaseEntryHeader.tsx new file mode 100644 index 000000000..323ac86dc --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/BaseEntryHeader.tsx @@ -0,0 +1,74 @@ +import type { FeedViewType } from "@follow/constants" +import { views } from "@follow/constants" +import { useHasEntry } from "@follow/store/entry/hooks" +import { cn } from "@follow/utils/utils" +import { memo } from "react" + +import { useUISettingKey } from "~/atoms/settings/ui" + +import { useEntryContentScrollToTop, useEntryTitleMeta } from "../../../atoms" +import { EntryHeaderActionsContainer } from "./EntryHeaderActionsContainer" +import { EntryHeaderMeta } from "./EntryHeaderMeta" +import { EntryHeaderReadHistory } from "./EntryHeaderReadHistory" + +interface EntryHeaderConfig { + showActionsWhenWide?: boolean + alwaysShowActions?: boolean +} + +interface BaseEntryHeaderProps { + view: FeedViewType + entryId: string + className?: string + compact?: boolean + config?: EntryHeaderConfig +} + +function BaseEntryHeaderImpl({ view, entryId, className, compact, config }: BaseEntryHeaderProps) { + const hasEntry = useHasEntry(entryId) + const entryTitleMeta = useEntryTitleMeta() + const isAtTop = useEntryContentScrollToTop() + const hideRecentReader = useUISettingKey("hideRecentReader") + + const shouldShowMeta = !isAtTop && !!entryTitleMeta?.title + const isWide = views[view]?.wideMode + + const shouldShowActions = + config?.alwaysShowActions ?? (config?.showActionsWhenWide ? true : !isWide) + + if (!hasEntry) return null + + return ( +
+ + +
+ + + +
+
+ ) +} + +export const BaseEntryHeader = memo(BaseEntryHeaderImpl) diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderActionsContainer.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderActionsContainer.tsx new file mode 100644 index 000000000..8375d22e0 --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderActionsContainer.tsx @@ -0,0 +1,30 @@ +import type { FeedViewType } from "@follow/constants" +import { memo } from "react" + +import { EntryHeaderActions } from "../../../actions/header-actions" +import { MoreActions } from "../../../actions/more-actions" + +interface EntryHeaderActionsContainerProps { + entryId: string + view: FeedViewType + compact?: boolean + shouldShow: boolean +} + +function EntryHeaderActionsContainerImpl({ + entryId, + view, + compact, + shouldShow, +}: EntryHeaderActionsContainerProps) { + if (!shouldShow) return null + + return ( +
+ + +
+ ) +} + +export const EntryHeaderActionsContainer = memo(EntryHeaderActionsContainerImpl) diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderMeta.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderMeta.tsx new file mode 100644 index 000000000..232f163fb --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderMeta.tsx @@ -0,0 +1,37 @@ +import { AnimatePresence, m } from "motion/react" +import { memo } from "react" + +interface EntryTitleMeta { + title: string + description?: string +} + +interface EntryHeaderMetaProps { + entryTitleMeta: Nullable + shouldShow: boolean +} + +function EntryHeaderMetaImpl({ entryTitleMeta, shouldShow }: EntryHeaderMetaProps) { + return ( +
+ + {shouldShow && entryTitleMeta && ( + + {entryTitleMeta.title} + + + {entryTitleMeta.description} + + + )} + +
+ ) +} + +export const EntryHeaderMeta = memo(EntryHeaderMetaImpl) diff --git a/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderReadHistory.tsx b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderReadHistory.tsx new file mode 100644 index 000000000..6a137aaa5 --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/entry-content/components/entry-header/internal/EntryHeaderReadHistory.tsx @@ -0,0 +1,37 @@ +import type { FeedViewType } from "@follow/constants" +import { views } from "@follow/constants" +import { cn } from "@follow/utils/utils" +import { memo } from "react" + +import { EntryReadHistory } from "../../entry-read-history" + +interface EntryHeaderReadHistoryProps { + entryId: string + view: FeedViewType + shouldShow: boolean + shouldHide: boolean +} + +function EntryHeaderReadHistoryImpl({ + entryId, + view, + shouldShow, + shouldHide, +}: EntryHeaderReadHistoryProps) { + if (!shouldShow) return null + + return ( +
+ +
+ ) +} + +export const EntryHeaderReadHistory = memo(EntryHeaderReadHistoryImpl) diff --git a/apps/desktop/layer/renderer/src/pages/(main)/(layer)/timeline/[timelineId]/[feedId]/[entryId]/index.tsx b/apps/desktop/layer/renderer/src/pages/(main)/(layer)/timeline/[timelineId]/[feedId]/[entryId]/index.tsx index c24a2cc6b..849def4e2 100644 --- a/apps/desktop/layer/renderer/src/pages/(main)/(layer)/timeline/[timelineId]/[feedId]/[entryId]/index.tsx +++ b/apps/desktop/layer/renderer/src/pages/(main)/(layer)/timeline/[timelineId]/[feedId]/[entryId]/index.tsx @@ -1 +1 @@ -export { RightContentLayout as Component } from "~/modules/app-layout/entry-content/index" +export { EntryLayoutContent as Component } from "~/modules/app-layout/entry-content/index" diff --git a/packages/internal/store/src/entry/hooks.ts b/packages/internal/store/src/entry/hooks.ts index 9f3e6e3ca..81f95c367 100644 --- a/packages/internal/store/src/entry/hooks.ts +++ b/packages/internal/store/src/entry/hooks.ts @@ -140,7 +140,9 @@ export function useEntry( return selector(entry) }) } - +export const useHasEntry = (id: string) => { + return useEntryStore((state) => !!state.data[id]) +} export function useEntryList(ids: string[]): Array export function useEntryList(ids: string[], selector: (state: EntryModel) => T): T[] | undefined export function useEntryList(