Use another server
parent
17e5af654b
commit
f23c7a2dbf
|
@ -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": {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()}/>
|
||||
|
|
|
@ -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 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 (
|
||||
<>
|
||||
|
|
|
@ -16,6 +16,15 @@ const theme = createTheme({
|
|||
main: '#444',
|
||||
}
|
||||
},
|
||||
components: {
|
||||
MuiListItemIcon: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
minWidth: '36px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default theme;
|
||||
|
|
Loading…
Reference in New Issue