From ced6d963de1426befbe67906d77bda90c1de6e4b Mon Sep 17 00:00:00 2001 From: Brian Peiris <brianpeiris@gmail.com> Date: Mon, 26 Mar 2018 17:38:45 -0700 Subject: [PATCH] i18n loading message --- src/assets/stylesheets/avatar-selector.scss | 9 +++++++ src/assets/stylesheets/profile.scss | 1 + src/assets/translations.data.json | 1 + src/avatar-selector.js | 28 ++++++++++++++------- src/react-components/avatar-selector.js | 28 +++++++++++++-------- src/react-components/profile-entry-panel.js | 25 ++++++++++-------- 6 files changed, 61 insertions(+), 31 deletions(-) diff --git a/src/assets/stylesheets/avatar-selector.scss b/src/assets/stylesheets/avatar-selector.scss index 319d6fb08..67856ca8e 100644 --- a/src/assets/stylesheets/avatar-selector.scss +++ b/src/assets/stylesheets/avatar-selector.scss @@ -1,3 +1,6 @@ +@import 'fonts'; +@import 'shared'; + #selector-root { height: 100%; } @@ -30,4 +33,10 @@ &__button-icon { font-size: 84pt; } + &__loading { + @extend %default-font; + display: block; + text-align: center; + color: white; + } } diff --git a/src/assets/stylesheets/profile.scss b/src/assets/stylesheets/profile.scss index db9326a45..9b45afd38 100644 --- a/src/assets/stylesheets/profile.scss +++ b/src/assets/stylesheets/profile.scss @@ -27,6 +27,7 @@ flex: 1 1 100%; width: 60vw; min-width: 300px; + max-width: 700px; height: 500px } diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index e10a95ae4..e45a644cc 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -15,6 +15,7 @@ "profile.save": "SAVE", "profile.display_name.validation_warning": "Alphanumerics and hyphens. At least 3 characters, no more than 32", "profile.header": "Your identity", + "profile.avatar-selector.loading": "Loading avatar selector...", "audio.title": "Test your audio", "audio.subtitle-desktop": "Confirm HMD speaker output", "audio.subtitle-mobile": "Earphones are recommended", diff --git a/src/avatar-selector.js b/src/avatar-selector.js index 55ac8df6f..d2e593596 100644 --- a/src/avatar-selector.js +++ b/src/avatar-selector.js @@ -1,6 +1,8 @@ import ReactDOM from "react-dom"; import React from "react"; import queryString from "query-string"; +import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl"; +import en from "react-intl/locale-data/en"; import "./assets/stylesheets/avatar-selector.scss"; import "./vendor/GLTFLoader"; @@ -15,6 +17,7 @@ import { avatarIds } from "./utils/identity"; import { App } from "./App"; import AvatarSelector from "./react-components/avatar-selector"; +import localeData from "./assets/translations.data.json"; window.APP = new App(); const hash = queryString.parse(location.hash); @@ -25,21 +28,28 @@ if (hash.quality) { window.APP.quality = isMobile ? "low" : "high"; } -const avatar = hash.avatar; +const lang = ((navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage) + .toLowerCase() + .split(/[_-]+/)[0]; +addLocaleData([...en]); +const messages = localeData[lang] || localeData.en; + +let avatar = hash.avatar; function postAvatarToParent(newAvatar) { - window.parent.postMessage({avatar: newAvatar}, location.origin); + window.parent.postMessage({ avatar: newAvatar }, location.origin); } function mountUI() { - const selector = ReactDOM.render( - <AvatarSelector {...{ avatars, avatar, onChange: postAvatarToParent }} />, + const hash = queryString.parse(location.hash); + const avatar = hash.avatar; + ReactDOM.render( + <IntlProvider locale={lang} messages={messages}> + <AvatarSelector {...{ avatars, avatar, onChange: postAvatarToParent }} /> + </IntlProvider>, document.getElementById("selector-root") ); - - window.addEventListener('hashchange', () => { - const hash = queryString.parse(location.hash); - selector.setState({avatar: hash.avatar}); - }); } + +window.addEventListener("hashchange", mountUI); document.addEventListener("DOMContentLoaded", mountUI); diff --git a/src/react-components/avatar-selector.js b/src/react-components/avatar-selector.js index 6b579c40b..6609af853 100644 --- a/src/react-components/avatar-selector.js +++ b/src/react-components/avatar-selector.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { injectIntl, FormattedMessage } from 'react-intl'; class AvatarSelector extends Component { static propTypes = { @@ -8,13 +9,8 @@ class AvatarSelector extends Component { onChange: PropTypes.func, } - constructor(props) { - super(props); - this.state = { avatar: this.props.avatar }; - } - getAvatarIndex(direction=0) { - const currAvatarIndex = this.props.avatars.findIndex(avatar => avatar.id === this.state.avatar); + const currAvatarIndex = this.props.avatars.findIndex(avatar => avatar.id === this.props.avatar); const numAvatars = this.props.avatars.length; return ((currAvatarIndex + direction) % numAvatars + numAvatars) % numAvatars; } @@ -29,6 +25,13 @@ class AvatarSelector extends Component { this.props.onChange(this.props.avatars[newAvatarIndex].id); } + componentDidMount() { + const start = performance.now(); + this.scene.addEventListener('loaded', () => { + this.loading.style.display = 'none'; + }); + } + render () { const avatarAssets = this.props.avatars.map(avatar => ( <a-progressive-asset @@ -53,7 +56,10 @@ class AvatarSelector extends Component { return ( <div className="avatar-selector"> - <a-scene vr-mode-ui="enabled: false" debug> + <span className="avatar-selector__loading" ref={ldg => this.loading = ldg}> + <FormattedMessage id="profile.avatar-selector.loading"/> + </span> + <a-scene vr-mode-ui="enabled: false" ref={sce => this.scene = sce}> <a-assets> {avatarAssets} <a-asset-item @@ -63,7 +69,7 @@ class AvatarSelector extends Component { ></a-asset-item> </a-assets> - <a-entity rotation={`0 ${360 * this.getAvatarIndex() / this.props.avatars.length - 180} 0`}> + <a-entity rotation={`0 ${360 * -this.getAvatarIndex() / this.props.avatars.length + 180} 0`}> {avatarEntities} </a-entity> @@ -84,10 +90,10 @@ class AvatarSelector extends Component { position="0 0 0" ></a-gltf-entity> </a-scene> - <button className="avatar-selector__prev-button" onClick={this.nextAvatar}> + <button className="avatar-selector__prev-button" onClick={this.prevAvatar}> <i className="avatar-selector__button-icon material-icons">keyboard_arrow_left</i> </button> - <button className="avatar-selector__next-button" onClick={this.prevAvatar}> + <button className="avatar-selector__next-button" onClick={this.nextAvatar}> <i className="avatar-selector__button-icon material-icons">keyboard_arrow_right</i> </button> </div> @@ -95,4 +101,4 @@ class AvatarSelector extends Component { } } -export default AvatarSelector; +export default injectIntl(AvatarSelector); diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js index 457211386..dd7899ea6 100644 --- a/src/react-components/profile-entry-panel.js +++ b/src/react-components/profile-entry-panel.js @@ -13,7 +13,7 @@ class ProfileEntryPanel extends Component { constructor(props) { super(props); window.store = this.props.store; - this.state = { + this.state = { display_name: this.props.store.state.profile.display_name, avatar: this.props.store.state.profile.avatar, }; @@ -21,7 +21,7 @@ class ProfileEntryPanel extends Component { } storeUpdated = () => { - this.setState({ + this.setState({ display_name: this.props.store.state.profile.display_name, avatar: this.props.store.state.profile.avatar, }); @@ -29,7 +29,7 @@ class ProfileEntryPanel extends Component { saveStateAndFinish = (e) => { e.preventDefault(); - this.props.store.update({profile: { + this.props.store.update({profile: { display_name: this.state.display_name, avatar: this.state.avatar }}); @@ -40,22 +40,25 @@ class ProfileEntryPanel extends Component { e.stopPropagation(); } + setAvatarStateFromIframeMessage = (e) => { + if (e.source !== this.avatarSelector.contentWindow) { return; } + this.setState({avatar: e.data.avatar}); + } + componentDidMount() { // stop propagation so that avatar doesn't move when wasd'ing during text input. this.nameInput.addEventListener('keydown', this.stopPropagation); this.nameInput.addEventListener('keypress', this.stopPropagation); this.nameInput.addEventListener('keyup', this.stopPropagation); - window.addEventListener('message', (e) => { - if (e.source !== this.avatarSelector.contentWindow) { return; } - this.setState({avatar: e.data.avatar}); - }); + window.addEventListener('message', this.setAvatarStateFromIframeMessage); } - + componentWillUnmount() { this.props.store.removeEventListener('statechanged', this.storeUpdated); this.nameInput.removeEventListener('keydown', this.stopPropagation); this.nameInput.removeEventListener('keypress', this.stopPropagation); this.nameInput.removeEventListener('keyup', this.stopPropagation); + window.removeEventListener('message', this.setAvatarStateFromIframeMessage); } render () { @@ -74,10 +77,10 @@ class ProfileEntryPanel extends Component { required pattern={SCHEMA.definitions.profile.properties.display_name.pattern} title={formatMessage({ id: "profile.display_name.validation_warning" })} ref={inp => this.nameInput = inp}/> - <iframe - className="profile-entry__avatar-selector" + <iframe + className="profile-entry__avatar-selector" src={`avatar-selector.html#avatar=${this.state.avatar}`} - ref={ifr => this.avatarSelector = ifr}>loading...</iframe> + ref={ifr => this.avatarSelector = ifr}></iframe> <input className="profile-entry__form-submit" type="submit" value={formatMessage({ id: "profile.save" }) }/> </div> </form> -- GitLab