From 029ec99a3f63649794efd1ece69f59e70f733a78 Mon Sep 17 00:00:00 2001
From: Greg Fodor <gfodor@gmail.com>
Date: Tue, 8 May 2018 16:35:08 -0700
Subject: [PATCH] Linking works, just styling left

---
 src/assets/images/device_entry.svg  |   2 +-
 src/assets/stylesheets/link.scss    |   4 +
 src/assets/translations.data.json   |   6 +-
 src/hub.js                          |   9 +--
 src/link.js                         |  13 +++-
 src/react-components/link-dialog.js |   2 +-
 src/react-components/link-root.js   | 115 +++++++++++++++++++++++++++-
 src/storage/store.js                |  15 ++++
 webpack.config.js                   |   3 +-
 9 files changed, 152 insertions(+), 17 deletions(-)

diff --git a/src/assets/images/device_entry.svg b/src/assets/images/device_entry.svg
index d8c507234..99612a01c 100755
--- a/src/assets/images/device_entry.svg
+++ b/src/assets/images/device_entry.svg
@@ -10,7 +10,7 @@
 <path d="M 17.0381 1.11457C 4.27651 2.70692 0.865483 1.71672 0 14.3953C 0 28.1778 7.50085 23.8457 19.1594 21.989C 27.984 20.099 33.092 20.952 33.092 8.25672C 32.2435 -2.02998 29.087 -0.388842 17.0381 1.11457Z"/>
 </mask>
 <g mask="url(#path-7-inside-1)" transform="translate(55.092 41.8761) scale(-1 1)">
