Routing
This commit is contained in:
		
							parent
							
								
									e7bd3abadc
								
							
						
					
					
						commit
						b5670d9a71
					
				
					 9 changed files with 149 additions and 106 deletions
				
			
		|  | @ -8,11 +8,16 @@ import SubscribeSettings from "./SubscribeSettings"; | |||
| import * as React from "react"; | ||||
| import Box from "@mui/material/Box"; | ||||
| import {topicShortUrl} from "../app/utils"; | ||||
| import {useLocation} from "react-router-dom"; | ||||
| 
 | ||||
| const ActionBar = (props) => { | ||||
|     const title = (props.selectedSubscription) | ||||
|         ? topicShortUrl(props.selectedSubscription.baseUrl, props.selectedSubscription.topic) | ||||
|         : "ntfy"; | ||||
|     const location = useLocation(); | ||||
|     let title = "ntfy"; | ||||
|     if (props.selectedSubscription) { | ||||
|         title = topicShortUrl(props.selectedSubscription.baseUrl, props.selectedSubscription.topic); | ||||
|     } else if (location.pathname === "/settings") { | ||||
|         title = "Settings"; | ||||
|     } | ||||
|     return ( | ||||
|         <AppBar position="fixed" sx={{ | ||||
|             width: '100%', | ||||
|  | @ -36,7 +41,7 @@ const ActionBar = (props) => { | |||
|                 <Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}> | ||||
|                     {title} | ||||
|                 </Typography> | ||||
|                 {props.selectedSubscription !== null && <SubscribeSettings | ||||
|                 {props.selectedSubscription && <SubscribeSettings | ||||
|                     subscription={props.selectedSubscription} | ||||
|                     onUnsubscribe={props.onUnsubscribe} | ||||
|                 />} | ||||
|  |  | |||
|  | @ -6,7 +6,6 @@ import CssBaseline from '@mui/material/CssBaseline'; | |||
| import Toolbar from '@mui/material/Toolbar'; | ||||
| import Notifications from "./Notifications"; | ||||
| import theme from "./theme"; | ||||
| import prefs from "../app/Prefs"; | ||||
| import connectionManager from "../app/ConnectionManager"; | ||||
| import Navigation from "./Navigation"; | ||||
| import ActionBar from "./ActionBar"; | ||||
|  | @ -18,65 +17,64 @@ 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 make default server functional
 | ||||
| // TODO routing
 | ||||
| // TODO embed into ntfy server
 | ||||
| // TODO new notification indicator
 | ||||
| 
 | ||||
| const App = () => { | ||||
|     return ( | ||||
|         <BrowserRouter> | ||||
|             <ThemeProvider theme={theme}> | ||||
|                 <CssBaseline/> | ||||
|                 <Root/> | ||||
|             </ThemeProvider> | ||||
|         </BrowserRouter> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| const Root = () => { | ||||
|     const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); | ||||
|     const [prefsOpen, setPrefsOpen] = useState(false); | ||||
|     const [selectedSubscription, setSelectedSubscription] = useState(null); | ||||
|     const [notificationsGranted, setNotificationsGranted] = useState(notificationManager.granted()); | ||||
|     const subscriptions = useLiveQuery(() => subscriptionManager.all()); | ||||
|     const navigate = useNavigate(); | ||||
|     const location = useLocation(); | ||||
|     const users = useLiveQuery(() => userManager.all()); | ||||
|     const subscriptions = useLiveQuery(() => subscriptionManager.all()); | ||||
|     const [selectedSubscription] = (subscriptions && location) ? subscriptions.filter(s => location.pathname === subscriptionRoute(s)) : []; | ||||
| 
 | ||||
|     const handleSubscriptionClick = async (subscriptionId) => { | ||||
|         const subscription = await subscriptionManager.get(subscriptionId); | ||||
|         setSelectedSubscription(subscription); | ||||
|         setPrefsOpen(false); | ||||
|         navigate(subscriptionRoute(subscription)); | ||||
|     } | ||||
|     const handleSubscribeSubmit = async (subscription) => { | ||||
|         console.log(`[App] New subscription: ${subscription.id}`, subscription); | ||||
|         setSelectedSubscription(subscription); | ||||
|         navigate(subscriptionRoute(subscription)); | ||||
|         handleRequestPermission(); | ||||
|     }; | ||||
|     const handleUnsubscribe = async (subscriptionId) => { | ||||
|         console.log(`[App] Unsubscribing from ${subscriptionId}`); | ||||
|         const newSelected = await subscriptionManager.first(); // May be undefined
 | ||||
|         setSelectedSubscription(newSelected); | ||||
|         if (newSelected) { | ||||
|             navigate(subscriptionRoute(newSelected)); | ||||
|         } | ||||
|     }; | ||||
|     const handleRequestPermission = () => { | ||||
|         notificationManager.maybeRequestPermission(granted => setNotificationsGranted(granted)); | ||||
|     }; | ||||
|     const handlePrefsClick = () => { | ||||
|         setPrefsOpen(true); | ||||
|         setSelectedSubscription(null); | ||||
|     }; | ||||
|     // Define hooks: Note that the order of the hooks is important. The "loading" hooks
 | ||||
|     // must be before the "saving" hooks.
 | ||||
|     useEffect(() => { | ||||
|         poller.startWorker(); | ||||
|         pruner.startWorker(); | ||||
|         const load = async () => { | ||||
|             const subs = await subscriptionManager.all();             // FIXME this is broken
 | ||||
|             const selectedSubscriptionId = await prefs.selectedSubscriptionId(); | ||||
| 
 | ||||
|             // Set selected subscription
 | ||||
|             const maybeSelectedSubscription = subs?.filter(s => s.id = selectedSubscriptionId); | ||||
|             if (maybeSelectedSubscription.length > 0) { | ||||
|                 setSelectedSubscription(maybeSelectedSubscription[0]); | ||||
|             } | ||||
| 
 | ||||
|         }; | ||||
|         setTimeout(() => load(), 5000); | ||||
|     }, [/* initial render */]); | ||||
|     useEffect(() => { | ||||
|         const handleNotification = async (subscriptionId, notification) => { | ||||
|             try { | ||||
|                 const added = await subscriptionManager.addNotification(subscriptionId, notification); | ||||
|                 if (added) { | ||||
|                     const defaultClickAction = (subscription) => setSelectedSubscription(subscription); | ||||
|                     const defaultClickAction = (subscription) => navigate(subscriptionRoute(subscription)); | ||||
|                     await notificationManager.notify(subscriptionId, notification, defaultClickAction) | ||||
|                 } | ||||
|             } catch (e) { | ||||
|  | @ -90,47 +88,37 @@ const App = () => { | |||
|             connectionManager.resetNotificationListener(); | ||||
|         } | ||||
|     }, [/* initial render */]); | ||||
|     useEffect(() => { | ||||
|         connectionManager.refresh(subscriptions, users); // Dangle
 | ||||
|     }, [subscriptions, users]); | ||||
|     useEffect(() => { | ||||
|         const subscriptionId = (selectedSubscription) ? selectedSubscription.id : ""; | ||||
|         prefs.setSelectedSubscriptionId(subscriptionId) | ||||
|     }, [selectedSubscription]); | ||||
| 
 | ||||
|     useEffect(() => { connectionManager.refresh(subscriptions, users) }, [subscriptions, users]); // Dangle!
 | ||||
|     return ( | ||||
|         <ThemeProvider theme={theme}> | ||||
|         <Box sx={{display: 'flex'}}> | ||||
|             <CssBaseline/> | ||||
|             <Box sx={{display: 'flex'}}> | ||||
|                 <CssBaseline/> | ||||
|                 <ActionBar | ||||
|             <ActionBar | ||||
|                 subscriptions={subscriptions} | ||||
|                 selectedSubscription={selectedSubscription} | ||||
|                 onUnsubscribe={handleUnsubscribe} | ||||
|                 onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} | ||||
|             /> | ||||
|             <Box component="nav" sx={{width: {sm: Navigation.width}, flexShrink: {sm: 0}}}> | ||||
|                 <Navigation | ||||
|                     subscriptions={subscriptions} | ||||
|                     selectedSubscription={selectedSubscription} | ||||
|                     onUnsubscribe={handleUnsubscribe} | ||||
|                     mobileDrawerOpen={mobileDrawerOpen} | ||||
|                     notificationsGranted={notificationsGranted} | ||||
|                     onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} | ||||
|                     onSubscriptionClick={handleSubscriptionClick} | ||||
|                     onSubscribeSubmit={handleSubscribeSubmit} | ||||
|                     onRequestPermissionClick={handleRequestPermission} | ||||
|                 /> | ||||
|                 <Box component="nav" sx={{width: {sm: Navigation.width}, flexShrink: {sm: 0}}}> | ||||
|                     <Navigation | ||||
|                         subscriptions={subscriptions} | ||||
|                         selectedSubscription={selectedSubscription} | ||||
|                         mobileDrawerOpen={mobileDrawerOpen} | ||||
|                         notificationsGranted={notificationsGranted} | ||||
|                         prefsOpen={prefsOpen} | ||||
|                         onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} | ||||
|                         onSubscriptionClick={handleSubscriptionClick} | ||||
|                         onSubscribeSubmit={handleSubscribeSubmit} | ||||
|                         onPrefsClick={handlePrefsClick} | ||||
|                         onRequestPermissionClick={handleRequestPermission} | ||||
|                     /> | ||||
|                 </Box> | ||||
|                 <Main> | ||||
|                     <Toolbar/> | ||||
|                     <Content | ||||
|                         subscription={selectedSubscription} | ||||
|                         prefsOpen={prefsOpen} | ||||
|                     /> | ||||
|                 </Main> | ||||
|             </Box> | ||||
|         </ThemeProvider> | ||||
|             <Main> | ||||
|                 <Toolbar/> | ||||
|                 <Routes> | ||||
|                     <Route path="/" element={<NoTopics />} /> | ||||
|                     <Route path="settings" element={<Preferences />} /> | ||||
|                     <Route path=":topic" element={<Notifications subscriptions={subscriptions}/>} /> | ||||
|                 </Routes> | ||||
|             </Main> | ||||
|         </Box> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
|  | @ -154,14 +142,4 @@ const Main = (props) => { | |||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const Content = (props) => { | ||||
|     if (props.prefsOpen) { | ||||
|         return <Preferences/>; | ||||
|     } | ||||
|     if (props.subscription) { | ||||
|         return <Notifications subscription={props.subscription}/>; | ||||
|     } | ||||
|     return <NoTopics/>; | ||||
| }; | ||||
| 
 | ||||
| export default App; | ||||
|  |  | |||
|  | @ -14,8 +14,9 @@ import SubscribeDialog from "./SubscribeDialog"; | |||
| import {Alert, AlertTitle, CircularProgress, ListSubheader} from "@mui/material"; | ||||
| import Button from "@mui/material/Button"; | ||||
| import Typography from "@mui/material/Typography"; | ||||
| import {topicShortUrl} from "../app/utils"; | ||||
| import {subscriptionRoute, topicShortUrl} from "../app/utils"; | ||||
| import {ConnectionState} from "../app/Connection"; | ||||
| import {useLocation, useNavigate} from "react-router-dom"; | ||||
| 
 | ||||
| const navWidth = 240; | ||||
| 
 | ||||
|  | @ -53,6 +54,8 @@ const Navigation = (props) => { | |||
| Navigation.width = navWidth; | ||||
| 
 | ||||
| const NavList = (props) => { | ||||
|     const navigate = useNavigate(); | ||||
|     const location = useLocation(); | ||||
|     const [subscribeDialogKey, setSubscribeDialogKey] = useState(0); | ||||
|     const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false); | ||||
|     const handleSubscribeReset = () => { | ||||
|  | @ -67,39 +70,24 @@ const NavList = (props) => { | |||
|     const showGrantPermissionsBox = props.subscriptions?.length > 0 && !props.notificationsGranted; | ||||
|     return ( | ||||
|         <> | ||||
|             <Toolbar sx={{ | ||||
|                 display: { xs: 'none', sm: 'block' } | ||||
|             }}/> | ||||
|             <List component="nav" sx={{ | ||||
|                 paddingTop: (showGrantPermissionsBox) ? '0' : '' | ||||
|             }}> | ||||
|             <Toolbar sx={{ display: { xs: 'none', sm: 'block' } }}/> | ||||
|             <List component="nav" sx={{ paddingTop: (showGrantPermissionsBox) ? '0' : '' }}> | ||||
|                 {showGrantPermissionsBox && <PermissionAlert onRequestPermissionClick={props.onRequestPermissionClick}/>} | ||||
|                 {showSubscriptionsList && | ||||
|                     <> | ||||
|                         <ListSubheader component="div" id="nested-list-subheader"> | ||||
|                             Subscribed topics | ||||
|                         </ListSubheader> | ||||
|                         <ListSubheader>Subscribed topics</ListSubheader> | ||||
|                         <SubscriptionList | ||||
|                             subscriptions={props.subscriptions} | ||||
|                             selectedSubscription={props.selectedSubscription} | ||||
|                             prefsOpen={props.prefsOpen} | ||||
|                             onSubscriptionClick={props.onSubscriptionClick} | ||||
|                         /> | ||||
|                         <Divider sx={{my: 1}}/> | ||||
|                     </>} | ||||
|                 <ListItemButton | ||||
|                     onClick={props.onPrefsClick} | ||||
|                     selected={props.prefsOpen} | ||||
|                 > | ||||
|                     <ListItemIcon> | ||||
|                         <SettingsIcon/> | ||||
|                     </ListItemIcon> | ||||
|                 <ListItemButton onClick={() => navigate("/settings")} selected={location.pathname === "/settings"}> | ||||
|                     <ListItemIcon><SettingsIcon/></ListItemIcon> | ||||
|                     <ListItemText primary="Settings"/> | ||||
|                 </ListItemButton> | ||||
|                 <ListItemButton onClick={() => setSubscribeDialogOpen(true)}> | ||||
|                     <ListItemIcon> | ||||
|                         <AddIcon/> | ||||
|                     </ListItemIcon> | ||||
|                     <ListItemIcon><AddIcon/></ListItemIcon> | ||||
|                     <ListItemText primary="Add subscription"/> | ||||
|                 </ListItemButton> | ||||
|             </List> | ||||
|  | @ -121,20 +109,20 @@ const SubscriptionList = (props) => { | |||
|                 <SubscriptionItem | ||||
|                     key={subscription.id} | ||||
|                     subscription={subscription} | ||||
|                     selected={props.selectedSubscription && !props.prefsOpen && props.selectedSubscription.id === subscription.id} | ||||
|                     onClick={() => props.onSubscriptionClick(subscription.id)} | ||||
|                     selected={props.selectedSubscription && props.selectedSubscription.id === subscription.id} | ||||
|             />)} | ||||
|         </> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| const SubscriptionItem = (props) => { | ||||
|     const navigate = useNavigate(); | ||||
|     const subscription = props.subscription; | ||||
|     const icon = (subscription.state === ConnectionState.Connecting) | ||||
|         ? <CircularProgress size="24px"/> | ||||
|         : <ChatBubbleOutlineIcon/>; | ||||
|     return ( | ||||
|         <ListItemButton onClick={props.onClick} selected={props.selected}> | ||||
|         <ListItemButton onClick={() => navigate(subscriptionRoute(subscription))} selected={props.selected}> | ||||
|             <ListItemIcon>{icon}</ListItemIcon> | ||||
|             <ListItemText primary={topicShortUrl(subscription.baseUrl, subscription.topic)}/> | ||||
|         </ListItemButton> | ||||
|  |  | |||
|  | @ -20,12 +20,23 @@ import {useLiveQuery} from "dexie-react-hooks"; | |||
| import Box from "@mui/material/Box"; | ||||
| import Button from "@mui/material/Button"; | ||||
| import subscriptionManager from "../app/SubscriptionManager"; | ||||
| import { useParams } from "react-router-dom"; | ||||
| 
 | ||||
| const Notifications = (props) => { | ||||
|     const params = useParams(); | ||||
|     if (!props.subscriptions) { | ||||
|         return null; | ||||
|     } | ||||
|     const [subscription] = props.subscriptions.filter(s => s.topic === params.topic); | ||||
|     if (!subscription) { | ||||
|         return null; // FIXME
 | ||||
|     } | ||||
|     return <NotificationList subscription={subscription}/>; | ||||
| }; | ||||
| 
 | ||||
| const NotificationList = (props) => { | ||||
|     const subscription = props.subscription; | ||||
|     const notifications = useLiveQuery(() => { | ||||
|         return subscriptionManager.getNotifications(subscription.id); | ||||
|     }, [subscription]); | ||||
|     const notifications = useLiveQuery(() => subscriptionManager.getNotifications(subscription.id), [subscription]); | ||||
|     if (!notifications || notifications.length === 0) { | ||||
|         return <NothingHereYet subscription={subscription}/>; | ||||
|     } | ||||
|  |  | |||
|  | @ -14,7 +14,6 @@ import { | |||
|     useMediaQuery | ||||
| } from "@mui/material"; | ||||
| import Typography from "@mui/material/Typography"; | ||||
| import Paper from "@mui/material/Paper"; | ||||
| import prefs from "../app/Prefs"; | ||||
| import {Paragraph} from "./styles"; | ||||
| import EditIcon from '@mui/icons-material/Edit'; | ||||
|  | @ -33,7 +32,7 @@ import DialogContent from "@mui/material/DialogContent"; | |||
| import DialogActions from "@mui/material/DialogActions"; | ||||
| import userManager from "../app/UserManager"; | ||||
| 
 | ||||
| const Preferences = (props) => { | ||||
| const Preferences = () => { | ||||
|     return ( | ||||
|         <Container maxWidth="md" sx={{marginTop: 3, marginBottom: 3}}> | ||||
|             <Stack spacing={3}> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue