From 65bfe084b8cae76bbbdbc199313775badd610b0c Mon Sep 17 00:00:00 2001
From: Greg Fodor <gfodor@gmail.com>
Date: Mon, 1 Oct 2018 22:51:45 +0000
Subject: [PATCH] Alpha link code entry

---
 src/assets/stylesheets/link.scss  |  50 +++++++--
 src/assets/translations.data.json |   6 +-
 src/link.js                       |  13 ++-
 src/react-components/link-root.js | 179 +++++++++++++++++++++---------
 4 files changed, 184 insertions(+), 64 deletions(-)

diff --git a/src/assets/stylesheets/link.scss b/src/assets/stylesheets/link.scss
index accfb2be2..f06d0c81e 100644
--- a/src/assets/stylesheets/link.scss
+++ b/src/assets/stylesheets/link.scss
@@ -74,6 +74,7 @@ a {
 :local(.entry-footer-image) {
   width: 200px;
   margin: 12px;
+  margin-top: 24px;
 }
 
 
@@ -128,26 +129,30 @@ a {
   background-color: $darker-grey;
 }
 
-:local(.keypad-zero-button) {
-  grid-column: 2;
-}
-
 :local(.keypad-button):disabled {
   color: $light-grey;
   border: 6px $dark-grey solid;
 }
 
-:local(.keypad-backspace) , :local(.keypad-backspace):disabled , :local(.keypad-backspace):active {
+:local(.keypad-backspace) , :local(.keypad-backspace):disabled , :local(.keypad-backspace):active, :local(.keypad-toggle-mode), :local(.keypad-toggle-mode):disabled, :local(.keypad-toggle-mode):active {
   border: none;
 }
 
+:local(.keypad-backspace) {
+  grid-column: 3;
+}
+
 :local(.keypad-backspace):active {
   background-color: transparent;
   color: $light-grey;
 }
 
-:local(.entered-digits) {
-  font-face: monospace;
+:local(.keypad-toggle-mode) {
+  font-size: 1.0em;
+}
+
+:local(.entered) {
+  font-family: monospace;
   height: 100px;
   width: 300px;
   text-align: center;
@@ -157,11 +162,11 @@ a {
   justify-content: center;
 }
 
-:local(.digit) {
+:local(.char) {
   margin: 8px;
 }
 
-:local(.digit-input) {
+:local(.char-input) {
   outline-style: none;
   appearance:textfield;
   -moz-appearance:textfield;
@@ -176,7 +181,32 @@ a {
   text-align: center;
 }
 
-:local(.digit-input::placeholder) {
+:local(.char-input::placeholder) {
   letter-spacing: 0;
 }
 
+:local(.headset-icon) {
+  width: 64px;
+  height: 64px;
+  background-color: white;
+  border-radius: 32px;
+  margin-bottom: 8px;
+  padding-right: 2px;
+  cursor: pointer;
+}
+
+:local(.link-headset-footer-link) {
+  display: flex;
+  align-items: center;
+
+  img {
+    width: 32px;
+    height: 32px;
+    background-color: white;
+    border-radius: 32px;
+    margin-bottom: 8px;
+    margin-right: 16px;
+    padding-right: 2px;
+    cursor: pointer;
+  }
+}
diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json
index 9ae5532c3..0a21762f7 100644
--- a/src/assets/translations.data.json
+++ b/src/assets/translations.data.json
@@ -74,9 +74,9 @@
     "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.link_page_header": "Enter your code:",
-    "link.dont_have_a_code": "Don't have a code?",
-    "link.create_a_room": "Create a Room",
+    "link.link_page_header_entry": "Enter your code:",
+    "link.link_page_header_headset": "Enter headset link code:",
+    "link.linking_a_headset": "Linking a Headset?",
     "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",
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-root.js b/src/react-components/link-root.js
index f9aab014b..fb1b40c3d 100644
--- a/src/react-components/link-root.js
+++ b/src/react-components/link-root.js
@@ -7,19 +7,24 @@ import { lang, messages } from "../utils/i18n";
 import classNames from "classnames";
 import styles from "../assets/stylesheets/link.scss";
 import { disableiOSZoom } from "../utils/disable-ios-zoom";
+import LinkDialogHeader from "../assets/images/link_dialog_header.svg";
 
 const MAX_DIGITS = 6;
+const MAX_LETTERS = 4;
 
 addLocaleData([...en]);
 disableiOSZoom();
 
 class LinkRoot extends Component {
   static propTypes = {
-    intl: PropTypes.object
+    intl: PropTypes.object,
+    store: PropTypes.object,
+    linkChannel: PropTypes.object
   };
 
   state = {
-    enteredDigits: "",
+    entered: "",
+    isAlphaMode: false,
     failedAtLeastOnce: false
   };
 
@@ -33,44 +38,97 @@ class LinkRoot extends Component {
 
   handleKeyDown = e => {
     // Number keys 0-9
-    if (e.keyCode < 48 || e.keyCode > 57) {
+    if ((e.keyCode < 48 || e.keyCode > 57) && !this.state.isAlphaMode) {
+      return;
+    }
+
+    // Alpha keys A-I
+    if ((e.keyCode < 65 || e.keyCode > 73) && this.state.isAlphaMode) {
       return;
     }
 
     e.preventDefault();
     e.stopPropagation();
 
-    this.addDigit(e.keyCode - 48);
+    if (this.state.isAlphaMode) {
+      this.addToEntry("IHGFEDCBA"[73 - e.keyCode]);
+    } else {
+      this.addToEntry(e.keyCode - 48);
+    }
+  };
+
+  maxAllowedChars = () => {
+    return this.state.isAlphaMode ? MAX_LETTERS : MAX_DIGITS;
   };
 
-  addDigit = digit => {
-    if (this.state.enteredDigits.length >= MAX_DIGITS) return;
-    const newDigits = `${this.state.enteredDigits}${digit}`;
+  addToEntry = ch => {
+    if (this.state.entered.length >= this.maxAllowedChars()) return;
+    const newChars = `${this.state.entered}${ch}`;
 
-    if (newDigits.length === MAX_DIGITS) {
-      this.attemptLink(newDigits);
+    if (newChars.length === this.maxAllowedChars()) {
+      this.attemptLookup(newChars);
     }
 
-    this.setState({ enteredDigits: newDigits });
+    this.setState({ entered: newChars });
   };
 
-  removeDigit = () => {
-    const enteredDigits = this.state.enteredDigits;
-    if (enteredDigits.length === 0) return;
-    this.setState({ enteredDigits: enteredDigits.substring(0, enteredDigits.length - 1) });
+  removeChar = () => {
+    const entered = this.state.entered;
+    if (entered.length === 0) return;
+    this.setState({ entered: entered.substring(0, entered.length - 1) });
   };
 
   attemptLink = async 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;
+        }
+      });
+  };
+
+  attemptEntry = async code => {
     const url = "https://hub.link/" + code;
     const res = await fetch(url);
 
     if (res.status >= 400) {
-      this.setState({ failedAtLeastOnce: true, enteredDigits: "" });
+      this.setState({ failedAtLeastOnce: true, entered: "" });
     } else {
       document.location = url;
     }
   };
 
+  attemptLookup = async code => {
+    if (this.state.isAlphaMode) {
+      // Headset link code
+      this.attemptLink(code);
+    } else {
+      // Room entry code
+      this.attemptEntry(code);
+    }
+  };
+
+  toggleMode = () => {
+    this.setState({ isAlphaMode: !this.state.isAlphaMode, entered: "" });
+  };
+
   render() {
     // Note we use type "tel" for the input due to https://bugzilla.mozilla.org/show_bug.cgi?id=1005603
 
@@ -78,7 +136,7 @@ class LinkRoot extends Component {
       <IntlProvider locale={lang} messages={messages}>
         <div className={styles.link}>
           <div className={styles.linkContents}>
-            {this.state.enteredDigits.length === MAX_DIGITS && (
+            {this.state.entered.length === this.maxAllowedChars() && (
               <div className={classNames("loading-panel", styles.codeLoadingPanel)}>
                 <div className="loader-wrap">
                   <div className="loader">
@@ -90,71 +148,92 @@ class LinkRoot extends Component {
 
             <div className={styles.enteredContents}>
               <div className={styles.header}>
-                <FormattedMessage id={this.state.failedAtLeastOnce ? "link.try_again" : "link.link_page_header"} />
+                <FormattedMessage
+                  id={
+                    this.state.failedAtLeastOnce
+                      ? "link.try_again"
+                      : "link.link_page_header_" + (!this.state.isAlphaMode ? "entry" : "headset")
+                  }
+                />
               </div>
 
-              <div className={styles.enteredDigits}>
+              <div className={styles.entered}>
                 <input
-                  className={styles.digitInput}
-                  type="tel"
-                  pattern="[0-9]*"
-                  value={this.state.enteredDigits}
+                  className={styles.charInput}
+                  type={this.state.isAlphaMode ? "text" : "tel"}
+                  pattern="[0-9A-I]*"
+                  value={this.state.entered}
                   onChange={ev => {
-                    this.setState({ enteredDigits: ev.target.value });
+                    this.setState({ entered: ev.target.value });
                   }}
-                  placeholder="- - - - - -"
+                  placeholder={this.state.isAlphaMode ? "- - - -" : "- - - - - -"}
                 />
               </div>
 
               <div className={styles.enteredFooter}>
-                <span>
-                  <FormattedMessage id="link.dont_have_a_code" />
-                </span>{" "}
-                <span>
-                  <a href="/">
-                    <FormattedMessage id="link.create_a_room" />
-                  </a>
-                </span>
+                {!this.state.isAlphaMode && (
+                  <img onClick={() => this.toggleMode()} src={LinkDialogHeader} className={styles.headsetIcon} />
+                )}
+                {!this.state.isAlphaMode && (
+                  <span>
+                    <a href="#" onClick={() => this.toggleMode()}>
+                      <FormattedMessage id="link.linking_a_headset" />
+                    </a>
+                  </span>
+                )}
                 <img className={styles.entryFooterImage} src="../assets/images/logo.svg" />
               </div>
             </div>
 
             <div className={styles.keypad}>
-              {[1, 2, 3, 4, 5, 6, 7, 8, 9].map((d, i) => (
+              {(this.state.isAlphaMode
+                ? ["A", "B", "C", "D", "E", "F", "G", "H", "I"]
+                : [1, 2, 3, 4, 5, 6, 7, 8, 9]
+              ).map((d, i) => (
                 <button
-                  disabled={this.state.enteredDigits.length === MAX_DIGITS}
-                  key={`digit_${i}`}
+                  disabled={this.state.entered.length === this.maxAllowedChars()}
+                  key={`char_${i}`}
                   className={styles.keypadButton}
-                  onClick={() => this.addDigit(d)}
+                  onClick={() => this.addToEntry(d)}
                 >
                   {d}
                 </button>
               ))}
               <button
-                disabled={this.state.enteredDigits.length === MAX_DIGITS}
-                className={classNames(styles.keypadButton, styles.keypadZeroButton)}
-                onClick={() => this.addDigit(0)}
+                className={classNames(styles.keypadButton, styles.keypadToggleMode)}
+                onClick={() => this.toggleMode()}
               >
-                0
+                {this.state.isAlphaMode ? "123" : "ABC"}
               </button>
+              {!this.state.isAlphaMode && (
+                <button
+                  disabled={this.state.entered.length === this.maxAllowedChars()}
+                  className={classNames(styles.keypadButton, styles.keypadZeroButton)}
+                  onClick={() => this.addToEntry(0)}
+                >
+                  0
+                </button>
+              )}
               <button
-                disabled={this.state.enteredDigits.length === 0 || this.state.enteredDigits.length === MAX_DIGITS}
+                disabled={this.state.entered.length === 0 || this.state.entered.length === this.maxAllowedChars()}
                 className={classNames(styles.keypadButton, styles.keypadBackspace)}
-                onClick={() => this.removeDigit()}
+                onClick={() => this.removeChar()}
               >
                 ⌫
               </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>
+              {!this.state.isAlphaMode && (
+                <div className={styles.linkHeadsetFooterLink}>
+                  <img onClick={() => this.toggleMode()} src={LinkDialogHeader} className={styles.headsetIcon} />
+                  <span>
+                    <a href="#" onClick={() => this.toggleMode()}>
+                      <FormattedMessage id="link.linking_a_headset" />
+                    </a>
+                  </span>
+                </div>
+              )}
               <img className={styles.footerImage} src="../assets/images/logo.svg" alt="Logo" />
             </div>
           </div>
-- 
GitLab