-<path d="M 0 14.3953L -3.99071 14.1229L -4 14.2589L -4 14.3953L 0 14.3953ZM 17.0381 1.11457L 16.5429 -2.85465L 16.5429 -2.85465L 17.0381 1.11457ZM 33.092 8.25672L 37.092 8.25672L 37.092 8.09203L 37.0785 7.92789L 33.092 8.25672ZM 19.1594 21.989L 19.7885 25.9393L 19.8933 25.9226L 19.9972 25.9003L 19.1594 21.989ZM 3.99071 14.6677C 4.42662 8.28204 5.48265 7.40822 6.11859 7.02601C 6.69787 6.67785 7.69892 6.34501 9.72536 6.03151C 11.7961 5.71115 14.1328 5.50811 17.5334 5.08379L 16.5429 -2.85465C 13.5627 -2.48279 10.6873 -2.21248 8.50229 -1.87445C 6.27291 -1.52955 3.9743 -1.01897 1.99744 0.16918C -2.47892 2.8596 -3.56114 7.82996 -3.99071 14.1229L 3.99071 14.6677ZM 17.5334 5.08379C 20.749 4.68257 22.9441 4.3304 24.9166 4.12903C 26.8922 3.92733 27.7656 3.9957 28.164 4.1098C 28.1994 4.11993 28.1688 4.01552 28.3076 4.30732C 28.5839 4.88851 28.9009 6.10498 29.1055 8.58555L 37.0785 7.92789C 36.8588 5.2651 36.4631 2.8292 35.5324 0.87196C 34.4642 -1.37468 32.7323 -2.9036 30.3664 -3.58109C 28.3635 -4.15462 26.1356 -4.037 24.1041 -3.8296C 22.0695 -3.62189 19.3518 -3.20513 16.5429 -2.85465L 17.5334 5.08379ZM 29.092 8.25672C 29.092 14.2794 27.8449 15.4212 27.1938 15.8616C 26.6416 16.2352 25.7755 16.5727 24.1872 16.9275C 23.4117 17.1007 22.5628 17.2587 21.563 17.444C 20.5854 17.6251 19.4873 17.8281 18.3217 18.0778L 19.9972 25.9003C 21.0377 25.6775 22.0296 25.4937 23.0204 25.3101C 23.989 25.1306 24.9869 24.946 25.9313 24.7351C 27.7826 24.3215 29.8482 23.7244 31.676 22.4881C 35.785 19.7087 37.092 14.9293 37.092 8.25672L 29.092 8.25672ZM 18.5304 18.0388C 15.5477 18.5138 12.619 19.1856 10.4897 19.5963C 8.12938 20.0516 6.6645 20.2052 5.69349 20.0864C 5.01742 20.0037 4.93914 19.8623 4.80067 19.6272C 4.46511 19.0575 4 17.6196 4 14.3953L -4 14.3953C -4 18.0622 -3.5275 21.2512 -2.09232 23.6876C -0.460049 26.4586 2.06554 27.7022 4.72201 28.0272C 7.08353 28.3161 9.67984 27.9 12.0049 27.4516C 14.561 26.9585 16.9419 26.3926 19.7885 25.9393L 18.5304 18.0388Z" fill="white"/>
+<path d="M 0 14.3953L -3.99071 14.1229L -4 14.2589L -4 14.3953L 0 14.3953ZM 17.0381 1.11457L 16.5429 -2.85465L 17.0381 1.11457ZM 33.092 8.25672L 37.092 8.25672L 37.092 8.09203L 37.0785 7.92789L 33.092 8.25672ZM 19.1594 21.989L 19.7885 25.9393L 19.8933 25.9226L 19.9972 25.9003L 19.1594 21.989ZM 3.99071 14.6677C 4.42662 8.28204 5.48266 7.40822 6.11859 7.02601C 6.69787 6.67785 7.69892 6.34501 9.72536 6.03151C 11.7961 5.71115 14.1328 5.50811 17.5334 5.08379L 16.5429 -2.85465C 13.5627 -2.48279 10.6873 -2.21248 8.50229 -1.87445C 6.27291 -1.52955 3.9743 -1.01897 1.99744 0.16918C -2.47892 2.8596 -3.56114 7.82996 -3.99071 14.1229L 3.99071 14.6677ZM 17.5334 5.08379C 20.749 4.68257 22.9441 4.3304 24.9166 4.12903C 26.8922 3.92733 27.7656 3.9957 28.164 4.1098C 28.1994 4.11993 28.1688 4.01552 28.3076 4.30732C 28.5839 4.88851 28.9009 6.10498 29.1055 8.58555L 37.0785 7.92789C 36.8588 5.2651 36.4631 2.8292 35.5324 0.87196C 34.4641 -1.37468 32.7323 -2.9036 30.3664 -3.58109C 28.3635 -4.15462 26.1356 -4.037 24.1041 -3.8296C 22.0695 -3.62189 19.3518 -3.20513 16.5429 -2.85465L 17.5334 5.08379ZM 29.092 8.25672C 29.092 14.2794 27.8449 15.4212 27.1938 15.8616C 26.6416 16.2352 25.7755 16.5727 24.1872 16.9275C 23.4117 17.1007 22.5628 17.2587 21.563 17.444C 20.5854 17.6251 19.4873 17.8281 18.3217 18.0778L 19.9972 25.9003C 21.0377 25.6775 22.0296 25.4937 23.0204 25.3101C 23.989 25.1306 24.9869 24.946 25.9313 24.7351C 27.7826 24.3215 29.8482 23.7244 31.676 22.4881C 35.785 19.7087 37.092 14.9293 37.092 8.25672L 29.092 8.25672ZM 18.5303 18.0388C 15.5477 18.5138 12.619 19.1856 10.4897 19.5963C 8.12938 20.0516 6.6645 20.2052 5.69349 20.0864C 5.01742 20.0037 4.93914 19.8623 4.80067 19.6272C 4.46511 19.0575 4 17.6196 4 14.3953L -4 14.3953C -4 18.0622 -3.5275 21.2512 -2.09232 23.6876C -0.460049 26.4585 2.06554 27.7022 4.72201 28.0272C 7.08353 28.3161 9.67984 27.9 12.0049 27.4516C 14.561 26.9585 16.9419 26.3926 19.7885 25.9393L 18.5303 18.0388Z" fill="white"/>
 </g>
 <path d="M 0 10.3292L 0.685179 4.47255C 4.92876 3.04264 9.69318 1.20359 12.5676 0.221048C 14.5183 -0.445764 15.9365 0.435362 16.0866 2.33438C 16.1919 3.66552 15.1923 4.93186 13.973 5.41957C 12.7537 5.90728 0 10.3292 0 10.3292Z" transform="matrix(0.993226 -0.1162 0.1162 0.993226 57.4919 45.7408)" fill="white"/>
 </g>
