Continued work on publishing from the web app
This commit is contained in:
		
							parent
							
								
									d5eff0cd34
								
							
						
					
					
						commit
						187c19f3b2
					
				
					 6 changed files with 182 additions and 52 deletions
				
			
		|  | @ -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) => { | ||||||
|  |  | ||||||
							
								
								
									
										136
									
								
								web/src/components/SendDialog.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								web/src/components/SendDialog.js
									
										
									
									
									
										Normal 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; | ||||||
							
								
								
									
										1
									
								
								web/src/img/priority-3.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web/src/img/priority-3.svg
									
										
									
									
									
										Normal 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 | 
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue