From 09147cacea6b80d348d4c8364b2815d9b4cac102 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 6 Dec 2018 20:34:00 +0700 Subject: [PATCH] add service worker and push notifications --- src/boot/after_store.js | 5 +- src/modules/users.js | 4 ++ src/services/push/push.js | 96 +++++++++++++++++++++++++++++++++++++++ static/sw.js | 32 +++++++++++++ 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 src/services/push/push.js create mode 100644 static/sw.js diff --git a/src/boot/after_store.js b/src/boot/after_store.js index a80baaf5e7..0c121fe263 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -17,16 +17,17 @@ import FollowRequests from '../components/follow_requests/follow_requests.vue' import OAuthCallback from '../components/oauth_callback/oauth_callback.vue' import UserSearch from '../components/user_search/user_search.vue' -const afterStoreSetup = ({store, i18n}) => { +const afterStoreSetup = ({ store, i18n }) => { window.fetch('/api/statusnet/config.json') .then((res) => res.json()) .then((data) => { - const {name, closed: registrationClosed, textlimit, server} = data.site + const { name, closed: registrationClosed, textlimit, server, vapidPublicKey } = data.site store.dispatch('setInstanceOption', { name: 'name', value: name }) store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') }) store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) }) store.dispatch('setInstanceOption', { name: 'server', value: server }) + store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) var apiConfig = data.site.pleromafe diff --git a/src/modules/users.js b/src/modules/users.js index 8630ee0dd4..791f168071 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -2,6 +2,8 @@ import backendInteractorService from '../services/backend_interactor_service/bac import { compact, map, each, merge } from 'lodash' import { set } from 'vue' +import registerPushNotifications from '../services/push/push.js' + // TODO: Unify with mergeOrAdd in statuses.js export const mergeOrAdd = (arr, obj, item) => { if (!item) { return false } @@ -125,6 +127,8 @@ const users = { // Fetch our friends store.rootState.api.backendInteractor.fetchFriends({id: user.id}) .then((friends) => commit('addNewUsers', friends)) + + registerPushNotifications(store) }) } else { // Authentication failed diff --git a/src/services/push/push.js b/src/services/push/push.js new file mode 100644 index 0000000000..4e4551bfe8 --- /dev/null +++ b/src/services/push/push.js @@ -0,0 +1,96 @@ + +function urlBase64ToUint8Array (base64String) { + const padding = '='.repeat((4 - base64String.length % 4) % 4) + const base64 = (base64String + padding) + .replace(/-/g, '+') + .replace(/_/g, '/') + + const rawData = window.atob(base64) + return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0))) +} + +function isPushSupported () { + return 'serviceWorker' in navigator && 'PushManager' in window +} + +function registerServiceWorker () { + return navigator.serviceWorker.register('/static/sw.js') + .then(function (registration) { + console.log('Service worker successfully registered.') + return registration + }) + .catch(function (err) { + console.error('Unable to register service worker.', err) + }) +} + +function askPermission () { + return new Promise(function (resolve, reject) { + if (!window.Notification) return resolve('Notifications disabled') + + const permissionResult = window.Notification.requestPermission(function (result) { + resolve(result) + }) + + if (permissionResult) permissionResult.then(resolve, reject) + }).then(function (permissionResult) { + if (permissionResult !== 'granted') { + throw new Error('We weren\'t granted permission.') + } + return permissionResult + }) +} + +function subscribe (registration, store) { + const subscribeOptions = { + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(store.rootState.instance.vapidPublicKey) + } + return registration.pushManager.subscribe(subscribeOptions) +} + +function sendSubscriptionToBackEnd (subscription, store) { + return window.fetch('/api/v1/push/subscription/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${store.rootState.oauth.token}` + }, + body: JSON.stringify({ + subscription, + data: { + alerts: { + follow: true, + favourite: true, + mention: true, + reblog: true + } + } + }) + }) + .then(function (response) { + if (!response.ok) { + throw new Error('Bad status code from server.') + } + + return response.json() + }) + .then(function (responseData) { + if (!responseData.id) { + throw new Error('Bad response from server.') + } + return responseData + }) +} + +export default function registerPushNotifications (store) { + if (isPushSupported()) { + registerServiceWorker() + .then(function (registration) { + return askPermission() + .then(() => subscribe(registration, store)) + .then((subscription) => sendSubscriptionToBackEnd(subscription, store)) + .catch((e) => console.warn(`Failed to setup Web Push Notifications: ${e.message}`)) + }) + } +} diff --git a/static/sw.js b/static/sw.js new file mode 100644 index 0000000000..0402a22050 --- /dev/null +++ b/static/sw.js @@ -0,0 +1,32 @@ +/* eslint-env serviceworker */ +self.addEventListener('push', function (event) { + if (event.data) { + const data = event.data.json() + + const promiseChain = clients.matchAll({ + includeUncontrolled: true + }).then(function (clientList) { + const list = clientList.filter((item) => item.type === 'window') + if (list.length) return + return self.registration.showNotification(data.title, data) + }) + + event.waitUntil(promiseChain) + } +}) + +self.addEventListener('notificationclick', function (event) { + event.notification.close() + + event.waitUntil(clients.matchAll({ + includeUncontrolled: true + }).then(function (clientList) { + const list = clientList.filter((item) => item.type === 'window') + + for (var i = 0; i < list.length; i++) { + var client = list[i] + if (client.url === '/' && 'focus' in client) { return client.focus() } + } + if (clients.openWindow) { return clients.openWindow('/') } + })) +})