about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
authorbeatrix <beatrix.bitrot@gmail.com>2018-01-05 18:29:57 -0500
committerGitHub <noreply@github.com>2018-01-05 18:29:57 -0500
commitf441770e50621ac59a7b022ee2127964935b2b8d (patch)
treebd7bca6f161424f2fa6c6595fe70bb2d7df1074f /app/javascript
parentb4e667f86b645a1ddaa7944b61c2d6ca3b9e3981 (diff)
parent72b99f6ee416888ad4ea041c4cf90390c75e6861 (diff)
Merge pull request #290 from chriswmartin/web-push-updates
Web push updates
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/flavours/glitch/actions/push_notifications/index.js23
-rw-r--r--app/javascript/flavours/glitch/actions/push_notifications/registerer.js149
-rw-r--r--app/javascript/flavours/glitch/actions/push_notifications/setter.js (renamed from app/javascript/flavours/glitch/actions/push_notifications.js)24
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/column_settings.js1
-rw-r--r--app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js9
-rw-r--r--app/javascript/flavours/glitch/reducers/push_notifications.js4
-rw-r--r--app/javascript/flavours/glitch/util/main.js6
-rw-r--r--app/javascript/flavours/glitch/util/settings.js46
-rw-r--r--app/javascript/flavours/glitch/util/web_push_subscription.js105
9 files changed, 228 insertions, 139 deletions
diff --git a/app/javascript/flavours/glitch/actions/push_notifications/index.js b/app/javascript/flavours/glitch/actions/push_notifications/index.js
new file mode 100644
index 000000000..376b55b62
--- /dev/null
+++ b/app/javascript/flavours/glitch/actions/push_notifications/index.js
@@ -0,0 +1,23 @@
+import {
+  SET_BROWSER_SUPPORT,
+  SET_SUBSCRIPTION,
+  CLEAR_SUBSCRIPTION,
+  SET_ALERTS,
+  setAlerts,
+} from './setter';
+import { register, saveSettings } from './registerer';
+
+export {
+  SET_BROWSER_SUPPORT,
+  SET_SUBSCRIPTION,
+  CLEAR_SUBSCRIPTION,
+  SET_ALERTS,
+  register,
+};
+
+export function changeAlerts(key, value) {
+  return dispatch => {
+    dispatch(setAlerts(key, value));
+    dispatch(saveSettings());
+  };
+}
diff --git a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js
new file mode 100644
index 000000000..3003d4149
--- /dev/null
+++ b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js
@@ -0,0 +1,149 @@
+import axios from 'axios';
+import { pushNotificationsSetting } from 'flavours/glitch/util/settings';
+import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
+
+// Taken from https://www.npmjs.com/package/web-push
+const urlBase64ToUint8Array = (base64String) => {
+  const padding = '='.repeat((4 - base64String.length % 4) % 4);
+  const base64 = (base64String + padding)
+    .replace(/\-/g, '+')
+    .replace(/_/g, '/');
+
+  const rawData = window.atob(base64);
+  const outputArray = new Uint8Array(rawData.length);
+
+  for (let i = 0; i < rawData.length; ++i) {
+    outputArray[i] = rawData.charCodeAt(i);
+  }
+  return outputArray;
+};
+
+const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
+
+const getRegistration = () => navigator.serviceWorker.ready;
+
+const getPushSubscription = (registration) =>
+  registration.pushManager.getSubscription()
+    .then(subscription => ({ registration, subscription }));
+
+const subscribe = (registration) =>
+  registration.pushManager.subscribe({
+    userVisibleOnly: true,
+    applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()),
+  });
+
+const unsubscribe = ({ registration, subscription }) =>
+  subscription ? subscription.unsubscribe().then(() => registration) : registration;
+
+const sendSubscriptionToBackend = (subscription, me) => {
+  const params = { subscription };
+
+  if (me) {
+    const data = pushNotificationsSetting.get(me);
+    if (data) {
+      params.data = data;
+    }
+  }
+
+  return axios.post('/api/web/push_subscriptions', params).then(response => response.data);
+};
+
+// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
+const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
+
+export function register () {
+  return (dispatch, getState) => {
+    dispatch(setBrowserSupport(supportsPushNotifications));
+    const me = getState().getIn(['meta', 'me']);
+
+    if (me && !pushNotificationsSetting.get(me)) {
+      const alerts = getState().getIn(['push_notifications', 'alerts']);
+      if (alerts) {
+        pushNotificationsSetting.set(me, { alerts: alerts });
+      }
+    }
+
+    if (supportsPushNotifications) {
+      if (!getApplicationServerKey()) {
+        console.error('The VAPID public key is not set. You will not be able to receive 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()).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(subscribe).then(
+                subscription => sendSubscriptionToBackend(subscription, me));
+            }
+          }
+
+          // No subscription, try to subscribe
+          return subscribe(registration).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());
+          }
+
+          // Clear alerts and hide UI settings
+          dispatch(clearSubscription());
+          if (me) {
+            pushNotificationsSetting.remove(me);
+          }
+
+          try {
+            getRegistration()
+              .then(getPushSubscription)
+              .then(unsubscribe);
+          } catch (e) {
+
+          }
+        });
+    } else {
+      console.warn('Your browser does not support Web Push Notifications.');
+    }
+  };
+}
+
+export function saveSettings() {
+  return (_, getState) => {
+    const state = getState().get('push_notifications');
+    const subscription = state.get('subscription');
+    const alerts = state.get('alerts');
+    const data = { alerts };
+
+    axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
+      data,
+    }).then(() => {
+      const me = getState().getIn(['meta', 'me']);
+      if (me) {
+        pushNotificationsSetting.set(me, data);
+      }
+    });
+  };
+}
diff --git a/app/javascript/flavours/glitch/actions/push_notifications.js b/app/javascript/flavours/glitch/actions/push_notifications/setter.js
index 55661d2b0..a2cc41c5a 100644
--- a/app/javascript/flavours/glitch/actions/push_notifications.js
+++ b/app/javascript/flavours/glitch/actions/push_notifications/setter.js
@@ -1,9 +1,7 @@
-import axios from 'axios';
-
 export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
 export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
 export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION';
