diff --git a/package.json b/package.json index d14d1c7a5166f8cf321335363872fa2da4806c6b..219811f62a5a6226c7d26fe712205e580c8f1858 100644 --- a/package.json +++ b/package.json @@ -32,13 +32,14 @@ "classnames": "^2.2.5", "detect-browser": "^2.1.0", "event-target-shim": "^3.0.1", + "form-urlencoded": "^2.0.4", "jsonschema": "^1.2.2", "minijanus": "^0.5.0", "mobile-detect": "^1.4.1", "moment": "^2.22.0", "moment-timezone": "^0.5.14", "moving-average": "^1.0.0", - "naf-janus-adapter": "https://github.com/mozilla/naf-janus-adapter#feature/disconnect", + "naf-janus-adapter": "^0.5.2", "networked-aframe": "https://github.com/mozillareality/networked-aframe#mr-social-client/master", "nipplejs": "^0.6.7", "phoenix": "^1.3.0", diff --git a/src/App.js b/src/App.js index c542f439f903e0a4426d087cb33d69852b25944d..b814e080d4e1df7ae5f6dbe7649d2548211f4c42 100644 --- a/src/App.js +++ b/src/App.js @@ -1,7 +1,10 @@ +import Store from "./storage/store"; + export class App { constructor() { this.scene = null; this.quality = "low"; + this.store = new Store(); } setQuality(quality) { diff --git a/src/assets/hud/avatar.jpg b/src/assets/hud/avatar.jpg deleted file mode 100755 index af8093c601f6dcbc74b431ff7f7aca414cf4dc01..0000000000000000000000000000000000000000 Binary files a/src/assets/hud/avatar.jpg and /dev/null differ diff --git a/src/assets/hud/avatar.png b/src/assets/hud/avatar.png deleted file mode 100644 index d604f8f18d51b481bcc0013f30e2c610100b231c..0000000000000000000000000000000000000000 Binary files a/src/assets/hud/avatar.png and /dev/null differ diff --git a/src/assets/hud/bubble_off-hover.png b/src/assets/hud/bubble_off-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..efa274eb628b529cbf3959cdb3db9597ead49112 Binary files /dev/null and b/src/assets/hud/bubble_off-hover.png differ diff --git a/src/assets/hud/bubble_off.png b/src/assets/hud/bubble_off.png new file mode 100644 index 0000000000000000000000000000000000000000..782f4d1aa025d5d2ae08bbf0fb745989c67b4157 Binary files /dev/null and b/src/assets/hud/bubble_off.png differ diff --git a/src/assets/hud/bubble_on-hover.png b/src/assets/hud/bubble_on-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..ddc5a202747eeaf513f1261664e56b294fb51b32 Binary files /dev/null and b/src/assets/hud/bubble_on-hover.png differ diff --git a/src/assets/hud/bubble_on.png b/src/assets/hud/bubble_on.png new file mode 100644 index 0000000000000000000000000000000000000000..0a70407d65498848bd50c6ebcb7167c35c0d8a5f Binary files /dev/null and b/src/assets/hud/bubble_on.png differ diff --git a/src/assets/hud/freeze_off-hover.png b/src/assets/hud/freeze_off-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..80af8637d4d75559166529fb53e96c424d0be695 Binary files /dev/null and b/src/assets/hud/freeze_off-hover.png differ diff --git a/src/assets/hud/freeze_off.png b/src/assets/hud/freeze_off.png new file mode 100644 index 0000000000000000000000000000000000000000..302c7a23742b0ac94a7ffc1406935bf90a0a9be6 Binary files /dev/null and b/src/assets/hud/freeze_off.png differ diff --git a/src/assets/hud/freeze_on-hover.png b/src/assets/hud/freeze_on-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..ce846be44344e1bfa5bb394bc36a1be25599ee67 Binary files /dev/null and b/src/assets/hud/freeze_on-hover.png differ diff --git a/src/assets/hud/freeze_on.png b/src/assets/hud/freeze_on.png new file mode 100644 index 0000000000000000000000000000000000000000..edf0da5b9e7f78a00af084cf0429e086289fba01 Binary files /dev/null and b/src/assets/hud/freeze_on.png differ diff --git a/src/assets/hud/mute_off-hover.png b/src/assets/hud/mute_off-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..4e064fb57702c605d08211f836e94dcbbf135a89 Binary files /dev/null and b/src/assets/hud/mute_off-hover.png differ diff --git a/src/assets/hud/mute_off.png b/src/assets/hud/mute_off.png new file mode 100644 index 0000000000000000000000000000000000000000..9dcce25b132006b1de0228458692659cec6c93f3 Binary files /dev/null and b/src/assets/hud/mute_off.png differ diff --git a/src/assets/hud/mute_on-hover.png b/src/assets/hud/mute_on-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..f823c06561115c5208cce101dde4fe3d203d4c18 Binary files /dev/null and b/src/assets/hud/mute_on-hover.png differ diff --git a/src/assets/hud/mute_on.png b/src/assets/hud/mute_on.png new file mode 100644 index 0000000000000000000000000000000000000000..e5862312db1c45889cf37613cf3a01dad0566615 Binary files /dev/null and b/src/assets/hud/mute_on.png differ diff --git a/src/assets/hud/muted.png b/src/assets/hud/muted.png deleted file mode 100644 index b557fd0195e53a1dc9b4873db65b69afb0b7d1b1..0000000000000000000000000000000000000000 Binary files a/src/assets/hud/muted.png and /dev/null differ diff --git a/src/assets/hud/unmuted.png b/src/assets/hud/unmuted.png deleted file mode 100755 index 71f1af21bdd72aaa22ebe740e84a481aa1dc8b3f..0000000000000000000000000000000000000000 Binary files a/src/assets/hud/unmuted.png and /dev/null differ diff --git a/src/assets/stylesheets/2d-hud.css b/src/assets/stylesheets/2d-hud.css deleted file mode 100644 index a50436114181d18997248dce77a3cc1d9500363f..0000000000000000000000000000000000000000 --- a/src/assets/stylesheets/2d-hud.css +++ /dev/null @@ -1,102 +0,0 @@ -:local(.container) { - position: absolute; - top: 10px; - display: flex; - justify-content: center; - align-items: center; - height: 80px; - width: 100%; -} - -:local(.panel) { - display: flex; - justify-content: space-around; - align-items: center; - padding: 10px; - background-color: rgba(0, 0, 0, 0.35); -} - -:local(.panel.left) { - border-bottom-left-radius: 30px; - border-top-left-radius: 30px; - padding-left: 20px; - padding-right: 50px; - margin-right: -40px; -} - -:local(.panel.right) { - border-bottom-right-radius: 30px; - border-top-right-radius: 30px; - padding-right: 20px; - padding-left: 50px; - margin-left: -40px; -} - -:local(.modeButton) { - width: 80px; - height: 80px; - display: flex; - align-items: center; - justify-content: center; - z-index: 10; -} - -:local(.panel) { - display: flex; - justify-content: space-around; - align-items: center; - padding: 10px; - background-color: rgba(0, 0, 0, 0.35); -} - - -:local(.nametag) { - display: flex; - justify-content: center; - font-size: 32px; - font-family: sans-serif; - color: white; - margin: 0 10px 0 0; -} - -:local(.avatar) { - width: 80px; - height: 80px; - background-size: 100%; - background-image: url(../hud/avatar.png); -} - -:local(.mic) { - display: flex; - width: 32px; - height: 32px; - -webkit-mask: url(../hud/unmuted.png); - -webkit-mask-size: 32px; - mask: url(../hud/unmuted.png); - mask-size: 32px; - background-color: white; - cursor: pointer; -} - -:local(.mic:hover) { - background-color: cyan; -} - -:local(.mic.muted:hover) { - background-color: cyan; -} - -:local(.mic:active) { - background-color: red; -} - -:local(.mic.muted) { - -webkit-mask: url(../hud/muted.png); - -webkit-mask-size: 32px; - mask: url(../hud/muted.png); - mask-size: 32px; -} - -:local(.mic.muted:active) { - background-color: green; -} diff --git a/src/assets/stylesheets/2d-hud.scss b/src/assets/stylesheets/2d-hud.scss new file mode 100644 index 0000000000000000000000000000000000000000..2fe1736009876a36c2dd29d0c0e6de6c709220bf --- /dev/null +++ b/src/assets/stylesheets/2d-hud.scss @@ -0,0 +1,85 @@ +:local(.container) { + position: absolute; + top: 10px; + display: flex; + justify-content: center; + align-items: center; + height: 80px; + width: 100%; +} + +:local(.panel) { + display: flex; + justify-content: space-around; + align-items: center; + padding: 5px; + background-color: rgba(#4F4F4F, 0.45); +} + +:local(.panel.left) { + border-bottom-left-radius: 30px; + border-top-left-radius: 30px; + padding-left: 5px; + padding-right: 45px; + margin-right: -40px; +} + +:local(.panel.right) { + border-bottom-right-radius: 30px; + border-top-right-radius: 30px; + padding-right: 5px; + padding-left: 45px; + margin-left: -40px; +} + +:local(.iconButton) { + width: 40px; + height: 40px; + background-size: 100%; + cursor: pointer; +} + +:local(.iconButton.large) { + width: 80px; + height: 80px; + z-index: 10; +} + +:local(.iconButton.mute) { + background-image: url(../hud/mute_off.png); +} +:local(.iconButton.mute:hover) { + background-image: url(../hud/mute_off-hover.png); +} +:local(.iconButton.mute.active) { + background-image: url(../hud/mute_on.png); +} +:local(.iconButton.mute.active:hover) { + background-image: url(../hud/mute_on-hover.png); +} + +:local(.iconButton.bubble) { + background-image: url(../hud/bubble_off.png); +} +:local(.iconButton.bubble:hover) { + background-image: url(../hud/bubble_off-hover.png); +} +:local(.iconButton.bubble.active) { + background-image: url(../hud/bubble_on.png); +} +:local(.iconButton.bubble.active:hover) { + background-image: url(../hud/bubble_on-hover.png); +} + +:local(.iconButton.freeze) { + background-image: url(../hud/freeze_off.png); +} +:local(.iconButton.freeze:hover) { + background-image: url(../hud/freeze_off-hover.png); +} +:local(.iconButton.freeze.active) { + background-image: url(../hud/freeze_on.png); +} +:local(.iconButton.freeze.active:hover) { + background-image: url(../hud/freeze_on-hover.png); +} \ No newline at end of file diff --git a/src/assets/stylesheets/entry.scss b/src/assets/stylesheets/entry.scss index abed31db312891d5d0d4c425b42310851df21553..7c7d18c840410378a86d9a4b41211777808a60b0 100644 --- a/src/assets/stylesheets/entry.scss +++ b/src/assets/stylesheets/entry.scss @@ -51,6 +51,7 @@ background: none; color: white; border: none; + align-items: center; @extend %default-font; &__icon { diff --git a/src/assets/stylesheets/index.scss b/src/assets/stylesheets/index.scss index 77615e9d341f4ca5688ebe6f0e209163f2c884cf..38833657208e9a58a19ef8b10add9e8a5ceaa25d 100644 --- a/src/assets/stylesheets/index.scss +++ b/src/assets/stylesheets/index.scss @@ -34,6 +34,10 @@ body { display: flex; flex-direction: column; z-index: 2; + + &--noninteractive { + pointer-events: none; + } } .background-video { @@ -49,7 +53,7 @@ body { .header-content { padding: 1.5em 2.5em 1.5em 2.5em; background-color: rgba(0, 0, 0, 0.85); - min-height: 90px; + height: 90px; display: flex; border-bottom: 2px solid #242424; @@ -217,3 +221,111 @@ body { } } } + +.overlay { + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + pointer-events: none; + color: white; + z-index: 2; +} + +.mailing-list-form { + display: flex; + height: 100%; + flex-direction: column; + justify-content: center; + text-align: center; + margin: 0; + + &__first { + width: 100%; + } + + &__email_field { + @extend %rounded-border; + @extend %default-font; + color: $light-text; + font-size: 1.2em; + background-color: transparent; + line-height: 2.0em; + padding-left: 1.25em; + padding-right: 1.25em; + margin: 0.5em 0; + width: 100%; + } + + &__submit { + @extend %bottom-button; + border: 0; + margin-top: 16px; + } + + &__privacy { + margin-top: 10px; + font-size: 0.7em; + } +} + +.dialog { + display: grid; + grid-template-columns: 1fr 20px minmax(200px,500px) 20px 1fr; + grid-template-rows: 1fr 20px 275px 20px 1fr; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,.6); + + &__box { + grid-column: 3; + grid-row: 3; + position: relative; + pointer-events: auto; + + &__contents { + background-color: rgba(0,0,0,0.8); + border-radius: 8px; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + position: relative; + + &__title { + @extend %top-title; + margin-top: 20px; + } + + &__body { + margin: 40px; + font-size: 1.1em; + margin-top: 20px; + color: $grey-text; + display: flex; + flex-direction: column; + + a { color: white } + } + + &__close { + position: absolute; + left: 12px; + top: 6px; + color: white; + font-size: 1.4em; + + background: none; + cursor: pointer; + border: none; + } + } + } +} + + + diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 1b9844c8abf2dc7d9ed7c089f84923faa601fd96..2ad5c1cf95deb131b7b8b4aa02add9f63c11cc7f 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -1,6 +1,5 @@ { - "en": - { + "en": { "entry.screen-prefix": "Enter on ", "entry.desktop-screen": "Screen", "entry.mobile-screen": "Phone", @@ -33,7 +32,7 @@ "audio.granted-title": "Mic permissions granted", "audio.granted-subtitle": "You can still mute yourself in-game", "audio.granted-next": "NEXT", - "exit.subtitle": "Your session has ended.", + "exit.subtitle": "Your session has ended. Refresh your browser to start a new one.", "autoexit.title": "Auto-ending session in ", "autoexit.title_units": " seconds", "autoexit.subtitle": "You have started another session.", @@ -45,12 +44,15 @@ "home.webvr_disclaimer_post": " experiment by ", "home.webvr_disclaimer_mr_team": "Mozilla Mixed Reality", "home.view_source": "View Source", - "home.join_on_slack": "Join us on Slack", + "home.join_us": "Join the Conversation", "home.report_issue": "Report an Issue", "home.get_updates": "Get Updates", "home.hero_title": "A new way to get together online.", "home.hero_subtitle": "Laugh, play, get stuff done, or just hang out.", - "home.made_with_love": "made with â¤ï¸ by ", - "home.environment_author_by": " by " + "home.made_with_love": "made with 🦆 by ", + "home.environment_author_by": " by ", + "home.dialog.close": "CLOSE", + "mailing_list.privacy_label": "I'm okay with Mozilla handling my info as explained in", + "mailing_list.privacy_link": "this Privacy Notice" } } diff --git a/src/components/freeze-controller.js b/src/components/freeze-controller.js new file mode 100644 index 0000000000000000000000000000000000000000..77e66f91f167d0f01ea4acac406c6f32eac83039 --- /dev/null +++ b/src/components/freeze-controller.js @@ -0,0 +1,27 @@ +AFRAME.registerComponent("freeze-controller", { + schema: { + toggleEvent: { type: "string" } + }, + + init: function() { + this.onToggle = this.onToggle.bind(this); + }, + + play: function() { + this.el.addEventListener(this.data.toggleEvent, this.onToggle); + }, + + pause: function() { + this.el.removeEventListener(this.data.toggleEvent, this.onToggle); + }, + + onToggle: function() { + window.APP.store.update({ profile: { has_found_freeze: true } }); + NAF.connection.adapter.toggleFreeze(); + if (NAF.connection.adapter.frozen) { + this.el.addState("frozen"); + } else { + this.el.removeState("frozen"); + } + } +}); diff --git a/src/components/hud-controller.js b/src/components/hud-controller.js index 4ee274f34ae9daa6f24c8e414eee824b845c49db..d2439b3765d0272ae5b113fa174b1738ab213c2b 100644 --- a/src/components/hud-controller.js +++ b/src/components/hud-controller.js @@ -15,7 +15,8 @@ AFRAME.registerComponent("hud-controller", { offset: { default: 0.7 }, // distance from hud above head, lookCutoff: { default: 20 }, // angle at which the hud should be "on", animRange: { default: 30 }, // degrees over which to animate the hud into view - yawCutoff: { default: 50 } // yaw degrees at wich the hud should reoirent even if the user is looking up + yawCutoff: { default: 50 }, // yaw degrees at wich the hud should reoirent even if the user is looking up + showTip: { type: "bool" } }, init() { this.isYLocked = false; @@ -33,14 +34,34 @@ AFRAME.registerComponent("hud-controller", { const head = this.data.head.object3D; const sceneEl = this.el.sceneEl; - const { offset, lookCutoff, animRange, yawCutoff } = this.data; + const { offset, lookCutoff, animRange, yawCutoff, showTip } = this.data; const pitch = head.rotation.x * THREE.Math.RAD2DEG; const yawDif = deltaAngle(head.rotation.y, hud.rotation.y) * THREE.Math.RAD2DEG; - // Reorient the hud only if the user is looking away from the hud, for right now this arbitrarily means the hud is 1/3 way animated away + // animate the hud into place over animRange degrees as the user aproaches the lookCutoff angle + let t = 1 - THREE.Math.clamp(lookCutoff - pitch, 0, animRange) / animRange; + + // HUD is locked down while showing tooltip + if (showTip) { + t = 1; + } + + // Once the HUD is in place it should stay in place until you look sufficiently far down + if (t === 1) { + this.lockedHeadPositionY = head.position.y; + this.hudLocked = true; + } else if (this.hudLocked && pitch < lookCutoff - animRange / 2) { + this.hudLocked = false; + } + + if (this.hudLocked) { + t = 1; + } + + // Reorient the hud only if the user is looking away from the hud, for right now this arbitrarily means the hud is 1/2 way animated away // TODO: come up with better huristics for this that maybe account for the user turning away from the hud "too far", also animate the position so that it doesnt just snap. - if (yawDif >= yawCutoff || pitch < lookCutoff - animRange / 3) { + if (yawDif >= yawCutoff || pitch < lookCutoff - animRange / 2) { const lookDir = new THREE.Vector3(0, 0, -1); lookDir.applyQuaternion(head.quaternion); lookDir.add(head.position); @@ -48,18 +69,6 @@ AFRAME.registerComponent("hud-controller", { hud.position.z = lookDir.z; hud.setRotationFromEuler(new THREE.Euler(0, head.rotation.y, 0)); } - - // animate the hud into place over animRange degrees as the user aproaches the lookCutoff angle - const t = 1 - THREE.Math.clamp(lookCutoff - pitch, 0, animRange) / animRange; - - // Lock the hud in place relative to a known head position so it doesn't bob up and down - // with the user's head - if (!this.isYLocked && t === 1) { - this.lockedHeadPositionY = head.position.y; - } - const EPSILON = 0.001; - this.isYLocked = t > 1 - EPSILON; - hud.position.y = (this.isYLocked ? this.lockedHeadPositionY : head.position.y) + offset + (1 - t) * offset; hud.rotation.x = (1 - t) * THREE.Math.DEG2RAD * 90; diff --git a/src/components/icon-button.js b/src/components/icon-button.js new file mode 100644 index 0000000000000000000000000000000000000000..eab80803f4d85ad425a6d5a0c71fc8f6f9ddc333 --- /dev/null +++ b/src/components/icon-button.js @@ -0,0 +1,57 @@ +AFRAME.registerComponent("icon-button", { + schema: { + image: { type: "string" }, + hoverImage: { type: "string" }, + activeImage: { type: "string" }, + activeHoverImage: { type: "string" }, + active: { type: "boolean" }, + haptic: { type: "selector" } + }, + + init() { + this.onHover = () => { + this.hovering = true; + this.updateButtonState(); + this.emitHapticPulse(); + }; + this.onHoverOut = () => { + this.hovering = false; + this.updateButtonState(); + }; + this.onClick = () => { + this.emitHapticPulse(); + }; + }, + + emitHapticPulse() { + if (this.data.haptic) { + this.data.haptic.emit("haptic_pulse", { intensity: "low" }); + } + }, + + play() { + this.updateButtonState(); + this.el.addEventListener("mouseover", this.onHover); + this.el.addEventListener("mouseout", this.onHoverOut); + this.el.addEventListener("click", this.onClick); + }, + + pause() { + this.el.removeEventListener("mouseover", this.onHover); + this.el.removeEventListener("mouseout", this.onHoverOut); + this.el.removeEventListener("click", this.onClick); + }, + + update() { + this.updateButtonState(); + }, + + updateButtonState() { + const hovering = this.hovering; + const active = this.data.active; + + const image = active ? (hovering ? "activeHoverImage" : "activeImage") : hovering ? "hoverImage" : "image"; + + this.el.setAttribute("src", this.data[image]); + } +}); diff --git a/src/components/in-world-hud.js b/src/components/in-world-hud.js index 9e307e498256da344e5e2e3533baf82b09f93b6f..6b6c28bfc2cc97f1a3a776f9c4c12edd867457a3 100644 --- a/src/components/in-world-hud.js +++ b/src/components/in-world-hud.js @@ -5,99 +5,41 @@ AFRAME.registerComponent("in-world-hud", { }, init() { this.mic = this.el.querySelector(".mic"); + this.freeze = this.el.querySelector(".freeze"); - const muted = this.el.sceneEl.is("muted"); - this.mic.setAttribute("src", muted ? "#muted" : "#unmuted"); - - this.showCorrectMuteState = () => { - const muted = this.el.sceneEl.is("muted"); - this.mic.setAttribute("src", muted ? "#muted" : "#unmuted"); + this.updateButtonStates = () => { + this.mic.setAttribute("icon-button", "active", this.el.sceneEl.is("muted")); + this.freeze.setAttribute("icon-button", "active", this.el.sceneEl.is("frozen")); }; + this.updateButtonStates(); this.onStateChange = evt => { - if (evt.detail !== "muted") return; - this.showCorrectMuteState(); - }; - - this.onMicHover = () => { - this.hoveredOnMic = true; - this.data.haptic.emit("haptic_pulse", { intensity: "low" }); - this.mic.setAttribute("material", "color", "#1DD"); - }; - - this.onMicHoverExit = () => { - this.hoveredOnMic = false; - this.mic.setAttribute("material", "color", "#FFF"); - this.showCorrectMuteState(); + if (!(evt.detail === "muted" || evt.detail === "frozen")) return; + this.updateButtonStates(); }; - this.onMicDown = () => { - this.data.haptic.emit("haptic_pulse", { intensity: "medium" }); - this.el.sceneEl.removeEventListener("micAudio", this.onAudioFrequencyChange); - this.mic.setAttribute("material", "color", this.el.sceneEl.is("muted") ? "#0FA" : "#F33"); + this.onMicClick = () => { this.el.emit("action_mute"); - window.setTimeout(() => { - this.mic.setAttribute("material", "color", "#FFF"); - this.el.sceneEl.addEventListener("micAudio", this.onAudioFrequencyChange); - }, 150); - }; - - this.onClick = () => { - if (this.hoveredOnMic) { - this.onMicDown(); - } }; - this.onAudioFrequencyChange = e => { - if (this.hoveredOnMic) return; - const red = 1.0 - e.detail.volume / 10.0; - this.mic.object3DMap.mesh.material.color = { r: red, g: 9, b: 9 }; + this.onFreezeClick = () => { + this.el.emit("action_freeze"); }; - - this.el.sceneEl.addEventListener("mediaStream", evt => { - this.ms = evt.detail.ms; - const ctx = THREE.AudioContext.getContext(); - const source = ctx.createMediaStreamSource(this.ms); - this.analyser = ctx.createAnalyser(); - this.levels = new Uint8Array(this.analyser.frequencyBinCount); - source.connect(this.analyser); - }); }, play() { - this.mic.addEventListener("raycaster-intersected", this.onMicHover); - this.mic.addEventListener("raycaster-intersected-cleared", this.onMicHoverExit); - this.el.sceneEl.addEventListener("stateadded", this.onStateChange); this.el.sceneEl.addEventListener("stateremoved", this.onStateChange); - this.el.addEventListener("click", this.onClick); - - this.el.sceneEl.addEventListener("micAudio", this.onAudioFrequencyChange); + this.mic.addEventListener("click", this.onMicClick); + this.freeze.addEventListener("click", this.onFreezeClick); }, pause() { this.el.sceneEl.removeEventListener("stateadded", this.onStateChange); this.el.sceneEl.removeEventListener("stateremoved", this.onStateChange); - this.el.removeEventListener("click", this.onClick); - - this.el.sceneEl.removeEventListener("micAudio", this.onAudioFrequencyChange); - }, - - tick: function() { - if (!this.analyser) return; - - this.analyser.getByteFrequencyData(this.levels); - - let sum = 0; - for (let i = 0; i < this.levels.length; i++) { - sum += this.levels[i]; - } - this.volume = sum / this.levels.length; - this.el.emit("micAudio", { - volume: this.volume, - levels: this.levels - }); + this.mic.removeEventListener("click", this.onMicClick); + this.freeze.removeEventListener("click", this.onFreezeClick); } }); diff --git a/src/components/stats-plus.css b/src/components/stats-plus.css new file mode 100644 index 0000000000000000000000000000000000000000..fc4417bf506b5a5916c286b72aeaf86189790732 --- /dev/null +++ b/src/components/stats-plus.css @@ -0,0 +1,26 @@ +:global(.rs-header) { + display: flex; + justify-content: space-between; + border-bottom: 1px rgba(255,255,255,0.1) solid; + margin-bottom: 8px; +} + +:global(.rs-collapse-btn) { + cursor: pointer; + font-size: 12px; +} + +:global(.rs-fps-counter) { + cursor: pointer; + position: absolute; + bottom: 0; + left: 0; + padding: 8px; + color: #aaa; + font-size: 10px; +} + +:global(.rs-mobile) { + bottom: auto; + top: 0; +} \ No newline at end of file diff --git a/src/components/stats-plus.js b/src/components/stats-plus.js new file mode 100644 index 0000000000000000000000000000000000000000..c64100eeafa0e069a10ff2b005181a2dc446435e --- /dev/null +++ b/src/components/stats-plus.js @@ -0,0 +1,130 @@ +import "./stats-plus.css"; +// Adapted from https://github.com/aframevr/aframe/blob/master/src/components/scene/stats.js + +function createStats(scene) { + const threeStats = new window.threeStats(scene.renderer); + const aframeStats = new window.aframeStats(scene); + const plugins = scene.isMobile ? [] : [threeStats, aframeStats]; + return new window.rStats({ + css: [], // Our stylesheet is injected from AFrame. + values: { + fps: { caption: "fps", below: 30 } + }, + groups: [{ caption: "Framerate", values: ["fps", "raf"] }], + plugins: plugins + }); +} + +const HIDDEN_CLASS = "a-hidden"; + +AFRAME.registerComponent("stats-plus", { + // Whether or not the stats panel is expanded. + // Shows FPS counter when collapsed. + schema: { default: false }, + init() { + this.onExpand = this.onExpand.bind(this); + this.onCollapse = this.onCollapse.bind(this); + this.onEnterVr = this.onEnterVr.bind(this); + this.onExitVr = this.onExitVr.bind(this); + + const scene = this.el.sceneEl; + this.stats = createStats(scene); + this.statsEl = document.querySelector(".rs-base"); + + // Add header to stats panel so we can collapse it + const statsHeaderEl = document.createElement("div"); + statsHeaderEl.classList.add("rs-header"); + + const statsTitleEl = document.createElement("h1"); + statsTitleEl.innerHTML = "Stats"; + statsHeaderEl.appendChild(statsTitleEl); + + const collapseEl = document.createElement("div"); + collapseEl.classList.add("rs-collapse-btn"); + collapseEl.innerHTML = "X"; + collapseEl.addEventListener("click", this.onCollapse); + statsHeaderEl.appendChild(collapseEl); + + this.statsEl.insertBefore(statsHeaderEl, this.statsEl.firstChild); + + // Add fps counter to the page + this.fpsEl = document.createElement("div"); + this.fpsEl.addEventListener("click", this.onExpand); + this.fpsEl.classList.add("rs-fps-counter"); + document.body.appendChild(this.fpsEl); + this.lastFpsUpdate = performance.now(); + this.frameCount = 0; + + if (scene.isMobile) { + this.statsEl.classList.add("rs-mobile"); + this.fpsEl.classList.add("rs-mobile"); + } + + scene.addEventListener("enter-vr", this.onEnterVr); + scene.addEventListener("exit-vr", this.onExitVr); + }, + update(oldData) { + if (oldData !== this.data) { + if (this.data) { + this.statsEl.classList.remove(HIDDEN_CLASS); + this.fpsEl.classList.add(HIDDEN_CLASS); + } else { + this.statsEl.classList.add(HIDDEN_CLASS); + this.fpsEl.classList.remove(HIDDEN_CLASS); + } + } + }, + tick() { + if (this.data) { + // Update rStats + const stats = this.stats; + stats("rAF").tick(); + stats("FPS").frame(); + stats().update(); + } else { + // Update the fps counter + const now = performance.now(); + this.frameCount++; + + // Update the fps counter text once a second + if (now >= this.lastFpsUpdate + 1000) { + const fps = this.frameCount / ((now - this.lastFpsUpdate) / 1000); + this.fpsEl.innerHTML = Math.round(fps) + " FPS"; + this.lastFpsUpdate = now; + this.frameCount = 0; + } + } + }, + onEnterVr() { + // Hide all stats elements when entering VR on mobile + if (this.el.sceneEl.isMobile) { + this.statsEl.classList.add(HIDDEN_CLASS); + this.fpsEl.classList.add(HIDDEN_CLASS); + } + }, + onExitVr() { + // Revert to previous state whe exiting VR on mobile + if (this.el.sceneEl.isMobile) { + if (this.data) { + this.statsEl.classList.remove(HIDDEN_CLASS); + } else { + this.fpsEl.classList.remove(HIDDEN_CLASS); + } + } + }, + onExpand() { + this.el.setAttribute(this.name, true); + }, + onCollapse() { + this.el.setAttribute(this.name, false); + }, + remove() { + this.el.sceneEl.removeListener("enter-vr", this.hide); + this.el.sceneEl.removeListener("exit-vr", this.show); + + if (this.statsEl) { + this.statsEl.parentNode.removeChild(this.statsEl); + this.fpsEl.parentNode.removeChild(this.fpsEl); + } + } +}); diff --git a/src/hub.html b/src/hub.html index e7ca7655ff31d81da8529db96cfecdf85f5c66a1..a80599f4fd9d6a125eed5c5bb33ab9094915de23 100644 --- a/src/hub.html +++ b/src/hub.html @@ -22,16 +22,26 @@ networked-scene="adapter: janus; audio: true; debug: true; connectOnLoad: false;" physics mute-mic="eventSrc: a-scene; toggleEvents: action_mute" + freeze-controller="toggleEvent: action_freeze" personal-space-bubble="debug: false;" app-mode-input-mappings="modes: default, hud; actionSets: default, hud;" > <a-assets> - <img id="unmuted" src="./assets/hud/unmuted.png" > - <img id="muted" src="./assets/hud/muted.png" > - <img id="avatar" src="./assets/hud/avatar.png" > <img id="tooltip" src="./assets/hud/tooltip.png" > + <img id="mute-off" src="./assets/hud/mute_off.png" > + <img id="mute-off-hover" src="./assets/hud/mute_off-hover.png" > + <img id="mute-on" src="./assets/hud/mute_on.png" > + <img id="mute-on-hover" src="./assets/hud/mute_on-hover.png" > + <img id="bubble-off" src="./assets/hud/bubble_off.png" > + <img id="bubble-off-hover" src="./assets/hud/bubble_off-hover.png" > + <img id="bubble-on" src="./assets/hud/bubble_on.png" > + <img id="bubble-on-hover" src="./assets/hud/bubble_on-hover.png" > + <img id="freeze-off" src="./assets/hud/freeze_off.png" > + <img id="freeze-off-hover" src="./assets/hud/freeze_off-hover.png" > + <img id="freeze-on" src="./assets/hud/freeze_on.png" > + <img id="freeze-on-hover" src="./assets/hud/freeze_on-hover.png" > <a-asset-item id="botdefault" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotDefault_Avatar-9f71f8ff22.gltf"></a-asset-item> <a-asset-item id="botbobo" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotBobo_Avatar-f9740a010b.gltf"></a-asset-item> @@ -183,10 +193,10 @@ vr-mode-toggle-playing__hud-controller > <a-entity in-world-hud="haptic:#player-right-controller;raycaster:#player-right-controller;" rotation="30 0 0"> - <a-rounded height="0.13" width="0.6" color="#000000" position="-0.3 -0.065 0" radius="0.065" opacity="0.35" class="hud bg"></a-rounded> - <a-image src="#unmuted" scale="0.1 0.1 0.1" position="-0.2 0 0.001" class="hud mic" material="alphaTest:0.1;"></a-image> - <a-image src="#avatar" scale="0.2 0.2 0.2" position="0 0 0.001" class="hud avatar"></a-image> - <a-image src="#unmuted" scale="0.1 0.1 0.1" position="0.2 0 0.001" class="hud mic" material="alphaTest:0.1;"></a-image> + <a-rounded height="0.13" width="0.48" color="#000000" position="-0.24 -0.065 0" radius="0.065" opacity="0.35" class="hud bg"></a-rounded> + <a-image icon-button="image: #mute-off; hoverImage: #mute-off-hover; activeImage: #mute-on; activeHoverImage: #mute-on-hover" scale="0.1 0.1 0.1" position="-0.17 0 0.001" class="hud mic" material="alphaTest:0.1;"></a-image> + <a-image icon-button="image: #freeze-off; hoverImage: #freeze-off-hover; activeImage: #freeze-on; activeHoverImage: #freeze-on-hover" scale="0.2 0.2 0.2" position="0 0 0.001" class="hud freeze"></a-image> + <a-image icon-button="image: #bubble-off; hoverImage: #bubble-off-hover; activeImage: #bubble-on; activeHoverImage: #bubble-on-hover" scale="0.1 0.1 0.1" position="0.17 0 0.001" class="hud mic" material="alphaTest:0.1;"></a-image> </a-entity> </a-entity> diff --git a/src/hub.js b/src/hub.js index 0489cb38e9afe45a50547030d2eba8eeacb1856e..f04c5ae7db83c8396186f1017202b5b4704c5167 100644 --- a/src/hub.js +++ b/src/hub.js @@ -50,6 +50,9 @@ import "./components/hand-poses"; import "./components/gltf-model-plus"; import "./components/gltf-bundle"; import "./components/hud-controller"; +import "./components/freeze-controller"; +import "./components/icon-button"; +import "./components/stats-plus"; import ReactDOM from "react-dom"; import React from "react"; @@ -58,12 +61,14 @@ import HubChannel from "./utils/hub-channel"; import "./systems/personal-space-bubble"; import "./systems/app-mode"; +import "./systems/exit-on-blur"; import "./gltf-component-mappings"; import { App } from "./App"; window.APP = new App(); +const store = window.APP.store; const qs = queryString.parse(location.search); const isMobile = AFRAME.utils.device.isMobile(); @@ -89,7 +94,6 @@ import "./components/nav-mesh-helper"; import registerNetworkSchemas from "./network-schemas"; import { inGameActions, config as inputConfig } from "./input-mappings"; import registerTelemetry from "./telemetry"; -import Store from "./storage/store"; import { generateDefaultProfile, generateRandomName } from "./utils/identity.js"; import { getAvailableVREntryTypes } from "./utils/vr-caps-detect.js"; @@ -109,7 +113,6 @@ AFRAME.registerInputActivator("pressedmove", PressedMove); AFRAME.registerInputActivator("reverseY", ReverseY); AFRAME.registerInputMappings(inputConfig, true); -const store = new Store(); const concurrentLoadDetector = new ConcurrentLoadDetector(); const hubChannel = new HubChannel(store); @@ -124,6 +127,9 @@ if (!store.state.profile.has_changed_name) { } async function exitScene() { + if (NAF.connection.adapter && NAF.connection.adapter.localMediaStream) { + NAF.connection.adapter.localMediaStream.getTracks().forEach(t => t.stop()); + } hubChannel.disconnect(); const scene = document.querySelector("a-scene"); scene.renderer.animate(null); // Stop animation loop, TODO A-Frame should do this @@ -136,6 +142,8 @@ function applyProfileFromStore(playerRig) { displayName, avatarSrc: "#" + (store.state.profile.avatar_id || "botdefault") }); + const hudController = playerRig.querySelector("[hud-controller]"); + hudController.setAttribute("hud-controller", { showTip: !store.state.profile.has_found_freeze }); document.querySelector("a-scene").emit("username-changed", { username: displayName }); } @@ -145,6 +153,8 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) { document.querySelector("a-scene canvas").classList.remove("blurred"); scene.render(); + scene.setAttribute("stats-plus", false); + if (enterInVR) { scene.enterVR(); } @@ -158,10 +168,6 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) { serverURL: process.env.JANUS_SERVER }); - if (!qsTruthy("no_stats")) { - scene.setAttribute("stats", true); - } - if (isMobile || qsTruthy("mobile")) { playerRig.setAttribute("virtual-gamepad-controls", {}); } diff --git a/src/react-components/2d-hud.js b/src/react-components/2d-hud.js index 9679bb9c090af49966c6f5cbf03072201c860f00..d02d6deb206275df0a2389d31a6b9354c035de4f 100644 --- a/src/react-components/2d-hud.js +++ b/src/react-components/2d-hud.js @@ -2,25 +2,28 @@ import React from "react"; import PropTypes from "prop-types"; import cx from "classnames"; -import styles from "../assets/stylesheets/2d-hud.css"; +import styles from "../assets/stylesheets/2d-hud.scss"; -const TwoDHUD = ({ muted, onToggleMute }) => ( +const TwoDHUD = ({ muted, frozen, onToggleMute, onToggleFreeze }) => ( <div className={styles.container}> <div className={cx("ui-interactive", styles.panel, styles.left)}> - <div className={cx(styles.mic, { [styles.muted]: muted })} onClick={onToggleMute} /> - </div> - <div className={cx("ui-interactive", styles.modeButton)}> - <div className={styles.avatar} /> + <div className={cx(styles.iconButton, styles.mute, { [styles.active]: muted })} onClick={onToggleMute} /> </div> + <div + className={cx("ui-interactive", styles.iconButton, styles.large, styles.freeze, { [styles.active]: frozen })} + onClick={onToggleFreeze} + /> <div className={cx("ui-interactive", styles.panel, styles.right)}> - <div className={cx(styles.mic, { [styles.muted]: muted })} onClick={onToggleMute} /> + <div className={cx(styles.iconButton, styles.bubble, { [styles.active]: muted })} onClick={onToggleMute} /> </div> </div> ); TwoDHUD.propTypes = { muted: PropTypes.bool, - onToggleMute: PropTypes.func + frozen: PropTypes.bool, + onToggleMute: PropTypes.func, + onToggleFreeze: PropTypes.func }; export default TwoDHUD; diff --git a/src/react-components/avatar-selector.js b/src/react-components/avatar-selector.js index 528d5b81558e37f12aea6a0182c4cc08d8971782..6ae6ab6ac91110ae8cc65b9d5b5f2db8d075f911 100644 --- a/src/react-components/avatar-selector.js +++ b/src/react-components/avatar-selector.js @@ -39,7 +39,7 @@ class AvatarSelector extends Component { // so we need to force it here. const currRot = this.animation.parentNode.getAttribute("rotation"); const currY = currRot.y; - const toRot = String.split(this.animation.attributes.to.value, " "); + const toRot = this.animation.getAttribute("to").split(" "); const toY = toRot[1]; const step = 360.0 / this.props.avatars.length; const brokenlyBigRotation = Math.abs(toY - currY) > 3 * step; diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js index af896b643db9e3cf38ac5403aa58e143f28c58ce..d91dfdbed6e726a785cbb8744d35e3dab066085c 100644 --- a/src/react-components/home-root.js +++ b/src/react-components/home-root.js @@ -3,6 +3,8 @@ import PropTypes from "prop-types"; import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl"; import en from "react-intl/locale-data/en"; import homeVideo from "../assets/video/home.webm"; +import classNames from "classnames"; +import formurlencoded from "form-urlencoded"; import HubCreatePanel from "./hub-create-panel.js"; @@ -28,7 +30,10 @@ class HomeRoot extends Component { }; state = { - environments: [] + environments: [], + dialogType: null, + mailingListEmail: "", + mailingListPrivacy: false }; componentDidMount() { @@ -36,6 +41,40 @@ class HomeRoot extends Component { document.querySelector("#background-video").playbackRate = 0.5; } + showDialog = dialogType => { + return e => { + e.preventDefault(); + e.stopPropagation(); + this.setState({ dialogType }); + }; + }; + + closeDialog = () => { + this.setState({ dialogType: null }); + }; + + signUpForMailingList = async e => { + e.preventDefault(); + e.stopPropagation(); + if (!this.state.mailingListPrivacy) return; + + const url = "https://www.mozilla.org/en-US/newsletter/"; + + const payload = { + email: this.state.mailingListEmail, + newsletters: "mixed-reality", + privacy: true, + fmt: "H", + source_url: document.location.href + }; + + await fetch(url, { + body: formurlencoded(payload), + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" } + }).then(() => this.setState({ dialogType: "email_submitted" })); + }; + loadEnvironments = () => { const environments = []; @@ -52,10 +91,98 @@ class HomeRoot extends Component { }; render() { + let dialogTitle = null; + let dialogBody = null; + + switch (this.state.dialogType) { + // TODO i18n, FormattedMessage doesn't play nicely with links + case "slack": + dialogTitle = "Get in Touch"; + dialogBody = ( + <span> + Want to join the conversation? + <p /> + Join us on the{" "} + <a href="https://webvr-slack.herokuapp.com/" target="_blank" rel="noopener noreferrer"> + WebVR Slack + </a>{" "} + in the #social channel.<br />VR meetups every Friday at noon PST! + <p /> Or, tweet at{" "} + <a href="https://twitter.com/mozillareality" target="_blank" rel="noopener noreferrer"> + @mozillareality + </a>{" "} + on Twitter. + </span> + ); + break; + case "email_submitted": + dialogTitle = ""; + dialogBody = "Great! Please check your e-mail to confirm your subscription."; + break; + case "updates": + dialogTitle = ""; + dialogBody = ( + <span> + Sign up to get release notes about new features. + <p /> + <form onSubmit={this.signUpForMailingList}> + <div className="mailing-list-form"> + <input + type="email" + value={this.state.mailingListEmail} + onChange={e => this.setState({ mailingListEmail: e.target.value })} + className="mailing-list-form__email_field" + required + placeholder="Your email here" + /> + <label className="mailing-list-form__privacy"> + <input + className="mailing-list-form__privacy_checkbox" + type="checkbox" + required + value={this.state.mailingListPrivacy} + onChange={e => this.setState({ mailingListPrivacy: e.target.checked })} + /> + <span className="mailing-list-form__privacy_label"> + <FormattedMessage id="mailing_list.privacy_label" />{" "} + <a target="_blank" rel="noopener noreferrer" href="https://www.mozilla.org/en-US/privacy/"> + <FormattedMessage id="mailing_list.privacy_link" /> + </a> + </span> + </label> + <input className="mailing-list-form__submit" type="submit" value="Sign Up Now" /> + </div> + </form> + </span> + ); + break; + case "report": + dialogTitle = "Report an Issue"; + dialogBody = ( + <span> + Need to report a problem? + <p /> + You can file a{" "} + <a href="https://github.com/mozilla/mr-social-client/issues" target="_blank" rel="noopener noreferrer"> + Github Issue + </a>{" "} + or e-mail us for support at <a href="mailto:hubs@mozilla.com">hubs@mozilla.com</a>. + <p /> + You can also find us in #social on the{" "} + <a href="http://webvr-slack.herokuapp.com/" target="_blank" rel="noopener noreferrer"> + WebVR Slack + </a>. + </span> + ); + break; + } + + const mainContentClassNames = classNames({ "main-content": true, "main-content--noninteractive": !!dialogTitle }); + return ( <IntlProvider locale={lang} messages={messages}> <div className="home"> - <div className="main-content"> + <div className={mainContentClassNames}> <div className="header-content"> <div className="header-content__title"> <img className="header-content__title__name" src="../assets/images/logo.svg" /> @@ -110,13 +237,28 @@ class HomeRoot extends Component { <div className="footer-content"> <div className="footer-content__links"> <div className="footer-content__links__top"> - <a className="footer-content__links__link" rel="noopener noreferrer" target="_blank" href="#"> - <FormattedMessage id="home.join_on_slack" /> + <a + className="footer-content__links__link" + rel="noopener noreferrer" + href="#" + onClick={this.showDialog("slack")} + > + <FormattedMessage id="home.join_us" /> </a> - <a className="footer-content__links__link" rel="noopener noreferrer" target="_blank" href="#"> + <a + className="footer-content__links__link" + rel="noopener noreferrer" + href="#" + onClick={this.showDialog("updates")} + > <FormattedMessage id="home.get_updates" /> </a> - <a className="footer-content__links__link" rel="noopener noreferrer" target="_blank" href="#"> + <a + className="footer-content__links__link" + rel="noopener noreferrer" + href="#" + onClick={this.showDialog("report")} + > <FormattedMessage id="home.report_issue" /> </a> </div> @@ -130,6 +272,22 @@ class HomeRoot extends Component { <video playsInline autoPlay muted loop className="background-video" id="background-video"> <source src={homeVideo} type="video/webm" /> </video> + {this.state.dialogType && ( + <div className="overlay"> + <div className="dialog"> + <div className="dialog__box"> + <div className="dialog__box__contents"> + <button className="dialog__box__contents__close" onClick={this.closeDialog}> + <span>🗙</span> + </button> + <div className="dialog__box__contents__title">{dialogTitle}</div> + <div className="dialog__box__contents__body">{dialogBody}</div> + <div className="dialog__box__contents__button-container" /> + </div> + </div> + </div> + </div> + )} </div> </IntlProvider> ); diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 3e22208dacc6219b59cc9276562c71d43f129f03..7ff3d25a675e484879619f00a26427b90592e26e 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -90,6 +90,9 @@ class UIRoot extends Component { autoExitTimerInterval: null, secondsRemainingBeforeAutoExit: Infinity, + muted: false, + frozen: false, + exited: false, showProfileEntry: false @@ -107,6 +110,12 @@ class UIRoot extends Component { this.props.scene.addEventListener("loaded", this.onSceneLoaded); this.props.scene.addEventListener("stateadded", this.onAframeStateChanged); this.props.scene.addEventListener("stateremoved", this.onAframeStateChanged); + this.props.scene.addEventListener("exit", this.exit); + } + + componentWillUnmount() { + this.props.scene.removeEventListener("loaded", this.onSceneLoaded); + this.props.scene.removeEventListener("exit", this.exit); } componentDidUpdate(prevProps) { @@ -121,9 +130,9 @@ class UIRoot extends Component { // TODO: mute state should probably actually just live in react land onAframeStateChanged = e => { - if (e.detail !== "muted") return; + if (!(e.detail === "muted" || e.detail === "frozen")) return; this.setState({ - muted: this.props.scene.is("muted") + [e.detail]: this.props.scene.is(e.detail) }); }; @@ -131,6 +140,10 @@ class UIRoot extends Component { this.props.scene.emit("action_mute"); }; + toggleFreeze = () => { + this.props.scene.emit("action_freeze"); + }; + handleForcedVREntryType = () => { if (!this.props.forcedVREntryType) return; @@ -758,7 +771,12 @@ class UIRoot extends Component { )} </div> {this.state.entryStep === ENTRY_STEPS.finished ? ( - <TwoDHUD muted={this.state.muted} onToggleMute={this.toggleMute} /> + <TwoDHUD + muted={this.state.muted} + frozen={this.state.frozen} + onToggleMute={this.toggleMute} + onToggleFreeze={this.toggleFreeze} + /> ) : null} </div> </IntlProvider> diff --git a/src/storage/store.js b/src/storage/store.js index 4351ebeda99e9f9665fcbbf395858296cd2e56a4..b117e1849bf6a20cc28d770c7ef66112bb8888ba 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -19,6 +19,7 @@ export const SCHEMA = { properties: { has_agreed_to_terms: { type: "boolean" }, has_changed_name: { type: "boolean" }, + has_found_freeze: { type: "boolean" }, display_name: { type: "string", pattern: "^[A-Za-z0-9-]{3,32}$" }, avatar_id: { type: "string" } } diff --git a/src/systems/exit-on-blur.js b/src/systems/exit-on-blur.js new file mode 100644 index 0000000000000000000000000000000000000000..e6263a101f5a2fd1f71cd4b122a12431104e2457 --- /dev/null +++ b/src/systems/exit-on-blur.js @@ -0,0 +1,30 @@ +AFRAME.registerSystem("exit-on-blur", { + init() { + this.onBlur = this.onBlur.bind(this); + this.onFocus = this.onFocus.bind(this); + + window.addEventListener("blur", this.onBlur); + window.addEventListener("focus", this.onFocus); + + this.exitTimeout = null; + }, + + onBlur() { + if (this.el.isMobile) { + this.exitTimeout = setTimeout(() => { + this.el.dispatchEvent(new CustomEvent("exit")); + }, 10 * 1000); + } + }, + + onFocus() { + if (this.el.isMobile) { + clearTimeout(this.exitTimeout); + } + }, + + remove() { + clearTimeout(this.exitTimeout); + window.removeEventListener("blur", this.onBlur); + } +}); diff --git a/src/utils/identity.js b/src/utils/identity.js index db78b027e3e851aa254532f264438e362062576c..4cce7fa3729e376be6d28d57eb3120715bceee8b 100644 --- a/src/utils/identity.js +++ b/src/utils/identity.js @@ -103,6 +103,7 @@ export function generateDefaultProfile() { return { has_agreed_to_terms: false, has_changed_name: false, + has_found_freeze: false, avatar_id: selectRandom(avatarIds) }; } diff --git a/yarn.lock b/yarn.lock index 4391edbc296f19f7af084c959e516b67caed2123..15edf71c1aed3e12ac16b63174f0cb2934c8a9d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -118,7 +118,7 @@ accepts@1.3.3: mime-types "~2.1.11" negotiator "0.6.1" -accepts@~1.3.4, accepts@~1.3.5: +accepts@~1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" dependencies: @@ -2011,18 +2011,14 @@ colormin@^1.0.5: css-color-names "0.0.4" has "^1.0.1" -colors@*: - version "1.2.1" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.1.tgz#f4a3d302976aaf042356ba1ade3b1a2c62d9d794" +colors@*, colors@^1.1.2, colors@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" colors@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" -colors@^1.1.2, colors@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" - combine-source-map@~0.7.1: version "0.7.2" resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.7.2.tgz#0870312856b307a87cc4ac486f3a9a62aeccc09e" @@ -2840,17 +2836,7 @@ error@^7.0.2: string-template "~0.2.1" xtend "~4.0.0" -es-abstract@^1.5.1: - version "1.11.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.11.0.tgz#cce87d518f0496893b1a30cd8461835535480681" - dependencies: - es-to-primitive "^1.1.1" - function-bind "^1.1.1" - has "^1.0.1" - is-callable "^1.1.3" - is-regex "^1.0.4" - -es-abstract@^1.7.0: +es-abstract@^1.5.1, es-abstract@^1.7.0: version "1.10.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864" dependencies: @@ -3080,42 +3066,7 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -express@^4.10.7: - version "4.16.3" - resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53" - dependencies: - accepts "~1.3.5" - array-flatten "1.1.1" - body-parser "1.18.2" - content-disposition "0.5.2" - content-type "~1.0.4" - cookie "0.3.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.2" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.1.1" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.2" - path-to-regexp "0.1.7" - proxy-addr "~2.0.3" - qs "6.5.1" - range-parser "~1.2.0" - safe-buffer "5.1.1" - send "0.16.2" - serve-static "1.13.2" - setprototypeof "1.1.0" - statuses "~1.4.0" - type-is "~1.6.16" - utils-merge "1.0.1" - vary "~1.1.2" - -express@^4.16.2: +express@^4.10.7, express@^4.16.2: version "4.16.2" resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" dependencies: @@ -3321,18 +3272,6 @@ finalhandler@1.1.0: statuses "~1.3.1" unpipe "~1.0.0" -finalhandler@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.2" - statuses "~1.4.0" - unpipe "~1.0.0" - find-cache-dir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" @@ -3420,6 +3359,10 @@ form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" +form-urlencoded@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/form-urlencoded/-/form-urlencoded-2.0.4.tgz#dbcd590a49ae35d5e9516bbba8567242d0291fe5" + forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" @@ -5186,10 +5129,6 @@ minijanus@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/minijanus/-/minijanus-0.5.0.tgz#78e1429bb5d83cb3957a538335d2ae901bf614fa" -"minijanus@https://github.com/mozilla/minijanus.js#master": - version "0.5.0" - resolved "https://github.com/mozilla/minijanus.js#497f4dd80fdb92e247238e638daed14ae6623575" - minimalistic-assert@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" @@ -5373,12 +5312,12 @@ mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -"naf-janus-adapter@https://github.com/mozilla/naf-janus-adapter#feature/disconnect": - version "0.4.1" - resolved "https://github.com/mozilla/naf-janus-adapter#4a4532014d6489403cf7e451790925ce747f8e41" +naf-janus-adapter@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.5.2.tgz#f4a9522c4e0b38fcbfe7c71b668afed67d5e133e" dependencies: debug "^3.1.0" - minijanus "https://github.com/mozilla/minijanus.js#master" + minijanus "^0.5.0" nan@^2.3.0, nan@^2.3.2: version "2.9.1" @@ -6399,7 +6338,7 @@ prop-types@^15.5.4, prop-types@^15.6.0: loose-envify "^1.3.1" object-assign "^4.1.1" -proxy-addr@~2.0.2, proxy-addr@~2.0.3: +proxy-addr@~2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341" dependencies: @@ -7167,7 +7106,7 @@ serve-static@1.13.1: parseurl "~1.3.2" send "0.16.1" -serve-static@1.13.2, serve-static@^1.10.0, serve-static@^1.8.0: +serve-static@^1.10.0, serve-static@^1.8.0: version "1.13.2" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" dependencies: @@ -7979,7 +7918,7 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-is@~1.6.15, type-is@~1.6.16: +type-is@~1.6.15: version "1.6.16" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" dependencies: