Support external routes
parent
b5670d9a71
commit
52a55f71e6
|
@ -0,0 +1,3 @@
|
||||||
|
var config = {
|
||||||
|
defaultBaseUrl: 'https://ntfy.sh'
|
||||||
|
};
|
|
@ -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" />
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
const config = window.config;
|
||||||
|
export default config;
|
|
@ -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}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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};
|
||||||
|
|
Loading…
Reference in New Issue