diff --git a/src/assets/stylesheets/entry.scss b/src/assets/stylesheets/entry.scss index 08f3144d61d0b8c5ccf24057cc67a404431417a0..04f15e84f90df3851181ea9b1362016ae0d5ebf3 100644 --- a/src/assets/stylesheets/entry.scss +++ b/src/assets/stylesheets/entry.scss @@ -10,15 +10,19 @@ :local(.collapse) { @extend %fa-icon-button; position: absolute; - top: 4px; - right: 24px; + top: 0px; + right: 12px; + width: 32px; + height: 32px; } :local(.expand) { @extend %fa-icon-button; position: absolute; - top: -32px; - right: 24px; + top: -48px; + right: 12px; + width: 32px; + height: 32px; } } @@ -86,6 +90,7 @@ :local(.profile-name) { margin-top: 4px; + margin-bottom: 16px; @extend %default-font; cursor: pointer; font-weight: bold; @@ -107,6 +112,7 @@ flex-direction: column; height: 100%; justify-content: flex-end; + align-items: center; :local(.action-button) { @extend %action-button; diff --git a/src/assets/stylesheets/hub.scss b/src/assets/stylesheets/hub.scss index e24bf7f06dd6c27231971ce03e244ac829087d7c..8ce382b26542abc5df4cc138c0846035c700f81a 100644 --- a/src/assets/stylesheets/hub.scss +++ b/src/assets/stylesheets/hub.scss @@ -13,11 +13,14 @@ display: none; } +.grab-cursor { + cursor: grab; +} + .no-cursor { cursor: none; } - .webxr-realities, .webxr-sessions { @extend %unselectable } diff --git a/src/assets/stylesheets/info-dialog.scss b/src/assets/stylesheets/info-dialog.scss index 27ea17a7683c90b7d522bb77f3e635f7fadec0da..6e82b74501cd5c142262a90dae6d5900fc6b4dd6 100644 --- a/src/assets/stylesheets/info-dialog.scss +++ b/src/assets/stylesheets/info-dialog.scss @@ -62,7 +62,7 @@ &__close { position: absolute; - left: 12px; + right: 12px; top: 6px; color: white; font-size: 1.4em; diff --git a/src/assets/stylesheets/invite-dialog.scss b/src/assets/stylesheets/invite-dialog.scss new file mode 100644 index 0000000000000000000000000000000000000000..cd4bb07e15364013160c98c024a33f99a770b8a3 --- /dev/null +++ b/src/assets/stylesheets/invite-dialog.scss @@ -0,0 +1,99 @@ +@import 'shared.scss'; + +:local(.dialog) { + background-color: $action-color; + border-radius: 12px; + box-shadow: 0px 5px 30px 1px #333; + display: flex; + flex-direction: column; + align-items: center; + margin-top: 10px; + padding: 14px; + text-align: center; + position: relative; + font-size: 1.1em; + + a { + color: white; + text-decoration: underline; + font-weight: bold; + } +} + +:local(.close) { + position: absolute; + width: 30px; + height: 30px; + right: 12px; + font-size: 2.0em; + top: 0px; + cursor: pointer; +} + +:local(.attach-point) { + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid $action-color; + position: absolute; + top: -5px; +} + +:local(.code) { + color: black; + font-family: monospace; + font-weight: bold; + text-decoration: none; + font-size: 2.0em; + display: flex; + margin: 12px; +} + +:local(.header) { + font-weight: bold; +} + +:local(.keep-open) { + font-size: 0.8em; +} + +:local(.domain) { + input { + @extend %default-font; + font-weight: bold; + text-decoration: none; + color: black; + text-align: center; + background-color: white; + border: 1px solid #e2e2e2; + border-radius: 12px; + margin: 12px; + font-size: 1.8em; + padding: 14px; + display: block; + width: 295px; + } +} + +:local(.copy-link-button) { + @extend %action-button-selected; + margin-top: 4px; +} + +:local(.digit) { + padding: 0 8px; + margin: 2px; + background-color: white; + border: 1px solid #e2e2e2; + border-radius: 12px; + width: 32px; + height: 64px; + display: flex; + align-items: center; + justify-content: center; +} + +:local(.code-loading-panel) { + background: none; +} diff --git a/src/assets/stylesheets/link-dialog.scss b/src/assets/stylesheets/link-dialog.scss deleted file mode 100644 index aafcac281e33a362fc69871f9474fff32ddd73b7..0000000000000000000000000000000000000000 --- a/src/assets/stylesheets/link-dialog.scss +++ /dev/null @@ -1,29 +0,0 @@ -:local(.domain) , :local(.code) { - color: white; - font-family: monospace; - font-weight: bold; - text-decoration: none; -} - -:local(.domain) { - font-size: 3em; - padding: 14px; - display: block; -} - -:local(.code) { - font-size: 4.0em; - padding: 8px; -} - -:local(.keep-open) { - font-size: 0.8em; -} - -:local(.digit) { - padding: 0 8px; -} - -:local(.code-loading-panel) { - background: none; -} diff --git a/src/assets/stylesheets/link.scss b/src/assets/stylesheets/link.scss index b94325c5039062d7848a4d254df9d27b88a7ceb4..accfb2be29a363c1022e558a31a9993d6acf92b8 100644 --- a/src/assets/stylesheets/link.scss +++ b/src/assets/stylesheets/link.scss @@ -169,7 +169,7 @@ a { background: transparent; color: white; margin: 0; - font-size: 64pt; + font-size: 52pt; border: 0; width: 295px; letter-spacing: 0.08em; diff --git a/src/assets/stylesheets/profile.scss b/src/assets/stylesheets/profile.scss index be3161f6f8b20bef71e3c65245517fe6c743ed8e..5ebac8182d8ef8d71b7430849ebb0231eea781c8 100644 --- a/src/assets/stylesheets/profile.scss +++ b/src/assets/stylesheets/profile.scss @@ -14,7 +14,7 @@ background-color: rgba(255, 255, 255, 0.9); :local(.logo) { - width: 100px; + width: 150px; position: absolute; right: 32px; bottom: 32px; @@ -71,7 +71,7 @@ a { margin: 0px 12px; - color: $grey-text; + color: $dark-grey-text; } } diff --git a/src/assets/stylesheets/shared.scss b/src/assets/stylesheets/shared.scss index 44155ae2ed6e7f48ea4ce9d78fecca10a1859d99..e81425cfe23065ac4316cfe52cd433e429dd550b 100644 --- a/src/assets/stylesheets/shared.scss +++ b/src/assets/stylesheets/shared.scss @@ -3,12 +3,13 @@ $dark-transparent: rgba(0, 0, 0, 0.4); $darker-transparent: rgba(0, 0, 0, 0.6); $darkest-transparent: rgba(0, 0, 0, 0.9); $grey-text: rgba(192, 192, 192, 1.0); +$dark-grey-text: rgba(64, 64, 64, 1.0); $light-text: rgba(240, 240, 240, 1.0); $light-grey: lightgrey; $dark-grey: rgba(128, 128, 128, 1.0); $darker-grey: rgba(64, 64, 64, 1.0); $darkest-grey: rgba(32, 32, 32, 1.0); -$action-text: #2F80ED; +$action-color: #2F80ED; %unselectable { -moz-user-select: none; @@ -96,6 +97,11 @@ $action-text: #2F80ED; min-width: 150px; } +%action-button-selected { + background: white; + color: $action-color; +} + %bottom-action-button { @extend %bottom-button; background: #2F80ED; diff --git a/src/assets/stylesheets/ui-root.scss b/src/assets/stylesheets/ui-root.scss index 2ceeacef10fa9224b02c58165197ae9ee3a37292..0de53b2874508c94b1a66260369a95de05e2971a 100644 --- a/src/assets/stylesheets/ui-root.scss +++ b/src/assets/stylesheets/ui-root.scss @@ -71,7 +71,7 @@ @extend %unselectable; } -:local(.nag-button) { +:local(.invite-container) { @extend %unselectable; position: absolute; top: 0; @@ -79,9 +79,10 @@ margin-top: 16px; width: 100%; display: flex; + flex-direction: column; align-items: center; justify-content: center; - pointer-events: none; + pointer-events: auto; button { @extend %action-button; @@ -95,7 +96,13 @@ } } -:local(.nag-button-below-hud) { +:local(.invite-container-inverted) { + button { + @extend %action-button-selected; + } +} + +:local(.invite-container-below-hud) { margin-top: 100px; } diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 53ee3718731361912f71b82454a794c04a5087f1..a94df090766c24d16ebf686ae03b59476e42bb55 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -74,15 +74,18 @@ "home.have_entry_code": "have a link code?", "mailing_list.privacy_label": "I'm okay with Mozilla handling my info as explained in", "mailing_list.privacy_link": "this Privacy Notice", - "link.in_your_browser": "In your headset's browser, go to:", - "link.enter_code": "Then, enter this link code:", - "link.do_not_close": "Keep this dialog open to use this code.", "link.link_page_header": "Enter your code:", "link.dont_have_a_code": "Don't have a code?", "link.create_a_room": "Create a Room", "link.try_again": "We couldn't find that code. Please try again.", "help.report_issue": "Report an Issue", "scene.logo_tagline": "A new way to get together", - "scene.create_button": "create a room with this scene" + "scene.create_button": "create a room with this scene", + "invite.in_your_browser": "In your headset's browser, go to:", + "invite.entry_code": "Entry Code:", + "invite.and_enter_code": "and enter code:", + "invite.join_at": "Join room at ", + "invite.direct_link": "Direct Link:", + "invite.enter_in_browser": "Enter in browser:" } } diff --git a/src/hub.html b/src/hub.html index 19a8282eab596478712464d1a9bef2fb95be8a8b..16dd5bdade66eef08ede23d13c7e104b49847882 100644 --- a/src/hub.html +++ b/src/hub.html @@ -21,6 +21,7 @@ </audio> <a-scene + class="grab-cursor" renderer="antialias: true; gammaOutput: true; sortObjects: true; physicallyCorrectLights: true;" gamma-factor networked-scene="adapter: janus; audio: true; debug: true; connectOnLoad: false;" diff --git a/src/hub.js b/src/hub.js index f7b2717741233ac5556f752fbb33a0841b1865e6..e397164f5ad63a6a3f6f97141f2dd4ab0632c171 100644 --- a/src/hub.js +++ b/src/hub.js @@ -73,7 +73,6 @@ import ReactDOM from "react-dom"; import React from "react"; import UIRoot from "./react-components/ui-root"; import HubChannel from "./utils/hub-channel"; -import LinkChannel from "./utils/link-channel"; import { connectToReticulum } from "./utils/phoenix-utils"; import { disableiOSZoom } from "./utils/disable-ios-zoom"; import { resolveMedia } from "./utils/media-utils"; @@ -252,7 +251,7 @@ async function handleHubChannelJoined(entryManager, hubChannel, data) { environmentScene.setAttribute("gltf-bundle", `src: ${sceneUrl}`); } - remountUI({ hubId: hub.hub_id, hubName: hub.name }); + remountUI({ hubId: hub.hub_id, hubName: hub.name, hubEntryCode: hub.entry_code }); scene.setAttribute("networked-scene", { room: hub.hub_id, @@ -294,7 +293,7 @@ document.addEventListener("DOMContentLoaded", () => { const scene = document.querySelector("a-scene"); const hubChannel = new HubChannel(store); const entryManager = new SceneEntryManager(hubChannel); - const linkChannel = new LinkChannel(store); + entryManager.init(); window.APP.scene = scene; @@ -384,6 +383,4 @@ document.addEventListener("DOMContentLoaded", () => { if (!NAF.connection.adapter) return; NAF.connection.adapter.onData(data); }); - - linkChannel.setSocket(socket); }); diff --git a/src/link.js b/src/link.js index 401fe54d9b8b9bd91df1c2140257710502a23add..908f7f64d4fadd8f906b09a7874e8e882c93b81d 100644 --- a/src/link.js +++ b/src/link.js @@ -3,18 +3,7 @@ import React from "react"; import ReactDOM from "react-dom"; import registerTelemetry from "./telemetry"; import LinkRoot from "./react-components/link-root"; -import LinkChannel from "./utils/link-channel"; -import { connectToReticulum } from "./utils/phoenix-utils"; -import Store from "./storage/store"; registerTelemetry(); -const socket = connectToReticulum(); -const store = new Store(); -store.init(); - -const linkChannel = new LinkChannel(store); - -linkChannel.setSocket(socket); - -ReactDOM.render(<LinkRoot store={store} linkChannel={linkChannel} />, document.getElementById("link-root")); +ReactDOM.render(<LinkRoot />, document.getElementById("link-root")); diff --git a/src/react-components/invite-dialog.js b/src/react-components/invite-dialog.js index e01fc1282cf835e8e5a65363b3dfc6ee0f4e891f..fce6863c845227dc3c5cd9a89fc7c3c5add2ba78 100644 --- a/src/react-components/invite-dialog.js +++ b/src/react-components/invite-dialog.js @@ -1,56 +1,101 @@ import React, { Component } from "react"; +import PropTypes from "prop-types"; import copy from "copy-to-clipboard"; -import DialogContainer from "./dialog-container.js"; +import { FormattedMessage } from "react-intl"; + +import styles from "../assets/stylesheets/invite-dialog.scss"; + +function pad(num, size) { + let s = `${num}`; + while (s.length < size) s = `0${s}`; + return s; +} export default class InviteDialog extends Component { + static propTypes = { + entryCode: PropTypes.number, + dialogType: PropTypes.string, + onClose: PropTypes.func + }; + state = { copyLinkButtonText: "copy" }; - constructor(props) { - super(props); - const loc = document.location; - this.shareLink = `${loc.protocol}//${loc.host}${loc.pathname}`; - } - copyLinkClicked = link => { copy(link); this.setState({ copyLinkButtonText: "copied!" }); }; - shareLinkClicked = () => { - navigator.share({ - title: document.title, - url: this.shareLink - }); - }; - render() { - return ( - <DialogContainer title="Invite Others" {...this.props}> - <div> - <div>Just share the link and they'll join you:</div> - <div className="invite-form"> - <input - type="text" - readOnly - onFocus={e => e.target.select()} - value={this.shareLink} - className="invite-form__link_field" - /> - <div className="invite-form__buttons"> - {navigator.share && ( - <button className="invite-form__action-button" onClick={this.shareLinkClicked}> - <span>share</span> - </button> - )} - <button className="invite-form__action-button" onClick={this.copyLinkClicked.bind(this, this.shareLink)}> - <span>{this.state.copyLinkButtonText}</span> - </button> - </div> + const { entryCode } = this.props; + + const entryCodeString = pad(entryCode, 6); + const shareLink = `hub.link/${entryCodeString}`; + const isHeadsetLink = this.props.dialogType === "headset"; + + if (isHeadsetLink) { + return ( + <div className={styles.dialog}> + <div className={styles.attachPoint} /> + <div className={styles.close} onClick={() => this.props.onClose()}> + <span>×</span> + </div> + <div> + <FormattedMessage id="invite.in_your_browser" /> + </div> + <div className={styles.domain}> + <input type="text" readOnly onFocus={e => e.target.select()} value="hub.link" /> + </div> + <div> + <FormattedMessage id="invite.and_enter_code" /> + </div> + <div className={styles.code}> + {entryCodeString.split("").map((d, i) => ( + <div className={styles.digit} key={`link_code_${i}`}> + {d} + </div> + ))} + </div> + </div> + ); + } else { + return ( + <div className={styles.dialog}> + <div className={styles.attachPoint} /> + <div className={styles.close} onClick={() => this.props.onClose()}> + <span>×</span> + </div> + <div className={styles.header}> + <FormattedMessage id="invite.entry_code" /> + </div> + <div> + <FormattedMessage id="invite.join_at" /> + <a href="https://hub.link" target="_blank" rel="noopener noreferrer"> + hub.link + </a> + </div> + <div className={styles.code}> + {entryCodeString.split("").map((d, i) => ( + <div className={styles.digit} key={`link_code_${i}`}> + {d} + </div> + ))} + </div> + <div className={styles.header} style={{ marginTop: "16px" }}> + <FormattedMessage id="invite.direct_link" /> + </div> + <div> + <FormattedMessage id="invite.enter_in_browser" /> + </div> + <div className={styles.domain}> + <input type="text" readOnly onFocus={e => e.target.select()} value={shareLink} /> </div> + <button className={styles.copyLinkButton} onClick={this.copyLinkClicked.bind(this, "https://" + shareLink)}> + <span>{this.state.copyLinkButtonText}</span> + </button> </div> - </DialogContainer> - ); + ); + } } } diff --git a/src/react-components/link-dialog.js b/src/react-components/link-dialog.js deleted file mode 100644 index 456090494e4cc9e00799d697530adbe460ae4bd2..0000000000000000000000000000000000000000 --- a/src/react-components/link-dialog.js +++ /dev/null @@ -1,58 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classNames from "classnames"; -import { FormattedMessage } from "react-intl"; -import DialogContainer from "./dialog-container.js"; - -import styles from "../assets/stylesheets/link-dialog.scss"; - -export default class LinkDialog extends Component { - static propTypes = { - linkCode: PropTypes.string - }; - - render() { - const { linkCode, ...other } = this.props; - if (!linkCode) { - return ( - <DialogContainer title="Open on Headset" {...other}> - <div> - <div className={classNames("loading-panel", styles.codeLoadingPanel)}> - <div className="loader-wrap"> - <div className="loader"> - <div className="loader-center" /> - </div> - </div> - </div> - </div> - </DialogContainer> - ); - } - - return ( - <DialogContainer title="Open on Headset" {...other}> - <div> - <div> - <FormattedMessage id="link.in_your_browser" /> - </div> - <a href="https://hub.link" className={styles.domain} target="_blank" rel="noopener noreferrer"> - hub.link - </a> - <div> - <FormattedMessage id="link.enter_code" /> - </div> - <div className={styles.code}> - {linkCode.split("").map((d, i) => ( - <span className={styles.digit} key={`link_code_${i}`}> - {d} - </span> - ))} - </div> - <div className={styles.keepOpen}> - <FormattedMessage id="link.do_not_close" /> - </div> - </div> - </DialogContainer> - ); - } -} diff --git a/src/react-components/link-root.js b/src/react-components/link-root.js index 84b7101637357253da095a0c69bd0f20382318e4..f9aab014bb2f0ba5e6779a4c1020d34aa8416f96 100644 --- a/src/react-components/link-root.js +++ b/src/react-components/link-root.js @@ -8,16 +8,14 @@ import classNames from "classnames"; import styles from "../assets/stylesheets/link.scss"; import { disableiOSZoom } from "../utils/disable-ios-zoom"; -const MAX_DIGITS = 4; +const MAX_DIGITS = 6; addLocaleData([...en]); disableiOSZoom(); class LinkRoot extends Component { static propTypes = { - intl: PropTypes.object, - store: PropTypes.object, - linkChannel: PropTypes.object + intl: PropTypes.object }; state = { @@ -62,30 +60,15 @@ class LinkRoot extends Component { this.setState({ enteredDigits: enteredDigits.substring(0, enteredDigits.length - 1) }); }; - attemptLink = code => { - this.props.linkChannel - .attemptLink(code) - .then(response => { - // If there is a profile from the linked device, copy it over if we don't have one yet. - if (response.profile) { - const { hasChangedName } = this.props.store.state.activity; - - if (!hasChangedName) { - this.props.store.update({ activity: { hasChangedName: true }, profile: response.profile }); - } - } - - if (response.path) { - window.location.href = response.path; - } - }) - .catch(e => { - this.setState({ failedAtLeastOnce: true, enteredDigits: "" }); - - if (!(e instanceof Error && (e.message === "in_use" || e.message === "failed"))) { - throw e; - } - }); + attemptLink = async code => { + const url = "https://hub.link/" + code; + const res = await fetch(url); + + if (res.status >= 400) { + this.setState({ failedAtLeastOnce: true, enteredDigits: "" }); + } else { + document.location = url; + } }; render() { @@ -119,7 +102,7 @@ class LinkRoot extends Component { onChange={ev => { this.setState({ enteredDigits: ev.target.value }); }} - placeholder="- - - -" + placeholder="- - - - - -" /> </div> diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js index 9beb6e7a4cff70e2083ac012df120d8f0a7b0e18..307ae73b0dac97d5b50908d5773f28bc2953aec1 100644 --- a/src/react-components/profile-entry-panel.js +++ b/src/react-components/profile-entry-panel.js @@ -81,6 +81,18 @@ class ProfileEntryPanel extends Component { <label htmlFor="#profile-entry-display-name" className={styles.title}> <FormattedMessage id="profile.header" /> </label> + <input + id="profile-entry-display-name" + className={styles.formFieldText} + value={this.state.displayName} + onFocus={e => e.target.select()} + onChange={e => this.setState({ displayName: e.target.value })} + required + spellCheck="false" + pattern={SCHEMA.definitions.profile.properties.displayName.pattern} + title={formatMessage({ id: "profile.display_name.validation_warning" })} + ref={inp => (this.nameInput = inp)} + /> <div className={styles.avatarSelectorContainer}> <div className="loading-panel"> <div className="loader-wrap"> @@ -95,18 +107,6 @@ class ProfileEntryPanel extends Component { ref={ifr => (this.avatarSelector = ifr)} /> </div> - <input - id="profile-entry-display-name" - className={styles.formFieldText} - value={this.state.displayName} - onFocus={e => e.target.select()} - onChange={e => this.setState({ displayName: e.target.value })} - required - spellCheck="false" - pattern={SCHEMA.definitions.profile.properties.displayName.pattern} - title={formatMessage({ id: "profile.display_name.validation_warning" })} - ref={inp => (this.nameInput = inp)} - /> <input className={styles.formSubmit} type="submit" value={formatMessage({ id: "profile.save" })} /> <div className={styles.links}> <a target="_blank" rel="noopener noreferrer" href="https://github.com/mozilla/hubs/blob/master/TERMS.md"> diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 0c3fa5e8d463aa68b88c5736b358a13654e36fbc..e08bad5d3bc4bcbb3b2f28071e01f935793f6729 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -22,9 +22,8 @@ import ProfileEntryPanel from "./profile-entry-panel"; import HelpDialog from "./help-dialog.js"; import SafariDialog from "./safari-dialog.js"; import WebVRRecommendDialog from "./webvr-recommend-dialog.js"; -import InviteDialog from "./invite-dialog.js"; import InviteTeamDialog from "./invite-team-dialog.js"; -import LinkDialog from "./link-dialog.js"; +import InviteDialog from "./invite-dialog.js"; import CreateObjectDialog from "./create-object-dialog.js"; import TwoDHUD from "./2d-hud"; import { faUsers } from "@fortawesome/free-solid-svg-icons/faUsers"; @@ -74,7 +73,7 @@ class UIRoot extends Component { store: PropTypes.object, scene: PropTypes.object, hubChannel: PropTypes.object, - linkChannel: PropTypes.object, + hubEntryCode: PropTypes.number, showProfileEntry: PropTypes.bool, availableVREntryTypes: PropTypes.object, environmentSceneLoaded: PropTypes.bool, @@ -90,8 +89,7 @@ class UIRoot extends Component { entryStep: ENTRY_STEPS.start, enterInVR: false, dialog: null, - linkCode: null, - linkCodeCancel: null, + inviteDialogType: null, shareScreen: false, requestedScreen: false, @@ -141,11 +139,7 @@ class UIRoot extends Component { this.props.scene.removeEventListener("exit", this.exit); } - componentDidUpdate(prevProps) { - if (this.props.availableVREntryTypes && prevProps.availableVREntryTypes !== this.props.availableVREntryTypes) { - this.handleForcedVREntryType(); - } - } + componentDidUpdate(prevProps) {} onSceneLoaded = () => { this.setState({ sceneLoaded: true }); @@ -175,10 +169,10 @@ class UIRoot extends Component { this.props.scene.emit("spawn_pen"); }; - handleForcedVREntryType = () => { - if (!this.props.forcedVREntryType) return; - - if (this.props.forcedVREntryType.startsWith("daydream")) { + handleStartEntry = () => { + if (!this.props.forcedVREntryType) { + this.setState({ entryStep: ENTRY_STEPS.device }); + } else if (this.props.forcedVREntryType.startsWith("daydream")) { this.enterDaydream(); } else if (this.props.forcedVREntryType.startsWith("vr")) { this.enterVR(); @@ -506,12 +500,16 @@ class UIRoot extends Component { this.setState({ entryStep: ENTRY_STEPS.finished }); }; - attemptLink = async () => { - this.showLinkDialog(); - const { code, cancel, onFinished } = await this.props.linkChannel.generateCode(); - this.setState({ linkCode: code, linkCodeCancel: cancel }); - this.showLinkDialog(); - onFinished.then(this.closeDialog); + showInviteDialog = async forHeadset => { + this.setState({ inviteDialogType: forHeadset ? "headset" : "invite" }); + }; + + toggleInviteDialog = async () => { + if (this.state.inviteDialogType) { + this.setState({ inviteDialogType: null }); + } else { + this.showInviteDialog(false); + } }; closeDialog = async () => { @@ -534,10 +532,6 @@ class UIRoot extends Component { this.setState({ dialog: <SafariDialog onClose={this.closeDialog} /> }); } - showInviteDialog() { - this.setState({ dialog: <InviteDialog onClose={this.closeDialog} /> }); - } - showInviteTeamDialog() { this.setState({ dialog: <InviteTeamDialog hubChannel={this.props.hubChannel} onClose={this.closeDialog} /> }); } @@ -546,10 +540,6 @@ class UIRoot extends Component { this.setState({ dialog: <CreateObjectDialog onCreate={this.createObject} onClose={this.closeDialog} /> }); } - showLinkDialog() { - this.setState({ dialog: <LinkDialog linkCode={this.state.linkCode} onClose={this.closeDialog} /> }); - } - showWebVRRecommendDialog() { this.setState({ dialog: <WebVRRecommendDialog onClose={this.closeDialog} /> }); } @@ -644,10 +634,7 @@ class UIRoot extends Component { renderEntryStartPanel = () => { return ( <div className={entryStyles.entryPanel}> - <div className={entryStyles.title}> - {this.props.hubName}'s - <FormattedMessage id="entry.enter-room-title" /> - </div> + <div className={entryStyles.title}>{this.props.hubName}</div> <div className={entryStyles.center}> <div onClick={() => this.setState({ showProfileEntry: true })} className={entryStyles.profileName}> @@ -659,7 +646,7 @@ class UIRoot extends Component { <div className={entryStyles.buttonContainer}> <button className={classNames([entryStyles.actionButton, entryStyles.wideButton])} - onClick={() => this.setState({ entryStep: ENTRY_STEPS.device })} + onClick={() => this.handleStartEntry()} > <FormattedMessage id="entry.enter-room" /> </button> @@ -695,7 +682,10 @@ class UIRoot extends Component { {this.props.availableVREntryTypes.daydream === VR_DEVICE_AVAILABILITY.yes && ( <DaydreamEntryButton onClick={this.enterDaydream} subtitle={null} /> )} - <DeviceEntryButton onClick={this.attemptLink} isInHMD={this.props.availableVREntryTypes.isInHMD} /> + <DeviceEntryButton + onClick={() => this.showInviteDialog(true)} + isInHMD={this.props.availableVREntryTypes.isInHMD} + /> {this.props.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && ( <div className={entryStyles.secondary} onClick={this.enterVR}> <FormattedMessage id="entry.cardboard" /> @@ -932,17 +922,6 @@ class UIRoot extends Component { <div className={styles.ui}> {this.state.dialog} - <button onClick={() => this.showHelpDialog()} className={styles.helpIcon}> - <i> - <FontAwesomeIcon icon={faQuestion} /> - </i> - </button> - - <div className={styles.presenceInfo}> - <FontAwesomeIcon icon={faUsers} /> - <span className={styles.occupantCount}>{this.props.occupantCount || "-"}</span> - </div> - {this.state.showProfileEntry && ( <ProfileEntryPanel finished={this.onProfileFinished} store={this.props.store} /> )} @@ -953,14 +932,45 @@ class UIRoot extends Component { </div> )} - {!this.props.availableVREntryTypes.isInHMD && - (!entryFinished || this.props.occupantCount <= 1) && ( - <div className={classNames({ [styles.nagButton]: true, [styles.nagButtonBelowHud]: entryFinished })}> - <button onClick={() => this.showInviteDialog()}> + <div + className={classNames({ + [styles.inviteContainer]: true, + [styles.inviteContainerBelowHud]: entryFinished, + [styles.inviteContainerInverted]: this.state.inviteDialogType + })} + > + {!this.props.availableVREntryTypes.isInHMD && + (!entryFinished || this.props.occupantCount <= 1) && ( + <button onClick={() => this.toggleInviteDialog()}> <FormattedMessage id="entry.invite-others-nag" /> </button> - </div> + )} + {this.props.availableVREntryTypes.isInHMD && + entryFinished && ( + <button onClick={() => this.props.scene.enterVR()}> + <FormattedMessage id="entry.return-to-vr" /> + </button> + )} + {this.state.inviteDialogType && ( + <InviteDialog + entryCode={this.props.hubEntryCode} + dialogType={this.state.inviteDialogType} + onClose={() => this.setState({ inviteDialogType: null })} + /> )} + </div> + + <button onClick={() => this.showHelpDialog()} className={styles.helpIcon}> + <i> + <FontAwesomeIcon icon={faQuestion} /> + </i> + </button> + + <div className={styles.presenceInfo}> + <FontAwesomeIcon icon={faUsers} /> + <span className={styles.occupantCount}>{this.props.occupantCount || "-"}</span> + </div> + {this.state.entryStep === ENTRY_STEPS.finished ? ( <div> <TwoDHUD.TopHUD @@ -973,13 +983,6 @@ class UIRoot extends Component { onSpawnPen={this.spawnPen} onSpawnCamera={() => this.props.scene.emit("action_spawn_camera")} /> - {this.props.availableVREntryTypes.isInHMD && ( - <div className={styles.nagButton}> - <button onClick={() => this.props.scene.enterVR()}> - <FormattedMessage id="entry.return-to-vr" /> - </button> - </div> - )} {this.props.isSupportAvailable && ( <div className={styles.nagCornerButton}> <button onClick={() => this.showInviteTeamDialog()}> diff --git a/src/scene-entry-manager.js b/src/scene-entry-manager.js index e89de2b5247155a538926528ab4fd9f419b3d196..f5b65c1fdb3234391cae1220c34a574293c0a742 100644 --- a/src/scene-entry-manager.js +++ b/src/scene-entry-manager.js @@ -21,9 +21,16 @@ export default class SceneEntryManager { this.hubChannel = hubChannel; this.store = window.APP.store; this.scene = document.querySelector("a-scene"); + this.cursorController = document.querySelector("#cursor-controller"); this.playerRig = document.querySelector("#player-rig"); } + init = () => { + this.whenSceneLoaded(() => { + this.cursorController.components["cursor-controller"].disable(); + }); + }; + enterScene = async (mediaStream, enterInVR) => { const playerCamera = document.querySelector("#player-camera"); playerCamera.removeAttribute("scene-preview-camera"); @@ -68,23 +75,30 @@ export default class SceneEntryManager { return; } + this.scene.classList.remove("hand-cursor"); this.scene.classList.add("no-cursor"); + const cursor = this.cursorController.components["cursor-controller"]; + cursor.enable(); + cursor.setCursorVisibility(true); + this.hubChannel.sendEntryEvent().then(() => { this.store.update({ activity: { lastEnteredAt: new Date().toISOString() } }); }); }; - enterSceneWhenLoaded = (mediaStream, enterInVR) => { - const enterSceneImmediately = () => this.enterScene(mediaStream, enterInVR); - + whenSceneLoaded = callback => { if (this.scene.hasLoaded) { - enterSceneImmediately(); + callback(); } else { - this.scene.addEventListener("loaded", enterSceneImmediately); + this.scene.addEventListener("loaded", callback); } }; + enterSceneWhenLoaded = (mediaStream, enterInVR) => { + this.whenSceneLoaded(() => this.enterScene(mediaStream, enterInVR)); + }; + exitScene = () => { if (NAF.connection.adapter && NAF.connection.adapter.localMediaStream) { NAF.connection.adapter.localMediaStream.getTracks().forEach(t => t.stop());