diff --git a/scripts/default.env b/scripts/default.env index a1186bbf1f20d84c6883a36bedea1e6ac88e4e7c..b5dbe2c7c9ec3b22eba8d36f07c2fbd700bf0cb0 100644 --- a/scripts/default.env +++ b/scripts/default.env @@ -3,3 +3,4 @@ ORIGIN_TRIAL_TOKEN="ArEZ0vY0uMo3pj+oY8Up4u4Hy8QolJwKxG4/2WRhSPnTZRrviiGhzP6/y72nBdsIhdEyoundxqg//KLbs2vGnQoAAABkeyJvcmlnaW4iOiJodHRwczovL3JldGljdWx1bS5pbzo0NDMiLCJmZWF0dXJlIjoiV2ViVlIxLjFNNjIiLCJleHBpcnkiOjE1MjYzNDg2MjEsImlzU3ViZG9tYWluIjp0cnVlfQ==" ORIGIN_TRIAL_EXPIRES="2018-05-15" JANUS_SERVER="wss://prod-janus.reticulum.io" +DEV_RETICULUM_SERVER="dev.reticulum.io" diff --git a/src/assets/stylesheets/exited.scss b/src/assets/stylesheets/exited.scss index 693d6d38798705979478930f0b175ea57aa6e183..e54066078bad963cb7fa37069be6bdbab05a56ab 100644 --- a/src/assets/stylesheets/exited.scss +++ b/src/assets/stylesheets/exited.scss @@ -10,11 +10,18 @@ flex-direction: column; &__title { + @extend %default-font; font-size: 1.2em; } &__subtitle { - font-size: 0.8em; + @extend %default-font; + padding: 12px; + text-align: center; + + a { + color: $light-text; + } } } diff --git a/src/assets/stylesheets/hub-create.scss b/src/assets/stylesheets/hub-create.scss index 636ab5d656725522d5a407d93c8a1d92361d61ca..31e49358ec78660c6bd9b10b1ce321bc6225c876 100644 --- a/src/assets/stylesheets/hub-create.scss +++ b/src/assets/stylesheets/hub-create.scss @@ -192,9 +192,13 @@ font-size: 1.4em; @media (max-width: 520px) { - display: none; + display: none; } - } + } + + a { + pointer-events: all; + } } } diff --git a/src/assets/stylesheets/index.scss b/src/assets/stylesheets/index.scss index 38833657208e9a58a19ef8b10add9e8a5ceaa25d..b09114616dd142d6272899ea88ca70564511f741 100644 --- a/src/assets/stylesheets/index.scss +++ b/src/assets/stylesheets/index.scss @@ -218,6 +218,10 @@ body { &__bottom { margin-top: 4px; + + a { + color: $grey-text; + } } } } diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 2ad5c1cf95deb131b7b8b4aa02add9f63c11cc7f..6d29a6306cfb0aa0438a456099ab93f986704fab 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -32,7 +32,9 @@ "audio.granted-title": "Mic permissions granted", "audio.granted-subtitle": "You can still mute yourself in-game", "audio.granted-next": "NEXT", - "exit.subtitle": "Your session has ended. Refresh your browser to start a new one.", + "exit.subtitle.exited": "Your session has ended. Refresh your browser to start a new one.", + "exit.subtitle.full": "This room is full, please try again later.", + "exit.subtitle.connect_error": "Unable to connect to this room, please try again later.", "autoexit.title": "Auto-ending session in ", "autoexit.title_units": " seconds", "autoexit.subtitle": "You have started another session.", diff --git a/src/assets/video/home.webm b/src/assets/video/home.webm old mode 100644 new mode 100755 index d7156cad9530c12a8b0b3f31332cfebda0f4cea9..e845e128ca83848e77a1884a944e543bc7179afe Binary files a/src/assets/video/home.webm and b/src/assets/video/home.webm differ diff --git a/src/hub.js b/src/hub.js index cf096150c926e89d4999d87cd0ff34210dc73cd3..3d0b85a4b7733864cd2915cf65944c5a9f5eb71b 100644 --- a/src/hub.js +++ b/src/hub.js @@ -112,7 +112,6 @@ AFRAME.registerInputMappings(inputConfig, true); const store = new Store(); const concurrentLoadDetector = new ConcurrentLoadDetector(); -const hubChannel = new HubChannel(store); concurrentLoadDetector.start(); @@ -124,111 +123,6 @@ if (!store.state.profile.has_changed_name) { store.update({ profile: { display_name: generateRandomName() } }); } -async function exitScene() { - if (NAF.connection.adapter && NAF.connection.adapter.localMediaStream) { - NAF.connection.adapter.localMediaStream.getTracks().forEach(t => t.stop()); - } - hubChannel.disconnect(); - const scene = document.querySelector("a-scene"); - scene.renderer.animate(null); // Stop animation loop, TODO A-Frame should do this - document.body.removeChild(scene); -} - -function applyProfileFromStore(playerRig) { - const displayName = store.state.profile.display_name; - playerRig.setAttribute("player-info", { - displayName, - avatarSrc: "#" + (store.state.profile.avatar_id || "botdefault") - }); - document.querySelector("a-scene").emit("username-changed", { username: displayName }); -} - -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"); - scene.render(); - - scene.setAttribute("stats-plus", false); - - if (enterInVR) { - scene.enterVR(); - } - - AFRAME.registerInputActions(inGameActions, "default"); - - document.querySelector("#player-camera").setAttribute("look-controls", ""); - - scene.setAttribute("networked-scene", { - room: janusRoomId, - serverURL: process.env.JANUS_SERVER - }); - - if (isMobile || qsTruthy("mobile")) { - playerRig.setAttribute("virtual-gamepad-controls", {}); - } - - const applyProfileOnPlayerRig = applyProfileFromStore.bind(null, playerRig); - applyProfileOnPlayerRig(); - store.addEventListener("statechanged", applyProfileOnPlayerRig); - - const avatarScale = parseInt(qs.avatar_scale, 10); - - if (avatarScale) { - playerRig.setAttribute("scale", { x: avatarScale, y: avatarScale, z: avatarScale }); - } - - const videoTracks = mediaStream.getVideoTracks(); - let sharingScreen = videoTracks.length > 0; - - const screenEntityId = `${NAF.clientId}-screen`; - let screenEntity = document.getElementById(screenEntityId); - - scene.addEventListener("action_share_screen", () => { - sharingScreen = !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 (!qsTruthy("offline")) { - document.body.addEventListener("connected", () => { - hubChannel.sendEntryEvent().then(() => { - store.update({ lastEnteredAt: moment().toJSON() }); - }); - }); - - scene.components["networked-scene"].connect(); - - if (mediaStream) { - NAF.connection.adapter.setLocalMediaStream(mediaStream); - - if (screenEntity) { - screenEntity.setAttribute("visible", sharingScreen); - } else if (sharingScreen) { - const sceneEl = document.querySelector("a-scene"); - screenEntity = document.createElement("a-entity"); - screenEntity.id = screenEntityId; - screenEntity.setAttribute("offset-relative-to", { - target: "#player-camera", - offset: "0 0 -2", - on: "action_share_screen" - }); - screenEntity.setAttribute("networked", { template: "#video-template" }); - sceneEl.appendChild(screenEntity); - } - } - } -} - function mountUI(scene, props = {}) { const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi"); const forcedVREntryType = qs.vr_entry_type || null; @@ -240,8 +134,6 @@ function mountUI(scene, props = {}) { <UIRoot {...{ scene, - enterScene, - exitScene, concurrentLoadDetector, disableAutoExitOnConcurrentLoad, forcedVREntryType, @@ -258,19 +150,135 @@ function mountUI(scene, props = {}) { const onReady = async () => { const scene = document.querySelector("a-scene"); + const hubChannel = new HubChannel(store); + document.querySelector("a-scene canvas").classList.add("blurred"); window.APP.scene = scene; registerNetworkSchemas(); + let uiProps = {}; + mountUI(scene); - let modifiedProps = {}; const remountUI = props => { - modifiedProps = { ...modifiedProps, ...props }; - mountUI(scene, modifiedProps); + uiProps = { ...uiProps, ...props }; + mountUI(scene, uiProps); + }; + + const applyProfileFromStore = playerRig => { + const displayName = store.state.profile.display_name; + playerRig.setAttribute("player-info", { + displayName, + avatarSrc: "#" + (store.state.profile.avatar_id || "botdefault") + }); + document.querySelector("a-scene").emit("username-changed", { username: displayName }); + }; + + const exitScene = () => { + hubChannel.disconnect(); + const scene = document.querySelector("a-scene"); + scene.renderer.animate(null); // Stop animation loop, TODO A-Frame should do this + document.body.removeChild(scene); + }; + + const enterScene = async (mediaStream, enterInVR, janusRoomId) => { + const scene = document.querySelector("a-scene"); + const playerRig = document.querySelector("#player-rig"); + document.querySelector("a-scene canvas").classList.remove("blurred"); + scene.render(); + + if (enterInVR) { + scene.enterVR(); + } + + AFRAME.registerInputActions(inGameActions, "default"); + + document.querySelector("#player-camera").setAttribute("look-controls", ""); + + scene.setAttribute("networked-scene", { + room: janusRoomId, + serverURL: process.env.JANUS_SERVER + }); + + if (!qsTruthy("no_stats")) { + scene.setAttribute("stats", true); + } + + if (isMobile || qsTruthy("mobile")) { + playerRig.setAttribute("virtual-gamepad-controls", {}); + } + + const applyProfileOnPlayerRig = applyProfileFromStore.bind(null, playerRig); + applyProfileOnPlayerRig(); + store.addEventListener("statechanged", applyProfileOnPlayerRig); + + const avatarScale = parseInt(qs.avatar_scale, 10); + + if (avatarScale) { + playerRig.setAttribute("scale", { x: avatarScale, y: avatarScale, z: avatarScale }); + } + + const videoTracks = mediaStream.getVideoTracks(); + let sharingScreen = videoTracks.length > 0; + + const screenEntityId = `${NAF.clientId}-screen`; + let screenEntity = document.getElementById(screenEntityId); + + scene.addEventListener("action_share_screen", () => { + sharingScreen = !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 (!qsTruthy("offline")) { + document.body.addEventListener("connected", () => { + hubChannel.sendEntryEvent().then(() => { + store.update({ lastEnteredAt: moment().toJSON() }); + }); + }); + + scene.components["networked-scene"].connect().catch(connectError => { + // hacky until we get return codes + const isFull = connectError.error && connectError.error.msg.match(/\bfull\b/i); + remountUI({ roomUnavailableReason: isFull ? "full" : "connect_error" }); + exitScene(); + + return; + }); + + if (mediaStream) { + NAF.connection.adapter.setLocalMediaStream(mediaStream); + + if (screenEntity) { + screenEntity.setAttribute("visible", sharingScreen); + } else if (sharingScreen) { + const sceneEl = document.querySelector("a-scene"); + screenEntity = document.createElement("a-entity"); + screenEntity.id = screenEntityId; + screenEntity.setAttribute("offset-relative-to", { + target: "#player-camera", + offset: "0 0 -2", + on: "action_share_screen" + }); + screenEntity.setAttribute("networked", { template: "#video-template" }); + sceneEl.appendChild(screenEntity); + } + } + } }; + remountUI({ enterScene, exitScene }); + getAvailableVREntryTypes().then(availableVREntryTypes => { remountUI({ availableVREntryTypes }); }); @@ -302,8 +310,10 @@ const onReady = async () => { console.log(`Hub ID: ${hubId}`); const socketProtocol = document.location.protocol === "https:" ? "wss:" : "ws:"; - const socketPort = qs.phx_port || document.location.port; - const socketHost = qs.phx_host || document.location.hostname; + const socketPort = qs.phx_port || (process.env.NODE_ENV === "production" ? document.location.port : 443); + const socketHost = + qs.phx_host || + (process.env.NODE_ENV === "production" ? document.location.hostname : process.env.DEV_RETICULUM_SERVER); const socketUrl = `${socketProtocol}//${socketHost}${socketPort ? `:${socketPort}` : ""}/socket`; console.log(`Phoenix Channel URL: ${socketUrl}`); @@ -322,7 +332,14 @@ const onReady = async () => { initialEnvironmentEl.setAttribute("gltf-bundle", `src: ${gltfBundleUrl}`); hubChannel.setPhoenixChannel(channel); }) - .receive("error", res => console.error(res)); + .receive("error", res => { + if (res.reason === "closed") { + exitScene(); + remountUI({ roomUnavailableReason: "closed" }); + } + + console.error(res); + }); }; document.addEventListener("DOMContentLoaded", onReady); diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js index d91dfdbed6e726a785cbb8744d35e3dab066085c..88d697a2d15ffc1ebfb4528f0e38f6de7134656e 100644 --- a/src/react-components/home-root.js +++ b/src/react-components/home-root.js @@ -19,9 +19,10 @@ const messages = localeData[lang] || localeData.en; const ENVIRONMENT_URLS = [ "https://asset-bundles-prod.reticulum.io/rooms/meetingroom/MeetingRoom.bundle.json", - "https://asset-bundles-prod.reticulum.io/rooms/theater/TheaterMeshes.bundle.json", - "https://asset-bundles-prod.reticulum.io/rooms/atrium/AtriumMeshes.bundle.json", - "https://asset-bundles-prod.reticulum.io/rooms/courtyard/CourtyardMeshes.bundle.json" + "https://asset-bundles-prod.reticulum.io/rooms/theater/Theater.bundle.json", + "https://asset-bundles-prod.reticulum.io/rooms/atrium/Atrium.bundle.json", + "https://asset-bundles-prod.reticulum.io/rooms/courtyard/Courtyard.bundle.json", + "https://asset-bundles-prod.reticulum.io/rooms/MedievalFantasyBook/MedievalFantasyBook.bundle.json" ]; class HomeRoot extends Component { @@ -38,7 +39,7 @@ class HomeRoot extends Component { componentDidMount() { this.loadEnvironments(); - document.querySelector("#background-video").playbackRate = 0.5; + document.querySelector("#background-video").playbackRate = 0.75; } showDialog = dialogType => { @@ -265,6 +266,16 @@ class HomeRoot extends Component { <div className="footer-content__links__bottom"> <FormattedMessage id="home.made_with_love" /> <span style={{ fontWeight: "bold", color: "white" }}>moz://a</span> + <span> + | Medieval Fantasy Book by{" "} + <a + target="_blank" + rel="noreferrer noopener" + href="https://sketchfab.com/models/06d5a80a04fc4c5ab552759e9a97d91a?utm_campaign=06d5a80a04fc4c5ab552759e9a97d91a&utm_medium=embed&utm_source=oembed" + > + Pixel + </a> + </span> </div> </div> </div> diff --git a/src/react-components/hub-create-panel.js b/src/react-components/hub-create-panel.js index 32fd19b4edfcb343b0cada0d4f0abf0052344d8e..6e689653bcc4129547ec780cacbe220cb9f954da 100644 --- a/src/react-components/hub-create-panel.js +++ b/src/react-components/hub-create-panel.js @@ -38,14 +38,25 @@ class HubCreatePanel extends Component { hub: { name: this.state.name, default_environment_gltf_bundle_url: environment.bundle_url } }; - const res = await fetch("/api/v1/hubs", { + let createUrl = "/api/v1/hubs"; + + if (process.env.NODE_ENV === "development") { + createUrl = `https://${process.env.DEV_RETICULUM_SERVER}${createUrl}`; + } + + const res = await fetch(createUrl, { body: JSON.stringify(payload), headers: { "content-type": "application/json" }, method: "POST" }); const hub = await res.json(); - document.location = hub.url; + + if (process.env.NODE_ENV === "production") { + document.location = hub.url; + } else { + document.location = `/hub.html?hub_id=${hub.hub_id}`; + } }; isHubNameValid = () => { @@ -149,12 +160,22 @@ class HubCreatePanel extends Component { {environmentTitle} </span> {environmentAuthor && - environmentAuthor.name && ( + environmentAuthor.name && + (environmentAuthor.url ? ( + <a + href={environmentAuthor.url} + target="_blank" + className="create-panel__form__environment__picker__labels__header__author" + > + <FormattedMessage id="home.environment_author_by" /> + <span>{environmentAuthor.name}</span> + </a> + ) : ( <span className="create-panel__form__environment__picker__labels__header__author"> <FormattedMessage id="home.environment_author_by" /> <span>{environmentAuthor.name}</span> </span> - )} + ))} </div> <div className="create-panel__form__environment__picker__labels__footer"> <FormattedMessage id="home.environment_picker_footer" /> diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 2883d23f066498e941d561b6722b9fc5b434590c..512cb62c14750d01c8cd9cbbff7c3bc2b8b9cc6d 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -64,7 +64,8 @@ class UIRoot extends Component { showProfileEntry: PropTypes.bool, availableVREntryTypes: PropTypes.object, initialEnvironmentLoaded: PropTypes.bool, - janusRoomId: PropTypes.number + janusRoomId: PropTypes.number, + roomUnavailableReason: PropTypes.string }; state = { @@ -505,33 +506,54 @@ class UIRoot extends Component { }; render() { - if (!this.props.initialEnvironmentLoaded || !this.props.availableVREntryTypes || !this.props.janusRoomId) { + if (this.state.exited || this.props.roomUnavailableReason) { + let subtitle = null; + if (this.props.roomUnavailableReason !== "closed") { + const exitSubtitleId = `exit.subtitle.${this.state.exited ? "exited" : this.props.roomUnavailableReason}`; + subtitle = <FormattedMessage id={exitSubtitleId} />; + } else { + // TODO i18n, due to links and markup + subtitle = ( + <div> + Sorry, this room is no longer available. + <p /> + A room may be closed if we receive reports that it violates our{" "} + <a target="_blank" rel="noreferrer noopener" href="https://github.com/mozilla/hubs/blob/master/TERMS.md"> + Terms of Use + </a>. + <br /> + If you have questions, contact us at <a href="mailto:hubs@mozilla.com">hubs@mozilla.com</a>. + <p /> + If you'd like to run your own server, Hubs's source code is available on{" "} + <a href="https://github.com/mozilla/hubs">Github</a>. + </div> + ); + } + return ( <IntlProvider locale={lang} messages={messages}> - <div className="loading-panel"> - <div className="loader-wrap"> - <div className="loader"> - <div className="loader-center" /> - </div> - </div> - <div className="loading-panel__title"> + <div className="exited-panel"> + <div className="exited-panel__title"> <b>moz://a</b> duck </div> + <div className="exited-panel__subtitle">{subtitle}</div> </div> </IntlProvider> ); } - if (this.state.exited) { + if (!this.props.initialEnvironmentLoaded || !this.props.availableVREntryTypes || !this.props.janusRoomId) { return ( <IntlProvider locale={lang} messages={messages}> - <div className="exited-panel"> + <div className="loading-panel"> + <div className="loader-wrap"> + <div className="loader"> + <div className="loader-center" /> + </div> + </div> <div className="loading-panel__title"> <b>moz://a</b> duck </div> - <div className="loading-panel__subtitle"> - <FormattedMessage id="exit.subtitle" /> - </div> </div> </IntlProvider> ); diff --git a/src/telemetry.js b/src/telemetry.js index 6eed4d002b96c507dcdaea78b6a5ab58f9791441..9fa4ae9ce79009d203fb4992cd6d643460633794 100644 --- a/src/telemetry.js +++ b/src/telemetry.js @@ -2,6 +2,6 @@ import Raven from "raven-js"; export default function registerTelemetry() { if (process.env.NODE_ENV === "production") { - Raven.config("https://f571beaf5cee4e3085e0bf436f3eb158@sentry.io/256771").install(); + Raven.config("https://013d6a364fed43cdb0539a61d520597a@sentry.prod.mozaws.net/370").install(); } } diff --git a/webpack.config.js b/webpack.config.js index 4f9b26d26cab4c3959a5b1796696f7a0b531c086..55bae82132e8d8611ba8b01ffcddf1ac49109c4b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -87,7 +87,7 @@ const config = { mode: "development", devtool: process.env.NODE_ENV === "production" ? "source-map" : "inline-source-map", devServer: { - open: true, + open: false, https: createHTTPSConfig(), host: "0.0.0.0", useLocalIp: true, @@ -216,7 +216,8 @@ const config = { new webpack.DefinePlugin({ "process.env": JSON.stringify({ NODE_ENV: process.env.NODE_ENV, - JANUS_SERVER: process.env.JANUS_SERVER + JANUS_SERVER: process.env.JANUS_SERVER, + DEV_RETICULUM_SERVER: process.env.DEV_RETICULUM_SERVER }) }) ]