import React, { FC, ReactNode, useContext, useEffect, useState } from 'react'; import Link from 'next/link'; import Head from 'next/head'; import { differenceInSeconds } from 'date-fns'; import { useRouter } from 'next/router'; import { Layout, Menu, Alert, Button, Space, Tooltip } from 'antd'; import classNames from 'classnames'; import dynamic from 'next/dynamic'; import { upgradeVersionAvailable } from '../../utils/apis'; import { parseSecondsToDurationString } from '../../utils/format'; import { OwncastLogo } from '../common/OwncastLogo/OwncastLogo'; import { ServerStatusContext } from '../../utils/server-status-context'; import { AlertMessageContext } from '../../utils/alert-message-context'; import { TextFieldWithSubmit } from './TextFieldWithSubmit'; import { TEXTFIELD_PROPS_STREAM_TITLE } from '../../utils/config-constants'; import { ComposeFederatedPost } from './ComposeFederatedPost'; import { UpdateArgs } from '../../types/config-section'; import { FatalErrorStateModal } from '../modals/FatalErrorStateModal/FatalErrorStateModal'; // Lazy loaded components const SettingOutlined = dynamic(() => import('@ant-design/icons/SettingOutlined'), { ssr: false, }); // Lazy loaded components const HomeOutlined = dynamic(() => import('@ant-design/icons/HomeOutlined'), { ssr: false, }); const LineChartOutlined = dynamic(() => import('@ant-design/icons/LineChartOutlined'), { ssr: false, }); const ToolOutlined = dynamic(() => import('@ant-design/icons/ToolOutlined'), { ssr: false, }); const PlayCircleFilled = dynamic(() => import('@ant-design/icons/PlayCircleFilled'), { ssr: false, }); const MinusSquareFilled = dynamic(() => import('@ant-design/icons/MinusSquareFilled'), { ssr: false, }); const QuestionCircleOutlined = dynamic(() => import('@ant-design/icons/QuestionCircleOutlined'), { ssr: false, }); const MessageOutlined = dynamic(() => import('@ant-design/icons/MessageOutlined'), { ssr: false, }); const ExperimentOutlined = dynamic(() => import('@ant-design/icons/ExperimentOutlined'), { ssr: false, }); const EditOutlined = dynamic(() => import('@ant-design/icons/EditOutlined'), { ssr: false, }); const FediverseOutlined = dynamic(() => import('../../assets/images/icons/fediverse.svg'), { ssr: false, }); export type MainLayoutProps = { children: ReactNode; }; export const MainLayout: FC = ({ children }) => { const context = useContext(ServerStatusContext); const { serverConfig, online, broadcaster, versionNumber, error: serverError } = context || {}; const { instanceDetails, chatDisabled, federation } = serverConfig; const { enabled: federationEnabled } = federation; const [currentStreamTitle, setCurrentStreamTitle] = useState(''); const [postModalDisplayed, setPostModalDisplayed] = useState(false); const alertMessage = useContext(AlertMessageContext); const router = useRouter(); const { route } = router || {}; const { Header, Footer, Content, Sider } = Layout; const [upgradeVersion, setUpgradeVersion] = useState(''); const checkForUpgrade = async () => { try { const result = await upgradeVersionAvailable(versionNumber); setUpgradeVersion(result); } catch (error) { console.log('==== error', error); } }; useEffect(() => { checkForUpgrade(); }, [versionNumber]); useEffect(() => { setCurrentStreamTitle(instanceDetails.streamTitle); }, [instanceDetails]); const handleStreamTitleChanged = ({ value }: UpdateArgs) => { setCurrentStreamTitle(value); }; const handleCreatePostButtonPressed = () => { setPostModalDisplayed(true); }; const appClass = classNames({ 'app-container': true, online, }); const upgradeVersionString = `${upgradeVersion}` || ''; const upgradeMessage = `Upgrade to v${upgradeVersionString}`; const openMenuItems = upgradeVersion ? ['utilities-menu'] : []; const clearAlertMessage = () => { alertMessage.setMessage(null); }; const headerAlertMessage = alertMessage.message ? ( ) : null; // status indicator items const streamDurationString = broadcaster ? parseSecondsToDurationString(differenceInSeconds(new Date(), new Date(broadcaster.time))) : ''; const statusIcon = online ? : ; const statusMessage = online ? `Online ${streamDurationString}` : 'Offline'; const statusIndicator = (
{statusMessage} {statusIcon}
); const integrationsMenu = [ { label: Webhooks, key: '/admin/webhooks', }, { label: Access Tokens, key: '/admin/access-tokens', }, { label: External Actions, key: '/admin/actions', }, ]; const chatMenu = [ { label: Messages, key: '/admin/chat/messages', }, { label: Users, key: '/admin/chat/users', }, { label: Emojis, key: '/admin/chat/emojis', }, ]; const utilitiesMenu = [ { label: Hardware, key: '/admin/hardware-info', }, { label: Stream Health, key: '/admin/stream-health', }, { label: Logs, key: '/admin/logs', }, federationEnabled && { label: Social Actions, key: '/admin/federation/actions', }, ]; const configurationMenu = [ { label: General, key: '/admin/config/general', }, { label: Server Setup, key: '/admin/config/server', }, { label: Video, key: '/admin/config-video', }, { label: Chat, key: '/admin/config-chat', }, { label: Social, key: '/admin/config-federation', }, { label: Notifications, key: '/admin/config-notify', }, ]; const menuItems = [ { label: Home, icon: , key: '/admin' }, { label: Viewers, icon: , key: '/admin/viewer-info', }, !chatDisabled && { label: Chat & Users, icon: , children: chatMenu, key: 'chat-and-users', }, federationEnabled && { key: '/admin/federation/followers', label: Followers, icon: ( {/* Wrapping the icon in span for consistency with other icons used directly from antd */} ), }, { key: 'configuration', label: 'Configuration', icon: , children: configurationMenu, }, { key: 'utilities', label: 'Utilities', icon: , children: utilitiesMenu, }, { key: 'integrations', label: 'Integrations', icon: , children: integrationsMenu, }, upgradeVersion && { key: '/admin/upgrade', label: {upgradeMessage}, }, { key: '/admin/help', label: Help, icon: , }, ]; const [openKeys, setOpenKeys] = useState(openMenuItems); const onOpenChange = (keys: string[]) => { setOpenKeys(keys); }; useEffect(() => { menuItems.forEach( item => item?.children?.forEach(child => { if (child?.key === route) setOpenKeys([...openMenuItems, item.key]); }), ); }, []); return ( Owncast Admin {serverError?.type === 'OWNCAST_SERVICE_UNREACHABLE' && ( )}

Owncast Admin

{statusIndicator}
{headerAlertMessage} {children}
setPostModalDisplayed(false)} /> ); };