diff --git a/package.json b/package.json index 033fc274b7e8b599ffd39c09c4cb1e399e4548e1..5baab2abb42b937e578c0b379d96fcc81f9f9e39 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "prettier": "prettier --write src/**/*.js" }, "dependencies": { + "@fortawesome/fontawesome": "^1.1.5", + "@fortawesome/fontawesome-free-solid": "^5.0.9", + "@fortawesome/react-fontawesome": "^0.0.18", "aframe-billboard-component": "^1.0.0", "aframe-extras": "^3.12.4", "aframe-input-mapping-component": "https://github.com/johnshaughnessy/aframe-input-mapping-component#feature/map-to-array", diff --git a/src/assets/avatars/BotBobo_Avatar.glb b/src/assets/avatars/BotBobo_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..66862adb28f35fb3bb1ca4954d840a1f9a91bb96 Binary files /dev/null and b/src/assets/avatars/BotBobo_Avatar.glb differ diff --git a/src/assets/avatars/BotBobo_Avatar_Unlit.glb b/src/assets/avatars/BotBobo_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..e4a939a242fc930dc52c7d60aa145b7b86023330 Binary files /dev/null and b/src/assets/avatars/BotBobo_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/BotDom_Avatar.glb b/src/assets/avatars/BotDom_Avatar.glb index 9c72e3d2fc170d6b0782d741eca441a62780daad..098a042ead7b919170c6c4c6a63ba40f62a1b81b 100644 Binary files a/src/assets/avatars/BotDom_Avatar.glb and b/src/assets/avatars/BotDom_Avatar.glb differ diff --git a/src/assets/avatars/BotDom_Avatar_Unlit.glb b/src/assets/avatars/BotDom_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..88c830ae9f73581683050705ad75f321bdeecb37 Binary files /dev/null and b/src/assets/avatars/BotDom_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/BotGreg_Avatar.glb b/src/assets/avatars/BotGreg_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..33ca3ee66249ed267688353ef0ab82fd97dfc8bc Binary files /dev/null and b/src/assets/avatars/BotGreg_Avatar.glb differ diff --git a/src/assets/avatars/BotGreg_Avatar_Unlit.glb b/src/assets/avatars/BotGreg_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..ed0a5291790f79029b6c7e624a45253c5f6caeb3 Binary files /dev/null and b/src/assets/avatars/BotGreg_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/BotGuest_Avatar.glb b/src/assets/avatars/BotGuest_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..e0be8e9cbad1a1d6167c0c0ccf333285d0857472 Binary files /dev/null and b/src/assets/avatars/BotGuest_Avatar.glb differ diff --git a/src/assets/avatars/BotGuest_Avatar_Unlit.glb b/src/assets/avatars/BotGuest_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..3bc50b18e3ffbf9f9007a6f7790a8512284949a7 Binary files /dev/null and b/src/assets/avatars/BotGuest_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/BotJim_Avatar.glb b/src/assets/avatars/BotJim_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..34c1a8cf274d0ed62b2bc22b51d573f6bafc1728 Binary files /dev/null and b/src/assets/avatars/BotJim_Avatar.glb differ diff --git a/src/assets/avatars/BotJim_Avatar_Unlit.glb b/src/assets/avatars/BotJim_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..534356f80deb17b8808bc778db2db048811baf6a Binary files /dev/null and b/src/assets/avatars/BotJim_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/BotKev_Avatar.glb b/src/assets/avatars/BotKev_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..a20a54bcc42926a79b623e360a4ed9ddb2380695 Binary files /dev/null and b/src/assets/avatars/BotKev_Avatar.glb differ diff --git a/src/assets/avatars/BotKev_Avatar_Unlit.glb b/src/assets/avatars/BotKev_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..d70196b7a3f83f25445cef27cf0fbc7493805cd2 Binary files /dev/null and b/src/assets/avatars/BotKev_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/BotPinky_Avatar.glb b/src/assets/avatars/BotPinky_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..da14ccd6ada339e7d28c1dfcd636a0810d45907c Binary files /dev/null and b/src/assets/avatars/BotPinky_Avatar.glb differ diff --git a/src/assets/avatars/BotPinky_Avatar_Unlit.glb b/src/assets/avatars/BotPinky_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..4029c8c6feb3dfe137d932ff0e7eb9223c60b6d1 Binary files /dev/null and b/src/assets/avatars/BotPinky_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/BotRobert_Avatar.glb b/src/assets/avatars/BotRobert_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..68d28e5a19616eae6e5e5c8694182efe02643c65 Binary files /dev/null and b/src/assets/avatars/BotRobert_Avatar.glb differ diff --git a/src/assets/avatars/BotRobert_Avatar_Unlit.glb b/src/assets/avatars/BotRobert_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..505eae50b5604bdbe3a7076421dabd1847e561f1 Binary files /dev/null and b/src/assets/avatars/BotRobert_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/BotWoody_Avatar.glb b/src/assets/avatars/BotWoody_Avatar.glb new file mode 100644 index 0000000000000000000000000000000000000000..0a205ee9311b5c73f96f5fdf562970bd362b1a21 Binary files /dev/null and b/src/assets/avatars/BotWoody_Avatar.glb differ diff --git a/src/assets/avatars/BotWoody_Avatar_Unlit.glb b/src/assets/avatars/BotWoody_Avatar_Unlit.glb new file mode 100644 index 0000000000000000000000000000000000000000..bd330d47f269ccaae436fbb3a33aa85b51eee6fd Binary files /dev/null and b/src/assets/avatars/BotWoody_Avatar_Unlit.glb differ diff --git a/src/assets/avatars/avatars.js b/src/assets/avatars/avatars.js new file mode 100644 index 0000000000000000000000000000000000000000..f7a5500048710c5bf0910e5bc781000764871312 --- /dev/null +++ b/src/assets/avatars/avatars.js @@ -0,0 +1,65 @@ +export const avatars = [ + { + "id": "botdefault", + "models": { + "low": `${ require("./BotDefault_Avatar_Unlit.glb") }`, + "high": `${ require("./BotDefault_Avatar.glb") }` + } + }, + { + "id": "botbobo", + "models": { + "low": `${ require("./BotBobo_Avatar_Unlit.glb") }`, + "high": `${ require("./BotBobo_Avatar.glb") }` + } + }, + { + "id": "botdom", + "models": { + "low": `${ require("./BotDom_Avatar_Unlit.glb") }`, + "high": `${ require("./BotDom_Avatar.glb") }` + } + }, + { + "id": "botgreg", + "models": { + "low": `${ require("./BotGreg_Avatar_Unlit.glb") }`, + "high": `${ require("./BotGreg_Avatar.glb") }` + } + }, + { + "id": "botguest", + "models": { + "low": `${ require("./BotGuest_Avatar_Unlit.glb") }`, + "high": `${ require("./BotGuest_Avatar.glb") }` + } + }, + { + "id": "botjim", + "models": { + "low": `${ require("./BotJim_Avatar_Unlit.glb") }`, + "high": `${ require("./BotJim_Avatar.glb") }` + } + }, + { + "id": "botpinky", + "models": { + "low": `${ require("./BotPinky_Avatar_Unlit.glb") }`, + "high": `${ require("./BotPinky_Avatar.glb") }` + } + }, + { + "id": "botrobert", + "models": { + "low": `${ require("./BotRobert_Avatar_Unlit.glb") }`, + "high": `${ require("./BotRobert_Avatar.glb") }` + } + }, + { + "id": "botwoody", + "models": { + "low": `${ require("./BotWoody_Avatar_Unlit.glb") }`, + "high": `${ require("./BotWoody_Avatar.glb") }` + } + } +]; diff --git a/src/assets/stylesheets/avatar-selector.scss b/src/assets/stylesheets/avatar-selector.scss new file mode 100644 index 0000000000000000000000000000000000000000..1663ec5b6ba92ef81c1b7fc9955f98dd0788272b --- /dev/null +++ b/src/assets/stylesheets/avatar-selector.scss @@ -0,0 +1,45 @@ +@import 'fonts'; +@import 'shared'; + +#selector-root { + height: 100%; +} +.avatar-selector { + overflow: hidden; + height: 100%; + display: flex; + justify-content: center; + + &__previous-button:focus, &__next-button:focus { + outline: none; + } + &__previous-button:active, &__next-button:active { + color: grey; + } + &__previous-button, &__next-button { + position: absolute; + top: 50%; + margin-top: -0.5em; + appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + background: transparent; + color: white; + border: none; + font-size: 64pt; + } + &__previous-button { + left: 0.2em; + } + &__next-button { + right: 0.2em; + } + &__loading { + @extend %default-font; + display: block; + position: absolute; + top: 50%; + text-align: center; + color: white; + } +} diff --git a/src/assets/stylesheets/entry.scss b/src/assets/stylesheets/entry.scss index d30ffc98bc332d450d24dc29f3e2ebbcf5d21772..2915f56da8f98f3fc79bda2b147d3750179c5dbc 100644 --- a/src/assets/stylesheets/entry.scss +++ b/src/assets/stylesheets/entry.scss @@ -18,6 +18,29 @@ flex-direction: column; flex: 10 1 auto; justify-content: center; + + &__screen-sharing { + font-size: 1.4em; + margin-left: 2.95em; + margin-top: 0.6em; + } + + &__screen-sharing-checkbox { + appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + width: 2em; + height: 2em; + border: 3px solid white; + border-radius: 9px; + vertical-align: sub; + margin: 0 0.6em + } + &__screen-sharing-checkbox:checked { + border: 9px double white; + outline: 9px solid white; + outline-offset: -18px; + } } .entry-panel__secondary { diff --git a/src/assets/stylesheets/profile.scss b/src/assets/stylesheets/profile.scss index 05b2e5d801f42cee41a9e245eb57daed200798cc..9b45afd389dd5006d1596ee11d94f41a1ee59650 100644 --- a/src/assets/stylesheets/profile.scss +++ b/src/assets/stylesheets/profile.scss @@ -8,17 +8,27 @@ align-items: center; display: flex; pointer-events: auto; + + &__avatar-selector { + border: none; + width: 95%; + height: 100%; + margin: 1em 0; + } } .profile-entry__box { - height: 150px; border-radius: 8px; display: flex; flex-direction: column; justify-content: space-between; align-items: center; - padding: 25px; + padding: 15px; flex: 1 1 100%; + width: 60vw; + min-width: 300px; + max-width: 700px; + height: 500px } .profile-entry__box--darkened { @@ -42,6 +52,7 @@ line-height: 2.0em; padding-left: 1.25em; padding-right: 1.25em; + margin: 0.5em 0; } .profile-entry__form-submit { diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 4251de47fb601c4b3c33c9297e419db38894dbc6..043290de7491a39a1d8f114be22913d915d1f06f 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -12,9 +12,11 @@ "entry.daydream-prefix": "Enter on ", "entry.daydream-medium": "Daydream", "entry.daydream-via-chrome": "Using Google Chrome", + "entry.enable-screen-sharing": "Share my desktop", "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 Avatars...", "audio.title": "Test your audio", "audio.subtitle-desktop": "Confirm HMD speaker output", "audio.subtitle-mobile": "Earphones are recommended", diff --git a/src/avatar-selector.html b/src/avatar-selector.html new file mode 100644 index 0000000000000000000000000000000000000000..8496ef94d7f95ec90d29ae5c7e3c030c15659238 --- /dev/null +++ b/src/avatar-selector.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="utf-8"> + <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> + <% if(NODE_ENV === "production") { %> + <script src="https://cdn.rawgit.com/brianpeiris/aframe/845825ae694449524c185c44a314d361eead4680/dist/aframe-master.min.js"></script> + <% } else { %> + <script src="https://cdn.rawgit.com/brianpeiris/aframe/845825ae694449524c185c44a314d361eead4680/dist/aframe-master.js"></script> + <% } %> +</head> + +<body> + <div id="selector-root"></div> +</body> + +</html> diff --git a/src/avatar-selector.js b/src/avatar-selector.js new file mode 100644 index 0000000000000000000000000000000000000000..6acfe154caebb1c9f799631940aeb918e945fd7e --- /dev/null +++ b/src/avatar-selector.js @@ -0,0 +1,53 @@ +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"; + +import "./components/animation-mixer"; +import "./components/audio-feedback"; +import "./components/loop-animation"; +import "./elements/a-progressive-asset"; +import "./gltf-component-mappings"; +import { avatars } from "./assets/avatars/avatars.js"; +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); +const isMobile = AFRAME.utils.device.isMobile(); +if (hash.quality) { + window.APP.quality = hash.quality; +} else { + window.APP.quality = isMobile ? "low" : "high"; +} + +const lang = ((navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage) + .toLowerCase() + .split(/[_-]+/)[0]; +addLocaleData([...en]); +const messages = localeData[lang] || localeData.en; + +function postAvatarIdToParent(newAvatarId) { + window.parent.postMessage({ avatarId: newAvatarId }, location.origin); +} + +function mountUI() { + const hash = queryString.parse(location.hash); + const avatarId = hash.avatar_id; + ReactDOM.render( + <IntlProvider locale={lang} messages={messages}> + <AvatarSelector {...{ avatars, avatarId, onChange: postAvatarIdToParent }} /> + </IntlProvider>, + document.getElementById("selector-root") + ); +} + +window.addEventListener("hashchange", mountUI); +document.addEventListener("DOMContentLoaded", mountUI); diff --git a/src/components/networked-video-player.js b/src/components/networked-video-player.js index 03cfba4f6d7a835ac825804724a4109d16690cdb..51adcf2f7f2ddd6d0023c54442e03627ea952c70 100644 --- a/src/components/networked-video-player.js +++ b/src/components/networked-video-player.js @@ -1,3 +1,5 @@ +import queryString from "query-string"; + import styles from "./networked-video-player.css"; const nafConnected = function() { @@ -9,14 +11,6 @@ const nafConnected = function() { AFRAME.registerComponent("networked-video-player", { schema: {}, async init() { - let container = document.getElementById("nvp-debug-container"); - if (!container) { - container = document.createElement("div"); - container.id = "nvp-debug-container"; - container.classList.add(styles.container); - document.body.appendChild(container); - } - await nafConnected(); const networkedEl = await NAF.utils.getNetworkedEntity(this.el); @@ -25,6 +19,24 @@ AFRAME.registerComponent("networked-video-player", { } const ownerId = networkedEl.components.networked.data.owner; + + const qs = queryString.parse(location.search); + const rejectScreenShares = qs.accept_screen_shares === undefined; + if (ownerId !== NAF.clientId && rejectScreenShares) { + // Toggle material visibility since object visibility is network-synced + // TODO: There ought to be a better way to disable network syncs on a remote entity + this.el.setAttribute("material", {visible: false}); + return; + } + + let container = document.getElementById("nvp-debug-container"); + if (!container) { + container = document.createElement("div"); + container.id = "nvp-debug-container"; + container.classList.add(styles.container); + document.body.appendChild(container); + } + const stream = await NAF.connection.adapter.getMediaStream(ownerId, "video"); if (!stream) { return; diff --git a/src/components/player-info.js b/src/components/player-info.js index 8cb265a67b9c67ac26510d6b8a03efb59703c931..ac2fc6b55fcdaf0215e26327368c07dd67e88fcc 100644 --- a/src/components/player-info.js +++ b/src/components/player-info.js @@ -1,7 +1,7 @@ AFRAME.registerComponent("player-info", { schema: { displayName: { type: "string" }, - avatar: { type: "string" } + avatarSrc: { type: "string" } }, init() { this.applyProperties = this.applyProperties.bind(this); @@ -24,8 +24,8 @@ AFRAME.registerComponent("player-info", { } const modelEl = this.el.querySelector(".model"); - if (this.data.avatar && modelEl) { - modelEl.setAttribute("src", this.data.avatar); + if (this.data.avatarSrc && modelEl) { + modelEl.setAttribute("src", this.data.avatarSrc); } } }); diff --git a/src/elements/a-gltf-entity.js b/src/elements/a-gltf-entity.js index e88f7a76bf6ee4218f37997d9497e576b3bed55a..33489e2bc1792aa2b9dd4bddca360caaa39c509b 100644 --- a/src/elements/a-gltf-entity.js +++ b/src/elements/a-gltf-entity.js @@ -2,6 +2,9 @@ const GLTFCache = {}; AFRAME.AGLTFEntity = { defaultInflator(el, componentName, componentData) { + if (!AFRAME.components[componentName]) { + throw new Error(`Inflator failed. "${componentName}" component does not exist.`); + } if (AFRAME.components[componentName].multiple && Array.isArray(componentData)) { for (let i = 0; i < componentData.length; i++) { el.setAttribute(componentName + "__" + i, componentData[i]); @@ -220,7 +223,10 @@ AFRAME.registerElement("a-gltf-entity", { this.querySelectorAll(":scope > template").forEach(templateEl => this.templates.push({ selector: templateEl.getAttribute("data-selector"), - templateRoot: document.importNode(templateEl.content.firstElementChild, true) + templateRoot: document.importNode( + templateEl.firstElementChild || templateEl.content.firstElementChild, + true + ) }) ); } @@ -232,6 +238,10 @@ AFRAME.registerElement("a-gltf-entity", { // If the src attribute is a selector, get the url from the asset item. if (src && src.charAt(0) === "#") { const assetEl = document.getElementById(src.substring(1)); + if (!assetEl) { + console.warn(`Attempted to use non-existent asset ${src} as src for`, this); + return; + } const fallbackSrc = assetEl.getAttribute("src"); const highSrc = assetEl.getAttribute("high-src"); @@ -282,8 +292,7 @@ AFRAME.registerElement("a-gltf-entity", { this.emit("model-loaded", { format: "gltf", model: this.model }); } catch (e) { - const message = (e && e.message) || "Failed to load glTF model"; - console.error(message); + console.error("Failed to load glTF model", e.message, this); this.emit("model-error", { format: "gltf", src }); } } @@ -303,7 +312,6 @@ AFRAME.registerElement("a-gltf-entity", { if (attr === "src") { this.applySrc(newVal); } - AFRAME.AEntity.prototype.attributeChangedCallback.call(this, attr, oldVal, newVal); } }, diff --git a/src/elements/a-progressive-asset.js b/src/elements/a-progressive-asset.js index a367e374aece30163bc73802ab8cf11950f28c1a..8bf922aa17b10ce6eec09baf413e93dcf6d871f1 100644 --- a/src/elements/a-progressive-asset.js +++ b/src/elements/a-progressive-asset.js @@ -9,17 +9,17 @@ AFRAME.registerElement("a-progressive-asset", { value() { this.data = null; this.isAssetItem = true; + } + }, + attachedCallback: { + value() { if (!this.parentNode.fileLoader) { throw new Error("a-progressive-asset must be the child of an a-assets element."); } this.fileLoader = this.parentNode.fileLoader; - } - }, - attachedCallback: { - value() { const self = this; const fallbackSrc = this.getAttribute("src"); const highSrc = this.getAttribute("high-src"); diff --git a/src/hub.html b/src/hub.html index 6a66bb7ac9dbb44ec6018b391df1ead0a0ea3f06..fd276abaec43666e9f57842b8e96a0da8c6b1ead 100644 --- a/src/hub.html +++ b/src/hub.html @@ -30,13 +30,69 @@ <img id="avatar" src="./assets/hud/avatar.jpg" > <a-progressive-asset - id="bot-skinned-mesh" + id="botdefault" response-type="arraybuffer" src="./assets/avatars/BotDefault_Avatar_Unlit.glb" high-src="./assets/avatars/BotDefault_Avatar.glb" low-src="./assets/avatars/BotDefault_Avatar_Unlit.glb" ></a-progressive-asset> - <a-asset-item id="bot-dom-mesh" response-type="arraybuffer" src="./assets/avatars/BotDom_Avatar.glb"></a-asset-item> + <a-progressive-asset + id="botbobo" + response-type="arraybuffer" + src="./assets/avatars/BotBobo_Avatar_Unlit.glb" + high-src="./assets/avatars/BotBobo_Avatar.glb" + low-src="./assets/avatars/BotBobo_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-progressive-asset + id="botdom" + response-type="arraybuffer" + src="./assets/avatars/BotDom_Avatar_Unlit.glb" + high-src="./assets/avatars/BotDom_Avatar.glb" + low-src="./assets/avatars/BotDom_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-progressive-asset + id="botgreg" + response-type="arraybuffer" + src="./assets/avatars/BotGreg_Avatar_Unlit.glb" + high-src="./assets/avatars/BotGreg_Avatar.glb" + low-src="./assets/avatars/BotGreg_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-progressive-asset + id="botguest" + response-type="arraybuffer" + src="./assets/avatars/BotGuest_Avatar_Unlit.glb" + high-src="./assets/avatars/BotGuest_Avatar.glb" + low-src="./assets/avatars/BotGuest_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-progressive-asset + id="botjim" + response-type="arraybuffer" + src="./assets/avatars/BotJim_Avatar_Unlit.glb" + high-src="./assets/avatars/BotJim_Avatar.glb" + low-src="./assets/avatars/BotJim_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-progressive-asset + id="botpinky" + response-type="arraybuffer" + src="./assets/avatars/BotPinky_Avatar_Unlit.glb" + high-src="./assets/avatars/BotPinky_Avatar.glb" + low-src="./assets/avatars/BotPinky_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-progressive-asset + id="botrobert" + response-type="arraybuffer" + src="./assets/avatars/BotRobert_Avatar_Unlit.glb" + high-src="./assets/avatars/BotRobert_Avatar.glb" + low-src="./assets/avatars/BotRobert_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-progressive-asset + id="botwoody" + response-type="arraybuffer" + src="./assets/avatars/BotWoody_Avatar_Unlit.glb" + high-src="./assets/avatars/BotWoody_Avatar.glb" + low-src="./assets/avatars/BotWoody_Avatar_Unlit.glb" + ></a-progressive-asset> + <a-asset-item id="watch-model" response-type="arraybuffer" src="./assets/hud/watch.glb"></a-asset-item> <a-asset-item id="interactable-duck" response-type="arraybuffer" src="./assets/interactables/duck/DuckyMesh.glb"></a-asset-item> @@ -45,7 +101,7 @@ <!-- Templates --> <template id="video-template"> - <a-entity class="video" geometry="primitive: plane;" material="side: double" networked-video-player></a-entity> + <a-entity class="video" geometry="primitive: plane;" material="side: double; shader: flat;" networked-video-player></a-entity> </template> <template id="remote-avatar-template"> @@ -120,7 +176,7 @@ <!-- Interactables --> <a-entity id="counter" networked-counter="max: 3; ttl: 120"></a-entity> - <a-entity + <a-entity gltf-model="#interactable-duck" scale="2 2 2" class="interactable" @@ -211,7 +267,7 @@ <a-gltf-entity class="model" inflate="true"> <template data-selector=".RootScene"> <a-entity - ik-controller + ik-controller animated-robot-hands animation-mixer ></a-entity> @@ -287,12 +343,12 @@ xr="ar: false" ></a-entity> - <a-cylinder - position="0 0.45 0" - material="visible: false" - height="1" radius="3.1" - segments-radial="12" - static-body + <a-cylinder + position="0 0.45 0" + material="visible: false" + height="1" radius="3.1" + segments-radial="12" + static-body class="collidable" ></a-cylinder> diff --git a/src/hub.js b/src/hub.js index b0899d5dda41acbe5aa2a88582f91556dba4648b..887e470250fd224c616ec66e8f53e2ceaea703fe 100644 --- a/src/hub.js +++ b/src/hub.js @@ -87,6 +87,12 @@ import { generateDefaultProfile } from "./utils/identity.js"; import { getAvailableVREntryTypes } from "./utils/vr-caps-detect.js"; import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js"; +function qsTruthy(param) { + const val = qs[param]; + // if the param exists but is not set (e.g. "?foo&bar"), its value is null. + return val === null || /1|on|true/i.test(val); +} + registerTelemetry(); AFRAME.registerInputBehaviour("vive_trackpad_dpad4", vive_trackpad_dpad4); @@ -103,44 +109,17 @@ concurrentLoadDetector.start(); // Always layer in any new default profile bits store.update({ profile: { ...generateDefaultProfile(), ...(store.state.profile || {}) } }); -async function shareMedia(audio, video) { - const constraints = { - audio: !!audio, - video: video ? { mediaSource: "screen", height: 720, frameRate: 30 } : false - }; - const mediaStream = await navigator.mediaDevices.getUserMedia(constraints); - NAF.connection.adapter.setLocalMediaStream(mediaStream); - - const id = `${NAF.clientId}-screen`; - let entity = document.getElementById(id); - if (entity) { - entity.setAttribute("visible", !!video); - } else if (video) { - const sceneEl = document.querySelector("a-scene"); - entity = document.createElement("a-entity"); - entity.id = id; - entity.setAttribute("offset-relative-to", { - target: "#player-camera", - offset: "0 0 -2", - on: "action_share_screen" - }); - entity.setAttribute("networked", { template: "#video-template" }); - sceneEl.appendChild(entity); - } -} - async function exitScene() { const scene = document.querySelector("a-scene"); scene.renderer.animate(null); // Stop animation loop, TODO A-Frame should do this document.body.removeChild(scene); } -function updatePlayerInfoFromStore() { +function applyProfileFromStore(playerRig) { const displayName = store.state.profile.display_name; - const playerRig = document.querySelector("#player-rig"); playerRig.setAttribute("player-info", { displayName, - avatar: qs.avatar || "#bot-skinned-mesh" + avatarSrc: "#" + (store.state.profile.avatar_id || "botdefault") }); document.querySelector("a-scene").emit("username-changed", { username: displayName }); } @@ -148,7 +127,6 @@ function updatePlayerInfoFromStore() { async function enterScene(mediaStream, enterInVR, janusRoomId) { const scene = document.querySelector("a-scene"); const playerRig = document.querySelector("#player-rig"); - document.querySelector("a-scene canvas").classList.remove("blurred"); registerNetworkSchemas(); @@ -169,32 +147,46 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) { serverURL: process.env.JANUS_SERVER }); - if (!qs.stats || !/off|false|0/.test(qs.stats)) { + if (!qsTruthy("no_stats")) { scene.setAttribute("stats", true); } - if (isMobile || qs.mobile) { + if (isMobile || qsTruthy(qs.mobile)) { playerRig.setAttribute("virtual-gamepad-controls", {}); } - updatePlayerInfoFromStore(); - store.addEventListener("statechanged", updatePlayerInfoFromStore); + const applyProfileOnPlayerRig = applyProfileFromStore.bind(null, playerRig); + applyProfileOnPlayerRig(); + store.addEventListener("statechanged", applyProfileOnPlayerRig); - const avatarScale = parseInt(qs.avatarScale, 10); + const avatarScale = parseInt(qs.avatar_scale, 10); if (avatarScale) { playerRig.setAttribute("scale", { x: avatarScale, y: avatarScale, z: avatarScale }); } - let sharingScreen = false; + const videoTracks = mediaStream.getVideoTracks(); + let sharingScreen = videoTracks.length > 0; + + const screenEntityId = `${NAF.clientId}-screen`; + let screenEntity = document.getElementById(screenEntityId); - // TODO remove scene.addEventListener("action_share_screen", () => { sharingScreen = !sharingScreen; - shareMedia(true, sharingScreen); + if (sharingScreen) { + for (const track of videoTracks) { + mediaStream.addTrack(track); + } + } else { + for (const track of mediaStream.getVideoTracks()) { + mediaStream.removeTrack(track); + } + } + NAF.connection.adapter.setLocalMediaStream(mediaStream); + screenEntity.setAttribute("visible", sharingScreen); }); - if (qs.offline) { + if (qsTruthy("offline")) { onConnect(); } else { document.body.addEventListener("connected", onConnect); @@ -204,23 +196,19 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) { if (mediaStream) { NAF.connection.adapter.setLocalMediaStream(mediaStream); - const hasVideo = !!(mediaStream.getVideoTracks().length > 0); - - const id = `${NAF.clientId}-screen`; - let entity = document.getElementById(id); - if (entity) { - entity.setAttribute("visible", hasVideo); - } else if (hasVideo) { + if (screenEntity) { + screenEntity.setAttribute("visible", sharingScreen); + } else if (sharingScreen) { const sceneEl = document.querySelector("a-scene"); - entity = document.createElement("a-entity"); - entity.id = id; - entity.setAttribute("offset-relative-to", { - target: "#head", + screenEntity = document.createElement("a-entity"); + screenEntity.id = screenEntityId; + screenEntity.setAttribute("offset-relative-to", { + target: "#player-camera", offset: "0 0 -2", on: "action_share_screen" }); - entity.setAttribute("networked", { template: "#video-template" }); - sceneEl.appendChild(entity); + screenEntity.setAttribute("networked", { template: "#video-template" }); + sceneEl.appendChild(screenEntity); } } } @@ -229,13 +217,9 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) { function onConnect() {} function mountUI(scene) { - const disableAutoExitOnConcurrentLoad = qs.allow_multi === "true"; - - let forcedVREntryType = null; - - if (qs.vr_entry_type) { - forcedVREntryType = qs.vr_entry_type; - } + const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi"); + const forcedVREntryType = qs.vr_entry_type || null; + const enableScreenSharing = qsTruthy("enable_screen_sharing"); const uiRoot = ReactDOM.render( <UIRoot @@ -246,6 +230,7 @@ function mountUI(scene) { concurrentLoadDetector, disableAutoExitOnConcurrentLoad, forcedVREntryType, + enableScreenSharing, store }} />, diff --git a/src/react-components/avatar-selector.js b/src/react-components/avatar-selector.js new file mode 100644 index 0000000000000000000000000000000000000000..7cf71b7c5e130c2bbd16a54e5b247f3a1cea74d5 --- /dev/null +++ b/src/react-components/avatar-selector.js @@ -0,0 +1,127 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import FontAwesomeIcon from '@fortawesome/react-fontawesome'; +import faAngleLeft from '@fortawesome/fontawesome-free-solid/faAngleLeft'; +import faAngleRight from '@fortawesome/fontawesome-free-solid/faAngleRight'; +import meetingSpace from '../assets/environments/MeetingSpace1_mesh.glb'; + +class AvatarSelector extends Component { + static propTypes = { + avatars: PropTypes.array, + avatarId: PropTypes.string, + onChange: PropTypes.func, + } + + getAvatarIndex = (direction=0) => { + const currAvatarIndex = this.props.avatars.findIndex(avatar => avatar.id === this.props.avatarId); + const numAvatars = this.props.avatars.length; + return ((currAvatarIndex + direction) % numAvatars + numAvatars) % numAvatars; + } + nextAvatarIndex = () => this.getAvatarIndex(1) + previousAvatarIndex = () => this.getAvatarIndex(-1) + + emitChangeToNext = () => { + const nextAvatarId = this.props.avatars[this.nextAvatarIndex()].id; + this.props.onChange(nextAvatarId); + } + + emitChangeToPrevious = () => { + const previousAvatarId = this.props.avatars[this.previousAvatarIndex()].id; + this.props.onChange(previousAvatarId); + } + + componentDidUpdate(prevProps) { + if (this.props.avatarId !== prevProps.avatarId) { + // HACK - a-animation ought to restart the animation when the `to` attribute changes, but it doesn't + // so we need to force it here. + const currRot = this.animation.parentNode.getAttribute('rotation'); + this.animation.setAttribute('from', `${currRot.x} ${currRot.y} ${currRot.z}`); + this.animation.stop(); + this.animation.handleMixinUpdate(); + this.animation.start(); + } + } + + render () { + const avatarAssets = this.props.avatars.map(avatar => ( + <a-progressive-asset + id={avatar.id} + key={avatar.id} + response-type="arraybuffer" + high-src={`${avatar.models.high}`} + low-src={`${avatar.models.low}`} + ></a-progressive-asset> + )); + + const avatarEntities = this.props.avatars.map((avatar, i) => ( + <a-entity key={avatar.id} position="0 0 0" rotation={`0 ${360 * -i / this.props.avatars.length} 0`}> + <a-gltf-entity position="0 0 5" rotation="0 0 0" src={'#' + avatar.id} inflate="true"> + <template data-selector=".RootScene"> + <a-entity animation-mixer></a-entity> + </template> + <a-animation + attribute="rotation" + dur="2000" + to={`0 ${this.getAvatarIndex() === i ? 360 : 0} 0`} + repeat="indefinite"> + </a-animation> + </a-gltf-entity> + </a-entity> + )); + + return ( + <div className="avatar-selector"> + <span className="avatar-selector__loading"> + <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 + id="meeting-space1-mesh" + response-type="arraybuffer" + src={meetingSpace} + ></a-asset-item> + </a-assets> + + <a-entity> + <a-animation + ref={anm => this.animation = anm} + attribute="rotation" + dur="1000" + easing="ease-out" + to={`0 ${360 * this.getAvatarIndex() / this.props.avatars.length + 180} 0`}> + </a-animation> + {avatarEntities} + </a-entity> + + <a-entity position="0 1.5 -5.6" rotation="-10 180 0" camera></a-entity> + + <a-entity + hide-when-quality="low" + light="type: directional; color: #F9FFCE; intensity: 0.6" + position="0 5 -15" + ></a-entity> + <a-entity + hide-when-quality="low" + light="type: ambient; color: #FFF" + ></a-entity> + <a-gltf-entity + id="meeting-space" + src="#meeting-space1-mesh" + position="0 0 0" + ></a-gltf-entity> + </a-scene> + <button className="avatar-selector__previous-button" onClick={this.emitChangeToPrevious}> + <FontAwesomeIcon icon={faAngleLeft} /> + </button> + <button className="avatar-selector__next-button" onClick={this.emitChangeToNext}> + <FontAwesomeIcon icon={faAngleRight} /> + </button> + </div> + ); + } +} + +export default injectIntl(AvatarSelector); diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js index 28c8cd7d9ce8c8bf9902f6a654c835bbd7fe95ea..91044c27cab1955c7e35698322c618b907a3f95d 100644 --- a/src/react-components/profile-entry-panel.js +++ b/src/react-components/profile-entry-panel.js @@ -13,29 +13,50 @@ class ProfileEntryPanel extends Component { constructor(props) { super(props); window.store = this.props.store; - this.state = {name: this.props.store.state.profile.display_name}; + this.state = { + display_name: this.props.store.state.profile.display_name, + avatar_id: this.props.store.state.profile.avatar_id, + }; this.props.store.addEventListener("statechanged", this.storeUpdated); } storeUpdated = () => { - this.setState({name: this.props.store.state.profile.display_name}); + const { avatar_id, display_name } = this.props.store.state.profile; + this.setState({ avatar_id, display_name }); } - saveName = (e) => { + saveStateAndFinish = (e) => { e.preventDefault(); - this.props.store.update({ profile: { display_name: this.nameInput.value } }); + this.props.store.update({profile: { + display_name: this.state.display_name, + avatar_id: this.state.avatar_id + }}); this.props.finished(); } + stopPropagation = (e) => { + e.stopPropagation(); + } + + setAvatarStateFromIframeMessage = (e) => { + if (e.source !== this.avatarSelector.contentWindow) { return; } + this.setState({avatar_id: e.data.avatarId}); + } + componentDidMount() { // stop propagation so that avatar doesn't move when wasd'ing during text input. - this.nameInput.addEventListener('keydown', e => e.stopPropagation()); - this.nameInput.addEventListener('keypress', e => e.stopPropagation()); - this.nameInput.addEventListener('keyup', e => e.stopPropagation()); + this.nameInput.addEventListener('keydown', this.stopPropagation); + this.nameInput.addEventListener('keypress', this.stopPropagation); + this.nameInput.addEventListener('keyup', this.stopPropagation); + 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 () { @@ -43,19 +64,26 @@ class ProfileEntryPanel extends Component { return ( <div className="profile-entry"> - <form onSubmit={this.saveName}> + <form onSubmit={this.saveStateAndFinish}> <div className="profile-entry__box profile-entry__box--darkened"> <div className="profile-entry__subtitle"> <FormattedMessage id="profile.header"/> </div> <input className="profile-entry__form-field-text" - value={this.state.name} onChange={(e) => this.setState({name: e.target.value})} + value={this.state.display_name} onChange={(e) => this.setState({display_name: e.target.value})} 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" + src={ + /* HACK: Have to account for the smoke test server like this. Feels wrong though. */ + `${/smoke/i.test(location.hostname) ? 'smoke-' : ''}avatar-selector.html#avatar_id=${this.state.avatar_id}` + } + ref={ifr => this.avatarSelector = ifr}></iframe> <input className="profile-entry__form-submit" type="submit" value={formatMessage({ id: "profile.save" }) }/> - </div> + </div> </form> </div> ); diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index f2d6f9102d23ccb25375abf75bf1733bb2b28375..7cca41e23076270371bb33ed3ce37d7b08fa5a38 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -41,11 +41,6 @@ async function grantedMicLabels() { return mediaDevices.filter(d => d.label && d.kind === "audioinput").map(d => d.label); } -async function hasGrantedMicPermissions() { - const micLabels = await grantedMicLabels(); - return micLabels.length > 0; -} - // This is a list of regexes that match the microphone labels of HMDs. // // If entering VR mode, and if any of these regexes match an audio device, @@ -64,6 +59,7 @@ class UIRoot extends Component { concurrentLoadDetector: PropTypes.object, disableAutoExitOnConcurrentLoad: PropTypes.bool, forcedVREntryType: PropTypes.string, + enableScreenSharing: PropTypes.bool, store: PropTypes.object, scene: PropTypes.object }; @@ -74,7 +70,10 @@ class UIRoot extends Component { enterInVR: false, shareScreen: false, + requestedScreen: false, mediaStream: null, + videoTrack: null, + audioTrack: null, toneInterval: null, tonePlaying: false, @@ -212,12 +211,27 @@ class UIRoot extends Component { }); }; + hasGrantedMicPermissions = async () => { + if (this.state.requestedScreen) { + // There is no way to tell if you've granted mic permissions in a previous session if we've + // already prompted for screen sharing permissions, so we have to assume that we've never granted permissions. + // Fortunately, if you *have* granted permissions permanently, there won't be a second browser prompt, but we + // can't determine that before hand. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1449783 for a potential solution in the future. + return false; + } + else { + // If we haven't requested the screen in this session, check if we've granted permissions in a previous session. + return (await grantedMicLabels()).length > 0; + } + } + performDirectEntryFlow = async enterInVR => { this.startTestTone(); this.setState({ enterInVR }); - const hasGrantedMic = await hasGrantedMicPermissions(); + const hasGrantedMic = await this.hasGrantedMicPermissions(); if (hasGrantedMic) { await this.setMediaStreamToDefault(); @@ -271,37 +285,60 @@ class UIRoot extends Component { } }; - mediaVideoConstraint = () => { - return this.state.shareScreen ? { mediaSource: "screen", height: 720, frameRate: 30 } : false; - }; - micDeviceChanged = async ev => { - const constraints = { audio: { deviceId: { exact: [ev.target.value] } }, video: this.mediaVideoConstraint() }; - await this.setupNewMediaStream(constraints); - }; + const constraints = { audio: { deviceId: { exact: [ev.target.value] } } }; + await this.fetchAudioTrack(constraints); + await this.setupNewMediaStream(); + } setMediaStreamToDefault = async () => { - await this.setupNewMediaStream({ audio: true, video: false }); - }; - - setupNewMediaStream = async constraints => { - const AudioContext = window.AudioContext || window.webkitAudioContext; - const audioContext = new AudioContext(); + await this.fetchAudioTrack({ audio: true }); + await this.setupNewMediaStream(); + } - if (this.state.mediaStream) { - clearInterval(this.state.micUpdateInterval); + setStateAndRequestScreen = async e => { + const checked = e.target.checked; + await this.setState({ requestedScreen: true, shareScreen: checked }); + if (checked) { + this.fetchVideoTrack({ video: { + mediaSource: "screen", + // Work around BMO 1449832 by calculating the width. This will break for multi monitors if you share anything + // other than your current monitor that has a different aspect ratio. + width: screen.width / screen.height * 720, + height: 720, + frameRate: 30 + } }); + } + else { + this.setState({ videoTrack: null }); + } + } - const previousStream = this.state.mediaStream; + fetchVideoTrack = async constraints => { + const mediaStream = await navigator.mediaDevices.getUserMedia(constraints); + this.setState({ videoTrack: mediaStream.getVideoTracks()[0] }); + } - for (const tracks of [previousStream.getAudioTracks(), previousStream.getVideoTracks()]) { - for (const track of tracks) { - track.stop(); - } - } + fetchAudioTrack = async constraints => { + if (this.state.audioTrack) { + this.state.audioTrack.stop(); } - const mediaStream = await navigator.mediaDevices.getUserMedia(constraints); + this.setState({ audioTrack: mediaStream.getAudioTracks()[0] }); + } + + setupNewMediaStream = async constraints => { + const mediaStream = new MediaStream(); + + // we should definitely have an audioTrack at this point. + mediaStream.addTrack(this.state.audioTrack); + if (this.state.videoTrack) { + mediaStream.addTrack(this.state.videoTrack); + } + + const AudioContext = window.AudioContext || window.webkitAudioContext; + const audioContext = new AudioContext(); const source = audioContext.createMediaStreamSource(mediaStream); const analyzer = audioContext.createAnalyser(); const levels = new Uint8Array(analyzer.fftSize); @@ -346,8 +383,10 @@ class UIRoot extends Component { fetchMicDevices = async () => { const mediaDevices = await navigator.mediaDevices.enumerateDevices(); - this.setState({ - micDevices: mediaDevices.filter(d => d.kind === "audioinput").map(d => ({ deviceId: d.deviceId, label: d.label })) + this.setState({ + micDevices: mediaDevices. + filter(d => d.kind === "audioinput"). + map(d => ({ deviceId: d.deviceId, label: d.label })) }); }; @@ -430,29 +469,47 @@ class UIRoot extends Component { const daydreamMaybeSubtitle = messages["entry.daydream-via-chrome"]; - const entryPanel = + // Only show this in desktop firefox since other browsers/platforms will ignore the "screen" media constraint and + // will attempt to share your webcam instead! + const screenSharingCheckbox = ( + this.props.enableScreenSharing && + !mobiledetect.mobile() && + /firefox/i.test(navigator.userAgent) && + ( + <label className="entry-panel__screen-sharing"> + <input className="entry-panel__screen-sharing-checkbox" type="checkbox" + value={this.state.shareScreen} + onChange={this.setStateAndRequestScreen} + /> + <FormattedMessage id="entry.enable-screen-sharing" /> + </label> + ) + ); + + const entryPanel = this.state.entryStep === ENTRY_STEPS.start ? ( <div className="entry-panel"> <TwoDEntryButton onClick={this.enter2D} /> - {this.state.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && ( - <GenericEntryButton onClick={this.enterVR} /> + { this.state.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && ( + <GenericEntryButton onClick={this.enterVR} /> )} - {this.state.availableVREntryTypes.gearvr !== VR_DEVICE_AVAILABILITY.no && ( - <GearVREntryButton onClick={this.enterGearVR} /> + { this.state.availableVREntryTypes.gearvr !== VR_DEVICE_AVAILABILITY.no && ( + <GearVREntryButton onClick={this.enterGearVR} /> )} - {this.state.availableVREntryTypes.daydream !== VR_DEVICE_AVAILABILITY.no && ( + { this.state.availableVREntryTypes.daydream !== VR_DEVICE_AVAILABILITY.no && ( <DaydreamEntryButton onClick={this.enterDaydream} subtitle={ - this.state.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe ? daydreamMaybeSubtitle : "" + this.state.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe ? daydreamMaybeSubtitle : "" } - /> + /> )} {this.state.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && ( <div className="entry-panel__secondary" onClick={this.enterVR}> <FormattedMessage id="entry.cardboard" /> </div> )} + { screenSharingCheckbox } </div> ) : null; diff --git a/src/storage/store.js b/src/storage/store.js index 5b00b7ac8f5570c28651587997ae4c0546a3b722..037e27bf1d9603a210423baa5ee6543c2ee77fdb 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -17,6 +17,7 @@ export const SCHEMA = { additionalProperties: false, properties: { display_name: { type: "string", pattern: "^[A-Za-z0-9-]{3,32}$" }, + avatar_id: { type: "string" }, } } }, diff --git a/src/utils/identity.js b/src/utils/identity.js index 72cabdf008bc2574b4408d62c8f7f17f30fc9a59..118fe30812c1013cb64c47a335c24465a12f6636 100644 --- a/src/utils/identity.js +++ b/src/utils/identity.js @@ -1,3 +1,5 @@ +import { avatars } from "../assets/avatars/avatars.js"; + const names = [ "albattani", "allen", @@ -161,7 +163,16 @@ const names = [ "yonath" ]; +function selectRandom(arr) { + return arr[Math.floor(Math.random() * arr.length)] +} + +export const avatarIds = avatars.map(av => av.id); + export function generateDefaultProfile() { - const name = names[Math.floor(Math.random() * names.length)]; - return { display_name: name.replace(/^./, name[0].toUpperCase()) }; + const name = selectRandom(names); + return { + display_name: name.replace(/^./, name[0].toUpperCase()) , + avatar_id: selectRandom(avatarIds) + }; } diff --git a/webpack.config.js b/webpack.config.js index a2fa5d229a890fb45261b6093305c725f11ed647..1fdc22406d4a3ab182b072c6aca32e96cb4d6b23 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -77,7 +77,8 @@ class LodashTemplatePlugin { const config = { entry: { index: path.join(__dirname, "src", "index.js"), - hub: path.join(__dirname, "src", "hub.js") + hub: path.join(__dirname, "src", "hub.js"), + "avatar-selector": path.join(__dirname, "src", "avatar-selector.js") }, output: { path: path.join(__dirname, "public"), @@ -205,6 +206,12 @@ const config = { chunks: [] }) ), + new HTMLWebpackPlugin({ + filename: "avatar-selector.html", + template: path.join(__dirname, "src", "avatar-selector.html"), + chunks: ["avatar-selector"], + inject: "head" + }), // Extract required css and add a content hash. new ExtractTextPlugin("assets/stylesheets/[name]-[contenthash].css", { disable: process.env.NODE_ENV !== "production" diff --git a/yarn.lock b/yarn.lock index cce545356bdd3a422811544e84dc33aafcbf63db..18f7727e7dd7217a0e581df22c2ddfc12a8ff16a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -78,6 +78,28 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" +"@fortawesome/fontawesome-common-types@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.1.3.tgz#8475e0f2d1ad1f858c4ec2e76ed9a2456a09ad83" + +"@fortawesome/fontawesome-free-solid@^5.0.9": + version "5.0.9" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free-solid/-/fontawesome-free-solid-5.0.9.tgz#456155a1cd82a0342ffe6a869d5a54fdadd78548" + dependencies: + "@fortawesome/fontawesome-common-types" "^0.1.3" + +"@fortawesome/fontawesome@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome/-/fontawesome-1.1.5.tgz#c7cfafdd3364245626293cc670357f9fb8487170" + dependencies: + "@fortawesome/fontawesome-common-types" "^0.1.3" + +"@fortawesome/react-fontawesome@^0.0.18": + version "0.0.18" + resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.0.18.tgz#4e0eb1cf9797715a67bb7705ae084fa0a410f185" + dependencies: + humps "^2.0.1" + JSONStream@^1.0.3: version "1.3.2" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.2.tgz#c102371b6ec3a7cf3b847ca00c20bb0fce4c6dea" @@ -3932,6 +3954,10 @@ https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" +humps@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa" + iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"