diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index befeda34041803b7e7320031b776703f372e5129..ba7a489dfb77305709156a513d1404ef2c7cd4ee 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 dc9ab1bdc525faf7a56e4619da415017da5a4b38..145a30e6bc8cad44a1fd03e4ec1a85ba4ce3ae92 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 0000000000000000000000000000000000000000..d039f7290f0122d7019e76d1f3656097f317a499 --- /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 c38b79d5edd9c66345dce6db5c598ca70dc3082e..017838b465d879441cfe28eb9742e14575487bb8 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 1b6d575ddd3f030a9b8e43897bfa907ba2842ffa..0db7decb6ec37c4256ae4941115a938820544b24 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 0000000000000000000000000000000000000000..09b9dfbdde7e94f96b2f3f65159e470492e8d0b1 --- /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 e6567532eb15a79f243b7e8a5df1b9064e5eeefd..55afb6708d844a885ade6473327360ca6198c243 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",