Support external routes

This commit is contained in:
Philipp Heckel 2022-03-05 08:52:52 -05:00
parent b5670d9a71
commit 52a55f71e6
10 changed files with 52 additions and 52 deletions

3
web/public/config.js Normal file
View File

@ -0,0 +1,3 @@
var config = {
defaultBaseUrl: 'https://ntfy.sh'
};

View File

@ -2,7 +2,6 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>ntfy web</title> <title>ntfy web</title>
<!-- Mobile view --> <!-- Mobile view -->
@ -24,11 +23,14 @@
<meta property="og:site_name" content="ntfy.sh" /> <meta property="og:site_name" content="ntfy.sh" />
<meta property="og:title" content="ntfy.sh | Send push notifications to your phone or desktop via PUT/POST" /> <meta property="og:title" content="ntfy.sh | Send push notifications to your phone or desktop via PUT/POST" />
<meta property="og:description" content="ntfy is a simple HTTP-based pub-sub notification service. It allows you to send desktop notifications via scripts from any computer, entirely without signup or cost. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy." /> <meta property="og:description" content="ntfy is a simple HTTP-based pub-sub notification service. It allows you to send desktop notifications via scripts from any computer, entirely without signup or cost. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy." />
<meta property="og:image" content="/static/img/ntfy.png" /> <meta property="og:image" content="%PUBLIC_URL%/static/img/ntfy.png" />
<meta property="og:url" content="https://ntfy.sh" /> <meta property="og:url" content="https://ntfy.sh" />
<!-- FIXME Never index topic page --> <!-- Never index -->
<!-- <meta name="robots" content="noindex, nofollow" /> --> <meta name="robots" content="noindex, nofollow" />
<!-- Server configuration -->
<script src="%PUBLIC_URL%/config.js"></script>
<!-- FIXME Roboto --> <!-- FIXME Roboto -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />

2
web/src/app/config.js Normal file
View File

@ -0,0 +1,2 @@
const config = window.config;
export default config;

View File

@ -1,4 +1,5 @@
import {rawEmojis} from "./emojis"; import {rawEmojis} from "./emojis";
import config from "./config";
export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`; export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`;
export const topicUrlWs = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/ws` export const topicUrlWs = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/ws`
@ -115,6 +116,9 @@ export const openUrl = (url) => {
}; };
export const subscriptionRoute = (subscription) => { export const subscriptionRoute = (subscription) => {
if (subscription.baseUrl !== config.defaultBaseUrl) {
return `/${shortUrl(subscription.baseUrl)}/${subscription.topic}`;
}
return `/${subscription.topic}`; return `/${subscription.topic}`;
} }

View File

