diff --git a/web/package-lock.json b/web/package-lock.json index d601ae0..4946e7c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -15,6 +15,7 @@ "dexie-react-hooks": "^1.1.1", "react": "latest", "react-dom": "latest", + "react-router-dom": "^6.2.2", "react-scripts": "^3.0.1" } }, @@ -8333,6 +8334,14 @@ "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" }, + "node_modules/history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "dependencies": { + "@babel/runtime": "^7.7.6" + } + }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -13693,6 +13702,30 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-router": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.2.2.tgz", + "integrity": "sha512-/MbxyLzd7Q7amp4gDOGaYvXwhEojkJD5BtExkuKmj39VEE0m3l/zipf6h2WIB2jyAO0lI6NGETh4RDcktRm4AQ==", + "dependencies": { + "history": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.2.tgz", + "integrity": "sha512-AtYEsAST7bDD4dLSQHDnk/qxWLJdad5t1HFa1qJyUrCeGgEuCSw0VB/27ARbF9Fi/W5598ujvJOm3ujUCVzuYQ==", + "dependencies": { + "history": "^5.2.0", + "react-router": "6.2.2" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.4.tgz", @@ -24061,6 +24094,14 @@ "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" }, + "history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "requires": { + "@babel/runtime": "^7.7.6" + } + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -28321,6 +28362,23 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "react-router": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.2.2.tgz", + "integrity": "sha512-/MbxyLzd7Q7amp4gDOGaYvXwhEojkJD5BtExkuKmj39VEE0m3l/zipf6h2WIB2jyAO0lI6NGETh4RDcktRm4AQ==", + "requires": { + "history": "^5.2.0" + } + }, + "react-router-dom": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.2.tgz", + "integrity": "sha512-AtYEsAST7bDD4dLSQHDnk/qxWLJdad5t1HFa1qJyUrCeGgEuCSw0VB/27ARbF9Fi/W5598ujvJOm3ujUCVzuYQ==", + "requires": { + "history": "^5.2.0", + "react-router": "6.2.2" + } + }, "react-scripts": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.4.tgz", diff --git a/web/package.json b/web/package.json index cdb1046..5f11024 100644 --- a/web/package.json +++ b/web/package.json @@ -16,6 +16,7 @@ "dexie-react-hooks": "^1.1.1", "react": "latest", "react-dom": "latest", + "react-router-dom": "^6.2.2", "react-scripts": "^3.0.1" }, "browserslist": { diff --git a/web/src/app/ConnectionManager.js b/web/src/app/ConnectionManager.js index 50cb4d5..3871c09 100644 --- a/web/src/app/ConnectionManager.js +++ b/web/src/app/ConnectionManager.js @@ -3,7 +3,6 @@ import {sha256} from "./utils"; class ConnectionManager { constructor() { - console.log(`connection manager`) this.connections = new Map(); // ConnectionId -> Connection (hash, see below) this.stateListener = null; // Fired when connection state changes this.notificationListener = null; // Fired when new notifications arrive diff --git a/web/src/app/utils.js b/web/src/app/utils.js index 9eabe1b..9ff26cb 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -114,6 +114,10 @@ export const openUrl = (url) => { window.open(url, "_blank", "noopener,noreferrer"); }; +export const subscriptionRoute = (subscription) => { + return `/${subscription.topic}`; +} + // From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch export async function* fetchLinesIterator(fileURL, headers) { const utf8Decoder = new TextDecoder('utf-8'); diff --git a/web/src/components/ActionBar.js b/web/src/components/ActionBar.js index 5ea4c96..bfad564 100644 --- a/web/src/components/ActionBar.js +++ b/web/src/components/ActionBar.js @@ -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 ( { {title} - {props.selectedSubscription !== null && } diff --git a/web/src/components/App.js b/web/src/components/App.js index 61da87a..a82140d 100644 --- a/web/src/components/App.js +++ b/web/src/components/App.js @@ -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 ( + + + + + + + ); +} + +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 ( - + - - - setMobileDrawerOpen(!mobileDrawerOpen)} + /> + + setMobileDrawerOpen(!mobileDrawerOpen)} + onSubscriptionClick={handleSubscriptionClick} + onSubscribeSubmit={handleSubscribeSubmit} + onRequestPermissionClick={handleRequestPermission} /> - - setMobileDrawerOpen(!mobileDrawerOpen)} - onSubscriptionClick={handleSubscriptionClick} - onSubscribeSubmit={handleSubscribeSubmit} - onPrefsClick={handlePrefsClick} - onRequestPermissionClick={handleRequestPermission} - /> - -
- - -
-
+
+ + + } /> + } /> + } /> + +
+ ); } @@ -154,14 +142,4 @@ const Main = (props) => { ); }; -const Content = (props) => { - if (props.prefsOpen) { - return ; - } - if (props.subscription) { - return ; - } - return ; -}; - export default App; diff --git a/web/src/components/Navigation.js b/web/src/components/Navigation.js index c60c9fc..8c6377b 100644 --- a/web/src/components/Navigation.js +++ b/web/src/components/Navigation.js @@ -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 ( <> - - + + {showGrantPermissionsBox && } {showSubscriptionsList && <> - - Subscribed topics - + Subscribed topics } - - - - + navigate("/settings")} selected={location.pathname === "/settings"}> + setSubscribeDialogOpen(true)}> - - - + @@ -121,20 +109,20 @@ const SubscriptionList = (props) => { 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) ? : ; return ( - + navigate(subscriptionRoute(subscription))} selected={props.selected}> {icon} diff --git a/web/src/components/Notifications.js b/web/src/components/Notifications.js index aced7fe..c45bdfe 100644 --- a/web/src/components/Notifications.js +++ b/web/src/components/Notifications.js @@ -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 ; +}; + +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 ; } diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js index d8695bc..117c272 100644 --- a/web/src/components/Preferences.js +++ b/web/src/components/Preferences.js @@ -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 (