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` | ||||
|     .replaceAll("https://", "wss://") | ||||
|     .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 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 theme from "./theme"; | ||||
| import LocalStorage from "../app/Storage"; | ||||
| import Api from "../app/Api"; | ||||
| 
 | ||||
| const drawerWidth = 240; | ||||
| 
 | ||||
|  | @ -107,13 +108,19 @@ const App = () => { | |||
|     const [selectedSubscription, setSelectedSubscription] = useState(null); | ||||
|     const [subscribeDialogOpen, setSubscribeDialogOpen] = useState(false); | ||||
|     const subscriptionChanged = (subscription) => { | ||||
|         setSubscriptions(prev => ({...prev, [subscription.id]: subscription})); // Fake-replace
 | ||||
|         setSubscriptions(prev => ({...prev, [subscription.id]: subscription})); | ||||
|     }; | ||||
|     const handleSubscribeSubmit = (subscription) => { | ||||
|         const connection = new WsConnection(subscription, subscriptionChanged); | ||||
|         setSubscribeDialogOpen(false); | ||||
|         setSubscriptions(prev => ({...prev, [subscription.id]: subscription})); | ||||
|         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(); | ||||
|     }; | ||||
|     const handleSubscribeCancel = () => { | ||||
|  | @ -124,8 +131,11 @@ const App = () => { | |||
|         setSubscriptions(prev => { | ||||
|             const newSubscriptions = {...prev}; | ||||
|             delete newSubscriptions[subscription.id]; | ||||
|             if (newSubscriptions.length > 0) { | ||||
|                 setSelectedSubscription(newSubscriptions[0]); | ||||
|             const newSubscriptionValues = Object.values(newSubscriptions); | ||||
|             if (newSubscriptionValues.length > 0) { | ||||
|                 setSelectedSubscription(newSubscriptionValues[0]); | ||||
|             } else { | ||||
|                 setSelectedSubscription(null); | ||||
|             } | ||||
|             return newSubscriptions; | ||||
|         }); | ||||
|  | @ -184,12 +194,12 @@ const App = () => { | |||
|                             noWrap | ||||
|                             sx={{ flexGrow: 1 }} | ||||
|                         > | ||||
|                             {(selectedSubscription != null) ? selectedSubscription.shortUrl() : "ntfy.sh"} | ||||
|                             {(selectedSubscription !== null) ? selectedSubscription.shortUrl() : "ntfy"} | ||||
|                         </Typography> | ||||
|                         <DetailSettingsIcon | ||||
|                         {selectedSubscription !== null && <DetailSettingsIcon | ||||
|                             subscription={selectedSubscription} | ||||
|                             onUnsubscribe={handleUnsubscribe} | ||||
|                         /> | ||||
|                         />} | ||||
|                     </Toolbar> | ||||
|                 </AppBar> | ||||
|                 <Drawer variant="permanent" open={drawerOpen}> | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ import MenuItem from '@mui/material/MenuItem'; | |||
| import MenuList from '@mui/material/MenuList'; | ||||
| import IconButton from "@mui/material/IconButton"; | ||||
| import MoreVertIcon from "@mui/icons-material/MoreVert"; | ||||
| import Api from "../app/Api"; | ||||
| 
 | ||||
| // Originally from https://mui.com/components/menus/#MenuListComposition.js
 | ||||
| const DetailSettingsIcon = (props) => { | ||||
|  | @ -23,9 +24,20 @@ const DetailSettingsIcon = (props) => { | |||
|             return; | ||||
|         } | ||||
|         setOpen(false); | ||||
|     }; | ||||
| 
 | ||||
|     const handleUnsubscribe = (event) => { | ||||
|         handleClose(event); | ||||
|         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) { | ||||
|         if (event.key === 'Tab') { | ||||
|             event.preventDefault(); | ||||
|  | @ -84,8 +96,8 @@ const DetailSettingsIcon = (props) => { | |||
|                                     aria-labelledby="composition-button" | ||||
|                                     onKeyDown={handleListKeyDown} | ||||
|                                 > | ||||
|                                     <MenuItem onClick={handleClose}>Send test notification</MenuItem> | ||||
|                                     <MenuItem onClick={handleClose}>Unsubscribe</MenuItem> | ||||
|                                     <MenuItem onClick={handleSendTestMessage}>Send test notification</MenuItem> | ||||
|                                     <MenuItem onClick={handleUnsubscribe}>Unsubscribe</MenuItem> | ||||
|                                 </MenuList> | ||||
|                             </ClickAwayListener> | ||||
|                         </Paper> | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ const NotificationItem = (props) => { | |||
|             <CardContent> | ||||
|                 <Typography sx={{ fontSize: 14 }} color="text.secondary">{date}</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>} | ||||
|             </CardContent> | ||||
|         </Card> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue