Continued work on publishing from the web app

pull/180/head
Philipp Heckel 2022-03-27 09:10:47 -04:00
parent d5eff0cd34
commit 187c19f3b2
6 changed files with 182 additions and 52 deletions

View File

@ -26,23 +26,18 @@ class Api {
return messages; return messages;
} }
async publish(baseUrl, topic, message, title, priority, tags) { async publish(baseUrl, topic, message, options) {
const user = await userManager.get(baseUrl); const user = await userManager.get(baseUrl);
const url = topicUrl(baseUrl, topic); console.log(`[Api] Publishing message to ${topicUrl(baseUrl, topic)}`);
console.log(`[Api] Publishing message to ${url}`);
const headers = {}; const headers = {};
if (title) { const body = {
headers["X-Title"] = title; topic: topic,
} message: message,
if (priority !== 3) { ...options
headers["X-Priority"] = `${priority}`; };
} await fetch(baseUrl, {
if (tags.length > 0) {
headers["X-Tags"] = tags.join(",");
}
await fetch(url, {
method: 'PUT', method: 'PUT',
body: message, body: JSON.stringify(body),
headers: maybeWithBasicAuth(headers, user) headers: maybeWithBasicAuth(headers, user)
}); });
} }

View File

@ -135,7 +135,11 @@ const SettingsIcons = (props) => {
`I'm really excited that you're trying out ntfy. Did you know that there are a few public topics, such as ntfy.sh/stats and ntfy.sh/announcements.`, `I'm really excited that you're trying out ntfy. Did you know that there are a few public topics, such as ntfy.sh/stats and ntfy.sh/announcements.`,
`It's interesting to hear what people use ntfy for. I've heard people talk about using it for so many cool things. What do you use it for?` `It's interesting to hear what people use ntfy for. I've heard people talk about using it for so many cool things. What do you use it for?`
])[0]; ])[0];
api.publish(baseUrl, topic, message, title, priority, tags); api.publish(baseUrl, topic, message, {
title: title,
priority: priority,
tags: tags
});
setOpen(false); setOpen(false);
} }

View File

@ -14,7 +14,7 @@ import {useLiveQuery} from "dexie-react-hooks";
import subscriptionManager from "../app/SubscriptionManager"; import subscriptionManager from "../app/SubscriptionManager";
import userManager from "../app/UserManager"; import userManager from "../app/UserManager";
import {BrowserRouter, Outlet, Route, Routes, useOutletContext, useParams} from "react-router-dom"; import {BrowserRouter, Outlet, Route, Routes, useOutletContext, useParams} from "react-router-dom";
import {expandUrl} from "../app/utils"; import {expandUrl, topicUrl} from "../app/utils";
import ErrorBoundary from "./ErrorBoundary"; import ErrorBoundary from "./ErrorBoundary";
import routes from "./routes"; import routes from "./routes";
import {useAutoSubscribe, useConnectionListeners, useLocalStorageMigration} from "./hooks"; import {useAutoSubscribe, useConnectionListeners, useLocalStorageMigration} from "./hooks";
@ -22,7 +22,6 @@ import {Backdrop, ListItemIcon, ListItemText, Menu} from "@mui/material";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import {MoreVert} from "@mui/icons-material"; import {MoreVert} from "@mui/icons-material";
import InsertEmoticonIcon from "@mui/icons-material/InsertEmoticon";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import SendIcon from "@mui/icons-material/Send"; import SendIcon from "@mui/icons-material/Send";
@ -30,6 +29,8 @@ import priority1 from "../img/priority-1.svg";
import priority2 from "../img/priority-2.svg"; import priority2 from "../img/priority-2.svg";
import priority4 from "../img/priority-4.svg"; import priority4 from "../img/priority-4.svg";
import priority5 from "../img/priority-5.svg"; import priority5 from "../img/priority-5.svg";
import api from "../app/Api";
import SendDialog from "./SendDialog";
// TODO add drag and drop // TODO add drag and drop
// TODO races when two tabs are open // TODO races when two tabs are open
@ -102,7 +103,7 @@ const Layout = () => {
<Toolbar/> <Toolbar/>
<Outlet context={{ subscriptions, selected }}/> <Outlet context={{ subscriptions, selected }}/>
</Main> </Main>
<Sender/> <Sender selected={selected}/>
</Box> </Box>
); );
} }
@ -128,23 +129,17 @@ const Main = (props) => {
); );
}; };
const priorityFiles = {
1: priority1,
2: priority2,
4: priority4,
5: priority5
};
const Sender = (props) => { const Sender = (props) => {
const [priority, setPriority] = useState(5); const [message, setMessage] = useState("");
const [priorityAnchorEl, setPriorityAnchorEl] = React.useState(null); const [sendDialogOpen, setSendDialogOpen] = useState(false);
const priorityMenuOpen = Boolean(priorityAnchorEl); const subscription = props.selected;
const handleSendClick = () => {
const handlePriorityClick = (p) => { api.publish(subscription.baseUrl, subscription.topic, message);
setPriority(p); setMessage("");
setPriorityAnchorEl(null);
}; };
if (!props.selected) {
return null;
}
return ( return (
<Paper <Paper
elevation={3} elevation={3}
@ -158,22 +153,9 @@ const Sender = (props) => {
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]
}} }}
> >
{false && <IconButton color="inherit" size="large" edge="start"> <IconButton color="inherit" size="large" edge="start" onClick={() => setSendDialogOpen(true)}>
<MoreVert/> <MoreVert/>
</IconButton>} </IconButton>
{false && <IconButton color="inherit" size="large" edge="start" onClick={(ev) => setPriorityAnchorEl(ev.currentTarget)}>
<img src={priorityFiles[priority]}/>
</IconButton>}
<Menu
anchorEl={priorityAnchorEl}
open={priorityMenuOpen}
onClose={() => setPriorityAnchorEl(null)}
>
{[5,4,2,1].map(p => <MenuItem onClick={() => handlePriorityClick(p)}>
<ListItemIcon><img src={priorityFiles[p]}/></ListItemIcon>
<ListItemText>Priority {p}</ListItemText>
</MenuItem>)}
</Menu>
<TextField <TextField
autoFocus autoFocus
margin="dense" margin="dense"
@ -181,11 +163,24 @@ const Sender = (props) => {
type="text" type="text"
fullWidth fullWidth
variant="standard" variant="standard"
multiline value={message}
onChange={ev => setMessage(ev.target.value)}
onKeyPress={(ev) => {
if (ev.key === 'Enter') {
ev.preventDefault();
handleSendClick();
}
}}
/> />
<IconButton color="inherit" size="large" edge="end"> <IconButton color="inherit" size="large" edge="end" onClick={handleSendClick}>
<SendIcon/> <SendIcon/>
</IconButton> </IconButton>
<SendDialog
open={sendDialogOpen}
onCancel={() => setSendDialogOpen(false)}
topicUrl={topicUrl(subscription.baseUrl, subscription.topic)}
message={message}
/>
</Paper> </Paper>
); );
}; };

View File

@ -120,13 +120,12 @@ const NotificationList = (props) => {
const NotificationItem = (props) => { const NotificationItem = (props) => {
const notification = props.notification; const notification = props.notification;
const subscriptionId = notification.subscriptionId;
const attachment = notification.attachment; const attachment = notification.attachment;
const date = formatShortDateTime(notification.time); const date = formatShortDateTime(notification.time);
const otherTags = unmatchedTags(notification.tags); const otherTags = unmatchedTags(notification.tags);
const tags = (otherTags.length > 0) ? otherTags.join(', ') : null; const tags = (otherTags.length > 0) ? otherTags.join(', ') : null;
const handleDelete = async () => { const handleDelete = async () => {
console.log(`[Notifications] Deleting notification ${notification.id} from ${subscriptionId}`); console.log(`[Notifications] Deleting notification ${notification.id}`);
await subscriptionManager.deleteNotification(notification.id) await subscriptionManager.deleteNotification(notification.id)
} }
const handleCopy = (s) => { const handleCopy = (s) => {

View File

@ -0,0 +1,136 @@
import * as React from 'react';
import {useState} from 'react';
import {NotificationItem} from "./Notifications";
import theme from "./theme";
import {Link, Rating, useMediaQuery} from "@mui/material";
import TextField from "@mui/material/TextField";
import priority1 from "../img/priority-1.svg";
import priority2 from "../img/priority-2.svg";
import priority3 from "../img/priority-3.svg";
import priority4 from "../img/priority-4.svg";
import priority5 from "../img/priority-5.svg";
import Dialog from "@mui/material/Dialog";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import DialogActions from "@mui/material/DialogActions";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
const priorityFiles = {
1: priority1,
2: priority2,
3: priority3,
4: priority4,
5: priority5
};
function IconContainer(props) {
const { value, ...other } = props;
return <span {...other}><img src={priorityFiles[value]}/></span>;
}
const PrioritySelect = () => {
return (
<Rating
defaultValue={3}
IconContainerComponent={IconContainer}
highlightSelectedOnly
/>
);
}
const SendDialog = (props) => {
const [topicUrl, setTopicUrl] = useState(props.topicUrl);
const [message, setMessage] = useState(props.message || "");
const [title, setTitle] = useState("");
const [tags, setTags] = useState("");
const [click, setClick] = useState("");
const [email, setEmail] = useState("");
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const sendButtonEnabled = (() => {
return true;
})();
const handleSubmit = async () => {
props.onSubmit({
baseUrl: "xx",
username: username,
password: password
})
};
return (
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
<DialogTitle>Publish notification</DialogTitle>
<DialogContent>
<TextField
margin="dense"
label="Topic URL"
value={topicUrl}
onChange={ev => setTopicUrl(ev.target.value)}
type="text"
variant="standard"
fullWidth
required
/>
<TextField
margin="dense"
label="Message"
value={message}
onChange={ev => setMessage(ev.target.value)}
type="text"
variant="standard"
fullWidth
required
autoFocus
multiline
/>
<TextField
margin="dense"
label="Title"
value={title}
onChange={ev => setTitle(ev.target.value)}
type="text"
fullWidth
variant="standard"
/>
<TextField
margin="dense"
label="Tags"
value={tags}
onChange={ev => setTags(ev.target.value)}
type="text"
fullWidth
variant="standard"
/>
<TextField
margin="dense"
label="Click URL"
value={click}
onChange={ev => setClick(ev.target.value)}
type="url"
fullWidth
variant="standard"
/>
<TextField
margin="dense"
label="Email"
value={email}
onChange={ev => setEmail(ev.target.value)}
type="email"
fullWidth
variant="standard"
/>
<PrioritySelect/>
<Typography variant="body1">
For details on what these fields mean, please check out the
{" "}<Link href="/docs">documentation</Link>.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={props.onCancel}>Cancel</Button>
<Button onClick={handleSubmit} disabled={!sendButtonEnabled}>Send</Button>
</DialogActions>
</Dialog>
);
};
export default SendDialog;

View File

@ -0,0 +1 @@
<svg height="24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M16.137 11.652a4.21 4.21 0 01-4.21 4.209 4.21 4.21 0 01-4.209-4.21 4.21 4.21 0 014.21-4.209 4.21 4.21 0 014.209 4.21z" fill="#999"/></svg>

After

Width:  |  Height:  |  Size: 210 B