diff --git a/package.json b/package.json index f99e62b319a688c56cd4d53d7110e203d50fa31b..8b2851b5edcd8fc13942b1dc88882760fe7c4728 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,13 @@ "jsonschema": "^1.2.2", "minijanus": "^0.5.0", "mobile-detect": "^1.4.1", + "moment": "^2.22.0", + "moment-timezone": "^0.5.14", "moving-average": "^1.0.0", "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", + "phoenix": "^1.3.0", "query-string": "^5.0.1", "raven-js": "^3.20.1", "react": "^16.1.1", diff --git a/src/hub.js b/src/hub.js index 9dc3247e3a033ba8bb08ddad32a33a0c9a899e8b..00dec151186d9527f6e76773eca67f5e002bf433 100644 --- a/src/hub.js +++ b/src/hub.js @@ -1,5 +1,8 @@ import "./assets/stylesheets/hub.scss"; +import moment from "moment-timezone"; +import uuid from "uuid/v4"; import queryString from "query-string"; +import { Socket } from "phoenix"; import { patchWebGLRenderingContext } from "./utils/webgl"; patchWebGLRenderingContext(); @@ -50,6 +53,7 @@ import "./components/hud-controller"; import ReactDOM from "react-dom"; import React from "react"; import UIRoot from "./react-components/ui-root"; +import HubChannel from "./utils/hub-channel"; import "./systems/personal-space-bubble"; import "./systems/app-mode"; @@ -106,6 +110,7 @@ AFRAME.registerInputMappings(inputConfig, true); const store = new Store(); const concurrentLoadDetector = new ConcurrentLoadDetector(); +const hubChannel = new HubChannel(store); concurrentLoadDetector.start(); @@ -113,6 +118,7 @@ concurrentLoadDetector.start(); store.update({ profile: { ...generateDefaultProfile(), ...(store.state.profile || {}) } }); async function exitScene() { + hubChannel.disconnect(); const scene = document.querySelector("a-scene"); scene.renderer.animate(null); // Stop animation loop, TODO A-Frame should do this document.body.removeChild(scene); @@ -186,6 +192,12 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) { }); if (!qsTruthy("offline")) { + document.body.addEventListener("connected", () => { + hubChannel.sendEntryEvent().then(() => { + store.update({ lastEnteredAt: moment().toJSON() }); + }); + }); + scene.components["networked-scene"].connect(); if (mediaStream) { @@ -274,15 +286,32 @@ const onReady = async () => { return; } - const hubId = document.location.pathname.substring(1).split("/")[0]; + // Connect to reticulum over phoenix channels to get hub info. + const hubId = qs.hub_id || document.location.pathname.substring(1).split("/")[0]; console.log(`Hub ID: ${hubId}`); - const res = await fetch(`/api/v1/hubs/${hubId}`); - const data = await res.json(); - const hub = data.hubs[0]; - const defaultSpaceTopic = hub.topics[0]; - const gltfBundleUrl = defaultSpaceTopic.assets.find(a => a.asset_type === "gltf_bundle").src; - uiRoot.setState({ janusRoomId: defaultSpaceTopic.janus_room_id }); - initialEnvironmentEl.setAttribute("gltf-bundle", `src: ${gltfBundleUrl}`); + + const socketProtocol = document.location.protocol === "https:" ? "wss:" : "ws:"; + const socketPort = qs.phx_port || document.location.port; + const socketHost = qs.phx_host || document.location.hostname; + const socketUrl = `${socketProtocol}//${socketHost}${socketPort ? `:${socketPort}` : ""}/socket`; + console.log(`Phoenix Channel URL: ${socketUrl}`); + + const socket = new Socket(socketUrl, { params: { session_id: uuid() } }); + socket.connect(); + + const channel = socket.channel(`hub:${hubId}`, {}); + + channel + .join() + .receive("ok", data => { + const hub = data.hubs[0]; + const defaultSpaceTopic = hub.topics[0]; + const gltfBundleUrl = defaultSpaceTopic.assets.find(a => a.asset_type === "gltf_bundle").src; + uiRoot.setState({ janusRoomId: defaultSpaceTopic.janus_room_id }); + initialEnvironmentEl.setAttribute("gltf-bundle", `src: ${gltfBundleUrl}`); + hubChannel.setPhoenixChannel(channel); + }) + .receive("error", res => console.error(res)); }; document.addEventListener("DOMContentLoaded", onReady); diff --git a/src/storage/store.js b/src/storage/store.js index 6f480aa76db6f14904b611cecf5b0b6b84b1f41f..67afe7841ce5eda0454faed6020b49d8f125a403 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -27,7 +27,8 @@ export const SCHEMA = { properties: { id: { type: "string", pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" }, profile: { $ref: "#/definitions/profile" }, - lastUsedMicDeviceId: { type: "string" } + lastUsedMicDeviceId: { type: "string" }, + lastEnteredAt: { type: "string" } }, additionalProperties: false diff --git a/src/utils/hub-channel.js b/src/utils/hub-channel.js new file mode 100644 index 0000000000000000000000000000000000000000..13e51b21b1e1f3702432a7a04c6804949c332b88 --- /dev/null +++ b/src/utils/hub-channel.js @@ -0,0 +1,75 @@ +import moment from "moment-timezone"; + +export default class HubChannel { + constructor(store) { + this.store = store; + } + + setPhoenixChannel = channel => { + this.channel = channel; + }; + + sendEntryEvent = async () => { + if (!this.channel) { + console.warn("No phoenix channel initialized before room entry."); + return; + } + + let entryDisplayType = "Screen"; + + if (navigator.getVRDisplays) { + const vrDisplay = (await navigator.getVRDisplays()).find(d => d.isPresenting); + + if (vrDisplay) { + entryDisplayType = vrDisplay.displayName; + } + } + + // This is fairly hacky, but gets the # of initial occupants + let initialOccupantCount = 0; + + if (NAF.connection.adapter && NAF.connection.adapter.publisher) { + initialOccupantCount = NAF.connection.adapter.publisher.initialOccupants.length; + } + + const entryTimingFlags = this.getEntryTimingFlags(); + + const entryEvent = { + ...entryTimingFlags, + initialOccupantCount, + entryDisplayType, + userAgent: navigator.userAgent + }; + + this.channel.push("events:entered", entryEvent); + }; + + getEntryTimingFlags = () => { + const entryTimingFlags = { isNewDaily: true, isNewMonthly: true, isNewDayWindow: true, isNewMonthWindow: true }; + + if (!this.store.state.lastEnteredAt) { + return entryTimingFlags; + } + + const lastEntered = moment(this.store.state.lastEnteredAt); + const lastEnteredPst = moment(lastEntered).tz("America/Los_Angeles"); + const nowPst = moment().tz("America/Los_Angeles"); + const dayWindowAgo = moment().subtract(1, "day"); + const monthWindowAgo = moment().subtract(1, "month"); + + entryTimingFlags.isNewDaily = + lastEnteredPst.dayOfYear() !== nowPst.dayOfYear() || lastEnteredPst.year() !== nowPst.year(); + entryTimingFlags.isNewMonthly = + lastEnteredPst.month() !== nowPst.month() || lastEnteredPst.year() !== nowPst.year(); + entryTimingFlags.isNewDayWindow = lastEntered.isBefore(dayWindowAgo); + entryTimingFlags.isNewMonthWindow = lastEntered.isBefore(monthWindowAgo); + + return entryTimingFlags; + }; + + disconnect = () => { + if (this.channel) { + this.channel.socket.disconnect(); + } + }; +} diff --git a/yarn.lock b/yarn.lock index 00fd724ba4590d8fb8f3d2a313e089d43e88f030..2f53dc8b77829473fcf0cc14e978f0eb226eaaaa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5304,6 +5304,16 @@ module-deps@^6.0.0: through2 "^2.0.0" xtend "^4.0.0" +moment-timezone@^0.5.14: + version "0.5.14" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.14.tgz#4eb38ff9538b80108ba467a458f3ed4268ccfcb1" + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@^2.22.0: + version "2.22.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.0.tgz#7921ade01017dd45186e7fee5f424f0b8663a730" + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -6010,6 +6020,10 @@ performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" +phoenix@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.3.0.tgz#1df2c27f986ee295e37c9983ec28ebac1d7f4a3e" + pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"