diff --git a/web/src/app/Api.js b/web/src/app/Api.js
new file mode 100644
index 00000000..f4894152
--- /dev/null
+++ b/web/src/app/Api.js
@@ -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;
diff --git a/web/src/app/utils.js b/web/src/app/utils.js
index b902a7e5..794af812 100644
--- a/web/src/app/utils.js
+++ b/web/src/app/utils.js
@@ -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
+ }
+}
diff --git a/web/src/components/App.js b/web/src/components/App.js
index 16f3484c..acca1e89 100644
--- a/web/src/components/App.js
+++ b/web/src/components/App.js
@@ -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"}
-
+ />}
diff --git a/web/src/components/DetailSettingsIcon.js b/web/src/components/DetailSettingsIcon.js
index e2c067ba..f7bcc615 100644
--- a/web/src/components/DetailSettingsIcon.js
+++ b/web/src/components/DetailSettingsIcon.js
@@ -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}
>
-
-
+
+
diff --git a/web/src/components/NotificationList.js b/web/src/components/NotificationList.js
index c2c02c57..36917c0d 100644
--- a/web/src/components/NotificationList.js
+++ b/web/src/components/NotificationList.js
@@ -26,7 +26,7 @@ const NotificationItem = (props) => {
{date}
{notification.title && {notification.title}}
- {notification.message}
+ {notification.message}
{tags && Tags: {tags}}