From a16e9872f06eb14a638290cb074185fcff93a4d7 Mon Sep 17 00:00:00 2001
From: Greg Fodor <gfodor@gmail.com>
Date: Wed, 14 Mar 2018 17:54:25 -0700
Subject: [PATCH] Add concurrent run disconnect warning

---
 package.json                          |  5 +-
 src/react-components/ui-root.js       | 88 ++++++++++++++++++++++++---
 src/room.js                           | 10 ++-
 src/utils/concurrent-load-detector.js | 44 ++++++++++++++
 yarn.lock                             | 18 +++---
 5 files changed, 147 insertions(+), 18 deletions(-)
 create mode 100644 src/utils/concurrent-load-detector.js

diff --git a/package.json b/package.json
index b952d3a7e..dbf01ec7d 100644
--- a/package.json
+++ b/package.json
@@ -20,10 +20,11 @@
     "aframe-teleport-controls": "https://github.com/netpro2k/aframe-teleport-controls#feature/teleport-origin",
     "aframe-xr": "github:brianpeiris/aframe-xr#3162aed",
     "detect-browser": "^2.1.0",
+    "event-target-shim": "^3.0.1",
     "jsonschema": "^1.2.2",
     "material-design-lite": "^1.3.0",
-    "minijanus": "^0.4.0",
-    "naf-janus-adapter": "^0.4.0",
+    "minijanus": "https://github.com/mozilla/minijanus.js#master",
+    "naf-janus-adapter": "https://github.com/mozilla/naf-janus-adapter#feature/disconnect",
     "networked-aframe": "https://github.com/mozillareality/networked-aframe#mr-social-client/master",
     "nipplejs": "^0.6.7",
     "query-string": "^5.0.1",
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index 7239f2289..2485960a7 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -44,6 +44,18 @@ const DaydreamEntryButton = (props) => (
   </button>
 );
 
+const AutoExitWarning = (props) => (
+  <div>
+    <p>
+    Exit in <span>{props.secondsRemaining}</span>
+    </p>
+    
+    <button onClick={props.onCancel}>
+    Cancel
+    </button>
+  </div>
+);
+
 // 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,
