diff --git a/web/src/app/Connection.js b/web/src/app/Connection.js index 9e479f94..914fcf45 100644 --- a/web/src/app/Connection.js +++ b/web/src/app/Connection.js @@ -1,6 +1,6 @@ import {shortTopicUrl, topicUrlWs, topicUrlWsWithSince} from "./utils"; -const retryBackoffSeconds = [5, 10, 15, 20, 30, 45, 60, 120]; +const retryBackoffSeconds = [5, 10, 15, 20, 30, 45]; class Connection { constructor(subscriptionId, baseUrl, topic, since, onNotification) { diff --git a/web/src/components/ActionBar.js b/web/src/components/ActionBar.js new file mode 100644 index 00000000..7ecce8e4 --- /dev/null +++ b/web/src/components/ActionBar.js @@ -0,0 +1,36 @@ +import AppBar from "@mui/material/AppBar"; +import Navigation from "./Navigation"; +import Toolbar from "@mui/material/Toolbar"; +import IconButton from "@mui/material/IconButton"; +import MenuIcon from "@mui/icons-material/Menu"; +import Typography from "@mui/material/Typography"; +import IconSubscribeSettings from "./IconSubscribeSettings"; +import * as React from "react"; + +const ActionBar = (props) => { + const title = (props.selectedSubscription !== null) + ? props.selectedSubscription.shortUrl() + : "ntfy"; + return ( + <AppBar position="fixed" sx={{ width: { sm: `calc(100% - ${Navigation.width}px)` }, ml: { sm: `${Navigation.width}px` } }}> + <Toolbar sx={{pr: '24px'}}> + <IconButton + color="inherit" + edge="start" + onClick={props.onMobileDrawerToggle} + sx={{ mr: 2, display: { sm: 'none' } }} + > + <MenuIcon /> + </IconButton> + <Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>{title}</Typography> + {props.selectedSubscription !== null && <IconSubscribeSettings + subscription={props.selectedSubscription} + onClearAll={props.onClearAll} + onUnsubscribe={props.onUnsubscribe} + />} + </Toolbar> + </AppBar> + ); +}; + +export default ActionBar; diff --git a/web/src/components/App.js b/web/src/components/App.js index 63055aa3..51097833 100644 --- a/web/src/components/App.js +++ b/web/src/components/App.js @@ -1,161 +1,17 @@ import * as React from 'react'; import {useEffect, useState} from 'react'; -import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; import {ThemeProvider} from '@mui/material/styles'; import CssBaseline from '@mui/material/CssBaseline'; -import Drawer from '@mui/material/Drawer'; -import AppBar from '@mui/material/AppBar'; import Toolbar from '@mui/material/Toolbar'; -import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline'; -import List from '@mui/material/List'; -import Divider from '@mui/material/Divider'; -import IconButton from '@mui/material/IconButton'; -import MenuIcon from '@mui/icons-material/Menu'; -import ListItemIcon from "@mui/material/ListItemIcon"; -import ListItemText from "@mui/material/ListItemText"; -import ListItemButton from "@mui/material/ListItemButton"; -import SettingsIcon from "@mui/icons-material/Settings"; -import AddIcon from "@mui/icons-material/Add"; -import SubscribeDialog from "./SubscribeDialog"; import NotificationList from "./NotificationList"; -import IconSubscribeSettings from "./IconSubscribeSettings"; import theme from "./theme"; import api from "../app/Api"; import repository from "../app/Repository"; import connectionManager from "../app/ConnectionManager"; import Subscriptions from "../app/Subscriptions"; - -const drawerWidth = 240; - -const NavSubscriptionList = (props) => { - const subscriptions = props.subscriptions; - return ( - <> - {subscriptions.map((id, subscription) => - <NavSubscriptionItem - key={id} - subscription={subscription} - selected={props.selectedSubscription && props.selectedSubscription.id === id} - onClick={() => props.onSubscriptionClick(id)} - />) - } - </> - ); -} - -const NavSubscriptionItem = (props) => { - const subscription = props.subscription; - return ( - <ListItemButton onClick={props.onClick} selected={props.selected}> - <ListItemIcon><ChatBubbleOutlineIcon /></ListItemIcon> - <ListItemText primary={subscription.shortUrl()}/> - </ListItemButton> - ); -} - -const NavList = (props) => { - const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false); - const handleSubscribeSubmit = (subscription) => { - setSubscribeDialogOpen(false); - props.onSubscribeSubmit(subscription); - } - return ( - <> - <Toolbar /> - <Divider /> - <List component="nav"> - <NavSubscriptionList - subscriptions={props.subscriptions} - selectedSubscription={props.selectedSubscription} - onSubscriptionClick={props.onSubscriptionClick} - /> - <Divider sx={{ my: 1 }} /> - <ListItemButton> - <ListItemIcon> - <SettingsIcon /> - </ListItemIcon> - <ListItemText primary="Settings" /> - </ListItemButton> - <ListItemButton onClick={() => setSubscribeDialogOpen(true)}> - <ListItemIcon> - <AddIcon /> - </ListItemIcon> - <ListItemText primary="Add subscription" /> - </ListItemButton> - </List> - <SubscribeDialog - open={subscribeDialogOpen} - onCancel={() => setSubscribeDialogOpen(false)} - onSubmit={handleSubscribeSubmit} - /> - </> - ); -}; - -const ActionBar = (props) => { - const title = (props.selectedSubscription !== null) - ? props.selectedSubscription.shortUrl() - : "ntfy"; - return ( - <AppBar position="fixed" sx={{ width: { sm: `calc(100% - ${drawerWidth}px)` }, ml: { sm: `${drawerWidth}px` } }}> - <Toolbar sx={{pr: '24px'}}> - <IconButton - color="inherit" - edge="start" - onClick={props.onMobileDrawerToggle} - sx={{ mr: 2, display: { sm: 'none' } }} - > - <MenuIcon /> - </IconButton> - <Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>{title}</Typography> - {props.selectedSubscription !== null && <IconSubscribeSettings - subscription={props.selectedSubscription} - onClearAll={props.onClearAll} - onUnsubscribe={props.onUnsubscribe} - />} - </Toolbar> - </AppBar> - ); -}; - -const Sidebar = (props) => { - const navigationList = - <NavList - subscriptions={props.subscriptions} - selectedSubscription={props.selectedSubscription} - onSubscriptionClick={props.onSubscriptionClick} - onSubscribeSubmit={props.onSubscribeSubmit} - />; - return ( - <> - {/* Mobile drawer; only shown if menu icon clicked (mobile open) and display is small */} - <Drawer - variant="temporary" - open={props.mobileDrawerOpen} - onClose={props.onMobileDrawerToggle} - ModalProps={{ keepMounted: true }} // Better open performance on mobile. - sx={{ - display: { xs: 'block', sm: 'none' }, - '& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth }, - }} - > - {navigationList} - </Drawer> - {/* Big screen drawer; persistent, shown if screen is big */} - <Drawer - open - variant="permanent" - sx={{ - display: { xs: 'none', sm: 'block' }, - '& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth }, - }} - > - {navigationList} - </Drawer> - </> - ); -}; +import Navigation from "./Navigation"; +import ActionBar from "./ActionBar"; const App = () => { console.log(`[App] Rendering main view`); @@ -222,8 +78,8 @@ const App = () => { onUnsubscribe={handleUnsubscribe} onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} /> - <Box component="nav" sx={{width: {sm: drawerWidth}, flexShrink: {sm: 0}}}> - <Sidebar + <Box component="nav" sx={{width: {sm: Navigation.width}, flexShrink: {sm: 0}}}> + <Navigation subscriptions={subscriptions} selectedSubscription={selectedSubscription} mobileDrawerOpen={mobileDrawerOpen} @@ -237,7 +93,7 @@ const App = () => { sx={{ flexGrow: 1, p: 3, - width: {sm: `calc(100% - ${drawerWidth}px)`}, + 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] diff --git a/web/src/components/Navigation.js b/web/src/components/Navigation.js new file mode 100644 index 00000000..5d8b6e7e --- /dev/null +++ b/web/src/components/Navigation.js @@ -0,0 +1,121 @@ +import Drawer from "@mui/material/Drawer"; +import * as React from "react"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline"; +import ListItemText from "@mui/material/ListItemText"; +import {useState} from "react"; +import Toolbar from "@mui/material/Toolbar"; +import Divider from "@mui/material/Divider"; +import List from "@mui/material/List"; +import SettingsIcon from "@mui/icons-material/Settings"; +import AddIcon from "@mui/icons-material/Add"; +import SubscribeDialog from "./SubscribeDialog"; + +const navWidth = 240; + +const Navigation = (props) => { + const navigationList = + <NavList + subscriptions={props.subscriptions} + selectedSubscription={props.selectedSubscription} + onSubscriptionClick={props.onSubscriptionClick} + onSubscribeSubmit={props.onSubscribeSubmit} + />; + return ( + <> + {/* Mobile drawer; only shown if menu icon clicked (mobile open) and display is small */} + <Drawer + variant="temporary" + open={props.mobileDrawerOpen} + onClose={props.onMobileDrawerToggle} + ModalProps={{ keepMounted: true }} // Better open performance on mobile. + sx={{ + display: { xs: 'block', sm: 'none' }, + '& .MuiDrawer-paper': { boxSizing: 'border-box', width: navWidth }, + }} + > + {navigationList} + </Drawer> + {/* Big screen drawer; persistent, shown if screen is big */} + <Drawer + open + variant="permanent" + sx={{ + display: { xs: 'none', sm: 'block' }, + '& .MuiDrawer-paper': { boxSizing: 'border-box', width: navWidth }, + }} + > + {navigationList} + </Drawer> + </> + ); +}; +Navigation.width = navWidth; + +const NavList = (props) => { + const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false); + const handleSubscribeSubmit = (subscription) => { + setSubscribeDialogOpen(false); + props.onSubscribeSubmit(subscription); + } + return ( + <> + <Toolbar /> + {props.subscriptions.size() > 0 && + <Divider />} + <List component="nav"> + <NavSubscriptionList + subscriptions={props.subscriptions} + selectedSubscription={props.selectedSubscription} + onSubscriptionClick={props.onSubscriptionClick} + /> + <Divider sx={{ my: 1 }} /> + <ListItemButton> + <ListItemIcon> + <SettingsIcon /> + </ListItemIcon> + <ListItemText primary="Settings" /> + </ListItemButton> + <ListItemButton onClick={() => setSubscribeDialogOpen(true)}> + <ListItemIcon> + <AddIcon /> + </ListItemIcon> + <ListItemText primary="Add subscription" /> + </ListItemButton> + </List> + <SubscribeDialog + open={subscribeDialogOpen} + onCancel={() => setSubscribeDialogOpen(false)} + onSubmit={handleSubscribeSubmit} + /> + </> + ); +}; +const NavSubscriptionList = (props) => { + const subscriptions = props.subscriptions; + return ( + <> + {subscriptions.map((id, subscription) => + <NavSubscriptionItem + key={id} + subscription={subscription} + selected={props.selectedSubscription && props.selectedSubscription.id === id} + onClick={() => props.onSubscriptionClick(id)} + />) + } + </> + ); +} + +const NavSubscriptionItem = (props) => { + const subscription = props.subscription; + return ( + <ListItemButton onClick={props.onClick} selected={props.selected}> + <ListItemIcon><ChatBubbleOutlineIcon /></ListItemIcon> + <ListItemText primary={subscription.shortUrl()}/> + </ListItemButton> + ); +} + +export default Navigation;