Use another server

This commit is contained in:
Philipp Heckel 2022-02-28 16:56:38 -05:00
parent 17e5af654b
commit f23c7a2dbf
9 changed files with 131 additions and 28 deletions

View file

@ -15,7 +15,6 @@
"@mui/styles": "^5.4.2",
"react": "latest",
"react-dom": "latest",
"react-router-dom": "^6.2.1",
"react-scripts": "^3.0.1"
},
"browserslist": {

View file

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<title>ntfy.sh | Send push notifications to your phone via PUT/POST</title>
<title>ntfy web</title>
<!-- Mobile view -->
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">

View file

@ -11,8 +11,12 @@ export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/aut
export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic));
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
export const validUrl = (url) => {
return url.match(/^https?:\/\//);
}
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)

View file

@ -6,6 +6,7 @@ import MenuIcon from "@mui/icons-material/Menu";
import Typography from "@mui/material/Typography";
import IconSubscribeSettings from "./IconSubscribeSettings";
import * as React from "react";
import Box from "@mui/material/Box";
const ActionBar = (props) => {
const title = (props.selectedSubscription !== null)
@ -26,7 +27,11 @@ const ActionBar = (props) => {
>
<MenuIcon />
</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 }}>
{title}
</Typography>

View file

@ -15,6 +15,7 @@ import ActionBar from "./ActionBar";
import Users from "../app/Users";
import notificationManager from "../app/NotificationManager";
import NoTopics from "./NoTopics";
import Preferences from "./Preferences";
// TODO subscribe dialog:
// - check/use existing user
@ -26,10 +27,15 @@ const App = () => {
console.log(`[App] Rendering main view`);
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
const [prefsOpen, setPrefsOpen] = useState(false);
const [subscriptions, setSubscriptions] = useState(new Subscriptions());
const [users, setUsers] = useState(new Users());
const [selectedSubscription, setSelectedSubscription] = useState(null);
const [notificationsGranted, setNotificationsGranted] = useState(notificationManager.granted());
const handleSubscriptionClick = (subscriptionId) => {
setSelectedSubscription(subscriptions.get(subscriptionId));
setPrefsOpen(false);
}
const handleSubscribeSubmit = (subscription, user) => {
console.log(`[App] New subscription: ${subscription.id}`);
if (user !== null) {
@ -67,6 +73,10 @@ const App = () => {
setNotificationsGranted(granted);
})
};
const handlePrefsClick = () => {
setPrefsOpen(true);
setSelectedSubscription(null);
};
const poll = (subscription, user) => {
const since = subscription.last;
api.poll(subscription.baseUrl, subscription.topic, since, user)
@ -138,9 +148,11 @@ const App = () => {
selectedSubscription={selectedSubscription}
mobileDrawerOpen={mobileDrawerOpen}
notificationsGranted={notificationsGranted}
prefsOpen={prefsOpen}
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
onSubscriptionClick={(subscriptionId) => setSelectedSubscription(subscriptions.get(subscriptionId))}
onSubscriptionClick={handleSubscriptionClick}
onSubscribeSubmit={handleSubscribeSubmit}
onPrefsClick={handlePrefsClick}
onRequestPermissionClick={handleRequestPermission}
/>
</Box>
@ -155,18 +167,34 @@ const App = () => {
height: '100vh',
overflow: 'auto',
backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900]
}}>
}}
>
<Toolbar/>
{selectedSubscription !== null &&
<Notifications
subscription={selectedSubscription}
onDeleteNotification={handleDeleteNotification}
/>}
{selectedSubscription == null && <NoTopics />}
<MainContent
subscription={selectedSubscription}
prefsOpen={prefsOpen}
onDeleteNotification={handleDeleteNotification}
/>
</Box>
</Box>
</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;

View file

