Use another server
This commit is contained in:
parent
17e5af654b
commit
f23c7a2dbf
9 changed files with 131 additions and 28 deletions
|
@ -15,7 +15,6 @@
|
||||||
"@mui/styles": "^5.4.2",
|
"@mui/styles": "^5.4.2",
|
||||||
"react": "latest",
|
"react": "latest",
|
||||||
"react-dom": "latest",
|
"react-dom": "latest",
|
||||||
"react-router-dom": "^6.2.1",
|
|
||||||
"react-scripts": "^3.0.1"
|
"react-scripts": "^3.0.1"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
<title>ntfy.sh | Send push notifications to your phone via PUT/POST</title>
|
<title>ntfy web</title>
|
||||||
|
|
||||||
<!-- Mobile view -->
|
<!-- Mobile view -->
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
||||||
|
|
|
@ -11,8 +11,12 @@ export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/aut
|
||||||
export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic));
|
export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic));
|
||||||
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
|
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
|
||||||
|
|
||||||
|
export const validUrl = (url) => {
|
||||||
|
return url.match(/^https?:\/\//);
|
||||||
|
}
|
||||||
|
|
||||||
export const validTopic = (topic) => {
|
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)
|
// Format emojis (see emoji.js)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconSubscribeSettings from "./IconSubscribeSettings";
|
import IconSubscribeSettings from "./IconSubscribeSettings";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
|
||||||
const ActionBar = (props) => {
|
const ActionBar = (props) => {
|
||||||
const title = (props.selectedSubscription !== null)
|
const title = (props.selectedSubscription !== null)
|
||||||
|
@ -26,7 +27,11 @@ const ActionBar = (props) => {
|
||||||
>
|
>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<img src="static/img/ntfy.svg" height="28" style={{ marginRight: '10px' }}/>
|
<Box component="img" src="static/img/ntfy.svg" sx={{
|
||||||
|
display: { xs: 'none', sm: 'block' },
|
||||||
|
marginRight: '10px',
|
||||||
|
height: '28px'
|
||||||
|
}}/>
|
||||||
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
|
@ -15,6 +15,7 @@ import ActionBar from "./ActionBar";
|
||||||
import Users from "../app/Users";
|
import Users from "../app/Users";
|
||||||
import notificationManager from "../app/NotificationManager";
|
import notificationManager from "../app/NotificationManager";
|
||||||
import NoTopics from "./NoTopics";
|
import NoTopics from "./NoTopics";
|
||||||
|
import Preferences from "./Preferences";
|
||||||
|
|
||||||
// TODO subscribe dialog:
|
// TODO subscribe dialog:
|
||||||
// - check/use existing user
|
// - check/use existing user
|
||||||
|
@ -26,10 +27,15 @@ const App = () => {
|
||||||
console.log(`[App] Rendering main view`);
|
console.log(`[App] Rendering main view`);
|
||||||
|
|
||||||
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
|
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
|
||||||
|
const [prefsOpen, setPrefsOpen] = useState(false);
|
||||||
const [subscriptions, setSubscriptions] = useState(new Subscriptions());
|
const [subscriptions, setSubscriptions] = useState(new Subscriptions());
|
||||||
const [users, setUsers] = useState(new Users());
|
const [users, setUsers] = useState(new Users());
|
||||||
const [selectedSubscription, setSelectedSubscription] = useState(null);
|
const [selectedSubscription, setSelectedSubscription] = useState(null);
|
||||||
const [notificationsGranted, setNotificationsGranted] = useState(notificationManager.granted());
|
const [notificationsGranted, setNotificationsGranted] = useState(notificationManager.granted());
|
||||||
|
const handleSubscriptionClick = (subscriptionId) => {
|
||||||
|
setSelectedSubscription(subscriptions.get(subscriptionId));
|
||||||
|
setPrefsOpen(false);
|
||||||
|
}
|
||||||
const handleSubscribeSubmit = (subscription, user) => {
|
const handleSubscribeSubmit = (subscription, user) => {
|
||||||
console.log(`[App] New subscription: ${subscription.id}`);
|
console.log(`[App] New subscription: ${subscription.id}`);
|
||||||
if (user !== null) {
|
if (user !== null) {
|
||||||
|
@ -67,6 +73,10 @@ const App = () => {
|
||||||
setNotificationsGranted(granted);
|
setNotificationsGranted(granted);
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
const handlePrefsClick = () => {
|
||||||
|
setPrefsOpen(true);
|
||||||
|
setSelectedSubscription(null);
|
||||||
|
};
|
||||||
const poll = (subscription, user) => {
|
const poll = (subscription, user) => {
|
||||||
const since = subscription.last;
|
const since = subscription.last;
|
||||||
api.poll(subscription.baseUrl, subscription.topic, since, user)
|
api.poll(subscription.baseUrl, subscription.topic, since, user)
|
||||||
|
@ -138,9 +148,11 @@ const App = () => {
|
||||||
selectedSubscription={selectedSubscription}
|
selectedSubscription={selectedSubscription}
|
||||||
mobileDrawerOpen={mobileDrawerOpen}
|
mobileDrawerOpen={mobileDrawerOpen}
|
||||||
notificationsGranted={notificationsGranted}
|
notificationsGranted={notificationsGranted}
|
||||||
|
prefsOpen={prefsOpen}
|
||||||
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
|
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
|
||||||
onSubscriptionClick={(subscriptionId) => setSelectedSubscription(subscriptions.get(subscriptionId))}
|
onSubscriptionClick={handleSubscriptionClick}
|
||||||
onSubscribeSubmit={handleSubscribeSubmit}
|
onSubscribeSubmit={handleSubscribeSubmit}
|
||||||
|
onPrefsClick={handlePrefsClick}
|
||||||
onRequestPermissionClick={handleRequestPermission}
|
onRequestPermissionClick={handleRequestPermission}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -155,18 +167,34 @@ const App = () => {
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900]
|
backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900]
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Toolbar/>
|
<Toolbar/>
|
||||||
{selectedSubscription !== null &&
|
<MainContent
|
||||||
<Notifications
|
subscription={selectedSubscription}
|
||||||
subscription={selectedSubscription}
|
prefsOpen={prefsOpen}
|
||||||
onDeleteNotification={handleDeleteNotification}
|
onDeleteNotification={handleDeleteNotification}
|
||||||
/>}
|
/>
|
||||||
{selectedSubscription == null && <NoTopics />}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MainContent = (props) => {
|
||||||
|
if (props.prefsOpen) {
|
||||||
|
return <Preferences/>;
|
||||||
|
}
|
||||||
|
if (props.subscription !== null) {
|
||||||
|
return (
|
||||||
|
<Notifications
|
||||||
|
subscription={props.subscription}
|
||||||
|
onDeleteNotification={props.onDeleteNotification}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <NoTopics/>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
@ -14,6 +14,7 @@ import SubscribeDialog from "./SubscribeDialog";
|
||||||
import {Alert, AlertTitle, ListSubheader} from "@mui/material";
|
import {Alert, AlertTitle, 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 Preferences from "./Preferences";
|
||||||
|
|
||||||
const navWidth = 240;
|
const navWidth = 240;
|
||||||
|
|
||||||
|
@ -97,11 +98,15 @@ const NavList = (props) => {
|
||||||
<SubscriptionList
|
<SubscriptionList
|
||||||
subscriptions={props.subscriptions}
|
subscriptions={props.subscriptions}
|
||||||
selectedSubscription={props.selectedSubscription}
|
selectedSubscription={props.selectedSubscription}
|
||||||
|
prefsOpen={props.prefsOpen}
|
||||||
onSubscriptionClick={props.onSubscriptionClick}
|
onSubscriptionClick={props.onSubscriptionClick}
|
||||||
/>
|
/>
|
||||||
<Divider sx={{my: 1}}/>
|
<Divider sx={{my: 1}}/>
|
||||||
</>}
|
</>}
|
||||||
<ListItemButton>
|
<ListItemButton
|
||||||
|
onClick={props.onPrefsClick}
|
||||||
|
selected={props.prefsOpen}
|
||||||
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<SettingsIcon/>
|
<SettingsIcon/>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
@ -115,7 +120,7 @@ const NavList = (props) => {
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</List>
|
</List>
|
||||||
<SubscribeDialog
|
<SubscribeDialog
|
||||||
key={subscribeDialogKey} // Resets dialog when canceled/closed
|
key={`subscribeDialog${subscribeDialogKey}`} // Resets dialog when canceled/closed
|
||||||
open={subscribeDialogOpen}
|
open={subscribeDialogOpen}
|
||||||
subscriptions={props.subscriptions}
|
subscriptions={props.subscriptions}
|
||||||
onCancel={handleSubscribeReset}
|
onCancel={handleSubscribeReset}
|
||||||
|
@ -132,7 +137,7 @@ const SubscriptionList = (props) => {
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
key={id}
|
key={id}
|
||||||
onClick={() => props.onSubscriptionClick(id)}
|
onClick={() => props.onSubscriptionClick(id)}
|
||||||
selected={props.selectedSubscription && props.selectedSubscription.id === id}
|
selected={props.selectedSubscription && !props.prefsOpen && props.selectedSubscription.id === id}
|
||||||
>
|
>
|
||||||
<ListItemIcon><ChatBubbleOutlineIcon /></ListItemIcon>
|
<ListItemIcon><ChatBubbleOutlineIcon /></ListItemIcon>
|
||||||
<ListItemText primary={subscription.shortUrl()}/>
|
<ListItemText primary={subscription.shortUrl()}/>
|
||||||
|
|
22
web/src/components/Preferences.js
Normal file
22
web/src/components/Preferences.js
Normal file
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<Typography variant="h5">
|
||||||
|
Manage users
|
||||||
|
</Typography>
|
||||||
|
<Card sx={{ minWidth: 275 }}>
|
||||||
|
<CardContent>
|
||||||
|
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.
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Preferences;
|
|
@ -8,10 +8,10 @@ import DialogContent from '@mui/material/DialogContent';
|
||||||
import DialogContentText from '@mui/material/DialogContentText';
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import Subscription from "../app/Subscription";
|
import Subscription from "../app/Subscription";
|
||||||
import {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 {topicUrl, validTopic} from "../app/utils";
|
import {topicUrl, validTopic, validUrl} from "../app/utils";
|
||||||
import useStyles from "./styles";
|
import useStyles from "./styles";
|
||||||
import User from "../app/User";
|
import User from "../app/User";
|
||||||
|
|
||||||
|
@ -19,18 +19,20 @@ const defaultBaseUrl = "http://127.0.0.1"
|
||||||
//const defaultBaseUrl = "https://ntfy.sh"
|
//const defaultBaseUrl = "https://ntfy.sh"
|
||||||
|
|
||||||
const SubscribeDialog = (props) => {
|
const SubscribeDialog = (props) => {
|
||||||
const [baseUrl, setBaseUrl] = useState(defaultBaseUrl); // FIXME
|
const [baseUrl, setBaseUrl] = useState("");
|
||||||
const [topic, setTopic] = useState("");
|
const [topic, setTopic] = useState("");
|
||||||
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 = (baseUrl, topic, user) => {
|
const handleSuccess = (user) => {
|
||||||
const subscription = new Subscription(baseUrl, topic);
|
const actualBaseUrl = (baseUrl) ? baseUrl : defaultBaseUrl; // FIXME
|
||||||
|
const subscription = new Subscription(actualBaseUrl, topic);
|
||||||
props.onSuccess(subscription, user);
|
props.onSuccess(subscription, user);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Dialog open={props.open} onClose={props.onClose} fullScreen={fullScreen}>
|
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
|
||||||
{!showLoginPage && <SubscribePage
|
{!showLoginPage && <SubscribePage
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
|
setBaseUrl={setBaseUrl}
|
||||||
topic={topic}
|
topic={topic}
|
||||||
setTopic={setTopic}
|
setTopic={setTopic}
|
||||||
subscriptions={props.subscriptions}
|
subscriptions={props.subscriptions}
|
||||||
|
@ -49,8 +51,12 @@ const SubscribeDialog = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const SubscribePage = (props) => {
|
const SubscribePage = (props) => {
|
||||||
const baseUrl = props.baseUrl;
|
const [anotherServerVisible, setAnotherServerVisible] = useState(false);
|
||||||
|
const baseUrl = (anotherServerVisible) ? props.baseUrl : defaultBaseUrl;
|
||||||
const topic = props.topic;
|
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 handleSubscribe = async () => {
|
||||||
const success = await api.auth(baseUrl, topic, null);
|
const success = await api.auth(baseUrl, topic, null);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
@ -59,10 +65,21 @@ const SubscribePage = (props) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for anonymous user`);
|
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 handleUseAnotherChanged = (e) => {
|
||||||
const subscribeButtonEnabled = validTopic(props.topic) && !existingTopicUrls.includes(topicUrl(baseUrl, topic));
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<DialogTitle>Subscribe to topic</DialogTitle>
|
<DialogTitle>Subscribe to topic</DialogTitle>
|
||||||
|
@ -75,13 +92,27 @@ const SubscribePage = (props) => {
|
||||||
autoFocus
|
autoFocus
|
||||||
margin="dense"
|
margin="dense"
|
||||||
id="topic"
|
id="topic"
|
||||||
label="Topic name, e.g. phil_alerts"
|
placeholder="Topic name, e.g. phil_alerts"
|
||||||
value={props.topic}
|
value={props.topic}
|
||||||
onChange={ev => props.setTopic(ev.target.value)}
|
onChange={ev => props.setTopic(ev.target.value)}
|
||||||
type="text"
|
type="text"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="standard"
|
variant="standard"
|
||||||
/>
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
sx={{pt: 1}}
|
||||||
|
control={<Checkbox onChange={handleUseAnotherChanged}/>}
|
||||||
|
label="Use another server" />
|
||||||
|
{anotherServerVisible && <Autocomplete
|
||||||
|
freeSolo
|
||||||
|
options={existingBaseUrls}
|
||||||
|
sx={{ maxWidth: 400 }}
|
||||||
|
inputValue={props.baseUrl}
|
||||||
|
onInputChange={(ev, newVal) => props.setBaseUrl(newVal)}
|
||||||
|
renderInput={ (params) =>
|
||||||
|
<TextField {...params} placeholder={defaultBaseUrl} variant="standard"/>
|
||||||
|
}
|
||||||
|
/>}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={props.onCancel}>Cancel</Button>
|
<Button onClick={props.onCancel}>Cancel</Button>
|
||||||
|
@ -96,7 +127,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;
|
const baseUrl = (props.baseUrl) ? props.baseUrl : defaultBaseUrl;
|
||||||
const topic = props.topic;
|
const topic = props.topic;
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
const user = new User(baseUrl, username, password);
|
const user = new User(baseUrl, username, password);
|
||||||
|
@ -107,7 +138,7 @@ const LoginPage = (props) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
|
console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
|
||||||
props.onSuccess(baseUrl, topic, user);
|
props.onSuccess(user);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -16,6 +16,15 @@ const theme = createTheme({
|
||||||
main: '#444',
|
main: '#444',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
|
MuiListItemIcon: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
minWidth: '36px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default theme;
|
export default theme;
|
||||||
|
|
Loading…
Reference in a new issue