@ -33,7 +33,7 @@ const ActionBar = (props) => {
> >
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
<Box component="img" src="static/img/ntfy.svg" sx={{ <Box component="img" src="/static/img/ntfy.svg" sx={{
display: { xs: 'none', sm: 'block' }, display: { xs: 'none', sm: 'block' },
marginRight: '10px', marginRight: '10px',
height: '28px' height: '28px'

View File

@ -20,9 +20,11 @@ import userManager from "../app/UserManager";
import {BrowserRouter, Route, Routes, useLocation, useNavigate} from "react-router-dom"; import {BrowserRouter, Route, Routes, useLocation, useNavigate} from "react-router-dom";
import {subscriptionRoute} from "../app/utils"; import {subscriptionRoute} from "../app/utils";
// TODO make default server functional // TODO support unsubscribed routes
// TODO embed into ntfy server // TODO embed into ntfy server
// TODO googlefonts
// TODO new notification indicator // TODO new notification indicator
// TODO sound
const App = () => { const App = () => {
return ( return (
@ -42,7 +44,7 @@ const Root = () => {
const location = useLocation(); const location = useLocation();
const users = useLiveQuery(() => userManager.all()); const users = useLiveQuery(() => userManager.all());
const subscriptions = useLiveQuery(() => subscriptionManager.all()); const subscriptions = useLiveQuery(() => subscriptionManager.all());
const [selectedSubscription] = (subscriptions && location) ? subscriptions.filter(s => location.pathname === subscriptionRoute(s)) : []; const selectedSubscription = findSelected(location, subscriptions);
const handleSubscriptionClick = async (subscriptionId) => { const handleSubscriptionClick = async (subscriptionId) => {
const subscription = await subscriptionManager.get(subscriptionId); const subscription = await subscriptionManager.get(subscriptionId);
@ -74,7 +76,7 @@ const Root = () => {
try { try {
const added = await subscriptionManager.addNotification(subscriptionId, notification); const added = await subscriptionManager.addNotification(subscriptionId, notification);
if (added) { if (added) {
const defaultClickAction = (subscription) => navigate(subscriptionRoute(subscription)); const defaultClickAction = (subscription) => navigate(subscriptionRoute(subscription)); // FIXME
await notificationManager.notify(subscriptionId, notification, defaultClickAction) await notificationManager.notify(subscriptionId, notification, defaultClickAction)
} }
} catch (e) { } catch (e) {
@ -115,7 +117,8 @@ const Root = () => {
<Routes> <Routes>
<Route path="/" element={<NoTopics />} /> <Route path="/" element={<NoTopics />} />
<Route path="settings" element={<Preferences />} /> <Route path="settings" element={<Preferences />} />
<Route path=":topic" element={<Notifications subscriptions={subscriptions}/>} /> <Route path=":baseUrl/:topic" element={<Notifications subscription={selectedSubscription}/>} />
<Route path=":topic" element={<Notifications subscription={selectedSubscription}/>} />
</Routes> </Routes>
</Main> </Main>
</Box> </Box>
@ -142,4 +145,13 @@ const Main = (props) => {
); );
}; };
const findSelected = (location, subscriptions) => {
if (!subscriptions || !location) {
return null;
}
const [subscription] = subscriptions
.filter(s => location.pathname === subscriptionRoute(s));
return subscription;
};
export default App; export default App;

View File

@ -14,9 +14,10 @@ import SubscribeDialog from "./SubscribeDialog";
import {Alert, AlertTitle, CircularProgress, ListSubheader} from "@mui/material"; import {Alert, AlertTitle, CircularProgress, ListSubheader} from "@mui/material";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import {subscriptionRoute, topicShortUrl} from "../app/utils"; import {subscriptionRoute, topicShortUrl, topicUrl} from "../app/utils";
import {ConnectionState} from "../app/Connection"; import {ConnectionState} from "../app/Connection";
import {useLocation, useNavigate} from "react-router-dom"; import {useLocation, useNavigate} from "react-router-dom";
import config from "../app/config";
const navWidth = 240; const navWidth = 240;
@ -103,9 +104,12 @@ const NavList = (props) => {
}; };
const SubscriptionList = (props) => { const SubscriptionList = (props) => {
const sortedSubscriptions = props.subscriptions.sort( (a, b) => {
return (topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic)) ? -1 : 1;
});
return ( return (
<> <>
{props.subscriptions.map(subscription => {sortedSubscriptions.map(subscription =>
<SubscriptionItem <SubscriptionItem
key={subscription.id} key={subscription.id}
subscription={subscription} subscription={subscription}
@ -121,10 +125,13 @@ const SubscriptionItem = (props) => {
const icon = (subscription.state === ConnectionState.Connecting) const icon = (subscription.state === ConnectionState.Connecting)
? <CircularProgress size="24px"/> ? <CircularProgress size="24px"/>
: <ChatBubbleOutlineIcon/>; : <ChatBubbleOutlineIcon/>;
const label = (subscription.baseUrl === config.defaultBaseUrl)
? subscription.topic
: topicShortUrl(subscription.baseUrl, subscription.topic);
return ( return (
<ListItemButton onClick={() => navigate(subscriptionRoute(subscription))} selected={props.selected}> <ListItemButton onClick={() => navigate(subscriptionRoute(subscription))} selected={props.selected}>
<ListItemIcon>{icon}</ListItemIcon> <ListItemIcon>{icon}</ListItemIcon>
<ListItemText primary={topicShortUrl(subscription.baseUrl, subscription.topic)}/> <ListItemText primary={label}/>
</ListItemButton> </ListItemButton>
); );
}; };

View File

@ -20,19 +20,14 @@ import {useLiveQuery} from "dexie-react-hooks";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import subscriptionManager from "../app/SubscriptionManager"; import subscriptionManager from "../app/SubscriptionManager";
import { useParams } from "react-router-dom";
const Notifications = (props) => { const Notifications = (props) => {
const params = useParams(); const subscription = props.subscription;
if (!props.subscriptions) { if (!subscription) {
return null; return null;
} }
const [subscription] = props.subscriptions.filter(s => s.topic === params.topic);
if (!subscription) {
return null; // FIXME
}
return <NotificationList subscription={subscription}/>; return <NotificationList subscription={subscription}/>;
}; }
const NotificationList = (props) => { const NotificationList = (props) => {
const subscription = props.subscription; const subscription = props.subscription;

View File

@ -37,7 +37,6 @@ const Preferences = () => {
<Container maxWidth="md" sx={{marginTop: 3, marginBottom: 3}}> <Container maxWidth="md" sx={{marginTop: 3, marginBottom: 3}}>
<Stack spacing={3}> <Stack spacing={3}>
<Notifications/> <Notifications/>
<DefaultServer/>
<Users/> <Users/>
</Stack> </Stack>
</Container> </Container>
@ -140,29 +139,6 @@ const Pref = (props) => {
); );
}; };
const DefaultServer = (props) => {
return (
<Card sx={{ padding: 1 }}>
<CardContent>
<Typography variant="h5">
Default server
</Typography>
<Paragraph>
This server is used as a default when adding new topics.
</Paragraph>
<TextField
margin="dense"
id="defaultBaseUrl"
placeholder="https://ntfy.sh"
type="text"
fullWidth
variant="standard"
/>
</CardContent>
</Card>
);
};
const Users = () => { const Users = () => {
const [dialogKey, setDialogKey] = useState(0); const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);

View File

@ -10,6 +10,7 @@ import DialogTitle from '@mui/material/DialogTitle';
import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/material"; import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
import theme from "./theme"; import theme from "./theme";
import api from "../app/Api"; import api from "../app/Api";
import config from "../app/config";
import {topicUrl, validTopic, validUrl} from "../app/utils"; import {topicUrl, validTopic, validUrl} from "../app/utils";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import userManager from "../app/UserManager"; import userManager from "../app/UserManager";
@ -17,8 +18,6 @@ import subscriptionManager from "../app/SubscriptionManager";
import poller from "../app/Poller"; import poller from "../app/Poller";
const publicBaseUrl = "https://ntfy.sh" const publicBaseUrl = "https://ntfy.sh"
const defaultBaseUrl = "http://127.0.0.1"
//const defaultBaseUrl = "https://ntfy.sh"
const SubscribeDialog = (props) => { const SubscribeDialog = (props) => {
const [baseUrl, setBaseUrl] = useState(""); const [baseUrl, setBaseUrl] = useState("");
@ -26,7 +25,7 @@ const SubscribeDialog = (props) => {
const [showLoginPage, setShowLoginPage] = useState(false); const [showLoginPage, setShowLoginPage] = useState(false);
const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const handleSuccess = async () => { const handleSuccess = async () => {
const actualBaseUrl = (baseUrl) ? baseUrl : defaultBaseUrl; // FIXME const actualBaseUrl = (baseUrl) ? baseUrl : config.defaultBaseUrl;
const subscription = { const subscription = {
id: topicUrl(actualBaseUrl, topic), id: topicUrl(actualBaseUrl, topic),
baseUrl: actualBaseUrl, baseUrl: actualBaseUrl,
@ -62,11 +61,11 @@ const SubscribeDialog = (props) => {
const SubscribePage = (props) => { const SubscribePage = (props) => {
const [anotherServerVisible, setAnotherServerVisible] = useState(false); const [anotherServerVisible, setAnotherServerVisible] = useState(false);
const [errorText, setErrorText] = useState(""); const [errorText, setErrorText] = useState("");
const baseUrl = (anotherServerVisible) ? props.baseUrl : defaultBaseUrl; const baseUrl = (anotherServerVisible) ? props.baseUrl : config.defaultBaseUrl;
const topic = props.topic; const topic = props.topic;
const existingTopicUrls = props.subscriptions.map(s => topicUrl(s.baseUrl, s.topic)); const existingTopicUrls = props.subscriptions.map(s => topicUrl(s.baseUrl, s.topic));
const existingBaseUrls = Array.from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)])) const existingBaseUrls = Array.from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)]))
.filter(s => s !== defaultBaseUrl); .filter(s => s !== config.defaultBaseUrl);
const handleSubscribe = async () => { const handleSubscribe = async () => {
const user = await userManager.get(baseUrl); // May be undefined const user = await userManager.get(baseUrl); // May be undefined
const username = (user) ? user.username : "anonymous"; const username = (user) ? user.username : "anonymous";
@ -93,7 +92,7 @@ const SubscribePage = (props) => {
const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic)); const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic));
return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl; return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl;
} else { } else {
const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(defaultBaseUrl, topic)); // FIXME const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(config.defaultBaseUrl, topic)); // FIXME
return validTopic(topic) && !isExistingTopicUrl; return validTopic(topic) && !isExistingTopicUrl;
} }
})(); })();
@ -127,7 +126,7 @@ const SubscribePage = (props) => {
inputValue={props.baseUrl} inputValue={props.baseUrl}
onInputChange={(ev, newVal) => props.setBaseUrl(newVal)} onInputChange={(ev, newVal) => props.setBaseUrl(newVal)}
renderInput={ (params) => renderInput={ (params) =>
<TextField {...params} placeholder={defaultBaseUrl} variant="standard"/> <TextField {...params} placeholder={config.defaultBaseUrl} variant="standard"/>
} }
/>} />}
</DialogContent> </DialogContent>
@ -143,7 +142,7 @@ const LoginPage = (props) => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [errorText, setErrorText] = useState(""); const [errorText, setErrorText] = useState("");
const baseUrl = (props.baseUrl) ? props.baseUrl : defaultBaseUrl; const baseUrl = (props.baseUrl) ? props.baseUrl : config.defaultBaseUrl;
const topic = props.topic; const topic = props.topic;
const handleLogin = async () => { const handleLogin = async () => {
const user = {baseUrl, username, password}; const user = {baseUrl, username, password};