diff --git a/src/assets/images/device_entry.svg b/src/assets/images/device_entry.svg index d8c50723485e125d65e597a51435e64f08d71ef8..99612a01c5b3ffe0dbab51aa54daf9f9ad12398d 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 452893f41f54ffa545171507fe48f40e8f75d52f..cd3f146c614788b6a676eeeb91a500150698ffc9 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 02a9a318a4bcd66990205dd92b145813ed7ae4ad..a4f0087c809289b4a250fa85108cea5217e6ead1 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 6f36492b1ade030641938c9cee7fceaf2cdc4718..a79251ac753dc21a5d0ab9e9c4c63747ae700785 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 908f7f64d4fadd8f906b09a7874e8e882c93b81d..401fe54d9b8b9bd91df1c2140257710502a23add 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 c6d8305f8039c43679b9c4ae57113967e2c377bb..046571cb420f8ccfde7522b05f10b5524a95896c 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 e5bc05cccdca3daca0506a4885483f58b8a60d6a..099114a201d9823c1e7bdd728fa87c556735578a 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 ce44da77198fca2ba570068ffdfa544a5f16732a..fedea9274e67fc1e6289499a239597293f238034 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 e7ca7d57a0f71e1ee5f4a028324c40f35da56bbd..17fbb4f8b666a03a7d1577f07d011040b6125f40 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",