mirror of
https://github.com/binwiederhier/ntfy.git
synced 2024-11-22 11:24:04 +01:00
Support external routes
This commit is contained in:
parent
b5670d9a71
commit
52a55f71e6
10 changed files with 52 additions and 52 deletions
3
web/public/config.js
Normal file
3
web/public/config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
var config = {
|
||||
defaultBaseUrl: 'https://ntfy.sh'
|
||||
};
|
|
@ -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
2
web/src/app/config.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
const config = window.config;
|
||||
export default config;
|
|
@ -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}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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};
|
||||
|
|
Loading…
Reference in a new issue