From eb343a26b5de6599e7bec67fa340d2db4e752508 Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Wed, 2 Jan 2019 17:16:36 -0800 Subject: [PATCH 01/47] Revert "Remove roles" This reverts commit a711ac4e4eb68cbd1e032439a24877f5e878dacf. --- src/hub.js | 2 +- src/react-components/ui-root.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/hub.js b/src/hub.js index 00c1df6d27..45c19c7bf5 100644 --- a/src/hub.js +++ b/src/hub.js @@ -555,7 +555,7 @@ document.addEventListener("DOMContentLoaded", async () => { subscriptions.setHubChannel(hubChannel); subscriptions.setSubscribed(data.subscriptions.web_push); - remountUI({ initialIsSubscribed: subscriptions.isSubscribed() }); + remountUI({ initialIsSubscribed: subscriptions.isSubscribed(), roles: data.roles }); await handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data); }) .receive("error", res => { diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index b9b2c8aae0..bececd8abb 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -109,6 +109,7 @@ class UIRoot extends Component { platformUnsupportedReason: PropTypes.string, hubId: PropTypes.string, hubName: PropTypes.string, + roles: PropTypes.object, isSupportAvailable: PropTypes.bool, presenceLogEntries: PropTypes.array, presences: PropTypes.object, @@ -821,7 +822,9 @@ class UIRoot extends Component { return (
-
{this.props.hubName}
+
+ {this.props.hubName} {this.props.roles.is_host ? "(host)" : ""} +
From 53de046af503e0f7082cfa8f29b0bdee9694341d Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Thu, 3 Jan 2019 11:30:59 -0800 Subject: [PATCH 02/47] Revert "Remove postWithAuth" This reverts commit c272bb34c9bf97a78de10fcad34cda797b1d0899. --- src/react-components/hub-create-panel.js | 11 ++--------- src/react-components/scene-ui.js | 10 ++-------- src/utils/phoenix-utils.js | 9 +++++++++ 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/react-components/hub-create-panel.js b/src/react-components/hub-create-panel.js index 11f846e744..8afc537be7 100644 --- a/src/react-components/hub-create-panel.js +++ b/src/react-components/hub-create-panel.js @@ -6,7 +6,7 @@ import { faAngleLeft } from "@fortawesome/free-solid-svg-icons/faAngleLeft"; import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { resolveURL, extractUrlBase } from "../utils/resolveURL"; -import { getReticulumFetchUrl } from "../utils/phoenix-utils"; +import { postWithAuth } from "../utils/phoenix-utils"; import CreateRoomDialog from "./create-room-dialog.js"; import { WithHoverSound } from "./wrap-with-audio"; @@ -99,14 +99,7 @@ class HubCreatePanel extends Component { payload.hub.default_environment_gltf_bundle_url = sceneUrl; } - const createUrl = getReticulumFetchUrl("/api/v1/hubs"); - - const res = await fetch(createUrl, { - body: JSON.stringify(payload), - headers: { "content-type": "application/json" }, - method: "POST" - }); - + const res = await postWithAuth("/api/v1/hubs", payload); const hub = await res.json(); if (!process.env.RETICULUM_SERVER || document.location.host === process.env.RETICULUM_SERVER) { diff --git a/src/react-components/scene-ui.js b/src/react-components/scene-ui.js index 43cd7df8a9..78d977b602 100644 --- a/src/react-components/scene-ui.js +++ b/src/react-components/scene-ui.js @@ -6,7 +6,7 @@ import en from "react-intl/locale-data/en"; import styles from "../assets/stylesheets/scene-ui.scss"; import hubLogo from "../assets/images/hub-preview-white.png"; import spokeLogo from "../assets/images/spoke_logo_black.png"; -import { getReticulumFetchUrl } from "../utils/phoenix-utils"; +import { postWithAuth } from "../utils/phoenix-utils"; import { generateHubName } from "../utils/name-generation"; import { WithHoverSound } from "./wrap-with-audio"; import CreateRoomDialog from "./create-room-dialog.js"; @@ -54,14 +54,8 @@ class SceneUI extends Component { createRoom = async () => { const payload = { hub: { name: this.state.customRoomName || generateHubName(), scene_id: this.props.sceneId } }; - const createUrl = getReticulumFetchUrl("/api/v1/hubs"); - - const res = await fetch(createUrl, { - body: JSON.stringify(payload), - headers: { "content-type": "application/json" }, - method: "POST" - }); + const res = await postWithAuth("/api/v1/hubs", payload); const hub = await res.json(); if (!process.env.RETICULUM_SERVER || document.location.host === process.env.RETICULUM_SERVER) { diff --git a/src/utils/phoenix-utils.js b/src/utils/phoenix-utils.js index 7cbca9fa04..2a40de8842 100644 --- a/src/utils/phoenix-utils.js +++ b/src/utils/phoenix-utils.js @@ -52,3 +52,12 @@ export function getLandingPageForPhoto(photoUrl) { const parsedUrl = new URL(photoUrl); return getReticulumFetchUrl(parsedUrl.pathname.replace(".png", ".html") + parsedUrl.search, true); } + +export async function postWithAuth(apiEndpoint, payload) { + const headers = { "content-type": "application/json" }; + const store = new Store(); + if (store.state && store.state.credentials.token) { + headers.authorization = `bearer ${store.state.credentials.token}`; + } + return fetch(getReticulumFetchUrl(apiEndpoint), { method: "POST", headers, body: JSON.stringify(payload) }); +} From 1c50edeecc18c09327a6c0471654b3d4b13eac62 Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Thu, 3 Jan 2019 11:32:59 -0800 Subject: [PATCH 03/47] Revert "Remove sign in UI from home page" This reverts commit c2b2988f79f2f6e5afe18045ec90e72b964d546a. --- src/assets/stylesheets/index.scss | 13 +++++++++ src/index.js | 9 ++++++ src/react-components/home-root.js | 48 ++++++++++++++++++++++++++++++- 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/assets/stylesheets/index.scss b/src/assets/stylesheets/index.scss index 6b1d3b5ffe..f1b8517025 100644 --- a/src/assets/stylesheets/index.scss +++ b/src/assets/stylesheets/index.scss @@ -80,6 +80,19 @@ body { } } } + + :local(.sign-in) { + flex: 3; + display: flex; + align-items: center; + justify-content: end; + + a { + text-decoration: underline; + font-weight: bold; + cursor: pointer; + } + } } :local(.video-container) { diff --git a/src/index.js b/src/index.js index f41be85db3..83a92fa483 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,9 @@ import ReactDOM from "react-dom"; import "./assets/stylesheets/index.scss"; import registerTelemetry from "./telemetry"; import HomeRoot from "./react-components/home-root"; +import AuthChannel from "./utils/auth-channel"; +import { connectToReticulum } from "./utils/phoenix-utils"; +import Store from "./storage/store"; const qs = new URLSearchParams(location.search); registerTelemetry(); @@ -11,10 +14,16 @@ registerTelemetry(); const { pathname } = document.location; const sceneId = qs.get("scene_id") || (pathname.startsWith("/scenes/") && pathname.substring(1).split("/")[1]); +const store = new Store(); +const authChannel = new AuthChannel(store); +authChannel.setSocket(connectToReticulum()); + const root = ( { + this.showDialog(SignInDialog, { + message: messages["sign-in.prompt"], + onSignIn: async email => { + const { authComplete } = await this.props.authChannel.startAuthentication(email); + this.showDialog(SignInDialog, { authStarted: true }); + await authComplete; + this.setState({ signedIn: true, email }); + this.closeDialog(); + } + }); + }; + + signOut = () => { + this.props.authChannel.removeCredentials(); + // TODO BP - should randomize avatar and display name on sign out. + this.setState({ signedIn: false }); + }; + loadEnvironmentFromScene = async () => { let sceneUrlBase = "/api/v1/scenes"; if (process.env.RETICULUM_SERVER) { @@ -204,6 +234,22 @@ class HomeRoot extends Component {
+
+ {this.state.signedIn ? ( +
+ + {maskEmail(this.state.email)} + {" "} + + + +
+ ) : ( + + + + )} +
From 3f101428e98dc9291a3bf8b2175cfd946af56021 Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Thu, 3 Jan 2019 11:33:56 -0800 Subject: [PATCH 04/47] Revert "Remove sign in UI from home page" This reverts commit 0a570be8b3b3ad8108237c7cb57ed128a6ccfc99. --- src/react-components/home-root.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js index 0a1fd224a5..57c6ac6ee2 100644 --- a/src/react-components/home-root.js +++ b/src/react-components/home-root.js @@ -145,6 +145,25 @@ class HomeRoot extends Component { this.setState({ signedIn: false }); }; + showSignInDialog = () => { + this.showDialog(SignInDialog, { + message: messages["sign-in.prompt"], + onSignIn: async email => { + const { authComplete } = await this.props.authChannel.startAuthentication(email); + this.showDialog(SignInDialog, { authStarted: true }); + await authComplete; + this.setState({ signedIn: true, email }); + this.closeDialog(); + } + }); + }; + + signOut = () => { + this.props.authChannel.removeCredentials(); + // TODO BP - should randomize avatar and display name on sign out. + this.setState({ signedIn: false }); + }; + loadEnvironmentFromScene = async () => { let sceneUrlBase = "/api/v1/scenes"; if (process.env.RETICULUM_SERVER) { From 03550734e8f94a4c1c1ab21bc6020235b120ea0b Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Thu, 3 Jan 2019 13:39:22 -0800 Subject: [PATCH 05/47] Fix link click callback --- src/react-components/home-root.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js index 57c6ac6ee2..065e251081 100644 --- a/src/react-components/home-root.js +++ b/src/react-components/home-root.js @@ -204,7 +204,7 @@ class HomeRoot extends Component { Promise.all(environmentLoads).then(() => this.setState({ environments })); }; - onDialogLinkClicked = trigger => { + onLinkClicked = trigger => { return e => { e.preventDefault(); e.stopPropagation(); @@ -329,7 +329,7 @@ class HomeRoot extends Component { className={styles.link} rel="noopener noreferrer" href="#" - onClick={this.onDialogLinkClicked(this.showJoinUsDialog.bind(this))} + onClick={this.onLinkClicked(this.showJoinUsDialog.bind(this))} > @@ -339,7 +339,7 @@ class HomeRoot extends Component { className={styles.link} rel="noopener noreferrer" href="#" - onClick={this.onDialogLinkClicked(this.showUpdatesDialog.bind(this))} + onClick={this.onLinkClicked(this.showUpdatesDialog.bind(this))} > @@ -349,7 +349,7 @@ class HomeRoot extends Component { className={styles.link} rel="noopener noreferrer" href="#" - onClick={this.onDialogLinkClicked(this.showReportDialog.bind(this))} + onClick={this.onLinkClicked(this.showReportDialog.bind(this))} > From b2ca98ad7fed1a9e0bc147465c2ce3eed19d5040 Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Thu, 3 Jan 2019 15:31:44 -0800 Subject: [PATCH 06/47] Fix signin/out on home page --- src/react-components/home-root.js | 69 ++++++++++--------------------- src/utils/auth-channel.js | 19 ++++++++- 2 files changed, 39 insertions(+), 49 deletions(-) diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js index 065e251081..51fea8bd21 100644 --- a/src/react-components/home-root.js +++ b/src/react-components/home-root.js @@ -53,11 +53,10 @@ class HomeRoot extends Component { constructor(props) { super(props); this.state.signedIn = props.authChannel.authenticated; - this.state.email = props.store.state.credentials.email; + this.state.email = props.authChannel.email; } componentDidMount() { - this.closeDialog = this.closeDialog.bind(this); if (this.props.authVerify) { this.showAuthDialog(true); this.verifyAuth().then(this.showAuthDialog); @@ -88,8 +87,14 @@ class HomeRoot extends Component { channel.push("auth_verified", { token: this.props.authToken }); } + showDialog = (DialogClass, props = {}) => { + this.setState({ + dialog: + }); + }; + showAuthDialog = verifying => { - this.setState({ dialog: }); + this.showDialog(AuthDialog, { closable: false, verifying, authOrigin: this.props.authOrigin }); }; loadHomeVideo = () => { @@ -98,52 +103,22 @@ class HomeRoot extends Component { playVideoWithStopOnBlur(videoEl); }; - closeDialog() { + closeDialog = () => { this.setState({ dialog: null }); - } - - showJoinUsDialog() { - this.setState({ dialog: }); - } - - showReportDialog() { - this.setState({ dialog: }); - } + }; - showUpdatesDialog() { - this.setState({ - dialog: this.showEmailSubmittedDialog()} /> - }); - } + showJoinUsDialog = () => this.showDialog(JoinUsDialog); - showEmailSubmittedDialog() { - this.setState({ - dialog: ( - - Great! Please check your e-mail to confirm your subscription. - - ) - }); - } + showReportDialog = () => this.showDialog(ReportDialog); - showSignInDialog = () => { - this.showDialog(SignInDialog, { - message: messages["sign-in.prompt"], - onSignIn: async email => { - const { authComplete } = await this.props.authChannel.startAuthentication(email); - this.showDialog(SignInDialog, { authStarted: true }); - await authComplete; - this.setState({ signedIn: true, email }); - this.closeDialog(); + showUpdatesDialog = () => + this.showDialog(UpdatesDialog, { + onSubmittedEmail: () => { + this.showDialog( + Great! Please check your e-mail to confirm your subscription. + ); } }); - }; - - signOut = () => { - this.props.authChannel.removeCredentials(); - // TODO BP - should randomize avatar and display name on sign out. - this.setState({ signedIn: false }); - }; showSignInDialog = () => { this.showDialog(SignInDialog, { @@ -159,7 +134,7 @@ class HomeRoot extends Component { }; signOut = () => { - this.props.authChannel.removeCredentials(); + this.props.authChannel.signOut(); // TODO BP - should randomize avatar and display name on sign out. this.setState({ signedIn: false }); }; @@ -329,7 +304,7 @@ class HomeRoot extends Component { className={styles.link} rel="noopener noreferrer" href="#" - onClick={this.onLinkClicked(this.showJoinUsDialog.bind(this))} + onClick={this.onLinkClicked(this.showJoinUsDialog)} > @@ -339,7 +314,7 @@ class HomeRoot extends Component { className={styles.link} rel="noopener noreferrer" href="#" - onClick={this.onLinkClicked(this.showUpdatesDialog.bind(this))} + onClick={this.onLinkClicked(this.showUpdatesDialog)} > @@ -349,7 +324,7 @@ class HomeRoot extends Component { className={styles.link} rel="noopener noreferrer" href="#" - onClick={this.onLinkClicked(this.showReportDialog.bind(this))} + onClick={this.onLinkClicked(this.showReportDialog)} > diff --git a/src/utils/auth-channel.js b/src/utils/auth-channel.js index b50f7d1fbc..0574c21b9f 100644 --- a/src/utils/auth-channel.js +++ b/src/utils/auth-channel.js @@ -4,15 +4,27 @@ export default class AuthChannel { constructor(store) { this.store = store; this.socket = null; + this._authenticated = !!this.store.state.credentials.token; } setSocket = socket => { this.socket = socket; }; + get email() { + return this.store.state.credentials.email; + } + + get authenticated() { + return this._authenticated; + } + signOut = async hubChannel => { - await hubChannel.signOut(); + if (hubChannel) { + await hubChannel.signOut(); + } this.store.update({ credentials: { token: null, email: null } }); + this._authenticated = false; }; async startAuthentication(email, hubChannel) { @@ -27,7 +39,10 @@ export default class AuthChannel { const authComplete = new Promise(resolve => channel.on("auth_credentials", async ({ credentials: token }) => { this.store.update({ credentials: { email, token } }); - await hubChannel.signIn(token); + if (hubChannel) { + await hubChannel.signIn(token); + } + this._authenticated = true; resolve(); }) ); From 5ea75a8d514adeaf2ce793c167f7e2ec8cfe1024 Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Thu, 3 Jan 2019 16:29:54 -0800 Subject: [PATCH 07/47] Send auth token in hub channel join payload --- src/hub.js | 10 ++++------ src/utils/hub-channel.js | 2 +- src/utils/phoenix-utils.js | 2 ++ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/hub.js b/src/hub.js index 45c19c7bf5..3e1481f32f 100644 --- a/src/hub.js +++ b/src/hub.js @@ -519,6 +519,10 @@ document.addEventListener("DOMContentLoaded", async () => { const pushSubscriptionEndpoint = await subscriptions.getCurrentEndpoint(); const joinPayload = { profile: store.state.profile, push_subscription_endpoint: pushSubscriptionEndpoint, context }; + const { token } = store.state.credentials; + if (token) { + joinPayload.token = token; + } const hubPhxChannel = socket.channel(`hub:${hubId}`, joinPayload); const presenceLogEntries = []; @@ -547,12 +551,6 @@ document.addEventListener("DOMContentLoaded", async () => { .join() .receive("ok", async data => { hubChannel.setPhoenixChannel(hubPhxChannel); - - const { token } = store.state.credentials; - if (token) { - await hubChannel.signIn(token); - } - subscriptions.setHubChannel(hubChannel); subscriptions.setSubscribed(data.subscriptions.web_push); remountUI({ initialIsSubscribed: subscriptions.isSubscribed(), roles: data.roles }); diff --git a/src/utils/hub-channel.js b/src/utils/hub-channel.js index c3b0e31546..e9064cc117 100644 --- a/src/utils/hub-channel.js +++ b/src/utils/hub-channel.js @@ -12,7 +12,7 @@ function isSameDay(da, db) { export default class HubChannel { constructor(store) { this.store = store; - this._signedIn = false; + this._signedIn = this.store.state.credentials.token; } get signedIn() { diff --git a/src/utils/phoenix-utils.js b/src/utils/phoenix-utils.js index 2a40de8842..255ee69679 100644 --- a/src/utils/phoenix-utils.js +++ b/src/utils/phoenix-utils.js @@ -1,6 +1,8 @@ import uuid from "uuid/v4"; import { Socket } from "phoenix"; +import Store from "../storage/store"; + export function connectToReticulum(debug = false) { const qs = new URLSearchParams(location.search); From 513b565a8c6ae5ea14fbe0dfa165d5441824a203 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Fri, 4 Jan 2019 02:10:19 +0000 Subject: [PATCH 08/47] Avatar selection using routes --- package-lock.json | 82 +++++++++++++++++++++ package.json | 2 + src/assets/stylesheets/entry.scss | 2 +- src/hub.js | 41 ++++++++--- src/react-components/profile-entry-panel.js | 11 ++- src/react-components/ui-root.js | 46 +++++++----- 6 files changed, 153 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2738454127..2edad72295 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6430,6 +6430,28 @@ "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", "dev": true }, + "history": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz", + "integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==", + "requires": { + "invariant": "^2.2.1", + "loose-envify": "^1.2.0", + "resolve-pathname": "^2.2.0", + "value-equal": "^0.4.0", + "warning": "^3.0.0" + }, + "dependencies": { + "warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", + "requires": { + "loose-envify": "^1.0.0" + } + } + } + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -6441,6 +6463,11 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoist-non-react-statics": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" + }, "home-or-tmp": { "version": "2.0.0", "resolved": "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz", @@ -10655,6 +10682,43 @@ "tlds": "^1.57.0" } }, + "react-router": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz", + "integrity": "sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg==", + "requires": { + "history": "^4.7.2", + "hoist-non-react-statics": "^2.5.0", + "invariant": "^2.2.4", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.1", + "warning": "^4.0.1" + }, + "dependencies": { + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "react-router-dom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz", + "integrity": "sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==", + "requires": { + "history": "^4.7.2", + "invariant": "^2.2.4", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.1", + "react-router": "^4.3.1", + "warning": "^4.0.1" + } + }, "react-select": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/react-select/-/react-select-1.3.0.tgz", @@ -11173,6 +11237,11 @@ "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", "dev": true }, + "resolve-pathname": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", + "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==" + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz", @@ -13442,6 +13511,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "value-equal": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz", + "integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz", @@ -13541,6 +13615,14 @@ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" }, + "warning": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz", + "integrity": "sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "watchpack": { "version": "1.6.0", "resolved": "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz", diff --git a/package.json b/package.json index baf374b1da..dae09ab2e5 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,8 @@ "react-infinite-scroller": "^1.2.2", "react-intl": "^2.4.0", "react-linkify": "^0.2.2", + "react-router": "^4.3.1", + "react-router-dom": "^4.3.1", "react-youtube": "^7.8.0", "screenfull": "^3.3.2", "super-hands": "github:mozillareality/aframe-super-hands-component#feature/drawing", diff --git a/src/assets/stylesheets/entry.scss b/src/assets/stylesheets/entry.scss index fdafd76260..87e6271b64 100644 --- a/src/assets/stylesheets/entry.scss +++ b/src/assets/stylesheets/entry.scss @@ -120,7 +120,7 @@ @extend %default-font; font-size: 1.1em; color: $action-color; - cursor: pointer; + text-decoration: none; font-weight: bold; display: flex; align-items: center; diff --git a/src/hub.js b/src/hub.js index 91db89e8d0..b727912118 100644 --- a/src/hub.js +++ b/src/hub.js @@ -84,6 +84,7 @@ import "./components/open-media-button"; import ReactDOM from "react-dom"; import React from "react"; +import { HashRouter, BrowserRouter, Route, Link } from "react-router-dom"; import UIRoot from "./react-components/ui-root"; import AuthChannel from "./utils/auth-channel"; import HubChannel from "./utils/hub-channel"; @@ -214,18 +215,36 @@ function mountUI(props = {}) { const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi"); const forcedVREntryType = qs.get("vr_entry_type"); + const Router = + process.env.RETICULUM_SERVER && process.env.RETICULUM_SERVER !== document.location.host + ? HashRouter + : BrowserRouter; + + // Hub ID and slug are the basename + const routerBaseName = document.location.pathname + .split("/") + .slice(0, 3) + .join("/"); + ReactDOM.render( - , + + ( + + )} + /> + , document.getElementById("ui-root") ); } diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js index ba81fc53b0..be3892ee77 100644 --- a/src/react-components/profile-entry-panel.js +++ b/src/react-components/profile-entry-panel.js @@ -13,7 +13,9 @@ class ProfileEntryPanel extends Component { store: PropTypes.object, messages: PropTypes.object, finished: PropTypes.func, - intl: PropTypes.object + intl: PropTypes.object, + history: PropTypes.object, + location: PropTypes.object }; constructor(props) { @@ -44,7 +46,14 @@ class ProfileEntryPanel extends Component { avatarId: this.state.avatarId } }); + this.props.finished(); + this.props.history.goBack(); + + // We may need to go to a new path after saving. + if (this.props.location.state.postPushPath) { + this.props.history.push(this.props.location.state.postPushPath); + } }; stopPropagation = e => { diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index a70b98e119..e9115d1677 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -2,6 +2,7 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import classNames from "classnames"; import copy from "copy-to-clipboard"; +import { Route, Link } from "react-router-dom"; import { VR_DEVICE_AVAILABILITY } from "../utils/vr-caps-detect"; import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl"; import en from "react-intl/locale-data/en"; @@ -56,6 +57,9 @@ const ENTRY_STEPS = { finished: "finished" }; +// This needs to be updated as we add modal routes. +const MODAL_ROUTER_PATHS = ["/profile"]; + // 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, @@ -121,7 +125,8 @@ class UIRoot extends Component { signInCompleteMessageId: PropTypes.string, signInContinueTextId: PropTypes.string, onContinueAfterSignIn: PropTypes.func, - showSafariMicDialog: PropTypes.bool + showSafariMicDialog: PropTypes.bool, + location: PropTypes.object }; state = { @@ -159,7 +164,6 @@ class UIRoot extends Component { exited: false, - showProfileEntry: false, pendingMessage: "", signedIn: false, videoShareMediaSource: null @@ -307,12 +311,6 @@ class UIRoot extends Component { }; handleStartEntry = () => { - const promptForNameAndAvatarBeforeEntry = !this.props.store.state.activity.hasChangedName; - - if (promptForNameAndAvatarBeforeEntry) { - this.setState({ showProfileEntry: true }); - } - if (!this.props.forcedVREntryType) { this.goToEntryStep(ENTRY_STEPS.device); } else if (this.props.forcedVREntryType.startsWith("daydream")) { @@ -533,7 +531,6 @@ class UIRoot extends Component { }; onProfileFinished = () => { - this.setState({ showProfileEntry: false }); this.props.hubChannel.sendProfileUpdate(); }; @@ -821,6 +818,7 @@ class UIRoot extends Component { const pendingMessageTextareaHeight = textRows * 28 + "px"; const pendingMessageFieldHeight = textRows * 28 + 20 + "px"; const hasPush = navigator.serviceWorker && "PushManager" in window; + const promptForNameAndAvatarBeforeEntry = !this.props.store.state.activity.hasChangedName; return (
@@ -837,10 +835,10 @@ class UIRoot extends Component {
-
this.setState({ showProfileEntry: true })} className={entryStyles.profileName}> +
{this.props.store.state.profile.displayName}
-
+
@@ -884,12 +882,16 @@ class UIRoot extends Component {
- +
@@ -1102,6 +1104,10 @@ class UIRoot extends Component { ); }; + isInModal = () => { + return !!MODAL_ROUTER_PATHS.find(p => this.props.location.pathname.startsWith(p)); + }; + render() { const isExited = this.state.exited || this.props.roomUnavailableReason || this.props.platformUnsupportedReason; const isLoading = @@ -1136,7 +1142,7 @@ class UIRoot extends Component { const dialogBoxContentsClassNames = classNames({ [styles.uiInteractive]: !this.state.dialog, [styles.uiDialogBoxContents]: true, - [styles.backgrounded]: this.state.showProfileEntry + [styles.backgrounded]: this.isInModal() }); const entryFinished = this.state.entryStep === ENTRY_STEPS.finished; @@ -1145,15 +1151,19 @@ class UIRoot extends Component { const textRows = this.state.pendingMessage.split("\n").length; const pendingMessageTextareaHeight = textRows * 28 + "px"; const pendingMessageFieldHeight = textRows * 28 + 20 + "px"; + return (
{this.state.dialog} - {this.state.showProfileEntry && ( - - )} + ( + + )} + /> {(!entryFinished || this.isWaitingForAutoExit()) && (
From f949561fffcb27a186752028f8d68abc6e4a1f60 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Fri, 4 Jan 2019 17:44:38 +0000 Subject: [PATCH 09/47] Start working on entry flow paths --- src/hub.js | 6 +++--- src/react-components/ui-root.js | 14 ++++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/hub.js b/src/hub.js index b727912118..4d1325871f 100644 --- a/src/hub.js +++ b/src/hub.js @@ -84,7 +84,7 @@ import "./components/open-media-button"; import ReactDOM from "react-dom"; import React from "react"; -import { HashRouter, BrowserRouter, Route, Link } from "react-router-dom"; +import { HashRouter, BrowserRouter, Route } from "react-router-dom"; import UIRoot from "./react-components/ui-root"; import AuthChannel from "./utils/auth-channel"; import HubChannel from "./utils/hub-channel"; @@ -231,7 +231,6 @@ function mountUI(props = {}) { ( )} diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index e9115d1677..26c735cc18 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -126,11 +126,11 @@ class UIRoot extends Component { signInContinueTextId: PropTypes.string, onContinueAfterSignIn: PropTypes.func, showSafariMicDialog: PropTypes.bool, - location: PropTypes.object + location: PropTypes.object, + history: PropTypes.object }; state = { - entryStep: ENTRY_STEPS.start, enterInVR: false, dialog: null, showInviteDialog: false, @@ -311,9 +311,7 @@ class UIRoot extends Component { }; handleStartEntry = () => { - if (!this.props.forcedVREntryType) { - this.goToEntryStep(ENTRY_STEPS.device); - } else if (this.props.forcedVREntryType.startsWith("daydream")) { + if (this.props.forcedVREntryType.startsWith("daydream")) { this.enterDaydream(); } else if (this.props.forcedVREntryType.startsWith("vr")) { this.enterVR(); @@ -408,7 +406,7 @@ class UIRoot extends Component { await this.setMediaStreamToDefault(); this.beginOrSkipAudioSetup(); } else { - this.goToEntryStep(ENTRY_STEPS.mic_grant); + this.props.history.push("/mic_grant"); } }; @@ -517,11 +515,11 @@ class UIRoot extends Component { }; onMicGrantButton = async () => { - if (this.state.entryStep == ENTRY_STEPS.mic_grant) { + if (this.props.location.pathname === "/mic_grant") { const { hasAudio } = await this.setMediaStreamToDefault(); if (hasAudio) { - this.goToEntryStep(ENTRY_STEPS.mic_granted); + this.props.history.push("/mic_granted"); } else { this.beginOrSkipAudioSetup(); } From 0be4bdc0ecba63f1599cede9a276d3347fe9db19 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sat, 5 Jan 2019 00:05:46 +0000 Subject: [PATCH 10/47] Entry flow now using HTML history --- src/assets/stylesheets/entry.scss | 15 ++++ src/react-components/ui-root.js | 132 +++++++++++++++++++----------- 2 files changed, 98 insertions(+), 49 deletions(-) diff --git a/src/assets/stylesheets/entry.scss b/src/assets/stylesheets/entry.scss index 87e6271b64..9086e5a3fa 100644 --- a/src/assets/stylesheets/entry.scss +++ b/src/assets/stylesheets/entry.scss @@ -204,3 +204,18 @@ } } } + +:local(.back) { + @extend %fa-icon-button; + + display: flex; + position: absolute; + top: 12px; + left: 26px; + font-size: 1.0em; + font-weight: bold; + + i { + margin-right: 18px; + } +} diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 26c735cc18..699084ccfe 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -2,7 +2,7 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import classNames from "classnames"; import copy from "copy-to-clipboard"; -import { Route, Link } from "react-router-dom"; +import { Route, Switch, Link } from "react-router-dom"; import { VR_DEVICE_AVAILABILITY } from "../utils/vr-caps-detect"; import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl"; import en from "react-intl/locale-data/en"; @@ -45,18 +45,10 @@ import { faUsers } from "@fortawesome/free-solid-svg-icons/faUsers"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faQuestion } from "@fortawesome/free-solid-svg-icons/faQuestion"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons/faInfoCircle"; +import { faArrowLeft } from "@fortawesome/free-solid-svg-icons/faArrowLeft"; addLocaleData([...en]); -const ENTRY_STEPS = { - start: "start", - device: "device", - mic_grant: "mic_grant", - mic_granted: "mic_granted", - audio_setup: "audio_setup", - finished: "finished" -}; - // This needs to be updated as we add modal routes. const MODAL_ROUTER_PATHS = ["/profile"]; @@ -76,6 +68,7 @@ async function grantedMicLabels() { } const AUTO_EXIT_TIMER_SECONDS = 10; +const ENTRY_FLOW_PATHS = ["/device", "/audio", "/mic_grant", "/mic_granted"]; import webmTone from "../assets/sfx/tone.webm"; import mp3Tone from "../assets/sfx/tone.mp3"; @@ -132,6 +125,8 @@ class UIRoot extends Component { state = { enterInVR: false, + entered: false, + lastEntryStepPath: null, dialog: null, showInviteDialog: false, showLinkDialog: false, @@ -199,6 +194,36 @@ class UIRoot extends Component { this.props.scene.addEventListener("share_video_disabled", this.onShareVideoDisabled); this.props.scene.addEventListener("exit", this.exit); const scene = this.props.scene; + + let preEntryHistoryLength = 0; + + // If we landed on the page with a path in the middle of the entry flow (eg we refreshed the + // page on the audio setup dialog) then reset the history entry to /. + // + // Note this isn't perfect, if we refresh the page mid-entry flow and then hit back, we end + // up in a bad state unless we were on the first step. But this seems reasonable enough for now. + if (ENTRY_FLOW_PATHS.find(x => x === this.props.history.location.pathname)) { + this.props.history.replace("/"); + } + + // Hacky technique to skip over the entry flow history entries when we've entered the room. + // + // This makes it so if we are in the room and hit back in the browser, we go to the page + // we were on before the entry flow, not back into the entry flow. + this.props.history.listen((newLocation, action) => { + if (!this.state.entered) { + preEntryHistoryLength++; + return; + } + + // Going back through entry flow, skip over it. + if (action === "POP" && newLocation.pathname === this.state.lastEntryStepPath) { + setTimeout(() => { + for (let i = 0; i < preEntryHistoryLength; i++) this.props.history.goBack(); + }, 0); + } + }); + this.setState({ audioContext: { playSound: sound => { @@ -320,10 +345,6 @@ class UIRoot extends Component { } }; - goToEntryStep = entryStep => { - this.setState({ entryStep: entryStep, showInviteDialog: false }); - }; - playTestTone = () => { toneClip.currentTime = 0; toneClip.play(); @@ -534,7 +555,7 @@ class UIRoot extends Component { beginOrSkipAudioSetup = () => { if (!this.props.forcedVREntryType || !this.props.forcedVREntryType.endsWith("_now")) { - this.goToEntryStep(ENTRY_STEPS.audio_setup); + this.props.history.push("/audio"); } else { this.onAudioReadyButton(); } @@ -611,7 +632,8 @@ class UIRoot extends Component { clearInterval(this.state.micUpdateInterval); } - this.goToEntryStep(ENTRY_STEPS.finished); + this.setState({ entered: true, lastEntryStepPath: this.props.history.location.pathname, showInviteDialog: false }); + this.props.history.push("/"); }; attemptLink = async () => { @@ -899,6 +921,13 @@ class UIRoot extends Component { renderDevicePanel = () => { return (
+
this.props.history.goBack()} className={entryStyles.back}> + + + + Back +
+
@@ -945,31 +974,34 @@ class UIRoot extends Component { ); }; - renderMicPanel = () => { + renderMicPanel = granted => { return (
+
this.props.history.goBack()} className={entryStyles.back}> + + + + Back +
+
- +
- +
- {this.state.entryStep == ENTRY_STEPS.mic_grant ? ( + {granted ? ( ) : ( )} @@ -995,6 +1027,13 @@ class UIRoot extends Component { const subtitleId = AFRAME.utils.device.isMobile() ? "audio.subtitle-mobile" : "audio.subtitle-desktop"; return (
+
this.props.history.goBack()} className={entryStyles.back}> + + + + Back +
+
@@ -1116,24 +1155,20 @@ class UIRoot extends Component { if (isLoading) return this.renderLoader(); if (this.props.isBotMode) return this.renderBotMode(); - const startPanel = this.state.entryStep === ENTRY_STEPS.start && this.renderEntryStartPanel(); - const devicePanel = this.state.entryStep === ENTRY_STEPS.device && this.renderDevicePanel(); - - const micPanel = - (this.state.entryStep === ENTRY_STEPS.mic_grant || this.state.entryStep === ENTRY_STEPS.mic_granted) && - this.renderMicPanel(); - - const audioSetupPanel = this.state.entryStep === ENTRY_STEPS.audio_setup && this.renderAudioSetupPanel(); + const entered = this.state.entered; // Dialog is empty if coll - const dialogContents = this.isWaitingForAutoExit() ? ( + const entryDialog = this.isWaitingForAutoExit() ? ( ) : (
- {startPanel} - {devicePanel} - {micPanel} - {audioSetupPanel} + + {this.renderDevicePanel()} + {this.renderMicPanel(false)} + {this.renderMicPanel(true)} + {this.renderAudioSetupPanel()} + {this.renderEntryStartPanel()} +
); @@ -1143,8 +1178,7 @@ class UIRoot extends Component { [styles.backgrounded]: this.isInModal() }); - const entryFinished = this.state.entryStep === ENTRY_STEPS.finished; - const showVREntryButton = entryFinished && this.props.availableVREntryTypes.isInHMD; + const showVREntryButton = entered && this.props.availableVREntryTypes.isInHMD; const textRows = this.state.pendingMessage.split("\n").length; const pendingMessageTextareaHeight = textRows * 28 + "px"; @@ -1163,17 +1197,17 @@ class UIRoot extends Component { )} /> - {(!entryFinished || this.isWaitingForAutoExit()) && ( + {(!this.state.entered || this.isWaitingForAutoExit()) && (
-
{dialogContents}
+
{entryDialog}
)} - {entryFinished && ( + {entered && ( )} - {entryFinished && ( + {entered && (
{this.state.pendingMessage.startsWith("/") && ( @@ -1230,7 +1264,7 @@ class UIRoot extends Component {
@@ -1238,7 +1272,7 @@ class UIRoot extends Component { !this.state.videoShareMediaSource && ( diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index e3fe7ba166..706d0c7c3e 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -50,7 +50,17 @@ import { faArrowLeft } from "@fortawesome/free-solid-svg-icons/faArrowLeft"; addLocaleData([...en]); // This needs to be updated as we add modal routes. -const MODAL_ROUTER_PATHS = ["/profile"]; +const MODAL_ROUTER_PATHS = [ + "/profile", + "/link", + "/help", + "/safari", + "/support", + "/create", + "/webvr", + "/webrtc-screenshare", + "/info" +]; // This is a list of regexes that match the microphone labels of HMDs. // @@ -258,17 +268,17 @@ class UIRoot extends Component { onContinueAfterSignIn(); }; - this.showDialog(SignInDialog, { + this.showNonHistoriedDialog(SignInDialog, { message: messages[signInMessageId], onSignIn: async email => { const { authComplete } = await authChannel.startAuthentication(email, this.props.hubChannel); - this.showDialog(SignInDialog, { authStarted: true, onClose: closeAndContinue }); + this.showNonHistoriedDialog(SignInDialog, { authStarted: true, onClose: closeAndContinue }); await authComplete; this.setState({ signedIn: true }); - this.showDialog(SignInDialog, { + this.showNonHistoriedDialog(SignInDialog, { authComplete: true, message: messages[signInCompleteMessageId], continueText: messages[signInContinueTextId], @@ -442,7 +452,7 @@ class UIRoot extends Component { if (this.props.forcedVREntryType || this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.maybe) { await this.performDirectEntryFlow(true); } else { - this.showWebVRRecommendDialog(); + this.props.history.push("/webvr"); } }; @@ -658,36 +668,37 @@ class UIRoot extends Component { this.props.scene.emit("add_media", media); }; - closeDialog = () => { - this.setState({ dialog: null }); + closeDialog = success => { + if (this.state.dialog) { + this.setState({ dialog: null }); + } else { + // If dialog was successful (eg user hit "OK") then move forward in history, o/w go back. + // + // This makes it so if you create an object, back will re-show the create object dialog, + // but if you cancel, it will not. + if (success) { + this.props.history.push("/"); + } else { + this.props.history.goBack(); + } + } }; - showDialog = (DialogClass, props = {}) => { + showNonHistoriedDialog = (DialogClass, props = {}) => { this.setState({ dialog: }); }; - showHelpDialog = () => this.showDialog(HelpDialog); - - showSafariDialog = () => this.showDialog(SafariDialog); - - showInviteTeamDialog = () => this.showDialog(InviteTeamDialog, { hubChannel: this.props.hubChannel }); - - showCreateObjectDialog = () => this.showDialog(CreateObjectDialog, { onCreate: this.createObject }); - - showWebVRRecommendDialog = () => this.showDialog(WebVRRecommendDialog); - - showRoomInfoDialog = () => - this.showDialog(RoomInfoDialog, { scene: this.props.hubScene, hubName: this.props.hubName }); + renderDialog = (DialogClass, props = {}) => ; showSignInDialog = () => { - this.showDialog(SignInDialog, { + this.showNonHistoriedDialog(SignInDialog, { message: messages["sign-in.prompt"], onSignIn: async email => { const { authComplete } = await this.props.authChannel.startAuthentication(email, this.props.hubChannel); - this.showDialog(SignInDialog, { authStarted: true }); + this.showNonHistoriedDialog(SignInDialog, { authStarted: true }); await authComplete; @@ -703,7 +714,7 @@ class UIRoot extends Component { }; showWebRTCScreenshareUnsupportedDialog = () => { - this.setState({ dialog: }); + this.props.history.push("/webrtc-screenshare"); }; onMiniInviteClicked = () => { @@ -848,11 +859,11 @@ class UIRoot extends Component {
{this.props.hubName} {this.props.hubScene && ( - this.showRoomInfoDialog()} className={entryStyles.collapse}> + - + )}
@@ -948,7 +959,9 @@ class UIRoot extends Component { isInHMD={this.props.availableVREntryTypes.isInHMD} /> {this.props.availableVREntryTypes.safari === VR_DEVICE_AVAILABILITY.maybe && ( - + + + )} {this.props.availableVREntryTypes.screen === VR_DEVICE_AVAILABILITY.yes && ( @@ -1176,7 +1189,7 @@ class UIRoot extends Component { ); const dialogBoxContentsClassNames = classNames({ - [styles.uiInteractive]: !this.state.dialog, + [styles.uiInteractive]: !this.isInModal(), [styles.uiDialogBoxContents]: true, [styles.backgrounded]: this.isInModal() }); @@ -1200,6 +1213,25 @@ class UIRoot extends Component { )} /> + this.renderDialog(HelpDialog)} /> + this.renderDialog(SafariDialog)} /> + this.renderDialog(InviteTeamDialog, { hubChannel: this.props.hubChannel })} + /> + this.renderDialog(CreateObjectDialog, { onCreate: this.createObject })} + /> + this.renderDialog(WebVRRecommendDialog)} /> + this.renderDialog(WebRTCScreenshareUnsupportedDialog)} /> + + this.renderDialog(RoomInfoDialog, { scene: this.props.hubScene, hubName: this.props.hubName }) + } + /> + {(!this.state.entered || this.isWaitingForAutoExit()) && (
@@ -1255,7 +1287,7 @@ class UIRoot extends Component { spawnChatMessage(this.state.pendingMessage); this.setState({ pendingMessage: "" }); } else { - this.showCreateObjectDialog(); + this.props.history.push("/create"); } }} /> @@ -1330,11 +1362,13 @@ class UIRoot extends Component { /> - + + +
this.showWebRTCScreenshareUnsupportedDialog()} /> - {this.props.isSupportAvailable && ( + {(this.props.isSupportAvailable || true) && (
- + + +
)} {!this.isWaitingForAutoExit() && ( this.showCreateObjectDialog()} showPhotoPicker={AFRAME.utils.device.isMobile()} onMediaPicked={this.createObject} /> From 70d15feaf3854215dfc95ccb459ccf7c69639c21 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sat, 5 Jan 2019 01:05:06 +0000 Subject: [PATCH 14/47] Turn off testing for support --- src/react-components/ui-root.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 706d0c7c3e..22cafd84e4 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -1409,7 +1409,7 @@ class UIRoot extends Component { onEndShareVideo={this.endShareVideo} onShareVideoNotCapable={() => this.showWebRTCScreenshareUnsupportedDialog()} /> - {(this.props.isSupportAvailable || true) && ( + {this.props.isSupportAvailable && (
From 9c9f3a4e97cc0d22e7cfa6440f30ccf8e030aa4c Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sat, 5 Jan 2019 01:10:43 +0000 Subject: [PATCH 15/47] Refactor const names --- src/react-components/ui-root.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index aee475f30b..a1825c5b4b 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -62,6 +62,8 @@ const MODAL_ROUTER_PATHS = [ "/info" ]; +const ENTRY_FLOW_ROUTES = ["/device", "/audio", "/mic_grant", "/mic_granted", "/link"]; + // 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, @@ -78,7 +80,6 @@ async function grantedMicLabels() { } const AUTO_EXIT_TIMER_SECONDS = 10; -const ENTRY_FLOW_PATHS = ["/device", "/audio", "/mic_grant", "/mic_granted", "/link"]; import webmTone from "../assets/sfx/tone.webm"; import mp3Tone from "../assets/sfx/tone.mp3"; @@ -211,7 +212,7 @@ class UIRoot extends Component { // // Note this isn't perfect, if we refresh the page mid-entry flow and then hit back, we end // up in a bad state unless we were on the first step. But this seems reasonable enough for now. - if (ENTRY_FLOW_PATHS.find(x => x === this.props.history.location.pathname)) { + if (ENTRY_FLOW_ROUTES.find(x => x === this.props.history.location.pathname)) { this.props.history.replace("/"); } From 04d48c497c7463bfa25396ae05b59ca1cb944fb7 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sat, 5 Jan 2019 01:11:38 +0000 Subject: [PATCH 16/47] Fix up comments --- src/react-components/ui-root.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index a1825c5b4b..0136390175 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -216,10 +216,11 @@ class UIRoot extends Component { this.props.history.replace("/"); } - // Hacky technique to skip over the entry flow history entries when we've entered the room. + // Hacky technique to skip over the entry flow history entries when we've entered the room and + // hit the back button. // - // This makes it so if we are in the room and hit back in the browser, we go to the page - // we were on before the entry flow, not back into the entry flow. + // This makes it so if we are in the room and hit back in the browser, we go to the URL + // the browser was on before the entry flow, not back into the entry flow. this.props.history.listen((newLocation, action) => { if (!this.state.entered) { preEntryHistoryLength++; From 28c376799b167db16d3deae2eb35e0a52d934aa0 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sat, 5 Jan 2019 01:19:34 +0000 Subject: [PATCH 17/47] Refactor rename constant --- src/react-components/ui-root.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 0136390175..66adf37272 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -50,7 +50,7 @@ import { faArrowLeft } from "@fortawesome/free-solid-svg-icons/faArrowLeft"; addLocaleData([...en]); // This needs to be updated as we add modal routes. -const MODAL_ROUTER_PATHS = [ +const MODAL_ROUTES = [ "/profile", "/link", "/help", @@ -1160,7 +1160,7 @@ class UIRoot extends Component { }; isInModal = () => { - return !!MODAL_ROUTER_PATHS.find(p => this.props.location.pathname.startsWith(p)); + return !!MODAL_ROUTES.find(p => this.props.location.pathname.startsWith(p)); }; render() { From f5549d6768a8b12958a117852722d13be507cd1a Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sat, 5 Jan 2019 01:37:25 +0000 Subject: [PATCH 18/47] Translations for Back --- src/assets/translations.data.json | 1 + src/react-components/ui-root.js | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index e42ee3d8fe..9d4574cab2 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -40,6 +40,7 @@ "entry.return-to-vr": "Return to VR", "entry.enter-in-vr": "Enter in VR", "entry.lobby": "Lobby", + "entry.back": "Back", "entry.notify_me": "Notify me when others are here", "profile.save": "Accept", "profile.display_name.validation_warning": "Alphanumerics and hyphens. At least 3 characters, no more than 32", diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 66adf37272..fa627e6cdf 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -941,7 +941,7 @@ class UIRoot extends Component { - Back +
@@ -999,7 +999,7 @@ class UIRoot extends Component { - Back +
@@ -1049,7 +1049,7 @@ class UIRoot extends Component { - Back +
From 9b8f067ede3adadcbf538d78aa91e710bf0b311c Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sat, 5 Jan 2019 02:04:50 +0000 Subject: [PATCH 19/47] Fix race condition on 2d now --- src/react-components/ui-root.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index fa627e6cdf..8432a1f884 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -246,7 +246,7 @@ class UIRoot extends Component { } }); - this.handleForceEntry(); + setTimeout(() => this.handleForceEntry(), 1000); } componentWillUnmount() { From 35f19b14e574cde5410255df8847ea83551e1745 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sat, 5 Jan 2019 02:05:24 +0000 Subject: [PATCH 20/47] Add hacky commands to test out scene update and room rename --- src/assets/translations.data.json | 2 ++ src/message-dispatch.js | 11 +++++++++-- src/react-components/chat-command-help.js | 4 ++-- src/utils/hub-channel.js | 8 ++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 9d4574cab2..4baedfcef0 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -127,6 +127,8 @@ "commands.fly": "Toggle fly mode.", "commands.grow": "Increase your avatar's size.", "commands.shrink": "Decrease your avatar's size.", + "commands.scene": "Change the scene.", + "commands.rename": "Rename the room.", "commands.help": "Show help.", "commands.leave": "Disconnect from the room.", "commands.duck": "The duck tested well. Quack.", diff --git a/src/message-dispatch.js b/src/message-dispatch.js index 1f1594d43a..6062f73d6d 100644 --- a/src/message-dispatch.js +++ b/src/message-dispatch.js @@ -13,14 +13,15 @@ export default class MessageDispatch { dispatch = message => { if (message.startsWith("/")) { - this.dispatchCommand(message.substring(1)); + const commandParts = message.substring(1).split(" "); + this.dispatchCommand(commandParts[0], commandParts[1]); document.activeElement.blur(); // Commands should blur } else { this.hubChannel.sendMessage(message); } }; - dispatchCommand = command => { + dispatchCommand = (command, arg) => { const entered = this.scene.is("entered"); switch (command) { @@ -76,6 +77,12 @@ export default class MessageDispatch { spawnChatMessage(DUCK_URL); this.scene.emit("quack"); break; + case "scene": + this.hubChannel.updateScene(arg); + break; + case "rename": + this.hubChannel.rename(arg); + break; } }; } diff --git a/src/react-components/chat-command-help.js b/src/react-components/chat-command-help.js index 50df7c9e90..cf036ccfda 100644 --- a/src/react-components/chat-command-help.js +++ b/src/react-components/chat-command-help.js @@ -9,7 +9,7 @@ export default class ChatCommandHelp extends Component { }; render() { - const commands = ["help", "leave", "fly", "grow", "shrink", "duck"]; + const commands = ["help", "leave", "fly", "grow", "shrink", "duck", "scene ", "rename "]; return (
@@ -19,7 +19,7 @@ export default class ChatCommandHelp extends Component {
/{c}
- +
) diff --git a/src/utils/hub-channel.js b/src/utils/hub-channel.js index c3b0e31546..12966da396 100644 --- a/src/utils/hub-channel.js +++ b/src/utils/hub-channel.js @@ -96,6 +96,14 @@ export default class HubChannel { this.channel.push("events:profile_updated", { profile: this.store.state.profile }); }; + updateScene = url => { + this.channel.push("update_scene", { url }); + }; + + rename = name => { + this.channel.push("rename", { name }); + }; + subscribe = subscription => { this.channel.push("subscribe", { subscription }); }; From ae8784fc5a7dbbb74aba5325faa77868dee57440 Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Mon, 7 Jan 2019 18:59:52 -0800 Subject: [PATCH 21/47] isOwner instead of roles --- src/hub.js | 2 +- src/react-components/ui-root.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hub.js b/src/hub.js index 3e1481f32f..3da2924a89 100644 --- a/src/hub.js +++ b/src/hub.js @@ -553,7 +553,7 @@ document.addEventListener("DOMContentLoaded", async () => { hubChannel.setPhoenixChannel(hubPhxChannel); subscriptions.setHubChannel(hubChannel); subscriptions.setSubscribed(data.subscriptions.web_push); - remountUI({ initialIsSubscribed: subscriptions.isSubscribed(), roles: data.roles }); + remountUI({ initialIsSubscribed: subscriptions.isSubscribed(), isOwner: data.is_owner }); await handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data); }) .receive("error", res => { diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index bececd8abb..a89d7c60e9 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -109,7 +109,7 @@ class UIRoot extends Component { platformUnsupportedReason: PropTypes.string, hubId: PropTypes.string, hubName: PropTypes.string, - roles: PropTypes.object, + isOwner: PropTypes.bool, isSupportAvailable: PropTypes.bool, presenceLogEntries: PropTypes.array, presences: PropTypes.object, @@ -823,7 +823,7 @@ class UIRoot extends Component { return (
- {this.props.hubName} {this.props.roles.is_host ? "(host)" : ""} + {this.props.hubName} {this.props.isOwner ? "(host)" : ""}
From 309c9e227189705fc026282f8958fc9a8db48255 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Thu, 10 Jan 2019 01:02:59 +0000 Subject: [PATCH 22/47] Remove gltf-bundle component --- src/components/gltf-bundle.js | 39 ------------------------------- src/components/nav-mesh-helper.js | 2 +- src/hub.js | 25 +++++++++++--------- 3 files changed, 15 insertions(+), 51 deletions(-) delete mode 100644 src/components/gltf-bundle.js diff --git a/src/components/gltf-bundle.js b/src/components/gltf-bundle.js deleted file mode 100644 index 360a0cb3ca..0000000000 --- a/src/components/gltf-bundle.js +++ /dev/null @@ -1,39 +0,0 @@ -// DEPRECATED -/** - * Instantiates GLTF models as specified in a bundle JSON. - * @namespace gltf - * @component gltf-bundle - */ -AFRAME.registerComponent("gltf-bundle", { - schema: { - src: { default: "" } - }, - - init: async function() { - this._addGltfEntitiesForBundleJson = this._addGltfEntitiesForBundleJson.bind(this); - this.baseURL = new URL(THREE.LoaderUtils.extractUrlBase(this.data.src), window.location.href); - - const res = await fetch(this.data.src); - const data = await res.json(); - this._addGltfEntitiesForBundleJson(data); - }, - - _addGltfEntitiesForBundleJson: function(bundleJson) { - const loaded = []; - - for (let i = 0; i < bundleJson.assets.length; i++) { - const asset = bundleJson.assets[i]; - - // TODO: for now just skip resources, eventually we will want to hold on to a reference so that we can use them - if (asset.type === "resource") continue; - - const src = new URL(asset.src, this.baseURL).href; - const gltfEl = document.createElement("a-entity"); - gltfEl.setAttribute("gltf-model-plus", { src, useCache: false, inflate: true }); - loaded.push(new Promise(resolve => gltfEl.addEventListener("model-loaded", resolve))); - this.el.appendChild(gltfEl); - } - - Promise.all(loaded).then(() => this.el.emit("bundleloaded")); - } -}); diff --git a/src/components/nav-mesh-helper.js b/src/components/nav-mesh-helper.js index 876f62a855..e0d1063da7 100644 --- a/src/components/nav-mesh-helper.js +++ b/src/components/nav-mesh-helper.js @@ -10,7 +10,7 @@ AFRAME.registerComponent("nav-mesh-helper", { init: function() { const teleportControls = this.data.teleportControls; - this.el.addEventListener("bundleloaded", () => { + this.el.addEventListener("model-loaded", () => { if (!teleportControls) return; for (let i = 0; i < teleportControls.length; i++) { diff --git a/src/hub.js b/src/hub.js index 4d1325871f..1f922cdad6 100644 --- a/src/hub.js +++ b/src/hub.js @@ -46,7 +46,6 @@ import "./components/offset-relative-to"; import "./components/player-info"; import "./components/debug"; import "./components/hand-poses"; -import "./components/gltf-bundle"; import "./components/hud-controller"; import "./components/freeze-controller"; import "./components/icon-button"; @@ -272,7 +271,6 @@ async function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, isLegacyBundle = false; sceneUrl = hub.scene.model_url; } else { - // Deprecated const defaultSpaceTopic = hub.topics[0]; const glbAsset = defaultSpaceTopic.assets.find(a => a.asset_type === "glb"); const bundleAsset = defaultSpaceTopic.assets.find(a => a.asset_type === "gltf_bundle"); @@ -281,7 +279,6 @@ async function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, isLegacyBundle = !(glbAsset || hasExtension); } - console.log(`Scene URL: ${sceneUrl}`); console.log(`Janus host: ${hub.host}`); const environmentScene = document.querySelector("#environment-scene"); const objectsScene = document.querySelector("#objects-scene"); @@ -290,16 +287,22 @@ async function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, objectsEl.setAttribute("gltf-model-plus", { src: objectsUrl, useCache: false, inflate: true }); objectsScene.appendChild(objectsEl); - if (!isLegacyBundle) { - const gltfEl = document.createElement("a-entity"); - gltfEl.setAttribute("gltf-model-plus", { src: proxiedUrlFor(sceneUrl), useCache: false, inflate: true }); - gltfEl.addEventListener("model-loaded", () => environmentScene.emit("bundleloaded")); - environmentScene.appendChild(gltfEl); - } else { + if (isLegacyBundle) { // Deprecated - environmentScene.setAttribute("gltf-bundle", `src: ${sceneUrl}`); + const res = await fetch(sceneUrl); + const data = await res.json(); + console.log(data); + const baseURL = new URL(THREE.LoaderUtils.extractUrlBase(sceneUrl), window.location.href); + sceneUrl = new URL(data.assets[0].src, baseURL).href; + } else { + sceneUrl = proxiedUrlFor(sceneUrl); } + console.log(`Scene URL: ${sceneUrl}`); + const environmentEl = document.createElement("a-entity"); + environmentEl.setAttribute("gltf-model-plus", { src: sceneUrl, useCache: false, inflate: true }); + environmentScene.appendChild(environmentEl); + remountUI({ hubId: hub.hub_id, hubName: hub.name, @@ -514,7 +517,7 @@ document.addEventListener("DOMContentLoaded", async () => { const environmentScene = document.querySelector("#environment-scene"); - environmentScene.addEventListener("bundleloaded", () => { + environmentScene.addEventListener("model-loaded", () => { remountUI({ environmentSceneLoaded: true }); for (const modelEl of environmentScene.children) { From bf0de6984fc9dd971ebea6342964192d7e311748 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Thu, 10 Jan 2019 05:23:09 +0000 Subject: [PATCH 23/47] Refactor initial load to support changing environment scene URL --- src/hub.js | 94 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/src/hub.js b/src/hub.js index 1f922cdad6..a638d907dd 100644 --- a/src/hub.js +++ b/src/hub.js @@ -156,6 +156,8 @@ import qsTruthy from "./utils/qs_truthy"; const isBotMode = qsTruthy("bot"); const isTelemetryDisabled = qsTruthy("disable_telemetry"); const isDebug = qsTruthy("debug"); +const loadingEnvironmentURL = + "https://hubs-proxy.com/https://uploads-prod.reticulum.io/files/58c034aa-ff17-4d3c-a6cc-c9095bb4822c.glb"; if (!isBotMode && !isTelemetryDisabled) { registerTelemetry(); @@ -253,20 +255,12 @@ function remountUI(props) { mountUI(uiProps); } -async function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data) { - const scene = document.querySelector("a-scene"); - - if (NAF.connection.isConnected()) { - // Send complete sync on phoenix re-join. - NAF.connection.entities.completeSync(null, true); - return; - } - - const hub = data.hubs[0]; - +async function updateEnvironmentForHub(hub) { let sceneUrl; let isLegacyBundle; // Deprecated + const environmentScene = document.querySelector("#environment-scene"); + if (hub.scene) { isLegacyBundle = false; sceneUrl = hub.scene.model_url; @@ -279,19 +273,10 @@ async function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, isLegacyBundle = !(glbAsset || hasExtension); } - console.log(`Janus host: ${hub.host}`); - const environmentScene = document.querySelector("#environment-scene"); - const objectsScene = document.querySelector("#objects-scene"); - const objectsUrl = getReticulumFetchUrl(`/${hub.hub_id}/objects.gltf`); - const objectsEl = document.createElement("a-entity"); - objectsEl.setAttribute("gltf-model-plus", { src: objectsUrl, useCache: false, inflate: true }); - objectsScene.appendChild(objectsEl); - if (isLegacyBundle) { // Deprecated const res = await fetch(sceneUrl); const data = await res.json(); - console.log(data); const baseURL = new URL(THREE.LoaderUtils.extractUrlBase(sceneUrl), window.location.href); sceneUrl = new URL(data.assets[0].src, baseURL).href; } else { @@ -299,9 +284,52 @@ async function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, } console.log(`Scene URL: ${sceneUrl}`); - const environmentEl = document.createElement("a-entity"); - environmentEl.setAttribute("gltf-model-plus", { src: sceneUrl, useCache: false, inflate: true }); - environmentScene.appendChild(environmentEl); + + let environmentEl = null; + + if (environmentScene.childNodes.length === 0) { + const environmentEl = document.createElement("a-entity"); + environmentEl.setAttribute("gltf-model-plus", { src: sceneUrl, useCache: false, inflate: true }); + environmentScene.appendChild(environmentEl); + } else { + // Change environment + environmentEl = environmentScene.childNodes[0]; + + const onLoad = () => { + environmentEl.setAttribute("gltf-model-plus", { src: sceneUrl }); + environmentEl.removeEventListener("model-loaded", onLoad); + }; + + // Clear the three.js image cache an unload the environment, then on next tick load the loading environment + THREE.Cache.clear(); + + environmentEl.setAttribute("gltf-model-plus", { src: null }); + await nextTick(); + + environmentEl.addEventListener("model-loaded", onLoad); + environmentEl.setAttribute("gltf-model-plus", { src: loadingEnvironmentURL }); + } +} + +async function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data) { + const scene = document.querySelector("a-scene"); + + if (NAF.connection.isConnected()) { + // Send complete sync on phoenix re-join. + NAF.connection.entities.completeSync(null, true); + return; + } + + const hub = data.hubs[0]; + + console.log(`Janus host: ${hub.host}`); + const objectsScene = document.querySelector("#objects-scene"); + const objectsUrl = getReticulumFetchUrl(`/${hub.hub_id}/objects.gltf`); + const objectsEl = document.createElement("a-entity"); + objectsEl.setAttribute("gltf-model-plus", { src: objectsUrl, useCache: false, inflate: true }); + objectsScene.appendChild(objectsEl); + + await updateEnvironmentForHub(hub); remountUI({ hubId: hub.hub_id, @@ -516,20 +544,26 @@ document.addEventListener("DOMContentLoaded", async () => { } const environmentScene = document.querySelector("#environment-scene"); + const onFirstEnvironmentLoad = () => { + setupLobbyCamera(); + + // Replace renderer with a noop renderer to reduce bot resource usage. + if (isBotMode) { + runBotMode(scene, entryManager); + } + + environmentScene.removeEventListener("model-loaded", onFirstEnvironmentLoad); + }; + + environmentScene.addEventListener("model-loaded", onFirstEnvironmentLoad); environmentScene.addEventListener("model-loaded", () => { + // This is re-entrant if the environment scene is changed. remountUI({ environmentSceneLoaded: true }); for (const modelEl of environmentScene.children) { addAnimationComponents(modelEl); } - - setupLobbyCamera(); - - // Replace renderer with a noop renderer to reduce bot resource usage. - if (isBotMode) { - runBotMode(scene, entryManager); - } }); const socket = connectToReticulum(isDebug); From 2135cf828d7d99593db271c99f6298dd2ad5af8a Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Fri, 11 Jan 2019 01:36:44 +0000 Subject: [PATCH 24/47] WIP --- src/utils/hub-channel.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils/hub-channel.js b/src/utils/hub-channel.js index 12966da396..624b1a086d 100644 --- a/src/utils/hub-channel.js +++ b/src/utils/hub-channel.js @@ -97,7 +97,9 @@ export default class HubChannel { }; updateScene = url => { - this.channel.push("update_scene", { url }); + this.channel.push("update_scene", { url }).receive("ok", res => { + console.log(res); + }); }; rename = name => { From 2426620c05fb06f24f93d3fc5a4480fd3ace27ee Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Fri, 11 Jan 2019 06:40:52 +0000 Subject: [PATCH 25/47] Scene changing command works --- src/assets/translations.data.json | 2 + src/hub.js | 62 ++++++++++++++++++---------- src/react-components/presence-log.js | 12 ++++++ src/utils/hub-channel.js | 4 +- 4 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 4baedfcef0..416ce0e650 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -73,6 +73,8 @@ "presence.join_lobby": "joined the lobby.", "presence.leave": "left.", "presence.name_change": "is now known as", + "presence.scene_change": "changed the scene to", + "presence.hub_name_change": "changed the name of the room to", "presence.in_lobby": "Lobby", "presence.in_room": "In Room", "home.room_create_options": "options", diff --git a/src/hub.js b/src/hub.js index a638d907dd..444f1daa60 100644 --- a/src/hub.js +++ b/src/hub.js @@ -255,6 +255,20 @@ function remountUI(props) { mountUI(uiProps); } +async function updateUIForHub(hub) { + remountUI({ + hubId: hub.hub_id, + hubName: hub.name, + hubScene: hub.scene, + hubEntryCode: hub.entry_code + }); + + document + .querySelector("#hud-hub-entry-link") + .setAttribute("text", { value: `hub.link/${hub.entry_code}`, width: 1.1, align: "center" }); +} + +// Sets or changes the environment, returns true if the environment was changed, false if its the first update. async function updateEnvironmentForHub(hub) { let sceneUrl; let isLegacyBundle; // Deprecated @@ -291,23 +305,22 @@ async function updateEnvironmentForHub(hub) { const environmentEl = document.createElement("a-entity"); environmentEl.setAttribute("gltf-model-plus", { src: sceneUrl, useCache: false, inflate: true }); environmentScene.appendChild(environmentEl); + return false; } else { // Change environment environmentEl = environmentScene.childNodes[0]; - const onLoad = () => { - environmentEl.setAttribute("gltf-model-plus", { src: sceneUrl }); - environmentEl.removeEventListener("model-loaded", onLoad); - }; - - // Clear the three.js image cache an unload the environment, then on next tick load the loading environment + // Clear the three.js image cache and load the loading environment before switching to the new one. THREE.Cache.clear(); - environmentEl.setAttribute("gltf-model-plus", { src: null }); - await nextTick(); + const onLoadingEnvironmentReady = () => { + environmentEl.setAttribute("gltf-model-plus", { src: sceneUrl }); + environmentEl.removeEventListener("model-loaded", onLoadingEnvironmentReady); + }; - environmentEl.addEventListener("model-loaded", onLoad); + environmentEl.addEventListener("model-loaded", onLoadingEnvironmentReady); environmentEl.setAttribute("gltf-model-plus", { src: loadingEnvironmentURL }); + return true; } } @@ -329,19 +342,10 @@ async function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, objectsEl.setAttribute("gltf-model-plus", { src: objectsUrl, useCache: false, inflate: true }); objectsScene.appendChild(objectsEl); - await updateEnvironmentForHub(hub); + updateEnvironmentForHub(hub); + updateUIForHub(hub); - remountUI({ - hubId: hub.hub_id, - hubName: hub.name, - hubScene: hub.scene, - hubEntryCode: hub.entry_code, - onSendMessage: messageDispatch.dispatch - }); - - document - .querySelector("#hud-hub-entry-link") - .setAttribute("text", { value: `hub.link/${hub.entry_code}`, width: 1.1, align: "center" }); + remountUI({ onSendMessage: messageDispatch.dispatch }); // Wait for scene objects to load before connecting, so there is no race condition on network state. objectsEl.addEventListener("model-loaded", async el => { @@ -719,6 +723,22 @@ document.addEventListener("DOMContentLoaded", async () => { addToPresenceLog(incomingMessage); }); + hubPhxChannel.on("hub_changed", ({ session_id, hubs }) => { + const hub = hubs[0]; + const userInfo = hubPhxPresence.state[session_id]; + + const changed = updateEnvironmentForHub(hub); + updateUIForHub(hub); + + if (changed && hub.scene) { + addToPresenceLog({ + type: "scene_changed", + name: userInfo.metas[0].profile.displayName, + sceneName: hub.scene.name + }); + } + }); + authChannel.setSocket(socket); linkChannel.setSocket(socket); }); diff --git a/src/react-components/presence-log.js b/src/react-components/presence-log.js index 7eabb323aa..935e6a8711 100644 --- a/src/react-components/presence-log.js +++ b/src/react-components/presence-log.js @@ -46,6 +46,18 @@ export default class PresenceLog extends Component { {e.oldName}  {e.newName}.
); + case "scene_changed": + return ( +
+ {e.name}  {e.sceneName}. +
+ ); + case "hub_name_changed": + return ( +
+ {e.name}  {e.hubName}. +
+ ); case "chat": return ( { - this.channel.push("update_scene", { url }).receive("ok", res => { - console.log(res); - }); + this.channel.push("update_scene", { url }); }; rename = name => { From 300223e4a8ffcbab4b0726014221cbddb35148ea Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Fri, 11 Jan 2019 19:30:09 +0000 Subject: [PATCH 26/47] Get /rename command working --- src/hub.js | 37 ++++++++++++++++++----- src/message-dispatch.js | 8 ++--- src/react-components/chat-command-help.js | 3 +- src/utils/hub-channel.js | 2 +- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/hub.js b/src/hub.js index 444f1daa60..1e17ad8c12 100644 --- a/src/hub.js +++ b/src/hub.js @@ -268,7 +268,6 @@ async function updateUIForHub(hub) { .setAttribute("text", { value: `hub.link/${hub.entry_code}`, width: 1.1, align: "center" }); } -// Sets or changes the environment, returns true if the environment was changed, false if its the first update. async function updateEnvironmentForHub(hub) { let sceneUrl; let isLegacyBundle; // Deprecated @@ -305,7 +304,6 @@ async function updateEnvironmentForHub(hub) { const environmentEl = document.createElement("a-entity"); environmentEl.setAttribute("gltf-model-plus", { src: sceneUrl, useCache: false, inflate: true }); environmentScene.appendChild(environmentEl); - return false; } else { // Change environment environmentEl = environmentScene.childNodes[0]; @@ -320,7 +318,6 @@ async function updateEnvironmentForHub(hub) { environmentEl.addEventListener("model-loaded", onLoadingEnvironmentReady); environmentEl.setAttribute("gltf-model-plus", { src: loadingEnvironmentURL }); - return true; } } @@ -723,18 +720,44 @@ document.addEventListener("DOMContentLoaded", async () => { addToPresenceLog(incomingMessage); }); - hubPhxChannel.on("hub_changed", ({ session_id, hubs }) => { + hubPhxChannel.on("hub_refresh", ({ session_id, hubs, stale_fields }) => { const hub = hubs[0]; const userInfo = hubPhxPresence.state[session_id]; - const changed = updateEnvironmentForHub(hub); updateUIForHub(hub); - if (changed && hub.scene) { + if (stale_fields.includes("scene")) { + updateEnvironmentForHub(hub); + addToPresenceLog({ type: "scene_changed", name: userInfo.metas[0].profile.displayName, - sceneName: hub.scene.name + sceneName: hub.scene ? hub.scene.name : "a custom URL" + }); + } + + if (stale_fields.includes("name")) { + // Re-write the slug in the browser history + if (window.history && window.history.replaceState) { + const pathParts = document.location.pathname.split("/"); + + if (pathParts.length >= 3 && pathParts[1] === hub.hub_id) { + const oldSlug = pathParts[2]; + + const title = + window.history.state && window.history.state.title ? window.history.state.title : document.title; + const state = window.history.state ? window.history.state.state : null; + const url = document.location.toString().replace(`${hub.hub_id}/${oldSlug}`, `${hub.hub_id}/${hub.slug}`); + console.log("old: " + oldSlug + " new: " + hub.slug); + + window.history.replaceState(state, title, url); + } + } + + addToPresenceLog({ + type: "hub_name_changed", + name: userInfo.metas[0].profile.displayName, + hubName: hub.name }); } }); diff --git a/src/message-dispatch.js b/src/message-dispatch.js index 6062f73d6d..be12cc3498 100644 --- a/src/message-dispatch.js +++ b/src/message-dispatch.js @@ -14,14 +14,14 @@ export default class MessageDispatch { dispatch = message => { if (message.startsWith("/")) { const commandParts = message.substring(1).split(" "); - this.dispatchCommand(commandParts[0], commandParts[1]); + this.dispatchCommand(commandParts[0], ...commandParts.slice(1)); document.activeElement.blur(); // Commands should blur } else { this.hubChannel.sendMessage(message); } }; - dispatchCommand = (command, arg) => { + dispatchCommand = (command, ...args) => { const entered = this.scene.is("entered"); switch (command) { @@ -78,10 +78,10 @@ export default class MessageDispatch { this.scene.emit("quack"); break; case "scene": - this.hubChannel.updateScene(arg); + this.hubChannel.updateScene(args[0]); break; case "rename": - this.hubChannel.rename(arg); + this.hubChannel.rename(args.join(" ")); break; } }; diff --git a/src/react-components/chat-command-help.js b/src/react-components/chat-command-help.js index cf036ccfda..a7dfbcf679 100644 --- a/src/react-components/chat-command-help.js +++ b/src/react-components/chat-command-help.js @@ -15,7 +15,8 @@ export default class ChatCommandHelp extends Component {
{commands.map( c => - (this.props.matchingPrefix === "" || c.startsWith(this.props.matchingPrefix)) && ( + (this.props.matchingPrefix === "" || + c.split(" ")[0].startsWith(this.props.matchingPrefix.split(" ")[0])) && (
/{c}
diff --git a/src/utils/hub-channel.js b/src/utils/hub-channel.js index 12966da396..90d45b804c 100644 --- a/src/utils/hub-channel.js +++ b/src/utils/hub-channel.js @@ -101,7 +101,7 @@ export default class HubChannel { }; rename = name => { - this.channel.push("rename", { name }); + this.channel.push("update_hub", { name }); }; subscribe = subscription => { From 1cb9b162326d5bf4740044f5afe4fd35b5707df5 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Fri, 11 Jan 2019 19:37:02 +0000 Subject: [PATCH 27/47] Comment --- src/hub.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hub.js b/src/hub.js index 1e17ad8c12..49cbb9f74b 100644 --- a/src/hub.js +++ b/src/hub.js @@ -559,7 +559,7 @@ document.addEventListener("DOMContentLoaded", async () => { environmentScene.addEventListener("model-loaded", onFirstEnvironmentLoad); environmentScene.addEventListener("model-loaded", () => { - // This is re-entrant if the environment scene is changed. + // This will be run every time the environment is changed (including the first load.) remountUI({ environmentSceneLoaded: true }); for (const modelEl of environmentScene.children) { From 4f88edc55bdfc6a69c4ba2fa52db3f580afa9932 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Fri, 11 Jan 2019 19:37:25 +0000 Subject: [PATCH 28/47] Remove logging --- src/hub.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/hub.js b/src/hub.js index 49cbb9f74b..431c492961 100644 --- a/src/hub.js +++ b/src/hub.js @@ -748,7 +748,6 @@ document.addEventListener("DOMContentLoaded", async () => { window.history.state && window.history.state.title ? window.history.state.title : document.title; const state = window.history.state ? window.history.state.state : null; const url = document.location.toString().replace(`${hub.hub_id}/${oldSlug}`, `${hub.hub_id}/${hub.slug}`); - console.log("old: " + oldSlug + " new: " + hub.slug); window.history.replaceState(state, title, url); } From 502cffb414cd46a0347bb889e604b9caf332ecdd Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Fri, 11 Jan 2019 13:09:43 -0800 Subject: [PATCH 29/47] Fix signedIn boolean --- src/utils/hub-channel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/hub-channel.js b/src/utils/hub-channel.js index e9064cc117..64b73132eb 100644 --- a/src/utils/hub-channel.js +++ b/src/utils/hub-channel.js @@ -12,7 +12,7 @@ function isSameDay(da, db) { export default class HubChannel { constructor(store) { this.store = store; - this._signedIn = this.store.state.credentials.token; + this._signedIn = !!this.store.state.credentials.token; } get signedIn() { From 8cdfd0673e5649faa45c587444b93b9afcbf9f87 Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Fri, 11 Jan 2019 13:30:32 -0800 Subject: [PATCH 30/47] Remove todo --- src/react-components/home-root.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js index 51fea8bd21..2e475d6762 100644 --- a/src/react-components/home-root.js +++ b/src/react-components/home-root.js @@ -135,7 +135,6 @@ class HomeRoot extends Component { signOut = () => { this.props.authChannel.signOut(); - // TODO BP - should randomize avatar and display name on sign out. this.setState({ signedIn: false }); }; From fd430000867499ab0a4ae9f0c239d8f28e8e5bcc Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Fri, 11 Jan 2019 13:39:43 -0800 Subject: [PATCH 31/47] Remove postWithAuth --- src/react-components/hub-create-panel.js | 2 +- src/react-components/scene-ui.js | 16 +++------------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/react-components/hub-create-panel.js b/src/react-components/hub-create-panel.js index 5cf6a7d7a5..22876dad7c 100644 --- a/src/react-components/hub-create-panel.js +++ b/src/react-components/hub-create-panel.js @@ -6,7 +6,7 @@ import { faAngleLeft } from "@fortawesome/free-solid-svg-icons/faAngleLeft"; import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { resolveURL, extractUrlBase } from "../utils/resolveURL"; -import { postWithAuth, createAndRedirectToNewHub } from "../utils/phoenix-utils"; +import { createAndRedirectToNewHub } from "../utils/phoenix-utils"; import CreateRoomDialog from "./create-room-dialog.js"; import { WithHoverSound } from "./wrap-with-audio"; diff --git a/src/react-components/scene-ui.js b/src/react-components/scene-ui.js index 16dc740c2a..822be821d3 100644 --- a/src/react-components/scene-ui.js +++ b/src/react-components/scene-ui.js @@ -6,8 +6,7 @@ import en from "react-intl/locale-data/en"; import styles from "../assets/stylesheets/scene-ui.scss"; import hubLogo from "../assets/images/hub-preview-white.png"; import spokeLogo from "../assets/images/spoke_logo_black.png"; -import { postWithAuth } from "../utils/phoenix-utils"; -import { generateHubName } from "../utils/name-generation"; +import { createAndRedirectToNewHub } from "../utils/phoenix-utils"; import { WithHoverSound } from "./wrap-with-audio"; import CreateRoomDialog from "./create-room-dialog.js"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -52,17 +51,8 @@ class SceneUI extends Component { this.props.scene.removeEventListener("loaded", this.onSceneLoaded); } - createRoom = async () => { - const payload = { hub: { name: this.state.customRoomName || generateHubName(), scene_id: this.props.sceneId } }; - - const res = await postWithAuth("/api/v1/hubs", payload); - const hub = await res.json(); - - if (!process.env.RETICULUM_SERVER || document.location.host === process.env.RETICULUM_SERVER) { - document.location = hub.url; - } else { - document.location = `/hub.html?hub_id=${hub.hub_id}`; - } + createRoom = () => { + createAndRedirectToNewHub(this.state.customRoomName, this.props.sceneId); }; render() { From 69e1f34763f102ab3d80d0aa18621371aae343fa Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sat, 12 Jan 2019 23:54:54 +0000 Subject: [PATCH 32/47] Use push state instead of URL --- src/react-components/2d-hud.js | 13 +- src/react-components/create-object-dialog.js | 2 +- src/react-components/state-link.js | 45 ++++ src/react-components/state-route.js | 30 +++ src/react-components/ui-root.js | 239 +++++++++++-------- src/utils/history.js | 69 ++++++ 6 files changed, 291 insertions(+), 107 deletions(-) create mode 100644 src/react-components/state-link.js create mode 100644 src/react-components/state-route.js create mode 100644 src/utils/history.js diff --git a/src/react-components/2d-hud.js b/src/react-components/2d-hud.js index fc6fceeda0..815a8059a7 100644 --- a/src/react-components/2d-hud.js +++ b/src/react-components/2d-hud.js @@ -7,7 +7,7 @@ import styles from "../assets/stylesheets/2d-hud.scss"; import uiStyles from "../assets/stylesheets/ui-root.scss"; import { WithHoverSound } from "./wrap-with-audio"; import { FormattedMessage } from "react-intl"; -import { Link } from "react-router-dom"; +import StateLink from "./state-link"; const browser = detect(); @@ -163,7 +163,7 @@ class TopHUD extends Component { } } -const BottomHUD = ({ showPhotoPicker, onMediaPicked }) => ( +const BottomHUD = ({ showPhotoPicker, onMediaPicked, history }) => (
{showPhotoPicker ? (
@@ -188,9 +188,11 @@ const BottomHUD = ({ showPhotoPicker, onMediaPicked }) => ( )}
- @@ -200,7 +202,8 @@ const BottomHUD = ({ showPhotoPicker, onMediaPicked }) => ( BottomHUD.propTypes = { showPhotoPicker: PropTypes.bool, - onMediaPicked: PropTypes.func + onMediaPicked: PropTypes.func, + history: PropTypes.object }; export default { TopHUD, BottomHUD }; diff --git a/src/react-components/create-object-dialog.js b/src/react-components/create-object-dialog.js index 31f1dae2b1..1951aefbdf 100644 --- a/src/react-components/create-object-dialog.js +++ b/src/react-components/create-object-dialog.js @@ -102,7 +102,7 @@ export default class CreateObjectDialog extends Component { onCreateClicked = e => { e.preventDefault(); this.props.onCreate(this.state.file || this.state.url || DEFAULT_OBJECT_URL); - this.props.onClose(true); + this.props.onClose(); }; render() { diff --git a/src/react-components/state-link.js b/src/react-components/state-link.js new file mode 100644 index 0000000000..85c29e316c --- /dev/null +++ b/src/react-components/state-link.js @@ -0,0 +1,45 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { pushHistoryState, replaceHistoryState } from "../utils/history"; + +function isModifiedEvent(event) { + return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); +} + +/** + * A history-aware link that pushes/replaces the state, but not the path, in the browser history. + */ +class StateLink extends React.Component { + static propTypes = { + history: PropTypes.object, + innerRef: PropTypes.object, + replace: PropTypes.bool, + stateKey: PropTypes.string, + stateValue: PropTypes.string, + target: PropTypes.string, + onClick: PropTypes.func + }; + + handleClick(event, history) { + if (this.props.onClick) this.props.onClick(event); + + if ( + !event.defaultPrevented && // onClick prevented default + event.button === 0 && // ignore everything but left clicks + (!this.props.target || this.props.target === "_self") && // let browser handle "target=_blank" etc. + !isModifiedEvent(event) // ignore clicks with modifier keys + ) { + event.preventDefault(); + + const method = this.props.replace ? replaceHistoryState : pushHistoryState; + method(history, this.props.stateKey, this.props.stateValue); + } + } + + render() { + const { innerRef, replace, stateKey, stateValue, ...rest } = this.props; // eslint-disable-line no-unused-vars + return this.handleClick(event, this.props.history)} href="#" ref={innerRef} />; + } +} + +export default StateLink; diff --git a/src/react-components/state-route.js b/src/react-components/state-route.js new file mode 100644 index 0000000000..87fdc11a52 --- /dev/null +++ b/src/react-components/state-route.js @@ -0,0 +1,30 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Route } from "react-router"; + +/** + * A react-router Route that looks for a key/value pair in the state to render something. + */ +class StateRoute extends React.Component { + static propTypes = { + stateKey: PropTypes.string, + stateValue: PropTypes.string, + history: PropTypes.object + }; + + render() { + const { history, stateKey, stateValue, ...routeProps } = this.props; + + if ( + (!history.location.state && !stateValue) || + (history.location.state && + (history.location.state[stateKey] === stateValue || (!history.location.state[stateKey] && !stateValue))) + ) { + return ; + } + + return null; + } +} + +export default StateRoute; diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 8432a1f884..c17c008a88 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -2,7 +2,6 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import classNames from "classnames"; import copy from "copy-to-clipboard"; -import { Route, Switch, Link } from "react-router-dom"; import { VR_DEVICE_AVAILABILITY } from "../utils/vr-caps-detect"; import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl"; import en from "react-intl/locale-data/en"; @@ -11,6 +10,14 @@ import screenfull from "screenfull"; import styles from "../assets/stylesheets/ui-root.scss"; import entryStyles from "../assets/stylesheets/entry.scss"; import { ReactAudioContext, WithHoverSound } from "./wrap-with-audio"; +import { + pushHistoryState, + clearHistoryState, + popToBeginningOfHubHistory, + navigateToPageBeforeBeginningOfAllHistory +} from "../utils/history"; +import StateLink from "./state-link.js"; +import StateRoute from "./state-route.js"; import { lang, messages } from "../utils/i18n"; import AutoExitWarning from "./auto-exit-warning"; @@ -49,21 +56,6 @@ import { faArrowLeft } from "@fortawesome/free-solid-svg-icons/faArrowLeft"; addLocaleData([...en]); -// This needs to be updated as we add modal routes. -const MODAL_ROUTES = [ - "/profile", - "/link", - "/help", - "/safari", - "/support", - "/create", - "/webvr", - "/webrtc-screenshare", - "/info" -]; - -const ENTRY_FLOW_ROUTES = ["/device", "/audio", "/mic_grant", "/mic_granted", "/link"]; - // 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, @@ -205,33 +197,33 @@ class UIRoot extends Component { this.props.scene.addEventListener("exit", this.exit); const scene = this.props.scene; - let preEntryHistoryLength = 0; - - // If we landed on the page with a path in the middle of the entry flow (eg we refreshed the - // page on the audio setup dialog) then reset the history entry to /. - // - // Note this isn't perfect, if we refresh the page mid-entry flow and then hit back, we end - // up in a bad state unless we were on the first step. But this seems reasonable enough for now. - if (ENTRY_FLOW_ROUTES.find(x => x === this.props.history.location.pathname)) { - this.props.history.replace("/"); + // If we refreshed the page with any state history (eg if we were in the entry flow + // or had a modal/overlay open) just reset everything to the beginning of the flow by + // erasing all history that was accumulated for this room. + if (this.props.history.location.state) { + popToBeginningOfHubHistory(this.props.history); } - // Hacky technique to skip over the entry flow history entries when we've entered the room and - // hit the back button. - // - // This makes it so if we are in the room and hit back in the browser, we go to the URL - // the browser was on before the entry flow, not back into the entry flow. - this.props.history.listen((newLocation, action) => { - if (!this.state.entered) { - preEntryHistoryLength++; - return; + this.props.history.listen((location, action) => { + const state = location.state; + + // Keep track of the previous history entry, so we can allow the close button + // in modals to just replace the top of the stack (so no forward button to go + // back into the dialog.) + if (action !== "REPLACE") { + this.previousHistoryEntry = this.currentHistoryEntry; } - // Going back through entry flow, skip over it. - if (action === "POP" && newLocation.pathname === this.state.lastEntryStepPath) { - setTimeout(() => { - for (let i = 0; i < preEntryHistoryLength; i++) this.props.history.goBack(); - }, 0); + this.currentHistoryEntry = { + pathname: location.pathname, + title: location.title, + state + }; + + // If we just hit back into the entry flow, just go back to the page before the room landing page. + if (action === "POP" && state && state.entry_step && this.state.entered) { + navigateToPageBeforeBeginningOfAllHistory(this.props.history); + return; } }); @@ -442,7 +434,7 @@ class UIRoot extends Component { await this.setMediaStreamToDefault(); this.beginOrSkipAudioSetup(); } else { - this.props.history.push("/mic_grant"); + this.pushHistoryState("entry_step", "mic_grant"); } }; @@ -454,7 +446,7 @@ class UIRoot extends Component { if (this.props.forcedVREntryType || this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.maybe) { await this.performDirectEntryFlow(true); } else { - this.props.history.push("/webvr"); + this.pushHistoryState("modal", "webvr"); } }; @@ -555,7 +547,7 @@ class UIRoot extends Component { const { hasAudio } = await this.setMediaStreamToDefault(); if (hasAudio) { - this.props.history.push("/mic_granted"); + this.pushHistoryState("entry_step", "mic_granted"); } else { this.beginOrSkipAudioSetup(); } @@ -570,7 +562,7 @@ class UIRoot extends Component { beginOrSkipAudioSetup = () => { if (!this.props.forcedVREntryType || !this.props.forcedVREntryType.endsWith("_now")) { - this.props.history.push("/audio"); + this.pushHistoryState("entry_step", "audio"); } else { this.onAudioReadyButton(); } @@ -648,11 +640,11 @@ class UIRoot extends Component { } this.setState({ entered: true, lastEntryStepPath: this.props.history.location.pathname, showInviteDialog: false }); - this.props.history.push("/"); + clearHistoryState(this.props.history); }; attemptLink = async () => { - this.props.history.push("/link"); + this.pushHistoryState("overlay", "link"); const { code, cancel, onFinished } = await this.props.linkChannel.generateCode(); this.setState({ linkCode: code, linkCodeCancel: cancel }); onFinished.then(() => this.setState({ log: false, linkCode: null, linkCodeCancel: null })); @@ -670,19 +662,11 @@ class UIRoot extends Component { this.props.scene.emit("add_media", media); }; - closeDialog = success => { + closeDialog = () => { if (this.state.dialog) { this.setState({ dialog: null }); } else { - // If dialog was successful (eg user hit "OK") then move forward in history, o/w go back. - // - // This makes it so if you create an object, back will re-show the create object dialog, - // but if you cancel, it will not. - if (success) { - this.props.history.push("/"); - } else { - this.props.history.goBack(); - } + this.props.history.replace(this.previousHistoryEntry); } }; @@ -716,7 +700,7 @@ class UIRoot extends Component { }; showWebRTCScreenshareUnsupportedDialog = () => { - this.props.history.push("/webrtc-screenshare"); + this.pushHistoryState("modal", "webrtc-screenshare"); }; onMiniInviteClicked = () => { @@ -744,6 +728,8 @@ class UIRoot extends Component { return this.props.presences ? Object.entries(this.props.presences).length : 0; }; + pushHistoryState = (k, v) => pushHistoryState(this.props.history, k, v); + renderExitedPane = () => { let subtitle = null; if (this.props.roomUnavailableReason === "closed") { @@ -861,20 +847,30 @@ class UIRoot extends Component {
{this.props.hubName} {this.props.hubScene && ( - + - + )}
- +
{this.props.store.state.profile.displayName}
- +
@@ -918,16 +914,14 @@ class UIRoot extends Component {
- - +
@@ -961,9 +955,9 @@ class UIRoot extends Component { isInHMD={this.props.availableVREntryTypes.isInHMD} /> {this.props.availableVREntryTypes.safari === VR_DEVICE_AVAILABILITY.maybe && ( - + - + )} {this.props.availableVREntryTypes.screen === VR_DEVICE_AVAILABILITY.yes && ( @@ -1159,8 +1153,13 @@ class UIRoot extends Component { ); }; - isInModal = () => { - return !!MODAL_ROUTES.find(p => this.props.location.pathname.startsWith(p)); + isInModalOrOverlay = () => { + return !!( + (this.props.history && + this.props.history.location.state && + (this.props.history.location.state.modal || this.props.history.location.state.overlay)) || + this.state.dialog + ); }; render() { @@ -1179,20 +1178,28 @@ class UIRoot extends Component { ) : (
- - {this.renderDevicePanel()} - {this.renderMicPanel(false)} - {this.renderMicPanel(true)} - {this.renderAudioSetupPanel()} - {this.renderEntryStartPanel()} - + + {this.renderDevicePanel()} + + + {this.renderMicPanel(false)} + + + {this.renderMicPanel(true)} + + + {this.renderAudioSetupPanel()} + + + {this.renderEntryStartPanel()} +
); const dialogBoxContentsClassNames = classNames({ - [styles.uiInteractive]: !this.isInModal(), + [styles.uiInteractive]: !this.isInModalOrOverlay(), [styles.uiDialogBoxContents]: true, - [styles.backgrounded]: this.isInModal() + [styles.backgrounded]: this.isInModalOrOverlay() }); const showVREntryButton = entered && this.props.availableVREntryTypes.isInHMD; @@ -1207,27 +1214,54 @@ class UIRoot extends Component {
{this.state.dialog} - ( )} /> - - this.renderDialog(HelpDialog)} /> - this.renderDialog(SafariDialog)} /> - this.renderDialog(HelpDialog)} + /> + this.renderDialog(SafariDialog)} + /> + this.renderDialog(InviteTeamDialog, { hubChannel: this.props.hubChannel })} /> - this.renderDialog(CreateObjectDialog, { onCreate: this.createObject })} /> - this.renderDialog(WebVRRecommendDialog)} /> - this.renderDialog(WebRTCScreenshareUnsupportedDialog)} /> - this.renderDialog(WebVRRecommendDialog)} + /> + this.renderDialog(WebRTCScreenshareUnsupportedDialog)} + /> + this.renderDialog(RoomInfoDialog, { scene: this.props.hubScene, hubName: this.props.hubName }) } @@ -1288,7 +1322,7 @@ class UIRoot extends Component { spawnChatMessage(this.state.pendingMessage); this.setState({ pendingMessage: "" }); } else { - this.props.history.push("/create"); + this.pushHistoryState("modal", "create"); } }} /> @@ -1348,28 +1382,30 @@ class UIRoot extends Component { )}
- ( { this.state.linkCodeCancel(); this.setState({ linkCode: null, linkCodeCancel: null }); - this.props.history.goBack(); + this.props.history.replace(this.previousHistoryEntry); }} /> )} /> - + - +
- + - +
)} @@ -1425,6 +1461,7 @@ class UIRoot extends Component { )}
diff --git a/src/utils/history.js b/src/utils/history.js new file mode 100644 index 0000000000..56c682a0ae --- /dev/null +++ b/src/utils/history.js @@ -0,0 +1,69 @@ +// Utilities for manipulating state history. This provides a number of functions which +// need to be used to manipulate history state so we can deal with edge cases around +// entry back button and refreshes once entered. +// + +// This pushes/replaces a new k, v pair into the history, while maintaining all the other +// keys, or clears state if k if null. +// +// Also maintains a length key that can be used to find the length of the history chain +// (across refreshes) for the current room. +function pushOrUpdateHistoryState(history, replace, k, v) { + let state = {}; + const newLength = ((history.location.state && history.location.state.__historyLength) || 0) + (replace ? 0 : 1); + + if (k) { + state = history.location.state ? { ...history.location.state } : {}; + delete state.__duplicate; + state[k] = v; + } + + const pathname = history.location.pathname === "/" ? "" : history.location.pathname; + + // If popToBeginningOfHubHistory was previously used, there is a duplicate entry + // at the top of the history stack (which was needed to wipe out forward history) + // so we use this opportunity to replace it. + const isDuplicate = history.location.state && history.location.state.__duplicate; + const method = replace || isDuplicate ? history.replace : history.push; + state.__historyLength = newLength; + + method({ pathname, state }); +} + +export function replaceHistoryState(history, k, v) { + pushOrUpdateHistoryState(history, true, k, v); +} + +export function pushHistoryState(history, k, v) { + pushOrUpdateHistoryState(history, false, k, v); +} + +export function clearHistoryState(history) { + pushOrUpdateHistoryState(history, true); +} + +// This will pop the browser history to the first entry that was for this hubs room, +// and then push a duplicate entry onto the history stack in order to wipe out forward +// history. +export function popToBeginningOfHubHistory(history) { + if (!history.location.state || history.location.state.__historyLength === undefined) return; + + const len = history.location.state.__historyLength; + if (len === 0) return; + + history.go(-len); + + // Push a duplicate entry to wipe out forward history + // + // Note this entry is tagged as "duplicate" so we can identify it on the next push, which will let + // us effectively remove the duplicate if there's an opportunity to do so. + history.push({ pathname: history.location.pathname, state: { __historyLength: 0, __duplicate: true } }); +} + +// This will pop the browser history to the entry before the first entry for this hubs room. +export function navigateToPageBeforeBeginningOfAllHistory(history) { + popToBeginningOfHubHistory(history); + + // Go back two entries, since we push a duplicate root entry to wipe out forward history + history.go(-2); +} From 8cf60f3f148bc0b9705e59118f809a57c2dbeae3 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sun, 13 Jan 2019 06:23:50 +0000 Subject: [PATCH 33/47] Fix up profile entry during entry flow --- src/react-components/profile-entry-panel.js | 7 ------- src/react-components/ui-root.js | 19 ++++++++++++++++++- src/utils/history.js | 2 +- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js index be3892ee77..3997a8f93d 100644 --- a/src/react-components/profile-entry-panel.js +++ b/src/react-components/profile-entry-panel.js @@ -14,7 +14,6 @@ class ProfileEntryPanel extends Component { messages: PropTypes.object, finished: PropTypes.func, intl: PropTypes.object, - history: PropTypes.object, location: PropTypes.object }; @@ -48,12 +47,6 @@ class ProfileEntryPanel extends Component { }); this.props.finished(); - this.props.history.goBack(); - - // We may need to go to a new path after saving. - if (this.props.location.state.postPushPath) { - this.props.history.push(this.props.location.state.postPushPath); - } }; stopPropagation = e => { diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index c17c008a88..3a05461037 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -12,6 +12,7 @@ import entryStyles from "../assets/stylesheets/entry.scss"; import { ReactAudioContext, WithHoverSound } from "./wrap-with-audio"; import { pushHistoryState, + replaceHistoryState, clearHistoryState, popToBeginningOfHubHistory, navigateToPageBeforeBeginningOfAllHistory @@ -557,6 +558,7 @@ class UIRoot extends Component { }; onProfileFinished = () => { + this.closeDialog(); this.props.hubChannel.sendProfileUpdate(); }; @@ -915,7 +917,7 @@ class UIRoot extends Component {
)} /> + ( + { + this.onProfileFinished(); + replaceHistoryState(this.props.history, "entry_step", "device"); + }} + store={this.props.store} + /> + )} + /> Date: Sun, 13 Jan 2019 06:28:07 +0000 Subject: [PATCH 34/47] Remove unneeded code --- src/react-components/profile-entry-panel.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js index 3997a8f93d..ba81fc53b0 100644 --- a/src/react-components/profile-entry-panel.js +++ b/src/react-components/profile-entry-panel.js @@ -13,8 +13,7 @@ class ProfileEntryPanel extends Component { store: PropTypes.object, messages: PropTypes.object, finished: PropTypes.func, - intl: PropTypes.object, - location: PropTypes.object + intl: PropTypes.object }; constructor(props) { @@ -45,7 +44,6 @@ class ProfileEntryPanel extends Component { avatarId: this.state.avatarId } }); - this.props.finished(); }; From b1024d2a3ef5a4dc3530d2ff05ce269a0a0ed8fb Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sun, 13 Jan 2019 06:29:32 +0000 Subject: [PATCH 35/47] Bump timeout for force entry --- src/react-components/ui-root.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 3a05461037..19068d280e 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -239,7 +239,7 @@ class UIRoot extends Component { } }); - setTimeout(() => this.handleForceEntry(), 1000); + setTimeout(() => this.handleForceEntry(), 2000); } componentWillUnmount() { From 4b017c2e383e5ec537dd9df0f8e1819155e64828 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sun, 13 Jan 2019 06:30:44 +0000 Subject: [PATCH 36/47] Refactor --- src/react-components/ui-root.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 19068d280e..f88bcdb735 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -731,6 +731,7 @@ class UIRoot extends Component { }; pushHistoryState = (k, v) => pushHistoryState(this.props.history, k, v); + replaceHistoryState = (k, v) => replaceHistoryState(this.props.history, k, v); renderExitedPane = () => { let subtitle = null; @@ -1233,7 +1234,7 @@ class UIRoot extends Component { {...props} finished={() => { this.onProfileFinished(); - replaceHistoryState(this.props.history, "entry_step", "device"); + this.replaceHistoryState("entry_step", "device"); }} store={this.props.store} /> From 135371d6edc869fb40d67d7e0fe0a90771e50117 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sun, 13 Jan 2019 06:32:39 +0000 Subject: [PATCH 37/47] Fix mic flow --- src/react-components/ui-root.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index f88bcdb735..3346e38132 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -544,7 +544,7 @@ class UIRoot extends Component { }; onMicGrantButton = async () => { - if (this.props.location.pathname === "/mic_grant") { + if (this.props.location.state && this.props.location.state.entry_step === "mic_grant") { const { hasAudio } = await this.setMediaStreamToDefault(); if (hasAudio) { From 73068e8e61037e399d406a7fefe0680dbad6628c Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sun, 13 Jan 2019 17:31:06 +0000 Subject: [PATCH 38/47] Fixes for Chrome --- src/react-components/ui-root.js | 21 ++++++++++++--------- src/utils/history.js | 28 ++++++++++++++++------------ 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 3346e38132..9ff62864d7 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -15,7 +15,7 @@ import { replaceHistoryState, clearHistoryState, popToBeginningOfHubHistory, - navigateToPageBeforeBeginningOfAllHistory + navigateToPriorPage } from "../utils/history"; import StateLink from "./state-link.js"; import StateRoute from "./state-route.js"; @@ -198,12 +198,7 @@ class UIRoot extends Component { this.props.scene.addEventListener("exit", this.exit); const scene = this.props.scene; - // If we refreshed the page with any state history (eg if we were in the entry flow - // or had a modal/overlay open) just reset everything to the beginning of the flow by - // erasing all history that was accumulated for this room. - if (this.props.history.location.state) { - popToBeginningOfHubHistory(this.props.history); - } + let isNavigatingToPriorPage = false; this.props.history.listen((location, action) => { const state = location.state; @@ -222,12 +217,20 @@ class UIRoot extends Component { }; // If we just hit back into the entry flow, just go back to the page before the room landing page. - if (action === "POP" && state && state.entry_step && this.state.entered) { - navigateToPageBeforeBeginningOfAllHistory(this.props.history); + if (action === "POP" && state && state.entry_step && this.state.entered && !isNavigatingToPriorPage) { + isNavigatingToPriorPage = true; + navigateToPriorPage(this.props.history); return; } }); + // If we refreshed the page with any state history (eg if we were in the entry flow + // or had a modal/overlay open) just reset everything to the beginning of the flow by + // erasing all history that was accumulated for this room. + if (this.props.history.location.state) { + popToBeginningOfHubHistory(this.props.history); + } + this.setState({ audioContext: { playSound: sound => { diff --git a/src/utils/history.js b/src/utils/history.js index 9440d4592b..de43fd6690 100644 --- a/src/utils/history.js +++ b/src/utils/history.js @@ -45,25 +45,29 @@ export function clearHistoryState(history) { // This will pop the browser history to the first entry that was for this hubs room, // and then push a duplicate entry onto the history stack in order to wipe out forward // history. -export function popToBeginningOfHubHistory(history) { +export function popToBeginningOfHubHistory(history, navigateToPriorPage) { if (!history.location.state || history.location.state.__historyLength === undefined) return; const len = history.location.state.__historyLength; if (len === 0) return; - history.go(-len); + // After the go() completes, we push a duplicate history entry onto the stack + // in order to wipe out forward history. We also optionally go back -2 if we wanted + // to go back to the prior page. + let unsubscribe = null; + + const finalizer = () => { + unsubscribe(); + history.push({ pathname: history.location.pathname, state: { __historyLength: 0, __duplicate: true } }); + if (navigateToPriorPage) history.go(-2); // Go back to history entry before beginning. + }; + + unsubscribe = history.listen(finalizer); - // Push a duplicate entry to wipe out forward history - // - // Note this entry is tagged as "duplicate" so we can identify it on the next push, which will let - // us effectively remove the duplicate if there's an opportunity to do so. - history.push({ pathname: history.location.pathname, state: { __historyLength: 0, __duplicate: true } }); + history.go(-len); } // This will pop the browser history to the entry before the first entry for this hubs room. -export function navigateToPageBeforeBeginningOfAllHistory(history) { - popToBeginningOfHubHistory(history); - - // Go back two entries, since we push a duplicate root entry to wipe out forward history - history.go(-2); +export function navigateToPriorPage(history) { + popToBeginningOfHubHistory(history, true); } From c17a0c367cb7bd5a6fcf20387b42b4b813c6c58b Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sun, 13 Jan 2019 18:39:36 +0000 Subject: [PATCH 39/47] Refactor and remove the need to keep track of previous history entry by just calling goBack when a modal is closed --- src/react-components/ui-root.js | 27 ++++++--------------------- src/utils/history.js | 8 ++------ 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 9ff62864d7..5e7e3bcdb7 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -198,27 +198,12 @@ class UIRoot extends Component { this.props.scene.addEventListener("exit", this.exit); const scene = this.props.scene; - let isNavigatingToPriorPage = false; - - this.props.history.listen((location, action) => { + const unsubscribe = this.props.history.listen((location, action) => { const state = location.state; - // Keep track of the previous history entry, so we can allow the close button - // in modals to just replace the top of the stack (so no forward button to go - // back into the dialog.) - if (action !== "REPLACE") { - this.previousHistoryEntry = this.currentHistoryEntry; - } - - this.currentHistoryEntry = { - pathname: location.pathname, - title: location.title, - state - }; - // If we just hit back into the entry flow, just go back to the page before the room landing page. - if (action === "POP" && state && state.entry_step && this.state.entered && !isNavigatingToPriorPage) { - isNavigatingToPriorPage = true; + if (action === "POP" && state && state.entry_step && this.state.entered) { + unsubscribe(); navigateToPriorPage(this.props.history); return; } @@ -226,7 +211,7 @@ class UIRoot extends Component { // If we refreshed the page with any state history (eg if we were in the entry flow // or had a modal/overlay open) just reset everything to the beginning of the flow by - // erasing all history that was accumulated for this room. + // erasing all history that was accumulated for this room (including across refreshes.) if (this.props.history.location.state) { popToBeginningOfHubHistory(this.props.history); } @@ -671,7 +656,7 @@ class UIRoot extends Component { if (this.state.dialog) { this.setState({ dialog: null }); } else { - this.props.history.replace(this.previousHistoryEntry); + this.props.history.goBack(); } }; @@ -1413,7 +1398,7 @@ class UIRoot extends Component { onClose={() => { this.state.linkCodeCancel(); this.setState({ linkCode: null, linkCodeCancel: null }); - this.props.history.replace(this.previousHistoryEntry); + this.props.history.goBack(); }} /> )} diff --git a/src/utils/history.js b/src/utils/history.js index de43fd6690..f87378ecda 100644 --- a/src/utils/history.js +++ b/src/utils/history.js @@ -54,15 +54,11 @@ export function popToBeginningOfHubHistory(history, navigateToPriorPage) { // After the go() completes, we push a duplicate history entry onto the stack // in order to wipe out forward history. We also optionally go back -2 if we wanted // to go back to the prior page. - let unsubscribe = null; - - const finalizer = () => { + const unsubscribe = history.listen(() => { unsubscribe(); history.push({ pathname: history.location.pathname, state: { __historyLength: 0, __duplicate: true } }); if (navigateToPriorPage) history.go(-2); // Go back to history entry before beginning. - }; - - unsubscribe = history.listen(finalizer); + }); history.go(-len); } From 118622fe2cba4191b3d95a9ffa616baf150e4d18 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sun, 13 Jan 2019 18:46:12 +0000 Subject: [PATCH 40/47] Fix for forced avatar picker flow --- src/react-components/ui-root.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 5e7e3bcdb7..2ae4b46a3a 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -10,13 +10,7 @@ import screenfull from "screenfull"; import styles from "../assets/stylesheets/ui-root.scss"; import entryStyles from "../assets/stylesheets/entry.scss"; import { ReactAudioContext, WithHoverSound } from "./wrap-with-audio"; -import { - pushHistoryState, - replaceHistoryState, - clearHistoryState, - popToBeginningOfHubHistory, - navigateToPriorPage -} from "../utils/history"; +import { pushHistoryState, clearHistoryState, popToBeginningOfHubHistory, navigateToPriorPage } from "../utils/history"; import StateLink from "./state-link.js"; import StateRoute from "./state-route.js"; @@ -719,7 +713,6 @@ class UIRoot extends Component { }; pushHistoryState = (k, v) => pushHistoryState(this.props.history, k, v); - replaceHistoryState = (k, v) => replaceHistoryState(this.props.history, k, v); renderExitedPane = () => { let subtitle = null; @@ -1222,7 +1215,7 @@ class UIRoot extends Component { {...props} finished={() => { this.onProfileFinished(); - this.replaceHistoryState("entry_step", "device"); + this.pushHistoryState("entry_step", "device"); }} store={this.props.store} /> From 5315c072766971066564e60e5b5edfa40a4734c1 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Sun, 13 Jan 2019 18:48:11 +0000 Subject: [PATCH 41/47] Fix for chrome for avatar picker flow --- src/react-components/ui-root.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 2ae4b46a3a..043bb5a458 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -1214,8 +1214,12 @@ class UIRoot extends Component { { + const unsubscribe = this.props.history.listen(() => { + unsubscribe(); + this.pushHistoryState("entry_step", "device"); + }); + this.onProfileFinished(); - this.pushHistoryState("entry_step", "device"); }} store={this.props.store} /> From 60bd1c0e4a53b1186213184b3b2e0513181d6f3c Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Mon, 14 Jan 2019 12:40:15 -0800 Subject: [PATCH 42/47] Rename token to auth_token --- src/hub.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hub.js b/src/hub.js index 3c8bad7050..c85e2454f3 100644 --- a/src/hub.js +++ b/src/hub.js @@ -533,7 +533,7 @@ document.addEventListener("DOMContentLoaded", async () => { const joinPayload = { profile: store.state.profile, push_subscription_endpoint: pushSubscriptionEndpoint, context }; const { token } = store.state.credentials; if (token) { - joinPayload.token = token; + joinPayload.auth_token = token; } const hubPhxChannel = socket.channel(`hub:${hubId}`, joinPayload); From e406fa98146ed1455fa77b24f73d569e07e7e6c7 Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Tue, 15 Jan 2019 18:02:33 -0800 Subject: [PATCH 43/47] Retrieve and decode permissions on join and sign-in. Enforce permissions on hub mutation commands. --- package-lock.json | 85 +++++++++++++++++++++++++++++++++ package.json | 1 + src/hub.js | 3 +- src/message-dispatch.js | 11 ++++- src/react-components/ui-root.js | 1 - src/utils/hub-channel.js | 14 +++++- 6 files changed, 110 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 429c7706e7..c690e723d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2398,6 +2398,11 @@ "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", "integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=" }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-from": { "version": "1.1.0", "resolved": "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz", @@ -4052,6 +4057,14 @@ "safer-buffer": "^2.1.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz", + "integrity": "sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "editions": { "version": "1.3.4", "resolved": "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz", @@ -7667,6 +7680,29 @@ "resolved": "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.2.4.tgz", "integrity": "sha1-pGusXTUGolRGW8VIh24mfG0NZGQ=" }, + "jsonwebtoken": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.4.0.tgz", + "integrity": "sha512-coyXjRTCy0pw5WYBpMvWOMN+Kjaik2MwTUIq9cna/W7NpO9E+iYbumZONAz3hcr+tXFJECoQVrtmIoC3Oz0gvg==", + "requires": { + "jws": "^3.1.5", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -7762,6 +7798,25 @@ } } }, + "jwa": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz", + "integrity": "sha512-tBO/cf++BUsJkYql/kBbJroKOgHWEigTKBAjjBEmrMGYd1QMBC74Hr4Wo2zCZw6ZrVhlJPvoMrkcOnlWR/DJfw==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.10", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz", + "integrity": "sha512-GsCSexFADNQUr8T5HPJvayTjvPIfoyJPtLQBwn5a4WZQchcrPMPMAWcC1AzJVRDKyD6ZPROPAxgv6rfHViO4uQ==", + "requires": { + "jwa": "^1.1.5", + "safe-buffer": "^5.0.1" + } + }, "karma": { "version": "0.13.22", "resolved": "https://registry.npmjs.org/karma/-/karma-0.13.22.tgz", @@ -8269,6 +8324,31 @@ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, "lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", @@ -8280,6 +8360,11 @@ "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==", "dev": true }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lodash.tail": { "version": "4.1.1", "resolved": "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz", diff --git a/package.json b/package.json index 1919093050..d28ef4e2cf 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "event-target-shim": "^3.0.1", "form-urlencoded": "^2.0.4", "jsonschema": "^1.2.2", + "jsonwebtoken": "^8.4.0", "jszip": "^3.1.5", "markdown-it": "^8.4.2", "moving-average": "^1.0.0", diff --git a/src/hub.js b/src/hub.js index 34eb29317c..65df747de0 100644 --- a/src/hub.js +++ b/src/hub.js @@ -620,9 +620,10 @@ document.addEventListener("DOMContentLoaded", async () => { .join() .receive("ok", async data => { hubChannel.setPhoenixChannel(hubPhxChannel); + hubChannel.setPermissionsFromToken(data.perms_token); subscriptions.setHubChannel(hubChannel); subscriptions.setSubscribed(data.subscriptions.web_push); - remountUI({ initialIsSubscribed: subscriptions.isSubscribed(), isOwner: data.is_owner }); + remountUI({ initialIsSubscribed: subscriptions.isSubscribed() }); await handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data); }) .receive("error", res => { diff --git a/src/message-dispatch.js b/src/message-dispatch.js index be12cc3498..ace9b4fd05 100644 --- a/src/message-dispatch.js +++ b/src/message-dispatch.js @@ -39,6 +39,7 @@ export default class MessageDispatch { const playerRig = document.querySelector("#player-rig"); const scales = [0.0625, 0.125, 0.25, 0.5, 1.0, 1.5, 3, 5, 7.5, 12.5]; const curScale = playerRig.object3D.scale; + let err; switch (command) { case "fly": @@ -78,10 +79,16 @@ export default class MessageDispatch { this.scene.emit("quack"); break; case "scene": - this.hubChannel.updateScene(args[0]); + err = this.hubChannel.updateScene(args[0]); + if (err === "unauthorized") { + this.addToPresenceLog({ type: "log", body: "You do not have permission to change the scene." }); + } break; case "rename": - this.hubChannel.rename(args.join(" ")); + err = this.hubChannel.rename(args.join(" ")); + if (err === "unauthorized") { + this.addToPresenceLog({ type: "log", body: "You do not have permission to rename this room." }); + } break; } }; diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 41ab181bb2..8432a1f884 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -117,7 +117,6 @@ class UIRoot extends Component { platformUnsupportedReason: PropTypes.string, hubId: PropTypes.string, hubName: PropTypes.string, - isOwner: PropTypes.bool, hubScene: PropTypes.object, isSupportAvailable: PropTypes.bool, presenceLogEntries: PropTypes.array, diff --git a/src/utils/hub-channel.js b/src/utils/hub-channel.js index 99907ebd22..6e2144382b 100644 --- a/src/utils/hub-channel.js +++ b/src/utils/hub-channel.js @@ -1,3 +1,5 @@ +import jsonwebtoken from "jsonwebtoken"; + const MS_PER_DAY = 1000 * 60 * 60 * 24; const MS_PER_MONTH = 1000 * 60 * 60 * 24 * 30; @@ -13,6 +15,7 @@ export default class HubChannel { constructor(store) { this.store = store; this._signedIn = !!this.store.state.credentials.token; + this._permissions = {}; } get signedIn() { @@ -23,6 +26,11 @@ export default class HubChannel { this.channel = channel; }; + setPermissionsFromToken = token => { + // Note: token is not verified. + this._permissions = jsonwebtoken.decode(token); + }; + sendEntryEvent = async () => { if (!this.channel) { console.warn("No phoenix channel initialized before room entry."); @@ -97,10 +105,12 @@ export default class HubChannel { }; updateScene = url => { + if (!this._permissions.update_hub) return "unauthorized"; this.channel.push("update_scene", { url }); }; rename = name => { + if (!this._permissions.update_hub) return "unauthorized"; this.channel.push("update_hub", { name }); }; @@ -121,7 +131,8 @@ export default class HubChannel { return new Promise((resolve, reject) => { this.channel .push("sign_in", { token }) - .receive("ok", () => { + .receive("ok", ({ perms_token }) => { + this.setPermissionsFromToken(perms_token); this._signedIn = true; resolve(); }) @@ -137,6 +148,7 @@ export default class HubChannel { this.channel .push("sign_out") .receive("ok", () => { + this._permissions = {}; this._signedIn = false; resolve(); }) From dfc856208ebeda47a3268863aacf43f931e5d4ad Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Wed, 16 Jan 2019 16:16:08 +0000 Subject: [PATCH 44/47] Fix for dev --- src/hub.js | 17 ++++++++--------- src/utils/history.js | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/hub.js b/src/hub.js index 0c885c7bb5..637bb6db70 100644 --- a/src/hub.js +++ b/src/hub.js @@ -84,7 +84,7 @@ import "./components/open-media-button"; import ReactDOM from "react-dom"; import React from "react"; -import { HashRouter, BrowserRouter, Route } from "react-router-dom"; +import { BrowserRouter, Route } from "react-router-dom"; import UIRoot from "./react-components/ui-root"; import AuthChannel from "./utils/auth-channel"; import HubChannel from "./utils/hub-channel"; @@ -215,19 +215,18 @@ function mountUI(props = {}) { const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi"); const forcedVREntryType = qs.get("vr_entry_type"); - const Router = - process.env.RETICULUM_SERVER && process.env.RETICULUM_SERVER !== document.location.host - ? HashRouter - : BrowserRouter; - // Hub ID and slug are the basename - const routerBaseName = document.location.pathname + let routerBaseName = document.location.pathname .split("/") .slice(0, 3) .join("/"); + if (document.location.pathname.includes("hub.html")) { + routerBaseName = ""; + } + ReactDOM.render( - + ( )} /> - , + , document.getElementById("ui-root") ); } diff --git a/src/utils/history.js b/src/utils/history.js index f87378ecda..4eddd68264 100644 --- a/src/utils/history.js +++ b/src/utils/history.js @@ -18,7 +18,7 @@ function pushOrUpdateHistoryState(history, replace, k, v) { state[k] = v; } - const pathname = history.location.pathname === "/" ? "" : history.location.pathname; + const pathname = (history.location.pathname === "/" ? "" : history.location.pathname) + history.location.search; // If popToBeginningOfHubHistory was previously used, there is a duplicate entry // at the top of the history stack (which was needed to wipe out forward history) From 534f0086ac00c9f8bbec8709d87385ed64868093 Mon Sep 17 00:00:00 2001 From: Greg Fodor Date: Wed, 16 Jan 2019 16:32:56 +0000 Subject: [PATCH 45/47] Fix refresh for dev --- src/utils/history.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils/history.js b/src/utils/history.js index 4eddd68264..4484b170f6 100644 --- a/src/utils/history.js +++ b/src/utils/history.js @@ -56,7 +56,10 @@ export function popToBeginningOfHubHistory(history, navigateToPriorPage) { // to go back to the prior page. const unsubscribe = history.listen(() => { unsubscribe(); - history.push({ pathname: history.location.pathname, state: { __historyLength: 0, __duplicate: true } }); + history.push({ + pathname: history.location.pathname + history.location.search, + state: { __historyLength: 0, __duplicate: true } + }); if (navigateToPriorPage) history.go(-2); // Go back to history entry before beginning. }); From d669027542a993ea856b189e949084e31d60b3fa Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Wed, 16 Jan 2019 15:32:42 -0800 Subject: [PATCH 46/47] Rename authenticated to signedin --- src/react-components/home-root.js | 2 +- src/utils/auth-channel.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js index 2e475d6762..b3d01e1a79 100644 --- a/src/react-components/home-root.js +++ b/src/react-components/home-root.js @@ -52,7 +52,7 @@ class HomeRoot extends Component { constructor(props) { super(props); - this.state.signedIn = props.authChannel.authenticated; + this.state.signedIn = props.authChannel.signedIn; this.state.email = props.authChannel.email; } diff --git a/src/utils/auth-channel.js b/src/utils/auth-channel.js index 0574c21b9f..a166237549 100644 --- a/src/utils/auth-channel.js +++ b/src/utils/auth-channel.js @@ -4,7 +4,7 @@ export default class AuthChannel { constructor(store) { this.store = store; this.socket = null; - this._authenticated = !!this.store.state.credentials.token; + this._signedIn = !!this.store.state.credentials.token; } setSocket = socket => { @@ -15,8 +15,8 @@ export default class AuthChannel { return this.store.state.credentials.email; } - get authenticated() { - return this._authenticated; + get signedIn() { + return this._signedIn; } signOut = async hubChannel => { @@ -24,7 +24,7 @@ export default class AuthChannel { await hubChannel.signOut(); } this.store.update({ credentials: { token: null, email: null } }); - this._authenticated = false; + this._signedIn = false; }; async startAuthentication(email, hubChannel) { @@ -42,7 +42,7 @@ export default class AuthChannel { if (hubChannel) { await hubChannel.signIn(token); } - this._authenticated = true; + this._signedIn = true; resolve(); }) ); From dab58128722b08989d887864d3542130008e0bad Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Wed, 23 Jan 2019 11:07:37 -0800 Subject: [PATCH 47/47] Fix pathfinding on scene change --- src/components/character-controller.js | 4 ++++ src/systems/nav.js | 1 + 2 files changed, 5 insertions(+) diff --git a/src/components/character-controller.js b/src/components/character-controller.js index b7a06a41bc..83bad37273 100644 --- a/src/components/character-controller.js +++ b/src/components/character-controller.js @@ -36,6 +36,10 @@ AFRAME.registerComponent("character-controller", { this.snapRotateRight = this.snapRotateRight.bind(this); this.setAngularVelocity = this.setAngularVelocity.bind(this); this.handleTeleport = this.handleTeleport.bind(this); + this.el.sceneEl.addEventListener("nav-mesh-loaded", () => { + this.navGroup = null; + this.navNode = null; + }); }, update: function() { diff --git a/src/systems/nav.js b/src/systems/nav.js index b3c504ecd5..7f3596ef1c 100644 --- a/src/systems/nav.js +++ b/src/systems/nav.js @@ -10,5 +10,6 @@ AFRAME.registerSystem("nav", { const geometry = new THREE.Geometry().fromBufferGeometry(mesh.geometry); geometry.applyMatrix(mesh.matrixWorld); this.pathfinder.setZoneData(zone, Pathfinding.createZone(geometry)); + this.el.sceneEl.emit("nav-mesh-loaded"); } });