From 7fdf22b206f1b33ec59d8e5c0c3550a4767ccf80 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 21 Oct 2021 15:31:09 -0500 Subject: [PATCH 1/2] ServiceWorker: clean up entry.js and web_push_notifications.js --- app/images/web-push/web-push-icon_expand.png | Bin 0 -> 1380 bytes .../web-push/web-push-icon_favourite.png | Bin 0 -> 1032 bytes app/images/web-push/web-push-icon_reblog.png | Bin 0 -> 811 bytes app/soapbox/service_worker/entry.js | 76 ------------------ .../service_worker/web_push_notifications.js | 39 +++------ webpack/production.js | 2 +- 6 files changed, 10 insertions(+), 107 deletions(-) create mode 100644 app/images/web-push/web-push-icon_expand.png create mode 100644 app/images/web-push/web-push-icon_favourite.png create mode 100644 app/images/web-push/web-push-icon_reblog.png diff --git a/app/images/web-push/web-push-icon_expand.png b/app/images/web-push/web-push-icon_expand.png new file mode 100644 index 0000000000000000000000000000000000000000..5c115c769709f62531aa817b6c5bae8678e3965a GIT binary patch literal 1380 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+yeRz_>QRC&U%V)zC2?MMEM4Dto3h z10$@jB*-tAfsu)sg_Vt+gOiJwk6%zoSX4}0LQ+avPF_J-MO95*SKrXc*u>P#+``hz z+Sbm|$=Suz%iG7-&p#kAC^{xKEk zf<=p$tysNw{f13jw(s1vXWxN?M~|O4b?*Gd%U5sSx^wTrqbJW^ynOxk{m0K=zyJLG z_iE)+cLoNg8=fwXArYK!5B_x)E|p>b@csLaY~|_E9;z=KF3E*Ortv%X^l+Uy)qcjo zdx?U3;}qA8oJ))=cWk`-JOAD1yKnbN-@d!>Z^WLs8{6O4AKaA7{lDv#W$DZ3Mz2rV z$JVTNf5>UEGum|9%Dbm5D{TG-_ZZyl-^;6OeC)k*$Myfx-wU?$Uwo5)B`+-sG+_vmCb$I%`L@$ z_+E?2Z1G$hSnQwFv*;SL!`|Ho&Ey~YPQUev@y(}0W?P+@vL-(YW{71iyTKsz`O3C= z?%i9wb7dUtTeF#8nK^%9wD3Rcd${74V(WLdBkdyB8TK4DSS+Bv()Nv8bMT@SybizT zC2<_M&tuG-dtkA~%@1dTukyx*}GizJk|z ziEY>Bj1>VX4K~g@Gk!E2vgTM}uc0Owv3y6h$@ZfG*&H#;Bf=_XFG^7~I2~cHkZ^ag zMXPF;v%xXR*TNEeManmL7IFDT9SskX%j5qdh=m%?*YjyZL^*y z%vs`zbJiA3e7atvv+PYMP>EwBB6D@HejwN*qzlC${w(!1^_5NT0v+(BE zQ&0VRbEKi^V_om9_5KTczv{jg<5zThuk?D~<_noq_DP>tayrlZ>zrap+RS~e(|2E3 z_)D zfk9tW666=mz{teR!pg?Z$<4#ZFCZu+EFvlZXk=z? zVQpjQ~S4m2uQO1WXD?JY5_^DsFAL&|Ifx z$iVVpW$x{5xpUWF+qmuSx!u?0%O~e-+W#*;p@IL}`S}IPttp~0KU_Z@NM+n4bS&&g z692MEKbOh0P5YT9o+k7%kW0Dy%ZFw=QlreK~Q1@Vt;)KWqQC z`Q6Dq`z36d%K_!hZ%^O%eziOD|HY}#|K7eLRp4U&&iR6axf?^z#XBZ(JQuHo?|Hd! z=J^j9%R4oHYzeq;@y9gix&hPGAb(m5HD2;BBDCL#+`GIm?Z)Z7i zn$r918kfBoOZb*)ZF8gF^O^8r;kcs&6(FYz2N`* zX8J+p>bZ626iQszPqzA@eQTk=1&h+l!h14J3Zd;^JlY+^?#Gn2A98!FdH;&@f=>NB z_d^N#OWro=t0`P?`}8oF<%rYod=EcIg>Ro8Mw>i2q@MTgl7f)XCli+0F|J?O8GP4d VmaGZLSPo2A44$rjF6*2UngG%#(+L0o literal 0 HcmV?d00001 diff --git a/app/images/web-push/web-push-icon_reblog.png b/app/images/web-push/web-push-icon_reblog.png new file mode 100644 index 0000000000000000000000000000000000000000..f70203ec84054a65d8f8800c3ae6fba461599aa4 GIT binary patch literal 811 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+yeRz^D-56XFWwYEQU1ig1O%R`GyP zU>L=h1o;IsFfuW-u(ES-a&hwt3WuHL+J@4=I2FJ8WV|MBzJ z@4x@D)W!Y+MoYA(i(^Q|t+$t0r`TZmenx*O zraGbD*U}Hv_3sLraKoj((bi<|&W^QTg=e@2*K*x%ty5?%n|p zCuu$KbN;ltzu_<6rXmmf4eV3Pr{^U|Wli7JXtIkVgRO7jwC&k!b5^H6e zYx4tVv=%sLg&o*kT6@??fXlq?@P@nxf?1KX(-w5;-I>+My5enC{3Ff~xi_~Mj|-bP0l+XkKw-S1# literal 0 HcmV?d00001 diff --git a/app/soapbox/service_worker/entry.js b/app/soapbox/service_worker/entry.js index 1147f003f..364b67066 100644 --- a/app/soapbox/service_worker/entry.js +++ b/app/soapbox/service_worker/entry.js @@ -1,77 +1 @@ import './web_push_notifications'; - -function openWebCache() { - return caches.open('soapbox-web'); -} - -function fetchRoot() { - return fetch('/', { credentials: 'include', redirect: 'manual' }); -} - -// Cause a new version of a registered Service Worker to replace an existing one -// that is already installed, and replace the currently active worker on open pages. -self.addEventListener('install', function(event) { - event.waitUntil(Promise.all([openWebCache(), fetchRoot()]).then(([cache, root]) => cache.put('/', root))); -}); -self.addEventListener('activate', function(event) { - event.waitUntil(self.clients.claim()); -}); -self.addEventListener('fetch', function(event) { - const url = new URL(event.request.url); - - if (url.pathname === '/auth/sign_out') { - const asyncResponse = fetch(event.request); - const asyncCache = openWebCache(); - - event.respondWith(asyncResponse.then(response => { - if (response.ok || response.type === 'opaqueredirect') { - return Promise.all([ - asyncCache.then(cache => cache.delete('/')), - indexedDB.deleteDatabase('soapbox'), - ]).then(() => response); - } - - return response; - })); - } else if ( - url.pathname.startsWith('/system') || - url.pathname.startsWith('/api') || - url.pathname.startsWith('/settings') || - url.pathname.startsWith('/media') || - url.pathname.startsWith('/admin') || - url.pathname.startsWith('/about') || - url.pathname.startsWith('/auth') || - url.pathname.startsWith('/oauth') || - url.pathname.startsWith('/invites') || - url.pathname.startsWith('/pghero') || - url.pathname.startsWith('/sidekiq') || - url.pathname.startsWith('/filters') || - url.pathname.startsWith('/tags') || - url.pathname.startsWith('/emojis') || - url.pathname.startsWith('/inbox') || - url.pathname.startsWith('/accounts') || - url.pathname.startsWith('/user') || - url.pathname.startsWith('/users') || - url.pathname.startsWith('/src') || - url.pathname.startsWith('/public') || - url.pathname.startsWith('/avatars') || - url.pathname.startsWith('/authorize_follow') || - url.pathname.startsWith('/media_proxy') || - url.pathname.startsWith('/relationships') || - url.pathname.startsWith('/main/ostatus') || - url.pathname.startsWith('/ostatus_subscribe')) { - //non-webapp routes - } else if (url.pathname.startsWith('/')) { - // : TODO : if is /web - const asyncResponse = fetchRoot(); - const asyncCache = openWebCache(); - - event.respondWith(asyncResponse.then( - response => { - const clonedResponse = response.clone(); - asyncCache.then(cache => cache.put('/', clonedResponse)).catch(); - return response; - }, - () => asyncCache.then(cache => cache.match('/')))); - } -}); diff --git a/app/soapbox/service_worker/web_push_notifications.js b/app/soapbox/service_worker/web_push_notifications.js index 00e948251..2b322b325 100644 --- a/app/soapbox/service_worker/web_push_notifications.js +++ b/app/soapbox/service_worker/web_push_notifications.js @@ -12,8 +12,6 @@ const notify = options => const group = { title: formatMessage('notifications.group', options.data.preferred_locale, { count: notifications.length + 1 }), body: notifications.sort((n1, n2) => n1.timestamp < n2.timestamp).map(notification => notification.title).join('\n'), - badge: '/badge.png', - icon: '/android-chrome-192x192.png', tag: GROUP_TAG, data: { url: (new URL('/notifications', self.location)).href, @@ -89,9 +87,8 @@ const handlePush = (event) => { options.icon = notification.account.avatar_static; options.timestamp = notification.created_at && new Date(notification.created_at); options.tag = notification.id; - options.badge = '/badge.png'; options.image = notification.status && notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url || undefined; - options.data = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/${notification.account.username}/posts/${notification.status.id}` : `/${notification.account.username}` }; + options.data = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/@${notification.account.username}/posts/${notification.status.id}` : `/@${notification.account.username}` }; if (notification.status && notification.status.spoiler_text || notification.status.sensitive) { options.data.hiddenBody = htmlToPlainText(notification.status.content); @@ -115,7 +112,6 @@ const handlePush = (event) => { icon, tag: notification_id, timestamp: new Date(), - badge: '/badge.png', data: { access_token, preferred_locale, url: '/notifications' }, }); }), @@ -124,19 +120,19 @@ const handlePush = (event) => { const actionExpand = preferred_locale => ({ action: 'expand', - icon: '/web-push-icon_expand.png', + icon: `/${require('../../images/web-push/web-push-icon_expand.png')}`, title: formatMessage('status.show_more', preferred_locale), }); const actionReblog = preferred_locale => ({ action: 'reblog', - icon: '/web-push-icon_reblog.png', + icon: `/${require('../../images/web-push/web-push-icon_reblog.png')}`, title: formatMessage('status.reblog', preferred_locale), }); const actionFavourite = preferred_locale => ({ action: 'favourite', - icon: '/web-push-icon_favourite.png', + icon: `/${require('../../images/web-push/web-push-icon_favourite.png')}`, title: formatMessage('status.favourite', preferred_locale), }); @@ -167,29 +163,12 @@ const removeActionFromNotification = (notification, action) => { const openUrl = url => self.clients.matchAll({ type: 'window' }).then(clientList => { - if (clientList.length !== 0) { - // : TODO : - const webClients = clientList.filter(client => /\//.test(client.url)); - - if (webClients.length !== 0) { - const client = findBestClient(webClients); - const { pathname } = new URL(url, self.location); - - // : TODO : - if (pathname.startsWith('/')) { - return client.focus().then(client => client.postMessage({ - type: 'navigate', - path: pathname.slice('/'.length - 1), - })); - } - } else if ('navigate' in clientList[0]) { // Chrome 42-48 does not support navigate - const client = findBestClient(clientList); - - return client.navigate(url).then(client => client.focus()); - } + if (clientList.length === 0) { + return self.clients.openWindow(url); + } else { + const client = findBestClient(clientList); + return client.navigate(url).then(client => client.focus()); } - - return self.clients.openWindow(url); }); const handleNotificationClick = (event) => { diff --git a/webpack/production.js b/webpack/production.js index 80defdc34..220ed8991 100644 --- a/webpack/production.js +++ b/webpack/production.js @@ -82,7 +82,7 @@ module.exports = merge(sharedConfig, { ], ServiceWorker: { cacheName: 'soapbox', - // entry: join(__dirname, '../app/soapbox/service_worker/entry.js'), + entry: join(__dirname, '../app/soapbox/service_worker/entry.js'), minify: true, }, cacheMaps: [{ From fc6911caad0ad1da1d357aaa0d971558b0580dd9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 21 Oct 2021 16:22:54 -0500 Subject: [PATCH 2/2] Push notifications: refactor, restore --- .../actions/push_notifications/registerer.js | 154 +++++++++--------- app/soapbox/actions/push_subscriptions.js | 3 +- app/soapbox/features/ui/index.js | 2 + app/soapbox/main.js | 5 - 4 files changed, 82 insertions(+), 82 deletions(-) diff --git a/app/soapbox/actions/push_notifications/registerer.js b/app/soapbox/actions/push_notifications/registerer.js index 91b6cd6a3..156641c2b 100644 --- a/app/soapbox/actions/push_notifications/registerer.js +++ b/app/soapbox/actions/push_notifications/registerer.js @@ -1,7 +1,7 @@ -import api from '../../api'; import { decode as decodeBase64 } from '../../utils/base64'; import { pushNotificationsSetting } from '../../settings'; import { setBrowserSupport, setSubscription, clearSubscription } from './setter'; +import { createPushSubsription, updatePushSubscription } from 'soapbox/actions/push_subscriptions'; // Taken from https://www.npmjs.com/package/web-push const urlBase64ToUint8Array = (base64String) => { @@ -13,10 +13,9 @@ const urlBase64ToUint8Array = (base64String) => { return decodeBase64(base64); }; -const getApplicationServerKey = getState => { - const key = getState().getIn(['auth', 'app', 'vapid_key']); - if (!key) console.error('Could not get vapid key. Push notifications will not work.'); - return key; +const getVapidKey = getState => { + const state = getState(); + return state.getIn(['auth', 'app', 'vapid_key']) || state.getIn(['instance', 'pleroma', 'vapid_public_key']); }; const getRegistration = () => navigator.serviceWorker.ready; @@ -28,23 +27,25 @@ const getPushSubscription = (registration) => const subscribe = (registration, getState) => registration.pushManager.subscribe({ userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey(getState)), + applicationServerKey: urlBase64ToUint8Array(getVapidKey(getState)), }); const unsubscribe = ({ registration, subscription }) => subscription ? subscription.unsubscribe().then(() => registration) : registration; const sendSubscriptionToBackend = (subscription, me) => { - const params = { subscription }; + return (dispatch, getState) => { + const params = { subscription }; - if (me) { - const data = pushNotificationsSetting.get(me); - if (data) { - params.data = data; + if (me) { + const data = pushNotificationsSetting.get(me); + if (data) { + params.data = data; + } } - } - return api().post('/api/web/push_subscriptions', params).then(response => response.data); + return dispatch(createPushSubsription(params)); + }; }; // Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload @@ -53,85 +54,86 @@ const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' export function register() { return (dispatch, getState) => { const me = getState().get('me'); + const vapidKey = getVapidKey(getState); + dispatch(setBrowserSupport(supportsPushNotifications)); - if (supportsPushNotifications) { - if (!getApplicationServerKey(getState)) { - console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.'); - return; - } + if (!supportsPushNotifications) { + console.warn('Your browser does not support Web Push Notifications.'); + return; + } - getRegistration() - .then(getPushSubscription) - .then(({ registration, subscription }) => { - if (subscription !== null) { - // We have a subscription, check if it is still valid - const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString(); - const subscriptionServerKey = urlBase64ToUint8Array(getApplicationServerKey(getState)).toString(); - const serverEndpoint = getState().getIn(['push_notifications', 'subscription', 'endpoint']); - - // If the VAPID public key did not change and the endpoint corresponds - // to the endpoint saved in the backend, the subscription is valid - if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) { - return subscription; - } else { - // Something went wrong, try to subscribe again - return unsubscribe({ registration, subscription }).then(registration => { - return subscribe(registration, getState); - }).then( - subscription => sendSubscriptionToBackend(subscription, me)); - } - } + if (!vapidKey) { + console.error('The VAPID public key is not set. You will not be able to receive Web Push Notifications.'); + return; + } - // No subscription, try to subscribe - return subscribe(registration, getState).then( - subscription => sendSubscriptionToBackend(subscription, me)); - }) - .then(subscription => { - // If we got a PushSubscription (and not a subscription object from the backend) - // it means that the backend subscription is valid (and was set during hydration) - if (!(subscription instanceof PushSubscription)) { - dispatch(setSubscription(subscription)); - if (me) { - pushNotificationsSetting.set(me, { alerts: subscription.alerts }); - } - } - }) - .catch(error => { - if (error.code === 20 && error.name === 'AbortError') { - console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.'); - } else if (error.code === 5 && error.name === 'InvalidCharacterError') { - console.error('The VAPID public key seems to be invalid:', getApplicationServerKey(getState)); + getRegistration() + .then(getPushSubscription) + .then(({ registration, subscription }) => { + if (subscription !== null) { + // We have a subscription, check if it is still valid + const currentServerKey = (new Uint8Array(subscription.options.applicationServerKey)).toString(); + const subscriptionServerKey = urlBase64ToUint8Array(vapidKey).toString(); + const serverEndpoint = getState().getIn(['push_notifications', 'subscription', 'endpoint']); + + // If the VAPID public key did not change and the endpoint corresponds + // to the endpoint saved in the backend, the subscription is valid + if (subscriptionServerKey === currentServerKey && subscription.endpoint === serverEndpoint) { + return subscription; + } else { + // Something went wrong, try to subscribe again + return unsubscribe({ registration, subscription }).then(registration => { + return subscribe(registration, getState); + }).then( + subscription => dispatch(sendSubscriptionToBackend(subscription, me))); } - - // Clear alerts and hide UI settings - dispatch(clearSubscription()); + } + + // No subscription, try to subscribe + return subscribe(registration, getState) + .then(subscription => dispatch(sendSubscriptionToBackend(subscription, me))); + }) + .then(subscription => { + // If we got a PushSubscription (and not a subscription object from the backend) + // it means that the backend subscription is valid (and was set during hydration) + if (!(subscription instanceof PushSubscription)) { + dispatch(setSubscription(subscription)); if (me) { - pushNotificationsSetting.remove(me); + pushNotificationsSetting.set(me, { alerts: subscription.alerts }); } - - return getRegistration() - .then(getPushSubscription) - .then(unsubscribe); - }) - .catch(console.warn); - } else { - console.warn('Your browser does not support Web Push Notifications.'); - } + } + }) + .catch(error => { + if (error.code === 20 && error.name === 'AbortError') { + console.warn('Your browser supports Web Push Notifications, but does not seem to implement the VAPID protocol.'); + } else if (error.code === 5 && error.name === 'InvalidCharacterError') { + console.error('The VAPID public key seems to be invalid:', vapidKey); + } + + // Clear alerts and hide UI settings + dispatch(clearSubscription()); + + if (me) { + pushNotificationsSetting.remove(me); + } + + return getRegistration() + .then(getPushSubscription) + .then(unsubscribe); + }) + .catch(console.warn); }; } export function saveSettings() { - return (_, getState) => { + return (dispatch, getState) => { const state = getState().get('push_notifications'); - const subscription = state.get('subscription'); const alerts = state.get('alerts'); const data = { alerts }; const me = getState().get('me'); - api().put(`/api/web/push_subscriptions/${subscription.get('id')}`, { - data, - }).then(() => { + return dispatch(updatePushSubscription({ data })).then(() => { if (me) { pushNotificationsSetting.set(me, data); } diff --git a/app/soapbox/actions/push_subscriptions.js b/app/soapbox/actions/push_subscriptions.js index d0bfa653d..5b47a4c93 100644 --- a/app/soapbox/actions/push_subscriptions.js +++ b/app/soapbox/actions/push_subscriptions.js @@ -21,6 +21,7 @@ export function createPushSubsription(params) { dispatch({ type: PUSH_SUBSCRIPTION_CREATE_REQUEST, params }); return api(getState).post('/api/v1/push/subscription', params).then(({ data: subscription }) => { dispatch({ type: PUSH_SUBSCRIPTION_CREATE_SUCCESS, params, subscription }); + return subscription; }).catch(error => { dispatch({ type: PUSH_SUBSCRIPTION_CREATE_FAIL, params, error }); }); @@ -38,7 +39,7 @@ export function fetchPushSubsription() { }; } -export function updatePushSubsription(params) { +export function updatePushSubscription(params) { return (dispatch, getState) => { dispatch({ type: PUSH_SUBSCRIPTION_UPDATE_REQUEST, params }); return api(getState).put('/api/v1/push/subscription', params).then(({ data: subscription }) => { diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 134501bbe..83f1bbbec 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -35,6 +35,7 @@ import EmptyPage from 'soapbox/pages/default_page'; import AdminPage from 'soapbox/pages/admin_page'; import RemoteInstancePage from 'soapbox/pages/remote_instance_page'; import { connectUserStream } from '../../actions/streaming'; +import { register as registerPushNotifications } from 'soapbox/actions/push_notifications'; import { Redirect } from 'react-router-dom'; import Icon from 'soapbox/components/icon'; import { isStaff, isAdmin } from 'soapbox/utils/accounts'; @@ -507,6 +508,7 @@ class UI extends React.PureComponent { dispatch(fetchCustomEmojis()); this.connectStreaming(); + dispatch(registerPushNotifications()); } componentDidUpdate(prevProps) { diff --git a/app/soapbox/main.js b/app/soapbox/main.js index 91822efef..4828f996e 100644 --- a/app/soapbox/main.js +++ b/app/soapbox/main.js @@ -1,9 +1,6 @@ 'use strict'; import './precheck'; -// FIXME: Push notifications are temporarily removed -// import * as registerPushNotifications from './actions/push_notifications'; -// import { default as Soapbox, store } from './containers/soapbox'; import { default as Soapbox } from './containers/soapbox'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -27,8 +24,6 @@ function main() { if (NODE_ENV === 'production') { // avoid offline in dev mode because it's harder to debug OfflinePluginRuntime.install(); - // FIXME: Push notifications are temporarily removed - // store.dispatch(registerPushNotifications.register()); } perf.stop('main()'); });