diff --git a/src/assets/stylesheets/link.scss b/src/assets/stylesheets/link.scss
index 452893f41..cd3f146c6 100644
--- a/src/assets/stylesheets/link.scss
+++ b/src/assets/stylesheets/link.scss
@@ -20,3 +20,7 @@ body {
   margin: 0;
   padding: 0;
 }
+
+:local(.code-loading-panel) {
+  background: rgba(0.4, 0.4, 0.4, 0.7);
+}
diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json
index 02a9a318a..a4f0087c8 100644
--- a/src/assets/translations.data.json
+++ b/src/assets/translations.data.json
@@ -64,6 +64,10 @@
     "link.in_your_browser": "In your device's browser, go to:",
     "link.link_domain": "hub.link",
     "link.enter_code": "Then, enter code:",
-    "link.do_not_close": "Keep this dialog open to use this 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."
   }
 }
diff --git a/src/hub.js b/src/hub.js
index 6f36492b1..a79251ac7 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -107,7 +107,6 @@ import registerNetworkSchemas from "./network-schemas";
 import { inGameActions, config as inputConfig } from "./input-mappings";
 import registerTelemetry from "./telemetry";
 
-import { generateDefaultProfile, generateRandomName } from "./utils/identity.js";
 import { getAvailableVREntryTypes } from "./utils/vr-caps-detect.js";
 import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js";
 
@@ -129,13 +128,7 @@ const concurrentLoadDetector = new ConcurrentLoadDetector();
 
 concurrentLoadDetector.start();
 