-export const ALERTS_CHANGE = 'PUSH_NOTIFICATIONS_ALERTS_CHANGE';
+export const SET_ALERTS = 'PUSH_NOTIFICATIONS_SET_ALERTS';
 
 export function setBrowserSupport (value) {
   return {
@@ -25,28 +23,12 @@ export function clearSubscription () {
   };
 }
 
-export function changeAlerts(key, value) {
+export function setAlerts (key, value) {
   return dispatch => {
     dispatch({
-      type: ALERTS_CHANGE,
+      type: SET_ALERTS,
       key,
       value,
     });
-
-    dispatch(saveSettings());
-  };
-}
-
-export function saveSettings() {
-  return (_, getState) => {
-    const state = getState().get('push_notifications');
-    const subscription = state.get('subscription');
-    const alerts = state.get('alerts');
-
-    axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
-      data: {
-        alerts,
-      },
-    });
   };
 }
diff --git a/app/javascript/flavours/glitch/features/notifications/components/column_settings.js b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js
index 57cded4f1..23545185c 100644
--- a/app/javascript/flavours/glitch/features/notifications/components/column_settings.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js
@@ -11,7 +11,6 @@ export default class ColumnSettings extends React.PureComponent {
     settings: ImmutablePropTypes.map.isRequired,
     pushSettings: ImmutablePropTypes.map.isRequired,
     onChange: PropTypes.func.isRequired,
-    onSave: PropTypes.func.isRequired,
     onClear: PropTypes.func.isRequired,
   };
 
diff --git a/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js
index ce502700c..95109fe4d 100644
--- a/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js
+++ b/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js
@@ -1,9 +1,9 @@
 import { connect } from 'react-redux';
 import { defineMessages, injectIntl } from 'react-intl';
 import ColumnSettings from '../components/column_settings';
-import { changeSetting, saveSettings } from 'flavours/glitch/actions/settings';
+import { changeSetting } from 'flavours/glitch/actions/settings';
 import { clearNotifications } from 'flavours/glitch/actions/notifications';
-import { changeAlerts as changePushNotifications, saveSettings as savePushNotificationSettings } from 'flavours/glitch/actions/push_notifications';
+import { changeAlerts as changePushNotifications } from 'flavours/glitch/actions/push_notifications';
 import { openModal } from 'flavours/glitch/actions/modal';
 
 const messages = defineMessages({
@@ -26,11 +26,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     }
   },
 
