diff --git a/scripts/hab-build-and-push.sh b/scripts/hab-build-and-push.sh index f9ffb81ad1ef8e780bdc4efd8499eee9c0eab4c4..1f0b129f00851cfae504fc347b17adfdde7ffb3f 100755 --- a/scripts/hab-build-and-push.sh +++ b/scripts/hab-build-and-push.sh @@ -31,6 +31,7 @@ npm rebuild node-sass # HACK sometimes node-sass build fails npm run build mkdir dist/pages mv dist/*.html dist/pages +mv dist/hub.service.js dist/pages aws s3 sync --acl public-read --cache-control "max-age=31556926" dist/assets "$TARGET_S3_URL/assets" aws s3 sync --acl public-read --cache-control "no-cache" --delete dist/pages "$TARGET_S3_URL/pages/latest" diff --git a/src/assets/stylesheets/entry.scss b/src/assets/stylesheets/entry.scss index f0424741082257a992e0adfd24de80703d1899c7..d94b199b07ffcaf6b6af11904fb8a779804ea13a 100644 --- a/src/assets/stylesheets/entry.scss +++ b/src/assets/stylesheets/entry.scss @@ -184,4 +184,24 @@ cursor: pointer; color: $dark-grey-text; } + + :local(.subscribe) { + margin-bottom: 24px; + display: flex; + align-items: center; + justify-content: center; + color: $darker-grey; + + input { + @extend %checkbox; + } + + input:checked { + @extend %checkbox-checked; + } + + label { + margin-right: 8px; + } + } } diff --git a/src/assets/stylesheets/hub-create.scss b/src/assets/stylesheets/hub-create.scss index bc9145df6e1246e5a34e60f29efc7fffe7297414..2af9cb72e0c16e35f58b5df5ffd16faadd90dca0 100644 --- a/src/assets/stylesheets/hub-create.scss +++ b/src/assets/stylesheets/hub-create.scss @@ -51,12 +51,12 @@ border-radius: 0px 0px 14px 14px; &::selection { - background-color: #2F80ED; + background-color: $bright-blue; color: white; } &::-moz-selection { - background-color: #2F80ED; + background-color: $bright-blue; color: white; } diff --git a/src/assets/stylesheets/index.scss b/src/assets/stylesheets/index.scss index 66595775efe1e43c7a835987940ff288eee63ef1..46029a3b32be5675fd1448ceb65ca9375e6ddd66 100644 --- a/src/assets/stylesheets/index.scss +++ b/src/assets/stylesheets/index.scss @@ -172,7 +172,7 @@ body { :local(.create) { padding: 2.1em; - padding-bottom: 3.5vw; + padding-bottom: 2vw; position: relative; @media (max-width: 768px) { @@ -189,6 +189,17 @@ body { @extend %action-button; } } + + :local(.spoke-button) { + display: flex; + justify-content: center; + + a { + margin-top: 16px; + @extend %action-button; + background: $spoke-action-color; + } + } } :local(.footer-content) { diff --git a/src/assets/stylesheets/shared.scss b/src/assets/stylesheets/shared.scss index 79a12fc6febb433dfa5453fa2729454971cbb690..d44b6b31916977101aea39ee8824cee35e388559 100644 --- a/src/assets/stylesheets/shared.scss +++ b/src/assets/stylesheets/shared.scss @@ -10,10 +10,12 @@ $light-grey: lightgrey; $dark-grey: rgba(128, 128, 128, 1.0); $darker-grey: rgba(64, 64, 64, 1.0); $darkest-grey: rgba(32, 32, 32, 1.0); +$bright-blue: #2F80ED; $action-color: #FF3464; $action-color-light: #FF74A4; $action-color-transparent: rgba(255, 52, 100, 0.9); $hud-panel-background: rgba(79, 79, 79, 0.45); +$spoke-action-color: $bright-blue; %unselectable { -moz-user-select: none; @@ -153,16 +155,15 @@ $hud-panel-background: rgba(79, 79, 79, 0.45); -webkit-appearance: none; width: 2em; height: 2em; - border: 1px solid #e2e2e2; - border-radius: 9px; + border: 3px solid #aaa; + border-radius: 6px; vertical-align: sub; margin: 0 0.6em } %checkbox-checked { - border: 9px double #aaa; - outline: 9px solid #aaa; - outline-offset: -18px; + outline: 9px solid $action-color; + outline-offset: -15px; } %background-agnostic { diff --git a/src/assets/stylesheets/spoke.scss b/src/assets/stylesheets/spoke.scss index 68f850879027e250f7bcce75a16a88692cf4a6e3..0219502e7a00014e7cbb07530ca3a78e06353b73 100644 --- a/src/assets/stylesheets/spoke.scss +++ b/src/assets/stylesheets/spoke.scss @@ -1,7 +1,6 @@ @import 'shared'; @import 'loader'; -$spoke-action-color: #2F80ED; $breakpoint: 1280px; $mobile-breakpoint-width: 450px; diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 47fef3295a0b67e0216c506bb504b8e96c9bf2d0..819a5a04649cf73d1cc12f9d8b64f6cd1b5ababd 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 are here", "profile.save": "Accept", "profile.display_name.validation_warning": "Alphanumerics and hyphens. At least 3 characters, no more than 32", "profile.header": "Name & Avatar", @@ -68,6 +69,7 @@ "home.create_name.validation_warning": "Invalid name, limited to 4 to 64 characters and limited symbols.", "home.join_us": "Join the Conversation", "home.join_room": "Join Room", + "home.create_with_spoke": "Create a Scene", "home.report_issue": "Report Issues", "home.source_link": "Source", "home.spoke_link": "Spoke", diff --git a/src/components/action-to-event.js b/src/components/action-to-event.js index ee6f397445fea8d556f268ca81ea978f366df6d0..8bcd22e43dcbc30759f7ec3ec17692f0bf75a161 100644 --- a/src/components/action-to-event.js +++ b/src/components/action-to-event.js @@ -8,7 +8,7 @@ AFRAME.registerComponent("action-to-event", { tick() { const userinput = AFRAME.scenes[0].systems.userinput; - if (userinput.readFrameValueAtPath(this.data.path)) { + if (userinput.get(this.data.path)) { this.el.emit(this.data.event); } } diff --git a/src/components/camera-tool.js b/src/components/camera-tool.js index 1e19cf492e33d69ac475211ea7f21bab6d10db58..2972d7fcd63b876aeaf22db191d998edb3183351 100644 --- a/src/components/camera-tool.js +++ b/src/components/camera-tool.js @@ -110,7 +110,7 @@ AFRAME.registerComponent("camera-tool", { const grabber = this.el.components.grabbable.grabbers[0]; if (grabber && !!pathsMap[grabber.id]) { const paths = pathsMap[grabber.id]; - if (AFRAME.scenes[0].systems.userinput.readFrameValueAtPath(paths.takeSnapshot)) { + if (AFRAME.scenes[0].systems.userinput.get(paths.takeSnapshot)) { this.takeSnapshotNextTick = true; } } diff --git a/src/components/character-controller.js b/src/components/character-controller.js index 910123ba8bb2f5e54e64037ac478a1ac2e57e577..083f47e195e7453136c958ecad62762be882c614 100644 --- a/src/components/character-controller.js +++ b/src/components/character-controller.js @@ -45,8 +45,6 @@ AFRAME.registerComponent("character-controller", { const eventSrc = this.el.sceneEl; eventSrc.addEventListener("move", this.setAccelerationInput); eventSrc.addEventListener("rotateY", this.setAngularVelocity); - eventSrc.addEventListener("snap_rotate_left", this.snapRotateLeft); - eventSrc.addEventListener("snap_rotate_right", this.snapRotateRight); eventSrc.addEventListener("teleported", this.handleTeleport); }, @@ -54,8 +52,6 @@ AFRAME.registerComponent("character-controller", { const eventSrc = this.el.sceneEl; eventSrc.removeEventListener("move", this.setAccelerationInput); eventSrc.removeEventListener("rotateY", this.setAngularVelocity); - eventSrc.removeEventListener("snap_rotate_left", this.snapRotateLeft); - eventSrc.removeEventListener("snap_rotate_right", this.snapRotateRight); eventSrc.removeEventListener("teleported", this.handleTeleport); this.reset(); }, @@ -78,10 +74,12 @@ AFRAME.registerComponent("character-controller", { snapRotateLeft: function() { this.pendingSnapRotationMatrix.copy(this.leftRotationMatrix); + this.el.emit("snap_rotate_left"); }, snapRotateRight: function() { this.pendingSnapRotationMatrix.copy(this.rightRotationMatrix); + this.el.emit("snap_rotate_right"); }, handleTeleport: function(event) { @@ -119,13 +117,13 @@ AFRAME.registerComponent("character-controller", { root.updateMatrix(); const userinput = AFRAME.scenes[0].systems.userinput; - if (userinput.readFrameValueAtPath(paths.actions.snapRotateLeft)) { + if (userinput.get(paths.actions.snapRotateLeft)) { this.snapRotateLeft(); } - if (userinput.readFrameValueAtPath(paths.actions.snapRotateRight)) { + if (userinput.get(paths.actions.snapRotateRight)) { this.snapRotateRight(); } - const acc = userinput.readFrameValueAtPath(paths.actions.characterAcceleration); + const acc = userinput.get(paths.actions.characterAcceleration); if (acc) { this.accelerationInput.set( this.accelerationInput.x + acc[0], @@ -145,7 +143,7 @@ AFRAME.registerComponent("character-controller", { this.updateVelocity(deltaSeconds); this.accelerationInput.set(0, 0, 0); - const boost = userinput.readFrameValueAtPath(paths.actions.boost) ? 2 : 1; + const boost = userinput.get(paths.actions.boost) ? 2 : 1; move.makeTranslation( this.velocity.x * distance * boost, this.velocity.y * distance * boost, diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js index c5113dba822b23944026cff06b41497deecf55f0..1b095fc05948d88e38cbbcebd8b3141046ffb69f 100644 --- a/src/components/cursor-controller.js +++ b/src/components/cursor-controller.js @@ -94,8 +94,8 @@ AFRAME.registerComponent("cursor-controller", { } const userinput = AFRAME.scenes[0].systems.userinput; - const cursorPose = userinput.readFrameValueAtPath(paths.actions.cursor.pose); - const rightHandPose = userinput.readFrameValueAtPath(paths.actions.rightHand.pose); + const cursorPose = userinput.get(paths.actions.cursor.pose); + const rightHandPose = userinput.get(paths.actions.rightHand.pose); this.data.cursor.object3D.visible = this.enabled && !!cursorPose; this.el.setAttribute("line", "visible", this.enabled && !!rightHandPose); @@ -119,7 +119,7 @@ AFRAME.registerComponent("cursor-controller", { const { cursor, near, far, camera, cursorColorHovered, cursorColorUnhovered } = this.data; - const cursorModDelta = userinput.readFrameValueAtPath(paths.actions.cursor.modDelta); + const cursorModDelta = userinput.get(paths.actions.cursor.modDelta); if (isGrabbing && cursorModDelta) { this.distance = THREE.Math.clamp(this.distance - cursorModDelta, near, far); } diff --git a/src/components/hand-controls2.js b/src/components/hand-controls2.js index 534f796c45c304097768d5e486cac12db4d76664..3a97888b172687838581fc720f516e50ceb6b248 100644 --- a/src/components/hand-controls2.js +++ b/src/components/hand-controls2.js @@ -106,10 +106,10 @@ AFRAME.registerComponent("hand-controls2", { const hand = this.data; const userinput = AFRAME.scenes[0].systems.userinput; const subpath = hand === "left" ? paths.actions.leftHand : paths.actions.rightHand; - const hasPose = userinput.readFrameValueAtPath(subpath.pose); - const thumb = userinput.readFrameValueAtPath(subpath.thumb); - const index = userinput.readFrameValueAtPath(subpath.index); - const middleRingPinky = userinput.readFrameValueAtPath(subpath.middleRingPinky); + const hasPose = userinput.get(subpath.pose); + const thumb = userinput.get(subpath.thumb); + const index = userinput.get(subpath.index); + const middleRingPinky = userinput.get(subpath.middleRingPinky); const pose = this.poseForFingers(thumb, index, middleRingPinky); if (pose !== this.pose) { this.el.emit("hand-pose", { previous: this.pose, current: pose }); diff --git a/src/components/pinch-to-move.js b/src/components/pinch-to-move.js index 28dc43cb5d2584532bf549fc3b08aaf01c1ce953..bc5c57a7631e8070c4eaafba13f52c0ceb757bca 100644 --- a/src/components/pinch-to-move.js +++ b/src/components/pinch-to-move.js @@ -9,7 +9,7 @@ AFRAME.registerComponent("pinch-to-move", { }, tick() { const userinput = AFRAME.scenes[0].systems.userinput; - const pinch = userinput.readFrameValueAtPath(paths.device.touchscreen.pinchDelta); + const pinch = userinput.get(paths.device.touchscreen.pinchDelta); if (pinch) { this.axis[1] = pinch * this.data.speed; this.el.emit("move", { axis: this.axis }); diff --git a/src/components/pitch-yaw-rotator.js b/src/components/pitch-yaw-rotator.js index 297bf4f81b5105ca229a5d34bdf527b2389725bb..a2c2ebdead1d580c4480e2578e59c918289941f0 100644 --- a/src/components/pitch-yaw-rotator.js +++ b/src/components/pitch-yaw-rotator.js @@ -36,7 +36,7 @@ AFRAME.registerComponent("pitch-yaw-rotator", { tick() { const userinput = AFRAME.scenes[0].systems.userinput; - const cameraDelta = userinput.readFrameValueAtPath(paths.actions.cameraDelta); + const cameraDelta = userinput.get(paths.actions.cameraDelta); let lookX = this.pendingXRotation; let lookY = 0; if (cameraDelta) { diff --git a/src/components/super-networked-interactable.js b/src/components/super-networked-interactable.js index 4c96f2452f9945c0b9bf29be259c487b8d665c3d..1fbbaa37c6f0786b91f9d8cc109df19e0716829d 100644 --- a/src/components/super-networked-interactable.js +++ b/src/components/super-networked-interactable.js @@ -98,6 +98,6 @@ AFRAME.registerComponent("super-networked-interactable", { if (!(grabber && pathsMap[grabber.id])) return; const userinput = AFRAME.scenes[0].systems.userinput; - this._changeScale(userinput.readFrameValueAtPath(pathsMap[grabber.id].scaleGrabbedGrabbable)); + this._changeScale(userinput.get(pathsMap[grabber.id].scaleGrabbedGrabbable)); } }); diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js index a786639e1b828e34d1daf36bedd10e500d275ed1..7ff4e1189f589d9d026ba63e920dfc5d47a78760 100644 --- a/src/components/super-spawner.js +++ b/src/components/super-spawner.js @@ -116,8 +116,8 @@ AFRAME.registerComponent("super-spawner", { async onSpawnEvent() { const userinput = AFRAME.scenes[0].systems.userinput; - const leftPose = userinput.readFrameValueAtPath(paths.actions.leftHand.pose); - const rightPose = userinput.readFrameValueAtPath(paths.actions.rightHand.pose); + const leftPose = userinput.get(paths.actions.leftHand.pose); + const rightPose = userinput.get(paths.actions.rightHand.pose); const controllerCount = leftPose && rightPose ? 2 : leftPose || rightPose ? 1 : 0; const using6DOF = controllerCount > 1 && this.el.sceneEl.is("vr-mode"); const hand = using6DOF ? this.data.superHand : this.data.cursorSuperHand; diff --git a/src/components/tools/pen.js b/src/components/tools/pen.js index 0f360e9c8469795977cbbaa78773b1dac15559ee..ed6583aa0f8ccc13b7246e40845e82ef4a045998 100644 --- a/src/components/tools/pen.js +++ b/src/components/tools/pen.js @@ -103,20 +103,20 @@ AFRAME.registerComponent("pen", { const userinput = AFRAME.scenes[0].systems.userinput; if (grabber && pathsMap[grabber.id]) { const paths = pathsMap[grabber.id]; - if (userinput.readFrameValueAtPath(paths.startDrawing)) { + if (userinput.get(paths.startDrawing)) { this._startDraw(); } - if (userinput.readFrameValueAtPath(paths.stopDrawing)) { + if (userinput.get(paths.stopDrawing)) { this._endDraw(); } - const penScaleMod = userinput.readFrameValueAtPath(paths.scalePenTip); + const penScaleMod = userinput.get(paths.scalePenTip); if (penScaleMod) { this._changeRadius(penScaleMod); } - if (userinput.readFrameValueAtPath(paths.penNextColor)) { + if (userinput.get(paths.penNextColor)) { this._changeColor(1); } - if (userinput.readFrameValueAtPath(paths.penPrevColor)) { + if (userinput.get(paths.penPrevColor)) { this._changeColor(-1); } } diff --git a/src/hub.js b/src/hub.js index 720be636d61713970d536fb39b55ab937d52dfb5..a19e4e857cc8f450e9c4471afc305c3cc2bf6a86 100644 --- a/src/hub.js +++ b/src/hub.js @@ -72,6 +72,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"; @@ -134,7 +135,6 @@ if (!isBotMode && !isTelemetryDisabled) { disableiOSZoom(); const concurrentLoadDetector = new ConcurrentLoadDetector(); - concurrentLoadDetector.start(); store.init(); @@ -288,6 +288,26 @@ async function runBotMode(scene, entryManager) { } document.addEventListener("DOMContentLoaded", async () => { + const hubId = qs.get("hub_id") || document.location.pathname.substring(1).split("/")[0]; + console.log(`Hub ID: ${hubId}`); + + const subscriptions = new Subscriptions(hubId); + + if (navigator.serviceWorker) { + try { + navigator.serviceWorker + .register("/hub.service.js") + .then(() => { + navigator.serviceWorker.ready + .then(registration => subscriptions.setRegistration(registration)) + .catch(() => subscriptions.setRegistrationFailed()); + }) + .catch(() => subscriptions.setRegistrationFailed()); + } catch (e) { + subscriptions.setRegistrationFailed(); + } + } + const scene = document.querySelector("a-scene"); const hubChannel = new HubChannel(store); const entryManager = new SceneEntryManager(hubChannel); @@ -298,7 +318,14 @@ document.addEventListener("DOMContentLoaded", async () => { window.APP.scene = scene; registerNetworkSchemas(); - remountUI({ hubChannel, linkChannel, enterScene: entryManager.enterScene, exitScene: entryManager.exitScene }); + remountUI({ + hubChannel, + linkChannel, + subscriptions, + enterScene: entryManager.enterScene, + exitScene: entryManager.exitScene, + initialIsSubscribed: subscriptions.isSubscribed() + }); pollForSupportAvailability(isSupportAvailable => remountUI({ isSupportAvailable })); @@ -346,10 +373,6 @@ document.addEventListener("DOMContentLoaded", async () => { } }); - // Connect to reticulum over phoenix channels to get hub info. - const hubId = qs.get("hub_id") || document.location.pathname.substring(1).split("/")[0]; - console.log(`Hub ID: ${hubId}`); - const socket = connectToReticulum(isDebug); remountUI({ sessionId: socket.params().session_id }); @@ -359,13 +382,27 @@ document.addEventListener("DOMContentLoaded", async () => { hmd: availableVREntryTypes.isInHMD }; - const joinPayload = { profile: store.state.profile, context }; + // Reticulum global channel + const retPhxChannel = socket.channel(`ret`, { hub_id: hubId }); + retPhxChannel + .join() + .receive("ok", async data => subscriptions.setVapidPublicKey(data.vapid_public_key)) + .receive("error", res => { + subscriptions.setVapidPublicKey(null); + console.error(res); + }); + + const pushSubscriptionEndpoint = await subscriptions.getCurrentEndpoint(); + const joinPayload = { profile: store.state.profile, push_subscription_endpoint: pushSubscriptionEndpoint, context }; const hubPhxChannel = socket.channel(`hub:${hubId}`, joinPayload); hubPhxChannel .join() .receive("ok", async data => { hubChannel.setPhoenixChannel(hubPhxChannel); + subscriptions.setHubChannel(hubChannel); + subscriptions.setSubscribed(data.subscriptions.web_push); + remountUI({ initialIsSubscribed: subscriptions.isSubscribed() }); await handleHubChannelJoined(entryManager, hubChannel, data); }) .receive("error", res => { @@ -470,8 +507,5 @@ document.addEventListener("DOMContentLoaded", async () => { addToPresenceLog({ name: userInfo.metas[0].profile.displayName, type, body }); }); - // Reticulum global channel - const retPhxChannel = socket.channel(`ret`, { hub_id: hubId }); - retPhxChannel.join().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..73a51a649b07c95425d98c389b0b848a56070968 --- /dev/null +++ b/src/hub.service.js @@ -0,0 +1,46 @@ +self.addEventListener("install", function(e) { + return e.waitUntil(self.skipWaiting()); +}); + +self.addEventListener("activate", function(e) { + return e.waitUntil(self.clients.claim()); +}); + +self.addEventListener("push", function(e) { + const payload = JSON.parse(e.data.text()); + + return e.waitUntil( + self.clients.matchAll({ type: "window" }).then(function(clientList) { + for (let i = 0; i < clientList.length; i++) { + const client = clientList[i]; + if (client.url.indexOf(e.notification.data.hub_id) >= 0) return; + } + + return self.registration.showNotification("Hubs by Mozilla", { + body: "Someone has joined " + payload.hub_name, + image: payload.image, + icon: "/favicon.ico", + badge: "/favicon.ico", + tag: payload.hub_id, + data: { hub_url: payload.hub_url } + }); + }) + ); +}); + +self.addEventListener("notificationclick", function(e) { + e.notification.close(); + + e.waitUntil( + self.clients.matchAll({ type: "window" }).then(function(clientList) { + for (let i = 0; i < clientList.length; i++) { + const client = clientList[i]; + if (client.url.indexOf(e.notification.data.hub_url) >= 0 && "focus" in client) return client.focus(); + } + + if (self.clients.openWindow) { + return self.clients.openWindow(e.notification.data.hub_url); + } + }) + ); +}); diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js index c975d38316c00b4835e1736bb07e1e6723eaf0d4..521490271cab4cc98bbdfab4e682f23d4fef974d 100644 --- a/src/react-components/home-root.js +++ b/src/react-components/home-root.js @@ -231,11 +231,20 @@ class HomeRoot extends Component { /> </div> {this.state.environments.length > 1 && ( - <div className={styles.joinButton}> + <div> <WithHoverSound> - <a href="/link"> - <FormattedMessage id="home.join_room" /> - </a> + <div className={styles.joinButton}> + <a href="/link"> + <FormattedMessage id="home.join_room" /> + </a> + </div> + </WithHoverSound> + <WithHoverSound> + <div className={styles.spokeButton}> + <a href="/spoke"> + <FormattedMessage id="home.create_with_spoke" /> + </a> + </div> </WithHoverSound> </div> )} diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 32f3abc471eb25986ad46b73cae0ac2f8813c1eb..bc7ffb1fc0d973b929a16e6184e433b6726941a3 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -90,7 +90,9 @@ class UIRoot extends Component { isSupportAvailable: PropTypes.bool, presenceLogEntries: PropTypes.array, presences: PropTypes.object, - sessionId: PropTypes.string + sessionId: PropTypes.string, + subscriptions: PropTypes.object, + initialIsSubscribed: PropTypes.bool }; state = { @@ -159,6 +161,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 }); }; @@ -187,6 +194,13 @@ class UIRoot extends Component { this.props.scene.emit("penButtonPressed"); }; + onSubscribeChanged = async () => { + if (!this.props.subscriptions) return; + + await this.props.subscriptions.toggle(); + this.updateSubscribedState(); + }; + handleStartEntry = () => { const promptForNameAndAvatarBeforeEntry = !this.props.store.state.activity.hasChangedName; @@ -713,6 +727,8 @@ class UIRoot extends Component { }; renderEntryStartPanel = () => { + const hasPush = navigator.serviceWorker && "PushManager" in window; + return ( <div className={entryStyles.entryPanel}> <div className={entryStyles.name}>{this.props.hubName}</div> @@ -741,6 +757,20 @@ class UIRoot extends Component { </form> </div> + {hasPush && ( + <div className={entryStyles.subscribe}> + <input + id="subscribe" + type="checkbox" + onChange={this.onSubscribeChanged} + checked={this.state.isSubscribed === undefined ? this.props.initialIsSubscribed : this.state.isSubscribed} + /> + <label htmlFor="subscribe"> + <FormattedMessage id="entry.notify_me" /> + </label> + </div> + )} + <div className={entryStyles.buttonContainer}> <WithHoverSound> <button diff --git a/src/subscriptions.js b/src/subscriptions.js new file mode 100644 index 0000000000000000000000000000000000000000..a5d9fc8afab5ad7b81fdeaf54fc9ecd90bab17f2 --- /dev/null +++ b/src/subscriptions.js @@ -0,0 +1,101 @@ +import nextTick from "./utils/next-tick.js"; + +// 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) { + this.hubId = hubId; + } + + setHubChannel = hubChannel => { + this.hubChannel = hubChannel; + }; + + setRegistration = registration => { + this.registration = registration; + }; + + setRegistrationFailed = () => { + this.registration = null; + }; + + setVapidPublicKey = vapidPublicKey => { + this.vapidPublicKey = vapidPublicKey; + }; + + setSubscribed = isSubscribed => { + this._isSubscribed = isSubscribed; + }; + + isSubscribed = () => { + return this._isSubscribed; + }; + + getCurrentEndpoint = async () => { + if (!navigator.serviceWorker) return null; + + // registration becomes null if failed, non null if registered + while (this.registration === undefined) await nextTick(); + if (!this.registration || !this.registration.pushManager) return null; + + while (this.vapidPublicKey === undefined) await nextTick(); + if (this.vapidPublicKey === null) return null; + + try { + const convertedVapidKey = urlBase64ToUint8Array(this.vapidPublicKey); + + if ( + (await this.registration.pushManager.permissionState({ + userVisibleOnly: true, + applicationServerKey: convertedVapidKey + })) !== "granted" + ) + return null; + } catch (e) { + return null; // Chrome can throw here complaining about userVisible if push is not right + } + const sub = await this.registration.pushManager.getSubscription(); + if (!sub) return null; + + return sub.endpoint; + }; + + toggle = async () => { + if (this._isSubscribed) { + const pushSubscription = await this.registration.pushManager.getSubscription(); + const res = await this.hubChannel.unsubscribe(pushSubscription); + + if (res && res.has_remaining_subscriptions === false) { + pushSubscription.unsubscribe(); + } + } else { + let pushSubscription = await this.registration.pushManager.getSubscription(); + + if (!pushSubscription) { + const convertedVapidKey = urlBase64ToUint8Array(this.vapidPublicKey); + + pushSubscription = await this.registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: convertedVapidKey + }); + } + + this.hubChannel.subscribe(pushSubscription); + } + + this._isSubscribed = !this._isSubscribed; + }; +} diff --git a/src/systems/userinput/bindings/keyboard-mouse-user.js b/src/systems/userinput/bindings/keyboard-mouse-user.js index 1f067d4447effc86541b93d26fb32ffaba5e471b..12e3b3e4f64b814c06427030a7b5e9be7d6aaf0a 100644 --- a/src/systems/userinput/bindings/keyboard-mouse-user.js +++ b/src/systems/userinput/bindings/keyboard-mouse-user.js @@ -3,6 +3,8 @@ import { sets } from "../sets"; import { xforms } from "./xforms"; const wasd_vec2 = "/var/mouse-and-keyboard/wasd_vec2"; +const keyboardCharacterAcceleration = "/var/mouse-and-keyboard/keyboardCharacterAcceleration"; +const arrows_vec2 = "/var/mouse-and-keyboard/arrows_vec2"; const dropWithRMB = "/vars/mouse-and-keyboard/drop_with_RMB"; const dropWithEsc = "/vars/mouse-and-keyboard/drop_with_esc"; @@ -10,7 +12,9 @@ const dropWithRMBorEscBindings = [ { src: { value: paths.device.mouse.buttonRight }, dest: { value: dropWithRMB }, - xform: xforms.falling + xform: xforms.falling, + root: "rmb", + priority: 200 }, { src: { value: paths.device.keyboard.key("Escape") }, @@ -35,6 +39,16 @@ export const keyboardMouseUserBindings = { }, xform: xforms.rising }, + { + src: { + w: paths.device.keyboard.key("arrowup"), + a: paths.device.keyboard.key("arrowleft"), + s: paths.device.keyboard.key("arrowdown"), + d: paths.device.keyboard.key("arrowright") + }, + dest: { vec2: arrows_vec2 }, + xform: xforms.wasd_to_vec2 + }, { src: { w: paths.device.keyboard.key("w"), @@ -46,9 +60,17 @@ export const keyboardMouseUserBindings = { xform: xforms.wasd_to_vec2 }, { - src: { value: wasd_vec2 }, + src: { + first: wasd_vec2, + second: arrows_vec2 + }, + dest: { value: keyboardCharacterAcceleration }, + xform: xforms.max_vec2 + }, + { + src: { value: keyboardCharacterAcceleration }, dest: { value: paths.actions.characterAcceleration }, - xform: xforms.copy + xform: xforms.normalize_vec2 }, { src: { value: paths.device.keyboard.key("shift") }, @@ -87,7 +109,7 @@ export const keyboardMouseUserBindings = { { src: { value: "/var/smartMouseCamDeltaX" }, dest: { value: "/var/smartMouseCamDeltaXScaled" }, - xform: xforms.scale(-0.06) + xform: xforms.scale(-0.2) }, { src: { value: "/var/smartMouseCamDeltaY" }, @@ -116,6 +138,26 @@ export const keyboardMouseUserBindings = { value: paths.actions.logDebugFrame }, xform: xforms.rising + }, + { + src: { + value: paths.device.mouse.buttonRight + }, + dest: { + value: paths.actions.startGazeTeleport + }, + xform: xforms.rising, + root: "rmb", + priority: 100 + }, + { + src: { + value: paths.device.mouse.buttonRight + }, + dest: { + value: paths.actions.stopGazeTeleport + }, + xform: xforms.falling } ], diff --git a/src/systems/userinput/bindings/oculus-touch-user.js b/src/systems/userinput/bindings/oculus-touch-user.js index d386016c27497b4c3faeae1fcc3d413f3a0476fd..1d557129c05e1d7ec718dbadfb08c7c22543b84c 100644 --- a/src/systems/userinput/bindings/oculus-touch-user.js +++ b/src/systems/userinput/bindings/oculus-touch-user.js @@ -47,6 +47,9 @@ const leftJoyY = `${name}left/joyY`; const leftJoyYCursorMod = `${name}left/joyYCursorMod`; const oculusTouchCharacterAcceleration = `${name}characterAcceleration`; const keyboardCharacterAcceleration = "/var/keyboard/characterAcceleration"; +const characterAcceleration = "/var/oculus-touch/nonNormalizedCharacterAcceleration"; +const wasd_vec2 = "/var/keyboard/wasd_vec2"; +const arrows_vec2 = "/var/keyboard/arrows_vec2"; const keyboardBoost = "/var/keyboard-oculus/boost"; const rightBoost = "/var/right-oculus/boost"; const leftBoost = "/var/left-oculus/boost"; @@ -224,6 +227,16 @@ export const oculusTouchUserBindings = { dest: { value: oculusTouchCharacterAcceleration }, xform: xforms.compose_vec2 }, + { + src: { + w: paths.device.keyboard.key("arrowup"), + a: paths.device.keyboard.key("arrowleft"), + s: paths.device.keyboard.key("arrowdown"), + d: paths.device.keyboard.key("arrowright") + }, + dest: { vec2: arrows_vec2 }, + xform: xforms.wasd_to_vec2 + }, { src: { w: paths.device.keyboard.key("w"), @@ -231,18 +244,31 @@ export const oculusTouchUserBindings = { s: paths.device.keyboard.key("s"), d: paths.device.keyboard.key("d") }, - dest: { vec2: keyboardCharacterAcceleration }, + dest: { vec2: wasd_vec2 }, xform: xforms.wasd_to_vec2 }, + { + src: { + first: wasd_vec2, + second: arrows_vec2 + }, + dest: { value: keyboardCharacterAcceleration }, + xform: xforms.max_vec2 + }, { src: { first: oculusTouchCharacterAcceleration, second: keyboardCharacterAcceleration }, dest: { - value: paths.actions.characterAcceleration + value: characterAcceleration }, - xform: xforms.add_vec2 + xform: xforms.max_vec2 + }, + { + src: { value: characterAcceleration }, + dest: { value: paths.actions.characterAcceleration }, + xform: xforms.normalize_vec2 }, { src: { value: paths.device.keyboard.key("shift") }, diff --git a/src/systems/userinput/bindings/vive-user.js b/src/systems/userinput/bindings/vive-user.js index 0e20233c7ff606ee29914e0d5293173404302491..39954438401c372a7070d876f8b04991b198817a 100644 --- a/src/systems/userinput/bindings/vive-user.js +++ b/src/systems/userinput/bindings/vive-user.js @@ -26,6 +26,7 @@ const lTriggerRisingGrab = v("right/trigger/rising/grab"); const lGripRisingGrab = v("right/grab/rising/grab"); const lTouchpadRising = v("left/touchpad/rising"); const lCharacterAcceleration = v("left/characterAcceleration"); +const characterAcceleration = v("nonNormalizedCharacterAcceleration"); const lGripFalling = v("left/grip/falling"); const lGripRising = v("left/grip/rising"); const leftBoost = v("left/boost"); @@ -69,6 +70,8 @@ const k = name => { const keyboardSnapRight = k("snap-right"); const keyboardSnapLeft = k("snap-left"); const keyboardCharacterAcceleration = k("characterAcceleration"); +const wasd_vec2 = k("wasd_vec2"); +const arrows_vec2 = k("arrows_vec2"); const keyboardBoost = k("boost"); const teleportLeft = [ @@ -314,6 +317,16 @@ export const viveUserBindings = { dest: { value: lCharacterAcceleration }, xform: xforms.copyIfTrue }, + { + src: { + w: paths.device.keyboard.key("arrowup"), + a: paths.device.keyboard.key("arrowleft"), + s: paths.device.keyboard.key("arrowdown"), + d: paths.device.keyboard.key("arrowright") + }, + dest: { vec2: arrows_vec2 }, + xform: xforms.wasd_to_vec2 + }, { src: { w: paths.device.keyboard.key("w"), @@ -321,18 +334,31 @@ export const viveUserBindings = { s: paths.device.keyboard.key("s"), d: paths.device.keyboard.key("d") }, - dest: { vec2: keyboardCharacterAcceleration }, + dest: { vec2: wasd_vec2 }, xform: xforms.wasd_to_vec2 }, + { + src: { + first: wasd_vec2, + second: arrows_vec2 + }, + dest: { value: keyboardCharacterAcceleration }, + xform: xforms.max_vec2 + }, { src: { first: lCharacterAcceleration, second: keyboardCharacterAcceleration }, dest: { - value: paths.actions.characterAcceleration + value: characterAcceleration }, - xform: xforms.add_vec2 + xform: xforms.max_vec2 + }, + { + src: { value: characterAcceleration }, + dest: { value: paths.actions.characterAcceleration }, + xform: xforms.normalize_vec2 }, { src: { value: paths.device.keyboard.key("shift") }, diff --git a/src/systems/userinput/bindings/xforms.js b/src/systems/userinput/bindings/xforms.js index 444134675a842316f0f5b52f55ed8ff30083ca15..0ab4cdf5226a3840a26475cf5cd93390b7723a22 100644 --- a/src/systems/userinput/bindings/xforms.js +++ b/src/systems/userinput/bindings/xforms.js @@ -104,6 +104,29 @@ export const xforms = { frame[dest.value] = first; } }, + max_vec2: function(frame, src, dest) { + const first = frame[src.first]; + const second = frame[src.second]; + if (first && second) { + frame[dest.value] = + first[0] * first[0] + first[1] * first[1] > second[0] * second[0] + second[1] * second[1] ? first : second; + } else if (second) { + frame[dest.value] = second; + } else if (first) { + frame[dest.value] = first; + } + }, + normalize_vec2: function(frame, src, dest) { + const vec2 = frame[src.value]; + if (vec2) { + if (vec2[0] === 0 && vec2[0] === 0) { + frame[dest.value] = vec2; + } else { + const l = Math.sqrt(vec2[0] * vec2[0] + vec2[1] * vec2[1]); + frame[dest.value] = [vec2[0] / l, vec2[1] / l]; + } + } + }, any: function(frame, src, dest) { for (const path in src) { if (frame[src[path]]) { diff --git a/src/systems/userinput/devices/app-aware-mouse.js b/src/systems/userinput/devices/app-aware-mouse.js index 4afd124e7006b86869309aaa3626a756b7f20292..66e006d44c56a98c07a39a498cc68581c4d0e6b9 100644 --- a/src/systems/userinput/devices/app-aware-mouse.js +++ b/src/systems/userinput/devices/app-aware-mouse.js @@ -30,13 +30,6 @@ export class AppAwareMouseDevice { this.camera = document.querySelector("#player-camera").components.camera.camera; } - const coords = frame[paths.device.mouse.coords]; - const isCursorGrabbing = this.cursorController.data.cursor.components["super-hands"].state.has("grab-start"); - if (isCursorGrabbing) { - frame[paths.device.smartMouse.cursorPose] = calculateCursorPose(this.camera, coords); - return; - } - const buttonLeft = frame[paths.device.mouse.buttonLeft]; if (buttonLeft && !this.prevButtonLeft) { const rawIntersections = []; @@ -54,8 +47,9 @@ export class AppAwareMouseDevice { if (!this.clickedOnAnything && buttonLeft) { frame[paths.device.smartMouse.cameraDelta] = frame[paths.device.mouse.movementXY]; - } else { - frame[paths.device.smartMouse.cursorPose] = calculateCursorPose(this.camera, coords); } + + const coords = frame[paths.device.mouse.coords]; + frame[paths.device.smartMouse.cursorPose] = calculateCursorPose(this.camera, coords); } } diff --git a/src/systems/userinput/devices/mouse.js b/src/systems/userinput/devices/mouse.js index 0ab7a3ac2aea0ce0ec44ad55fb5804348443bcae..3252fb7372a53406da0992b8b3445a9f854dd065 100644 --- a/src/systems/userinput/devices/mouse.js +++ b/src/systems/userinput/devices/mouse.js @@ -18,24 +18,19 @@ export class MouseDevice { const queueEvent = this.events.push.bind(this.events); const canvas = document.querySelector("canvas"); - ["mousedown", "mouseup", "mousemove", "wheel"].map(x => canvas.addEventListener(x, queueEvent)); - ["mouseout", "blur"].map(x => document.addEventListener(x, queueEvent)); + ["mousedown", "wheel"].map(x => canvas.addEventListener(x, queueEvent)); + ["mousemove", "mouseup"].map(x => window.addEventListener(x, queueEvent)); + document.addEventListener("wheel", e => { + e.preventDefault(); + }); } process(event) { if (event.type === "wheel") { - this.wheel += event.deltaY / modeMod[event.deltaMode]; + this.wheel += (event.deltaX + event.deltaY) / modeMod[event.deltaMode]; return; } - if (event.type === "mouseout" || event.type === "blur") { - this.coords[0] = 0; - this.coords[1] = 0; - this.movementXY[0] = 0; - this.movementXY[1] = 0; - this.buttonLeft = false; - this.buttonRight = false; - this.wheel = 0; - } + const left = event.button === 0; const right = event.button === 2; this.coords[0] = (event.clientX / window.innerWidth) * 2 - 1; diff --git a/src/systems/userinput/resolve-action-sets.js b/src/systems/userinput/resolve-action-sets.js index d1375f1b02df1c6f3d56463824a13600383fea6b..4341064a1085ba55f3584d4a3a5e00bcd5a8d6d7 100644 --- a/src/systems/userinput/resolve-action-sets.js +++ b/src/systems/userinput/resolve-action-sets.js @@ -136,30 +136,30 @@ export function updateActionSetsBasedOnSuperhands() { !cursorHoveringOnUI; const userinput = AFRAME.scenes[0].systems.userinput; - userinput.toggleActive(sets.leftHandHoveringOnInteractable, leftHandHoveringOnInteractable); - userinput.toggleActive(sets.leftHandHoveringOnPen, leftHandHoveringOnPen); - userinput.toggleActive(sets.leftHandHoveringOnCamera, leftHandHoveringOnCamera); - userinput.toggleActive(sets.leftHandHoveringOnNothing, leftHandHoveringOnNothing); - userinput.toggleActive(sets.leftHandHoldingPen, leftHandHoldingPen); - userinput.toggleActive(sets.leftHandHoldingInteractable, leftHandHoldingInteractable); - userinput.toggleActive(sets.leftHandHoldingCamera, leftHandHoldingCamera); - userinput.toggleActive(sets.leftHandTeleporting, leftHandTeleporting); + userinput.toggleSet(sets.leftHandHoveringOnInteractable, leftHandHoveringOnInteractable); + userinput.toggleSet(sets.leftHandHoveringOnPen, leftHandHoveringOnPen); + userinput.toggleSet(sets.leftHandHoveringOnCamera, leftHandHoveringOnCamera); + userinput.toggleSet(sets.leftHandHoveringOnNothing, leftHandHoveringOnNothing); + userinput.toggleSet(sets.leftHandHoldingPen, leftHandHoldingPen); + userinput.toggleSet(sets.leftHandHoldingInteractable, leftHandHoldingInteractable); + userinput.toggleSet(sets.leftHandHoldingCamera, leftHandHoldingCamera); + userinput.toggleSet(sets.leftHandTeleporting, leftHandTeleporting); - userinput.toggleActive(sets.rightHandHoveringOnInteractable, rightHandHoveringOnInteractable); - userinput.toggleActive(sets.rightHandHoveringOnPen, rightHandHoveringOnPen); - userinput.toggleActive(sets.rightHandHoveringOnNothing, rightHandHoveringOnNothing); - userinput.toggleActive(sets.rightHandHoveringOnCamera, rightHandHoveringOnCamera); - userinput.toggleActive(sets.rightHandHoldingPen, rightHandHoldingPen); - userinput.toggleActive(sets.rightHandHoldingInteractable, rightHandHoldingInteractable); - userinput.toggleActive(sets.rightHandTeleporting, rightHandTeleporting); - userinput.toggleActive(sets.rightHandHoldingCamera, rightHandHoldingCamera); + userinput.toggleSet(sets.rightHandHoveringOnInteractable, rightHandHoveringOnInteractable); + userinput.toggleSet(sets.rightHandHoveringOnPen, rightHandHoveringOnPen); + userinput.toggleSet(sets.rightHandHoveringOnNothing, rightHandHoveringOnNothing); + userinput.toggleSet(sets.rightHandHoveringOnCamera, rightHandHoveringOnCamera); + userinput.toggleSet(sets.rightHandHoldingPen, rightHandHoldingPen); + userinput.toggleSet(sets.rightHandHoldingInteractable, rightHandHoldingInteractable); + userinput.toggleSet(sets.rightHandTeleporting, rightHandTeleporting); + userinput.toggleSet(sets.rightHandHoldingCamera, rightHandHoldingCamera); - userinput.toggleActive(sets.cursorHoveringOnPen, cursorHoveringOnPen); - userinput.toggleActive(sets.cursorHoveringOnCamera, cursorHoveringOnCamera); - userinput.toggleActive(sets.cursorHoveringOnInteractable, cursorHoveringOnInteractable); - userinput.toggleActive(sets.cursorHoveringOnUI, cursorHoveringOnUI); - userinput.toggleActive(sets.cursorHoveringOnNothing, cursorHoveringOnNothing); - userinput.toggleActive(sets.cursorHoldingPen, cursorHoldingPen); - userinput.toggleActive(sets.cursorHoldingCamera, cursorHoldingCamera); - userinput.toggleActive(sets.cursorHoldingInteractable, cursorHoldingInteractable); + userinput.toggleSet(sets.cursorHoveringOnPen, cursorHoveringOnPen); + userinput.toggleSet(sets.cursorHoveringOnCamera, cursorHoveringOnCamera); + userinput.toggleSet(sets.cursorHoveringOnInteractable, cursorHoveringOnInteractable); + userinput.toggleSet(sets.cursorHoveringOnUI, cursorHoveringOnUI); + userinput.toggleSet(sets.cursorHoveringOnNothing, cursorHoveringOnNothing); + userinput.toggleSet(sets.cursorHoldingPen, cursorHoldingPen); + userinput.toggleSet(sets.cursorHoldingCamera, cursorHoldingCamera); + userinput.toggleSet(sets.cursorHoldingInteractable, cursorHoldingInteractable); } diff --git a/src/systems/userinput/userinput.js b/src/systems/userinput/userinput.js index 2ffbdca79db0e2d07d464f32ea320fc8cb31c329..8442cd73a763cf2fa8881e04267fb68d653ac0f1 100644 --- a/src/systems/userinput/userinput.js +++ b/src/systems/userinput/userinput.js @@ -26,29 +26,27 @@ import { updateActionSetsBasedOnSuperhands } from "./resolve-action-sets"; import { GamepadDevice } from "./devices/gamepad"; import { gamepadBindings } from "./bindings/generic-gamepad"; -const prioritizedBindings = new Map(); +const priorityMap = new Map(); function prioritizeBindings(registeredMappings, activeSets) { const activeBindings = new Set(); - prioritizedBindings.clear(); + priorityMap.clear(); for (const mapping of registeredMappings) { for (const setName in mapping) { if (!activeSets.has(setName) || !mapping[setName]) continue; for (const binding of mapping[setName]) { const { root, priority } = binding; + const prevBinding = priorityMap.get(root); if (!root || !priority) { activeBindings.add(binding); - } else if (!prioritizedBindings.has(root)) { + } else if (!prevBinding) { activeBindings.add(binding); - prioritizedBindings.set(root, binding); - } else { - const prevPriority = prioritizedBindings.get(root).priority; - if (priority > prevPriority) { - activeBindings.delete(prioritizedBindings.get(root)); - activeBindings.add(binding); - prioritizedBindings.set(root, binding); - } else if (prevPriority === priority) { - console.error("equal priorities on same root", binding, prioritizedBindings.get(root)); - } + priorityMap.set(root, binding); + } else if (priority > prevBinding.priority) { + activeBindings.delete(priorityMap.get(root)); + activeBindings.add(binding); + priorityMap.set(root, binding); + } else if (prevBinding.priority === priority) { + console.error("equal priorities on same root", binding, priorityMap.get(root)); } } } @@ -57,11 +55,11 @@ function prioritizeBindings(registeredMappings, activeSets) { } AFRAME.registerSystem("userinput", { - readFrameValueAtPath(path) { + get(path) { return this.frame && this.frame[path]; }, - toggleActive(set, value) { + toggleSet(set, value) { this.pendingSetChanges.push({ set, value }); }, diff --git a/src/systems/userinput/userinput.md b/src/systems/userinput/userinput.md index d3701d5c93dd34a4be3828108d29a7a5c094c2a0..b8ceb54539665f0130863a6018b187bf0562a8aa 100644 --- a/src/systems/userinput/userinput.md +++ b/src/systems/userinput/userinput.md @@ -1,19 +1,4 @@ -# Table of Contents - -1. [The userinput system](#org6030eab) - 1. [Overview](#org2da9acd) - 2. [Terms and Conventions](#org4721ce9) - 1. [path](#orgd62cc68) - 2. [action](#orgb8066a6) - 3. [frame](#org15eafde) - 4. [device](#orgea2f123) - 5. [binding](#org47c9c20) - 6. [xforms](#org876e7b0) - 7. [set](#orgbe4669b) - 8. [priority and root](#orgdd3c0c5) - - <a id="org6030eab"></a> # The userinput system @@ -25,7 +10,7 @@ The userinput system is a module that manages mappings from device state changes ## Overview -The userinput system happens to be an `aframe` `system`; its `tick` is called once a frame within the `aframe` `scene`'s `tick`. When the userinput system `tick` happens, it is responsible for creating a map called the frame. The keys of the frame are called "paths". The values stored in the frame can be any type, but are usually one of: bool, number, vec2, vec3, vec4, pose. On each tick, each connected `device` writes "raw" input values to known "device paths" within the frame. Configuration units called `bindings` are then applied to transform "raw" input values to app-specific "actions". The userinput system exposes the state of a given `action` in the current frame via `readFrameValueAtPath`. The `bindings` that are applied to transform input to "actions" must be `available`, `active`, and `prioritized`. +The userinput system happens to be an `aframe` `system`; its `tick` is called once a frame within the `aframe` `scene`'s `tick`. When the userinput system `tick` happens, it is responsible for creating a map called the frame. The keys of the frame are called "paths". The values stored in the frame can be any type, but are usually one of: bool, number, vec2, vec3, vec4, pose. On each tick, each connected `device` writes "raw" input values to known "device paths" within the frame. Configuration units called `bindings` are then applied to transform "raw" input values to app-specific "actions". The userinput system exposes the state of a given `action` in the current frame via `get`. The `bindings` that are applied to transform input to "actions" must be `available`, `active`, and `prioritized`. 1. A `binding` is made `available` when the userinput system detects a change to the user's device configuration that matches certain criteria. A touchscreen user only has `availableBindings` related to touchscreen input. A mouse-and-keyboard user only has `availableBindings` related to mouse-and-keyboard input. An oculus/vive user has `bindings` related to mouse, keyboard, and oculus/vive controllers. @@ -70,14 +55,14 @@ A path is used as a key when writing or querying the state a user input frame. P A path used by app code when reading a user input frame. const userinput = AFRAME.scenes[0].systems.userinput; - if (userinput.readFrameValueAtPath("/actions/rightHandGrab")) { + if (userinput.get("/actions/rightHandGrab")) { this.startInteraction(); } The value in the frame can be of any type, but we have tried to keep it to simple types like bool, number, vec2, vec3, and pose. const userinput = AFRAME.scenes[0].systems.userinput; - const acceleration = userinput.readFrameValueAtPath("/actions/characterAcceleration"); + const acceleration = userinput.get("/actions/characterAcceleration"); this.updateVelocity( this.velocity, acceleration || zero ); this.move( this.velocity ); diff --git a/src/utils/hub-channel.js b/src/utils/hub-channel.js index 734cafa5fcc11c8ef84d708da4e16c32b54396aa..81714efc2fe069ce43cc8d0721c4550a2f42667d 100644 --- a/src/utils/hub-channel.js +++ b/src/utils/hub-channel.js @@ -91,6 +91,14 @@ export default class HubChannel { this.channel.push("events:profile_updated", { profile: this.store.state.profile }); }; + subscribe = subscription => { + this.channel.push("subscribe", { subscription }); + }; + + unsubscribe = subscription => { + return new Promise(resolve => this.channel.push("unsubscribe", { subscription }).receive("ok", resolve)); + }; + sendMessage = (body, type = "chat") => { if (!body) return; this.channel.push("message", { body, type }); 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",