diff --git a/web/package.json b/web/package.json index 735efcda..c1749e0e 100644 --- a/web/package.json +++ b/web/package.json @@ -15,7 +15,6 @@ "@mui/styles": "^5.4.2", "react": "latest", "react-dom": "latest", - "react-router-dom": "^6.2.1", "react-scripts": "^3.0.1" }, "browserslist": { diff --git a/web/public/index.html b/web/public/index.html index db52435b..c02c0952 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -3,7 +3,7 @@ - ntfy.sh | Send push notifications to your phone via PUT/POST + ntfy web diff --git a/web/src/app/utils.js b/web/src/app/utils.js index ea796618..0c04a27e 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -11,8 +11,12 @@ export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/aut export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic)); export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, ""); +export const validUrl = (url) => { + return url.match(/^https?:\/\//); +} + export const validTopic = (topic) => { - return topic.match(/^([-_a-zA-Z0-9]{1,64})$/) // Regex must match Go & Android app! + return topic.match(/^([-_a-zA-Z0-9]{1,64})$/); // Regex must match Go & Android app! } // Format emojis (see emoji.js) diff --git a/web/src/components/ActionBar.js b/web/src/components/ActionBar.js index 132f47e4..6970f4f4 100644 --- a/web/src/components/ActionBar.js +++ b/web/src/components/ActionBar.js @@ -6,6 +6,7 @@ import MenuIcon from "@mui/icons-material/Menu"; import Typography from "@mui/material/Typography"; import IconSubscribeSettings from "./IconSubscribeSettings"; import * as React from "react"; +import Box from "@mui/material/Box"; const ActionBar = (props) => { const title = (props.selectedSubscription !== null) @@ -26,7 +27,11 @@ const ActionBar = (props) => { > - + {title} diff --git a/web/src/components/App.js b/web/src/components/App.js index e55bd95e..a95e8d70 100644 --- a/web/src/components/App.js +++ b/web/src/components/App.js @@ -15,6 +15,7 @@ import ActionBar from "./ActionBar"; import Users from "../app/Users"; import notificationManager from "../app/NotificationManager"; import NoTopics from "./NoTopics"; +import Preferences from "./Preferences"; // TODO subscribe dialog: // - check/use existing user @@ -26,10 +27,15 @@ const App = () => { console.log(`[App] Rendering main view`); const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false); + const [prefsOpen, setPrefsOpen] = useState(false); const [subscriptions, setSubscriptions] = useState(new Subscriptions()); const [users, setUsers] = useState(new Users()); const [selectedSubscription, setSelectedSubscription] = useState(null); const [notificationsGranted, setNotificationsGranted] = useState(notificationManager.granted()); + const handleSubscriptionClick = (subscriptionId) => { + setSelectedSubscription(subscriptions.get(subscriptionId)); + setPrefsOpen(false); + } const handleSubscribeSubmit = (subscription, user) => { console.log(`[App] New subscription: ${subscription.id}`); if (user !== null) { @@ -67,6 +73,10 @@ const App = () => { setNotificationsGranted(granted); }) }; + const handlePrefsClick = () => { + setPrefsOpen(true); + setSelectedSubscription(null); + }; const poll = (subscription, user) => { const since = subscription.last; api.poll(subscription.baseUrl, subscription.topic, since, user) @@ -138,9 +148,11 @@ const App = () => { selectedSubscription={selectedSubscription} mobileDrawerOpen={mobileDrawerOpen} notificationsGranted={notificationsGranted} + prefsOpen={prefsOpen} onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} - onSubscriptionClick={(subscriptionId) => setSelectedSubscription(subscriptions.get(subscriptionId))} + onSubscriptionClick={handleSubscriptionClick} onSubscribeSubmit={handleSubscribeSubmit} + onPrefsClick={handlePrefsClick} onRequestPermissionClick={handleRequestPermission} /> @@ -155,18 +167,34 @@ const App = () => { height: '100vh', overflow: 'auto', backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900] - }}> + }} + > - {selectedSubscription !== null && - } - {selectedSubscription == null && } + ); } +const MainContent = (props) => { + if (props.prefsOpen) { + return ; + } + if (props.subscription !== null) { + return ( + + ); + } else { + return ; + } +}; + export default App; diff --git a/web/src/components/Navigation.js b/web/src/components/Navigation.js index 940be5d0..26ef22b9 100644 --- a/web/src/components/Navigation.js +++ b/web/src/components/Navigation.js @@ -14,6 +14,7 @@ import SubscribeDialog from "./SubscribeDialog"; import {Alert, AlertTitle, ListSubheader} from "@mui/material"; import Button from "@mui/material/Button"; import Typography from "@mui/material/Typography"; +import Preferences from "./Preferences"; const navWidth = 240; @@ -97,11 +98,15 @@ const NavList = (props) => { } - + @@ -115,7 +120,7 @@ const NavList = (props) => { { props.onSubscriptionClick(id)} - selected={props.selectedSubscription && props.selectedSubscription.id === id} + selected={props.selectedSubscription && !props.prefsOpen && props.selectedSubscription.id === id} > diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js new file mode 100644 index 00000000..eb10b74f --- /dev/null +++ b/web/src/components/Preferences.js @@ -0,0 +1,22 @@ +import * as React from 'react'; +import {CardContent} from "@mui/material"; +import Typography from "@mui/material/Typography"; +import Card from "@mui/material/Card"; + +const Preferences = (props) => { + return ( + <> + + Manage users + + + + You may manage users for your protected topics here. Please note that since this is a client + application only, username and password are stored in the browser's local storage. + + + + ); +}; + +export default Preferences; diff --git a/web/src/components/SubscribeDialog.js b/web/src/components/SubscribeDialog.js index 0b74f725..203aa8b6 100644 --- a/web/src/components/SubscribeDialog.js +++ b/web/src/components/SubscribeDialog.js @@ -8,10 +8,10 @@ import DialogContent from '@mui/material/DialogContent'; import DialogContentText from '@mui/material/DialogContentText'; import DialogTitle from '@mui/material/DialogTitle'; import Subscription from "../app/Subscription"; -import {useMediaQuery} from "@mui/material"; +import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/material"; import theme from "./theme"; import api from "../app/Api"; -import {topicUrl, validTopic} from "../app/utils"; +import {topicUrl, validTopic, validUrl} from "../app/utils"; import useStyles from "./styles"; import User from "../app/User"; @@ -19,18 +19,20 @@ const defaultBaseUrl = "http://127.0.0.1" //const defaultBaseUrl = "https://ntfy.sh" const SubscribeDialog = (props) => { - const [baseUrl, setBaseUrl] = useState(defaultBaseUrl); // FIXME + const [baseUrl, setBaseUrl] = useState(""); const [topic, setTopic] = useState(""); const [showLoginPage, setShowLoginPage] = useState(false); const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - const handleSuccess = (baseUrl, topic, user) => { - const subscription = new Subscription(baseUrl, topic); + const handleSuccess = (user) => { + const actualBaseUrl = (baseUrl) ? baseUrl : defaultBaseUrl; // FIXME + const subscription = new Subscription(actualBaseUrl, topic); props.onSuccess(subscription, user); } return ( - + {!showLoginPage && { }; const SubscribePage = (props) => { - const baseUrl = props.baseUrl; + const [anotherServerVisible, setAnotherServerVisible] = useState(false); + const baseUrl = (anotherServerVisible) ? props.baseUrl : defaultBaseUrl; const topic = props.topic; + const existingTopicUrls = props.subscriptions.map((id, s) => s.url()); + const existingBaseUrls = Array.from(new Set(["https://ntfy.sh", ...props.subscriptions.map((id, s) => s.baseUrl)])) + .filter(s => s !== defaultBaseUrl); const handleSubscribe = async () => { const success = await api.auth(baseUrl, topic, null); if (!success) { @@ -59,10 +65,21 @@ const SubscribePage = (props) => { return; } console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for anonymous user`); - props.onSuccess(baseUrl, topic, null); + props.onSuccess(null); }; - const existingTopicUrls = props.subscriptions.map((id, s) => s.url()); - const subscribeButtonEnabled = validTopic(props.topic) && !existingTopicUrls.includes(topicUrl(baseUrl, topic)); + const handleUseAnotherChanged = (e) => { + props.setBaseUrl(""); + setAnotherServerVisible(e.target.checked); + }; + const subscribeButtonEnabled = (() => { + if (anotherServerVisible) { + const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic)); + return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl; + } else { + const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(defaultBaseUrl, topic)); // FIXME + return validTopic(topic) && !isExistingTopicUrl; + } + })(); return ( <> Subscribe to topic @@ -75,13 +92,27 @@ const SubscribePage = (props) => { autoFocus margin="dense" id="topic" - label="Topic name, e.g. phil_alerts" + placeholder="Topic name, e.g. phil_alerts" value={props.topic} onChange={ev => props.setTopic(ev.target.value)} type="text" fullWidth variant="standard" /> + } + label="Use another server" /> + {anotherServerVisible && props.setBaseUrl(newVal)} + renderInput={ (params) => + + } + />} @@ -96,7 +127,7 @@ const LoginPage = (props) => { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [errorText, setErrorText] = useState(""); - const baseUrl = props.baseUrl; + const baseUrl = (props.baseUrl) ? props.baseUrl : defaultBaseUrl; const topic = props.topic; const handleLogin = async () => { const user = new User(baseUrl, username, password); @@ -107,7 +138,7 @@ const LoginPage = (props) => { return; } console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`); - props.onSuccess(baseUrl, topic, user); + props.onSuccess(user); }; return ( <> diff --git a/web/src/components/theme.js b/web/src/components/theme.js index 669d5e7d..6e850bfb 100644 --- a/web/src/components/theme.js +++ b/web/src/components/theme.js @@ -16,6 +16,15 @@ const theme = createTheme({ main: '#444', } }, + components: { + MuiListItemIcon: { + styleOverrides: { + root: { + minWidth: '36px', + }, + }, + }, + }, }); export default theme;