diff --git a/package.json b/package.json
index f99e62b319a688c56cd4d53d7110e203d50fa31b..a97713d44790787795d671f30d6f5d9a384ac8ae 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",
+    "naf-janus-adapter": "^0.5.2",
     "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/assets/images/dropdown_arrow.png b/src/assets/images/dropdown_arrow.png
new file mode 100755
index 0000000000000000000000000000000000000000..caa42c1ffed82796540acdc192201cf20e822e0b
Binary files /dev/null and b/src/assets/images/dropdown_arrow.png differ
diff --git a/src/assets/images/dropdown_arrow@2x.png b/src/assets/images/dropdown_arrow@2x.png
new file mode 100755
index 0000000000000000000000000000000000000000..d4e74eb212652021837a17d860578c6f7114dcd5
Binary files /dev/null and b/src/assets/images/dropdown_arrow@2x.png differ
diff --git a/src/assets/images/level_background.png b/src/assets/images/level_background.png
new file mode 100755
index 0000000000000000000000000000000000000000..9d53b3c6dc75552b225d5717fa6fb8cd883b05cb
Binary files /dev/null and b/src/assets/images/level_background.png differ
diff --git a/src/assets/images/level_background@2x.png b/src/assets/images/level_background@2x.png
new file mode 100755
index 0000000000000000000000000000000000000000..4a9f08acc76396f4133673faed5b4ae38ca6cc87
Binary files /dev/null and b/src/assets/images/level_background@2x.png differ
diff --git a/src/assets/images/level_fill.png b/src/assets/images/level_fill.png
old mode 100644
new mode 100755
index 99f77b5655e6a50e0444364a3c2cfb4882b3b2d9..49a4f8a75064870db870bf994e8b25671205bfb0
Binary files a/src/assets/images/level_fill.png and b/src/assets/images/level_fill.png differ
diff --git a/src/assets/images/level_fill@2x.png b/src/assets/images/level_fill@2x.png
old mode 100644
new mode 100755
index 477d9801bb6d33737b571fce454ff265ff79e77c..28f313bc9d541fc92fd65c03945b35c1affdf9cb
Binary files a/src/assets/images/level_fill@2x.png and b/src/assets/images/level_fill@2x.png differ
diff --git a/src/assets/images/mic_level.png b/src/assets/images/mic_level.png
old mode 100644
new mode 100755
index 5be15458d9ed41c46f861d8dd8435a11e452f80c..e4c1367ddf78efd48173a3d0a64c4c48c953a871
Binary files a/src/assets/images/mic_level.png and b/src/assets/images/mic_level.png differ
diff --git a/src/assets/images/mic_level@2x.png b/src/assets/images/mic_level@2x.png
old mode 100644
new mode 100755
index 94739aa1977cc5d5317eeb770905ed212ff248b4..621f944ed0b07b1a625a2627f5646406fcefbd98
Binary files a/src/assets/images/mic_level@2x.png and b/src/assets/images/mic_level@2x.png differ
diff --git a/src/assets/images/speaker_level.png b/src/assets/images/speaker_level.png
old mode 100644
new mode 100755
index 9ccedcc0350f90c95744d928128594829b5f5b90..f0557615258997bb7c54e7a6028e052c9c8a33f4
Binary files a/src/assets/images/speaker_level.png and b/src/assets/images/speaker_level.png differ
diff --git a/src/assets/images/speaker_level@2x.png b/src/assets/images/speaker_level@2x.png
old mode 100644
new mode 100755
index a807745cbcaaf823e6e8e99deda15459d1ed1d9a..3d60f4b8d287742ad3076ae7e63f988cca029f89
Binary files a/src/assets/images/speaker_level@2x.png and b/src/assets/images/speaker_level@2x.png differ
diff --git a/src/assets/stylesheets/audio.scss b/src/assets/stylesheets/audio.scss
index 67aac69962d861e993fb0e58e5dec9441536d2a9..d73a85eddc926c4b9a2f58c21fd4a97521822fd8 100644
--- a/src/assets/stylesheets/audio.scss
+++ b/src/assets/stylesheets/audio.scss
@@ -28,17 +28,29 @@
       @extend %rounded-border;
       @extend %default-font;
 
+      appearance: none;
+      -moz-appearance: none;
+      -webkit-appearance: none;
       background-color: black;
       padding: 6px;
+      padding-right: 30px;
       color: white;
       font-size: 1.1em;
       width: 90%;
     }
 
     &__mic-icon {
+      pointer-events: none;
       position: absolute;
-      left: 7.5%;
-      top: 10px;
+      left: 8%;
+      top: 9px;
+    }
+
+    &__dropdown-arrow {
+      pointer-events: none;
+      position: absolute;
+      right: 7.5%;
+      top: 16px;
     }
   }
 
@@ -50,42 +62,16 @@
     align-items: center;
     width: 100%;
 
-    &__mic {
-      position:relative;
-      width: 111px;
-      height: 111px;
-    }
-
-    &__mic_icon {
-      position: absolute;
-      top: 0;
-      left: 0;
-      z-index: 2;
-      min-width: 111px;
-      min-height: 111px;
-    }
-
-    &__speaker {
+    &__icon {
       position:relative;
       width: 111px;
       height: 111px;
     }
 
-    &__speaker_icon {
-      position: absolute;
+    &__icon-part {
+      position:absolute;
       top: 0;
       left: 0;
-      z-index: 2;
-      min-width: 111px;
-      min-height: 111px;
-    }
-
-    &__level {
-      position: absolute;
-      top: 0;
-      left: 0;
-      opacity: 1.0;
-      z-index: 1;
     }
   }
 
@@ -118,17 +104,26 @@
     @extend %top-subtitle;
   }
 
