diff --git a/src/assets/stylesheets/index.scss b/src/assets/stylesheets/index.scss index 014ca0a7a7f7f32e20db86d9a4725da7beb5d011..84e67fb075f83ea1d303cfbce7e4ba7b0d4ea8b9 100644 --- a/src/assets/stylesheets/index.scss +++ b/src/assets/stylesheets/index.scss @@ -2,6 +2,8 @@ @import 'hub-create'; @import 'info-dialog'; +$title-width: 350px; + * { box-sizing: border-box; } @@ -53,20 +55,24 @@ body { .header-content { padding: 1.5em 2.5em 1.5em 2.5em; - background-color: rgba(0, 0, 0, 0.85); + background-color: $darkest-transparent; min-height: 90px; height: 90px; display: flex; - border-bottom: 2px solid #242424; + border-bottom: 1px solid $darkest-grey; &__title { - flex: 10; display: flex; + width: $title-width; @media (max-width: 768px) { justify-content: center; } + @media (max-width: 1024px) { + flex: 1 1 350px; + } + &__name { width: 200px; } @@ -77,12 +83,32 @@ body { } } + &__entry-code { + @media (max-width: 1024px) { + display: none; + } + + flex: 10; + text-align: center; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + &__link { + color: white; + text-decoration-color: $light-grey; + } + } + &__experiment { text-align: right; flex: 1 1 350px; + width: 350px; color: $grey-text; font-size: 1.0em; font-weight: lighter; + white-space: nowrap; @media (max-width: 768px) { display: none; @@ -117,6 +143,26 @@ body { } } +.header-subtitle { + @media (min-width: 1024px) { + display: none; + } + + padding: 8px; + background-color: $darkest-transparent; + text-align: center; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + border-bottom: 2px solid $darkest-grey; + + &__link { + color: white; + text-decoration-color: $light-grey; + } +} + .hero-content { flex: 10; min-height: 740px; @@ -208,7 +254,7 @@ body { .footer-content { padding: 1em 2.25em 1em 2.25em; - background-color: rgba(0, 0, 0, 0.85); + background-color: $darkest-transparent; min-height: 80px; display: flex; border-top: 2px solid #242424; diff --git a/src/assets/stylesheets/link.scss b/src/assets/stylesheets/link.scss index 63da92c9709fa72367461af1006d85de03e4873a..b94325c5039062d7848a4d254df9d27b88a7ceb4 100644 --- a/src/assets/stylesheets/link.scss +++ b/src/assets/stylesheets/link.scss @@ -108,6 +108,7 @@ a { display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr 1fr 1fr; + text-align: center; } :local(.keypad-button) { @@ -116,8 +117,8 @@ a { font-family: sans-serif; border: 4px $light-grey solid; border-radius: 128px; - min-width: 88px; - min-height: 88px; + min-width: 80px; + min-height: 80px; cursor: pointer; line-height: 68px; margin: 8px; @@ -170,7 +171,7 @@ a { margin: 0; font-size: 64pt; border: 0; - width: 225px; + width: 295px; letter-spacing: 0.08em; text-align: center; } diff --git a/src/assets/stylesheets/shared.scss b/src/assets/stylesheets/shared.scss index 18944a4080dd096f4f6d917e61d3834cb05ab04a..e54447f58f853687b1f3f3d2be6d136d95df01c1 100644 --- a/src/assets/stylesheets/shared.scss +++ b/src/assets/stylesheets/shared.scss @@ -1,11 +1,12 @@ $dark-transparent: rgba(0, 0, 0, 0.4); $darker-transparent: rgba(0, 0, 0, 0.6); -$darkest-transparent: rgba(0, 0, 0, 0.95); +$darkest-transparent: rgba(0, 0, 0, 0.9); $grey-text: rgba(192, 192, 192, 1.0); $light-text: rgba(240, 240, 240, 1.0); $light-grey: lightgrey; $dark-grey: rgba(128, 128, 128, 1.0); $darker-grey: rgba(64, 64, 64, 1.0); +$darkest-grey: rgba(32, 32, 32, 1.0); %default-font { font-family: 'Zilla Slab', sans-serif; diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index c6ef31c5c944ea85568fb6fe1cded6c47e0341b7..0201cf0fca4a81f60217ab97679f6f2e7316fc43 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -13,6 +13,7 @@ "entry.device-medium": "Device", "entry.device-subtitle-desktop": "Standalone Headset or Phone", "entry.device-subtitle-mobile": "Mobile Headset or PC", + "entry.device-subtitle-vr": "Phone or PC", "entry.cardboard": "Enter on Google Cardboard", "entry.daydream-prefix": "Enter on ", "entry.daydream-medium": "Daydream", @@ -59,10 +60,11 @@ "home.made_with_love": "made with 🦆 by ", "home.environment_author_by": " by ", "home.dialog.close": "CLOSE", + "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.in_your_browser": "In your device's browser, go to:", - "link.enter_code": "Then, enter this code:", + "link.enter_code": "Then, enter this link 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?", diff --git a/src/hub.js b/src/hub.js index f85a4980f3dd50dbda64fc368627d77fe42c0d1a..8bd8e2a29f9dbe957b0c20cd2798c160fbe7ede0 100644 --- a/src/hub.js +++ b/src/hub.js @@ -69,6 +69,7 @@ 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 "./systems/personal-space-bubble"; import "./systems/app-mode"; @@ -133,6 +134,8 @@ if (!isBotMode) { registerTelemetry(); } +disableiOSZoom(); + AFRAME.registerInputBehaviour("trackpad_dpad4", trackpad_dpad4); AFRAME.registerInputBehaviour("joystick_dpad4", joystick_dpad4); AFRAME.registerInputBehaviour("msft_mr_axis_with_deadzone", msft_mr_axis_with_deadzone); diff --git a/src/link.html b/src/link.html index 8f44654c0562b9ce2afd5b3bb95f482fffc213ab..954061491faa3b257adedf87df06a26cfc58f79b 100644 --- a/src/link.html +++ b/src/link.html @@ -7,6 +7,7 @@ <link rel="shortcut icon" type="image/png" href="/favicon.ico"/> <title>Enter Code | Hubs by Mozilla</title> <link href="https://fonts.googleapis.com/css?family=Zilla+Slab:300,300i,400,400i,700" rel="stylesheet"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> </head> <body> diff --git a/src/react-components/entry-buttons.js b/src/react-components/entry-buttons.js index 90dd40cfd23724418228cc7bb3b46fe45dc8e66f..9e8302b3c7bbf9bd44edbaf6538100f11abf3fb8 100644 --- a/src/react-components/entry-buttons.js +++ b/src/react-components/entry-buttons.js @@ -38,7 +38,8 @@ EntryButton.propTypes = { iconSrc: PropTypes.string, prefixMessageId: PropTypes.string, mediumMessageId: PropTypes.string, - subtitle: PropTypes.string + subtitle: PropTypes.string, + isInHMD: PropTypes.bool }; export const TwoDEntryButton = props => { @@ -91,9 +92,12 @@ export const DeviceEntryButton = props => { ...props, iconSrc: DeviceEntryImg, prefixMessageId: mobiledetect.mobile() ? "entry.device-prefix-mobile" : "entry.device-prefix-desktop", - mediumMessageId: "entry.device-medium", - subtitle: mobiledetect.mobile() ? "entry.device-subtitle-mobile" : "entry.device-subtitle-desktop" + mediumMessageId: "entry.device-medium" }; + entryButtonProps.subtitle = entryButtonProps.isInHMD + ? "entry.device-subtitle-vr" + : mobiledetect.mobile() ? "entry.device-subtitle-mobile" : "entry.device-subtitle-desktop"; + return <EntryButton {...entryButtonProps} />; }; diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js index df85b425646d1b9a0eae4f216ba3c00cae6dfdc8..7c9a502446fd8587bf063d68977cfc644cec91e4 100644 --- a/src/react-components/home-root.js +++ b/src/react-components/home-root.js @@ -99,6 +99,11 @@ class HomeRoot extends Component { <img className="header-content__title__name" src="../assets/images/logo.svg" /> <div className="header-content__title__preview">preview</div> </div> + <div className="header-content__entry-code"> + <a className="header-content__entry-code__link" href="/link" rel="nofollow"> + <FormattedMessage id="home.have_entry_code" /> + </a> + </div> <div className="header-content__experiment"> <div className="header-content__experiment__container"> <img src="../assets/images/webvr_cube.svg" className="header-content__experiment__icon" /> @@ -132,6 +137,13 @@ class HomeRoot extends Component { </div> </div> </div> + <div className="header-subtitle"> + <div> + <a className="header-subtitle__link" href="/link" rel="nofollow"> + <FormattedMessage id="home.have_entry_code" /> + </a> + </div> + </div> <div className="hero-content"> <div className="hero-content__attribution"> Medieval Fantasy Book by{" "} diff --git a/src/react-components/link-root.js b/src/react-components/link-root.js index 1ab8aa47829ad5181f3796b72502d3f587241b55..66ab987b6493a0cb99ab0742eb966d074ffe5ad5 100644 --- a/src/react-components/link-root.js +++ b/src/react-components/link-root.js @@ -6,10 +6,12 @@ import en from "react-intl/locale-data/en"; import { lang, messages } from "../utils/i18n"; import classNames from "classnames"; import styles from "../assets/stylesheets/link.scss"; +import { disableiOSZoom } from "../utils/disable-ios-zoom"; const MAX_DIGITS = 4; addLocaleData([...en]); +disableiOSZoom(); class LinkRoot extends Component { static propTypes = { @@ -78,12 +80,17 @@ class LinkRoot extends Component { } }) .catch(e => { - console.error(e); this.setState({ failedAtLeastOnce: true, enteredDigits: "" }); + + if (!(e instanceof Error && (e.message === "in_use" || e.message === "failed"))) { + throw e; + } }); }; render() { + // Note we use type "tel" for the input due to https://bugzilla.mozilla.org/show_bug.cgi?id=1005603 + return ( <IntlProvider locale={lang} messages={messages}> <div className={styles.link}> @@ -106,7 +113,8 @@ class LinkRoot extends Component { <div className={styles.enteredDigits}> <input className={styles.digitInput} - type="number" + type="tel" + pattern="[0-9]*" value={this.state.enteredDigits} onChange={ev => { this.setState({ enteredDigits: ev.target.value }); diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 803c28acded3354c4065be0fa380424e0b7c2237..cb69c63436f8d9d72479fc10e9ce541d5fde2ceb 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -612,7 +612,9 @@ class UIRoot extends Component { this.state.entryStep === ENTRY_STEPS.start ? ( <div className="entry-panel"> <div className="entry-panel__button-container"> - <TwoDEntryButton onClick={this.enter2D} /> + {this.props.availableVREntryTypes.screen !== VR_DEVICE_AVAILABILITY.no && ( + <TwoDEntryButton onClick={this.enter2D} /> + )} {this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && ( <GenericEntryButton onClick={this.enterVR} /> )} @@ -626,7 +628,7 @@ class UIRoot extends Component { } /> )} - <DeviceEntryButton onClick={this.attemptLink} /> + <DeviceEntryButton onClick={this.attemptLink} isInHMD={this.props.availableVREntryTypes.isInHMD} /> {this.props.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && ( <div className="entry-panel__secondary" onClick={this.enterVR}> <FormattedMessage id="entry.cardboard" /> diff --git a/src/utils/disable-ios-zoom.js b/src/utils/disable-ios-zoom.js new file mode 100644 index 0000000000000000000000000000000000000000..c2d17104a6ae4b9b8c99221fe5bec8a8f79cc909 --- /dev/null +++ b/src/utils/disable-ios-zoom.js @@ -0,0 +1,24 @@ +import MobileDetect from "mobile-detect"; +const mobiledetect = new MobileDetect(navigator.userAgent); + +export function disableiOSZoom() { + if (!mobiledetect.is("iPhone") && !mobiledetect.is("iPad")) return; + + let lastTouchAtMs = 0; + + document.addEventListener("touchmove", ev => { + if (ev.scale === 1) return; + + ev.preventDefault(); + }); + + document.addEventListener("touchend", ev => { + const now = new Date().getTime(); + const isDoubleTouch = now - lastTouchAtMs <= 300; + lastTouchAtMs = now; + + if (isDoubleTouch) { + ev.preventDefault(); + } + }); +} diff --git a/src/utils/vr-caps-detect.js b/src/utils/vr-caps-detect.js index 79c425da26428b227daa71890b443f913640abc2..3ab8f4370638e49ccea8389f99e14bdac2c1db43 100644 --- a/src/utils/vr-caps-detect.js +++ b/src/utils/vr-caps-detect.js @@ -35,15 +35,19 @@ const GENERIC_ENTRY_TYPE_DEVICE_BLACKLIST = [/cardboard/i]; // Once in a compatible browser, we should assume it will work (if it doesn't, it's because they don't have the headset, // haven't installed the software, our guess about their phone was wrong, etc.) // -// At the time of this writing there are three VR "entry types" that will be validated by this method: +// At the time of this writing there are the following VR "entry types" that will be validated by this method: // +// - screen: Enter "on-screen" in 2D // - generic: Generic WebVR (platform/OS agnostic indicator if a general 'Enter VR' option should be presented.) // - daydream: Google Daydream // - gearvr: Oculus GearVR +// - cardboard: Google Cardboard // +// This function also detects if the user is already in a headset, and returns the isInHMD key to be `true` if so. export async function getAvailableVREntryTypes() { const isSamsungBrowser = browser.name === "chrome" && navigator.userAgent.match(/SamsungBrowser/); const isOculusBrowser = navigator.userAgent.match(/Oculus/); + const isInHMD = isOculusBrowser; // This needs to be kept up-to-date with the latest browsers that can support VR and Hubs. // Checking for navigator.getVRDisplays always passes b/c of polyfill. @@ -51,6 +55,7 @@ export async function getAvailableVREntryTypes() { const isDaydreamCapableBrowser = !!(isWebVRCapableBrowser && browser.name === "chrome" && !isSamsungBrowser); + const screen = isInHMD ? VR_DEVICE_AVAILABILITY.no : VR_DEVICE_AVAILABILITY.yes; let generic = mobiledetect.mobile() ? VR_DEVICE_AVAILABILITY.no : VR_DEVICE_AVAILABILITY.maybe; let cardboard = VR_DEVICE_AVAILABILITY.no; @@ -65,7 +70,8 @@ export async function getAvailableVREntryTypes() { // For daydream detection, we first check if they are on an Android compatible device, and assume they // may support daydream *unless* this browser has WebVR capabilities, in which case we can do better. - let daydream = isMaybeDaydreamCompatibleDevice() ? VR_DEVICE_AVAILABILITY.maybe : VR_DEVICE_AVAILABILITY.no; + let daydream = + isMaybeDaydreamCompatibleDevice() && !isInHMD ? VR_DEVICE_AVAILABILITY.maybe : VR_DEVICE_AVAILABILITY.no; if (isWebVRCapableBrowser) { const displays = await navigator.getVRDisplays(); @@ -94,5 +100,5 @@ export async function getAvailableVREntryTypes() { } } - return { generic, gearvr, daydream, cardboard }; + return { screen, generic, gearvr, daydream, cardboard, isInHMD }; }