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">
<head>
<meta charset="UTF-8">
<title>ntfy web</title>
<!-- Mobile view -->
@ -24,11 +23,14 @@
<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: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" />
<!-- FIXME Never index topic page -->
<!-- <meta name="robots" content="noindex, nofollow" /> -->
<!-- Never index -->
<meta name="robots" content="noindex, nofollow" />
<!-- Server configuration -->
<script src="%PUBLIC_URL%/config.js"></script>
<!-- FIXME Roboto -->
<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 config from "./config";
export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`;
export const topicUrlWs = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/ws`
@ -115,6 +116,9 @@ export const openUrl = (url) => {
};
export const subscriptionRoute = (subscription) => {
if (subscription.baseUrl !== config.defaultBaseUrl) {
return `/${shortUrl(subscription.baseUrl)}/${subscription.topic}`;
}
return `/${subscription.topic}`;
}

View file

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

View file

@ -20,9 +20,11 @@ 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 support unsubscribed routes
// TODO embed into ntfy server
// TODO googlefonts
// TODO new notification indicator
// TODO sound
const App = () => {
return (
@ -42,7 +44,7 @@ const Root = () => {
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 selectedSubscription = findSelected(location, subscriptions);
const handleSubscriptionClick = async (subscriptionId) => {
const subscription = await subscriptionManager.get(subscriptionId);
@ -74,7 +76,7 @@ const Root = () => {
try {
const added = await subscriptionManager.addNotification(subscriptionId, notification);
if (added) {
const defaultClickAction = (subscription) => navigate(subscriptionRoute(subscription));
const defaultClickAction = (subscription) => navigate(subscriptionRoute(subscription)); // FIXME
await notificationManager.notify(subscriptionId, notification, defaultClickAction)
}
} catch (e) {
@ -115,7 +117,8 @@ const Root = () => {
<Routes>
<Route path="/" element={<NoTopics />} />
<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>
</Main>
</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;

View file

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

View file

@ -20,19 +20,14 @@ 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) {
const subscription = props.subscription;
if (!subscription) {
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;

View file

@ -37,7 +37,6 @@ const Preferences = () => {
<Container maxWidth="md" sx={{marginTop: 3, marginBottom: 3}}>
<Stack spacing={3}>
<Notifications/>
<DefaultServer/>
<Users/>
</Stack>
</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 [dialogKey, setDialogKey] = useState(0);
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 theme from "./theme";
import api from "../app/Api";
import config from "../app/config";
import {topicUrl, validTopic, validUrl} from "../app/utils";
import Box from "@mui/material/Box";
import userManager from "../app/UserManager";
@ -17,8 +18,6 @@ import subscriptionManager from "../app/SubscriptionManager";
import poller from "../app/Poller";
const publicBaseUrl = "https://ntfy.sh"
const defaultBaseUrl = "http://127.0.0.1"
//const defaultBaseUrl = "https://ntfy.sh"
const SubscribeDialog = (props) => {
const [baseUrl, setBaseUrl] = useState("");
@ -26,7 +25,7 @@ const SubscribeDialog = (props) => {
const [showLoginPage, setShowLoginPage] = useState(false);
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const handleSuccess = async () => {
const actualBaseUrl = (baseUrl) ? baseUrl : defaultBaseUrl; // FIXME
const actualBaseUrl = (baseUrl) ? baseUrl : config.defaultBaseUrl;
const subscription = {
id: topicUrl(actualBaseUrl, topic),
baseUrl: actualBaseUrl,
@ -62,11 +61,11 @@ const SubscribeDialog = (props) => {
const SubscribePage = (props) => {
const [anotherServerVisible, setAnotherServerVisible] = useState(false);
const [errorText, setErrorText] = useState("");
const baseUrl = (anotherServerVisible) ? props.baseUrl : defaultBaseUrl;
const baseUrl = (anotherServerVisible) ? props.baseUrl : config.defaultBaseUrl;
const topic = props.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)]))
.filter(s => s !== defaultBaseUrl);
.filter(s => s !== config.defaultBaseUrl);
const handleSubscribe = async () => {
const user = await userManager.get(baseUrl); // May be undefined
const username = (user) ? user.username : "anonymous";
@ -93,7 +92,7 @@ const SubscribePage = (props) => {
const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(baseUrl, topic));
return validTopic(topic) && validUrl(baseUrl) && !isExistingTopicUrl;
} else {
const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(defaultBaseUrl, topic)); // FIXME
const isExistingTopicUrl = existingTopicUrls.includes(topicUrl(config.defaultBaseUrl, topic)); // FIXME
return validTopic(topic) && !isExistingTopicUrl;
}
})();
@ -127,7 +126,7 @@ const SubscribePage = (props) => {
inputValue={props.baseUrl}
onInputChange={(ev, newVal) => props.setBaseUrl(newVal)}
renderInput={ (params) =>
<TextField {...params} placeholder={defaultBaseUrl} variant="standard"/>
<TextField {...params} placeholder={config.defaultBaseUrl} variant="standard"/>
}
/>}
</DialogContent>
@ -143,7 +142,7 @@ const LoginPage = (props) => {
const [username, setUsername] = useState("");
const [password, setPassword] = 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 handleLogin = async () => {
const user = {baseUrl, username, password};