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} > - Send test notification - Unsubscribe + Send test notification + Unsubscribe 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}}