diff --git a/src/converse-notification.js b/src/converse-notification.js index 5787f076ab..516d4b4557 100644 --- a/src/converse-notification.js +++ b/src/converse-notification.js @@ -8,15 +8,39 @@ import st from "@converse/headless/utils/stanza"; import { __ } from '@converse/headless/i18n'; import { _converse, api, converse } from "@converse/headless/converse-core"; -const { Strophe, sizzle } = converse.env; +const { $iq, Strophe, sizzle } = converse.env; const u = converse.env.utils; const supports_html5_notification = "Notification" in window; +function buf2hex(buffer) { // buffer is an ArrayBuffer + return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(''); +} + + +function urlBase64ToUint8Array(base64String) { + var padding = '='.repeat((4 - base64String.length % 4) % 4); + var base64 = (base64String + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + var rawData = window.atob(base64); + var outputArray = new Uint8Array(rawData.length); + + for (var i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; +} + + +Strophe.addNamespace('WEBPUSH', 'urn:xmpp:webpush:0'); + + converse.plugins.add('converse-notification', { - dependencies: ["converse-chatboxes"], + dependencies: ["converse-chatboxes", "converse-disco"], initialize () { /* The initialize function gets called as soon as the plugin is @@ -285,7 +309,23 @@ converse.plugins.add('converse-notification', { } }; - api.listen.on('pluginsInitialized', function () { + _converse.sendWebPushCredentials = function (subscription) { + const iq = $iq({type: 'set'}) + .c('enable', {xmlns: Strophe.NS.WEBPUSH}) + ; + + subscription.getKey('auth'); + subscription.getKey('p256dh'); + + iq.c('endpoint').t(subscription.endpoint).up(); + iq.c('auth').t(buf2hex(subscription.getKey('auth'))).up(); + iq.c('p256dh').t(buf2hex(subscription.getKey('p256dh'))).up(); + + return _converse.api.sendIQ(iq); + }; + + _converse.api.listen.on('pluginsInitialized', function () { + // We only register event handlers after all plugins are // registered, because other plugins might override some of our // handlers. @@ -295,5 +335,50 @@ converse.plugins.add('converse-notification', { api.listen.on('feedback', _converse.handleFeedback); api.listen.on('connected', _converse.requestPermission); }); + + _converse.api.listen.on('connected', async function () { + // only bother continuing if the server supports webpush + if (!(await _converse.api.disco.supports(Strophe.NS.WEBPUSH, _converse.bare_jid))) { + return; + } + + // get the server's VAPID public key from disco + const fields = await _converse.api.disco.getFields(_converse.bare_jid); + let vapid_key = fields.findWhere({'var': "webpush#public-key"})?.attributes.value; + + // XXX: reattaching an existing BOSH session makes disco.getFields() empty?! + if (!vapid_key) { + vapid_key = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhxZpb8yIVc/2hNesGLGAxEakyYy0MqEetjgL7BIOm8ybhVKxapKqNXjXJ+NOO5/b0Z0UuBg/HynGnf0xKKNhBQ=='; + } + + const converted_vapid_key = urlBase64ToUint8Array(vapid_key); + + navigator.serviceWorker.register('./worker.js').then(function(registration) { + console.log('ServiceWorker registration successful with scope: ', registration.scope); + }); + + navigator.serviceWorker.ready + .then(function(registration) { + return registration.pushManager.getSubscription() + .then(subscription => { + if (subscription) { + console.log("existing", subscription); + _converse.sendWebPushCredentials(subscription); + return; + } + return registration.pushManager.subscribe({ + applicationServerKey: converted_vapid_key, + userVisibleOnly: true, + }); + }); + }).then(function(subscription) { + if (subscription) { + console.log("new", subscription); + _converse.sendWebPushCredentials(subscription); + } + }); + }); + + _converse.api.listen.on('addClientFeatures', () => _converse.api.disco.own.features.add(Strophe.NS.WEBPUSH)); } }); diff --git a/worker.js b/worker.js new file mode 100644 index 0000000000..f2c6099627 --- /dev/null +++ b/worker.js @@ -0,0 +1,31 @@ +self.addEventListener('install', function(event) { + event.waitUntil(self.skipWaiting()); +}); + +self.addEventListener('activate', function(event) { + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener('push', function(event) { + const payload = event.data ? event.data.json() : null; + + // if converse is open, it'll create notifications automatically + event.waitUntil( + self.clients.matchAll().then(function(clientList) { + if (!clientList.length) { + self.registration.showNotification("Converse XMPP notification", { + body: "body", + tag: "tag", + requireInteraction: true, + data: { + a: "b" + } + }); + } + }) + ); +}); + +self.addEventListener('notificationclick', function(event) { + return self.clients.openWindow('/'); +});