Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement service worker for offline notifications #480

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions ui/public/static/notification-worker.js
Original file line number Diff line number Diff line change
@@ -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) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need some reconnect logic here, otherwise the connection can break in the background and then the user doesn't get notifications anymore.


// 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")
}
})

}

})
5 changes: 4 additions & 1 deletion ui/src/CurrentUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -78,12 +79,13 @@ export class CurrentUser {
headers: {Authorization: 'Basic ' + Base64.encode(username + ':' + password)},
})
.then((resp: AxiosResponse<IClient>) => {
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;
Expand Down Expand Up @@ -150,6 +152,7 @@ export class CurrentUser {
window.localStorage.removeItem(tokenKey);
this.tokenCache = null;
this.loggedIn = false;
unregisterNotificationWorker();
};

public changePassword = (pass: string) => {
Expand Down
13 changes: 10 additions & 3 deletions ui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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(() => {});
Comment on lines +64 to +71
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add another button under the enable notifications button, something like "enable background notifications". Should probably only visible if the notification permission was given. Gotify must not request this permission without user interaction, see #264

The button should be a toggle, enabling and disabling the notifications.


window.onbeforeunload = () => {
stores.wsStore.close();
Expand All @@ -73,5 +80,5 @@ const initStores = (): StoreMapping => {
</InjectProvider>,
document.getElementById('root')
);
unregister();
//unregister();
})();
33 changes: 33 additions & 0 deletions ui/src/registerServiceWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!")
}
}