From 329b751c2fd1b34d79d7da03005b90171757b2cf Mon Sep 17 00:00:00 2001
From: Greg Fodor <gfodor@gmail.com>
Date: Wed, 17 Oct 2018 06:26:28 +0000
Subject: [PATCH] WIP

---
 src/assets/translations.data.json |  1 +
 src/hub.js                        | 17 +++++++-
 src/hub.service.js                | 10 +++++
 src/react-components/ui-root.js   | 26 +++++++++++-
 src/storage/store.js              |  7 ++++
 src/subscriptions.js              | 66 +++++++++++++++++++++++++++++++
 webpack.config.js                 |  6 +++
 7 files changed, 130 insertions(+), 3 deletions(-)
 create mode 100644 src/hub.service.js
 create mode 100644 src/subscriptions.js

diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json
index befeda340..ba7a489df 100644
--- a/src/assets/translations.data.json
+++ b/src/assets/translations.data.json
@@ -32,6 +32,7 @@
     "entry.enable-screen-sharing": "Share my desktop",
     "entry.return-to-vr": "Enter in VR",
     "entry.lobby": "Lobby",
+    "entry.notify_me": "Notify me when others join",
     "profile.save": "Accept",
     "profile.display_name.validation_warning": "Alphanumerics and hyphens. At least 3 characters, no more than 32",
     "profile.header": "Name & Avatar",
diff --git a/src/hub.js b/src/hub.js
index dc9ab1bdc..145a30e6b 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -81,6 +81,7 @@ import { connectToReticulum } from "./utils/phoenix-utils";
 import { disableiOSZoom } from "./utils/disable-ios-zoom";
 import { proxiedUrlFor } from "./utils/media-utils";
 import SceneEntryManager from "./scene-entry-manager";
+import Subscriptions from "./subscriptions";
 
 import "./systems/nav";
 import "./systems/personal-space-bubble";
@@ -490,6 +491,20 @@ document.addEventListener("DOMContentLoaded", async () => {
 
   // Reticulum global channel
   const retPhxChannel = socket.channel(`ret`, { hub_id: hubId });
-  retPhxChannel.join().receive("error", res => console.error(res));
+  retPhxChannel
+    .join()
+    .receive("ok", async data => {
+      const vapidPublicKey = data.vapid_public_key;
+      console.log("Load service");
+      navigator.serviceWorker.register("/hub.service.js");
+      console.log("222");
+      navigator.serviceWorker.ready.then(serviceWorkerRegistration => {
+        console.log("333");
+        const subscriptions = new Subscriptions(hubId, hubChannel, vapidPublicKey, store, serviceWorkerRegistration);
+        remountUI({ subscriptions });
+      });
+    })
+    .receive("error", res => console.error(res));
+
   linkChannel.setSocket(socket);
 });
