Skip to content
Snippets Groups Projects
ui-root.js 44.1 KiB
Newer Older
netpro2k's avatar
netpro2k committed
import React, { Component } from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import copy from "copy-to-clipboard";
import { VR_DEVICE_AVAILABILITY } from "../utils/vr-caps-detect";
netpro2k's avatar
netpro2k committed
import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl";
import en from "react-intl/locale-data/en";
import MovingAverage from "moving-average";
Greg Fodor's avatar
Greg Fodor committed
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";
Greg Fodor's avatar
Greg Fodor committed

import { lang, messages } from "../utils/i18n";
netpro2k's avatar
netpro2k committed
import AutoExitWarning from "./auto-exit-warning";
import {
  TwoDEntryButton,
  DeviceEntryButton,
  GenericEntryButton,
  DaydreamEntryButton,
  SafariEntryButton
} from "./entry-buttons.js";
netpro2k's avatar
netpro2k committed
import ProfileEntryPanel from "./profile-entry-panel";
import HelpDialog from "./help-dialog.js";
import SafariDialog from "./safari-dialog.js";
import WebVRRecommendDialog from "./webvr-recommend-dialog.js";
Greg Fodor's avatar
Greg Fodor committed
import InviteTeamDialog from "./invite-team-dialog.js";
Greg Fodor's avatar
Greg Fodor committed
import InviteDialog from "./invite-dialog.js";
Greg Fodor's avatar
Greg Fodor committed
import LinkDialog from "./link-dialog.js";
import CreateObjectDialog from "./create-object-dialog.js";
Greg Fodor's avatar
Greg Fodor committed
import PresenceLog from "./presence-log.js";
import PresenceList from "./presence-list.js";
netpro2k's avatar
netpro2k committed
import TwoDHUD from "./2d-hud";
Greg Fodor's avatar
Greg Fodor committed
import ChatCommandHelp from "./chat-command-help";
import { spawnChatMessage } from "./chat-message";
import { faUsers } from "@fortawesome/free-solid-svg-icons/faUsers";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faQuestion } from "@fortawesome/free-solid-svg-icons/faQuestion";
Greg Fodor's avatar
Greg Fodor committed
import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp";
Greg Fodor's avatar
Greg Fodor committed
addLocaleData([...en]);

Greg Fodor's avatar
Greg Fodor committed
const ENTRY_STEPS = {
  start: "start",
Greg Fodor's avatar
Greg Fodor committed
  device: "device",
Greg Fodor's avatar
Greg Fodor committed
  mic_grant: "mic_grant",
Greg Fodor's avatar
Greg Fodor committed
  mic_granted: "mic_granted",
Greg Fodor's avatar
Greg Fodor committed
  audio_setup: "audio_setup",
Greg Fodor's avatar
Greg Fodor committed
  finished: "finished"
netpro2k's avatar
netpro2k committed
};
Greg Fodor's avatar
Greg Fodor committed

Greg Fodor's avatar
Greg Fodor committed
// 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,
// the user will be prevented from entering VR until one of those devices is
// selected as the microphone.
//
// Note that this doesn't have to be exhaustive: if no devices match any regex
// then we rely upon the user to select the proper mic.
Brian Peiris's avatar
Brian Peiris committed
const HMD_MIC_REGEXES = [/\Wvive\W/i, /\Wrift\W/i];

async function grantedMicLabels() {
  const mediaDevices = await navigator.mediaDevices.enumerateDevices();
  return mediaDevices.filter(d => d.label && d.kind === "audioinput").map(d => d.label);
}
const AUTO_EXIT_TIMER_SECONDS = 10;