@ -14,6 +14,7 @@ import SubscribeDialog from "./SubscribeDialog";
import {Alert, AlertTitle, ListSubheader} from "@mui/material";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
import Preferences from "./Preferences";
const navWidth = 240;
@ -97,11 +98,15 @@ const NavList = (props) => {
<SubscriptionList
subscriptions={props.subscriptions}
selectedSubscription={props.selectedSubscription}
prefsOpen={props.prefsOpen}
onSubscriptionClick={props.onSubscriptionClick}
/>
<Divider sx={{my: 1}}/>
</>}
<ListItemButton>
<ListItemButton
onClick={props.onPrefsClick}
selected={props.prefsOpen}
>
<ListItemIcon>
<SettingsIcon/>
</ListItemIcon>
@ -115,7 +120,7 @@ const NavList = (props) => {
</ListItemButton>
</List>
<SubscribeDialog
key={subscribeDialogKey} // Resets dialog when canceled/closed
key={`subscribeDialog${subscribeDialogKey}`} // Resets dialog when canceled/closed
open={subscribeDialogOpen}
subscriptions={props.subscriptions}
onCancel={handleSubscribeReset}
@ -132,7 +137,7 @@ const SubscriptionList = (props) => {
<ListItemButton
key={id}
onClick={() => props.onSubscriptionClick(id)}
selected={props.selectedSubscription && props.selectedSubscription.id === id}
selected={props.selectedSubscription && !props.prefsOpen && props.selectedSubscription.id === id}
>
<ListItemIcon><ChatBubbleOutlineIcon /></ListItemIcon>
<ListItemText primary={subscription.shortUrl()}/>

View 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;

View file

@ -8,10 +8,10 @@ import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import Subscription from "../app/Subscription";
import {useMediaQuery} from "@mui/material";
import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
import theme from "./theme";
import api from "../app/Api";
import {topicUrl, validTopic} from "../app/utils";
import {topicUrl, validTopic, validUrl} from "../app/utils";
import useStyles from "./styles";
import User from "../app/User";
@ -19,18 +19,20 @@ const defaultBaseUrl = "http://127.0.0.1"
//const defaultBaseUrl = "https://ntfy.sh"
const SubscribeDialog = (props) => {
const [baseUrl, setBaseUrl] = useState(defaultBaseUrl); // FIXME
const [baseUrl, setBaseUrl] = useState("");
const [topic, setTopic] = useState("");
const [showLoginPage, setShowLoginPage] = useState(false);
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const handleSuccess = (baseUrl, topic, user) => {
const subscription = new Subscription(baseUrl, topic);
const handleSuccess = (user) => {
const actualBaseUrl = (baseUrl) ? baseUrl : defaultBaseUrl; // FIXME
const subscription = new Subscription(actualBaseUrl, topic);
props.onSuccess(subscription, user);
}
return (
<Dialog open={props.open} onClose={props.onClose} fullScreen={fullScreen}>
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
{!showLoginPage && <SubscribePage
baseUrl={baseUrl}
setBaseUrl={setBaseUrl}
topic={topic}
setTopic={setTopic}
subscriptions={props.subscriptions}
@ -49,8 +51,12 @@ const SubscribeDialog = (props) => {
};
const SubscribePage = (props) => {
const baseUrl = props.baseUrl;
const [anotherServerVisible, setAnotherServerVisible] = useState(false);
const baseUrl = (anotherServerVisible) ? props.baseUrl : defaultBaseUrl;
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 success = await api.auth(baseUrl, topic, null);
if (!success) {
@ -59,10 +65,21 @@ const SubscribePage = (props) => {
return;
}
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 subscribeButtonEnabled = validTopic(props.topic) && !existingTopicUrls.includes(topicUrl(baseUrl, topic));
const handleUseAnotherChanged = (e) => {
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 (
<>
<DialogTitle>Subscribe to topic</DialogTitle>
@ -75,13 +92,27 @@ const SubscribePage = (props) => {
autoFocus
margin="dense"
id="topic"
label="Topic name, e.g. phil_alerts"
placeholder="Topic name, e.g. phil_alerts"
value={props.topic}
onChange={ev => props.setTopic(ev.target.value)}
type="text"
fullWidth
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>
<DialogActions>
<Button onClick={props.onCancel}>Cancel</Button>
@ -96,7 +127,7 @@ const LoginPage = (props) => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [errorText, setErrorText] = useState("");
const baseUrl = props.baseUrl;
const baseUrl = (props.baseUrl) ? props.baseUrl : defaultBaseUrl;
const topic = props.topic;
const handleLogin = async () => {
const user = new User(baseUrl, username, password);
@ -107,7 +138,7 @@ const LoginPage = (props) => {
return;
}
console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
props.onSuccess(baseUrl, topic, user);
props.onSuccess(user);
};
return (
<>

View file

@ -16,6 +16,15 @@ const theme = createTheme({
main: '#444',
}
},
components: {
MuiListItemIcon: {
styleOverrides: {
root: {
minWidth: '36px',
},
},
},
},
});
export default theme;