import { msg } from "@lingui/core/macro"
import { ActionIcon, AppShell, Box, Center, Group, ScrollArea, Title, useMantineTheme } from "@mantine/core"
import { type ReactNode, type RefObject, Suspense, useEffect, useRef, useState } from "react"
import Draggable from "react-draggable"
import { TbMenu2, TbPlus, TbX } from "react-icons/tb"
import { Outlet } from "react-router-dom"
import { useSwipeable } from "react-swipeable"
import Tinycon from "tinycon"
import { Constants } from "@/app/constants"
import { redirectToAdd, redirectToRootCategory } from "@/app/redirect/thunks"
import { useAppDispatch, useAppSelector } from "@/app/store"
import { setMobileMenuOpen } from "@/app/tree/slice"
import { reloadTree } from "@/app/tree/thunks"
import { setSidebarWidth } from "@/app/user/slice"
import { reloadProfile, reloadSettings, reloadTags } from "@/app/user/thunks"
import { categoryUnreadCount } from "@/app/utils"
import { ActionButton } from "@/components/ActionButton"
import { AnnouncementDialog } from "@/components/AnnouncementDialog"
import { DisablePullToRefresh } from "@/components/DisablePullToRefresh"
import { Loader } from "@/components/Loader"
import { Logo } from "@/components/Logo"
import { MarkAllAsReadConfirmationDialog } from "@/components/MarkAllAsReadConfirmationDialog"
import { OnDesktop } from "@/components/responsive/OnDesktop"
import { OnMobile } from "@/components/responsive/OnMobile"
import { useAppLoading } from "@/hooks/useAppLoading"
import { useBrowserExtension } from "@/hooks/useBrowserExtension"
import { useMobile } from "@/hooks/useMobile"
import { useWebSocket } from "@/hooks/useWebSocket"
import { LoadingPage } from "@/pages/LoadingPage"
import { tss } from "@/tss"
interface LayoutProps {
sidebar: ReactNode
sidebarVisible: boolean
header: ReactNode
}
function LogoAndTitle() {
const dispatch = useAppDispatch()
return (
await dispatch(redirectToRootCategory())}
style={{ cursor: "pointer" }}
>
CommaFeed
)
}
function UnreadCountTitleHandler({
enabled,
}: Readonly<{
enabled?: boolean
}>) {
const root = useAppSelector(state => state.tree.rootCategory)
const unreadCount = categoryUnreadCount(root)
return {enabled && unreadCount > 0 ? `(${unreadCount}) CommaFeed` : "CommaFeed"}
}
function UnreadCountFaviconHandler({ enabled }: { enabled?: boolean }) {
const root = useAppSelector(state => state.tree.rootCategory)
const unreadCount = categoryUnreadCount(root)
useEffect(() => {
if (enabled && unreadCount > 0) {
Tinycon.setBubble(unreadCount)
} else {
Tinycon.reset()
}
}, [unreadCount, enabled])
return null
}
function BrowserExtensionBadgeUnreadCountHandler() {
const root = useAppSelector(state => state.tree.rootCategory)
const { setBadgeUnreadCount } = useBrowserExtension()
useEffect(() => {
if (!root) return
const unreadCount = categoryUnreadCount(root)
setBadgeUnreadCount(unreadCount)
}, [root, setBadgeUnreadCount])
return null
}
function CustomJsHandler() {
const [scriptLoaded, setScriptLoaded] = useState(false)
const { loading } = useAppLoading()
useEffect(() => {
if (scriptLoaded || loading) {
return
}
const script = document.createElement("script")
script.src = "custom_js.js"
script.async = true
document.body.appendChild(script)
setScriptLoaded(true)
return () => script.remove()
}, [scriptLoaded, loading])
return null
}
function CustomCssHandler() {
useEffect(() => {
const link = document.createElement("link")
link.rel = "stylesheet"
link.type = "text/css"
link.href = "custom_css.css"
document.head.appendChild(link)
return () => link.remove()
}, [])
return null
}
const useStyles = tss
.withParams<{
sidebarWidth: number
sidebarPadding: string
sidebarRightBorderWidth: string
}>()
.create(({ sidebarWidth, sidebarPadding, sidebarRightBorderWidth }) => {
return {
sidebarContent: {
maxWidth: `calc(${sidebarWidth}px - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
[`@media (max-width: ${Constants.layout.mobileBreakpoint}px)`]: {
maxWidth: `calc(100vw - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
},
},
}
})
export default function Layout(props: Readonly) {
const theme = useMantineTheme()
const mobile = useMobile()
const { isBrowserExtensionPopup } = useBrowserExtension()
const draggableSeparator = useRef(null)
const { loading } = useAppLoading()
const unreadCountTitle = useAppSelector(state => state.user.settings?.unreadCountTitle)
const unreadCountFavicon = useAppSelector(state => state.user.settings?.unreadCountFavicon)
const disablePullToRefresh = useAppSelector(state => state.user.settings?.disablePullToRefresh)
const mobileMenuOpen = useAppSelector(state => state.tree.mobileMenuOpen)
const webSocketConnected = useAppSelector(state => state.server.webSocketConnected)
const treeReloadInterval = useAppSelector(state => state.server.serverInfos?.treeReloadInterval)
const mobileFooter = useAppSelector(state => state.user.settings?.mobileFooter)
const sidebarWidth = useAppSelector(state => state.user.localSettings.sidebarWidth)
const headerInFooter = mobile && !isBrowserExtensionPopup && mobileFooter
const dispatch = useAppDispatch()
useWebSocket()
const sidebarPadding = theme.spacing.xs
const { classes } = useStyles({
sidebarWidth,
sidebarPadding,
sidebarRightBorderWidth: "1px",
})
useEffect(() => {
// load initial data
dispatch(reloadSettings())
dispatch(reloadProfile())
dispatch(reloadTree())
dispatch(reloadTags())
}, [dispatch])
useEffect(() => {
let timer: number | undefined
if (!webSocketConnected && treeReloadInterval) {
// reload tree periodically if not receiving websocket events
timer = window.setInterval(async () => await dispatch(reloadTree()), treeReloadInterval)
}
return () => clearInterval(timer)
}, [dispatch, webSocketConnected, treeReloadInterval])
const burger = (
: }
onClick={() => dispatch(setMobileMenuOpen(!mobileMenuOpen))}
/>
)
const addButton = (
await dispatch(redirectToAdd())}
aria-label="Subscribe"
>
)
const header = (
<>
{mobileMenuOpen && (
{burger}
{addButton}
)}
{!mobileMenuOpen && (
{burger}
{props.header}
)}
{addButton}
{props.header}
>
)
const swipeHandlers = useSwipeable({
onSwiping: e => {
const threshold = document.documentElement.clientWidth / 6
if (e.absX > threshold) {
dispatch(setMobileMenuOpen(e.dir === "Right"))
}
},
})
if (loading) return
return (
<>
{!headerInFooter && header}
{headerInFooter && header}
{props.sidebar}
}
axis="x"
defaultPosition={{
x: sidebarWidth,
y: 0,
}}
bounds={{
left: 120,
right: 1000,
}}
grid={[30, 30]}
onDrag={(_e, data) => {
dispatch(setSidebarWidth(data.x))
}}
>
}>
>
)
}