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 Navigation from "./Navigation"; import ActionBar from "./ActionBar"; import notifier from "../app/Notifier"; import Preferences from "./Preferences"; import {useLiveQuery} from "dexie-react-hooks"; import subscriptionManager from "../app/SubscriptionManager"; import userManager from "../app/UserManager"; import {BrowserRouter, Outlet, Route, Routes, useOutletContext, useParams} from "react-router-dom"; import {expandUrl} from "../app/utils"; import ErrorBoundary from "./ErrorBoundary"; import routes from "./routes"; import {useAutoSubscribe, useConnectionListeners, useLocalStorageMigration} from "./hooks"; import {Backdrop, ListItemIcon, ListItemText, Menu} from "@mui/material"; import Paper from "@mui/material/Paper"; import IconButton from "@mui/material/IconButton"; import {MoreVert} from "@mui/icons-material"; import InsertEmoticonIcon from "@mui/icons-material/InsertEmoticon"; import MenuItem from "@mui/material/MenuItem"; import TextField from "@mui/material/TextField"; import SendIcon from "@mui/icons-material/Send"; import priority1 from "../img/priority-1.svg"; import priority2 from "../img/priority-2.svg"; import priority4 from "../img/priority-4.svg"; import priority5 from "../img/priority-5.svg"; // TODO add drag and drop // TODO races when two tabs are open // TODO investigate service workers const App = () => { return ( <BrowserRouter> <ThemeProvider theme={theme}> <CssBaseline/> <ErrorBoundary> <Routes> <Route element={<Layout/>}> <Route path={routes.root} element={<AllSubscriptions/>}/> <Route path={routes.settings} element={<Preferences/>}/> <Route path={routes.subscription} element={<SingleSubscription/>}/> <Route path={routes.subscriptionExternal} element={<SingleSubscription/>}/> </Route> </Routes> </ErrorBoundary> </ThemeProvider> </BrowserRouter> ); } const AllSubscriptions = () => { const { subscriptions } = useOutletContext(); return <Notifications mode="all" subscriptions={subscriptions}/>; }; const SingleSubscription = () => { const { subscriptions, selected } = useOutletContext(); useAutoSubscribe(subscriptions, selected); return <Notifications mode="one" subscription={selected}/>; }; const Layout = () => { const params = useParams(); const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); const [notificationsGranted, setNotificationsGranted] = useState(notifier.granted()); const users = useLiveQuery(() => userManager.all()); const subscriptions = useLiveQuery(() => subscriptionManager.all()); const newNotificationsCount = subscriptions?.reduce((prev, cur) => prev + cur.new, 0) || 0; const [selected] = (subscriptions || []).filter(s => { return (params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic) || (window.location.origin === s.baseUrl && params.topic === s.topic) }); useConnectionListeners(subscriptions, users); useLocalStorageMigration(); useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]); return ( <Box sx={{display: 'flex'}}> <CssBaseline/> <DropZone/> <ActionBar selected={selected} onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} /> <Navigation subscriptions={subscriptions} selectedSubscription={selected} notificationsGranted={notificationsGranted} mobileDrawerOpen={mobileDrawerOpen} onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} onNotificationGranted={setNotificationsGranted} /> <Main> <Toolbar/> <Outlet context={{ subscriptions, selected }}/> </Main> <Sender/> </Box> ); } const Main = (props) => { return ( <Box id="main" 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 priorityFiles = { 1: priority1, 2: priority2, 4: priority4, 5: priority5 }; const Sender = (props) => { const [priority, setPriority] = useState(5); const [priorityAnchorEl, setPriorityAnchorEl] = React.useState(null); const priorityMenuOpen = Boolean(priorityAnchorEl); const handlePriorityClick = (p) => { setPriority(p); setPriorityAnchorEl(null); }; return ( <Paper elevation={3} sx={{ display: "flex", position: 'fixed', bottom: 0, right: 0, padding: 2, width: `calc(100% - ${Navigation.width}px)`, backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900] }} > {false && <IconButton color="inherit" size="large" edge="start"> <MoreVert/> </IconButton>} {false && <IconButton color="inherit" size="large" edge="start" onClick={(ev) => setPriorityAnchorEl(ev.currentTarget)}> <img src={priorityFiles[priority]}/> </IconButton>} <Menu anchorEl={priorityAnchorEl} open={priorityMenuOpen} onClose={() => setPriorityAnchorEl(null)} > {[5,4,2,1].map(p => <MenuItem onClick={() => handlePriorityClick(p)}> <ListItemIcon><img src={priorityFiles[p]}/></ListItemIcon> <ListItemText>Priority {p}</ListItemText> </MenuItem>)} </Menu> <TextField autoFocus margin="dense" placeholder="Message" type="text" fullWidth variant="standard" multiline /> <IconButton color="inherit" size="large" edge="end"> <SendIcon/> </IconButton> </Paper> ); }; const DropZone = (props) => { const [showDropZone, setShowDropZone] = useState(false); useEffect(() => { window.addEventListener('dragenter', () => setShowDropZone(true)); }, []); const allowSubmit = () => true; const allowDrag = (e) => { if (allowSubmit()) { e.dataTransfer.dropEffect = 'copy'; e.preventDefault(); } }; const handleDrop = (e) => { e.preventDefault(); setShowDropZone(false); console.log(e.dataTransfer.files[0]); }; if (!showDropZone) { return null; } return ( <Backdrop sx={{ color: '#fff', zIndex: 3500 }} open={showDropZone} onClick={() => setShowDropZone(false)} onDragEnter={allowDrag} onDragOver={allowDrag} onDragLeave={() => setShowDropZone(false)} onDrop={handleDrop} > </Backdrop> ); }; const updateTitle = (newNotificationsCount) => { document.title = (newNotificationsCount > 0) ? `(${newNotificationsCount}) ntfy` : "ntfy"; } export default App;