diff --git a/src/hub.service.js b/src/hub.service.js
new file mode 100644
index 000000000..d039f7290
--- /dev/null
+++ b/src/hub.service.js
@@ -0,0 +1,10 @@
+self.addEventListener("install", function(e) {
+  e.waitUntil(self.skipWaiting());
+});
+self.addEventListener("activate", function(e) {
+  e.waitUntil(self.clients.claim());
+});
+
+self.addEventListener("push", function() {});
+
+self.addEventListener("notificationclick", function() {});
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index c38b79d5e..017838b46 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -89,7 +89,8 @@ class UIRoot extends Component {
     isSupportAvailable: PropTypes.bool,
     presenceLogEntries: PropTypes.array,
     presences: PropTypes.object,
-    sessionId: PropTypes.string
+    sessionId: PropTypes.string,
+    subscriptions: PropTypes.object
   };
 
   state = {
@@ -130,7 +131,8 @@ class UIRoot extends Component {
     exited: false,
 
     showProfileEntry: false,
-    pendingMessage: ""
+    pendingMessage: "",
+    isSubscribed: false
   };
 
   componentDidMount() {
@@ -140,6 +142,7 @@ class UIRoot extends Component {
     this.props.scene.addEventListener("stateadded", this.onAframeStateChanged);
     this.props.scene.addEventListener("stateremoved", this.onAframeStateChanged);
     this.props.scene.addEventListener("exit", this.exit);
+    this.updateSubscribedState();
   }
 
   componentWillUnmount() {
@@ -147,6 +150,11 @@ class UIRoot extends Component {
     this.props.scene.removeEventListener("exit", this.exit);
   }
 
+  updateSubscribedState = () => {
+    const isSubscribed = this.props.subscriptions && this.props.subscriptions.isSubscribed();
+    this.setState({ isSubscribed });
+  };
+
   onSceneLoaded = () => {
     this.setState({ sceneLoaded: true });
   };
@@ -175,6 +183,13 @@ class UIRoot extends Component {
     this.props.scene.emit("spawn_pen");
   };
 
+  onSubscribeClicked = async () => {
+    if (!this.props.subscriptions) return;
+
+    await this.props.subscriptions.toggle();
+    this.updateSubscribedState();
+  };
+
   handleStartEntry = () => {
     const promptForNameAndAvatarBeforeEntry = !this.props.store.state.activity.hasChangedName;
 
@@ -697,6 +712,13 @@ class UIRoot extends Component {
           </form>
         </div>
 
+        <div>
+          <input id="subscribe" type="checkbox" onClick={this.onSubscribeClicked} checked={this.state.isSubscribed} />
+          <label htmlFor="subscribe">
+            <FormattedMessage id="entry.notify_me" />
+          </label>
+        </div>
+
         <div className={entryStyles.buttonContainer}>
           <button
             className={classNames([entryStyles.actionButton, entryStyles.wideButton])}
diff --git a/src/storage/store.js b/src/storage/store.js
index 1b6d575dd..0db7decb6 100644
--- a/src/storage/store.js
+++ b/src/storage/store.js
@@ -38,6 +38,13 @@ export const SCHEMA = {
       properties: {
         lastUsedMicDeviceId: { type: "string" }
       }
+    },
+
+    // Map of sid -> { endpoint: "<endpoint>" }
+    // If entry exists, it means there is a subscription to that room, wired to that endpoint.
+    subscriptions: {
+      type: "object",
+      additionalProperties: true
     }
   },
 
diff --git a/src/subscriptions.js b/src/subscriptions.js
new file mode 100644
index 000000000..09b9dfbdd
--- /dev/null
+++ b/src/subscriptions.js
@@ -0,0 +1,66 @@
+// Manages web push subscriptions
+//
+function 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;
+}
+
+export default class Subscriptions {
+  constructor(hubId, hubChannel, vapidPublicKey, store, registration) {
+    this.store = store;
+    this.hubId = hubId;
+    this.hubChannel = hubChannel;
+    this.registration = registration;
+    this.vapidPublicKey = vapidPublicKey;
+  }
+
+  setVapidPublicKey = vapidPublicKey => {
+    this.vapidPublicKey = vapidPublicKey;
+  };
+
+  isSubscribed = () => {
+    return !!this.store.subscriptions[this.hubId];
+  };
+
+  toggle = async () => {
+    console.log("toggle");
+    const subscriptions = this.store.subscriptions;
+
+    if (this.isSubscribed()) {
+      console.log("Send channel unsubscribe");
+
+      delete subscriptions[this.hubId];
+
+      if (Object.keys(subscriptions).length === 0) {
+        console.log("Remove push subscription from browser");
+      }
+    } else {
+      console.log("Get current");
+      let subscription = await this.registration.pushManager.getSubscription();
+      console.log(subscription);
+
+      if (!subscription) {
+        const convertedVapidKey = urlBase64ToUint8Array(this.vapidPublicKey);
+
+        subscription = await this.registration.pushManager.subscribe({
+          userVisibleOnly: true,
+          applicationServerKey: convertedVapidKey
+        });
+      }
+
+      console.log(JSON.stringify(subscription));
+      const endpoint = subscription.endpoint;
+      subscriptions[this.hubId] = { endpoint };
+    }
+
+    this.store.update({ subscriptions });
+  };
+}
diff --git a/webpack.config.js b/webpack.config.js
index e6567532e..55afb6708 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -240,6 +240,12 @@ module.exports = (env, argv) => ({
         to: "hub-preview.png"
       }
     ]),
+    new CopyWebpackPlugin([
+      {
+        from: "src/hub.service.js",
+        to: "hub.service.js"
+      }
+    ]),
     // Extract required css and add a content hash.
     new ExtractTextPlugin({
       filename: "assets/stylesheets/[name]-[md5:contenthash:hex:20].css",
-- 
GitLab