diff --git a/src/assets/stylesheets/avatar-selector.scss b/src/assets/stylesheets/avatar-selector.scss index 319d6fb08a291dd87ce1ff0bd9fb849ac849ceb9..67856ca8ef29e3f467b114062c61e4d8bb6cf300 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 db9326a45fb17c644e654eef901d576c518703fe..9b45afd389dd5006d1596ee11d94f41a1ee59650 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 e10a95ae40bb533716f17b93f15d0f196a6de13e..e45a644cc1992e965200facb0c45b84f63830ad9 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 55ac8df6f8aff0d76a6e9fff96a922a3cd21b652..d2e59359634f0e0650aeacec8bcc5727c13c3bbc 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 6b579c40b86136dac2f80225da1604e8b3dfd72a..6609af85307427b0bba30c77d86f4cefa75e4b9b 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 45721138607615d4ded41ecc4fcced174df9b20d..dd7899ea60da19e7b4b503564ace0a2f4d169db5 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>