diff --git a/src/assets/stylesheets/scene-ui.scss b/src/assets/stylesheets/scene-ui.scss index bd3abf0a6d82cf898081da35c23add7ce692b024..99f3ddae3255817ec0d7ad22e2bd15a4fa6d9a7f 100644 --- a/src/assets/stylesheets/scene-ui.scss +++ b/src/assets/stylesheets/scene-ui.scss @@ -38,10 +38,6 @@ justify-content: center; pointer-events: auto; - button { - @extend %action-button; - border: 0; - } } :local(.logoTagline) { @@ -95,6 +91,12 @@ :local(.attribution) { font-size: 1.0em; white-space: wrap; + + a { + font-size: 0.8em; + color: black; + pointer-events: auto; + } } :local(.screenshot) { @@ -147,3 +149,23 @@ width: 200px; } } + +:local(.createButtons) { + position: relative; + display: flex; +} + +:local(.createButton) { + @extend %action-button; + width: 100%; + border: 0; +} + +:local(.optionsButton) { + @extend %fa-icon-button; + @extend %fa-icon-big; + position: absolute; + right: 10px; + top: -12px; + color: white; +} diff --git a/src/assets/stylesheets/scene.scss b/src/assets/stylesheets/scene.scss index 44e6591aacfa994bb79a4a0354b129ae171c4741..49f588251e3067da70aae0151e0463d6508910e1 100644 --- a/src/assets/stylesheets/scene.scss +++ b/src/assets/stylesheets/scene.scss @@ -1,2 +1,3 @@ @import 'shared'; @import 'loader'; +@import 'info-dialog'; diff --git a/src/hub.js b/src/hub.js index 7a623bf7dfb53daf1bfd1f64d44e5e46acfbc87b..f877d9df0172d74d1f5232ca418e0e033b95f106 100644 --- a/src/hub.js +++ b/src/hub.js @@ -433,7 +433,7 @@ document.addEventListener("DOMContentLoaded", async () => { presenceLogEntries.splice(presenceLogEntries.indexOf(entry), 1); remountUI({ presenceLogEntries }); }, 5000); - }, 30000); + }, 20000); }; let isInitialSync = true; diff --git a/src/react-components/create-object-dialog.js b/src/react-components/create-object-dialog.js index 5ccfe2ce692a234fff6e76d8657516c1ac1d8816..6fa624612dcf4eadb658fa7f74f069983a9aeb53 100644 --- a/src/react-components/create-object-dialog.js +++ b/src/react-components/create-object-dialog.js @@ -15,12 +15,33 @@ const attributionHostnames = { const DEFAULT_OBJECT_URL = "https://asset-bundles-prod.reticulum.io/interactables/Ducky/DuckyMesh-438ff8e022.gltf"; const isMobile = AFRAME.utils.device.isMobile(); -const instructions = "Paste a URL or upload a file."; -const desktopTips = "Tip: You can paste links directly into Hubs with Ctrl+V"; -const mobileInstructions = <div>{instructions}</div>; +const instructions = "Paste a URL to an image, video, model, or upload a file."; +const desktopTips = "Tip: You can paste media directly into Hubs with Ctrl+V"; +const references = ( + <span> + For models, try{" "} + <a href="https://sketchfab.com/search?features=downloadable&type=models" target="_blank" rel="noopener noreferrer"> + Sketchfab + </a>,{" "} + <a href="http://poly.google.com/" target="_blank" rel="noopener noreferrer"> + Google Poly + </a>, or our{" "} + <a href="https://sketchfab.com/mozillareality" target="_blank" rel="noopener noreferrer"> + collection + </a>. + </span> +); + +const mobileInstructions = ( + <div> + <p>{instructions}</p> + <p>{references}</p> + </div> +); const desktopInstructions = ( <div> <p>{instructions}</p> + <p>{references}</p> <p>{desktopTips}</p> </div> ); diff --git a/src/react-components/create-room-dialog.js b/src/react-components/create-room-dialog.js index 756cc9bf43736059999a1a66b2fdfe5d856a4e96..a42a2f36be7fea6bdfef0d74c6120ce59e24df01 100644 --- a/src/react-components/create-room-dialog.js +++ b/src/react-components/create-room-dialog.js @@ -4,8 +4,9 @@ import DialogContainer from "./dialog-container.js"; const HUB_NAME_PATTERN = "^[A-Za-z0-9-'\":!@#$%^&*(),.?~ ]{4,64}$"; -export default class CreateObjectDialog extends Component { +export default class CreateRoomDialog extends Component { static propTypes = { + includeScenePrompt: PropTypes.bool, onCustomScene: PropTypes.func, onClose: PropTypes.func }; @@ -25,7 +26,12 @@ export default class CreateObjectDialog extends Component { return ( <DialogContainer title="Create a Room" onClose={onClose} {...other}> <div> - <div>Choose a name and GLTF URL for your room's scene:</div> + {this.props.includeScenePrompt ? ( + <div>Choose a name and GLTF URL for your room's scene:</div> + ) : ( + <div>Choose a name for your room:</div> + )} + <form onSubmit={onCustomSceneClicked}> <div className="custom-scene-form"> <input @@ -38,16 +44,18 @@ export default class CreateObjectDialog extends Component { onChange={e => this.setState({ customRoomName: e.target.value })} required /> - <input - type="url" - placeholder="URL to Scene GLTF or GLB (Optional)" - className="custom-scene-form__link_field" - value={this.state.customSceneUrl} - onChange={e => this.setState({ customSceneUrl: e.target.value })} - /> + {this.props.includeScenePrompt && ( + <input + type="url" + placeholder="URL to Scene GLTF or GLB (Optional)" + className="custom-scene-form__link_field" + value={this.state.customSceneUrl} + onChange={e => this.setState({ customSceneUrl: e.target.value })} + /> + )} <div className="custom-scene-form__buttons"> <button className="custom-scene-form__action-button"> - <span>create</span> + <span>Create Room</span> </button> </div> </div> diff --git a/src/react-components/hub-create-panel.js b/src/react-components/hub-create-panel.js index 8a9ca35e28edb3d99c3685e483e3f031c755c557..640f00d5d790516e0d8cd2d71f9bb71f1a13e89c 100644 --- a/src/react-components/hub-create-panel.js +++ b/src/react-components/hub-create-panel.js @@ -235,6 +235,7 @@ class HubCreatePanel extends Component { </form> {this.state.showCustomSceneDialog && ( <CreateRoomDialog + includeScenePrompt={true} onClose={() => this.setState({ showCustomSceneDialog: false })} onCustomScene={(name, url) => { this.setState({ showCustomSceneDialog: false, name: name, customSceneUrl: url }, () => this.createHub()); diff --git a/src/react-components/scene-ui.js b/src/react-components/scene-ui.js index 2db97a04c5a0d56824f8b4677474b6ddb97540d9..168c6d517c68ee4f3520c2ea7fb770c45417eb1d 100644 --- a/src/react-components/scene-ui.js +++ b/src/react-components/scene-ui.js @@ -8,6 +8,9 @@ import hubLogo from "../assets/images/hub-preview-white.png"; import spokeLogo from "../assets/images/spoke_logo_black.png"; import { getReticulumFetchUrl } from "../utils/phoenix-utils"; import { generateHubName } from "../utils/name-generation"; +import CreateRoomDialog from "./create-room-dialog.js"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faEllipsisH } from "@fortawesome/free-solid-svg-icons/faEllipsisH"; import { lang, messages } from "../utils/i18n"; @@ -20,12 +23,13 @@ class SceneUI extends Component { sceneId: PropTypes.string, sceneName: PropTypes.string, sceneDescription: PropTypes.string, - sceneAttribution: PropTypes.string, + sceneAttributions: PropTypes.object, sceneScreenshotURL: PropTypes.string }; state = { - showScreenshot: false + showScreenshot: false, + showCustomRoomDialog: false }; constructor(props) { @@ -48,7 +52,7 @@ class SceneUI extends Component { } createRoom = async () => { - const payload = { hub: { name: generateHubName(), scene_id: this.props.sceneId } }; + const payload = { hub: { name: this.state.customRoomName || generateHubName(), scene_id: this.props.sceneId } }; const createUrl = getReticulumFetchUrl("/api/v1/hubs"); const res = await fetch(createUrl, { @@ -73,6 +77,47 @@ class SceneUI extends Component { tweetText )}`; + let attributions; + + const toAttributionSpan = a => { + if (a.url) { + const source = a.url.indexOf("sketchfab.com") + ? "on Sketchfab" + : a.url.indexOf("poly.google.com") + ? "on Google Poly" + : ""; + + return ( + <span key={a.url}> + <a href={a.url} target="_blank" rel="noopener noreferrer"> + {a.name} by {a.author} {source} + </a> + </span> + ); + } else { + return ( + <span key={`${a.name} ${a.author}`}> + {a.name} by {a.author} + </span> + ); + } + }; + + if (this.props.sceneAttributions) { + if (!this.props.sceneAttributions.extras) { + attributions = ( + <span> + <span>by {this.props.sceneAttributions.creator}</span> + <br /> + {this.props.sceneAttributions.content && this.props.sceneAttributions.content.map(toAttributionSpan)} + </span> + ); + } else { + // Legacy + attributions = <span>{this.props.sceneAttributions.extras}</span>; + } + } + return ( <IntlProvider locale={lang} messages={messages}> <div className={styles.ui}> @@ -93,9 +138,14 @@ class SceneUI extends Component { <div className={styles.logoTagline}> <FormattedMessage id="scene.logo_tagline" /> </div> - <button onClick={this.createRoom}> - <FormattedMessage id="scene.create_button" /> - </button> + <div className={styles.createButtons}> + <button className={styles.createButton} onClick={this.createRoom}> + <FormattedMessage id="scene.create_button" /> + </button> + <button className={styles.optionsButton} onClick={() => this.setState({ showCustomRoomDialog: true })}> + <FontAwesomeIcon icon={faEllipsisH} /> + </button> + </div> <a href={tweetLink} rel="noopener noreferrer" target="_blank" className={styles.tweetButton}> <img src="../assets/images/twitter.svg" /> <div> @@ -106,7 +156,7 @@ class SceneUI extends Component { </div> <div className={styles.info}> <div className={styles.name}>{this.props.sceneName}</div> - <div className={styles.attribution}>{this.props.sceneAttribution}</div> + <div className={styles.attribution}>{attributions}</div> </div> <div className={styles.spoke}> <div className={styles.madeWith}>made with</div> @@ -114,6 +164,15 @@ class SceneUI extends Component { <img src={spokeLogo} /> </a> </div> + {this.state.showCustomRoomDialog && ( + <CreateRoomDialog + includeScenePrompt={false} + onClose={() => this.setState({ showCustomRoomDialog: false })} + onCustomScene={name => { + this.setState({ showCustomRoomDialog: false, customRoomName: name }, () => this.createRoom()); + }} + /> + )} </div> </IntlProvider> ); diff --git a/src/scene.js b/src/scene.js index 6f2f0732d0da231b0bebef56b8ce526326aa5ea4..4172e624771d7fbd3ca5ce217052023ed113c930 100644 --- a/src/scene.js +++ b/src/scene.js @@ -102,7 +102,7 @@ const onReady = async () => { remountUI({ sceneName: sceneInfo.name, sceneDescription: sceneInfo.description, - sceneAttribution: sceneInfo.attribution, + sceneAttributions: sceneInfo.attributions, sceneScreenshotURL: sceneInfo.screenshot_url }); };