@@ -54,26 +66,39 @@ const DaydreamEntryButton = (props) => (
 // then we rely upon the user to select the proper mic.
 const VR_DEVICE_MIC_LABEL_REGEXES = [];
 
+const AUTO_EXIT_TIMER_SECONDS = 10;
+
 class UIRoot extends Component {
   static propTypes = {
     enterScene: PropTypes.func,
-    availableVREntryTypes: PropTypes.object
+    availableVREntryTypes: PropTypes.object,
+    concurrentLoadDetector: PropTypes.object,
   };
 
   state = {
     entryStep: ENTRY_STEPS.start,
-    shareScreen: false,
     enterInVR: false,
-    micDevices: [],
+
+    shareScreen: false,
     mediaStream: null,
+
     toneInterval: null,
     tonePlaying: false,
+
     micLevel: 0,
+    micDevices: [],
     micUpdateInterval: null,
+
+    autoExitTimerStartedAt: null,
+    autoExitTimerInterval: null,
+    secondsRemainingBeforeAutoExit: Infinity,
+
+    exited: false
   }
 
   componentDidMount() {
     this.setupTestTone();
+    this.props.concurrentLoadDetector.addEventListener("concurrentload", this.onConcurrentLoad);
   }
 
   setupTestTone = () => { 
@@ -108,6 +133,42 @@ class UIRoot extends Component {
     this.setState({ tonePlaying: false })
   }
 
+  onConcurrentLoad = () => {
+    const autoExitTimerInterval = setInterval(() => {
+      let secondsRemainingBeforeAutoExit = Infinity;
+
+      if (this.state.autoExitTimerStartedAt) {
+        const secondsSinceStart = (new Date() - this.state.autoExitTimerStartedAt) / 1000;
+        secondsRemainingBeforeAutoExit = Math.max(0, Math.floor(AUTO_EXIT_TIMER_SECONDS - secondsSinceStart));
+      }
+
+      this.setState({ secondsRemainingBeforeAutoExit });
+      this.checkForAutoExit();
+    }, 500);
+
+    this.setState({ autoExitTimerStartedAt: new Date(), autoExitTimerInterval })
+  }
+
+  checkForAutoExit = () => {
+    if (this.state.secondsRemainingBeforeAutoExit !== 0) return;
+    this.endAutoExitTimer();
+    this.exit();
+  }
+
+  exit = () => {
+    this.props.exitScene();
+    this.setState({ exited: true });
+  }
+
+  isWaitingForAutoExit = () => {
+    return this.state.secondsRemainingBeforeAutoExit <= AUTO_EXIT_TIMER_SECONDS;
+  }
+
+  endAutoExitTimer = () => {
+    clearInterval(this.state.autoExitTimerInterval);
+    this.setState({ autoExitTimerStartedAt: null, autoExitTimerInterval: null, secondsRemainingBeforeAutoExit: Infinity });
+  }
+
   performDirectEntryFlow = async (enterInVR) => {
     this.startTestTone();
 
@@ -274,14 +335,25 @@ class UIRoot extends Component {
         </div>
       ) : null;
 
-    return (
-      <div>
-        UI Here
+    const overlay = this.isWaitingForAutoExit() ?
+      (<AutoExitWarning secondsRemaining={this.state.secondsRemainingBeforeAutoExit} onCancel={this.endAutoExitTimer} />) :
+      (<div>
         {entryPanel}
         {micPanel}
         {audioSetupPanel}
-      </div>
-    );
+        </div>
+      );
+
+    return !this.state.exited ?
+      (
+        <div>
+          Base UI here
+          {overlay}
+        </div>
+      ) :
+      (
+        <div>Exited</div>
+      )
   }
 }
 
diff --git a/src/room.js b/src/room.js
index afe163d27..7b70b7aaa 100644
--- a/src/room.js
+++ b/src/room.js
@@ -70,6 +70,7 @@ import Store from "./storage/store";
 
 import { generateDefaultProfile } from "./utils/identity.js";
 import { getAvailableVREntryTypes } from "./utils/vr-caps-detect.js";
+import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js";
 
 AFRAME.registerInputBehaviour("vive_trackpad_dpad4", vive_trackpad_dpad4);
 AFRAME.registerInputBehaviour("oculus_touch_joystick_dpad4", oculus_touch_joystick_dpad4);
@@ -82,6 +83,8 @@ registerNetworkSchemas();
 registerTelemetry();
 
 const store = new Store();
+const concurrentLoadDetector = new ConcurrentLoadDetector();
+concurrentLoadDetector.start();
 
 // Always layer in any new default profile bits
 store.update({ profile:  { ...generateDefaultProfile(), ...(store.state.profile || {}) }})
@@ -112,6 +115,11 @@ async function shareMedia(audio, video) {
   }
 }
 
+async function exitScene() {
+  if (NAF.connection && NAF.connection.adapter) {
+    NAF.connection.disconnect();
+  }
+}
 
 async function enterScene(mediaStream) {
   const qs = queryString.parse(location.search);
@@ -186,7 +194,7 @@ function onConnect() {
 
 function mountUI() {
   getAvailableVREntryTypes().then(availableVREntryTypes => {
-    ReactDOM.render(<UIRoot {...{ availableVREntryTypes, enterScene }} />, document.getElementById("ui-root"));
+    ReactDOM.render(<UIRoot {...{ availableVREntryTypes, enterScene, exitScene, concurrentLoadDetector }} />, document.getElementById("ui-root"));
     document.getElementById("loader").style.display = "none";
   });
 }
diff --git a/src/utils/concurrent-load-detector.js b/src/utils/concurrent-load-detector.js
new file mode 100644
index 000000000..f05f619a3
--- /dev/null
+++ b/src/utils/concurrent-load-detector.js
@@ -0,0 +1,44 @@
+// Detects if another instance of ConcurrentLoadDetector is start()'ed by in the same local storage
+// context with the same instance key. Once a duplicate run is detected this will not fire any additional
+// events.
+
+const LOCAL_STORE_KEY = "___concurrent_load_detector";
+import { EventTarget } from "event-target-shim"
+
+export default class ConcurrentLoadDetector extends EventTarget {
+  constructor(instanceKey) {
+    super();
+
+    this.interval = null;
+    this.startedAt = null;
+    this.instanceKey = instanceKey || "global";
+  }
+
+  start = () => {
+    this.startedAt = new Date();
+    localStorage.setItem(this.localStorageKey(), JSON.stringify({ started_at: this.startedAt }));
+
+    // Check for concurrent load every second
+    this.interval = setInterval(this._step, 1000);
+  }
+
+  stop = () => {
+    if (this.interval) {
+      clearInterval(this.interval);
+    }
+  }
+
+  localStorageKey = () => {
+    return `${LOCAL_STORE_KEY}_${this.instanceKey}`;
+  }
+
+  _step = () => {
+    const currentState = JSON.parse(localStorage.getItem(this.localStorageKey()));
+    const maxStartedAt = new Date(currentState.started_at);
+
+    if (maxStartedAt.getTime() !== this.startedAt.getTime()) {
+      this.dispatchEvent(new CustomEvent("concurrentload"));
+      this.stop();
+    }
+  }
+}
diff --git a/yarn.lock b/yarn.lock
index ca49471c8..87fa58be5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2558,6 +2558,10 @@ etag@~1.8.1:
   version "1.8.1"
   resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
 
+event-target-shim@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-3.0.1.tgz#a4a62f0795e5b65363e86c6780413224d1eea688"
+
 eventemitter3@1.x.x:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
@@ -4396,9 +4400,9 @@ min-document@^2.19.0:
   dependencies:
     dom-walk "^0.1.0"
 
-minijanus@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/minijanus/-/minijanus-0.4.0.tgz#4d08529da795886b1aab6714ee7c9ff122c8c802"
+"minijanus@https://github.com/mozilla/minijanus.js#master":
+  version "0.5.0"
+  resolved "https://github.com/mozilla/minijanus.js#497f4dd80fdb92e247238e638daed14ae6623575"
 
 minimalistic-assert@^1.0.0:
   version "1.0.0"
@@ -4505,12 +4509,12 @@ mute-stream@0.0.7:
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
 
-naf-janus-adapter@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.4.0.tgz#22f14212a14d9e3d30c8d9441978704ff58392f4"
+"naf-janus-adapter@https://github.com/mozilla/naf-janus-adapter#feature/disconnect":
+  version "0.4.1"
+  resolved "https://github.com/mozilla/naf-janus-adapter#4a4532014d6489403cf7e451790925ce747f8e41"
   dependencies:
     debug "^3.1.0"
-    minijanus "^0.4.0"
+    minijanus "https://github.com/mozilla/minijanus.js#master"
 
 nan@^2.3.0:
   version "2.9.1"
-- 
GitLab