Poll on subscribe; test message
This commit is contained in:
		
							parent
							
								
									c57fac283e
								
							
						
					
					
						commit
						415ab57749
					
				
					 5 changed files with 89 additions and 9 deletions
				
			
		
							
								
								
									
										24
									
								
								web/src/app/Api.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								web/src/app/Api.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | ||||||
|  | import {topicUrlJsonPoll, fetchLinesIterator, topicUrl} from "./utils"; | ||||||
|  | 
 | ||||||
|  | class Api { | ||||||
|  |     static async poll(baseUrl, topic) { | ||||||
|  |         const url = topicUrlJsonPoll(baseUrl, topic); | ||||||
|  |         const messages = []; | ||||||
|  |         console.log(`[Api] Polling ${url}`); | ||||||
|  |         for await (let line of fetchLinesIterator(url)) { | ||||||
|  |             messages.push(JSON.parse(line)); | ||||||
|  |         } | ||||||
|  |         return messages.sort((a, b) => { return a.time < b.time ? 1 : -1; }); // Newest first
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     static async publish(baseUrl, topic, message) { | ||||||
|  |         const url = topicUrl(baseUrl, topic); | ||||||
|  |         console.log(`[Api] Publishing message to ${url}`); | ||||||
|  |         await fetch(url, { | ||||||
|  |             method: 'PUT', | ||||||
|  |             body: message | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default Api; | ||||||
|  | @ -2,5 +2,39 @@ export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`; | ||||||
| export const topicUrlWs = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/ws` | export const topicUrlWs = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/ws` | ||||||
|     .replaceAll("https://", "wss://") |     .replaceAll("https://", "wss://") | ||||||
|     .replaceAll("http://", "ws://"); |     .replaceAll("http://", "ws://"); | ||||||
|  | export const topicUrlJson = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/json`; | ||||||
|  | export const topicUrlJsonPoll = (baseUrl, topic) => `${topicUrlJson(baseUrl, topic)}?poll=1`; | ||||||
| export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, ""); | export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, ""); | ||||||
| export const shortTopicUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic)); | export const shortTopicUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic)); | ||||||
|  | 
 | ||||||
|  | // From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
 | ||||||
