257 lines
14 KiB
TypeScript
257 lines
14 KiB
TypeScript
import { RootPortal } from "@follow/components/ui/portal/index.jsx"
|
|
import { IN_ELECTRON, PROD } from "@follow/shared/constants"
|
|
import { useWhoami } from "@follow/store/user/hooks"
|
|
import { preventDefault } from "@follow/utils/dom"
|
|
import type { PropsWithChildren } from "react"
|
|
import * as React from "react"
|
|
import { Suspense, useRef, useState } from "react"
|
|
import { Outlet } from "react-router"
|
|
|
|
import { setMainContainerElement, setRootContainerElement } from "~/atoms/dom"
|
|
import { useUISettingKey } from "~/atoms/settings/ui"
|
|
import { useLoginModalShow } from "~/atoms/user"
|
|
import { AppErrorBoundary } from "~/components/common/AppErrorBoundary"
|
|
import { ErrorComponentType } from "~/components/errors/enum"
|
|
import { PlainModal } from "~/components/ui/modal/stacked/custom-modal"
|
|
import { DeclarativeModal } from "~/components/ui/modal/stacked/declarative-modal"
|
|
import { ROOT_CONTAINER_ID } from "~/constants/dom"
|
|
import { EnvironmentIndicator } from "~/modules/app/EnvironmentIndicator"
|
|
import { LoginModalContent } from "~/modules/auth/LoginModalContent"
|
|
import { DebugRegistry } from "~/modules/debug/registry"
|
|
import { EntriesProvider } from "~/modules/entry-column/context/EntriesContext"
|
|
import { CmdF } from "~/modules/panel/cmdf"
|
|
import { SearchCmdK } from "~/modules/panel/cmdk"
|
|
import { CmdNTrigger } from "~/modules/panel/cmdn"
|
|
import { AppNotificationContainer } from "~/modules/upgrade/lazy/index"
|
|
|
|
import { NewUserGuide } from "./subscription-column/components/NewUserGuide"
|
|
import { SubscriptionColumnContainer } from "./subscription-column/SubscriptionColumn"
|
|
|
|
const errorTypes = [
|
|
ErrorComponentType.Page,
|
|
ErrorComponentType.FeedFoundCanBeFollow,
|
|
ErrorComponentType.FeedNotFound,
|
|
] as ErrorComponentType[]
|
|
|
|
/**
|
|
* MainDestopLayout Component
|
|
*
|
|
* The main desktop layout that serves as the primary container for the Follow application.
|
|
* This layout is responsible for:
|
|
* - Providing the root layout structure with subscription sidebar and main content area
|
|
* - Handling authentication states and displaying login modals
|
|
* - Managing error boundaries for critical app errors
|
|
* - Rendering app-wide panels (search, commands, notifications)
|
|
*
|
|
* ## Layout Scenarios
|
|
*
|
|
* ### Scenario 1: Timeline View (/timeline/1/feed-123/entry-456)
|
|
* ```
|
|
* ┌─────────────────────────────────────────────────────────────────┐
|
|
* │ MainDestopLayout │
|
|
* ├─────────────┬───────────────────────────────────────────────────┤
|
|
* │ Subscription│ TimelineEntryTwoColumnLayout │
|
|
* │ Column ├─────────────────┬─────────────────────────────────┤
|
|
* │ │ EntryColumn │ EntryContentView │
|
|
* │ ┌─────────┐ │ ┌─────────────┐ │ ┌─────────────────────────────┐ │
|
|
* │ │ Feeds │ │ │ Entry List │ │ │ Article Content │ │
|
|
* │ │ - Tech │ │ │ - Article 1 │ │ │ │ │
|
|
* │ │ - News │ │ │ - Article 2 │ │ │ # Article Title │ │
|
|
* │ │ - Blog │ │ │ - Article 3 │ │ │ Article content here... │ │
|
|
* │ └─────────┘ │ └─────────────┘ │ └─────────────────────────────┘ │
|
|
* └─────────────┴─────────────────┴─────────────────────────────────┘
|
|
* ```
|
|
*
|
|
* ### Scenario 2: Discover Page (/discover)
|
|
* ```
|
|
* ┌─────────────────────────────────────────────────────────────────┐
|
|
* │ MainDestopLayout │
|
|
* ├─────────────┬───────────────────────────────────────────────────┤
|
|
* │ Subscription│ SubviewLayout (Full-screen Modal) │
|
|
* │ Column │ ┌─────────────────────────────────────────────────┐ │
|
|
* │ │ │ ◄ Back Discover Feeds [Import] [Add] │ │
|
|
* │ ┌─────────┐ │ ├─────────────────────────────────────────────────┤ │
|
|
* │ │ Feeds │ │ │ │ │
|
|
* │ │ - Tech │ │ │ 🔍 Search for feeds... │ │
|
|
* │ │ - News │ │ │ │ │
|
|
* │ │ - Blog │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
|
|
* │ └─────────┘ │ │ │ Tech │ │ News │ │ Design │ │ │
|
|
* │ │ │ │ Feeds │ │ Sources │ │ Blogs │ │ │
|
|
* │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │
|
|
* │ │ └─────────────────────────────────────────────────┘ │
|
|
* └─────────────┴───────────────────────────────────────────────────┘
|
|
* ```
|
|
*
|
|
* ### Scenario 3: AI Chat (/ai)
|
|
* ```
|
|
* ┌─────────────────────────────────────────────────────────────────┐
|
|
* │ MainDestopLayout │
|
|
* ├─────────────┬───────────────────────────────────────────────────┤
|
|
* │ Subscription│ AIChatLayout │
|
|
* │ Column │ ┌─────────────────────────────────────────────────┐ │
|
|
* │ │ │ 🤖 AI Assistant ⚙️ │ │
|
|
* │ ┌─────────┐ │ ├─────────────────────────────────────────────────┤ │
|
|
* │ │ Feeds │ │ │ 💬 How can I help you today? │ │
|
|
* │ │ - Tech │ │ │ │ │
|
|
* │ │ - News │ │ │ 👤 Summarize my latest tech articles │ │
|
|
* │ │ - Blog │ │ │ │ │
|
|
* │ └─────────┘ │ │ 🤖 Here's a summary of your recent tech... │ │
|
|
* │ │ │ │ │
|
|
* │ │ ├─────────────────────────────────────────────────┤ │
|
|
* │ │ │ Type a message... [📎] [🎙️] [📤] │ │
|
|
* │ │ └─────────────────────────────────────────────────┘ │
|
|
* └─────────────┴───────────────────────────────────────────────────┘
|
|
* ```
|
|
*
|
|
* ### Scenario 4: Default View (/) - Timeline Home
|
|
* ```
|
|
* ┌─────────────────────────────────────────────────────────────────┐
|
|
* │ MainDestopLayout │
|
|
* ├─────────────┬───────────────────────────────────────────────────┤
|
|
* │ Subscription│ Default Timeline (All Feeds) │
|
|
* │ Column │ ┌─────────────────────────────────────────────────┐ │
|
|
* │ │ │ 📰 All Articles ⚙️ │ │
|
|
* │ ┌─────────┐ │ ├─────────────────────────────────────────────────┤ │
|
|
* │ │ 📌 Today │ │ │ [Tech Blog] New React Features │ │
|
|
* │ │ ⭐ Starred │ │ │ [News Site] Breaking: AI Breakthrough │ │
|
|
* │ │ 📚 All │ │ │ [Design Blog] UI Trends 2024 │ │
|
|
* │ │ │ │ │ [Tech News] JavaScript Updates │ │
|
|
* │ │ Feeds: │ │ │ [Blog] How to Build Better Apps │ │
|
|
* │ │ • Tech │ │ │ │ │
|
|
* │ │ • News │ │ │ Load more articles... │ │
|
|
* │ │ • Design│ │ │ │ │
|
|
* │ └─────────┘ │ └─────────────────────────────────────────────────┘ │
|
|
* └─────────────┴───────────────────────────────────────────────────┘
|
|
* ```
|
|
*
|
|
* ## Router Outlet Flow
|
|
* The `<Outlet />` in this component renders different child layouts based on the current route:
|
|
* - `/` → Default timeline view
|
|
* - `/timeline/*` → TimelineEntryTwoColumnLayout (two-column feed reader)
|
|
* - `/discover` → SubviewLayout (full-screen discovery)
|
|
* - `/ai` → AIChatLayout (AI chat interface)
|
|
* - `/power`, `/action`, `/rsshub` → SubviewLayout (utility pages)
|
|
*
|
|
* @component
|
|
* @example
|
|
* // This component is automatically rendered by React Router
|
|
* // based on the route configuration in generated-routes.ts
|
|
*/
|
|
export function MainDestopLayout() {
|
|
const isAuthFail = useLoginModalShow()
|
|
const user = useWhoami()
|
|
|
|
const containerRef = useRef<HTMLDivElement | null>(null)
|
|
|
|
return (
|
|
<RootContainer ref={containerRef}>
|
|
{!PROD && <EnvironmentIndicator />}
|
|
|
|
<Suspense>
|
|
<AppNotificationContainer />
|
|
</Suspense>
|
|
|
|
<EntriesProvider>
|
|
<SubscriptionColumnContainer />
|
|
|
|
<main
|
|
ref={setMainContainerElement}
|
|
className="flex min-w-0 flex-1 bg-theme-background pt-[calc(var(--fo-window-padding-top)_-10px)] !outline-none"
|
|
// NOTE: tabIndex for main element can get by `document.activeElement`
|
|
tabIndex={-1}
|
|
>
|
|
<AppErrorBoundary errorType={errorTypes}>
|
|
<Outlet />
|
|
</AppErrorBoundary>
|
|
</main>
|
|
</EntriesProvider>
|
|
|
|
<NewUserGuide />
|
|
|
|
{isAuthFail && !user && (
|
|
<RootPortal>
|
|
<DeclarativeModal
|
|
id="login"
|
|
defaultOpen
|
|
CustomModalComponent={PlainModal}
|
|
overlay
|
|
title="Login"
|
|
canClose={!IN_ELECTRON}
|
|
clickOutsideToDismiss={!IN_ELECTRON}
|
|
>
|
|
<LoginModalContent canClose={!IN_ELECTRON} runtime={IN_ELECTRON ? "app" : "browser"} />
|
|
</DeclarativeModal>
|
|
</RootPortal>
|
|
)}
|
|
|
|
<SearchCmdK />
|
|
<CmdNTrigger />
|
|
{IN_ELECTRON && <CmdF />}
|
|
</RootContainer>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* RootContainer Component
|
|
*
|
|
* The root container wrapper that:
|
|
* - Sets up CSS custom properties for layout dimensions
|
|
* - Provides the base container styling and dimensions
|
|
* - Manages DOM element references for the layout system
|
|
* - Handles context menu prevention and responsive behavior
|
|
*
|
|
* @param ref - Ref forwarded to the root div element
|
|
* @param children - Child components to render within the container
|
|
* @component
|
|
*/
|
|
const RootContainer = ({
|
|
ref,
|
|
children,
|
|
}: PropsWithChildren & { ref?: React.Ref<HTMLDivElement | null> }) => {
|
|
const feedColWidth = useUISettingKey("feedColWidth")
|
|
|
|
const [elementRef, _setElementRef] = useState<HTMLDivElement | null>(null)
|
|
const setElementRef = React.useCallback((el: HTMLDivElement | null) => {
|
|
_setElementRef(el)
|
|
setRootContainerElement(el)
|
|
}, [])
|
|
React.useImperativeHandle(ref, () => elementRef!)
|
|
return (
|
|
<div
|
|
ref={setElementRef}
|
|
style={
|
|
{
|
|
"--fo-feed-col-w": `${feedColWidth}px`,
|
|
} as any
|
|
}
|
|
className="relative z-0 flex h-screen overflow-hidden print:h-auto print:overflow-auto"
|
|
onContextMenu={preventDefault}
|
|
id={ROOT_CONTAINER_ID}
|
|
>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
DebugRegistry.add("New User Guide", () => {
|
|
import("~/modules/new-user-guide/guide-modal-content").then((m) => {
|
|
window.presentModal({
|
|
title: "New User Guide",
|
|
content: ({ dismiss }) => (
|
|
<m.GuideModalContent
|
|
onClose={() => {
|
|
dismiss()
|
|
}}
|
|
/>
|
|
),
|
|
|
|
CustomModalComponent: PlainModal,
|
|
modalContainerClassName: "flex items-center justify-center",
|
|
|
|
canClose: false,
|
|
clickOutsideToDismiss: false,
|
|
overlay: true,
|
|
})
|
|
})
|
|
})
|