-  onSave () {
-    dispatch(saveSettings());
-    dispatch(savePushNotificationSettings());
-  },
-
   onClear () {
     dispatch(openModal('CONFIRM', {
       message: intl.formatMessage(messages.clearMessage),
diff --git a/app/javascript/flavours/glitch/reducers/push_notifications.js b/app/javascript/flavours/glitch/reducers/push_notifications.js
index f0a800d23..4eba2a5e8 100644
--- a/app/javascript/flavours/glitch/reducers/push_notifications.js
+++ b/app/javascript/flavours/glitch/reducers/push_notifications.js
@@ -1,5 +1,5 @@
 import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
-import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, ALERTS_CHANGE } from 'flavours/glitch/actions/push_notifications';
+import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, SET_ALERTS } from 'flavours/glitch/actions/push_notifications';
 import Immutable from 'immutable';
 
 const initialState = Immutable.Map({
@@ -43,7 +43,7 @@ export default function push_subscriptions(state = initialState, action) {
     return state.set('browserSupport', action.value);
   case CLEAR_SUBSCRIPTION:
     return initialState;
-  case ALERTS_CHANGE:
+  case SET_ALERTS:
     return state.setIn(action.key, action.value);
   default:
     return state;
diff --git a/app/javascript/flavours/glitch/util/main.js b/app/javascript/flavours/glitch/util/main.js
index fe57fa962..c00210677 100644
--- a/app/javascript/flavours/glitch/util/main.js
+++ b/app/javascript/flavours/glitch/util/main.js
@@ -1,5 +1,5 @@
-import * as WebPushSubscription from './web_push_subscription';
-import Mastodon from 'flavours/glitch/containers/mastodon';
+import * as registerPushNotifications from 'flavours/glitch/actions/push_notifications';
+import { default as Mastodon, store } from 'flavours/glitch/containers/mastodon';
 import React from 'react';
 import ReactDOM from 'react-dom';
 import ready from './ready';
@@ -25,7 +25,7 @@ function main() {
     if (process.env.NODE_ENV === 'production') {
       // avoid offline in dev mode because it's harder to debug
       require('offline-plugin/runtime').install();
-      WebPushSubscription.register();
+      store.dispatch(registerPushNotifications.register());
     }
     perf.stop('main()');
 
diff --git a/app/javascript/flavours/glitch/util/settings.js b/app/javascript/flavours/glitch/util/settings.js
new file mode 100644
index 000000000..dbd969cb1
--- /dev/null
+++ b/app/javascript/flavours/glitch/util/settings.js
@@ -0,0 +1,46 @@
+export default class Settings {
+
+  constructor(keyBase = null) {
+    this.keyBase = keyBase;
+  }
+
+  generateKey(id) {
+    return this.keyBase ? [this.keyBase, `id${id}`].join('.') : id;
+  }
+
+  set(id, data) {
+    const key = this.generateKey(id);
+    try {
+      const encodedData = JSON.stringify(data);
+      localStorage.setItem(key, encodedData);
+      return data;
+    } catch (e) {
+      return null;
+    }
+  }
+
+  get(id) {
+    const key = this.generateKey(id);
+    try {
+      const rawData = localStorage.getItem(key);
+      return JSON.parse(rawData);
+    } catch (e) {
+      return null;
+    }
+  }
+
+  remove(id) {
+    const data = this.get(id);
+    if (data) {
+      const key = this.generateKey(id);
+      try {
+        localStorage.removeItem(key);
+      } catch (e) {
+      }
+    }
+    return data;
+  }
+
+}
+
+export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');
diff --git a/app/javascript/flavours/glitch/util/web_push_subscription.js b/app/javascript/flavours/glitch/util/web_push_subscription.js
deleted file mode 100644
index de185b6d9..000000000
--- a/app/javascript/flavours/glitch/util/web_push_subscription.js
+++ /dev/null
@@ -1,105 +0,0 @@
-import axios from 'axios';
-import { store } from 'flavours/glitch/containers/mastodon';
-import { setBrowserSupport, setSubscription, clearSubscription } from 'flavours/glitch/actions/push_notifications';
-
-// Taken from https://www.npmjs.com/package/web-push
-const urlBase64ToUint8Array = (base64String) => {
-  const padding = '='.repeat((4 - base64String.length % 4) % 4);
-  const base64 = (base64String + padding)
-    .replace(/\-/g, '+')
-    .replace(/_/g, '/');
-
-  const rawData = window.atob(base64);
-  const outputArray = new Uint8Array(rawData.length);
-
-  for (let i = 0; i < rawData.length; ++i) {
-    outputArray[i] = rawData.charCodeAt(i);
-  }
-  return outputArray;
-};
-
-const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
-
-const getRegistration = () => navigator.serviceWorker.ready;
-
-const getPushSubscription = (registration) =>
-  registration.pushManager.getSubscription()
-    .then(subscription => ({ registration, subscription }));
-
-const subscribe = (registration) =>
-  registration.pushManager.subscribe({
-    userVisibleOnly: true,
-    applicationServerKey: urlBase64ToUint8Array(getApplicationServerKey()),
-  });
-
-const unsubscribe = ({ registration, subscription }) =>
-  subscription ? subscription.unsubscribe().then(() => registration) : registration;
-
-const sendSubscriptionToBackend = (subscription) =>
-  axios.post('/api/web/push_subscriptions', {
-    subscription,
-  }).then(response => response.data);
-
-// Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload
-const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype);
-
-export function register () {
-  store.dispatch(setBrowserSupport(supportsPushNotifications));
-
-  if (supportsPushNotifications) {
-    if (!getApplicationServerKey()) {
-      console.error('The VAPID public key is not set. You will not be able to receive 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()).toString();
-          const serverEndpoint = store.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(subscribe).then(sendSubscriptionToBackend);
-          }
-        }
-
-        // No subscription, try to subscribe
-        return subscribe(registration).then(sendSubscriptionToBackend);
-      })
-      .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)) {
-          store.dispatch(setSubscription(subscription));
-        }
-      })
-      .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());
-        }
-
-        // Clear alerts and hide UI settings
-        store.dispatch(clearSubscription());
-
-        try {
-          getRegistration()
-            .then(getPushSubscription)
-            .then(unsubscribe);
-        } catch (e) {
-
-        }
-      });
-  } else {
-    console.warn('Your browser does not support Web Push Notifications.');
-  }
-}