Poll on subscribe; test message
parent
c57fac283e
commit
415ab57749
|
@ -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…
Reference in New Issue