|  | export async function* fetchLinesIterator(fileURL) { | ||||||
|  |     const utf8Decoder = new TextDecoder('utf-8'); | ||||||
|  |     const response = await fetch(fileURL); | ||||||
|  |     const reader = response.body.getReader(); | ||||||
|  |     let { value: chunk, done: readerDone } = await reader.read(); | ||||||
|  |     chunk = chunk ? utf8Decoder.decode(chunk) : ''; | ||||||
|  | 
 | ||||||
|  |     const re = /\n|\r|\r\n/gm; | ||||||
|  |     let startIndex = 0; | ||||||
|  |     let result; | ||||||
|  | 
 | ||||||
|  |     for (;;) { | ||||||
|  |         let result = re.exec(chunk); | ||||||
|  |         if (!result) { | ||||||
|  |             if (readerDone) { | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             let remainder = chunk.substr(startIndex); | ||||||
|  |             ({ value: chunk, done: readerDone } = await reader.read()); | ||||||
|  |             chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : ''); | ||||||
|  |             startIndex = re.lastIndex = 0; | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|  |         yield chunk.substring(startIndex, result.index); | ||||||
|  |         startIndex = re.lastIndex; | ||||||
|  |     } | ||||||
|  |     if (startIndex < chunk.length) { | ||||||
|  |         yield chunk.substr(startIndex); // last line didn't end in a newline char
 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ import NotificationList from "./NotificationList"; | ||||||
| import DetailSettingsIcon from "./DetailSettingsIcon"; | import DetailSettingsIcon from "./DetailSettingsIcon"; | ||||||
| import theme from "./theme"; | import theme from "./theme"; | ||||||
| import LocalStorage from "../app/Storage"; | import LocalStorage from "../app/Storage"; | ||||||
|  | import Api from "../app/Api"; | ||||||
| 
 | 
 | ||||||
| const drawerWidth = 240; | const drawerWidth = 240; | ||||||
| 
 | 
 | ||||||
|  | @ -107,13 +108,19 @@ const App = () => { | ||||||
|     const [selectedSubscription, setSelectedSubscription] = useState(null); |     const [selectedSubscription, setSelectedSubscription] = useState(null); | ||||||
|     const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false); |     const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false); | ||||||
|     const subscriptionChanged = (subscription) => { |     const subscriptionChanged = (subscription) => { | ||||||
|         setSubscriptions(prev => ({...prev, [subscription.id]: subscription})); // Fake-replace
 |         setSubscriptions(prev => ({...prev, [subscription.id]: subscription})); | ||||||
|     }; |     }; | ||||||
|     const handleSubscribeSubmit = (subscription) => { |     const handleSubscribeSubmit = (subscription) => { | ||||||
|         const connection = new WsConnection(subscription, subscriptionChanged); |         const connection = new WsConnection(subscription, subscriptionChanged); | ||||||
|         setSubscribeDialogOpen(false); |         setSubscribeDialogOpen(false); | ||||||
|         setSubscriptions(prev => ({...prev, [subscription.id]: subscription})); |         setSubscriptions(prev => ({...prev, [subscription.id]: subscription})); | ||||||
|         setConnections(prev => ({...prev, [subscription.id]: connection})); |         setConnections(prev => ({...prev, [subscription.id]: connection})); | ||||||
|  |         setSelectedSubscription(subscription); | ||||||
|  |         Api.poll(subscription.baseUrl, subscription.topic) | ||||||
|  |             .then(messages => { | ||||||
|  |                 messages.forEach(m => subscription.addNotification(m)); | ||||||
|  |                 setSubscriptions(prev => ({...prev, [subscription.id]: subscription})); | ||||||
|  |             }); | ||||||
|         connection.start(); |         connection.start(); | ||||||
|     }; |     }; | ||||||
|     const handleSubscribeCancel = () => { |     const handleSubscribeCancel = () => { | ||||||
|  | @ -124,8 +131,11 @@ const App = () => { | ||||||
|         setSubscriptions(prev => { |         setSubscriptions(prev => { | ||||||
|             const newSubscriptions = {...prev}; |             const newSubscriptions = {...prev}; | ||||||
|             delete newSubscriptions[subscription.id]; |             delete newSubscriptions[subscription.id]; | ||||||
|             if (newSubscriptions.length > 0) { |             const newSubscriptionValues = Object.values(newSubscriptions); | ||||||
|                 setSelectedSubscription(newSubscriptions[0]); |             if (newSubscriptionValues.length > 0) { | ||||||
|  |                 setSelectedSubscription(newSubscriptionValues[0]); | ||||||
|  |             } else { | ||||||
|  |                 setSelectedSubscription(null); | ||||||
|             } |             } | ||||||
|             return newSubscriptions; |             return newSubscriptions; | ||||||
|         }); |         }); | ||||||
|  | @ -184,12 +194,12 @@ const App = () => { | ||||||
|                             noWrap |                             noWrap | ||||||
|                             sx={{ flexGrow: 1 }} |                             sx={{ flexGrow: 1 }} | ||||||
|                         > |                         > | ||||||
|                             {(selectedSubscription != null) ? selectedSubscription.shortUrl() : "ntfy.sh"} |                             {(selectedSubscription !== null) ? selectedSubscription.shortUrl() : "ntfy"} | ||||||
|                         </Typography> |                         </Typography> | ||||||
|                         <DetailSettingsIcon |                         {selectedSubscription !== null && <DetailSettingsIcon | ||||||
|                             subscription={selectedSubscription} |                             subscription={selectedSubscription} | ||||||
|                             onUnsubscribe={handleUnsubscribe} |                             onUnsubscribe={handleUnsubscribe} | ||||||
|                         /> |                         />} | ||||||
|                     </Toolbar> |                     </Toolbar> | ||||||
|                 </AppBar> |                 </AppBar> | ||||||
|                 <Drawer variant="permanent" open={drawerOpen}> |                 <Drawer variant="permanent" open={drawerOpen}> | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import MenuItem from '@mui/material/MenuItem'; | ||||||
| import MenuList from '@mui/material/MenuList'; | import MenuList from '@mui/material/MenuList'; | ||||||
| import IconButton from "@mui/material/IconButton"; | import IconButton from "@mui/material/IconButton"; | ||||||
| import MoreVertIcon from "@mui/icons-material/MoreVert"; | import MoreVertIcon from "@mui/icons-material/MoreVert"; | ||||||
|  | import Api from "../app/Api"; | ||||||
| 
 | 
 | ||||||
| // Originally from https://mui.com/components/menus/#MenuListComposition.js
 | // Originally from https://mui.com/components/menus/#MenuListComposition.js
 | ||||||
| const DetailSettingsIcon = (props) => { | const DetailSettingsIcon = (props) => { | ||||||
|  | @ -23,9 +24,20 @@ const DetailSettingsIcon = (props) => { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         setOpen(false); |         setOpen(false); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const handleUnsubscribe = (event) => { | ||||||
|  |         handleClose(event); | ||||||
|         props.onUnsubscribe(props.subscription); |         props.onUnsubscribe(props.subscription); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  |     const handleSendTestMessage = () => { | ||||||
|  |         const baseUrl = props.subscription.baseUrl; | ||||||
|  |         const topic = props.subscription.topic; | ||||||
|  |         Api.publish(baseUrl, topic, `This is a test notification sent by the ntfy.sh Web UI at ${new Date().toString()}.`); // FIXME result ignored
 | ||||||
|  |         setOpen(false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     function handleListKeyDown(event) { |     function handleListKeyDown(event) { | ||||||
|         if (event.key === 'Tab') { |         if (event.key === 'Tab') { | ||||||
|             event.preventDefault(); |             event.preventDefault(); | ||||||
|  | @ -84,8 +96,8 @@ const DetailSettingsIcon = (props) => { | ||||||
|                                     aria-labelledby="composition-button" |                                     aria-labelledby="composition-button" | ||||||
|                                     onKeyDown={handleListKeyDown} |                                     onKeyDown={handleListKeyDown} | ||||||
|                                 > |                                 > | ||||||
|                                     <MenuItem onClick={handleClose}>Send test notification</MenuItem> |                                     <MenuItem onClick={handleSendTestMessage}>Send test notification</MenuItem> | ||||||
|                                     <MenuItem onClick={handleClose}>Unsubscribe</MenuItem> |                                     <MenuItem onClick={handleUnsubscribe}>Unsubscribe</MenuItem> | ||||||
|                                 </MenuList> |                                 </MenuList> | ||||||
|                             </ClickAwayListener> |                             </ClickAwayListener> | ||||||
|                         </Paper> |                         </Paper> | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ const NotificationItem = (props) => { | ||||||
|             <CardContent> |             <CardContent> | ||||||
|                 <Typography sx={{ fontSize: 14 }} color="text.secondary">{date}</Typography> |                 <Typography sx={{ fontSize: 14 }} color="text.secondary">{date}</Typography> | ||||||
|                 {notification.title && <Typography variant="h5" component="div">{notification.title}</Typography>} |                 {notification.title && <Typography variant="h5" component="div">{notification.title}</Typography>} | ||||||
|                 <Typography variant="body1" gutterBottom>{notification.message}</Typography> |                 <Typography variant="body1" sx={{ whiteSpace: 'pre-line' }}>{notification.message}</Typography> | ||||||
|                 {tags && <Typography sx={{ fontSize: 14 }} color="text.secondary">Tags: {tags}</Typography>} |                 {tags && <Typography sx={{ fontSize: 14 }} color="text.secondary">Tags: {tags}</Typography>} | ||||||
|             </CardContent> |             </CardContent> | ||||||
|         </Card> |         </Card> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue