From e9a5ec5f79762615b752f4f626c321949e2dd2f7 Mon Sep 17 00:00:00 2001 From: Robert Long Date: Tue, 20 Oct 2020 13:45:39 -0700 Subject: [PATCH 01/13] Add additional toolbar icons --- src/react-components/ui-root.js | 52 ++++++++++++--------------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 4ff1660953..d65f1035fd 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -52,7 +52,6 @@ import AvatarEditor from "./avatar-editor"; import PreferencesScreen from "./preferences-screen.js"; import PresenceLog from "./presence-log.js"; import PreloadOverlay from "./preload-overlay.js"; -import TwoDHUD from "./2d-hud"; import { SpectatingLabel } from "./spectating-label"; import { showFullScreenIfAvailable, showFullScreenIfWasFullScreen } from "../utils/fullscreen"; import { exit2DInterstitialAndEnterVR, isIn2DInterstitial } from "../utils/vr-interstitial"; @@ -92,6 +91,11 @@ import { ReactComponent as DiscordIcon } from "./icons/Discord.svg"; import { ReactComponent as VRIcon } from "./icons/VR.svg"; import { ReactComponent as PeopleIcon } from "./icons/People.svg"; import { ReactComponent as ObjectsIcon } from "./icons/Objects.svg"; +import { ReactComponent as MicrophoneIcon } from "./icons/Microphone.svg"; +import { ReactComponent as ShareIcon } from "./icons/Share.svg"; +import { ReactComponent as ObjectIcon } from "./icons/Object.svg"; +import { ReactComponent as ReactionIcon } from "./icons/Reaction.svg"; +import { ReactComponent as LeaveIcon } from "./icons/Leave.svg"; import { PeopleSidebarContainer, userFromPresence } from "./room/PeopleSidebarContainer"; import { ObjectListProvider } from "./room/useObjectList"; import { ObjectsSidebarContainer } from "./room/ObjectsSidebarContainer"; @@ -1364,7 +1368,6 @@ class UIRoot extends Component { const streaming = this.state.isStreaming; - const showTopHud = enteredOrWatching; const showObjectList = enteredOrWatching; const streamingTip = streaming && @@ -1829,36 +1832,6 @@ class UIRoot extends Component { )} {streamingTip} {!entered && !streaming && !isMobile && streamerName && } - {showTopHud && ( -
- this.setState({ watching: false })} - videoShareMediaSource={this.state.videoShareMediaSource} - showVideoShareFailed={this.state.showVideoShareFailed} - hideVideoShareFailedTip={() => this.setState({ showVideoShareFailed: false })} - activeTip={this.props.activeTips && this.props.activeTips.top} - isCursorHoldingPen={this.props.isCursorHoldingPen} - hasActiveCamera={this.props.hasActiveCamera} - onToggleMute={this.toggleMute} - onSpawnPen={this.spawnPen} - onSpawnCamera={() => this.props.scene.emit("action_toggle_camera")} - onShareVideo={this.shareVideo} - onEndShareVideo={this.endShareVideo} - onShareVideoNotCapable={() => this.showWebRTCScreenshareUnsupportedDialog()} - isStreaming={streaming} - showStreamingTip={this.state.showStreamingTip} - hideStreamingTip={() => { - this.setState({ showStreamingTip: false }); - }} - /> -
- )} } sidebar={ @@ -1917,7 +1890,19 @@ class UIRoot extends Component { } modal={renderEntryFlow && entryDialog} toolbarLeft={} - toolbarCenter={ this.toggleSidebar("chat")} />} + toolbarCenter={ + <> + {entered && ( + <> + } label="Voice" preset="basic" /> + } label="Share" preset="purple" /> + } label="Place" preset="green" /> + } label="React" preset="orange" /> + + )} + this.toggleSidebar("chat")} /> + + } toolbarRight={ <> {entered && @@ -1929,6 +1914,7 @@ class UIRoot extends Component { onClick={() => exit2DInterstitialAndEnterVR(true)} /> )} + {entered && } label="Leave" preset="red" />} } From 29caf0ce69e22d17867276e2fe424baf3bf35f91 Mon Sep 17 00:00:00 2001 From: Robert Long Date: Tue, 20 Oct 2020 13:52:29 -0700 Subject: [PATCH 02/13] Stub out PlacePopover and SharePopover --- .../room/PlacePopoverContainer.js | 30 +++++++++++++++++++ .../room/SharePopoverContainer.js | 16 ++++++++++ src/react-components/ui-root.js | 6 ++-- 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/react-components/room/PlacePopoverContainer.js create mode 100644 src/react-components/room/SharePopoverContainer.js diff --git a/src/react-components/room/PlacePopoverContainer.js b/src/react-components/room/PlacePopoverContainer.js new file mode 100644 index 0000000000..4817913d8d --- /dev/null +++ b/src/react-components/room/PlacePopoverContainer.js @@ -0,0 +1,30 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { ReactComponent as PenIcon } from "../icons/Pen.svg"; +import { ReactComponent as CameraIcon } from "../icons/Camera.svg"; +import { ReactComponent as TextIcon } from "../icons/Text.svg"; +import { ReactComponent as LinkIcon } from "../icons/Link.svg"; +import { ReactComponent as GIFIcon } from "../icons/GIF.svg"; +import { ReactComponent as ObjectIcon } from "../icons/Object.svg"; +import { ReactComponent as AvatarIcon } from "../icons/Avatar.svg"; +import { ReactComponent as SceneIcon } from "../icons/Scene.svg"; +import { ReactComponent as UploadIcon } from "../icons/Upload.svg"; +import { PlacePopoverButton } from "./PlacePopover"; + +const items = [ + { id: "pen", icon: PenIcon, color: "purple", label: "Pen" }, + { id: "camera", icon: CameraIcon, color: "purple", label: "Camera" }, + { id: "text", icon: TextIcon, color: "blue", label: "Text" }, + { id: "link", icon: LinkIcon, color: "blue", label: "Link" }, + { id: "gif", icon: GIFIcon, color: "orange", label: "GIF" }, + { id: "model", icon: ObjectIcon, color: "orange", label: "3D Model" }, + { id: "avatar", icon: AvatarIcon, color: "red", label: "Avatar" }, + { id: "scene", icon: SceneIcon, color: "red", label: "Scene" }, + { id: "upload", icon: UploadIcon, color: "green", label: "Upload" } +]; + +export function PlacePopoverContainer() { + return console.log(item)} />; +} + +PlacePopoverContainer.propTypes = {}; diff --git a/src/react-components/room/SharePopoverContainer.js b/src/react-components/room/SharePopoverContainer.js new file mode 100644 index 0000000000..ae31d6c8d9 --- /dev/null +++ b/src/react-components/room/SharePopoverContainer.js @@ -0,0 +1,16 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { ReactComponent as VideoIcon } from "../icons/Video.svg"; +import { ReactComponent as DesktopIcon } from "../icons/Desktop.svg"; +import { SharePopoverButton } from "./SharePopover"; + +const items = [ + { id: "camera", icon: VideoIcon, color: "purple", label: "Camera" }, + { id: "screen", icon: DesktopIcon, color: "purple", label: "Screen" } +]; + +export function SharePopoverContainer() { + return console.log(item)} />; +} + +SharePopoverContainer.propTypes = {}; diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index d65f1035fd..77b293f8b5 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -101,6 +101,8 @@ import { ObjectListProvider } from "./room/useObjectList"; import { ObjectsSidebarContainer } from "./room/ObjectsSidebarContainer"; import { ObjectMenuContainer } from "./room/ObjectMenuContainer"; import { useCssBreakpoints } from "react-use-css-breakpoints"; +import { PlacePopoverContainer } from "./room/PlacePopoverContainer"; +import { SharePopoverContainer } from "./room/SharePopoverContainer"; const avatarEditorDebug = qsTruthy("avatarEditorDebug"); @@ -1895,8 +1897,8 @@ class UIRoot extends Component { {entered && ( <> } label="Voice" preset="basic" /> - } label="Share" preset="purple" /> - } label="Place" preset="green" /> + + } label="React" preset="orange" /> )} From f757d6f8aad6ccc029a535bbdb392c302b40a3e4 Mon Sep 17 00:00:00 2001 From: Robert Long Date: Tue, 20 Oct 2020 15:05:30 -0700 Subject: [PATCH 03/13] Add place popover item logic --- .../popover/ButtonGridPopover.js | 14 ++-- src/react-components/room/PlacePopover.js | 7 +- .../room/PlacePopoverContainer.js | 81 +++++++++++++++---- src/react-components/ui-root.js | 6 +- 4 files changed, 82 insertions(+), 26 deletions(-) diff --git a/src/react-components/popover/ButtonGridPopover.js b/src/react-components/popover/ButtonGridPopover.js index 0470774f71..d710e7a4bd 100644 --- a/src/react-components/popover/ButtonGridPopover.js +++ b/src/react-components/popover/ButtonGridPopover.js @@ -4,7 +4,7 @@ import classNames from "classnames"; import { ToolbarButton } from "../input/ToolbarButton"; import styles from "./ButtonGridPopover.scss"; -export function ButtonGridPopover({ fullscreen, items, onSelect }) { +export function ButtonGridPopover({ fullscreen, items }) { return (
{items.map(item => { @@ -14,7 +14,11 @@ export function ButtonGridPopover({ fullscreen, items, onSelect }) { key={item.id} icon={} preset={item.color} - onClick={() => onSelect(item)} + onClick={() => { + if (item.onSelect) { + item.onSelect(item); + } + }} label={item.label} /> ); @@ -30,8 +34,8 @@ ButtonGridPopover.propTypes = { id: PropTypes.string.isRequired, icon: PropTypes.elementType.isRequired, color: PropTypes.string, - name: PropTypes.string.isRequired + name: PropTypes.string.isRequired, + onSelect: PropTypes.func }) - ).isRequired, - onSelect: PropTypes.func.isRequired + ).isRequired }; diff --git a/src/react-components/room/PlacePopover.js b/src/react-components/room/PlacePopover.js index 9bbe1fae11..f9237012ac 100644 --- a/src/react-components/room/PlacePopover.js +++ b/src/react-components/room/PlacePopover.js @@ -5,7 +5,7 @@ import { Popover } from "../popover/Popover"; import { ToolbarButton } from "../input/ToolbarButton"; import { ReactComponent as ObjectIcon } from "../icons/Object.svg"; -export function PlacePopoverButton({ items, onSelect }) { +export function PlacePopoverButton({ items }) { // The button is removed if you can't place anything. if (items.length === 0) { return undefined; @@ -14,7 +14,7 @@ export function PlacePopoverButton({ items, onSelect }) { return ( } + content={props => } placement="top" offsetDistance={28} initiallyVisible @@ -34,6 +34,5 @@ export function PlacePopoverButton({ items, onSelect }) { } PlacePopoverButton.propTypes = { - items: ButtonGridPopover.propTypes.items, - onSelect: PropTypes.func + items: ButtonGridPopover.propTypes.items }; diff --git a/src/react-components/room/PlacePopoverContainer.js b/src/react-components/room/PlacePopoverContainer.js index 4817913d8d..299422a164 100644 --- a/src/react-components/room/PlacePopoverContainer.js +++ b/src/react-components/room/PlacePopoverContainer.js @@ -2,8 +2,8 @@ import React from "react"; import PropTypes from "prop-types"; import { ReactComponent as PenIcon } from "../icons/Pen.svg"; import { ReactComponent as CameraIcon } from "../icons/Camera.svg"; -import { ReactComponent as TextIcon } from "../icons/Text.svg"; -import { ReactComponent as LinkIcon } from "../icons/Link.svg"; +// import { ReactComponent as TextIcon } from "../icons/Text.svg"; +// import { ReactComponent as LinkIcon } from "../icons/Link.svg"; import { ReactComponent as GIFIcon } from "../icons/GIF.svg"; import { ReactComponent as ObjectIcon } from "../icons/Object.svg"; import { ReactComponent as AvatarIcon } from "../icons/Avatar.svg"; @@ -11,20 +11,69 @@ import { ReactComponent as SceneIcon } from "../icons/Scene.svg"; import { ReactComponent as UploadIcon } from "../icons/Upload.svg"; import { PlacePopoverButton } from "./PlacePopover"; -const items = [ - { id: "pen", icon: PenIcon, color: "purple", label: "Pen" }, - { id: "camera", icon: CameraIcon, color: "purple", label: "Camera" }, - { id: "text", icon: TextIcon, color: "blue", label: "Text" }, - { id: "link", icon: LinkIcon, color: "blue", label: "Link" }, - { id: "gif", icon: GIFIcon, color: "orange", label: "GIF" }, - { id: "model", icon: ObjectIcon, color: "orange", label: "3D Model" }, - { id: "avatar", icon: AvatarIcon, color: "red", label: "Avatar" }, - { id: "scene", icon: SceneIcon, color: "red", label: "Scene" }, - { id: "upload", icon: UploadIcon, color: "green", label: "Upload" } -]; +export function PlacePopoverContainer({ scene, mediaSearchStore, pushHistoryState }) { + // TODO: Check permissions for each item + const items = [ + { + id: "pen", + icon: PenIcon, + color: "purple", + label: "Pen", + onSelect: () => scene.emit("penButtonPressed") + }, + { + id: "camera", + icon: CameraIcon, + color: "purple", + label: "Camera", + onSelect: () => scene.emit("action_toggle_camera") + }, + // TODO: Create text/link dialog + // { id: "text", icon: TextIcon, color: "blue", label: "Text" }, + // { id: "link", icon: LinkIcon, color: "blue", label: "Link" }, + { + id: "gif", + icon: GIFIcon, + color: "orange", + label: "GIF", + onSelect: () => mediaSearchStore.sourceNavigate("gifs") + }, + { + id: "model", + icon: ObjectIcon, + color: "orange", + label: "3D Model", + onSelect: () => mediaSearchStore.sourceNavigate("poly") + }, + { + id: "avatar", + icon: AvatarIcon, + color: "red", + label: "Avatar", + onSelect: () => mediaSearchStore.sourceNavigate("avatars") + }, + { + id: "scene", + icon: SceneIcon, + color: "red", + label: "Scene", + onSelect: () => mediaSearchStore.sourceNavigate("scenes") + }, + // TODO: Launch system file prompt directly + { + id: "upload", + icon: UploadIcon, + color: "green", + label: "Upload", + onSelect: () => pushHistoryState("modal", "create") + } + ]; -export function PlacePopoverContainer() { - return console.log(item)} />; + return ; } -PlacePopoverContainer.propTypes = {}; +PlacePopoverContainer.propTypes = { + scene: PropTypes.object.isRequired, + mediaSearchStore: PropTypes.object.isRequired, + pushHistoryState: PropTypes.object.isRequired +}; diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 77b293f8b5..47c9461b13 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -1898,7 +1898,11 @@ class UIRoot extends Component { <> } label="Voice" preset="basic" /> - + } label="React" preset="orange" /> )} From c35203d9883b3bc8552f570407762ad3cb138814 Mon Sep 17 00:00:00 2001 From: Robert Long Date: Tue, 20 Oct 2020 17:28:46 -0700 Subject: [PATCH 04/13] Add SharePopover functionality --- src/react-components/room/SharePopover.js | 29 +++-- .../room/SharePopover.stories.js | 12 +- .../room/SharePopoverContainer.js | 123 ++++++++++++++++-- src/react-components/ui-root.js | 2 +- 4 files changed, 136 insertions(+), 30 deletions(-) diff --git a/src/react-components/room/SharePopover.js b/src/react-components/room/SharePopover.js index f2d4e844e4..82afaf775a 100644 --- a/src/react-components/room/SharePopover.js +++ b/src/react-components/room/SharePopover.js @@ -5,22 +5,28 @@ import { Popover } from "../popover/Popover"; import { ToolbarButton } from "../input/ToolbarButton"; import { ReactComponent as ShareIcon } from "../icons/Share.svg"; -export function SharePopoverButton({ items, onSelect }) { +export function SharePopoverButton({ items }) { + const filteredItems = items.filter(item => !!item); + // The button is removed if you can't share anything. - if (items.length === 0) { - return undefined; + if (filteredItems.length === 0) { + return null; } - const activeItem = items.find(item => item.active); + const activeItem = filteredItems.find(item => item.active); // If there's one item to share (your smartphone camera), or an item is active (recording), then only show that button. - if (items.length === 1 || activeItem) { - const item = items[0]; + if (filteredItems.length === 1 || activeItem) { + const item = filteredItems[0]; const Icon = item.icon; return ( } - onClick={() => onSelect(item)} + onClick={() => { + if (item.onSelect) { + item.onSelect(item); + } + }} label="Share" preset="purple" statusColor={activeItem && "red"} @@ -31,10 +37,9 @@ export function SharePopoverButton({ items, onSelect }) { return ( } + content={props => } placement="top" offsetDistance={28} - initiallyVisible disableFullscreen > {({ togglePopover, popoverVisible, triggerRef }) => ( @@ -58,8 +63,8 @@ SharePopoverButton.propTypes = { icon: PropTypes.elementType.isRequired, color: PropTypes.string, name: PropTypes.string.isRequired, - active: PropTypes.bool + active: PropTypes.bool, + onSelect: PropTypes.func }) - ).isRequired, - onSelect: PropTypes.func + ).isRequired }; diff --git a/src/react-components/room/SharePopover.stories.js b/src/react-components/room/SharePopover.stories.js index ffc6d93e2d..d8f77210c6 100644 --- a/src/react-components/room/SharePopover.stories.js +++ b/src/react-components/room/SharePopover.stories.js @@ -13,17 +13,13 @@ const items = [ { id: "screen", icon: DesktopIcon, color: "purple", label: "Screen" } ]; -export const Base = () => ( - console.log(item)} />} /> -); +export const Base = () => } />; Base.parameters = { layout: "fullscreen" }; -export const Mobile = () => ( - console.log(item)} />} /> -); +export const Mobile = () => } />; Mobile.parameters = { layout: "fullscreen" @@ -34,9 +30,7 @@ const activeItems = [ { id: "screen", icon: DesktopIcon, color: "purple", label: "Screen" } ]; -export const Active = () => ( - console.log(item)} />} /> -); +export const Active = () => } />; Active.parameters = { layout: "fullscreen" diff --git a/src/react-components/room/SharePopoverContainer.js b/src/react-components/room/SharePopoverContainer.js index ae31d6c8d9..b7064254fb 100644 --- a/src/react-components/room/SharePopoverContainer.js +++ b/src/react-components/room/SharePopoverContainer.js @@ -1,16 +1,123 @@ -import React from "react"; +import React, { useCallback, useEffect, useState } from "react"; import PropTypes from "prop-types"; import { ReactComponent as VideoIcon } from "../icons/Video.svg"; import { ReactComponent as DesktopIcon } from "../icons/Desktop.svg"; import { SharePopoverButton } from "./SharePopover"; -const items = [ - { id: "camera", icon: VideoIcon, color: "purple", label: "Camera" }, - { id: "screen", icon: DesktopIcon, color: "purple", label: "Screen" } -]; +function useShare(scene, hubChannel) { + const [sharingSource, setSharingSource] = useState(null); + const [canShareCamera, setCanShareCamera] = useState(false); + const [canShareScreen, setCanShareScreen] = useState(false); -export function SharePopoverContainer() { - return console.log(item)} />; + useEffect( + () => { + function onShareVideoEnabled(event) { + setSharingSource(event.detail.source); + } + + function onShareVideoDisabled() { + setSharingSource(null); + } + + function onPermissionsUpdated() { + const canShareMedia = hubChannel.can("spawn_and_move_media"); + + if (canShareMedia) { + navigator.mediaDevices + .enumerateDevices() + .then(devices => { + const hasCamera = devices.find(device => device.kind === "videoinput"); + setCanShareCamera(hasCamera); + }) + .catch(() => { + setCanShareCamera(false); + }); + + setCanShareScreen(!!navigator.mediaDevices.getDisplayMedia); + } else { + setCanShareScreen(false); + setCanShareCamera(false); + } + } + + scene.addEventListener("share_video_enabled", onShareVideoEnabled); + scene.addEventListener("share_video_disabled", onShareVideoDisabled); + // TODO: Show share error dialog + scene.addEventListener("share_video_failed", onShareVideoDisabled); + hubChannel.addEventListener("permissions_updated", onPermissionsUpdated); + + onPermissionsUpdated(); + + return () => { + scene.removeEventListener("share_video_enabled", onShareVideoEnabled); + scene.removeEventListener("share_video_disabled", onShareVideoDisabled); + scene.removeEventListener("share_video_failed", onShareVideoDisabled); + hubChannel.removeEventListener("permissions_updated", onPermissionsUpdated); + }; + }, + [scene, hubChannel] + ); + + const toggleShareCamera = useCallback( + () => { + if (sharingSource) { + scene.emit("action_end_video_sharing"); + } else { + scene.emit("action_share_camera"); + } + }, + [scene, sharingSource] + ); + + const toggleShareScreen = useCallback( + () => { + if (sharingSource) { + scene.emit("action_end_video_sharing"); + } else { + scene.emit("action_share_screen"); + } + }, + [scene, sharingSource] + ); + + return { + sharingSource, + canShareCamera, + canShareScreen, + toggleShareCamera, + toggleShareScreen + }; +} + +export function SharePopoverContainer({ scene, hubChannel }) { + const { sharingSource, canShareCamera, toggleShareCamera, canShareScreen, toggleShareScreen } = useShare( + scene, + hubChannel + ); + + const items = [ + canShareCamera && { + id: "camera", + icon: VideoIcon, + color: "purple", + label: "Camera", + onSelect: toggleShareCamera, + active: sharingSource === "camera" + }, + canShareScreen && { + id: "screen", + icon: DesktopIcon, + color: "purple", + label: "Screen", + onSelect: toggleShareScreen, + active: sharingSource === "screen" + } + ]; + + return ; } -SharePopoverContainer.propTypes = {}; +SharePopoverContainer.propTypes = { + hubChannel: PropTypes.object.isRequired, + scene: PropTypes.object.isRequired +}; diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 47c9461b13..2f4157c2b1 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -1897,7 +1897,7 @@ class UIRoot extends Component { {entered && ( <> } label="Voice" preset="basic" /> - + Date: Tue, 20 Oct 2020 17:29:12 -0700 Subject: [PATCH 05/13] Add permissions to PlacePopover --- src/react-components/room/PlacePopover.js | 10 +- .../room/PlacePopover.stories.js | 4 +- .../room/PlacePopoverContainer.js | 94 ++++++++++--------- src/react-components/ui-root.js | 3 +- 4 files changed, 57 insertions(+), 54 deletions(-) diff --git a/src/react-components/room/PlacePopover.js b/src/react-components/room/PlacePopover.js index f9237012ac..b46f45c42c 100644 --- a/src/react-components/room/PlacePopover.js +++ b/src/react-components/room/PlacePopover.js @@ -1,23 +1,23 @@ import React from "react"; -import PropTypes from "prop-types"; import { ButtonGridPopover } from "../popover/ButtonGridPopover"; import { Popover } from "../popover/Popover"; import { ToolbarButton } from "../input/ToolbarButton"; import { ReactComponent as ObjectIcon } from "../icons/Object.svg"; export function PlacePopoverButton({ items }) { + const filteredItems = items.filter(item => !!item); + // The button is removed if you can't place anything. - if (items.length === 0) { - return undefined; + if (filteredItems.length === 0) { + return null; } return ( } + content={props => } placement="top" offsetDistance={28} - initiallyVisible > {({ togglePopover, popoverVisible, triggerRef }) => ( ( - console.log(item)} />} /> -); +export const Base = () => } />; Base.parameters = { layout: "fullscreen" diff --git a/src/react-components/room/PlacePopoverContainer.js b/src/react-components/room/PlacePopoverContainer.js index 299422a164..8501ba9b34 100644 --- a/src/react-components/room/PlacePopoverContainer.js +++ b/src/react-components/room/PlacePopoverContainer.js @@ -11,68 +11,74 @@ import { ReactComponent as SceneIcon } from "../icons/Scene.svg"; import { ReactComponent as UploadIcon } from "../icons/Upload.svg"; import { PlacePopoverButton } from "./PlacePopover"; -export function PlacePopoverContainer({ scene, mediaSearchStore, pushHistoryState }) { - // TODO: Check permissions for each item - const items = [ - { +export function PlacePopoverContainer({ scene, mediaSearchStore, pushHistoryState, hubChannel }) { + let items = [ + hubChannel.can("spawn_drawing") && { id: "pen", icon: PenIcon, color: "purple", label: "Pen", onSelect: () => scene.emit("penButtonPressed") }, - { + hubChannel.can("spawn_camera") && { id: "camera", icon: CameraIcon, color: "purple", label: "Camera", onSelect: () => scene.emit("action_toggle_camera") - }, - // TODO: Create text/link dialog - // { id: "text", icon: TextIcon, color: "blue", label: "Text" }, - // { id: "link", icon: LinkIcon, color: "blue", label: "Link" }, - { - id: "gif", - icon: GIFIcon, - color: "orange", - label: "GIF", - onSelect: () => mediaSearchStore.sourceNavigate("gifs") - }, - { - id: "model", - icon: ObjectIcon, - color: "orange", - label: "3D Model", - onSelect: () => mediaSearchStore.sourceNavigate("poly") - }, - { - id: "avatar", - icon: AvatarIcon, - color: "red", - label: "Avatar", - onSelect: () => mediaSearchStore.sourceNavigate("avatars") - }, - { - id: "scene", - icon: SceneIcon, - color: "red", - label: "Scene", - onSelect: () => mediaSearchStore.sourceNavigate("scenes") - }, - // TODO: Launch system file prompt directly - { - id: "upload", - icon: UploadIcon, - color: "green", - label: "Upload", - onSelect: () => pushHistoryState("modal", "create") } ]; + if (hubChannel.can("spawn_and_move_media")) { + items = [ + ...items, + // TODO: Create text/link dialog + // { id: "text", icon: TextIcon, color: "blue", label: "Text" }, + // { id: "link", icon: LinkIcon, color: "blue", label: "Link" }, + { + id: "gif", + icon: GIFIcon, + color: "orange", + label: "GIF", + onSelect: () => mediaSearchStore.sourceNavigate("gifs") + }, + { + id: "model", + icon: ObjectIcon, + color: "orange", + label: "3D Model", + onSelect: () => mediaSearchStore.sourceNavigate("poly") + }, + { + id: "avatar", + icon: AvatarIcon, + color: "red", + label: "Avatar", + onSelect: () => mediaSearchStore.sourceNavigate("avatars") + }, + { + id: "scene", + icon: SceneIcon, + color: "red", + label: "Scene", + onSelect: () => mediaSearchStore.sourceNavigate("scenes") + }, + // TODO: Launch system file prompt directly + { + id: "upload", + icon: UploadIcon, + color: "green", + label: "Upload", + onSelect: () => pushHistoryState("modal", "create") + } + ]; + } + return ; } PlacePopoverContainer.propTypes = { + hubChannel: PropTypes.object.isRequired, scene: PropTypes.object.isRequired, mediaSearchStore: PropTypes.object.isRequired, pushHistoryState: PropTypes.object.isRequired diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 2f4157c2b1..d14e42a633 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -92,8 +92,6 @@ import { ReactComponent as VRIcon } from "./icons/VR.svg"; import { ReactComponent as PeopleIcon } from "./icons/People.svg"; import { ReactComponent as ObjectsIcon } from "./icons/Objects.svg"; import { ReactComponent as MicrophoneIcon } from "./icons/Microphone.svg"; -import { ReactComponent as ShareIcon } from "./icons/Share.svg"; -import { ReactComponent as ObjectIcon } from "./icons/Object.svg"; import { ReactComponent as ReactionIcon } from "./icons/Reaction.svg"; import { ReactComponent as LeaveIcon } from "./icons/Leave.svg"; import { PeopleSidebarContainer, userFromPresence } from "./room/PeopleSidebarContainer"; @@ -1900,6 +1898,7 @@ class UIRoot extends Component { From adfc22bc349f92aba7fb4731831bf17ad3551734 Mon Sep 17 00:00:00 2001 From: Robert Long Date: Wed, 21 Oct 2020 11:56:28 -0700 Subject: [PATCH 06/13] Fix prop types for popovers --- src/react-components/room/PlacePopover.js | 3 ++- src/react-components/room/PlacePopoverContainer.js | 2 +- src/react-components/room/SharePopover.js | 11 +---------- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/react-components/room/PlacePopover.js b/src/react-components/room/PlacePopover.js index b46f45c42c..8a5e1ca3e7 100644 --- a/src/react-components/room/PlacePopover.js +++ b/src/react-components/room/PlacePopover.js @@ -1,4 +1,5 @@ import React from "react"; +import PropTypes from "prop-types"; import { ButtonGridPopover } from "../popover/ButtonGridPopover"; import { Popover } from "../popover/Popover"; import { ToolbarButton } from "../input/ToolbarButton"; @@ -34,5 +35,5 @@ export function PlacePopoverButton({ items }) { } PlacePopoverButton.propTypes = { - items: ButtonGridPopover.propTypes.items + items: PropTypes.array.isRequired }; diff --git a/src/react-components/room/PlacePopoverContainer.js b/src/react-components/room/PlacePopoverContainer.js index 8501ba9b34..09e68067e6 100644 --- a/src/react-components/room/PlacePopoverContainer.js +++ b/src/react-components/room/PlacePopoverContainer.js @@ -81,5 +81,5 @@ PlacePopoverContainer.propTypes = { hubChannel: PropTypes.object.isRequired, scene: PropTypes.object.isRequired, mediaSearchStore: PropTypes.object.isRequired, - pushHistoryState: PropTypes.object.isRequired + pushHistoryState: PropTypes.func.isRequired }; diff --git a/src/react-components/room/SharePopover.js b/src/react-components/room/SharePopover.js index 82afaf775a..31082862d2 100644 --- a/src/react-components/room/SharePopover.js +++ b/src/react-components/room/SharePopover.js @@ -57,14 +57,5 @@ export function SharePopoverButton({ items }) { } SharePopoverButton.propTypes = { - items: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - icon: PropTypes.elementType.isRequired, - color: PropTypes.string, - name: PropTypes.string.isRequired, - active: PropTypes.bool, - onSelect: PropTypes.func - }) - ).isRequired + items: PropTypes.array.isRequired }; From 8ecd1a2ac23c1d751b30d246e3a0a0ab824edb93 Mon Sep 17 00:00:00 2001 From: Robert Long Date: Wed, 21 Oct 2020 13:56:29 -0700 Subject: [PATCH 07/13] Add VoiceButtonContainer logic --- .storybook/main.js | 5 ++- src/components/mute-mic.js | 1 - src/react-components/icons/Microphone.svg | 1 + .../icons/MicrophoneMuted.svg | 1 + .../room/MicSetupModalContainer.js | 4 +- src/react-components/room/PeopleSidebar.js | 6 +-- .../room/VoiceButtonContainer.js | 45 +++++++++++++++++++ ...seMicrophoneVolume.js => useMicrophone.js} | 30 ++++++++++--- src/react-components/ui-root.js | 4 +- webpack.config.js | 5 ++- 10 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 src/react-components/room/VoiceButtonContainer.js rename src/react-components/room/{useMicrophoneVolume.js => useMicrophone.js} (54%) diff --git a/.storybook/main.js b/.storybook/main.js index 6bc6d75b29..b11cdf7d1e 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -35,7 +35,10 @@ module.exports = { template: require("../src/react-components/icons/IconTemplate"), svgoConfig: { plugins: { - removeViewBox: false + removeViewBox: false, + mergePaths: false, + convertShapeToPath: false, + removeHiddenElems: false } } } diff --git a/src/components/mute-mic.js b/src/components/mute-mic.js index 41c0ad8fd2..c723f317f4 100644 --- a/src/components/mute-mic.js +++ b/src/components/mute-mic.js @@ -55,7 +55,6 @@ AFRAME.registerComponent("mute-mic", { if (!NAF.connection.adapter) return; if (!this.el.sceneEl.is("entered")) return; - this.el.sceneEl.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_TOGGLE_MIC); if (this.el.is("muted")) { NAF.connection.adapter.enableMicrophone(true); this.el.removeState("muted"); diff --git a/src/react-components/icons/Microphone.svg b/src/react-components/icons/Microphone.svg index 3c19ecdaeb..5478383c32 100644 --- a/src/react-components/icons/Microphone.svg +++ b/src/react-components/icons/Microphone.svg @@ -1,5 +1,6 @@ + diff --git a/src/react-components/icons/MicrophoneMuted.svg b/src/react-components/icons/MicrophoneMuted.svg index 8bb2e5a171..98e5d9af98 100644 --- a/src/react-components/icons/MicrophoneMuted.svg +++ b/src/react-components/icons/MicrophoneMuted.svg @@ -1,5 +1,6 @@ + diff --git a/src/react-components/room/MicSetupModalContainer.js b/src/react-components/room/MicSetupModalContainer.js index d168db9c47..623845e11e 100644 --- a/src/react-components/room/MicSetupModalContainer.js +++ b/src/react-components/room/MicSetupModalContainer.js @@ -1,7 +1,7 @@ import React from "react"; import PropTypes from "prop-types"; import { MicSetupModal } from "./MicSetupModal"; -import { useMicrophoneVolume } from "./useMicrophoneVolume"; +import { useMicrophone } from "./useMicrophone"; import { useSound } from "./useSound"; import webmSrc from "../../assets/sfx/tone.webm"; import mp3Src from "../../assets/sfx/tone.mp3"; @@ -9,7 +9,7 @@ import oggSrc from "../../assets/sfx/tone.ogg"; import wavSrc from "../../assets/sfx/tone.wav"; export function MicSetupModalContainer({ scene, ...rest }) { - const volume = useMicrophoneVolume(scene); + const { volume } = useMicrophone(scene); const [soundPlaying, playSound] = useSound({ webmSrc, mp3Src, oggSrc, wavSrc }); return ; } diff --git a/src/react-components/room/PeopleSidebar.js b/src/react-components/room/PeopleSidebar.js index dbdc6da13a..0992c7bcec 100644 --- a/src/react-components/room/PeopleSidebar.js +++ b/src/react-components/room/PeopleSidebar.js @@ -54,10 +54,10 @@ function getVoiceLabel(micPresence) { function getVoiceIconComponent(micPresence) { if (micPresence) { - if (micPresence.talking) { - return VolumeHighIcon; - } else if (micPresence.muted) { + if (micPresence.muted) { return VolumeMutedIcon; + } else if (micPresence.talking) { + return VolumeHighIcon; } } diff --git a/src/react-components/room/VoiceButtonContainer.js b/src/react-components/room/VoiceButtonContainer.js new file mode 100644 index 0000000000..f78c0e88c5 --- /dev/null +++ b/src/react-components/room/VoiceButtonContainer.js @@ -0,0 +1,45 @@ +import React, { useEffect, useRef } from "react"; +import PropTypes from "prop-types"; +import { ReactComponent as MicrophoneIcon } from "../icons/Microphone.svg"; +import { ReactComponent as MicrophoneMutedIcon } from "../icons/MicrophoneMuted.svg"; +import { ToolbarButton } from "../input/ToolbarButton"; +import { useMicrophone } from "./useMicrophone"; + +export function VoiceButtonContainer({ scene, microphoneEnabled }) { + const buttonRef = useRef(); + + const { isMuted, volume, toggleMute } = useMicrophone(scene); + + useEffect( + () => { + const rect = buttonRef.current.querySelector("rect"); + + if (volume < 0.05) { + rect.setAttribute("height", 0); + } else if (volume < 0.3) { + rect.setAttribute("y", 8); + rect.setAttribute("height", 4); + } else { + rect.setAttribute("y", 4); + rect.setAttribute("height", 8); + } + }, + [volume, isMuted] + ); + + return ( + : } + label="Voice" + preset="basic" + onClick={toggleMute} + statusColor={microphoneEnabled ? "green" : undefined} + /> + ); +} + +VoiceButtonContainer.propTypes = { + scene: PropTypes.object.isRequired, + microphoneEnabled: PropTypes.bool +}; diff --git a/src/react-components/room/useMicrophoneVolume.js b/src/react-components/room/useMicrophone.js similarity index 54% rename from src/react-components/room/useMicrophoneVolume.js rename to src/react-components/room/useMicrophone.js index 348556752d..68a78715c1 100644 --- a/src/react-components/room/useMicrophoneVolume.js +++ b/src/react-components/room/useMicrophone.js @@ -1,17 +1,17 @@ -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import MovingAverage from "moving-average"; -const MOVING_AVG_TIMESPAN = 100; const UPDATE_RATE = 50; -export function useMicrophoneVolume(scene) { +export function useMicrophone(scene, updateRate = UPDATE_RATE) { const movingAvgRef = useRef(); + const [isMuted, setIsMuted] = useState(scene.is("muted")); const [volume, setVolume] = useState(0); useEffect( () => { if (!movingAvgRef.current) { - movingAvgRef.current = MovingAverage(MOVING_AVG_TIMESPAN); + movingAvgRef.current = MovingAverage(updateRate * 2); } let max = 0; @@ -31,12 +31,30 @@ export function useMicrophoneVolume(scene) { updateMicVolume(); + function onSceneStateChange(event) { + if (event.detail === "muted") { + setIsMuted(scene.is("muted")); + } + } + + scene.addEventListener("stateadded", onSceneStateChange); + scene.addEventListener("stateremoved", onSceneStateChange); + return () => { clearTimeout(timeout); + scene.removeEventListener("stateadded", onSceneStateChange); + scene.removeEventListener("stateremoved", onSceneStateChange); }; }, - [setVolume, scene] + [setVolume, scene, updateRate] + ); + + const toggleMute = useCallback( + () => { + scene.emit("action_mute"); + }, + [scene] ); - return volume; + return { isMuted, volume, toggleMute }; } diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index d14e42a633..470ec5d82a 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -91,7 +91,6 @@ import { ReactComponent as DiscordIcon } from "./icons/Discord.svg"; import { ReactComponent as VRIcon } from "./icons/VR.svg"; import { ReactComponent as PeopleIcon } from "./icons/People.svg"; import { ReactComponent as ObjectsIcon } from "./icons/Objects.svg"; -import { ReactComponent as MicrophoneIcon } from "./icons/Microphone.svg"; import { ReactComponent as ReactionIcon } from "./icons/Reaction.svg"; import { ReactComponent as LeaveIcon } from "./icons/Leave.svg"; import { PeopleSidebarContainer, userFromPresence } from "./room/PeopleSidebarContainer"; @@ -101,6 +100,7 @@ import { ObjectMenuContainer } from "./room/ObjectMenuContainer"; import { useCssBreakpoints } from "react-use-css-breakpoints"; import { PlacePopoverContainer } from "./room/PlacePopoverContainer"; import { SharePopoverContainer } from "./room/SharePopoverContainer"; +import { VoiceButtonContainer } from "./room/VoiceButtonContainer"; const avatarEditorDebug = qsTruthy("avatarEditorDebug"); @@ -1894,7 +1894,7 @@ class UIRoot extends Component { <> {entered && ( <> - } label="Voice" preset="basic" /> + { template: require("./src/react-components/icons/IconTemplate"), svgoConfig: { plugins: { - removeViewBox: false + removeViewBox: false, + mergePaths: false, + convertShapeToPath: false, + removeHiddenElems: false } } } From f5ed3beae32d4c8fb382ab50fe0315d56247cca7 Mon Sep 17 00:00:00 2001 From: Robert Long Date: Wed, 21 Oct 2020 14:18:37 -0700 Subject: [PATCH 08/13] Add leave room functionality --- src/hub.js | 42 +++---- src/react-components/room/ExitedRoomScreen.js | 75 +++++++++++++ src/react-components/ui-root.js | 106 +++--------------- 3 files changed, 114 insertions(+), 109 deletions(-) create mode 100644 src/react-components/room/ExitedRoomScreen.js diff --git a/src/hub.js b/src/hub.js index 6c004e8e8b..c93d834b49 100644 --- a/src/hub.js +++ b/src/hub.js @@ -126,6 +126,7 @@ import { Router, Route } from "react-router-dom"; import { createBrowserHistory, createMemoryHistory } from "history"; import { pushHistoryState } from "./utils/history"; import UIRoot from "./react-components/ui-root"; +import { ExitedRoomScreen } from "./react-components/room/ExitedRoomScreen"; import AuthChannel from "./utils/auth-channel"; import HubChannel from "./utils/hub-channel"; import LinkChannel from "./utils/link-channel"; @@ -285,22 +286,26 @@ function mountUI(props = {}) { ( - - )} + render={routeProps => + props.roomUnavailableReason ? ( + + ) : ( + + ) + } /> , @@ -1000,10 +1005,7 @@ document.addEventListener("DOMContentLoaded", async () => { enterScene: entryManager.enterScene, exitScene: reason => { entryManager.exitScene(); - - if (reason) { - remountUI({ roomUnavailableReason: reason }); - } + remountUI({ roomUnavailableReason: reason || "exited" }); }, initialIsSubscribed: subscriptions.isSubscribed(), activeTips: scene.systems.tips.activeTips diff --git a/src/react-components/room/ExitedRoomScreen.js b/src/react-components/room/ExitedRoomScreen.js new file mode 100644 index 0000000000..0c007bd38a --- /dev/null +++ b/src/react-components/room/ExitedRoomScreen.js @@ -0,0 +1,75 @@ +import React from "react"; +import PropTypes from "prop-types"; +import IfFeature from "../if-feature"; +import { getMessages } from "../../utils/i18n"; +import configs from "../../utils/configs"; +import { FormattedMessage } from "react-intl"; + +export function ExitedRoomScreen({ reason }) { + let subtitle = null; + if (reason === "closed") { + // TODO i18n, due to links and markup + subtitle = ( +
+ Sorry, this room is no longer available. +

+ + A room may be closed by the room owner, or if we receive reports that it violates our{" "} + + Terms of Use + + .
+
+ If you have questions, contact us at{" "} + + + + .

+ + If you'd like to run your own server, Hubs's source code is available on{" "} + GitHub + . + +

+ ); + } else { + const tcpUrl = new URL(document.location.toString()); + const tcpParams = new URLSearchParams(tcpUrl.search); + tcpParams.set("force_tcp", true); + tcpUrl.search = tcpParams.toString(); + + const exitSubtitleId = `exit.subtitle.${reason}`; + subtitle = ( +
+ +

+ {reason === "connect_error" && ( +

+ You can try connecting via TCP, which may work better on some networks. +
+ )} + {!["left", "disconnected", "scene_error"].includes(reason) && ( +
+ You can also create a new room + . +
+ )} +
+ ); + } + + return ( +
+ +
{subtitle}
+
+ ); +} + +ExitedRoomScreen.propTypes = { + reason: PropTypes.string +}; diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 470ec5d82a..6f56a9af1a 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -6,7 +6,6 @@ import { FormattedMessage } from "react-intl"; import screenfull from "screenfull"; import configs from "../utils/configs"; -import IfFeature from "./if-feature"; import { VR_DEVICE_AVAILABILITY } from "../utils/vr-caps-detect"; import { canShare } from "../utils/share"; import styles from "../assets/stylesheets/ui-root.scss"; @@ -225,8 +224,6 @@ class UIRoot extends Component { muted: false, frozen: false, - exited: false, - signedIn: false, videoShareMediaSource: null, showVideoShareFailed: false, @@ -246,7 +243,7 @@ class UIRoot extends Component { props.mediaSearchStore.setHistory(props.history); // An exit handler that discards event arguments and can be cleaned up. - this.exitEventHandler = () => this.exit(); + this.exitEventHandler = () => this.props.exitScene(); } componentDidUpdate(prevProps) { @@ -274,7 +271,7 @@ class UIRoot extends Component { try { this.props.scene.renderer.compileAndUploadMaterials(this.props.scene.object3D, this.props.scene.camera); } catch { - this.exit("scene_error"); // https://github.com/mozilla/hubs/issues/1950 + this.props.exitScene("scene_error"); // https://github.com/mozilla/hubs/issues/1950 } } @@ -407,6 +404,9 @@ class UIRoot extends Component { this.props.scene.removeEventListener("share_video_disabled", this.onShareVideoDisabled); this.props.scene.removeEventListener("share_video_failed", this.onShareVideoFailed); this.props.store.removeEventListener("statechanged", this.storeUpdated); + window.removeEventListener("concurrentload", this.onConcurrentLoad); + window.removeEventListener("idle_detected", this.onIdleDetected); + window.removeEventListener("activity_detected", this.onActivityDetected); } storeUpdated = () => { @@ -554,19 +554,7 @@ class UIRoot extends Component { checkForAutoExit = () => { if (this.state.secondsRemainingBeforeAutoExit !== 0) return; this.endAutoExitTimer(); - this.exit(); - }; - - exit = reason => { - window.removeEventListener("concurrentload", this.onConcurrentLoad); - window.removeEventListener("idle_detected", this.onIdleDetected); - window.removeEventListener("activity_detected", this.onActivityDetected); - - if (this.props.exitScene) { - this.props.exitScene(reason); - } - - this.setState({ exited: true }); + this.props.exitScene(); }; isWaitingForAutoExit = () => { @@ -830,7 +818,7 @@ class UIRoot extends Component { this.setState({ linkCode: code, linkCodeCancel: cancel }); onFinished.then(() => { this.setState({ log: false, linkCode: null, linkCodeCancel: null }); - this.exit(); + this.props.exitScene(); }); }; @@ -1021,72 +1009,6 @@ class UIRoot extends Component { ); }; - renderExitedPane = () => { - let subtitle = null; - if (this.props.roomUnavailableReason === "closed") { - // TODO i18n, due to links and markup - subtitle = ( -
- Sorry, this room is no longer available. -

- - A room may be closed by the room owner, or if we receive reports that it violates our{" "} - - Terms of Use - - .
-
- If you have questions, contact us at{" "} - - - - .

- - If you'd like to run your own server, Hubs's source code is available on{" "} - GitHub - . - -

- ); - } else { - const reason = this.props.roomUnavailableReason; - const tcpUrl = new URL(document.location.toString()); - const tcpParams = new URLSearchParams(tcpUrl.search); - tcpParams.set("force_tcp", true); - tcpUrl.search = tcpParams.toString(); - - const exitSubtitleId = `exit.subtitle.${reason || "exited"}`; - subtitle = ( -
- -

- {this.props.roomUnavailableReason === "connect_error" && ( -

- You can try connecting via TCP, which may work better on some networks. -
- )} - {!["left", "disconnected", "scene_error"].includes(this.props.roomUnavailableReason) && ( -
- You can also create a new room - . -
- )} -
- ); - } - - return ( -
- -
{subtitle}
-
- ); - }; - renderBotMode = () => { return (
@@ -1248,7 +1170,6 @@ class UIRoot extends Component { }; if (this.props.hide || this.state.hide) return
; - const isExited = this.state.exited || this.props.roomUnavailableReason; const preload = this.props.showPreload; const isLoading = !preload && !this.state.hideLoader && !this.props.showSafariMicDialog; @@ -1259,7 +1180,7 @@ class UIRoot extends Component {
); - if (isExited) return this.renderExitedPane(); + if (isLoading && this.state.showPrefs) { return (
@@ -1800,7 +1721,7 @@ class UIRoot extends Component { /> )} {this.state.frozen && ( - )} @@ -1919,7 +1840,14 @@ class UIRoot extends Component { onClick={() => exit2DInterstitialAndEnterVR(true)} /> )} - {entered && } label="Leave" preset="red" />} + {entered && ( + } + label="Leave" + preset="red" + onClick={() => this.props.exitScene("left")} + /> + )} } From 1fcb722cdc02c8e68b069cc9202ad1376292c3f6 Mon Sep 17 00:00:00 2001 From: Robert Long Date: Wed, 21 Oct 2020 14:43:59 -0700 Subject: [PATCH 09/13] Remove leave button from pause screen --- src/react-components/ui-root.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 6f56a9af1a..83b2efe2cc 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -221,9 +221,6 @@ class UIRoot extends Component { autoExitMessage: null, secondsRemainingBeforeAutoExit: Infinity, - muted: false, - frozen: false, - signedIn: false, videoShareMediaSource: null, showVideoShareFailed: false, @@ -478,14 +475,6 @@ class UIRoot extends Component { this.setState({ sceneLoaded: true }); }; - // 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 - onAframeStateChanged = e => { - if (!(e.detail === "muted" || e.detail === "frozen")) return; - this.setState({ - [e.detail]: this.props.scene.is(e.detail) - }); - }; - onShareVideoEnabled = e => { this.setState({ videoShareMediaSource: e.detail.source }); }; @@ -1720,11 +1709,6 @@ class UIRoot extends Component { onClose={() => this.confirmBroadcastedRoom()} /> )} - {this.state.frozen && ( - - )} Date: Thu, 22 Oct 2020 13:11:20 -0700 Subject: [PATCH 10/13] Fix pen/camera state in place menu --- src/components/tools/pen.js | 4 + src/hub.js | 16 +- .../popover/ButtonGridPopover.js | 8 +- .../room/PlacePopoverContainer.js | 155 +++++++++++------- src/react-components/ui-root.js | 2 - src/systems/pen-tools.js | 43 +++++ 6 files changed, 149 insertions(+), 79 deletions(-) create mode 100644 src/systems/pen-tools.js diff --git a/src/components/tools/pen.js b/src/components/tools/pen.js index 5765f6a074..9e6e1aa760 100644 --- a/src/components/tools/pen.js +++ b/src/components/tools/pen.js @@ -164,6 +164,9 @@ AFRAME.registerComponent("pen", { scene.addEventListener("object3dset", this.setDirty); scene.addEventListener("object3dremove", this.setDirty); }); + + this.penSystem = this.el.sceneEl.systems["pen-tools"]; + this.penSystem.register(this.el); }, play() { @@ -498,5 +501,6 @@ AFRAME.registerComponent("pen", { this.observer.disconnect(); AFRAME.scenes[0].removeEventListener("object3dset", this.setDirty); AFRAME.scenes[0].removeEventListener("object3dremove", this.setDirty); + this.penSystem.deregister(this.el); } }); diff --git a/src/hub.js b/src/hub.js index c93d834b49..fce211f457 100644 --- a/src/hub.js +++ b/src/hub.js @@ -118,7 +118,6 @@ import "./components/optional-alternative-to-not-hide"; import "./components/set-max-resolution"; import "./components/avatar-audio-source"; import "./components/avatar-inspect-collider"; -import { sets as userinputSets } from "./systems/userinput/sets"; import ReactDOM from "react-dom"; import React from "react"; @@ -150,6 +149,7 @@ import "./systems/exit-on-blur"; import "./systems/auto-pixel-ratio"; import "./systems/idle-detector"; import "./systems/camera-tools"; +import "./systems/pen-tools"; import "./systems/userinput/userinput"; import "./systems/userinput/userinput-debug"; import "./systems/ui-hotkeys"; @@ -275,11 +275,6 @@ function mountUI(props = {}) { const scene = document.querySelector("a-scene"); const disableAutoExitOnIdle = qsTruthy("allow_idle") || (process.env.NODE_ENV === "development" && !qs.get("idle_timeout")); - const isCursorHoldingPen = - scene && - (scene.systems.userinput.activeSets.includes(userinputSets.rightCursorHoldingPen) || - scene.systems.userinput.activeSets.includes(userinputSets.leftCursorHoldingPen)); - const hasActiveCamera = scene && !!scene.systems["camera-tools"].getMyCamera(); const forcedVREntryType = qsVREntryType; ReactDOM.render( @@ -298,8 +293,6 @@ function mountUI(props = {}) { forcedVREntryType, store, mediaSearchStore, - isCursorHoldingPen, - hasActiveCamera, ...props, ...routeProps }} @@ -1025,13 +1018,6 @@ document.addEventListener("DOMContentLoaded", async () => { remountUI({ roomUnavailableReason: "left" }); }); - const updateCameraUI = function(e) { - if (e.detail !== "camera") return; - remountUI({}); - }; - scene.addEventListener("stateadded", updateCameraUI); - scene.addEventListener("stateremoved", updateCameraUI); - scene.addEventListener("hub_closed", () => { scene.exitVR(); entryManager.exitScene("closed"); diff --git a/src/react-components/popover/ButtonGridPopover.js b/src/react-components/popover/ButtonGridPopover.js index d710e7a4bd..884c07abfc 100644 --- a/src/react-components/popover/ButtonGridPopover.js +++ b/src/react-components/popover/ButtonGridPopover.js @@ -4,7 +4,7 @@ import classNames from "classnames"; import { ToolbarButton } from "../input/ToolbarButton"; import styles from "./ButtonGridPopover.scss"; -export function ButtonGridPopover({ fullscreen, items }) { +export function ButtonGridPopover({ fullscreen, items, closePopover }) { return (
{items.map(item => { @@ -18,8 +18,11 @@ export function ButtonGridPopover({ fullscreen, items }) { if (item.onSelect) { item.onSelect(item); } + + closePopover(); }} label={item.label} + selected={item.selected} /> ); })} @@ -37,5 +40,6 @@ ButtonGridPopover.propTypes = { name: PropTypes.string.isRequired, onSelect: PropTypes.func }) - ).isRequired + ).isRequired, + closePopover: PropTypes.func.isRequired }; diff --git a/src/react-components/room/PlacePopoverContainer.js b/src/react-components/room/PlacePopoverContainer.js index 09e68067e6..30353300e6 100644 --- a/src/react-components/room/PlacePopoverContainer.js +++ b/src/react-components/room/PlacePopoverContainer.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import PropTypes from "prop-types"; import { ReactComponent as PenIcon } from "../icons/Pen.svg"; import { ReactComponent as CameraIcon } from "../icons/Camera.svg"; @@ -12,67 +12,102 @@ import { ReactComponent as UploadIcon } from "../icons/Upload.svg"; import { PlacePopoverButton } from "./PlacePopover"; export function PlacePopoverContainer({ scene, mediaSearchStore, pushHistoryState, hubChannel }) { - let items = [ - hubChannel.can("spawn_drawing") && { - id: "pen", - icon: PenIcon, - color: "purple", - label: "Pen", - onSelect: () => scene.emit("penButtonPressed") - }, - hubChannel.can("spawn_camera") && { - id: "camera", - icon: CameraIcon, - color: "purple", - label: "Camera", - onSelect: () => scene.emit("action_toggle_camera") - } - ]; + const [items, setItems] = useState([]); + + useEffect( + () => { + function updateItems() { + const hasActiveCamera = !!scene.systems["camera-tools"].getMyCamera(); + const hasActivePen = !!scene.systems["pen-tools"].getMyPen(); + + let nextItems = [ + hubChannel.can("spawn_drawing") && { + id: "pen", + icon: PenIcon, + color: "purple", + label: "Pen", + onSelect: () => scene.emit("penButtonPressed"), + selected: hasActivePen + }, + hubChannel.can("spawn_camera") && { + id: "camera", + icon: CameraIcon, + color: "purple", + label: "Camera", + onSelect: () => scene.emit("action_toggle_camera"), + selected: hasActiveCamera + } + ]; - if (hubChannel.can("spawn_and_move_media")) { - items = [ - ...items, - // TODO: Create text/link dialog - // { id: "text", icon: TextIcon, color: "blue", label: "Text" }, - // { id: "link", icon: LinkIcon, color: "blue", label: "Link" }, - { - id: "gif", - icon: GIFIcon, - color: "orange", - label: "GIF", - onSelect: () => mediaSearchStore.sourceNavigate("gifs") - }, - { - id: "model", - icon: ObjectIcon, - color: "orange", - label: "3D Model", - onSelect: () => mediaSearchStore.sourceNavigate("poly") - }, - { - id: "avatar", - icon: AvatarIcon, - color: "red", - label: "Avatar", - onSelect: () => mediaSearchStore.sourceNavigate("avatars") - }, - { - id: "scene", - icon: SceneIcon, - color: "red", - label: "Scene", - onSelect: () => mediaSearchStore.sourceNavigate("scenes") - }, - // TODO: Launch system file prompt directly - { - id: "upload", - icon: UploadIcon, - color: "green", - label: "Upload", - onSelect: () => pushHistoryState("modal", "create") + if (hubChannel.can("spawn_and_move_media")) { + nextItems = [ + ...nextItems, + // TODO: Create text/link dialog + // { id: "text", icon: TextIcon, color: "blue", label: "Text" }, + // { id: "link", icon: LinkIcon, color: "blue", label: "Link" }, + { + id: "gif", + icon: GIFIcon, + color: "orange", + label: "GIF", + onSelect: () => mediaSearchStore.sourceNavigate("gifs") + }, + { + id: "model", + icon: ObjectIcon, + color: "orange", + label: "3D Model", + onSelect: () => mediaSearchStore.sourceNavigate("poly") + }, + { + id: "avatar", + icon: AvatarIcon, + color: "red", + label: "Avatar", + onSelect: () => mediaSearchStore.sourceNavigate("avatars") + }, + { + id: "scene", + icon: SceneIcon, + color: "red", + label: "Scene", + onSelect: () => mediaSearchStore.sourceNavigate("scenes") + }, + // TODO: Launch system file prompt directly + { + id: "upload", + icon: UploadIcon, + color: "green", + label: "Upload", + onSelect: () => pushHistoryState("modal", "create") + } + ]; + } + + setItems(nextItems); } - ]; - } + + hubChannel.addEventListener("permissions_updated", updateItems); + + updateItems(); + + function onSceneStateChange(event) { + if (event.detail === "camera" || event.detail === "pen") { + updateItems(); + } + } + + scene.addEventListener("stateadded", onSceneStateChange); + scene.addEventListener("stateremoved", onSceneStateChange); + + return () => { + hubChannel.removeEventListener("permissions_updated", updateItems); + scene.removeEventListener("stateadded", onSceneStateChange); + scene.removeEventListener("stateremoved", onSceneStateChange); + }; + }, + [hubChannel, mediaSearchStore, pushHistoryState, scene] + ); return ; } diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 83b2efe2cc..cae6abe001 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -170,8 +170,6 @@ class UIRoot extends Component { showOAuthDialog: PropTypes.bool, onCloseOAuthDialog: PropTypes.func, oauthInfo: PropTypes.array, - isCursorHoldingPen: PropTypes.bool, - hasActiveCamera: PropTypes.bool, onMediaSearchResultEntrySelected: PropTypes.func, onAvatarSaved: PropTypes.func, activeTips: PropTypes.object, diff --git a/src/systems/pen-tools.js b/src/systems/pen-tools.js new file mode 100644 index 0000000000..b119e1a657 --- /dev/null +++ b/src/systems/pen-tools.js @@ -0,0 +1,43 @@ +import { waitForDOMContentLoaded } from "../utils/async-utils"; + +// Used for tracking and managing pen tools in the scene +AFRAME.registerSystem("pen-tools", { + init() { + this.penEls = []; + this.updateMyPen = this.updateMyPen.bind(this); + + waitForDOMContentLoaded().then(() => { + this.updateMyPen(); + }); + }, + + register(el) { + this.penEls.push(el); + el.addEventListener("ownership-changed", this.updateMyPen); + this.updateMyPen(); + }, + + deregister(el) { + this.penEls.splice(this.penEls.indexOf(el), 1); + el.removeEventListener("ownership-changed", this.updateMyPen); + this.updateMyPen(); + }, + + getMyPen() { + return this.myPen; + }, + + updateMyPen() { + if (!this.penEls.length) { + this.myPen = null; + } else { + this.myPen = this.penEls.find(NAF.utils.isMine); + } + + if (this.myPen) { + this.sceneEl.addState("pen"); + } else { + this.sceneEl.removeState("pen"); + } + } +}); From 175d1ae5805629699ba03594673eb83054094d97 Mon Sep 17 00:00:00 2001 From: Robert Long Date: Thu, 22 Oct 2020 13:37:06 -0700 Subject: [PATCH 11/13] Add React button functionality --- .../room/ReactionButtonContainer.js | 43 +++++++++++++++++++ src/react-components/ui-root.js | 4 +- 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 src/react-components/room/ReactionButtonContainer.js diff --git a/src/react-components/room/ReactionButtonContainer.js b/src/react-components/room/ReactionButtonContainer.js new file mode 100644 index 0000000000..8dc6a3eb86 --- /dev/null +++ b/src/react-components/room/ReactionButtonContainer.js @@ -0,0 +1,43 @@ +import React, { useEffect, useState, useCallback } from "react"; +import PropTypes from "prop-types"; +import { ReactComponent as ReactionIcon } from "../icons/Reaction.svg"; +import { ToolbarButton } from "../input/ToolbarButton"; + +export function ReactionButtonContainer({ scene }) { + const [isFrozen, setIsFrozen] = useState(scene.is("frozen")); + + useEffect( + () => { + function onSceneStateChange(event) { + if (event.detail === "frozen") { + setIsFrozen(scene.is("frozen")); + } + } + + scene.addEventListener("stateadded", onSceneStateChange); + scene.addEventListener("stateremoved", onSceneStateChange); + + return () => { + scene.removeEventListener("stateadded", onSceneStateChange); + scene.removeEventListener("stateremoved", onSceneStateChange); + }; + }, + [scene] + ); + + // TODO: We probably shouldn't use freeze mode for users spawning emojis on a 2d device. + const toggleFreeze = useCallback( + () => { + scene.components["freeze-controller"].onToggle(); + }, + [scene] + ); + + return ( + } label="React" preset="orange" selected={isFrozen} onClick={toggleFreeze} /> + ); +} + +ReactionButtonContainer.propTypes = { + scene: PropTypes.object.isRequired +}; diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index cae6abe001..fcca953cad 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -90,7 +90,6 @@ import { ReactComponent as DiscordIcon } from "./icons/Discord.svg"; import { ReactComponent as VRIcon } from "./icons/VR.svg"; import { ReactComponent as PeopleIcon } from "./icons/People.svg"; import { ReactComponent as ObjectsIcon } from "./icons/Objects.svg"; -import { ReactComponent as ReactionIcon } from "./icons/Reaction.svg"; import { ReactComponent as LeaveIcon } from "./icons/Leave.svg"; import { PeopleSidebarContainer, userFromPresence } from "./room/PeopleSidebarContainer"; import { ObjectListProvider } from "./room/useObjectList"; @@ -100,6 +99,7 @@ import { useCssBreakpoints } from "react-use-css-breakpoints"; import { PlacePopoverContainer } from "./room/PlacePopoverContainer"; import { SharePopoverContainer } from "./room/SharePopoverContainer"; import { VoiceButtonContainer } from "./room/VoiceButtonContainer"; +import { ReactionButtonContainer } from "./room/ReactionButtonContainer"; const avatarEditorDebug = qsTruthy("avatarEditorDebug"); @@ -1805,7 +1805,7 @@ class UIRoot extends Component { mediaSearchStore={this.props.mediaSearchStore} pushHistoryState={this.pushHistoryState} /> - } label="React" preset="orange" /> + )} this.toggleSidebar("chat")} /> From e3825be8be86c343acb562f02bdd32f64ddaae8d Mon Sep 17 00:00:00 2001 From: Robert Long Date: Thu, 22 Oct 2020 13:42:38 -0700 Subject: [PATCH 12/13] Fix linting --- src/components/mute-mic.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/mute-mic.js b/src/components/mute-mic.js index c723f317f4..fd220f0a32 100644 --- a/src/components/mute-mic.js +++ b/src/components/mute-mic.js @@ -1,5 +1,3 @@ -import { SOUND_TOGGLE_MIC } from "../systems/sound-effects-system"; - const bindAllEvents = function(elements, events, f) { if (!elements || !elements.length) return; for (const el of elements) { From 8adfe267f8350ed62be2c1c65a00f70d51aa95cd Mon Sep 17 00:00:00 2001 From: Robert Long Date: Tue, 27 Oct 2020 12:13:10 -0700 Subject: [PATCH 13/13] Fix PR feedback --- src/components/mute-mic.js | 3 +++ src/react-components/room/useMicrophone.js | 6 ++---- src/systems/camera-tools.js | 6 +----- src/systems/pen-tools.js | 6 +----- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/components/mute-mic.js b/src/components/mute-mic.js index fd220f0a32..41c0ad8fd2 100644 --- a/src/components/mute-mic.js +++ b/src/components/mute-mic.js @@ -1,3 +1,5 @@ +import { SOUND_TOGGLE_MIC } from "../systems/sound-effects-system"; + const bindAllEvents = function(elements, events, f) { if (!elements || !elements.length) return; for (const el of elements) { @@ -53,6 +55,7 @@ AFRAME.registerComponent("mute-mic", { if (!NAF.connection.adapter) return; if (!this.el.sceneEl.is("entered")) return; + this.el.sceneEl.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_TOGGLE_MIC); if (this.el.is("muted")) { NAF.connection.adapter.enableMicrophone(true); this.el.removeState("muted"); diff --git a/src/react-components/room/useMicrophone.js b/src/react-components/room/useMicrophone.js index 68a78715c1..014ec234c9 100644 --- a/src/react-components/room/useMicrophone.js +++ b/src/react-components/room/useMicrophone.js @@ -1,9 +1,7 @@ import { useState, useEffect, useRef, useCallback } from "react"; import MovingAverage from "moving-average"; -const UPDATE_RATE = 50; - -export function useMicrophone(scene, updateRate = UPDATE_RATE) { +export function useMicrophone(scene, updateRate = 50) { const movingAvgRef = useRef(); const [isMuted, setIsMuted] = useState(scene.is("muted")); const [volume, setVolume] = useState(0); @@ -26,7 +24,7 @@ export function useMicrophone(scene, updateRate = UPDATE_RATE) { const average = movingAvgRef.current.movingAverage(); const nextVolume = max === 0 ? 0 : average / max; setVolume(prevVolume => (Math.abs(prevVolume - nextVolume) > 0.05 ? nextVolume : prevVolume)); - timeout = setTimeout(updateMicVolume, UPDATE_RATE); + timeout = setTimeout(updateMicVolume, updateRate); }; updateMicVolume(); diff --git a/src/systems/camera-tools.js b/src/systems/camera-tools.js index f3d5ff4533..3372d44259 100644 --- a/src/systems/camera-tools.js +++ b/src/systems/camera-tools.js @@ -51,11 +51,7 @@ AFRAME.registerSystem("camera-tools", { }, updateMyCamera() { - if (!this.cameraEls.length) { - this.myCamera = null; - } else { - this.myCamera = this.cameraEls.find(NAF.utils.isMine); - } + this.myCamera = this.cameraEls.find(NAF.utils.isMine); if (this.myCamera) { this.sceneEl.addState("camera"); diff --git a/src/systems/pen-tools.js b/src/systems/pen-tools.js index b119e1a657..10dba58783 100644 --- a/src/systems/pen-tools.js +++ b/src/systems/pen-tools.js @@ -28,11 +28,7 @@ AFRAME.registerSystem("pen-tools", { }, updateMyPen() { - if (!this.penEls.length) { - this.myPen = null; - } else { - this.myPen = this.penEls.find(NAF.utils.isMine); - } + this.myPen = this.penEls.find(NAF.utils.isMine); if (this.myPen) { this.sceneEl.addState("pen");