From bd8dee787750158b83f2c0c85257510ff62abddd Mon Sep 17 00:00:00 2001 From: Greg Fodor <gfodor@gmail.com> Date: Mon, 1 Oct 2018 15:42:31 +0000 Subject: [PATCH] WIP --- src/assets/stylesheets/link-dialog.scss | 31 ++++++++ src/assets/translations.data.json | 4 +- src/hub.js | 7 +- src/react-components/invite-dialog.js | 97 +++++++++---------------- src/react-components/link-dialog.js | 58 +++++++++++++++ src/react-components/ui-root.js | 55 +++++++------- src/utils/link-channel.js | 5 +- 7 files changed, 164 insertions(+), 93 deletions(-) create mode 100644 src/assets/stylesheets/link-dialog.scss create mode 100644 src/react-components/link-dialog.js diff --git a/src/assets/stylesheets/link-dialog.scss b/src/assets/stylesheets/link-dialog.scss new file mode 100644 index 000000000..568c3363e --- /dev/null +++ b/src/assets/stylesheets/link-dialog.scss @@ -0,0 +1,31 @@ +@import 'shared.scss'; + +: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/translations.data.json b/src/assets/translations.data.json index a94df0907..e757be527 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -81,7 +81,9 @@ "help.report_issue": "Report an Issue", "scene.logo_tagline": "A new way to get together", "scene.create_button": "create a room with this scene", - "invite.in_your_browser": "In your headset's browser, go to:", + "link.in_your_browser": "In your headset's browser, go to:", + "link.enter_code": "Then, enter this one-time link code:", + "link.do_not_close": "Keep this open to use this code.", "invite.entry_code": "Entry Code:", "invite.and_enter_code": "and enter code:", "invite.join_at": "Join room at ", diff --git a/src/hub.js b/src/hub.js index 3131ee8ee..f05c4d995 100644 --- a/src/hub.js +++ b/src/hub.js @@ -73,6 +73,7 @@ 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"; @@ -295,11 +296,13 @@ document.addEventListener("DOMContentLoaded", () => { const entryManager = new SceneEntryManager(hubChannel); entryManager.init(); + const linkChannel = new LinkChannel(store); + window.APP.scene = scene; registerNetworkSchemas(); mountUI({}); - remountUI({ hubChannel, enterScene: entryManager.enterScene, exitScene: entryManager.exitScene }); + remountUI({ hubChannel, linkChannel, enterScene: entryManager.enterScene, exitScene: entryManager.exitScene }); pollForSupportAvailability(isSupportAvailable => remountUI({ isSupportAvailable })); @@ -384,4 +387,6 @@ document.addEventListener("DOMContentLoaded", () => { if (!NAF.connection.adapter) return; NAF.connection.adapter.onData(data); }); + + linkChannel.setSocket(socket); }); diff --git a/src/react-components/invite-dialog.js b/src/react-components/invite-dialog.js index fce6863c8..440f60d20 100644 --- a/src/react-components/invite-dialog.js +++ b/src/react-components/invite-dialog.js @@ -14,7 +14,6 @@ function pad(num, size) { export default class InviteDialog extends Component { static propTypes = { entryCode: PropTypes.number, - dialogType: PropTypes.string, onClose: PropTypes.func }; @@ -32,70 +31,42 @@ export default class InviteDialog extends Component { 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> + return ( + <div className={styles.dialog}> + <div className={styles.attachPoint} /> + <div className={styles.close} onClick={() => this.props.onClose()}> + <span>×</span> </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 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> + ); } } diff --git a/src/react-components/link-dialog.js b/src/react-components/link-dialog.js new file mode 100644 index 000000000..456090494 --- /dev/null +++ b/src/react-components/link-dialog.js @@ -0,0 +1,58 @@ +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/ui-root.js b/src/react-components/ui-root.js index 3ac74cad2..27bec1b64 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -24,6 +24,7 @@ import SafariDialog from "./safari-dialog.js"; import WebVRRecommendDialog from "./webvr-recommend-dialog.js"; import InviteTeamDialog from "./invite-team-dialog.js"; import InviteDialog from "./invite-dialog.js"; +import LinkDialog from "./link-dialog.js"; import CreateObjectDialog from "./create-object-dialog.js"; import TwoDHUD from "./2d-hud"; import { faUsers } from "@fortawesome/free-solid-svg-icons/faUsers"; @@ -73,6 +74,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, @@ -89,7 +91,10 @@ class UIRoot extends Component { entryStep: ENTRY_STEPS.start, enterInVR: false, dialog: null, - inviteDialogType: null, + showInviteDialog: false, + showLinkDialog: false, + linkCode: null, + linkCodeCancel: null, shareScreen: false, requestedScreen: false, @@ -172,8 +177,7 @@ class UIRoot extends Component { handleStartEntry = () => { if (!this.props.forcedVREntryType) { this.setState({ entryStep: ENTRY_STEPS.device }); - } - if (this.props.forcedVREntryType.startsWith("daydream")) { + } else if (this.props.forcedVREntryType.startsWith("daydream")) { this.enterDaydream(); } else if (this.props.forcedVREntryType.startsWith("vr")) { this.enterVR(); @@ -501,24 +505,19 @@ class UIRoot extends Component { this.setState({ entryStep: ENTRY_STEPS.finished }); }; - showInviteDialog = async forHeadset => { - this.setState({ inviteDialogType: forHeadset ? "headset" : "invite" }); + attemptLink = async () => { + this.setState({ showLinkDialog: true }); + const { code, cancel, onFinished } = await this.props.linkChannel.generateCode(); + this.setState({ linkCode: code, linkCodeCancel: cancel }); + onFinished.then(() => this.setState({ showLinkDialog: false, linkCode: null, linkCodeCancel: null })); }; - toggleInviteDialog = async () => { - if (this.state.inviteDialogType) { - this.setState({ inviteDialogType: null }); - } else { - this.showInviteDialog(false); - } + showInviteDialog = () => { + this.setState({ showInviteDialog: true }); }; - closeDialog = async () => { - if (this.state.linkCodeCancel) { - this.state.linkCodeCancel(); - } - - this.setState({ dialog: null, linkCode: null, linkCodeCancel: null }); + toggleInviteDialog = async () => { + this.setState({ showInviteDialog: !this.state.showInviteDialog }); }; createObject = media => { @@ -683,10 +682,7 @@ class UIRoot extends Component { {this.props.availableVREntryTypes.daydream === VR_DEVICE_AVAILABILITY.yes && ( <DaydreamEntryButton onClick={this.enterDaydream} subtitle={null} /> )} - <DeviceEntryButton - onClick={() => this.showInviteDialog(true)} - isInHMD={this.props.availableVREntryTypes.isInHMD} - /> + <DeviceEntryButton onClick={() => this.attemptLink()} isInHMD={this.props.availableVREntryTypes.isInHMD} /> {this.props.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && ( <div className={entryStyles.secondary} onClick={this.enterVR}> <FormattedMessage id="entry.cardboard" /> @@ -937,7 +933,7 @@ class UIRoot extends Component { className={classNames({ [styles.inviteContainer]: true, [styles.inviteContainerBelowHud]: entryFinished, - [styles.inviteContainerInverted]: this.state.inviteDialogType + [styles.inviteContainerInverted]: this.state.showInviteDialog })} > {!this.props.availableVREntryTypes.isInHMD && @@ -952,15 +948,24 @@ class UIRoot extends Component { <FormattedMessage id="entry.return-to-vr" /> </button> )} - {this.state.inviteDialogType && ( + {this.state.showInviteDialog && ( <InviteDialog entryCode={this.props.hubEntryCode} - dialogType={this.state.inviteDialogType} - onClose={() => this.setState({ inviteDialogType: null })} + onClose={() => this.setState({ showInviteDialog: false })} /> )} </div> + {this.state.showLinkDialog && ( + <LinkDialog + linkCode={this.state.linkCode} + onClose={() => { + this.state.linkCodeCancel(); + this.setState({ showLinkDialog: false, linkCode: null, linkCodeCancel: null }); + }} + /> + )} + <button onClick={() => this.showHelpDialog()} className={styles.helpIcon}> <i> <FontAwesomeIcon icon={faQuestion} /> diff --git a/src/utils/link-channel.js b/src/utils/link-channel.js index b4bac1695..172a327ad 100644 --- a/src/utils/link-channel.js +++ b/src/utils/link-channel.js @@ -24,9 +24,8 @@ export default class LinkChannel { return new Promise(resolve => { const onFinished = new Promise(finished => { const step = () => { - const code = Math.floor(Math.random() * 9999) - .toString() - .padStart(4, "0"); + const getLetter = () => "ABCDEFGHI"[Math.floor(Math.random() * 9)]; + const code = `${getLetter()}${getLetter()}${getLetter()}${getLetter()}`; // Only respond to one link_request in this channel. let readyToSend = false; -- GitLab