import * as React from 'react'; import {useEffect, useState} from 'react'; import Box from '@mui/material/Box'; import {ThemeProvider} from '@mui/material/styles'; import CssBaseline from '@mui/material/CssBaseline'; import Toolbar from '@mui/material/Toolbar'; import Notifications from "./Notifications"; import theme from "./theme"; import connectionManager from "../app/ConnectionManager"; import Navigation from "./Navigation"; import ActionBar from "./ActionBar"; import notifier from "../app/Notifier"; import Preferences from "./Preferences"; import {useLiveQuery} from "dexie-react-hooks"; import poller from "../app/Poller"; import pruner from "../app/Pruner"; import subscriptionManager from "../app/SubscriptionManager"; import userManager from "../app/UserManager"; import {BrowserRouter, Route, Routes, useLocation, useNavigate} from "react-router-dom"; import {subscriptionRoute} from "../app/utils"; // TODO support unsubscribed routes // TODO add "home" route that is selected when nothing else fits // TODO new notification indicator // TODO "copy url" toast // TODO "copy link url" button // TODO races when two tabs are open // TODO investigate service workers const App = () => { return ( <BrowserRouter> <ThemeProvider theme={theme}> <CssBaseline/> <Root/> </ThemeProvider> </BrowserRouter> ); } const Root = () => { const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); const [notificationsGranted, setNotificationsGranted] = useState(notifier.granted()); const location = useLocation(); const users = useLiveQuery(() => userManager.all()); const subscriptions = useLiveQuery(() => subscriptionManager.all()); const selectedSubscription = findSelected(location, subscriptions); const newNotificationsCount = subscriptions?.reduce((prev, cur) => prev + cur.new, 0) || 0; useWorkers(); useConnectionListeners(); useEffect(() => { connectionManager.refresh(subscriptions, users); }, [subscriptions, users]); // Dangle! useEffect(() => { document.title = (newNotificationsCount > 0) ? `(${newNotificationsCount}) ntfy web` : "ntfy web"; }, [newNotificationsCount]); return ( <Box sx={{display: 'flex'}}> <CssBaseline/> <ActionBar subscriptions={subscriptions} selectedSubscription={selectedSubscription} onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} /> <Box component="nav" sx={{width: {sm: Navigation.width}, flexShrink: {sm: 0}}}> <Navigation subscriptions={subscriptions} selectedSubscription={selectedSubscription} notificationsGranted={notificationsGranted} requestNotificationPermission={() => notifier.maybeRequestPermission(granted => setNotificationsGranted(granted))} mobileDrawerOpen={mobileDrawerOpen} onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} /> </Box> <Main> <Toolbar/> <Routes> <Route path="settings" element={<Preferences />} /> <Route path="/" element={<Notifications mode="all" subscriptions={subscriptions} />} /> <Route path=":baseUrl/:topic" element={<Notifications mode="one" subscription={selectedSubscription}/>} /> <Route path=":topic" element={<Notifications mode="one" subscription={selectedSubscription}/>} /> </Routes> </Main> </Box> ); } const Main = (props) => { return ( <Box component="main" sx={{ display: 'flex', flexGrow: 1, flexDirection: 'column', padding: 3, width: {sm: `calc(100% - ${Navigation.width}px)`}, height: '100vh', overflow: 'auto', backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900] }} > {props.children} </Box> ); }; const findSelected = (location, subscriptions) => { if (!subscriptions || !location) { return null; } const [subscription] = subscriptions.filter(s => location.pathname === subscriptionRoute(s)); return subscription; /* if (location.pathname === "/" || location.pathname === "/settings") { return null; } if (!subscription) { const [, topic] = location.pathname.split("/"); const subscription = { id: topicUrl(window.location.origin, topic), baseUrl: window.location.origin, topic: topic, last: "" } subscriptionManager.save(subscription); return subscription; } */ }; const useWorkers = () => { useEffect(() => { poller.startWorker(); pruner.startWorker(); }, []); }; const useConnectionListeners = () => { const navigate = useNavigate(); useEffect(() => { const handleNotification = async (subscriptionId, notification) => { const added = await subscriptionManager.addNotification(subscriptionId, notification); if (added) { const defaultClickAction = (subscription) => navigate(subscriptionRoute(subscription)); await notifier.notify(subscriptionId, notification, defaultClickAction) } }; connectionManager.registerStateListener(subscriptionManager.updateState); connectionManager.registerNotificationListener(handleNotification); return () => { connectionManager.resetStateListener(); connectionManager.resetNotificationListener(); } }, // We have to disable dep checking for "navigate". This is fine, it never changes. // eslint-disable-next-line []); }; export default App;