-  &__icon {
+  &__button-container {
     flex: 10;
     display: flex;
     justify-content: center;
     align-items: center;
     cursor: pointer;
+    width: 111px;
+    height: 111px;
+  }
+
+  &__button {
+    background: none;
+    border: none;
+    cursor: pointer;
   }
 
   &__next {
     @extend %bottom-button;
-    margin: auto;
-    flex: 1 1 20px;
+    padding-top: 0;
+    padding-bottom: 0;
+    flex: 1 1;
   }
 }
diff --git a/src/assets/stylesheets/entry.scss b/src/assets/stylesheets/entry.scss
index bdd20d1ee7af717ac0b2808b696c85fe49c8d775..7c7d18c840410378a86d9a4b41211777808a60b0 100644
--- a/src/assets/stylesheets/entry.scss
+++ b/src/assets/stylesheets/entry.scss
@@ -20,28 +20,18 @@
   justify-content: center;
 
   &__screen-sharing {
-	font-size: 1.4em;
-	margin-left: 2.95em;
-	margin-top: 0.6em;
-  }
+    font-size: 1.4em;
+    margin-left: 2.95em;
+    margin-top: 0.6em;
 
-  &__screen-sharing-checkbox {
-	appearance: none;
-	-moz-appearance: none;
-	-webkit-appearance: none;
-	width: 2em;
-	height: 2em;
-	border: 3px solid white;
-	border-radius: 9px;
-	vertical-align: sub;
-	margin: 0 0.6em
+    &__checkbox {
+      @extend %checkbox;
+    }
+    &__checkbox:checked {
+      @extend %checkbox-checked;
+    }
   }
 
-  &__screen-sharing-checkbox:checked {
-	border: 9px double white;
-	outline: 9px solid white;
-	outline-offset: -18px;
-  }
 
   &__secondary {
     width: 100%;
@@ -58,6 +48,11 @@
   margin-top: 10px;
   margin-bottom: 10px;
   cursor: pointer;
+  background: none;
+  color: white;
+  border: none;
+  align-items: center;
+  @extend %default-font;
 
   &__icon {
     flex: 1 1 90px;
diff --git a/src/assets/stylesheets/exited.scss b/src/assets/stylesheets/exited.scss
index 72959090e6cf5ed18d29c2750beb7bbb4280fcae..693d6d38798705979478930f0b175ea57aa6e183 100644
--- a/src/assets/stylesheets/exited.scss
+++ b/src/assets/stylesheets/exited.scss
@@ -1,4 +1,6 @@
 .exited-panel {
+  position: absolute;
+  color: white;
   background-color: black;
   width: 100%;
   height: 100%;
diff --git a/src/assets/stylesheets/profile.scss b/src/assets/stylesheets/profile.scss
index 000e974bbad3aec3760eb51e9863c78755a590fc..95f2caa2629d34e3f99fbab242f5cbf28b498038 100644
--- a/src/assets/stylesheets/profile.scss
+++ b/src/assets/stylesheets/profile.scss
@@ -41,6 +41,10 @@
     color: $grey-text;
   }
 
+  &__display-name-label {
+    font-size: 1.2em;
+    margin-right: 0.5em;
+  }
   &__form-field-text {
     @extend %rounded-border;
     @extend %default-font;
@@ -54,19 +58,34 @@
     margin: 0.5em 0;
   }
 
-  &__form-submit {
-    @extend %default-font;
-    border: none;
+  &__terms {
+    margin-bottom: 16px;
 
-    margin: 8px;
-    width: 100px;
-    line-height: 1.5em;
-    font-size: 1.0em;
+    &__checkbox {
+      @extend %checkbox;
+      vertical-align: unset;
+    }
+    &__checkbox:checked {
+      @extend %checkbox-checked;
+    }
 
-    background-color: transparent;
-    font-weight: bold;
-    color: white;
-    cursor: pointer;
+    &__text {
+      display: inline-block;
+      max-width: 20em;
+    }
+
+    &__link {
+      color: white;
+    }
+
+    &__link:visited {
+      color: grey;
+    }
+  }
+
+  &__form-submit {
+    @extend %bottom-button;
+    margin: 0;
   }
 }
 
@@ -87,12 +106,16 @@
     flex: 6 1 auto;
     font-size: 1.2em;
     line-height: 50px;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: nowrap;
   }
 
   &__app_name {
     font-size: 1.8em;
     padding-right: 18px;
     line-height: 50px;
+    white-space: nowrap;
   }
 }
 
diff --git a/src/assets/stylesheets/shared.scss b/src/assets/stylesheets/shared.scss
index c2d4f013de5b853664b751400969602b356a53f9..f959943585bbfacf37791586f5485add3e524cd1 100644
--- a/src/assets/stylesheets/shared.scss
+++ b/src/assets/stylesheets/shared.scss
@@ -17,11 +17,17 @@ $darker-grey: rgba(64, 64, 64, 1.0);
 }
 
 %bottom-button {
+  @extend %default-font;
   font-size: 1em;
   font-weight: bold;
   margin-top: auto;
   margin-bottom: 30px;
   cursor: pointer;
+  border: 3px solid white;
+  border-radius: 14px;
+  padding: 12px;
+  background: none;
+  color: white;
 }
 
 %top-title {
@@ -42,3 +48,21 @@ $darker-grey: rgba(64, 64, 64, 1.0);
   border: none;
   font-size: 64pt;
 }
+
+%checkbox {
+  appearance: none;
+  -moz-appearance: none;
+  -webkit-appearance: none;
+  width: 2em;
+  height: 2em;
+  border: 3px solid white;
+  border-radius: 9px;
+  vertical-align: sub;
+  margin: 0 0.6em
+}
+
+%checkbox-checked {
+  border: 9px double white;
+  outline: 9px solid white;
+  outline-offset: -18px;
+}
diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json
index 48bd0d1b4db58dfc385d902cd38b69e1ed2945a4..9948dfdc528fbdd8fdc3881ef59e0d2543ae18ca 100644
--- a/src/assets/translations.data.json
+++ b/src/assets/translations.data.json
@@ -13,8 +13,14 @@
     "entry.daydream-via-chrome": "Using Google Chrome",
     "entry.enable-screen-sharing": "Share my desktop",
     "profile.save": "SAVE",
