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