Greg Fodor's avatar
Greg Fodor committed
class UIRoot extends Component {
  static propTypes = {
Greg Fodor's avatar
Greg Fodor committed
    enterScene: PropTypes.func,
Brian Peiris's avatar
Brian Peiris committed
    exitScene: PropTypes.func,
Greg Fodor's avatar
Greg Fodor committed
    onSendMessage: PropTypes.func,
    concurrentLoadDetector: PropTypes.object,
    disableAutoExitOnConcurrentLoad: PropTypes.bool,
Greg Fodor's avatar
Greg Fodor committed
    forcedVREntryType: PropTypes.string,
    isBotMode: PropTypes.bool,
    scene: PropTypes.object,
Greg Fodor's avatar
Greg Fodor committed
    hubChannel: PropTypes.object,
Greg Fodor's avatar
Greg Fodor committed
    linkChannel: PropTypes.object,
Greg Fodor's avatar
Greg Fodor committed
    hubEntryCode: PropTypes.number,
    availableVREntryTypes: PropTypes.object,
Greg Fodor's avatar
Greg Fodor committed
    environmentSceneLoaded: PropTypes.bool,
Brian Peiris's avatar
Brian Peiris committed
    roomUnavailableReason: PropTypes.string,
    platformUnsupportedReason: PropTypes.string,
    hubId: PropTypes.string,
Greg Fodor's avatar
Greg Fodor committed
    isSupportAvailable: PropTypes.bool,
    presenceLogEntries: PropTypes.array,
    presences: PropTypes.object,
Greg Fodor's avatar
Greg Fodor committed
    sessionId: PropTypes.string,
    subscriptions: PropTypes.object,
    initialIsSubscribed: PropTypes.bool
netpro2k's avatar
netpro2k committed
  };
Greg Fodor's avatar
Greg Fodor committed
  state = {
    entryStep: ENTRY_STEPS.start,
Greg Fodor's avatar
Greg Fodor committed
    enterInVR: false,
    dialog: null,
Greg Fodor's avatar
Greg Fodor committed
    showInviteDialog: false,
    showLinkDialog: false,
Greg Fodor's avatar
Greg Fodor committed
    showPresenceList: false,
Greg Fodor's avatar
Greg Fodor committed
    linkCode: null,
    linkCodeCancel: null,
    miniInviteActivated: false,

    shareScreen: false,
    requestedScreen: false,
Greg Fodor's avatar
Greg Fodor committed
    mediaStream: null,
Greg Fodor's avatar
Greg Fodor committed
    entryPanelCollapsed: false,
Greg Fodor's avatar
Greg Fodor committed
    toneInterval: null,
Greg Fodor's avatar
Greg Fodor committed
    tonePlaying: false,
Greg Fodor's avatar
Greg Fodor committed
    micLevel: 0,
    micDevices: [],
    micUpdateInterval: null,
Greg Fodor's avatar
Greg Fodor committed
    profileNamePending: "Hello",

    autoExitTimerStartedAt: null,
    autoExitTimerInterval: null,
    secondsRemainingBeforeAutoExit: Infinity,

netpro2k's avatar
netpro2k committed
    spacebubble: true,
Greg Fodor's avatar
Greg Fodor committed
    exited: false,

Greg Fodor's avatar
Greg Fodor committed
    showProfileEntry: false,
    pendingMessage: ""
netpro2k's avatar
netpro2k committed
  };
Greg Fodor's avatar
Greg Fodor committed

Greg Fodor's avatar
Greg Fodor committed
  componentDidMount() {
    this.props.concurrentLoadDetector.addEventListener("concurrentload", this.onConcurrentLoad);
    this.micLevelMovingAverage = MovingAverage(100);
    this.props.scene.addEventListener("loaded", this.onSceneLoaded);
netpro2k's avatar
netpro2k committed
    this.props.scene.addEventListener("stateadded", this.onAframeStateChanged);
    this.props.scene.addEventListener("stateremoved", this.onAframeStateChanged);
Robert Long's avatar
Robert Long committed
    this.props.scene.addEventListener("exit", this.exit);
johnshaughnessy's avatar
johnshaughnessy committed
    const scene = this.props.scene;
    this.setState({
      audioContext: {
johnshaughnessy's avatar
johnshaughnessy committed
        },
        onMouseLeave: () => {
          //          scene.emit("play_sound-hud_mouse_leave");
        }
      }
    });
  }

  componentWillUnmount() {
    this.props.scene.removeEventListener("loaded", this.onSceneLoaded);
Robert Long's avatar
Robert Long committed
    this.props.scene.removeEventListener("exit", this.exit);
Greg Fodor's avatar
Greg Fodor committed
  updateSubscribedState = () => {
    const isSubscribed = this.props.subscriptions && this.props.subscriptions.isSubscribed();
    this.setState({ isSubscribed });
  };

  onSceneLoaded = () => {
    this.setState({ sceneLoaded: true });
netpro2k's avatar
netpro2k committed
  // TODO: we need to come up with a cleaner way to handle the shared state between aframe and react than emmitting events and setting state on the scene
netpro2k's avatar
netpro2k committed
  onAframeStateChanged = e => {
netpro2k's avatar
netpro2k committed
    if (!(e.detail === "muted" || e.detail === "frozen" || e.detail === "spacebubble")) return;
netpro2k's avatar
netpro2k committed
    this.setState({
      [e.detail]: this.props.scene.is(e.detail)
netpro2k's avatar
netpro2k committed
    });
  };
Greg Fodor's avatar
Greg Fodor committed

netpro2k's avatar
netpro2k committed
  toggleMute = () => {
    this.props.scene.emit("action_mute");
  };
  toggleFreeze = () => {
    this.props.scene.emit("action_freeze");
  };

netpro2k's avatar
netpro2k committed
  toggleSpaceBubble = () => {
    this.props.scene.emit("action_space_bubble");
  };

johnshaughnessy's avatar
johnshaughnessy committed
    this.props.scene.emit("penButtonPressed");
  onSubscribeChanged = async () => {
Greg Fodor's avatar
Greg Fodor committed
    if (!this.props.subscriptions) return;

    await this.props.subscriptions.toggle();
    this.updateSubscribedState();
  };

Greg Fodor's avatar
Greg Fodor committed
  handleStartEntry = () => {
    const promptForNameAndAvatarBeforeEntry = !this.props.store.state.activity.hasChangedName;

    if (promptForNameAndAvatarBeforeEntry) {
      this.setState({ showProfileEntry: true });
    }

Greg Fodor's avatar
Greg Fodor committed
    if (!this.props.forcedVREntryType) {
      this.goToEntryStep(ENTRY_STEPS.device);
Greg Fodor's avatar
Greg Fodor committed
    } else if (this.props.forcedVREntryType.startsWith("daydream")) {
Greg Fodor's avatar
Greg Fodor committed
    } else if (this.props.forcedVREntryType.startsWith("vr")) {
      this.enterVR();
Greg Fodor's avatar
Greg Fodor committed
    } else if (this.props.forcedVREntryType.startsWith("2d")) {
      this.enter2D();
netpro2k's avatar
netpro2k committed
  };
  goToEntryStep = entryStep => {
    this.setState({ entryStep: entryStep, showInviteDialog: false });
  };

  playTestTone = () => {
Greg Fodor's avatar
Greg Fodor committed
    const toneClip = document.querySelector("#test-tone");
    toneClip.currentTime = 0;
Greg Fodor's avatar
Greg Fodor committed
    toneClip.play();
    clearTimeout(this.testToneTimeout);
    this.setState({ tonePlaying: true });
    const toneLength = 1393;
    this.testToneTimeout = setTimeout(() => {
      this.setState({ tonePlaying: false });
    }, toneLength);
netpro2k's avatar
netpro2k committed
  };
Greg Fodor's avatar
Greg Fodor committed

  stopTestTone = () => {
netpro2k's avatar
netpro2k committed
    const toneClip = document.querySelector("#test-tone");
Greg Fodor's avatar
Greg Fodor committed
    toneClip.pause();
    toneClip.currentTime = 0;
netpro2k's avatar
netpro2k committed
    this.setState({ tonePlaying: false });
  };
  onConcurrentLoad = () => {
Greg Fodor's avatar
Greg Fodor committed
    if (this.props.disableAutoExitOnConcurrentLoad) return;

    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);

netpro2k's avatar
netpro2k committed
    this.setState({ autoExitTimerStartedAt: new Date(), autoExitTimerInterval });
  };

  checkForAutoExit = () => {
    if (this.state.secondsRemainingBeforeAutoExit !== 0) return;
    this.endAutoExitTimer();
    this.exit();
netpro2k's avatar
netpro2k committed
  };

  exit = () => {
    this.props.exitScene();
    this.setState({ exited: true });
netpro2k's avatar
netpro2k committed
  };

  isWaitingForAutoExit = () => {
    return this.state.secondsRemainingBeforeAutoExit <= AUTO_EXIT_TIMER_SECONDS;
netpro2k's avatar
netpro2k committed
  };

  endAutoExitTimer = () => {
    clearInterval(this.state.autoExitTimerInterval);
netpro2k's avatar
netpro2k committed
    this.setState({
      autoExitTimerStartedAt: null,
      autoExitTimerInterval: null,
      secondsRemainingBeforeAutoExit: Infinity
    });
  };
  hasGrantedMicPermissions = async () => {
    if (this.state.requestedScreen) {
      // There is no way to tell if you've granted mic permissions in a previous session if we've
      // already prompted for screen sharing permissions, so we have to assume that we've never granted permissions.
      // Fortunately, if you *have* granted permissions permanently, there won't be a second browser prompt, but we
      // can't determine that before hand.
      // See https://bugzilla.mozilla.org/show_bug.cgi?id=1449783 for a potential solution in the future.
      return false;
Brian Peiris's avatar
Brian Peiris committed
      // If we haven't requested the screen in this session, check if we've granted permissions in a previous session.
      return (await grantedMicLabels()).length > 0;
    }
netpro2k's avatar
netpro2k committed
  performDirectEntryFlow = async enterInVR => {
    this.setState({ enterInVR });
    const hasGrantedMic = await this.hasGrantedMicPermissions();
Greg Fodor's avatar
Greg Fodor committed

Greg Fodor's avatar
Greg Fodor committed
    if (hasGrantedMic) {
Greg Fodor's avatar
Greg Fodor committed
      await this.setMediaStreamToDefault();
Greg Fodor's avatar
Greg Fodor committed
      this.beginOrSkipAudioSetup();
Greg Fodor's avatar
Greg Fodor committed
    } else {
      this.goToEntryStep(ENTRY_STEPS.mic_grant);
Greg Fodor's avatar
Greg Fodor committed
    }
netpro2k's avatar
netpro2k committed
  };
Greg Fodor's avatar
Greg Fodor committed

  enter2D = async () => {
    await this.performDirectEntryFlow(false);
netpro2k's avatar
netpro2k committed
  };
Greg Fodor's avatar
Greg Fodor committed

    if (this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.maybe) {
      await this.performDirectEntryFlow(true);
    } else {
      this.showWebVRRecommendDialog();
netpro2k's avatar
netpro2k committed
  };

  enterDaydream = async () => {
Greg Fodor's avatar
Greg Fodor committed
    await this.performDirectEntryFlow(true);
netpro2k's avatar
netpro2k committed
  };
netpro2k's avatar
netpro2k committed
  micDeviceChanged = async ev => {
    const constraints = { audio: { deviceId: { exact: [ev.target.value] } } };
    await this.fetchAudioTrack(constraints);
    await this.setupNewMediaStream();
Greg Fodor's avatar
Greg Fodor committed

  setMediaStreamToDefault = async () => {
    let hasAudio = false;
    const { lastUsedMicDeviceId } = this.props.store.state.settings;
    // Try to fetch last used mic, if there was one.
    if (lastUsedMicDeviceId) {
      hasAudio = await this.fetchAudioTrack({ audio: { deviceId: { ideal: lastUsedMicDeviceId } } });
      hasAudio = await this.fetchAudioTrack({ audio: true });
    }


    return { hasAudio };
    const checked = e.target.checked;
    await this.setState({ requestedScreen: true, shareScreen: checked });
    if (checked) {
      this.fetchVideoTrack({
        video: {
          mediaSource: "screen",
          // Work around BMO 1449832 by calculating the width. This will break for multi monitors if you share anything
          // other than your current monitor that has a different aspect ratio.
Marshall Quander's avatar
Marshall Quander committed
          width: 720 * (screen.width / screen.height),
          height: 720,
          frameRate: 30
        }
      });
    } else {
Greg Fodor's avatar
Greg Fodor committed

    const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
    this.setState({ videoTrack: mediaStream.getVideoTracks()[0] });
Greg Fodor's avatar
Greg Fodor committed

    if (this.state.audioTrack) {
      this.state.audioTrack.stop();
    }

    try {
      const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
      this.setState({ audioTrack: mediaStream.getAudioTracks()[0] });
      return true;
    } catch (e) {
      // Error fetching audio track, most likely a permission denial.
      this.setState({ audioTrack: null });
      return false;
    }
Greg Fodor's avatar
Greg Fodor committed

Brian Peiris's avatar
Brian Peiris committed
  setupNewMediaStream = async () => {
Greg Fodor's avatar
Greg Fodor committed

    await this.fetchMicDevices();

    if (this.state.videoTrack) {
      mediaStream.addTrack(this.state.videoTrack);
    }
    // we should definitely have an audioTrack at this point unless they denied mic access
    if (this.state.audioTrack) {
      mediaStream.addTrack(this.state.audioTrack);
Greg Fodor's avatar
Greg Fodor committed

      const AudioContext = window.AudioContext || window.webkitAudioContext;
      const micLevelAudioContext = new AudioContext();
      const micSource = micLevelAudioContext.createMediaStreamSource(mediaStream);
      const analyser = micLevelAudioContext.createAnalyser();
      analyser.fftSize = 32;
      const levels = new Uint8Array(analyser.frequencyBinCount);
Greg Fodor's avatar
Greg Fodor committed

      micSource.connect(analyser);
Greg Fodor's avatar
Greg Fodor committed

      const micUpdateInterval = setInterval(() => {
        analyser.getByteTimeDomainData(levels);
        let v = 0;
        for (let x = 0; x < levels.length; x++) {
          v = Math.max(levels[x] - 128, v);
        }
        const level = v / 128.0;
        // 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(state => {
          if (Math.abs(average - state.micLevel) > 0.0001) {
            return { micLevel: average };
          }
        });
Greg Fodor's avatar
Greg Fodor committed

      const micDeviceId = this.micDeviceIdForMicLabel(this.micLabelForMediaStream(mediaStream));
      if (micDeviceId) {
        this.props.store.update({ settings: { lastUsedMicDeviceId: micDeviceId } });
      this.setState({ micLevelAudioContext, micUpdateInterval });
    }

    this.setState({ mediaStream });
netpro2k's avatar
netpro2k committed
  };
Greg Fodor's avatar
Greg Fodor committed

  onMicGrantButton = async () => {
Greg Fodor's avatar
Greg Fodor committed
    if (this.state.entryStep == ENTRY_STEPS.mic_grant) {
      const { hasAudio } = await this.setMediaStreamToDefault();

      if (hasAudio) {
        this.goToEntryStep(ENTRY_STEPS.mic_granted);
Greg Fodor's avatar
Greg Fodor committed
        this.beginOrSkipAudioSetup();
Greg Fodor's avatar
Greg Fodor committed
    } else {
Greg Fodor's avatar
Greg Fodor committed
      this.beginOrSkipAudioSetup();
netpro2k's avatar
netpro2k committed
  };
Greg Fodor's avatar
Greg Fodor committed

Greg Fodor's avatar
Greg Fodor committed
  onProfileFinished = () => {
netpro2k's avatar
netpro2k committed
    this.setState({ showProfileEntry: false });
    this.props.hubChannel.sendProfileUpdate();
netpro2k's avatar
netpro2k committed
  };
Greg Fodor's avatar
Greg Fodor committed
  beginOrSkipAudioSetup = () => {
    if (!this.props.forcedVREntryType || !this.props.forcedVREntryType.endsWith("_now")) {
      this.goToEntryStep(ENTRY_STEPS.audio_setup);
Greg Fodor's avatar
Greg Fodor committed
    } else {
Greg Fodor's avatar
Greg Fodor committed
      this.onAudioReadyButton();
Greg Fodor's avatar
Greg Fodor committed
    }
netpro2k's avatar
netpro2k committed
  };
Greg Fodor's avatar
Greg Fodor committed

Greg Fodor's avatar
Greg Fodor committed
  fetchMicDevices = () => {
    return new Promise(resolve => {
      navigator.mediaDevices.enumerateDevices().then(mediaDevices => {
        this.setState(
          {
            micDevices: mediaDevices
              .filter(d => d.kind === "audioinput")
              .map(d => ({ deviceId: d.deviceId, label: d.label }))
          },
          resolve
        );
      });
netpro2k's avatar
netpro2k committed
  };
Greg Fodor's avatar
Greg Fodor committed

Greg Fodor's avatar
Greg Fodor committed
  shouldShowHmdMicWarning = () => {
    if (AFRAME.utils.device.isMobile()) return false;
Greg Fodor's avatar
Greg Fodor committed
    if (!this.state.enterInVR) return false;
    if (!this.hasHmdMicrophone()) return false;

netpro2k's avatar
netpro2k committed
    return !HMD_MIC_REGEXES.find(r => this.selectedMicLabel().match(r));
  };
Greg Fodor's avatar
Greg Fodor committed

  hasHmdMicrophone = () => {
netpro2k's avatar
netpro2k committed
    return !!this.state.micDevices.find(d => HMD_MIC_REGEXES.find(r => d.label.match(r)));
  };
Greg Fodor's avatar
Greg Fodor committed

  micLabelForMediaStream = mediaStream => {
    return (mediaStream && mediaStream.getAudioTracks().length > 0 && mediaStream.getAudioTracks()[0].label) || "";
  };

Greg Fodor's avatar
Greg Fodor committed
  selectedMicLabel = () => {
    return this.micLabelForMediaStream(this.state.mediaStream);
  };

  micDeviceIdForMicLabel = label => {
    return this.state.micDevices.filter(d => d.label === label).map(d => d.deviceId)[0];
netpro2k's avatar
netpro2k committed
  };
Greg Fodor's avatar
Greg Fodor committed

  selectedMicDeviceId = () => {
    return this.micDeviceIdForMicLabel(this.selectedMicLabel());
netpro2k's avatar
netpro2k committed
  };
Greg Fodor's avatar
Greg Fodor committed

  onAudioReadyButton = () => {
    if (AFRAME.utils.device.isMobile() && !this.state.enterInVR && screenfull.enabled) {
    this.props.enterScene(this.state.mediaStream, this.state.enterInVR, this.props.hubId);
Greg Fodor's avatar
Greg Fodor committed

    const mediaStream = this.state.mediaStream;

    if (mediaStream) {
      if (mediaStream.getAudioTracks().length > 0) {
netpro2k's avatar
netpro2k committed
        console.log(`Using microphone: ${mediaStream.getAudioTracks()[0].label}`);
      }

      if (mediaStream.getVideoTracks().length > 0) {
netpro2k's avatar
netpro2k committed
        console.log("Screen sharing enabled.");
Greg Fodor's avatar
Greg Fodor committed
    }
Greg Fodor's avatar
Greg Fodor committed
    this.stopTestTone();
    clearTimeout(this.testToneTimeout);

    if (this.state.micLevelAudioContext) {
      this.state.micLevelAudioContext.close();
      clearInterval(this.state.micUpdateInterval);
    }

    this.goToEntryStep(ENTRY_STEPS.finished);
netpro2k's avatar
netpro2k committed
  };
Greg Fodor's avatar
Greg Fodor committed
  attemptLink = async () => {
    this.setState({ showLinkDialog: true });
    const { code, cancel, onFinished } = await this.props.linkChannel.generateCode();
    this.setState({ linkCode: code, linkCodeCancel: cancel });
    onFinished.then(() => this.setState({ showLinkDialog: false, linkCode: null, linkCodeCancel: null }));
Greg Fodor's avatar
Greg Fodor committed
  };

Greg Fodor's avatar
Greg Fodor committed
  showInviteDialog = () => {
    this.setState({ showInviteDialog: true });
Greg Fodor's avatar
Greg Fodor committed
  toggleInviteDialog = async () => {
    this.setState({ showInviteDialog: !this.state.showInviteDialog });
  createObject = media => {
    this.props.scene.emit("add_media", media);
  closeDialog = () => {
    this.setState({ dialog: null });
  };

johnshaughnessy's avatar
johnshaughnessy committed
  showHelpDialog() {
    this.setState({
      dialog: <HelpDialog onClose={this.closeDialog} />
    });
  }
johnshaughnessy's avatar
johnshaughnessy committed
  showSafariDialog() {
    this.setState({
      dialog: <SafariDialog onClose={this.closeDialog} />
    });
  }
johnshaughnessy's avatar
johnshaughnessy committed
  showInviteTeamDialog() {
    this.setState({
      dialog: <InviteTeamDialog hubChannel={this.props.hubChannel} onClose={this.closeDialog} />
    });
  }
Greg Fodor's avatar
Greg Fodor committed

johnshaughnessy's avatar
johnshaughnessy committed
  showCreateObjectDialog() {
    this.setState({
      dialog: <CreateObjectDialog onCreate={this.createObject} onClose={this.closeDialog} />
    });
  }
johnshaughnessy's avatar
johnshaughnessy committed
  showWebVRRecommendDialog() {
    this.setState({
      dialog: <WebVRRecommendDialog onClose={this.closeDialog} />
    });
  }
  onMiniInviteClicked = () => {
    const link = "https://hub.link/" + this.props.hubId;

    this.setState({ miniInviteActivated: true });
    setTimeout(() => {
      this.setState({ miniInviteActivated: false });
    }, 5000);

    if (navigator.share) {
      navigator.share({ title: document.title, url: link });
    } else {
      copy(link);
    }
  };

Greg Fodor's avatar
Greg Fodor committed
  sendMessage = e => {
    e.preventDefault();
    this.props.onSendMessage(this.state.pendingMessage);
    this.setState({ pendingMessage: "" });
  };

  occupantCount = () => {
    return this.props.presences ? Object.entries(this.props.presences).length : 0;
  };

Greg Fodor's avatar
Greg Fodor committed
  renderExitedPane = () => {
    let subtitle = null;
    if (this.props.roomUnavailableReason === "closed") {
      // TODO i18n, due to links and markup
      subtitle = (
        <div>
          Sorry, this room is no longer available.
          <p />A room may be closed if we receive reports that it violates our{" "}
          <WithHoverSound>
            <a target="_blank" rel="noreferrer noopener" href="https://github.com/mozilla/hubs/blob/master/TERMS.md">
              Terms of Use
            </a>
          </WithHoverSound>
          .<br />
          If you have questions, contact us at{" "}
          <WithHoverSound>
            <a href="mailto:hubs@mozilla.com">hubs@mozilla.com</a>
          </WithHoverSound>
          .<p />
          If you&apos;d like to run your own server, hubs&apos;s source code is available on{" "}
          <WithHoverSound>
            <a href="https://github.com/mozilla/hubs">GitHub</a>
          </WithHoverSound>
          .
        </div>
Greg Fodor's avatar
Greg Fodor committed
      );
Greg Fodor's avatar
Greg Fodor committed
    } else if (this.props.platformUnsupportedReason === "no_data_channels") {
      // TODO i18n, due to links and markup
      subtitle = (
        <div>
          Your browser does not support{" "}
          <WithHoverSound>
            <a
              href="https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createDataChannel#Browser_compatibility"
              rel="noreferrer noopener"
            >
              WebRTC Data Channels
            </a>
          </WithHoverSound>
          , which is required to use Hubs.
          <br />
          If you&quot;d like to use Hubs with Oculus or SteamVR, you can{" "}
          <WithHoverSound>
            <a href="https://www.mozilla.org/firefox" rel="noreferrer noopener">
              Download Firefox
            </a>
          </WithHoverSound>
          .
        </div>
Greg Fodor's avatar
Greg Fodor committed
    } else {
      const reason = this.props.roomUnavailableReason || this.props.platformUnsupportedReason;
      const exitSubtitleId = `exit.subtitle.${this.state.exited ? "exited" : reason}`;
      subtitle = (
        <div>
          <FormattedMessage id={exitSubtitleId} />
          <p />
Greg Fodor's avatar
Greg Fodor committed
          {this.props.roomUnavailableReason !== "left" && (
Greg Fodor's avatar
Greg Fodor committed
                <a href="/">create a new room</a>
              </WithHoverSound>.
Greg Fodor's avatar
Greg Fodor committed
          )}
        </div>
Greg Fodor's avatar
Greg Fodor committed
    return (
      <IntlProvider locale={lang} messages={messages}>
        <div className="exited-panel">
          <img className="exited-panel__logo" src="../assets/images/logo.svg" />
          <div className="exited-panel__subtitle">{subtitle}</div>
        </div>
      </IntlProvider>
    );
  };
Greg Fodor's avatar
Greg Fodor committed
  renderBotMode = () => {
    return (
      <div className="loading-panel">
        <img className="loading-panel__logo" src="../assets/images/logo.svg" />
        <input type="file" id="bot-audio-input" accept="audio/*" />
        <input type="file" id="bot-data-input" accept="application/json" />
      </div>
    );
  };

  renderLoader = () => {
    return (
      <IntlProvider locale={lang} messages={messages}>
        <div className="loading-panel">
          <div className="loader-wrap">
            <div className="loader">
              <div className="loader-center" />
            </div>
          </div>
Greg Fodor's avatar
Greg Fodor committed
          <img className="loading-panel__logo" src="../assets/images/hub-preview-light-no-shadow.png" />
netpro2k's avatar
netpro2k committed
        </div>
Greg Fodor's avatar
Greg Fodor committed
      </IntlProvider>
    );
  };
Greg Fodor's avatar
Greg Fodor committed

Greg Fodor's avatar
Greg Fodor committed
  renderEntryStartPanel = () => {
Greg Fodor's avatar
Greg Fodor committed
    const textRows = this.state.pendingMessage.split("\n").length;
    const pendingMessageTextareaHeight = textRows * 28 + "px";
    const pendingMessageFieldHeight = textRows * 28 + 20 + "px";
Greg Fodor's avatar
Greg Fodor committed
    const hasPush = navigator.serviceWorker && "PushManager" in window;
Greg Fodor's avatar
Greg Fodor committed

Greg Fodor's avatar
Greg Fodor committed
    return (
      <div className={entryStyles.entryPanel}>
Greg Fodor's avatar
Greg Fodor committed
        <div className={entryStyles.name}>{this.props.hubName}</div>
        <div className={entryStyles.center}>
          <WithHoverSound>
            <div onClick={() => this.setState({ showProfileEntry: true })} className={entryStyles.profileName}>
              <img src="../assets/images/account.svg" className={entryStyles.profileIcon} />
              <div title={this.props.store.state.profile.displayName}>{this.props.store.state.profile.displayName}</div>
johnshaughnessy's avatar
johnshaughnessy committed
            </div>
Greg Fodor's avatar
Greg Fodor committed

          <form onSubmit={this.sendMessage}>
Greg Fodor's avatar
Greg Fodor committed
            <div className={styles.messageEntry} style={{ height: pendingMessageFieldHeight }}>
              <textarea
Greg Fodor's avatar
Greg Fodor committed
                className={classNames([styles.messageEntryInput, "chat-focus-target"])}
Greg Fodor's avatar
Greg Fodor committed
                value={this.state.pendingMessage}
Greg Fodor's avatar
Greg Fodor committed
                rows={textRows}
                style={{ height: pendingMessageTextareaHeight }}
Greg Fodor's avatar
Greg Fodor committed
                onFocus={e => e.target.select()}
                onChange={e => this.setState({ pendingMessage: e.target.value })}
Greg Fodor's avatar
Greg Fodor committed
                onKeyDown={e => {
                  if (e.keyCode === 13 && !e.shiftKey) {
                    this.sendMessage(e);
Greg Fodor's avatar
Greg Fodor committed
                  } else if (e.keyCode === 27) {
                    e.target.blur();
Greg Fodor's avatar
Greg Fodor committed
                placeholder="Send a message..."
              />
              <WithHoverSound>
                <input className={styles.messageEntrySubmit} type="submit" value="send" />
              </WithHoverSound>
Greg Fodor's avatar
Greg Fodor committed
            </div>
          </form>
Greg Fodor's avatar
Greg Fodor committed
        {hasPush && (
          <div className={entryStyles.subscribe}>
            <input
              id="subscribe"
              type="checkbox"
              onChange={this.onSubscribeChanged}
Greg Fodor's avatar
Greg Fodor committed
              checked={this.state.isSubscribed === undefined ? this.props.initialIsSubscribed : this.state.isSubscribed}
Greg Fodor's avatar
Greg Fodor committed
            />
            <label htmlFor="subscribe">
              <FormattedMessage id="entry.notify_me" />
            </label>
          </div>
        )}
Greg Fodor's avatar
Greg Fodor committed

        <div className={entryStyles.buttonContainer}>
          <WithHoverSound>
            <button
              className={classNames([entryStyles.actionButton, entryStyles.wideButton])}
              onClick={() => this.handleStartEntry()}
            >
              <FormattedMessage id="entry.enter-room" />
            </button>
          </WithHoverSound>
        </div>
      </div>
Greg Fodor's avatar
Greg Fodor committed
    );
  };

  renderDevicePanel = () => {
Greg Fodor's avatar
Greg Fodor committed
    // Only screen sharing in desktop firefox since other browsers/platforms will ignore the "screen" media constraint and will attempt to share your webcam instead!
    const isFireFox = /firefox/i.test(navigator.userAgent);
    const isNonMobile = !AFRAME.utils.device.isMobile();

    const screenSharingCheckbox =
      this.props.enableScreenSharing && isNonMobile && isFireFox && this.renderScreensharing();

    return (
      <div className={entryStyles.entryPanel}>
Greg Fodor's avatar
Greg Fodor committed
        <div className={entryStyles.title}>
          <FormattedMessage id="entry.choose-device" />
        </div>

Greg Fodor's avatar
Greg Fodor committed
        <div className={entryStyles.buttonContainer}>
          {this.props.availableVREntryTypes.screen === VR_DEVICE_AVAILABILITY.yes && (
            <TwoDEntryButton onClick={this.enter2D} />
          )}
          {this.props.availableVREntryTypes.safari === VR_DEVICE_AVAILABILITY.maybe && (
            <SafariEntryButton onClick={this.showSafariDialog} />
          )}
          {this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && (
            <GenericEntryButton onClick={this.enterVR} />
          )}
          {this.props.availableVREntryTypes.daydream === VR_DEVICE_AVAILABILITY.yes && (
            <DaydreamEntryButton onClick={this.enterDaydream} subtitle={null} />
          )}
Greg Fodor's avatar
Greg Fodor committed
          <DeviceEntryButton onClick={() => this.attemptLink()} isInHMD={this.props.availableVREntryTypes.isInHMD} />
Greg Fodor's avatar
Greg Fodor committed
          {this.props.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && (
            <div className={entryStyles.secondary} onClick={this.enterVR}>
              <FormattedMessage id="entry.cardboard" />
Greg Fodor's avatar
Greg Fodor committed
          )}
          {screenSharingCheckbox}
        </div>
      </div>
    );
  };

  renderScreensharing = () => {
    return (
      <label className={entryStyles.screenSharing}>
        <input
          className={entryStyles.checkbox}
          type="checkbox"
          value={this.state.shareScreen}
          onChange={this.setStateAndRequestScreen}
        />
        <FormattedMessage id="entry.enable-screen-sharing" />
      </label>
    );
  };

  renderMicPanel = () => {
    return (
      <div className="mic-grant-panel">
        <div className="mic-grant-panel__grant-container">
          <div className="mic-grant-panel__title">
            <FormattedMessage
              id={this.state.entryStep == ENTRY_STEPS.mic_grant ? "audio.grant-title" : "audio.granted-title"}
            />
          </div>
          <div className="mic-grant-panel__subtitle">
            <FormattedMessage
              id={this.state.entryStep == ENTRY_STEPS.mic_grant ? "audio.grant-subtitle" : "audio.granted-subtitle"}
            />
          </div>
          <div className="mic-grant-panel__button-container">
            {this.state.entryStep == ENTRY_STEPS.mic_grant ? (
              <WithHoverSound>
                <button className="mic-grant-panel__button" onClick={this.onMicGrantButton}>
                  <img src="../assets/images/mic_denied.png" srcSet="../assets/images/mic_denied@2x.png 2x" />
johnshaughnessy's avatar
johnshaughnessy committed
                </button>
              </WithHoverSound>
            ) : (
              <WithHoverSound>
                <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>
              </WithHoverSound>
            )}
          <div className="mic-grant-panel__next-container">
            <WithHoverSound>
              <button className={classNames("mic-grant-panel__next")} onClick={this.onMicGrantButton}>
                <FormattedMessage id="audio.granted-next" />
              </button>
            </WithHoverSound>
          </div>
        </div>
      </div>
Greg Fodor's avatar
Greg Fodor committed

Greg Fodor's avatar
Greg Fodor committed
  renderAudioSetupPanel = () => {
Greg Fodor's avatar
Greg Fodor committed
    const maxLevelHeight = 111;
netpro2k's avatar
netpro2k committed
    const micClip = {
      clip: `rect(${maxLevelHeight - Math.floor(this.state.micLevel * maxLevelHeight)}px, 111px, 111px, 0px)`
    };
Greg Fodor's avatar
Greg Fodor committed
    const speakerClip = { clip: `rect(${this.state.tonePlaying ? 0 : maxLevelHeight}px, 111px, 111px, 0px)` };
    const subtitleId = AFRAME.utils.device.isMobile() ? "audio.subtitle-mobile" : "audio.subtitle-desktop";
Greg Fodor's avatar
Greg Fodor committed
    return (
      <div className="audio-setup-panel">
        <div>
          <div className="audio-setup-panel__title">
            <FormattedMessage id="audio.title" />
          </div>
          <div className="audio-setup-panel__subtitle">
            {(AFRAME.utils.device.isMobile() || this.state.enterInVR) && <FormattedMessage id={subtitleId} />}
          </div>
          <div className="audio-setup-panel__levels">
            <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__icon-part"
                />
              ) : (
                <img
                  src="../assets/images/mic_denied.png"
                  srcSet="../assets/images/mic_denied@2x.png 2x"
                  className="audio-setup-panel__levels__icon-part"
                />
              )}
            </div>
            <WithHoverSound>
              <div className="audio-setup-panel__levels__icon" onClick={this.playTestTone}>
                <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={speakerClip}
                />
                <img
                  src="../assets/images/speaker_level.png"
                  srcSet="../assets/images/speaker_level@2x.png 2x"
                  className="audio-setup-panel__levels__icon-part"
                />
johnshaughnessy's avatar
johnshaughnessy committed
              </div>
            </WithHoverSound>
          </div>
          {this.state.audioTrack && (
            <div className="audio-setup-panel__device-chooser">
              <WithHoverSound>
                <select
                  className="audio-setup-panel__device-chooser__dropdown"
                  value={this.selectedMicDeviceId()}
                  onChange={this.micDeviceChanged}
                  {this.state.micDevices.map(d => (
                    <option key={d.deviceId} value={d.deviceId}>
                      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
                      {d.label}
                    </option>
                  ))}
                </select>
              </WithHoverSound>
              <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"
              />
Greg Fodor's avatar
Greg Fodor committed
            </div>
          )}
          {this.shouldShowHmdMicWarning() && (
            <div className="audio-setup-panel__hmd-mic-warning">
              <img
                src="../assets/images/warning_icon.png"
                srcSet="../assets/images/warning_icon@2x.png 2x"
                className="audio-setup-panel__hmd-mic-warning__icon"
              />
              <span className="audio-setup-panel__hmd-mic-warning__label">
                <FormattedMessage id="audio.hmd-mic-warning" />
              </span>
Greg Fodor's avatar
Greg Fodor committed
            </div>