-// Always layer in any new default profile bits
-store.update({ activity: {}, settings: {}, profile: { ...generateDefaultProfile(), ...(store.state.profile || {}) } });
-
-// Regenerate name to encourage users to change it.
-if (!store.state.activity.hasChangedName) {
-  store.update({ profile: { displayName: generateRandomName() } });
-}
+store.init();
 
 function mountUI(scene, props = {}) {
   const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi");
diff --git a/src/link.js b/src/link.js
index 908f7f64d..401fe54d9 100644
--- a/src/link.js
+++ b/src/link.js
@@ -3,7 +3,18 @@ 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();
 
-ReactDOM.render(<LinkRoot />, document.getElementById("link-root"));
+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"));
diff --git a/src/react-components/link-dialog.js b/src/react-components/link-dialog.js
index c6d8305f8..046571cb4 100644
--- a/src/react-components/link-dialog.js
+++ b/src/react-components/link-dialog.js
@@ -38,7 +38,7 @@ class LinkDialog extends Component {
         </div>
         <div className={styles.code}>
           {this.props.linkCode.split("").map((d, i) => (
-            <span className={styles.digit} key={i}>
+            <span className={styles.digit} key={`link_code_${i}`}>
               {d}
             </span>
           ))}
diff --git a/src/react-components/link-root.js b/src/react-components/link-root.js
index e5bc05ccc..099114a20 100644
--- a/src/react-components/link-root.js
+++ b/src/react-components/link-root.js
@@ -7,21 +7,130 @@ import { lang, messages } from "../utils/i18n";
 import classNames from "classnames";
 import styles from "../assets/stylesheets/link.scss";
 
+const MAX_DIGITS = 4;
+
 addLocaleData([...en]);
 
 class LinkRoot extends Component {
   static propTypes = {
-    intl: PropTypes.object
+    intl: PropTypes.object,
+    store: PropTypes.object,
+    linkChannel: PropTypes.object
+  };
+
+  state = {
+    enteredDigits: [],
+    failedAtLeastOnce: false
+  };
+
+  addDigit = digit => {
+    if (this.state.enteredDigits.length >= MAX_DIGITS) return;
+    const newDigits = [...this.state.enteredDigits, digit];
+
+    if (newDigits.length === MAX_DIGITS) {
+      this.attemptLink(newDigits.join(""));
+    }
+
+    this.setState({ enteredDigits: newDigits });
+  };
+
+  removeDigit = () => {
+    if (this.state.enteredDigits.length === 0) return;
+    this.setState({ enteredDigits: [...this.state.enteredDigits.slice(0, -1)] });
   };
 
-  state = {};
+  attemptLink = code => {
+    console.log("link " + 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 => {
+        console.error(e);
+        this.setState({ failedAtLeastOnce: true, enteredDigits: [] });
+      });
+  };
 
   componentDidMount() {}
 
   render() {
     return (
       <IntlProvider locale={lang} messages={messages}>
-        <div className="link">Hello</div>
+        <div className={styles.link}>
+          {this.state.enteredDigits.length === MAX_DIGITS && (
+            <div className={classNames("loading-panel", styles.codeLoadingPanel)}>
+              <div className="loader-wrap">
+                <div className="loader">
+                  <div className="loader-center" />
+                </div>
+              </div>
+            </div>
+          )}
+          <img className={styles.headerImage} src="../assets/images/logo.svg" />
+
+          <div className={styles.header}>
+            <FormattedMessage id={this.state.failedAtLeastOnce ? "link.try_again" : "link.link_page_header"} />
+          </div>
+
+          <div className={styles.enteredDigits}>
+            {this.state.enteredDigits.map((d, i) => (
+              <span className={styles.digit} key={i}>
+                {d}
+              </span>
+            ))}
+          </div>
+
+          <div className={styles.keypad}>
+            {[1, 2, 3, 4, 5, 6, 7, 8, 9].map((d, i) => (
+              <button
+                disabled={this.state.enteredDigits.length === MAX_DIGITS}
+                key={`digit_${i}`}
+                className={styles.keypadButton}
+                onClick={() => this.addDigit(d)}
+              >
+                {d}
+              </button>
+            ))}
+            <button
+              disabled={this.state.enteredDigits.length === MAX_DIGITS}
+              className={styles.keypadZeroButton}
+              onClick={() => this.addDigit(0)}
+            >
+              0
+            </button>
+            <button
+              disabled={this.state.enteredDigits.length === 0 || this.state.enteredDigits.length === MAX_DIGITS}
+              className={styles.keypadBackspace}
+              onClick={() => this.removeDigit()}
+            >
+              ⌫
+            </button>
+          </div>
+
+          <div className={styles.footer}>
+            <span>
+              <FormattedMessage id="link.dont_have_a_code" />
+            </span>{" "}
+            <span>
+              <a href="/">
+                <FormattedMessage id="link.create_a_room" />
+              </a>
+            </span>
+          </div>
+        </div>
       </IntlProvider>
     );
   }
diff --git a/src/storage/store.js b/src/storage/store.js
index ce44da771..fedea9274 100644
--- a/src/storage/store.js
+++ b/src/storage/store.js
@@ -5,6 +5,7 @@ const LOCAL_STORE_KEY = "___hubs_store";
 const STORE_STATE_CACHE_KEY = Symbol();
 const validator = new Validator();
 import { EventTarget } from "event-target-shim";
+import { generateDefaultProfile, generateRandomName } from "../utils/identity.js";
 
 // Durable (via local-storage) schema-enforced state that is meant to be consumed via forward data flow.
 // (Think flux but with way less incidental complexity, at least for now :))
@@ -60,6 +61,20 @@ export default class Store extends EventTarget {
     }
   }
 
+  // Initializes store with any default bits
+  init = () => {
+    this.update({
+      activity: {},
+      settings: {},
+      profile: { ...generateDefaultProfile(), ...(this.state.profile || {}) }
+    });
+
+    // Regenerate name to encourage users to change it.
+    if (!this.state.activity.hasChangedName) {
+      this.update({ profile: { displayName: generateRandomName() } });
+    }
+  };
+
   get state() {
     if (!this.hasOwnProperty(STORE_STATE_CACHE_KEY)) {
       this[STORE_STATE_CACHE_KEY] = JSON.parse(localStorage.getItem(LOCAL_STORE_KEY));
diff --git a/webpack.config.js b/webpack.config.js
index e7ca7d57a..17fbb4f8b 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -198,8 +198,7 @@ const config = {
     new HTMLWebpackPlugin({
       filename: "link.html",
       template: path.join(__dirname, "src", "link.html"),
-      chunks: ["link"],
-      inject: "head"
+      chunks: ["link"]
     }),
     new HTMLWebpackPlugin({
       filename: "avatar-selector.html",
-- 
GitLab