diff --git a/ui/public/static/notification-worker.js b/ui/public/static/notification-worker.js new file mode 100644 index 00000000..9d747661 --- /dev/null +++ b/ui/public/static/notification-worker.js @@ -0,0 +1,61 @@ +var gotifyKey = undefined + +self.addEventListener("install", event => { + event.waitUntil(new Promise((resolve, reject) => { + try { + // resolves install promise only if search param 'key' was provided + gotifyKey = new URL(location).searchParams.get('key'); + } catch (e) { + reject(e) + } + if (!gotifyKey) { + reject("gotify-login-key not provided") + } + console.log("Worker recieved gotify-login-key, successfully installed") + resolve() + })) +}) + +self.addEventListener("activate", () => { + + console.log("Notification worker activated") + + const host = location.host + const wsProto = location.protocol === "https:" ? "wss:" : "ws:" + const ws = new WebSocket(`${wsProto}//${host}/stream?token=${gotifyKey}`) + console.log("Notification worker connected to websocket, waiting for messages") + + ws.onmessage = (event) => { + + // check if any client is currently visible + // if so, skip sending notification from worker + self.clients.matchAll({ + type: "window", + includeUncontrolled: true + }) + .then((windowClients) => { + var clientVisible = false + for (var i = 0; i < windowClients.length; i++) { + const windowClient = windowClients[i] + // check if a client is visible, then break + if (windowClient.visibilityState === "visible") { + clientVisible = true + break + } + } + return clientVisible + }) // use the bool to evaluate whether to send a notification + .then((clientVisible) => { + if (!clientVisible) { + var msgObj = JSON.parse(event.data) // parse event data, only if not visible + self.registration.showNotification("WORKER: " + msgObj.title, { + body: msgObj.message + }) + } else { + console.log("not sending worker notification, as gotify window is visible") + } + }) + + } + +}) \ No newline at end of file diff --git a/ui/src/CurrentUser.ts b/ui/src/CurrentUser.ts index 6de4edb4..ea6eacf8 100644 --- a/ui/src/CurrentUser.ts +++ b/ui/src/CurrentUser.ts @@ -5,6 +5,7 @@ import {detect} from 'detect-browser'; import {SnackReporter} from './snack/SnackManager'; import {observable} from 'mobx'; import {IClient, IUser} from './types'; +import {registerNotificationWorker, unregisterNotificationWorker} from './registerServiceWorker'; const tokenKey = 'gotify-login-key'; @@ -78,12 +79,13 @@ export class CurrentUser { headers: {Authorization: 'Basic ' + Base64.encode(username + ':' + password)}, }) .then((resp: AxiosResponse) => { - this.snack(`A client named '${name}' was created for your session.`); + this.snack(`A client named '${name}' and a notification worker was created for your session.`); this.setToken(resp.data.token); this.tryAuthenticate() .then(() => { this.authenticating = false; this.loggedIn = true; + registerNotificationWorker(resp.data.token); }) .catch(() => { this.authenticating = false; @@ -150,6 +152,7 @@ export class CurrentUser { window.localStorage.removeItem(tokenKey); this.tokenCache = null; this.loggedIn = false; + unregisterNotificationWorker(); }; public changePassword = (pass: string) => { diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 80732075..debff565 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -4,7 +4,7 @@ import 'typeface-roboto'; import {initAxios} from './apiAuth'; import * as config from './config'; import Layout from './layout/Layout'; -import {unregister} from './registerServiceWorker'; +import {registerNotificationWorker} from './registerServiceWorker'; import {CurrentUser} from './CurrentUser'; import {AppStore} from './application/AppStore'; import {WebSocketStore} from './message/WebSocketStore'; @@ -61,7 +61,14 @@ const initStores = (): StoreMapping => { registerReactions(stores); - stores.currentUser.tryAuthenticate().catch(() => {}); + stores.currentUser.tryAuthenticate().then(() => { + // always request notification permission when logged in + Notification.requestPermission() + .then(perm => console.log("Notification permissions " + perm)) + .catch(console.error) + // reregister worker + registerNotificationWorker(stores.currentUser.token()); + }).catch(() => {}); window.onbeforeunload = () => { stores.wsStore.close(); @@ -73,5 +80,5 @@ const initStores = (): StoreMapping => { , document.getElementById('root') ); - unregister(); + //unregister(); })(); diff --git a/ui/src/registerServiceWorker.ts b/ui/src/registerServiceWorker.ts index 69277f63..e81c4e4e 100644 --- a/ui/src/registerServiceWorker.ts +++ b/ui/src/registerServiceWorker.ts @@ -5,3 +5,36 @@ export function unregister() { }); } } + +export function registerNotificationWorker(key: string) { + if ('serviceWorker' in navigator) { + // provide the gotify-login-key as query parameter, as the service worker cannot access + // localStorage. There is no need to implement a mechanism to update the key as the + // worker will be unregistered on logout. + navigator.serviceWorker.register("static/notification-worker.js?key=" + key, { + scope: "/static/notification-worker" + }) + .catch(console.error) + } else { + console.error("Service workers are not supported in your browser!") + } +} + +export function unregisterNotificationWorker() { + if ('serviceWorker' in navigator) { + // get service worker by scope + navigator.serviceWorker.getRegistration("/static/notification-worker").then((reg) => { + if (reg) { + reg.unregister().then(ok => { // ok: bool === true, if unregister was successfull + if (!ok) { + console.error("Error unregistering service worker") + } + }) + } else { + console.error("Error finding service worker by scope") + } + }) + } else { + console.error("Service workers are not supported in your browser!") + } +} \ No newline at end of file