+    "profile.display_name.label": "Display name:",
     "profile.display_name.validation_warning": "Alphanumerics and hyphens. At least 3 characters, no more than 32",
     "profile.header": "Your identity",
+    "profile.terms.prefix": "I confirm that I am over the age of 13 and agree to the",
+    "profile.terms.privacy": "privacy policy",
+    "profile.terms.conjunction": "and",
+    "profile.terms.tou": "terms of use",
+    "profile.terms.suffix": ".",
     "profile.avatar-selector.loading": "Loading Avatars...",
     "audio.title": "Test your audio",
     "audio.subtitle-desktop": "Confirm HMD speaker output",
@@ -25,7 +31,6 @@
     "audio.grant-subtitle": "Mic access needed to be heard by others",
     "audio.granted-title": "Mic permissions granted",
     "audio.granted-subtitle": "You can still mute yourself in-game",
-    "audio.grant-next": "  ",
     "audio.granted-next": "NEXT",
     "exit.subtitle": "Your session has ended. Refresh your browser to start a new one.",
     "autoexit.title": "Auto-ending session in ",
diff --git a/src/hub.html b/src/hub.html
index 0f6df33ece622b7d5f3bea8ff007628213623893..edb7395d4641f10c27994b3399dc580e72f6952b 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -3,10 +3,11 @@
 
 <head>
     <meta charset="utf-8">
-    <title>moz://a duck</title>
-
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <meta http-equiv="origin-trial" data-feature="WebVR (For Chrome M62+)" data-expires="<%= ORIGIN_TRIAL_EXPIRES %>" content="<%= ORIGIN_TRIAL_TOKEN %>">
+    <title>moz://a duck</title>
+    <link href="https://fonts.googleapis.com/css?family=Zilla+Slab:300,300i,400,400i,700" rel="stylesheet">
+
     <% if(NODE_ENV === "production") { %>
         <script src="https://cdn.rawgit.com/brianpeiris/aframe/845825ae694449524c185c44a314d361eead4680/dist/aframe-master.min.js"></script>
     <% } else { %>
diff --git a/src/hub.js b/src/hub.js
index af1dcced39aa0ec0ad2c52a22a6136595e1f2d25..07adb361c6dffd822e253d9cb50be4b15fc921e6 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";
@@ -87,7 +91,7 @@ import { inGameActions, config as inputConfig } from "./input-mappings";
 import registerTelemetry from "./telemetry";
 import Store from "./storage/store";
 
-import { generateDefaultProfile } from "./utils/identity.js";
+import { generateDefaultProfile, generateRandomName } from "./utils/identity.js";
 import { getAvailableVREntryTypes } from "./utils/vr-caps-detect.js";
 import ConcurrentLoadDetector from "./utils/concurrent-load-detector.js";
 
@@ -107,16 +111,23 @@ AFRAME.registerInputMappings(inputConfig, true);
 
 const store = new Store();
 const concurrentLoadDetector = new ConcurrentLoadDetector();
+const hubChannel = new HubChannel(store);
 
 concurrentLoadDetector.start();
 
 // Always layer in any new default profile bits
 store.update({ profile: { ...generateDefaultProfile(), ...(store.state.profile || {}) } });
 
+// Regenerate name to encourage users to change it.
+if (!store.state.profile.has_changed_name) {
+  store.update({ profile: { display_name: generateRandomName() } });
+}
+
 async function exitScene() {
   if (NAF.connection.adapter && NAF.connection.adapter.localMediaStream) {
     NAF.connection.adapter.localMediaStream.getTracks().forEach(t => t.stop());
   }
+  hubChannel.disconnect();
   const scene = document.querySelector("a-scene");
   scene.renderer.animate(null); // Stop animation loop, TODO A-Frame should do this
   document.body.removeChild(scene);
@@ -190,6 +201,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) {
@@ -213,15 +230,14 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) {
   }
 }
 
