diff --git a/package.json b/package.json index f99e62b319a688c56cd4d53d7110e203d50fa31b..a97713d44790787795d671f30d6f5d9a384ac8ae 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,13 @@ "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", "query-string": "^5.0.1", "raven-js": "^3.20.1", "react": "^16.1.1", diff --git a/src/assets/images/dropdown_arrow.png b/src/assets/images/dropdown_arrow.png new file mode 100755 index 0000000000000000000000000000000000000000..caa42c1ffed82796540acdc192201cf20e822e0b Binary files /dev/null and b/src/assets/images/dropdown_arrow.png differ diff --git a/src/assets/images/dropdown_arrow@2x.png b/src/assets/images/dropdown_arrow@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..d4e74eb212652021837a17d860578c6f7114dcd5 Binary files /dev/null and b/src/assets/images/dropdown_arrow@2x.png differ diff --git a/src/assets/images/level_background.png b/src/assets/images/level_background.png new file mode 100755 index 0000000000000000000000000000000000000000..9d53b3c6dc75552b225d5717fa6fb8cd883b05cb Binary files /dev/null and b/src/assets/images/level_background.png differ diff --git a/src/assets/images/level_background@2x.png b/src/assets/images/level_background@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..4a9f08acc76396f4133673faed5b4ae38ca6cc87 Binary files /dev/null and b/src/assets/images/level_background@2x.png differ diff --git a/src/assets/images/level_fill.png b/src/assets/images/level_fill.png old mode 100644 new mode 100755 index 99f77b5655e6a50e0444364a3c2cfb4882b3b2d9..49a4f8a75064870db870bf994e8b25671205bfb0 Binary files a/src/assets/images/level_fill.png and b/src/assets/images/level_fill.png differ diff --git a/src/assets/images/level_fill@2x.png b/src/assets/images/level_fill@2x.png old mode 100644 new mode 100755 index 477d9801bb6d33737b571fce454ff265ff79e77c..28f313bc9d541fc92fd65c03945b35c1affdf9cb Binary files a/src/assets/images/level_fill@2x.png and b/src/assets/images/level_fill@2x.png differ diff --git a/src/assets/images/mic_level.png b/src/assets/images/mic_level.png old mode 100644 new mode 100755 index 5be15458d9ed41c46f861d8dd8435a11e452f80c..e4c1367ddf78efd48173a3d0a64c4c48c953a871 Binary files a/src/assets/images/mic_level.png and b/src/assets/images/mic_level.png differ diff --git a/src/assets/images/mic_level@2x.png b/src/assets/images/mic_level@2x.png old mode 100644 new mode 100755 index 94739aa1977cc5d5317eeb770905ed212ff248b4..621f944ed0b07b1a625a2627f5646406fcefbd98 Binary files a/src/assets/images/mic_level@2x.png and b/src/assets/images/mic_level@2x.png differ diff --git a/src/assets/images/speaker_level.png b/src/assets/images/speaker_level.png old mode 100644 new mode 100755 index 9ccedcc0350f90c95744d928128594829b5f5b90..f0557615258997bb7c54e7a6028e052c9c8a33f4 Binary files a/src/assets/images/speaker_level.png and b/src/assets/images/speaker_level.png differ diff --git a/src/assets/images/speaker_level@2x.png b/src/assets/images/speaker_level@2x.png old mode 100644 new mode 100755 index a807745cbcaaf823e6e8e99deda15459d1ed1d9a..3d60f4b8d287742ad3076ae7e63f988cca029f89 Binary files a/src/assets/images/speaker_level@2x.png and b/src/assets/images/speaker_level@2x.png differ diff --git a/src/assets/stylesheets/audio.scss b/src/assets/stylesheets/audio.scss index 67aac69962d861e993fb0e58e5dec9441536d2a9..d73a85eddc926c4b9a2f58c21fd4a97521822fd8 100644 --- a/src/assets/stylesheets/audio.scss +++ b/src/assets/stylesheets/audio.scss @@ -28,17 +28,29 @@ @extend %rounded-border; @extend %default-font; + appearance: none; + -moz-appearance: none; + -webkit-appearance: none; background-color: black; padding: 6px; + padding-right: 30px; color: white; font-size: 1.1em; width: 90%; } &__mic-icon { + pointer-events: none; position: absolute; - left: 7.5%; - top: 10px; + left: 8%; + top: 9px; + } + + &__dropdown-arrow { + pointer-events: none; + position: absolute; + right: 7.5%; + top: 16px; } } @@ -50,42 +62,16 @@ align-items: center; width: 100%; - &__mic { - position:relative; - width: 111px; - height: 111px; - } - - &__mic_icon { - position: absolute; - top: 0; - left: 0; - z-index: 2; - min-width: 111px; - min-height: 111px; - } - - &__speaker { + &__icon { position:relative; width: 111px; height: 111px; } - &__speaker_icon { - position: absolute; + &__icon-part { + position:absolute; top: 0; left: 0; - z-index: 2; - min-width: 111px; - min-height: 111px; - } - - &__level { - position: absolute; - top: 0; - left: 0; - opacity: 1.0; - z-index: 1; } } @@ -118,17 +104,26 @@ @extend %top-subtitle; } - &__icon { + &__button-container { flex: 10; display: flex; justify-content: center; align-items: center; cursor: pointer; + width: 111px; + height: 111px; + } + + &__button { + background: none; + border: none; + cursor: pointer; } &__next { @extend %bottom-button; - margin: auto; - flex: 1 1 20px; + padding-top: 0; + padding-bottom: 0; + flex: 1 1; } } diff --git a/src/assets/stylesheets/entry.scss b/src/assets/stylesheets/entry.scss index bdd20d1ee7af717ac0b2808b696c85fe49c8d775..7c7d18c840410378a86d9a4b41211777808a60b0 100644 --- a/src/assets/stylesheets/entry.scss +++ b/src/assets/stylesheets/entry.scss @@ -20,28 +20,18 @@ justify-content: center; &__screen-sharing { - font-size: 1.4em; - margin-left: 2.95em; - margin-top: 0.6em; - } + font-size: 1.4em; + margin-left: 2.95em; + margin-top: 0.6em; - &__screen-sharing-checkbox { - appearance: none; - -moz-appearance: none; - -webkit-appearance: none; - width: 2em; - height: 2em; - border: 3px solid white; - border-radius: 9px; - vertical-align: sub; - margin: 0 0.6em + &__checkbox { + @extend %checkbox; + } + &__checkbox:checked { + @extend %checkbox-checked; + } } - &__screen-sharing-checkbox:checked { - border: 9px double white; - outline: 9px solid white; - outline-offset: -18px; - } &__secondary { width: 100%; @@ -58,6 +48,11 @@ margin-top: 10px; margin-bottom: 10px; cursor: pointer; + background: none; + color: white; + border: none; + align-items: center; + @extend %default-font; &__icon { flex: 1 1 90px; diff --git a/src/assets/stylesheets/exited.scss b/src/assets/stylesheets/exited.scss index 72959090e6cf5ed18d29c2750beb7bbb4280fcae..693d6d38798705979478930f0b175ea57aa6e183 100644 --- a/src/assets/stylesheets/exited.scss +++ b/src/assets/stylesheets/exited.scss @@ -1,4 +1,6 @@ .exited-panel { + position: absolute; + color: white; background-color: black; width: 100%; height: 100%; diff --git a/src/assets/stylesheets/profile.scss b/src/assets/stylesheets/profile.scss index 000e974bbad3aec3760eb51e9863c78755a590fc..95f2caa2629d34e3f99fbab242f5cbf28b498038 100644 --- a/src/assets/stylesheets/profile.scss +++ b/src/assets/stylesheets/profile.scss @@ -41,6 +41,10 @@ color: $grey-text; } + &__display-name-label { + font-size: 1.2em; + margin-right: 0.5em; + } &__form-field-text { @extend %rounded-border; @extend %default-font; @@ -54,19 +58,34 @@ margin: 0.5em 0; } - &__form-submit { - @extend %default-font; - border: none; + &__terms { + margin-bottom: 16px; - margin: 8px; - width: 100px; - line-height: 1.5em; - font-size: 1.0em; + &__checkbox { + @extend %checkbox; + vertical-align: unset; + } + &__checkbox:checked { + @extend %checkbox-checked; + } - background-color: transparent; - font-weight: bold; - color: white; - cursor: pointer; + &__text { + display: inline-block; + max-width: 20em; + } + + &__link { + color: white; + } + + &__link:visited { + color: grey; + } + } + + &__form-submit { + @extend %bottom-button; + margin: 0; } } @@ -87,12 +106,16 @@ flex: 6 1 auto; font-size: 1.2em; line-height: 50px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; } &__app_name { font-size: 1.8em; padding-right: 18px; line-height: 50px; + white-space: nowrap; } } diff --git a/src/assets/stylesheets/shared.scss b/src/assets/stylesheets/shared.scss index c2d4f013de5b853664b751400969602b356a53f9..f959943585bbfacf37791586f5485add3e524cd1 100644 --- a/src/assets/stylesheets/shared.scss +++ b/src/assets/stylesheets/shared.scss @@ -17,11 +17,17 @@ $darker-grey: rgba(64, 64, 64, 1.0); } %bottom-button { + @extend %default-font; font-size: 1em; font-weight: bold; margin-top: auto; margin-bottom: 30px; cursor: pointer; + border: 3px solid white; + border-radius: 14px; + padding: 12px; + background: none; + color: white; } %top-title { @@ -42,3 +48,21 @@ $darker-grey: rgba(64, 64, 64, 1.0); border: none; font-size: 64pt; } + +%checkbox { + appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + width: 2em; + height: 2em; + border: 3px solid white; + border-radius: 9px; + vertical-align: sub; + margin: 0 0.6em +} + +%checkbox-checked { + border: 9px double white; + outline: 9px solid white; + outline-offset: -18px; +} diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 48bd0d1b4db58dfc385d902cd38b69e1ed2945a4..9948dfdc528fbdd8fdc3881ef59e0d2543ae18ca 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -13,8 +13,14 @@ "entry.daydream-via-chrome": "Using Google Chrome", "entry.enable-screen-sharing": "Share my desktop", "profile.save": "SAVE", + "profile.display_name.label": "Display name:", "profile.display_name.validation_warning": "Alphanumerics and hyphens. At least 3 characters, no more than 32", "profile.header": "Your identity", + "profile.terms.prefix": "I confirm that I am over the age of 13 and agree to the", + "profile.terms.privacy": "privacy policy", + "profile.terms.conjunction": "and", + "profile.terms.tou": "terms of use", + "profile.terms.suffix": ".", "profile.avatar-selector.loading": "Loading Avatars...", "audio.title": "Test your audio", "audio.subtitle-desktop": "Confirm HMD speaker output", @@ -25,7 +31,6 @@ "audio.grant-subtitle": "Mic access needed to be heard by others", "audio.granted-title": "Mic permissions granted", "audio.granted-subtitle": "You can still mute yourself in-game", - "audio.grant-next": " ", "audio.granted-next": "NEXT", "exit.subtitle": "Your session has ended. Refresh your browser to start a new one.", "autoexit.title": "Auto-ending session in ", diff --git a/src/hub.html b/src/hub.html index 0f6df33ece622b7d5f3bea8ff007628213623893..edb7395d4641f10c27994b3399dc580e72f6952b 100644 --- a/src/hub.html +++ b/src/hub.html @@ -3,10 +3,11 @@ <head> <meta charset="utf-8"> - <title>moz://a duck</title> - <meta name="viewport" content="width=device-width, initial-scale=1"> <meta http-equiv="origin-trial" data-feature="WebVR (For Chrome M62+)" data-expires="<%= ORIGIN_TRIAL_EXPIRES %>" content="<%= ORIGIN_TRIAL_TOKEN %>"> + <title>moz://a duck</title> + <link href="https://fonts.googleapis.com/css?family=Zilla+Slab:300,300i,400,400i,700" rel="stylesheet"> + <% if(NODE_ENV === "production") { %> <script src="https://cdn.rawgit.com/brianpeiris/aframe/845825ae694449524c185c44a314d361eead4680/dist/aframe-master.min.js"></script> <% } else { %> diff --git a/src/hub.js b/src/hub.js index af1dcced39aa0ec0ad2c52a22a6136595e1f2d25..07adb361c6dffd822e253d9cb50be4b15fc921e6 100644 --- a/src/hub.js +++ b/src/hub.js @@ -1,5 +1,8 @@ import "./assets/stylesheets/hub.scss"; +import moment from "moment-timezone"; +import uuid from "uuid/v4"; import queryString from "query-string"; +import { Socket } from "phoenix"; import { patchWebGLRenderingContext } from "./utils/webgl"; patchWebGLRenderingContext(); @@ -50,6 +53,7 @@ import "./components/hud-controller"; import ReactDOM from "react-dom"; import React from "react"; import UIRoot from "./react-components/ui-root"; +import HubChannel from "./utils/hub-channel"; import "./systems/personal-space-bubble"; import "./systems/app-mode"; @@ -87,7 +91,7 @@ import { inGameActions, config as inputConfig } from "./input-mappings"; import registerTelemetry from "./telemetry"; import Store from "./storage/store"; -import { generateDefaultProfile } from "./utils/identity.js"; +import { generateDefaultProfile, generateRandomName } from "./utils/identity.js"; import { getAvailableVREntryTypes } from "./utils/vr-caps-detect.js"; import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js"; @@ -107,16 +111,23 @@ AFRAME.registerInputMappings(inputConfig, true); const store = new Store(); const concurrentLoadDetector = new ConcurrentLoadDetector(); +const hubChannel = new HubChannel(store); concurrentLoadDetector.start(); // Always layer in any new default profile bits store.update({ profile: { ...generateDefaultProfile(), ...(store.state.profile || {}) } }); +// Regenerate name to encourage users to change it. +if (!store.state.profile.has_changed_name) { + store.update({ profile: { display_name: generateRandomName() } }); +} + 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 document.body.removeChild(scene); @@ -190,6 +201,12 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) { }); if (!qsTruthy("offline")) { + document.body.addEventListener("connected", () => { + hubChannel.sendEntryEvent().then(() => { + store.update({ lastEnteredAt: moment().toJSON() }); + }); + }); + scene.components["networked-scene"].connect(); if (mediaStream) { @@ -213,15 +230,14 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) { } } -function mountUI(scene) { +function mountUI(scene, props = {}) { const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi"); const forcedVREntryType = qs.vr_entry_type || null; const enableScreenSharing = qsTruthy("enable_screen_sharing"); const htmlPrefix = document.body.dataset.htmlPrefix || ""; + const showProfileEntry = !store.state.profile.has_changed_name; - // TODO: Refactor to avoid using return value - /* eslint-disable react/no-render-return-value */ - const uiRoot = ReactDOM.render( + ReactDOM.render( <UIRoot {...{ scene, @@ -232,14 +248,13 @@ function mountUI(scene) { forcedVREntryType, enableScreenSharing, store, - htmlPrefix + htmlPrefix, + showProfileEntry, + ...props }} />, document.getElementById("ui-root") ); - /* eslint-enable react/no-render-return-value */ - - return uiRoot; } const onReady = async () => { @@ -249,26 +264,31 @@ const onReady = async () => { registerNetworkSchemas(); - const uiRoot = mountUI(scene); + mountUI(scene); + + let modifiedProps = {}; + const remountUI = props => { + modifiedProps = { ...modifiedProps, ...props }; + mountUI(scene, modifiedProps); + }; getAvailableVREntryTypes().then(availableVREntryTypes => { - uiRoot.setState({ availableVREntryTypes }); - uiRoot.handleForcedVREntryType(); + remountUI({ availableVREntryTypes }); }); const environmentRoot = document.querySelector("#environment-root"); const initialEnvironmentEl = document.createElement("a-entity"); initialEnvironmentEl.addEventListener("bundleloaded", () => { - uiRoot.setState({ initialEnvironmentLoaded: true }); - // Wait a tick so that the environments actually render. - setTimeout(() => scene.renderer.animate(null)); + remountUI({ initialEnvironmentLoaded: true }); + // Wait a tick plus some margin so that the environments actually render. + setTimeout(() => scene.renderer.animate(null), 100); }); environmentRoot.appendChild(initialEnvironmentEl); if (qs.room) { // If ?room is set, this is `yarn start`, so just use a default environment and query string room. - uiRoot.setState({ janusRoomId: qs.room && !isNaN(parseInt(qs.room)) ? parseInt(qs.room) : 1 }); + remountUI({ janusRoomId: qs.room && !isNaN(parseInt(qs.room)) ? parseInt(qs.room) : 1 }); initialEnvironmentEl.setAttribute("gltf-bundle", { src: "https://asset-bundles-prod.reticulum.io/rooms/meetingroom/MeetingRoom.bundle.json" // src: "https://asset-bundles-prod.reticulum.io/rooms/theater/TheaterMeshes.bundle.json" @@ -278,15 +298,32 @@ const onReady = async () => { return; } - const hubId = document.location.pathname.substring(1).split("/")[0]; + // Connect to reticulum over phoenix channels to get hub info. + const hubId = qs.hub_id || document.location.pathname.substring(1).split("/")[0]; console.log(`Hub ID: ${hubId}`); - const res = await fetch(`/api/v1/hubs/${hubId}`); - const data = await res.json(); - const hub = data.hubs[0]; - const defaultSpaceTopic = hub.topics[0]; - const gltfBundleUrl = defaultSpaceTopic.assets.find(a => a.asset_type === "gltf_bundle").src; - uiRoot.setState({ janusRoomId: defaultSpaceTopic.janus_room_id }); - initialEnvironmentEl.setAttribute("gltf-bundle", `src: ${gltfBundleUrl}`); + + const socketProtocol = document.location.protocol === "https:" ? "wss:" : "ws:"; + const socketPort = qs.phx_port || document.location.port; + const socketHost = qs.phx_host || document.location.hostname; + const socketUrl = `${socketProtocol}//${socketHost}${socketPort ? `:${socketPort}` : ""}/socket`; + console.log(`Phoenix Channel URL: ${socketUrl}`); + + const socket = new Socket(socketUrl, { params: { session_id: uuid() } }); + socket.connect(); + + const channel = socket.channel(`hub:${hubId}`, {}); + + channel + .join() + .receive("ok", data => { + const hub = data.hubs[0]; + const defaultSpaceTopic = hub.topics[0]; + const gltfBundleUrl = defaultSpaceTopic.assets.find(a => a.asset_type === "gltf_bundle").src; + remountUI({ janusRoomId: defaultSpaceTopic.janus_room_id }); + initialEnvironmentEl.setAttribute("gltf-bundle", `src: ${gltfBundleUrl}`); + hubChannel.setPhoenixChannel(channel); + }) + .receive("error", res => console.error(res)); }; document.addEventListener("DOMContentLoaded", onReady); 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/entry-buttons.js b/src/react-components/entry-buttons.js index 8ed8b171d6e7ec08d82d59af2c5314d9353a7910..92d0ef5ef8ccd4992d9913bfb30144fff018c0b9 100644 --- a/src/react-components/entry-buttons.js +++ b/src/react-components/entry-buttons.js @@ -12,7 +12,7 @@ import DaydreamEntyImg from "../assets/images/daydream_entry.svg"; const mobiledetect = new MobileDetect(navigator.userAgent); const EntryButton = props => ( - <div className="entry-button" onClick={props.onClick}> + <button className="entry-button" onClick={props.onClick}> <img src={props.iconSrc} className="entry-button__icon" /> <div className="entry-button__label"> <div className="entry-button__label__contents"> @@ -25,7 +25,7 @@ const EntryButton = props => ( {props.subtitle && <div className="entry-button__subtitle">{props.subtitle}</div>} </div> </div> - </div> + </button> ); EntryButton.propTypes = { diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js index 523016e25bdf1e0f127bf5a889165b1ede806322..2732f3ca9bd173cc3390bd1af05316b8f6614747 100644 --- a/src/react-components/profile-entry-panel.js +++ b/src/react-components/profile-entry-panel.js @@ -14,10 +14,8 @@ class ProfileEntryPanel extends Component { constructor(props) { super(props); - this.state = { - display_name: this.props.store.state.profile.display_name, - avatar_id: this.props.store.state.profile.avatar_id - }; + const { display_name, avatar_id } = this.props.store.state.profile; + this.state = { display_name, avatar_id }; this.props.store.addEventListener("statechanged", this.storeUpdated); } @@ -28,10 +26,15 @@ class ProfileEntryPanel extends Component { saveStateAndFinish = e => { e.preventDefault(); + const has_agreed_to_terms = this.props.store.state.profile.has_agreed_to_terms || this.state.has_agreed_to_terms; + if (!has_agreed_to_terms) return; + const { has_changed_name, display_name } = this.props.store.state.profile; + const hasChangedName = has_changed_name || this.state.display_name !== display_name; this.props.store.update({ profile: { - display_name: this.state.display_name, - avatar_id: this.state.avatar_id + has_agreed_to_terms: true, + has_changed_name: hasChangedName, + ...this.state } }); this.props.finished(); @@ -74,20 +77,47 @@ class ProfileEntryPanel extends Component { <div className="profile-entry__subtitle"> <FormattedMessage id="profile.header" /> </div> - <input - className="profile-entry__form-field-text" - value={this.state.display_name} - onChange={e => this.setState({ display_name: e.target.value })} - required - pattern={SCHEMA.definitions.profile.properties.display_name.pattern} - title={formatMessage({ id: "profile.display_name.validation_warning" })} - ref={inp => (this.nameInput = inp)} - /> + <label> + <span className="profile-entry__display-name-label"> + <FormattedMessage id="profile.display_name.label" /> + </span> + <input + className="profile-entry__form-field-text" + value={this.state.display_name} + onChange={e => this.setState({ display_name: e.target.value })} + required + pattern={SCHEMA.definitions.profile.properties.display_name.pattern} + title={formatMessage({ id: "profile.display_name.validation_warning" })} + ref={inp => (this.nameInput = inp)} + /> + </label> <iframe className="profile-entry__avatar-selector" src={`/${this.props.htmlPrefix}avatar-selector.html#avatar_id=${this.state.avatar_id}`} ref={ifr => (this.avatarSelector = ifr)} /> + {!this.props.store.state.profile.has_agreed_to_terms && ( + <label className="profile-entry__terms"> + <input + className="profile-entry__terms__checkbox" + type="checkbox" + required + value={this.state.has_agreed_to_terms} + onChange={e => this.setState({ has_agreed_to_terms: e.target.checked })} + /> + <span className="profile-entry__terms__text"> + <FormattedMessage id="profile.terms.prefix" />{" "} + <a className="profile-entry__terms__link" target="_blank" href="/privacy"> + <FormattedMessage id="profile.terms.privacy" /> + </a>{" "} + <FormattedMessage id="profile.terms.conjunction" />{" "} + <a className="profile-entry__terms__link" target="_blank" href="/terms"> + <FormattedMessage id="profile.terms.tou" /> + </a> + <FormattedMessage id="profile.terms.suffix" /> + </span> + </label> + )} <input className="profile-entry__form-submit" type="submit" value={formatMessage({ id: "profile.save" })} /> </div> </form> diff --git a/src/react-components/profile-info-header.js b/src/react-components/profile-info-header.js index 43ec49291007089d54e1d1a52c7a586e766e5fb4..ca7a3b891c3c99cc3aca753244ba4289abe525b4 100644 --- a/src/react-components/profile-info-header.js +++ b/src/react-components/profile-info-header.js @@ -4,7 +4,7 @@ import PropTypes from "prop-types"; export const ProfileInfoHeader = props => ( <div className="profile-info-header"> <img src="../assets/images/account.svg" onClick={props.onClick} className="profile-info-header__icon" /> - <div className="profile-info-header__profile_display_name" onClick={props.onClick}> + <div className="profile-info-header__profile_display_name" onClick={props.onClick} title={props.name}> {props.name} </div> <div className="profile-info-header__app_name"> diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 39983ef6baa59f529ccc0c9ed985c1efad4b7955..2883d23f066498e941d561b6722b9fc5b434590c 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -60,11 +60,14 @@ class UIRoot extends Component { enableScreenSharing: PropTypes.bool, store: PropTypes.object, scene: PropTypes.object, - htmlPrefix: PropTypes.string + htmlPrefix: PropTypes.string, + showProfileEntry: PropTypes.bool, + availableVREntryTypes: PropTypes.object, + initialEnvironmentLoaded: PropTypes.bool, + janusRoomId: PropTypes.number }; state = { - availableVREntryTypes: null, entryStep: ENTRY_STEPS.start, enterInVR: false, @@ -87,14 +90,16 @@ class UIRoot extends Component { autoExitTimerInterval: null, secondsRemainingBeforeAutoExit: Infinity, - initialEnvironmentLoaded: false, exited: false, - showProfileEntry: false, - - janusRoomId: null + showProfileEntry: false }; + constructor(props) { + super(props); + this.state.showProfileEntry = this.props.showProfileEntry; + } + componentDidMount() { this.setupTestTone(); this.props.concurrentLoadDetector.addEventListener("concurrentload", this.onConcurrentLoad); @@ -110,6 +115,12 @@ class UIRoot extends Component { this.props.scene.removeEventListener("exit", this.exit); } + componentDidUpdate(prevProps) { + if (this.props.availableVREntryTypes && prevProps.availableVREntryTypes !== this.props.availableVREntryTypes) { + this.handleForcedVREntryType(); + } + } + onSceneLoaded = () => { this.setState({ sceneLoaded: true }); }; @@ -252,7 +263,7 @@ class UIRoot extends Component { }; enterGearVR = async () => { - if (this.state.availableVREntryTypes.gearvr === VR_DEVICE_AVAILABILITY.yes) { + if (this.props.availableVREntryTypes.gearvr === VR_DEVICE_AVAILABILITY.yes) { await this.performDirectEntryFlow(true); } else { this.exit(); @@ -271,7 +282,7 @@ class UIRoot extends Component { }; enterDaydream = async () => { - if (this.state.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe) { + if (this.props.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe) { this.exit(); // We are not in mobile chrome, so launch into chrome via an Intent URL @@ -368,23 +379,26 @@ class UIRoot extends Component { const AudioContext = window.AudioContext || window.webkitAudioContext; const micLevelAudioContext = new AudioContext(); const micSource = micLevelAudioContext.createMediaStreamSource(mediaStream); - const analyzer = micLevelAudioContext.createAnalyser(); - const levels = new Uint8Array(analyzer.fftSize); + const analyser = micLevelAudioContext.createAnalyser(); + analyser.fftSize = 32; + const levels = new Uint8Array(analyser.frequencyBinCount); - micSource.connect(analyzer); + micSource.connect(analyser); const micUpdateInterval = setInterval(() => { - analyzer.getByteTimeDomainData(levels); - + analyser.getByteTimeDomainData(levels); let v = 0; - for (let x = 0; x < levels.length; x++) { - v = Math.max(levels[x] - 127, v); + v = Math.max(levels[x] - 128, v); } - const level = v / 128.0; - this.micLevelMovingAverage.push(Date.now(), level); - this.setState({ micLevel: this.micLevelMovingAverage.movingAverage() }); + // Multiplier to increase visual indicator. + const multiplier = 6; + // We use a moving average to smooth out the visual animation or else it would twitch too fast for + // the css renderer to keep up. + this.micLevelMovingAverage.push(Date.now(), level * multiplier); + const average = this.micLevelMovingAverage.movingAverage(); + this.setState({ micLevel: average }); }, 50); const micDeviceId = this.micDeviceIdForMicLabel(this.micLabelForMediaStream(mediaStream)); @@ -466,7 +480,7 @@ class UIRoot extends Component { }; onAudioReadyButton = () => { - this.props.enterScene(this.state.mediaStream, this.state.enterInVR, this.state.janusRoomId); + this.props.enterScene(this.state.mediaStream, this.state.enterInVR, this.props.janusRoomId); const mediaStream = this.state.mediaStream; @@ -491,7 +505,7 @@ class UIRoot extends Component { }; render() { - if (!this.state.initialEnvironmentLoaded || !this.state.availableVREntryTypes || !this.state.janusRoomId) { + if (!this.props.initialEnvironmentLoaded || !this.props.availableVREntryTypes || !this.props.janusRoomId) { return ( <IntlProvider locale={lang} messages={messages}> <div className="loading-panel"> @@ -532,7 +546,7 @@ class UIRoot extends Component { /firefox/i.test(navigator.userAgent) && ( <label className="entry-panel__screen-sharing"> <input - className="entry-panel__screen-sharing-checkbox" + className="entry-panel__screen-sharing__checkbox" type="checkbox" value={this.state.shareScreen} onChange={this.setStateAndRequestScreen} @@ -545,21 +559,21 @@ class UIRoot extends Component { this.state.entryStep === ENTRY_STEPS.start ? ( <div className="entry-panel"> <TwoDEntryButton onClick={this.enter2D} /> - {this.state.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && ( + {this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && ( <GenericEntryButton onClick={this.enterVR} /> )} - {this.state.availableVREntryTypes.gearvr !== VR_DEVICE_AVAILABILITY.no && ( + {this.props.availableVREntryTypes.gearvr !== VR_DEVICE_AVAILABILITY.no && ( <GearVREntryButton onClick={this.enterGearVR} /> )} - {this.state.availableVREntryTypes.daydream !== VR_DEVICE_AVAILABILITY.no && ( + {this.props.availableVREntryTypes.daydream !== VR_DEVICE_AVAILABILITY.no && ( <DaydreamEntryButton onClick={this.enterDaydream} subtitle={ - this.state.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe ? daydreamMaybeSubtitle : "" + this.props.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe ? daydreamMaybeSubtitle : "" } /> )} - {this.state.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && ( + {this.props.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && ( <div className="entry-panel__secondary" onClick={this.enterVR}> <FormattedMessage id="entry.cardboard" /> </div> @@ -581,28 +595,22 @@ class UIRoot extends Component { id={this.state.entryStep == ENTRY_STEPS.mic_grant ? "audio.grant-subtitle" : "audio.granted-subtitle"} /> </div> - <div className="mic-grant-panel__icon"> + <div className="mic-grant-panel__button-container"> {this.state.entryStep == ENTRY_STEPS.mic_grant ? ( - <img - onClick={this.onMicGrantButton} - src="../assets/images/mic_denied.png" - srcSet="../assets/images/mic_denied@2x.png 2x" - className="mic-grant-panel__icon" - /> + <button className="mic-grant-panel__button" onClick={this.onMicGrantButton}> + <img src="../assets/images/mic_denied.png" srcSet="../assets/images/mic_denied@2x.png 2x" /> + </button> ) : ( - <img - onClick={this.onMicGrantButton} - src="../assets/images/mic_granted.png" - srcSet="../assets/images/mic_granted@2x.png 2x" - className="mic-grant-panel__icon" - /> + <button className="mic-grant-panel__button" onClick={this.onMicGrantButton}> + <img src="../assets/images/mic_granted.png" srcSet="../assets/images/mic_granted@2x.png 2x" /> + </button> )} </div> - <div className="mic-grant-panel__next" onClick={this.onMicGrantButton}> - <FormattedMessage - id={this.state.entryStep == ENTRY_STEPS.mic_grant ? "audio.grant-next" : "audio.granted-next"} - /> - </div> + {this.state.entryStep == ENTRY_STEPS.mic_granted && ( + <button className="mic-grant-panel__next" onClick={this.onMicGrantButton}> + <FormattedMessage id="audio.granted-next" /> + </button> + )} </div> ) : null; @@ -624,39 +632,49 @@ class UIRoot extends Component { )} </div> <div className="audio-setup-panel__levels"> - <div className="audio-setup-panel__levels__mic"> + <div className="audio-setup-panel__levels__icon"> + <img + src="../assets/images/level_background.png" + srcSet="../assets/images/level_background@2x.png 2x" + className="audio-setup-panel__levels__icon-part" + /> + <img + src="../assets/images/level_fill.png" + srcSet="../assets/images/level_fill@2x.png 2x" + className="audio-setup-panel__levels__icon-part" + style={micClip} + /> {this.state.audioTrack ? ( <img src="../assets/images/mic_level.png" srcSet="../assets/images/mic_level@2x.png 2x" - className="audio-setup-panel__levels__mic_icon" + className="audio-setup-panel__levels__icon-part" /> ) : ( <img src="../assets/images/mic_denied.png" srcSet="../assets/images/mic_denied@2x.png 2x" - className="audio-setup-panel__levels__mic_icon" + className="audio-setup-panel__levels__icon-part" /> )} - <img - src="../assets/images/level_fill.png" - srcSet="../assets/images/level_fill@2x.png 2x" - className="audio-setup-panel__levels__level" - style={micClip} - /> </div> - <div className="audio-setup-panel__levels__speaker"> + <div className="audio-setup-panel__levels__icon"> <img - src="../assets/images/speaker_level.png" - srcSet="../assets/images/speaker_level@2x.png 2x" - className="audio-setup-panel__levels__speaker_icon" + src="../assets/images/level_background.png" + srcSet="../assets/images/level_background@2x.png 2x" + className="audio-setup-panel__levels__icon-part" /> <img src="../assets/images/level_fill.png" srcSet="../assets/images/level_fill@2x.png 2x" - className="audio-setup-panel__levels__level" + className="audio-setup-panel__levels__icon-part" style={speakerClip} /> + <img + src="../assets/images/speaker_level.png" + srcSet="../assets/images/speaker_level@2x.png 2x" + className="audio-setup-panel__levels__icon-part" + /> </div> </div> {this.state.audioTrack && ( @@ -672,9 +690,16 @@ class UIRoot extends Component { </option> ))} </select> - <div className="audio-setup-panel__device-chooser__mic-icon"> - <img src="../assets/images/mic_small.png" srcSet="../assets/images/mic_small@2x.png 2x" /> - </div> + <img + className="audio-setup-panel__device-chooser__mic-icon" + src="../assets/images/mic_small.png" + srcSet="../assets/images/mic_small@2x.png 2x" + /> + <img + className="audio-setup-panel__device-chooser__dropdown-arrow" + src="../assets/images/dropdown_arrow.png" + srcSet="../assets/images/dropdown_arrow@2x.png 2x" + /> </div> )} {this.shouldShowHmdMicWarning() && ( @@ -689,9 +714,9 @@ class UIRoot extends Component { </span> </div> )} - <div className="audio-setup-panel__enter-button" onClick={this.onAudioReadyButton}> + <button className="audio-setup-panel__enter-button" onClick={this.onAudioReadyButton}> <FormattedMessage id="audio.enter-now" /> - </div> + </button> </div> ) : null; diff --git a/src/storage/store.js b/src/storage/store.js index 6f480aa76db6f14904b611cecf5b0b6b84b1f41f..4351ebeda99e9f9665fcbbf395858296cd2e56a4 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -1,5 +1,6 @@ import uuid from "uuid/v4"; import { Validator } from "jsonschema"; +import { merge } from "lodash"; const LOCAL_STORE_KEY = "___mozilla_duck"; const STORE_STATE_CACHE_KEY = Symbol(); @@ -16,6 +17,8 @@ export const SCHEMA = { type: "object", additionalProperties: false, properties: { + has_agreed_to_terms: { type: "boolean" }, + has_changed_name: { type: "boolean" }, display_name: { type: "string", pattern: "^[A-Za-z0-9-]{3,32}$" }, avatar_id: { type: "string" } } @@ -27,7 +30,8 @@ export const SCHEMA = { properties: { id: { type: "string", pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" }, profile: { $ref: "#/definitions/profile" }, - lastUsedMicDeviceId: { type: "string" } + lastUsedMicDeviceId: { type: "string" }, + lastEnteredAt: { type: "string" } }, additionalProperties: false @@ -55,7 +59,7 @@ export default class Store extends EventTarget { throw new Error("Store id is immutable."); } - const finalState = { ...this.state, ...newState }; + const finalState = merge(this.state, newState); const isValid = validator.validate(finalState, SCHEMA).valid; if (!isValid) { diff --git a/src/utils/hub-channel.js b/src/utils/hub-channel.js new file mode 100644 index 0000000000000000000000000000000000000000..13e51b21b1e1f3702432a7a04c6804949c332b88 --- /dev/null +++ b/src/utils/hub-channel.js @@ -0,0 +1,75 @@ +import moment from "moment-timezone"; + +export default class HubChannel { + constructor(store) { + this.store = store; + } + + setPhoenixChannel = channel => { + this.channel = channel; + }; + + sendEntryEvent = async () => { + if (!this.channel) { + console.warn("No phoenix channel initialized before room entry."); + return; + } + + let entryDisplayType = "Screen"; + + if (navigator.getVRDisplays) { + const vrDisplay = (await navigator.getVRDisplays()).find(d => d.isPresenting); + + if (vrDisplay) { + entryDisplayType = vrDisplay.displayName; + } + } + + // This is fairly hacky, but gets the # of initial occupants + let initialOccupantCount = 0; + + if (NAF.connection.adapter && NAF.connection.adapter.publisher) { + initialOccupantCount = NAF.connection.adapter.publisher.initialOccupants.length; + } + + const entryTimingFlags = this.getEntryTimingFlags(); + + const entryEvent = { + ...entryTimingFlags, + initialOccupantCount, + entryDisplayType, + userAgent: navigator.userAgent + }; + + this.channel.push("events:entered", entryEvent); + }; + + getEntryTimingFlags = () => { + const entryTimingFlags = { isNewDaily: true, isNewMonthly: true, isNewDayWindow: true, isNewMonthWindow: true }; + + if (!this.store.state.lastEnteredAt) { + return entryTimingFlags; + } + + const lastEntered = moment(this.store.state.lastEnteredAt); + const lastEnteredPst = moment(lastEntered).tz("America/Los_Angeles"); + const nowPst = moment().tz("America/Los_Angeles"); + const dayWindowAgo = moment().subtract(1, "day"); + const monthWindowAgo = moment().subtract(1, "month"); + + entryTimingFlags.isNewDaily = + lastEnteredPst.dayOfYear() !== nowPst.dayOfYear() || lastEnteredPst.year() !== nowPst.year(); + entryTimingFlags.isNewMonthly = + lastEnteredPst.month() !== nowPst.month() || lastEnteredPst.year() !== nowPst.year(); + entryTimingFlags.isNewDayWindow = lastEntered.isBefore(dayWindowAgo); + entryTimingFlags.isNewMonthWindow = lastEntered.isBefore(monthWindowAgo); + + return entryTimingFlags; + }; + + disconnect = () => { + if (this.channel) { + this.channel.socket.disconnect(); + } + }; +} diff --git a/src/utils/identity.js b/src/utils/identity.js index def830cbc4df4d9d1a71b0c10d7b54c5a5610b4a..db78b027e3e851aa254532f264438e362062576c 100644 --- a/src/utils/identity.js +++ b/src/utils/identity.js @@ -1,178 +1,108 @@ import { avatars } from "../assets/avatars/avatars.js"; const names = [ - "albattani", - "allen", - "almeida", - "agnesi", - "archimedes", - "ardinghelli", - "aryabhata", - "austin", - "babbage", - "banach", - "bardeen", - "bartik", - "bassi", - "beaver", - "bell", - "benz", - "bhabha", - "bhaskara", - "blackwell", - "bohr", - "booth", - "borg", - "bose", - "boyd", - "brahmagupta", - "brattain", - "brown", - "carson", - "chandrasekhar", - "shannon", - "clarke", - "colden", - "cori", - "cray", - "curran", - "curie", - "darwin", - "davinci", - "dijkstra", - "dubinsky", - "easley", - "edison", - "einstein", - "elion", - "engelbart", - "euclid", - "euler", - "fermat", - "fermi", - "feynman", - "franklin", - "galileo", - "gates", - "goldberg", - "goldstine", - "goldwasser", - "golick", - "goodall", - "haibt", - "hamilton", - "hawking", - "heisenberg", - "hermann", - "heyrovsky", - "hodgkin", - "hoover", - "hopper", - "hugle", - "hypatia", - "jackson", - "jang", - "jennings", - "jepsen", - "johnson", - "joliot", - "jones", - "kalam", - "kare", - "keller", - "kepler", - "khorana", - "kilby", - "kirch", - "knuth", - "kowalevski", - "lalande", - "lamarr", - "lamport", - "leakey", - "leavitt", - "lewin", - "lichterman", - "liskov", - "lovelace", - "lumiere", - "mahavira", - "mayer", - "mccarthy", - "mcclintock", - "mclean", - "mcnulty", - "meitner", - "meninsky", - "mestorf", - "minsky", - "mirzakhani", - "morse", - "murdock", - "neumann", - "newton", - "nightingale", - "nobel", - "noether", - "northcutt", - "noyce", - "panini", - "pare", - "pasteur", - "payne", - "perlman", - "pike", - "poincare", - "poitras", - "ptolemy", - "raman", - "ramanujan", - "ride", - "montalcini", - "ritchie", - "roentgen", - "rosalind", - "saha", - "sammet", - "shaw", - "shirley", - "shockley", - "sinoussi", - "snyder", - "spence", - "stallman", - "stonebraker", - "swanson", - "swartz", - "swirles", - "tesla", - "thompson", - "torvalds", - "turing", - "varahamihira", - "visvesvaraya", - "volhard", - "wescoff", - "wiles", - "williams", - "wilson", - "wing", - "wozniak", - "wright", - "yalow", - "yonath" + "Baers-Pochard", + "Baikal-Teal", + "Barrows-Goldeneye", + "Blue-Billed", + "Blue-Duck", + "Blue-Winged", + "Brown-Teal", + "Bufflehead", + "Canvasback", + "Cape-Shoveler", + "Chestnut-Teal", + "Chiloe-Wigeon", + "Cinnamon-Teal", + "Comb-Duck", + "Common-Eider", + "Common-Goldeneye", + "Common-Merganser", + "Common-Pochard", + "Common-Scoter", + "Common-Shelduck", + "Cotton-Pygmy", + "Crested-Duck", + "Crested-Shelduck", + "Eatons-Pintail", + "Falcated", + "Ferruginous", + "Freckled-Duck", + "Gadwall", + "Garganey", + "Greater-Scaup", + "Green-Pygmy", + "Grey-Teal", + "Hardhead", + "Harlequin", + "Hartlaubs", + "Hooded-Merganser", + "Hottentot-Teal", + "Kelp-Goose", + "King-Eider", + "Lake-Duck", + "Laysan-Duck", + "Lesser-Scaup", + "Long-Tailed", + "Maccoa-Duck", + "Mallard", + "Mandarin", + "Marbled-Teal", + "Mellers-Duck", + "Merganser", + "Northern-Pintail", + "Orinoco-Goose", + "Paradise-Shelduck", + "Plumed-Whistler", + "Puna-Teal", + "Pygmy-Goose", + "Radjah-Shelduck", + "Red-Billed", + "Red-Crested", + "Red-Shoveler", + "Ring-Necked", + "Ringed-Teal", + "Rosy-Billed", + "Ruddy-Shelduck", + "Salvadoris-Teal", + "Scaly-Sided", + "Shelduck", + "Shoveler", + "Silver-Teal", + "Smew", + "Spectacled-Eider", + "Spot-Billed", + "Spotted-Whistler", + "Steamerduck", + "Stellers-Eider", + "Sunda-Teal", + "Surf-Scoter", + "Tufted-Duck", + "Velvet-Scoter", + "Wandering-Whistler", + "Whistling-duck", + "White-Backed", + "White-Cheeked", + "White-Winged", + "Wigeon", + "Wood-Duck", + "Yellow-Billed" ]; function selectRandom(arr) { return arr[Math.floor(Math.random() * arr.length)]; } +export function generateRandomName() { + return `${selectRandom(names)}-${Math.floor(10000 + Math.random() * 10000)}`; +} + export const avatarIds = avatars.map(av => av.id); export function generateDefaultProfile() { - const name = selectRandom(names); return { - display_name: name.replace(/^./, name[0].toUpperCase()), + has_agreed_to_terms: false, + has_changed_name: false, avatar_id: selectRandom(avatarIds) }; } diff --git a/yarn.lock b/yarn.lock index 00fd724ba4590d8fb8f3d2a313e089d43e88f030..8820642a27070f29460b409b87626ac173c298f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5182,10 +5182,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" @@ -5304,6 +5300,16 @@ module-deps@^6.0.0: through2 "^2.0.0" xtend "^4.0.0" +moment-timezone@^0.5.14: + version "0.5.14" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.14.tgz#4eb38ff9538b80108ba467a458f3ed4268ccfcb1" + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@^2.22.0: + version "2.22.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.0.tgz#7921ade01017dd45186e7fee5f424f0b8663a730" + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -5359,12 +5365,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" @@ -6010,6 +6016,10 @@ performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" +phoenix@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.3.0.tgz#1df2c27f986ee295e37c9983ec28ebac1d7f4a3e" + pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"