Poll on subscribe; test message

This commit is contained in:
Philipp Heckel 2022-02-22 23:22:30 -05:00
parent c57fac283e
commit 415ab57749
5 changed files with 89 additions and 9 deletions

24
web/src/app/Api.js Normal file
View 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;

View file

@ -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
}
}

View file

@ -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}>

View file

@ -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>

View file

@ -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>