-function mountUI(scene) {
+function mountUI(scene, props = {}) {
   const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi");
   const forcedVREntryType = qs.vr_entry_type || null;
   const enableScreenSharing = qsTruthy("enable_screen_sharing");
   const htmlPrefix = document.body.dataset.htmlPrefix || "";
+  const showProfileEntry = !store.state.profile.has_changed_name;
 
-  // TODO: Refactor to avoid using return value
-  /* eslint-disable react/no-render-return-value */
-  const uiRoot = ReactDOM.render(
+  ReactDOM.render(
     <UIRoot
       {...{
         scene,
@@ -232,14 +248,13 @@ function mountUI(scene) {
         forcedVREntryType,
         enableScreenSharing,
         store,
-        htmlPrefix
+        htmlPrefix,
+        showProfileEntry,
+        ...props
       }}
     />,
     document.getElementById("ui-root")
   );
-  /* eslint-enable react/no-render-return-value */
-
-  return uiRoot;
 }
 
 const onReady = async () => {
@@ -249,26 +264,31 @@ const onReady = async () => {
 
   registerNetworkSchemas();
 
-  const uiRoot = mountUI(scene);
+  mountUI(scene);
+
+  let modifiedProps = {};
+  const remountUI = props => {
+    modifiedProps = { ...modifiedProps, ...props };
+    mountUI(scene, modifiedProps);
+  };
 
   getAvailableVREntryTypes().then(availableVREntryTypes => {
-    uiRoot.setState({ availableVREntryTypes });
-    uiRoot.handleForcedVREntryType();
+    remountUI({ availableVREntryTypes });
   });
 
   const environmentRoot = document.querySelector("#environment-root");
 
   const initialEnvironmentEl = document.createElement("a-entity");
   initialEnvironmentEl.addEventListener("bundleloaded", () => {
-    uiRoot.setState({ initialEnvironmentLoaded: true });
-    // Wait a tick so that the environments actually render.
-    setTimeout(() => scene.renderer.animate(null));
+    remountUI({ initialEnvironmentLoaded: true });
+    // Wait a tick plus some margin so that the environments actually render.
+    setTimeout(() => scene.renderer.animate(null), 100);
   });
   environmentRoot.appendChild(initialEnvironmentEl);
 
   if (qs.room) {
     // If ?room is set, this is `yarn start`, so just use a default environment and query string room.
-    uiRoot.setState({ janusRoomId: qs.room && !isNaN(parseInt(qs.room)) ? parseInt(qs.room) : 1 });
+    remountUI({ janusRoomId: qs.room && !isNaN(parseInt(qs.room)) ? parseInt(qs.room) : 1 });
     initialEnvironmentEl.setAttribute("gltf-bundle", {
       src: "https://asset-bundles-prod.reticulum.io/rooms/meetingroom/MeetingRoom.bundle.json"
       // src: "https://asset-bundles-prod.reticulum.io/rooms/theater/TheaterMeshes.bundle.json"
@@ -278,15 +298,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;
+      remountUI({ 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/react-components/avatar-selector.js b/src/react-components/avatar-selector.js
index 528d5b81558e37f12aea6a0182c4cc08d8971782..6ae6ab6ac91110ae8cc65b9d5b5f2db8d075f911 100644
--- a/src/react-components/avatar-selector.js
+++ b/src/react-components/avatar-selector.js
@@ -39,7 +39,7 @@ class AvatarSelector extends Component {
       // so we need to force it here.
       const currRot = this.animation.parentNode.getAttribute("rotation");
       const currY = currRot.y;
-      const toRot = String.split(this.animation.attributes.to.value, " ");
+      const toRot = this.animation.getAttribute("to").split(" ");
       const toY = toRot[1];
       const step = 360.0 / this.props.avatars.length;
       const brokenlyBigRotation = Math.abs(toY - currY) > 3 * step;
diff --git a/src/react-components/entry-buttons.js b/src/react-components/entry-buttons.js
index 8ed8b171d6e7ec08d82d59af2c5314d9353a7910..92d0ef5ef8ccd4992d9913bfb30144fff018c0b9 100644
--- a/src/react-components/entry-buttons.js
+++ b/src/react-components/entry-buttons.js
@@ -12,7 +12,7 @@ import DaydreamEntyImg from "../assets/images/daydream_entry.svg";
 const mobiledetect = new MobileDetect(navigator.userAgent);
 
 const EntryButton = props => (
-  <div className="entry-button" onClick={props.onClick}>
+  <button className="entry-button" onClick={props.onClick}>
     <img src={props.iconSrc} className="entry-button__icon" />
     <div className="entry-button__label">
       <div className="entry-button__label__contents">
@@ -25,7 +25,7 @@ const EntryButton = props => (
         {props.subtitle && <div className="entry-button__subtitle">{props.subtitle}</div>}
       </div>
     </div>
-  </div>
+  </button>
 );
 
 EntryButton.propTypes = {
diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js
index 523016e25bdf1e0f127bf5a889165b1ede806322..2732f3ca9bd173cc3390bd1af05316b8f6614747 100644
--- a/src/react-components/profile-entry-panel.js
+++ b/src/react-components/profile-entry-panel.js
@@ -14,10 +14,8 @@ class ProfileEntryPanel extends Component {
 
   constructor(props) {
     super(props);
-    this.state = {
-      display_name: this.props.store.state.profile.display_name,
-      avatar_id: this.props.store.state.profile.avatar_id
-    };
+    const { display_name, avatar_id } = this.props.store.state.profile;
+    this.state = { display_name, avatar_id };
     this.props.store.addEventListener("statechanged", this.storeUpdated);
   }
 
@@ -28,10 +26,15 @@ class ProfileEntryPanel extends Component {
 
   saveStateAndFinish = e => {
     e.preventDefault();
+    const has_agreed_to_terms = this.props.store.state.profile.has_agreed_to_terms || this.state.has_agreed_to_terms;
+    if (!has_agreed_to_terms) return;
+    const { has_changed_name, display_name } = this.props.store.state.profile;
+    const hasChangedName = has_changed_name || this.state.display_name !== display_name;
     this.props.store.update({
       profile: {
-        display_name: this.state.display_name,
-        avatar_id: this.state.avatar_id
+        has_agreed_to_terms: true,
+        has_changed_name: hasChangedName,
+        ...this.state
       }
     });
     this.props.finished();
@@ -74,20 +77,47 @@ class ProfileEntryPanel extends Component {
             <div className="profile-entry__subtitle">
               <FormattedMessage id="profile.header" />
             </div>
-            <input
-              className="profile-entry__form-field-text"
-              value={this.state.display_name}
-              onChange={e => this.setState({ display_name: e.target.value })}
-              required
-              pattern={SCHEMA.definitions.profile.properties.display_name.pattern}
-              title={formatMessage({ id: "profile.display_name.validation_warning" })}
-              ref={inp => (this.nameInput = inp)}
-            />
+            <label>
+              <span className="profile-entry__display-name-label">
+                <FormattedMessage id="profile.display_name.label" />
+              </span>
+              <input
+                className="profile-entry__form-field-text"
+                value={this.state.display_name}
+                onChange={e => this.setState({ display_name: e.target.value })}
+                required
+                pattern={SCHEMA.definitions.profile.properties.display_name.pattern}
+                title={formatMessage({ id: "profile.display_name.validation_warning" })}
+                ref={inp => (this.nameInput = inp)}
+              />
+            </label>
             <iframe
               className="profile-entry__avatar-selector"
               src={`/${this.props.htmlPrefix}avatar-selector.html#avatar_id=${this.state.avatar_id}`}
               ref={ifr => (this.avatarSelector = ifr)}
             />
+            {!this.props.store.state.profile.has_agreed_to_terms && (
+              <label className="profile-entry__terms">
+                <input
+                  className="profile-entry__terms__checkbox"
+                  type="checkbox"
+                  required
+                  value={this.state.has_agreed_to_terms}
+                  onChange={e => this.setState({ has_agreed_to_terms: e.target.checked })}
+                />
+                <span className="profile-entry__terms__text">
+                  <FormattedMessage id="profile.terms.prefix" />{" "}
+                  <a className="profile-entry__terms__link" target="_blank" href="/privacy">
+                    <FormattedMessage id="profile.terms.privacy" />
+                  </a>{" "}
+                  <FormattedMessage id="profile.terms.conjunction" />{" "}
+                  <a className="profile-entry__terms__link" target="_blank" href="/terms">
+                    <FormattedMessage id="profile.terms.tou" />
+                  </a>
+                  <FormattedMessage id="profile.terms.suffix" />
+                </span>
+              </label>
+            )}
             <input className="profile-entry__form-submit" type="submit" value={formatMessage({ id: "profile.save" })} />
           </div>
         </form>
diff --git a/src/react-components/profile-info-header.js b/src/react-components/profile-info-header.js
index 43ec49291007089d54e1d1a52c7a586e766e5fb4..ca7a3b891c3c99cc3aca753244ba4289abe525b4 100644
--- a/src/react-components/profile-info-header.js
+++ b/src/react-components/profile-info-header.js
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
 export const ProfileInfoHeader = props => (
   <div className="profile-info-header">
     <img src="../assets/images/account.svg" onClick={props.onClick} className="profile-info-header__icon" />
-    <div className="profile-info-header__profile_display_name" onClick={props.onClick}>
+    <div className="profile-info-header__profile_display_name" onClick={props.onClick} title={props.name}>
       {props.name}
     </div>
     <div className="profile-info-header__app_name">
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index 39983ef6baa59f529ccc0c9ed985c1efad4b7955..2883d23f066498e941d561b6722b9fc5b434590c 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -60,11 +60,14 @@ class UIRoot extends Component {
     enableScreenSharing: PropTypes.bool,
     store: PropTypes.object,
     scene: PropTypes.object,
-    htmlPrefix: PropTypes.string
+    htmlPrefix: PropTypes.string,
+    showProfileEntry: PropTypes.bool,
+    availableVREntryTypes: PropTypes.object,
+    initialEnvironmentLoaded: PropTypes.bool,
+    janusRoomId: PropTypes.number
   };
 
   state = {
-    availableVREntryTypes: null,
     entryStep: ENTRY_STEPS.start,
     enterInVR: false,
 
@@ -87,14 +90,16 @@ class UIRoot extends Component {
     autoExitTimerInterval: null,
     secondsRemainingBeforeAutoExit: Infinity,
 
-    initialEnvironmentLoaded: false,
     exited: false,
 
-    showProfileEntry: false,
-
-    janusRoomId: null
+    showProfileEntry: false
   };
 
+  constructor(props) {
+    super(props);
+    this.state.showProfileEntry = this.props.showProfileEntry;
+  }
+
   componentDidMount() {
     this.setupTestTone();
     this.props.concurrentLoadDetector.addEventListener("concurrentload", this.onConcurrentLoad);
@@ -110,6 +115,12 @@ class UIRoot extends Component {
     this.props.scene.removeEventListener("exit", this.exit);
   }
 
+  componentDidUpdate(prevProps) {
+    if (this.props.availableVREntryTypes && prevProps.availableVREntryTypes !== this.props.availableVREntryTypes) {
+      this.handleForcedVREntryType();
+    }
+  }
+
   onSceneLoaded = () => {
     this.setState({ sceneLoaded: true });
   };
@@ -252,7 +263,7 @@ class UIRoot extends Component {
   };
 
   enterGearVR = async () => {
-    if (this.state.availableVREntryTypes.gearvr === VR_DEVICE_AVAILABILITY.yes) {
+    if (this.props.availableVREntryTypes.gearvr === VR_DEVICE_AVAILABILITY.yes) {
       await this.performDirectEntryFlow(true);
     } else {
       this.exit();
@@ -271,7 +282,7 @@ class UIRoot extends Component {
   };
 
   enterDaydream = async () => {
-    if (this.state.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe) {
+    if (this.props.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe) {
       this.exit();
 
       // We are not in mobile chrome, so launch into chrome via an Intent URL
@@ -368,23 +379,26 @@ class UIRoot extends Component {
       const AudioContext = window.AudioContext || window.webkitAudioContext;
       const micLevelAudioContext = new AudioContext();
       const micSource = micLevelAudioContext.createMediaStreamSource(mediaStream);
-      const analyzer = micLevelAudioContext.createAnalyser();
-      const levels = new Uint8Array(analyzer.fftSize);
+      const analyser = micLevelAudioContext.createAnalyser();
+      analyser.fftSize = 32;
+      const levels = new Uint8Array(analyser.frequencyBinCount);
 
-      micSource.connect(analyzer);
+      micSource.connect(analyser);
 
       const micUpdateInterval = setInterval(() => {
-        analyzer.getByteTimeDomainData(levels);
-
+        analyser.getByteTimeDomainData(levels);
         let v = 0;
-
         for (let x = 0; x < levels.length; x++) {
-          v = Math.max(levels[x] - 127, v);
+          v = Math.max(levels[x] - 128, v);
         }
-
         const level = v / 128.0;
-        this.micLevelMovingAverage.push(Date.now(), level);
-        this.setState({ micLevel: this.micLevelMovingAverage.movingAverage() });
+        // Multiplier to increase visual indicator.
+        const multiplier = 6;
+        // We use a moving average to smooth out the visual animation or else it would twitch too fast for
+        // the css renderer to keep up.
+        this.micLevelMovingAverage.push(Date.now(), level * multiplier);
+        const average = this.micLevelMovingAverage.movingAverage();
+        this.setState({ micLevel: average });
       }, 50);
 
       const micDeviceId = this.micDeviceIdForMicLabel(this.micLabelForMediaStream(mediaStream));
@@ -466,7 +480,7 @@ class UIRoot extends Component {
   };
 
   onAudioReadyButton = () => {
-    this.props.enterScene(this.state.mediaStream, this.state.enterInVR, this.state.janusRoomId);
+    this.props.enterScene(this.state.mediaStream, this.state.enterInVR, this.props.janusRoomId);
 
     const mediaStream = this.state.mediaStream;
 
@@ -491,7 +505,7 @@ class UIRoot extends Component {
   };
 
   render() {
-    if (!this.state.initialEnvironmentLoaded || !this.state.availableVREntryTypes || !this.state.janusRoomId) {
+    if (!this.props.initialEnvironmentLoaded || !this.props.availableVREntryTypes || !this.props.janusRoomId) {
       return (
         <IntlProvider locale={lang} messages={messages}>
           <div className="loading-panel">
@@ -532,7 +546,7 @@ class UIRoot extends Component {
       /firefox/i.test(navigator.userAgent) && (
         <label className="entry-panel__screen-sharing">
           <input
-            className="entry-panel__screen-sharing-checkbox"
+            className="entry-panel__screen-sharing__checkbox"
             type="checkbox"
             value={this.state.shareScreen}
             onChange={this.setStateAndRequestScreen}
@@ -545,21 +559,21 @@ class UIRoot extends Component {
       this.state.entryStep === ENTRY_STEPS.start ? (
         <div className="entry-panel">
           <TwoDEntryButton onClick={this.enter2D} />
-          {this.state.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && (
+          {this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && (
             <GenericEntryButton onClick={this.enterVR} />
           )}
-          {this.state.availableVREntryTypes.gearvr !== VR_DEVICE_AVAILABILITY.no && (
+          {this.props.availableVREntryTypes.gearvr !== VR_DEVICE_AVAILABILITY.no && (
             <GearVREntryButton onClick={this.enterGearVR} />
           )}
-          {this.state.availableVREntryTypes.daydream !== VR_DEVICE_AVAILABILITY.no && (
+          {this.props.availableVREntryTypes.daydream !== VR_DEVICE_AVAILABILITY.no && (
             <DaydreamEntryButton
               onClick={this.enterDaydream}
               subtitle={
-                this.state.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe ? daydreamMaybeSubtitle : ""
+                this.props.availableVREntryTypes.daydream == VR_DEVICE_AVAILABILITY.maybe ? daydreamMaybeSubtitle : ""
               }
             />
           )}
-          {this.state.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && (
+          {this.props.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && (
             <div className="entry-panel__secondary" onClick={this.enterVR}>
               <FormattedMessage id="entry.cardboard" />
             </div>
@@ -581,28 +595,22 @@ class UIRoot extends Component {
               id={this.state.entryStep == ENTRY_STEPS.mic_grant ? "audio.grant-subtitle" : "audio.granted-subtitle"}
             />
           </div>
-          <div className="mic-grant-panel__icon">
+          <div className="mic-grant-panel__button-container">
             {this.state.entryStep == ENTRY_STEPS.mic_grant ? (
-              <img
-                onClick={this.onMicGrantButton}
-                src="../assets/images/mic_denied.png"
-                srcSet="../assets/images/mic_denied@2x.png 2x"
-                className="mic-grant-panel__icon"
-              />
+              <button className="mic-grant-panel__button" onClick={this.onMicGrantButton}>
+                <img src="../assets/images/mic_denied.png" srcSet="../assets/images/mic_denied@2x.png 2x" />
+              </button>
             ) : (
-              <img
-                onClick={this.onMicGrantButton}
-                src="../assets/images/mic_granted.png"
-                srcSet="../assets/images/mic_granted@2x.png 2x"
-                className="mic-grant-panel__icon"
-              />
+              <button className="mic-grant-panel__button" onClick={this.onMicGrantButton}>
+                <img src="../assets/images/mic_granted.png" srcSet="../assets/images/mic_granted@2x.png 2x" />
+              </button>
             )}
           </div>
-          <div className="mic-grant-panel__next" onClick={this.onMicGrantButton}>
-            <FormattedMessage
-              id={this.state.entryStep == ENTRY_STEPS.mic_grant ? "audio.grant-next" : "audio.granted-next"}
-            />
-          </div>
+          {this.state.entryStep == ENTRY_STEPS.mic_granted && (
+            <button className="mic-grant-panel__next" onClick={this.onMicGrantButton}>
+              <FormattedMessage id="audio.granted-next" />
+            </button>
+          )}
         </div>
       ) : null;
 
@@ -624,39 +632,49 @@ class UIRoot extends Component {
             )}
           </div>
           <div className="audio-setup-panel__levels">
-            <div className="audio-setup-panel__levels__mic">
+            <div className="audio-setup-panel__levels__icon">
+              <img
+                src="../assets/images/level_background.png"
+                srcSet="../assets/images/level_background@2x.png 2x"
+                className="audio-setup-panel__levels__icon-part"
+              />
+              <img
+                src="../assets/images/level_fill.png"
+                srcSet="../assets/images/level_fill@2x.png 2x"
+                className="audio-setup-panel__levels__icon-part"
+                style={micClip}
+              />
               {this.state.audioTrack ? (
                 <img
                   src="../assets/images/mic_level.png"
                   srcSet="../assets/images/mic_level@2x.png 2x"
-                  className="audio-setup-panel__levels__mic_icon"
+                  className="audio-setup-panel__levels__icon-part"
                 />
               ) : (
                 <img
                   src="../assets/images/mic_denied.png"
                   srcSet="../assets/images/mic_denied@2x.png 2x"
-                  className="audio-setup-panel__levels__mic_icon"
+                  className="audio-setup-panel__levels__icon-part"
                 />
               )}
-              <img
-                src="../assets/images/level_fill.png"
-                srcSet="../assets/images/level_fill@2x.png 2x"
-                className="audio-setup-panel__levels__level"
-                style={micClip}
-              />
             </div>
-            <div className="audio-setup-panel__levels__speaker">
+            <div className="audio-setup-panel__levels__icon">
               <img
-                src="../assets/images/speaker_level.png"
-                srcSet="../assets/images/speaker_level@2x.png 2x"
-                className="audio-setup-panel__levels__speaker_icon"
+                src="../assets/images/level_background.png"
+                srcSet="../assets/images/level_background@2x.png 2x"
+                className="audio-setup-panel__levels__icon-part"
               />
               <img
                 src="../assets/images/level_fill.png"
                 srcSet="../assets/images/level_fill@2x.png 2x"
-                className="audio-setup-panel__levels__level"
+                className="audio-setup-panel__levels__icon-part"
                 style={speakerClip}
               />
+              <img
+                src="../assets/images/speaker_level.png"
+                srcSet="../assets/images/speaker_level@2x.png 2x"
+                className="audio-setup-panel__levels__icon-part"
+              />
             </div>
           </div>
           {this.state.audioTrack && (
@@ -672,9 +690,16 @@ class UIRoot extends Component {
                   </option>
                 ))}
               </select>
-              <div className="audio-setup-panel__device-chooser__mic-icon">
-                <img src="../assets/images/mic_small.png" srcSet="../assets/images/mic_small@2x.png 2x" />
-              </div>
+              <img
+                className="audio-setup-panel__device-chooser__mic-icon"
+                src="../assets/images/mic_small.png"
+                srcSet="../assets/images/mic_small@2x.png 2x"
+              />
+              <img
+                className="audio-setup-panel__device-chooser__dropdown-arrow"
+                src="../assets/images/dropdown_arrow.png"
+                srcSet="../assets/images/dropdown_arrow@2x.png 2x"
+              />
             </div>
           )}
           {this.shouldShowHmdMicWarning() && (
@@ -689,9 +714,9 @@ class UIRoot extends Component {
               </span>
             </div>
           )}
-          <div className="audio-setup-panel__enter-button" onClick={this.onAudioReadyButton}>
+          <button className="audio-setup-panel__enter-button" onClick={this.onAudioReadyButton}>
             <FormattedMessage id="audio.enter-now" />
-          </div>
+          </button>
         </div>
       ) : null;
 
diff --git a/src/storage/store.js b/src/storage/store.js
index 6f480aa76db6f14904b611cecf5b0b6b84b1f41f..4351ebeda99e9f9665fcbbf395858296cd2e56a4 100644
--- a/src/storage/store.js
+++ b/src/storage/store.js
@@ -1,5 +1,6 @@
 import uuid from "uuid/v4";
 import { Validator } from "jsonschema";
+import { merge } from "lodash";
 
 const LOCAL_STORE_KEY = "___mozilla_duck";
 const STORE_STATE_CACHE_KEY = Symbol();
@@ -16,6 +17,8 @@ export const SCHEMA = {
       type: "object",
       additionalProperties: false,
       properties: {
+        has_agreed_to_terms: { type: "boolean" },
+        has_changed_name: { type: "boolean" },
         display_name: { type: "string", pattern: "^[A-Za-z0-9-]{3,32}$" },
         avatar_id: { type: "string" }
       }
@@ -27,7 +30,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
@@ -55,7 +59,7 @@ export default class Store extends EventTarget {
       throw new Error("Store id is immutable.");
     }
 
-    const finalState = { ...this.state, ...newState };
+    const finalState = merge(this.state, newState);
     const isValid = validator.validate(finalState, SCHEMA).valid;
 
     if (!isValid) {
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/src/utils/identity.js b/src/utils/identity.js
index def830cbc4df4d9d1a71b0c10d7b54c5a5610b4a..db78b027e3e851aa254532f264438e362062576c 100644
--- a/src/utils/identity.js
+++ b/src/utils/identity.js
@@ -1,178 +1,108 @@
 import { avatars } from "../assets/avatars/avatars.js";
 
 const names = [
-  "albattani",
-  "allen",
-  "almeida",
-  "agnesi",
-  "archimedes",
-  "ardinghelli",
-  "aryabhata",
-  "austin",
-  "babbage",
-  "banach",
-  "bardeen",
-  "bartik",
-  "bassi",
-  "beaver",
-  "bell",
-  "benz",
-  "bhabha",
-  "bhaskara",
-  "blackwell",
-  "bohr",
-  "booth",
-  "borg",
-  "bose",
-  "boyd",
-  "brahmagupta",
-  "brattain",
-  "brown",
-  "carson",
-  "chandrasekhar",
-  "shannon",
-  "clarke",
-  "colden",
-  "cori",
-  "cray",
-  "curran",
-  "curie",
-  "darwin",
-  "davinci",
-  "dijkstra",
-  "dubinsky",
-  "easley",
-  "edison",
-  "einstein",
-  "elion",
-  "engelbart",
-  "euclid",
-  "euler",
-  "fermat",
-  "fermi",
-  "feynman",
-  "franklin",
-  "galileo",
-  "gates",
-  "goldberg",
-  "goldstine",
-  "goldwasser",
-  "golick",
-  "goodall",
-  "haibt",
-  "hamilton",
-  "hawking",
-  "heisenberg",
-  "hermann",
-  "heyrovsky",
-  "hodgkin",
-  "hoover",
-  "hopper",
-  "hugle",
-  "hypatia",
-  "jackson",
-  "jang",
-  "jennings",
-  "jepsen",
-  "johnson",
-  "joliot",
-  "jones",
-  "kalam",
-  "kare",
-  "keller",
-  "kepler",
-  "khorana",
-  "kilby",
-  "kirch",
-  "knuth",
-  "kowalevski",
-  "lalande",
-  "lamarr",
-  "lamport",
-  "leakey",
-  "leavitt",
-  "lewin",
-  "lichterman",
-  "liskov",
-  "lovelace",
-  "lumiere",
-  "mahavira",
-  "mayer",
-  "mccarthy",
-  "mcclintock",
-  "mclean",
-  "mcnulty",
-  "meitner",
-  "meninsky",
-  "mestorf",
-  "minsky",
-  "mirzakhani",
-  "morse",
-  "murdock",
-  "neumann",
-  "newton",
-  "nightingale",
-  "nobel",
-  "noether",
-  "northcutt",
-  "noyce",
-  "panini",
-  "pare",
-  "pasteur",
-  "payne",
-  "perlman",
-  "pike",
-  "poincare",
-  "poitras",
-  "ptolemy",
-  "raman",
-  "ramanujan",
-  "ride",
-  "montalcini",
-  "ritchie",
-  "roentgen",
-  "rosalind",
-  "saha",
-  "sammet",
-  "shaw",
-  "shirley",
-  "shockley",
-  "sinoussi",
-  "snyder",
-  "spence",
-  "stallman",
-  "stonebraker",
-  "swanson",
-  "swartz",
-  "swirles",
-  "tesla",
-  "thompson",
-  "torvalds",
-  "turing",
-  "varahamihira",
-  "visvesvaraya",
-  "volhard",
-  "wescoff",
-  "wiles",
-  "williams",
-  "wilson",
-  "wing",
-  "wozniak",
-  "wright",
-  "yalow",
-  "yonath"
+  "Baers-Pochard",
+  "Baikal-Teal",
+  "Barrows-Goldeneye",
+  "Blue-Billed",
+  "Blue-Duck",
+  "Blue-Winged",
+  "Brown-Teal",
+  "Bufflehead",
+  "Canvasback",
+  "Cape-Shoveler",
+  "Chestnut-Teal",
+  "Chiloe-Wigeon",
+  "Cinnamon-Teal",
+  "Comb-Duck",
+  "Common-Eider",
+  "Common-Goldeneye",
+  "Common-Merganser",
+  "Common-Pochard",
+  "Common-Scoter",
+  "Common-Shelduck",
+  "Cotton-Pygmy",
+  "Crested-Duck",
+  "Crested-Shelduck",
+  "Eatons-Pintail",
+  "Falcated",
+  "Ferruginous",
+  "Freckled-Duck",
+  "Gadwall",
+  "Garganey",
+  "Greater-Scaup",
+  "Green-Pygmy",
+  "Grey-Teal",
+  "Hardhead",
+  "Harlequin",
+  "Hartlaubs",
+  "Hooded-Merganser",
+  "Hottentot-Teal",
+  "Kelp-Goose",
+  "King-Eider",
+  "Lake-Duck",
+  "Laysan-Duck",
+  "Lesser-Scaup",
+  "Long-Tailed",
+  "Maccoa-Duck",
+  "Mallard",
+  "Mandarin",
+  "Marbled-Teal",
+  "Mellers-Duck",
+  "Merganser",
+  "Northern-Pintail",
+  "Orinoco-Goose",
+  "Paradise-Shelduck",
+  "Plumed-Whistler",
+  "Puna-Teal",
+  "Pygmy-Goose",
+  "Radjah-Shelduck",
+  "Red-Billed",
+  "Red-Crested",
+  "Red-Shoveler",
+  "Ring-Necked",
+  "Ringed-Teal",
+  "Rosy-Billed",
+  "Ruddy-Shelduck",
+  "Salvadoris-Teal",
+  "Scaly-Sided",
+  "Shelduck",
+  "Shoveler",
+  "Silver-Teal",
+  "Smew",
+  "Spectacled-Eider",
+  "Spot-Billed",
+  "Spotted-Whistler",
+  "Steamerduck",
+  "Stellers-Eider",
+  "Sunda-Teal",
+  "Surf-Scoter",
+  "Tufted-Duck",
+  "Velvet-Scoter",
+  "Wandering-Whistler",
+  "Whistling-duck",
+  "White-Backed",
+  "White-Cheeked",
+  "White-Winged",
+  "Wigeon",
+  "Wood-Duck",
+  "Yellow-Billed"
 ];
 
 function selectRandom(arr) {
   return arr[Math.floor(Math.random() * arr.length)];
 }
 
+export function generateRandomName() {
+  return `${selectRandom(names)}-${Math.floor(10000 + Math.random() * 10000)}`;
+}
+
 export const avatarIds = avatars.map(av => av.id);
 
 export function generateDefaultProfile() {
-  const name = selectRandom(names);
   return {
-    display_name: name.replace(/^./, name[0].toUpperCase()),
+    has_agreed_to_terms: false,
+    has_changed_name: false,
     avatar_id: selectRandom(avatarIds)
   };
 }
diff --git a/yarn.lock b/yarn.lock
index 00fd724ba4590d8fb8f3d2a313e089d43e88f030..8820642a27070f29460b409b87626ac173c298f5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5182,10 +5182,6 @@ minijanus@^0.5.0:
   version "0.5.0"
   resolved "https://registry.yarnpkg.com/minijanus/-/minijanus-0.5.0.tgz#78e1429bb5d83cb3957a538335d2ae901bf614fa"
 
-"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"
   resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
@@ -5304,6 +5300,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"
@@ -5359,12 +5365,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@https://github.com/mozilla/naf-janus-adapter#feature/disconnect":
-  version "0.4.1"
-  resolved "https://github.com/mozilla/naf-janus-adapter#4a4532014d6489403cf7e451790925ce747f8e41"
+naf-janus-adapter@^0.5.2:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.5.2.tgz#f4a9522c4e0b38fcbfe7c71b668afed67d5e133e"
   dependencies:
     debug "^3.1.0"
-    minijanus "https://github.com/mozilla/minijanus.js#master"
+    minijanus "^0.5.0"
 
 nan@^2.3.0, nan@^2.3.2:
   version "2.9.1"
@@ -6010,6 +6016,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"