Continued work on publishing from the web app
parent
d5eff0cd34
commit
187c19f3b2
|
@ -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)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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;
|
|
@ -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 |
Loading…
Reference in New Issue