This commit is contained in:
Greg Fodor 2018-10-17 21:56:58 +00:00
Родитель b4bcb53717 32827d1584
Коммит a8daf72227
30 изменённых файлов: 837 добавлений и 122 удалений

Просмотреть файл

@ -1,10 +1,10 @@
# Privacy Notice for Hubs by Mozilla
# Privacy Notice for Hubs and Spoke
Version 2.0, Effective July 23, 2018
Version 3.0, October 16, 2018
## At Mozilla (thats us), we believe that privacy is fundamental to a healthy internet.
In this Privacy Notice, we explain what data may be accessible to Mozilla or others when you use [Hubs by Mozilla](https://hubs.mozilla.com). We also adhere to the practices outlined in the Mozilla [privacy policy](https://www.mozilla.org/en-US/privacy/) for how we receive, handle, and share information we collect from Hubs.
In this Privacy Notice, we explain what data may be accessible to Mozilla or others when you use [Hubs](https://hubs.mozilla.com) or [Spoke](https://hubs.mozilla.com/spoke). We also adhere to the practices outlined in the Mozilla [privacy policy](https://www.mozilla.org/en-US/privacy/) for how we receive, handle, and share information we collect from Hubs.
## Things you should know:
@ -16,17 +16,26 @@ In this Privacy Notice, we explain what data may be accessible to Mozilla or oth
- **Avatar data**: We receive and send to others in the Room the name of your Avatar, its position in the Room, and your interactions with objects in the Room. Mozilla does not record or store this data. You can optionally store information about your Avatar in your browsers local storage.
- **Room data**: Rooms are publicly accessible to anyone with the URL. Mozilla receives data about the virtual objects and Avatars in a Room and shares that data with others in the Room.
- **Voice data**: If your microphone is on, Mozilla receives and sends audio to other users in the Room. Mozilla does not record or store the audio. *Be aware that once you agree to let Hubs use your microphone, it will stay on as long as you remain in a Hubs room, unless you turn it off.*
- You can learn more by looking at the code itself. [Janus SFU](https://github.com/mozilla/janus-plugin-sfu), [Reticulum](https://github.com/mozilla/reticulum), [Hubs](https://github.com/mozilla/hubs), [Hubs-Ops](https://github.com/mozilla/hubs-ops)
- You can learn more by looking at the [code itself](https://github.com/mozilla/hubs) for Hubs. [Janus SFU](https://github.com/mozilla/janus-plugin-sfu), [Reticulum](https://github.com/mozilla/reticulum), [Hubs](https://github.com/mozilla/hubs), [Hubs-Ops](https://github.com/mozilla/hubs-ops)
</details>
<details open>
<summary>
<strong>Mozilla receives data you share to display to the room.</strong>
<strong>Mozilla receives data you create and share with Spoke and Hubs.</strong>
</summary>
- **Images and Video**: Mozilla receives video and image file links to process and display them in the Room. Mozilla stores this data as long as you remain in the Room.
- **Scenes**: Mozilla receives 3D Room model links and the name of the Room in order to process and display the Room. Mozilla stores the name and the URL for the link you share so you and others with the link to the Room can use it again.
- You can learn more by looking at the code itself. [Janus SFU](https://github.com/mozilla/janus-plugin-sfu), [Reticulum](https://github.com/mozilla/reticulum), [Hubs](https://github.com/mozilla/hubs), [Hubs-Ops](https://github.com/mozilla/hubs-ops)
- **Images and Video**: Mozilla receives video and image file links to process and display them in the Hubs Room. Mozilla stores this data as long as you remain in the Room.
- **Scenes You Share**: Mozilla receives 3D Room model links and the name of the Room in order to process and display the Room. Mozilla stores the name and the URL for the link you share so you and others with the link to the Room can use it again.
<details open>
<summary>
<strong>Mozilla receives data when you create and publish Scenes with Spoke.</strong>
</summary>
- **Scenes You Create**: When you create a scene with Spoke, Mozilla receives a copy of that scene. Mozilla stores that data in order to be able to process and display the scene through Hubs.
- **Publishing Your Scene**: When you publish a scene to Hubs using Spoke, Mozilla will ask for your email address to send you a link to verify your scene. Mozilla will receive and store your email address to allow you to log in and view your 3D Room models. Mozilla stores a hashed version of email addresses you use to publish a scene, so the stored versions are not available in readable form.
- **Remixing and Promotion**: When you use Spoke to publish a scene to Hubs, you have the option to “Allow Remixing with Creative Commons [CC-BY 3.0](https://creativecommons.org/licenses/by/3.0/)” or “allow Mozilla to promote my scene.” If you choose one or both of these options, Mozilla will share your scene publicly and you will have the option of including attribution information, which will also be publicly available.
- You can learn more by looking at the [code itself](https://github.com/mozillareality/spoke) for Spoke.
</details>
<details open>
@ -37,5 +46,5 @@ In this Privacy Notice, we explain what data may be accessible to Mozilla or oth
- **Technical data**: We receive and store data about Room URLs and names; the type of device you use to interact with Hubs, as well as its operating system, language, the name and version of browser; and other data to load and operate the Room.
- **Interaction data**: We receive data about your interactions with the Hubs service itself such as the number of Rooms created, the maximum number of users in a particular room at one same time, the start and end time of a users interaction with Hubs, the amount of time a user interacts with Hubs through Virtual Reality, the first time in a particular month or day that a user begins to use Hubs. Mozilla uses third party services to store and analyze these operational messages.
- **Error Data**: In order to diagnose problems, Hubs sends Mozilla logs of error messages (which include the Room URL, response time for requests, the page you were on when you encountered the error, your operating system, browser information, and may include your IP address).
- You can learn more by looking at the code itself. [Janus SFU](https://github.com/mozilla/janus-plugin-sfu), [Reticulum](https://github.com/mozilla/reticulum), [Hubs](https://github.com/mozilla/hubs), [Hubs-Ops](https://github.com/mozilla/hubs-ops)
- You can learn more by looking at the [code itself](https://github.com/mozilla/hubs) for Hubs. [Janus SFU](https://github.com/mozilla/janus-plugin-sfu), [Reticulum](https://github.com/mozilla/reticulum), [Hubs](https://github.com/mozilla/hubs), [Hubs-Ops](https://github.com/mozilla/hubs-ops)
</details>

Просмотреть файл

@ -1,22 +1,34 @@
# Terms of Service for Hubs by Mozilla
# Terms of Service for Hubs and Spoke
Version 2.0, Effective July 23, 2018
Version 3.0, Effective October 16, 2018
[Hubs by Mozilla](https://hubs.mozilla.com) is a real-time communications platform for Virtual Reality, Augmented Reality, Desktop, Laptop, Mobile, or however else you browse the internet. These Terms of Service explain your rights and responsibilities when you use Hubs.
[Hubs by Mozilla](https://hubs.mozilla.com) is a real-time communications platform for Virtual Reality, Augmented Reality, Desktop, Laptop, Mobile, or however else you browse the internet.
Spoke is a tool to arrange 3D models into scenes for use in Hubs.
These Terms of Service explain your rights and responsibilities when you use Hubs.
### 1. Privacy Policy
The Hubs [Privacy Notice](https://github.com/mozilla/hubs/blob/master/PRIVACY.md) explains what information we collect when you use Hubs by Mozilla and how that information is handled and shared.
### 2. Communications and Content
Hubs allows users to send information (such as audio, video, images, and 3D models) to other users. By using Hubs, you agree to give Mozilla all rights necessary to operate Hubs by Mozilla. This includes, but is not limited to, a license and permission to transmit and display the information you send through Hubs and to gather and share information as described in the [Privacy Notice](https://github.com/mozilla/hubs/blob/master/PRIVACY.md) for Hubs by Mozilla.
When you submit information to Hubs, you grant us a worldwide, royalty-free, perpetual, irrevocable, non-exclusive, transferable, and sublicensable license to use, copy, modify, adapt, prepare derivative works from, distribute, perform, and display that information, audio, video, images, or 3D models. You also agree that we may remove metadata associated with the information or data you submit, and you irrevocably waive any claims and assertions of moral rights or attribution with respect to the data you submit.
Hubs allows users to send information (such as audio, video, images, 3D models, and scenes) to other users.
You also represent and warrant that you have the authority to grant Mozilla all rights and permissions necessary for the operation of Hubs by Mozilla. To learn more about how Hubs operates, you can see the source code [here](https://github.com/mozilla/hubs).
Spoke allows users to arrange 3D Room models into scenes that can appear in Hubs.
Any ideas, suggestions, and feedback about Hubs that you provide to us are entirely voluntary, and you agree that Mozilla may use such ideas, suggestions, and feedback without compensation or obligation to you.
By using Hubs or Spoke, you agree to give Mozilla all rights necessary to operate Hubs and Spoke. This includes, but is not limited to, a license and permission to process, transmit, and display the information you send through Hubs or Spoke. It also includes permission to gather and share information as described in the [Privacy Notice](https://github.com/mozilla/hubs/blob/master/PRIVACY.md) for Hubs and Spoke.
You are solely responsible for the information you send using Hubs and the consequences of sending that information.
When you submit information to Hubs or Spoke, you grant us a worldwide, royalty-free, perpetual, irrevocable, non-exclusive, transferable, and sublicensable license to use, copy, modify, adapt, prepare derivative works from, distribute, perform, and display that information, audio, video, images, or 3D models. You also agree that we may remove metadata associated with the information or data you submit, and you irrevocably waive any claims and assertions of moral rights or attribution with respect to the data you submit. If you allow allow remixing of a scene you create using Spoke, you agree to license your scene under a [CC-BY 3.0](https://creativecommons.org/licenses/by/3.0/legalcode) license.
You also represent and warrant that you have the authority to grant Mozilla all rights and permissions necessary for the operation of Hubs and Spoke.
To learn more about how Hubs operates, you can see the source code [here](https://github.com/mozilla/hubs).
To learn more about how Spoke operates, you can see the source code [here](https://github.com/mozillareality/spoke).
Any ideas, suggestions, and feedback about Hubs or Spoke that you provide to us are entirely voluntary, and you agree that Mozilla may use such ideas, suggestions, and feedback without compensation or obligation to you.
You are solely responsible for the information you send, create, or edit using Hubs or Spoke, and the consequences of sending, creating, or editing that information.
### 3. Conditions of Use
By using Mozilla Hubs, you agree that your use will comply with Mozillas [Conditions of Use](https://www.mozilla.org/en-US/about/legal/acceptable-use/). Mozilla reserves the right to remove any content, suspend any users, and shut down any room it reasonably believes has violated these conditions.
@ -24,21 +36,21 @@ By using Mozilla Hubs, you agree that your use will comply with Mozillas [Con
Please also be aware of [Mozillas Community Participation Guidelines](https://www.mozilla.org/en-US/about/governance/policies/participation/), which address participation in Mozilla communities.
### 4. Mozilla's Rights
Mozilla does not grant you any intellectual property rights in Hubs unless these Terms specifically say otherwise. For example, these Terms do not provide the right to use any of Mozillas copyrights, trade names, trademarks, service marks, logos, domain names, or other distinctive brand features.
Mozilla does not grant you any intellectual property rights in Hubs or Spoke unless these Terms specifically say otherwise. For example, these Terms do not provide the right to use any of Mozillas copyrights, trade names, trademarks, service marks, logos, domain names, or other distinctive brand features.
Mozilla distributes the Hubs software under an open source license. To learn more, you can read the [license itself](https://github.com/mozilla/hubs/blob/master/LICENSE) or read the [FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/).
Mozilla distributes the Hubs and Spoke software under an open source license. To learn more, you can read the [license for Spoke]((https://github.com/mozillareality/spoke/blob/master/LICENSE)), and you can read the [license for Hubs](https://github.com/mozilla/hubs/blob/master/LICENSE) or read the [FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/).
### 5. Services Interruption; Term; Termination
We are continuing to develop Hubs. As a result, we plan to upgrade and change Hubs over time. To do this, we might have to temporarily suspend Hubs and it is not always possible for us to give notice. You will not be entitled to claim expenses or damages for such suspension or limitation of the use of Hubs.
We are continuing to develop Hubs and Spoke. As a result, we plan to upgrade and change them over time. To do this, we might have to temporarily suspend their service and it is not always possible for us to give notice. You will not be entitled to claim expenses or damages for such suspension or limitation of the use of Hubs or Spoke.
These Terms apply to your use of Hubs and will continue to apply until ended by either you or upon notice from Mozilla. You can choose to end them at any time for any reason by discontinuing your use of Hubs.
These Terms apply to your use of Hubs and Spoke and will continue to apply until ended by either you or upon notice from Mozilla. You can choose to end them at any time for any reason by discontinuing your use of Hubs and Spoke.
We may cut off your access to Hubs, either temporarily or permanently at any time for any reason. This includes, but is not limited to, situations where we reasonably believe: (i) you have violated these Terms (ii) you create risk or possible legal exposure for Mozilla; or (iii) providing and operating Hubs is no longer commercially viable. If possible, we will make reasonable efforts to notify you through Hubs.
We may cut off your access to Hubs or Spoke, either temporarily or permanently at any time for any reason. This includes, but is not limited to, situations where we reasonably believe: (i) you have violated these Terms (ii) you create risk or possible legal exposure for Mozilla; or (iii) providing and operating Hubs is no longer commercially viable. If possible, we will make reasonable efforts to notify you through the relevant program, either Hubs or Spoke .
In all such cases, these Terms shall terminate, including, without limitation, your license to use Hubs, except that the sections with the following titles shall continue to apply: Indemnification, Disclaimer; Limitation of Liability and Miscellaneous.
In all such cases, these Terms shall terminate, including, without limitation, your license to use Hubs and Spoke, except that the sections with the following titles shall continue to apply: Indemnification, Disclaimer; Limitation of Liability and Miscellaneous.
### 6. Indemnification
You agree to defend, indemnify and hold harmless Mozilla, and its respective parent and affiliate companies, contractors, contributors, licensors, partners, directors, officers, employees and agents ("Indemnified Parties") from and against any and all third party claims and expenses, including attorneys' fees, arising out of or related to your use of Hubs. This includes, but is not limited to, claims and expenses from any content you transmit using Hubs.
You agree to defend, indemnify and hold harmless Mozilla, and its respective parent and affiliate companies, contractors, contributors, licensors, partners, directors, officers, employees and agents ("Indemnified Parties") from and against any and all third party claims and expenses, including attorneys' fees, arising out of or related to your use of Hubs or Spoke. This includes, but is not limited to, claims and expenses from any content you transmit, edit, or create using Hubs or Spoke.
### 7. Disclaimer; Limitation of Liability
THE SERVICES ARE PROVIDED "AS IS" WITH ALL FAULTS. TO THE EXTENT PERMITTED BY LAW, THE INDEMNIFIED PARTIES, HEREBY DISCLAIM ALL WARRANTIES, WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES THAT THE SERVICES ARE FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE, AND NON-INFRINGING.
@ -49,17 +61,17 @@ THIS LIMITATION WILL APPLY NOTWITHSTANDING THE FAILURE OF ESSENTIAL PURPOSE OF A
EXCEPT AS REQUIRED BY LAW, THE INDEMNIFIED PARTIES, WILL NOT BE LIABLE FOR ANY INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL, OR EXEMPLARY DAMAGES ARISING OUT OF OR IN ANY WAY RELATING TO THESE TERMS OR THE USE OF OR INABILITY TO USE THE SERVICES, INCLUDING WITHOUT LIMITATION DIRECT AND INDIRECT DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, LOST PROFITS, LOSS OF DATA, AND COMPUTER FAILURE OR MALFUNCTION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES AND REGARDLESS OF THE THEORY (CONTRACT, TORT, OR OTHERWISE) UPON WHICH SUCH CLAIM IS BASED. THE COLLECTIVE LIABILITY OF THE INDEMNIFIED PARTIES, UNDER THIS AGREEMENT WILL NOT EXCEED $500 (FIVE HUNDRED DOLLARS). SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL, CONSEQUENTIAL, OR SPECIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
### 8. Modifications to These Terms
Mozilla may update these Terms from time to time. We will post the updated Terms online. If the changes are substantive, we will announce the update through Mozilla's usual channels for such announcements such as blog posts, forums, or in the particular service itself, in this case: Hubs by Mozilla.
Mozilla may update these Terms from time to time. We will post the updated Terms online. If the changes are substantive, we may announce the update through Mozilla's usual channels for such announcements such as blog posts, forums, or in the particular service itself, in this case: Hubs and Spoke.
Your continued use of Hubs after we post the new Terms constitutes your acceptance of the new Terms. To make your review more convenient, we will post an effective date at the top of this page.
Your continued use of Hubs or Spoke after we post the new Terms constitutes your acceptance of the new Terms. To make your review more convenient, we will post an effective date at the top of this page.
### 9. Miscellaneous
These Terms make up the entire agreement between you and Mozilla concerning Hubs. The laws of the state of California, U.S.A (excluding its conflict of law provisions) govern this agreement.
These Terms make up the entire agreement between you and Mozilla concerning Hubs and Spoke. The laws of the state of California, U.S.A (excluding its conflict of law provisions) govern this agreement.
If any portion of these Terms is held to be invalid or unenforceable, the remaining portions remain in full force and effect. If there is a conflict or ambiguity between a translated version of these terms and the English language version, the English language version applies.
### 10. Contact Us
For support, to provide feedback, or to report abuse of Hubs or violations of the Conditions of Use, you can email us at [hubs@mozilla.com](mailto:hubs@mozilla.com).
For support, to provide feedback, or to report abuse of Hubs or Spoke or violations of the Conditions of Use, you can email us at [hubs@mozilla.com](mailto:hubs@mozilla.com).
To report a claim of copyright or trademark infringement, see [our policy](https://www.mozilla.org/en-US/about/legal/report-infringement/).

65
package-lock.json сгенерированный
Просмотреть файл

@ -4094,6 +4094,11 @@
"minimalistic-crypto-utils": "^1.0.0"
}
},
"emoji-regex": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz",
"integrity": "sha512-PAHp6TxrCy7MGMFidro8uikr+zlJJKJ/Q6mm2ExZ7HwkyR9lSVFfE3kt36qcwa24BQL7y0G9axycGjK1A/0uNQ=="
},
"emojis-list": {
"version": "2.1.0",
"resolved": "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz",
@ -7939,6 +7944,14 @@
"immediate": "~3.0.5"
}
},
"linkify-it": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz",
"integrity": "sha1-2UpGSPmxwXnWT6lykSaL22zpQ08=",
"requires": {
"uc.micro": "^1.0.1"
}
},
"listr": {
"version": "0.14.1",
"resolved": "https://registry.yarnpkg.com/listr/-/listr-0.14.1.tgz",
@ -8222,6 +8235,16 @@
"resolved": "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
},
"lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
},
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
},
"lodash.mergewith": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
@ -10545,6 +10568,18 @@
"prop-types": "^15.6.0"
}
},
"react-emoji-render": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/react-emoji-render/-/react-emoji-render-0.4.6.tgz",
"integrity": "sha512-ARB8E4j/dndQxC7Bn4b+Oymt7pqhh9GjP87NYcxC8KONejysnXD5O9KpnJeW/U3Ke3+XsWrWAr9K5riVA6emfg==",
"requires": {
"classnames": "^2.2.5",
"emoji-regex": "^6.4.1",
"lodash.flatten": "^4.4.0",
"prop-types": "^15.5.8",
"string-replace-to-array": "^1.0.1"
}
},
"react-file-reader-input": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/react-file-reader-input/-/react-file-reader-input-1.1.4.tgz",
@ -10572,6 +10607,16 @@
"invariant": "^2.1.1"
}
},
"react-linkify": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/react-linkify/-/react-linkify-0.2.2.tgz",
"integrity": "sha512-0S8cvUNtEgfJpIGDPKklyrnrTffJ63WuJAc4KaYLBihl5TjgH5cHUmYD+AXLpsV+CVmfoo/56SUNfrZcY4zYMQ==",
"requires": {
"linkify-it": "^2.0.3",
"prop-types": "^15.5.8",
"tlds": "^1.57.0"
}
},
"react-select": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-1.3.0.tgz",
@ -12038,6 +12083,16 @@
"resolved": "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
},
"string-replace-to-array": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string-replace-to-array/-/string-replace-to-array-1.0.3.tgz",
"integrity": "sha1-yT66mZpe4k1zGuu69auja18Y978=",
"requires": {
"invariant": "^2.2.1",
"lodash.flatten": "^4.2.0",
"lodash.isstring": "^4.0.1"
}
},
"string-template": {
"version": "0.2.1",
"resolved": "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz",
@ -12700,6 +12755,11 @@
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz",
"integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow=="
},
"tlds": {
"version": "1.203.1",
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.203.1.tgz",
"integrity": "sha512-7MUlYyGJ6rSitEZ3r1Q1QNV8uSIzapS8SmmhSusBuIc7uIxPPwsKllEP0GRp1NS6Ik6F+fRZvnjDWm3ecv2hDw=="
},
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz",
@ -12901,6 +12961,11 @@
"resolved": "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz",
"integrity": "sha1-p7/ZL1bt+xFwg7aeMdKqiILUse0="
},
"uc.micro": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.5.tgz",
"integrity": "sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg=="
},
"uglify-es": {
"version": "3.3.9",
"resolved": "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz",

Просмотреть файл

@ -52,8 +52,10 @@
"raven-js": "^3.20.1",
"react": "^16.1.1",
"react-dom": "^16.1.1",
"react-emoji-render": "^0.4.6",
"react-intl": "^2.4.0",
"react-youtube": "^7.8.0",
"react-linkify": "^0.2.2",
"screenfull": "^3.3.2",
"super-hands": "github:mozillareality/aframe-super-hands-component#feature/drawing",
"three": "github:mozillareality/three.js#8b1886c384371c3e6305b757d1db7577c5201a9b",

Двоичные данные
src/assets/images/presence_desktop.png Executable file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 252 B

Двоичные данные
src/assets/images/presence_phone.png Executable file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 237 B

Двоичные данные
src/assets/images/presence_vr.png Executable file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 558 B

3
src/assets/images/twitter.svg Executable file
Просмотреть файл

@ -0,0 +1,3 @@
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M153.6 301.6C247.9 301.6 299.5 223.4 299.5 155.7C299.5 153.5 299.5 151.3 299.4 149.1C309.4 141.9 318.1 132.8 325 122.5C315.8 126.6 305.9 129.3 295.5 130.6C306.1 124.3 314.2 114.2 318.1 102.2C308.2 108.1 297.2 112.3 285.5 114.6C276.1 104.6 262.8 98.4 248.1 98.4C219.8 98.4 196.8 121.4 196.8 149.7C196.8 153.7 197.3 157.6 198.1 161.4C155.5 159.3 117.7 138.8 92.4 107.8C88 115.4 85.5 124.2 85.5 133.6C85.5 151.4 94.6 167.1 108.3 176.3C99.9 176 92 173.7 85.1 169.9C85.1 170.1 85.1 170.3 85.1 170.6C85.1 195.4 102.8 216.2 126.2 220.9C121.9 222.1 117.4 222.7 112.7 222.7C109.4 222.7 106.2 222.4 103.1 221.8C109.6 242.2 128.6 257 151 257.4C133.4 271.2 111.3 279.4 87.3 279.4C83.2 279.4 79.1 279.2 75.1 278.7C97.7 293.1 124.7 301.6 153.6 301.6Z" fill="white"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 869 B

Просмотреть файл

@ -14,7 +14,7 @@
&:local(.column) {
flex-direction: column;
bottom: 20px;
bottom: 0;
z-index: 1;
}
}

Просмотреть файл

@ -84,6 +84,7 @@
margin: 24px;
min-height: 150px;
height: 100%;
width: 100%;
:local(.title) {
@extend %top-title;
@ -93,14 +94,30 @@
margin-left: 8px;
}
:local(.name) {
@extend %top-title;
@extend %glass-text;
margin-bottom: 4px;
margin-right: 8px;
margin-left: 8px;
}
:local(.lobby) {
margin-bottom: 24px;
margin-right: 8px;
margin-left: 8px;
font-size: 0.9em;
}
:local(.center) {
@extend %glass-text;
flex: 10;
width: 100%;
}
:local(.profile-name) {
margin-top: 4px;
margin-bottom: 16px;
margin-bottom: 32px;
@extend %default-font;
font-size: 1.1em;
color: $action-color;

Просмотреть файл

@ -22,8 +22,11 @@
}
:local(.link-button) {
@extend %action-button;
@extend %action-button-selected;
min-width: auto;
margin-top: 4px;
flex: 1;
@media (max-height: 370px) {
display: none;
@ -115,8 +118,9 @@
:local(.buttons) {
display: flex;
justify-content: space-between;
width: 100%;
button {
button, a {
margin: 0 12px;
}
}

Просмотреть файл

@ -0,0 +1,82 @@
@import 'shared.scss';
:local(.attach-point) {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 5px solid $white-transparent;
position: absolute;
top: -5px;
left: 44px;
@media(max-width: 768px), (max-height: 420px) {
left: 34px;
}
}
:local(.presence-list) {
position: absolute;
top: 72px;
left: 16px;
bottom: 0;
z-index: 5;
}
:local(.contents) {
background-color: white;
border-radius: 12px;
padding: 12px 18px;
min-width: 308px;
max-height: 75%;
overflow-y: auto;
pointer-events: auto;
}
:local(.rows) {
display: flex;
flex-direction: column;
align-items: center;
}
:local(.row) {
width: 100%;
display: flex;
flex-direction: row;
font-weight: bold;
justify-content: space-between;
align-items: center;
margin: 6px 0;
}
:local(.device) {
width: 32px;
height: 32px;
position: relative;
margin: 0px 12px 0px 0px;
img {
position: absolute;
left: 2px;
width: 32px;
height: 32px;
}
}
:local(.display-name) {
flex: 10;
white-space: nowrap;
margin-right: 24px;
max-width: 45vw;
overflow: hidden;
}
:local(.self-display-name) {
text-decoration: underline;
}
:local(.presence) {
flex: 1;
white-space: nowrap;
text-align: right;
}

Просмотреть файл

@ -0,0 +1,73 @@
@import 'shared.scss';
:local(.presence-log) {
align-self: flex-start;
flex: 10;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-start;
margin-bottom: 8px;
margin-top: 90px;
overflow: hidden;
width: 100%;
:local(.presence-log-entry) {
@extend %default-font;
pointer-events: auto;
user-select: text;
-moz-user-select: text;
-webkit-user-select: text;
-ms-user-select: text;
background-color: $white-transparent;
margin: 8px 64px 8px 16px;
font-size: 0.8em;
padding: 8px 16px;
border-radius: 16px;
a {
color: $action-color;
}
@media (max-width: 1000px) {
max-width: 75%;
}
}
:local(.expired) {
visibility: hidden;
opacity: 0;
transform: translateY(-8px);
transition: visibility 0s 0.5s, opacity 0.5s linear, transform 0.5s;
}
}
:local(.presence-log-in-room) {
max-height: 200px;
@media(min-height: 800px) and (min-width: 600px) {
max-height: 400px;
}
position: absolute;
bottom: 165px;
:local(.presence-log-entry) {
background-color: $hud-panel-background;
color: $light-text;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
}
:local(.emoji) {
// Undo annoying CSS in emoji plugin
margin: auto !important;
vertical-align: 0em !important;
}

Просмотреть файл

@ -52,6 +52,22 @@
margin-bottom: 32px;
}
:local(.tweetButton) {
@extend %action-button;
margin-top: 12px;
background-color: #1b95e0;
align-self: center;
padding-right: 32px;
display: flex;
flex-direction: row;
img {
width: 42px;
height: 42px;
margin-right: 6px;
}
}
:local(.logo) {
width: 100%;
display: block;

Просмотреть файл

@ -50,6 +50,7 @@
width: 100%;
max-width: 600px;
z-index: 2;
position: relative;
:local(.backgrounded) {
filter: blur(1px);
@ -164,6 +165,8 @@
border-radius: 24px;
font-weight: bold;
padding: 8px 18px;
pointer-events: auto;
cursor: pointer;
@media (min-width: 769px) and (min-height: 421px) {
flex: 1;
@ -177,3 +180,90 @@
margin: 0 12px;
}
}
:local(.presence-info-selected) {
color: $action-color;
}
:local(.message-entry) {
position: relative;
margin: 8px 24px 24px 24px;
height: 48px;
display: flex;
justify-content: center;
align-items: center;
background-color: white;
border: 1px solid #e2e2e2;
border-radius: 16px;
}
:local(.message-entry-input) {
@extend %default-font;
pointer-events: auto;
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
outline-style: none;
background-color: transparent;
color: black;
padding: 8px 1.25em;
line-height: 2em;
font-size: 1.1em;
width: 100%;
border: 0px;
height: 32px;
margin-right: 100px;
}
:local(.message-entry-input)::placeholder{
color: $dark-grey;
font-weight: 300;
font-style: italic;
}
:local(.message-entry-submit) {
@extend %action-button;
position: absolute;
right: 12px;
height: 32px;
min-width: 80px;
}
:local(.message-entry-in-room) {
@media(max-width: 900px) {
display:none;
}
position: absolute;
left: 16px;
bottom: 20px;
width: 33%;
height: 48px;
display: flex;
justify-content: center;
align-items: center;
background-color: $darker-grey;
border-radius: 16px;
pointer-events: auto;
opacity: 0.3;
transition: opacity 0.25s linear;
:local(.message-entry-input-in-room) {
color: white;
padding: 8px 1.25em;
}
:local(.message-entry-submit-in-room) {
border: 0;
visibility: hidden;
}
}
:local(.message-entry-in-room):hover {
opacity: 1.0;
transition: opacity 0.25s linear;
:local(.message-entry-submit-in-room) {
visibility: visible;
}
}

Просмотреть файл

@ -10,17 +10,17 @@
"entry.desktop-screen": "Screen",
"entry.mobile-screen": "Phone",
"entry.mobile-safari": "Safari",
"entry.generic-prefix": "Enter with ",
"entry.generic-medium": "PC VR",
"entry.generic-prefix": " ",
"entry.generic-medium": "Connected Headset",
"entry.generic-subtitle-desktop": "Oculus or SteamVR",
"entry.gearvr-prefix": "Enter on ",
"entry.gearvr-medium": "Gear VR",
"entry.choose-device": "Choose Device",
"entry.device-prefix-desktop": "Use a ",
"entry.device-prefix-mobile": "Use a ",
"entry.device-prefix-desktop": " ",
"entry.device-prefix-mobile": " ",
"entry.device-medium": "Mobile Headset",
"entry.device-subtitle-desktop": "Standalone or Phone Clip-in",
"entry.device-subtitle-mobile": "Standalone or Phone Clip-in",
"entry.device-subtitle-desktop": "Standalone or Mobile VR",
"entry.device-subtitle-mobile": "Standalone or Mobile VR",
"entry.device-subtitle-vr": "Phone or PC",
"entry.cardboard": "Enter on Google Cardboard",
"entry.daydream-prefix": "Enter on ",
@ -31,6 +31,7 @@
"entry.invite-team-nag": "Invite a hubs team member",
"entry.enable-screen-sharing": "Share my desktop",
"entry.return-to-vr": "Enter in VR",
"entry.lobby": "Lobby",
"profile.save": "Accept",
"profile.display_name.validation_warning": "Alphanumerics and hyphens. At least 3 characters, no more than 32",
"profile.header": "Name & Avatar",
@ -56,6 +57,12 @@
"autoexit.title_units": " seconds",
"autoexit.subtitle": "You have started another session.",
"autoexit.cancel": "CANCEL",
"presence.entered_room": "entered the room.",
"presence.join_lobby": "joined the lobby.",
"presence.leave": "left.",
"presence.name_change": "is now known as",
"presence.in_lobby": "Lobby",
"presence.in_room": "In Room",
"home.room_create_options": "options",
"home.room_create_button": "Create Room",
"home.create_name.validation_warning": "Invalid name, limited to 4 to 64 characters and limited symbols.",
@ -82,14 +89,16 @@
"help.report_issue": "Report an Issue",
"scene.logo_tagline": "A new way to get together",
"scene.create_button": "Create a room with this scene",
"scene.tweet_button": "Share on Twitter",
"link.in_your_browser": "In your headset's browser, go to:",
"link.enter_code": "Then, enter this one-time link code:",
"link.do_not_close": "Keep this open to use this code.",
"link.connect_headset": "Connect Mobile Headset",
"link.connect_headset": "Link VR Headset",
"link.cancel": "cancel",
"invite.enter_via": "Enter via ",
"invite.tweet": "tweet",
"invite.and_enter_code": " with code:",
"invite.or_visit": "or visit",
"invite.or_visit": "or share permalink",
"spoke.primary_tagline": "make your space",
"spoke.secondary_tagline": "Create 3D social scenes for ",
"spoke.thank_you": "Thank you for downloading Spoke!",

Просмотреть файл

@ -46,7 +46,7 @@ AFRAME.registerComponent("networked-audio-analyser", {
AFRAME.registerComponent("scale-audio-feedback", {
schema: {
minScale: { default: 1 },
maxScale: { default: 2 }
maxScale: { default: 1.5 }
},
tick() {

Просмотреть файл

@ -192,10 +192,10 @@ AFRAME.registerComponent("character-controller", {
resetPositionOnNavMesh: function(position, navPosition, object3D) {
const { pathfinder } = this.el.sceneEl.systems.nav;
if (!(this.navZone in pathfinder.zones)) return;
this.navGroup = pathfinder.getGroup(this.navZone, navPosition, true);
this.navGroup = pathfinder.getGroup(this.navZone, navPosition, true, true);
this.navNode = null;
this._setNavNode(navPosition);
object3D.position.copy(navPosition);
pathfinder.clampStep(position, navPosition, this.navNode, this.navZone, this.navGroup, object3D.position);
},
updateVelocity: function(dt) {

Просмотреть файл

@ -34,6 +34,7 @@ AFRAME.registerComponent("scene-preview-camera", {
tick: function() {
let t = (new Date().getTime() - this.startTime) / (1000.0 * this.data.duration);
t = Math.min(1.0, Math.max(0.0, t));
if (!this.ranOnePass) {
t = t * (2 - t);

Просмотреть файл

@ -29,6 +29,7 @@ import joystick_dpad4 from "./behaviours/joystick-dpad4";
import msft_mr_axis_with_deadzone from "./behaviours/msft-mr-axis-with-deadzone";
import { PressedMove } from "./activators/pressedmove";
import { ReverseY } from "./activators/reversey";
import { Presence } from "phoenix";
import "./activators/shortpress";
@ -253,7 +254,12 @@ async function handleHubChannelJoined(entryManager, hubChannel, data) {
environmentScene.setAttribute("gltf-bundle", `src: ${sceneUrl}`);
}
remountUI({ hubId: hub.hub_id, hubName: hub.name, hubEntryCode: hub.entry_code });
remountUI({
hubId: hub.hub_id,
hubName: hub.name,
hubEntryCode: hub.entry_code,
onSendMessage: hubChannel.sendMessage
});
document
.querySelector("#hud-hub-entry-link")
@ -299,7 +305,7 @@ async function runBotMode(scene, entryManager) {
entryManager.enterSceneWhenLoaded(new MediaStream(), false);
}
document.addEventListener("DOMContentLoaded", () => {
document.addEventListener("DOMContentLoaded", async () => {
const scene = document.querySelector("a-scene");
const hubChannel = new HubChannel(store);
const entryManager = new SceneEntryManager(hubChannel);
@ -314,22 +320,6 @@ document.addEventListener("DOMContentLoaded", () => {
pollForSupportAvailability(isSupportAvailable => remountUI({ isSupportAvailable }));
document.body.addEventListener("connected", () =>
remountUI({ occupantCount: NAF.connection.adapter.publisher.initialOccupants.length + 1 })
);
document.body.addEventListener("clientConnected", () =>
remountUI({
occupantCount: Object.keys(NAF.connection.adapter.occupants).length + 1
})
);
document.body.addEventListener("clientDisconnected", () =>
remountUI({
occupantCount: Object.keys(NAF.connection.adapter.occupants).length + 1
})
);
const platformUnsupportedReason = getPlatformUnsupportedReason();
if (platformUnsupportedReason) {
@ -349,13 +339,13 @@ document.addEventListener("DOMContentLoaded", () => {
}
}
getAvailableVREntryTypes().then(availableVREntryTypes => {
if (availableVREntryTypes.isInHMD) {
remountUI({ availableVREntryTypes, forcedVREntryType: "vr" });
} else {
remountUI({ availableVREntryTypes });
}
});
const availableVREntryTypes = await getAvailableVREntryTypes();
if (availableVREntryTypes.isInHMD) {
remountUI({ availableVREntryTypes, forcedVREntryType: "vr" });
} else {
remountUI({ availableVREntryTypes });
}
const environmentScene = document.querySelector("#environment-scene");
@ -379,9 +369,16 @@ document.addEventListener("DOMContentLoaded", () => {
console.log(`Hub ID: ${hubId}`);
const socket = connectToReticulum(isDebug);
remountUI({ sessionId: socket.params().session_id });
// Hub local channel
const hubPhxChannel = socket.channel(`hub:${hubId}`, {});
const context = {
mobile: isMobile,
hmd: availableVREntryTypes.isInHMD
};
const joinPayload = { profile: store.state.profile, context };
const hubPhxChannel = socket.channel(`hub:${hubId}`, joinPayload);
hubPhxChannel
.join()
@ -398,16 +395,101 @@ document.addEventListener("DOMContentLoaded", () => {
console.error(res);
});
const hubPhxPresence = new Presence(hubPhxChannel);
const presenceLogEntries = [];
const addToPresenceLog = entry => {
entry.key = Date.now().toString();
presenceLogEntries.push(entry);
remountUI({ presenceLogEntries });
// Fade out and then remove
setTimeout(() => {
entry.expired = true;
remountUI({ presenceLogEntries });
setTimeout(() => {
presenceLogEntries.splice(presenceLogEntries.indexOf(entry), 1);
remountUI({ presenceLogEntries });
}, 5000);
}, entryManager.hasEntered() ? 10000 : 30000); // Fade out things faster once entered.
};
let isInitialSync = true;
hubPhxPresence.onSync(() => {
remountUI({ presences: hubPhxPresence.state });
if (!isInitialSync) return;
// Wire up join/leave event handlers after initial sync.
isInitialSync = false;
hubPhxPresence.onJoin((sessionId, current, info) => {
const meta = info.metas[info.metas.length - 1];
if (current) {
// Change to existing presence
const isSelf = sessionId === socket.params().session_id;
const currentMeta = current.metas[0];
if (!isSelf && currentMeta.presence !== meta.presence && meta.profile.displayName) {
addToPresenceLog({
type: "entered",
presence: meta.presence,
name: meta.profile.displayName
});
}
if (currentMeta.profile && meta.profile && currentMeta.profile.displayName !== meta.profile.displayName) {
addToPresenceLog({
type: "display_name_changed",
oldName: currentMeta.profile.displayName,
newName: meta.profile.displayName
});
}
} else {
// New presence
const meta = info.metas[0];
if (meta.presence && meta.profile.displayName) {
addToPresenceLog({
type: "join",
presence: meta.presence,
name: meta.profile.displayName
});
}
}
});
hubPhxPresence.onLeave((sessionId, current, info) => {
if (current && current.metas.length > 0) return;
const meta = info.metas[0];
if (meta.profile.displayName) {
addToPresenceLog({
type: "leave",
name: meta.profile.displayName
});
}
});
});
hubPhxChannel.on("naf", data => {
if (!NAF.connection.adapter) return;
NAF.connection.adapter.onData(data);
});
// Reticulum global channel
const retPhxChannel = socket.channel(`ret`, { hub_id: hubId });
retPhxChannel.join().receive("error", res => {
console.error(res);
hubPhxChannel.on("message", data => {
const userInfo = hubPhxPresence.state[data.session_id];
if (!userInfo) return;
addToPresenceLog({ type: "message", name: userInfo.metas[0].profile.displayName, body: data.body });
});
// Reticulum global channel
const retPhxChannel = socket.channel(`ret`, { hub_id: hubId });
retPhxChannel.join().receive("error", res => console.error(res));
linkChannel.setSocket(socket);
});

Просмотреть файл

@ -6,6 +6,7 @@ import LinkRoot from "./react-components/link-root";
import LinkChannel from "./utils/link-channel";
import { connectToReticulum } from "./utils/phoenix-utils";
import Store from "./storage/store";
import { detectInHMD } from "./utils/vr-caps-detect.js";
registerTelemetry();
@ -17,4 +18,7 @@ const linkChannel = new LinkChannel(store);
linkChannel.setSocket(socket);
ReactDOM.render(<LinkRoot store={store} linkChannel={linkChannel} />, document.getElementById("link-root"));
ReactDOM.render(
<LinkRoot store={store} linkChannel={linkChannel} showHeadsetLinkOption={detectInHMD()} />,
document.getElementById("link-root")
);

23
src/react-components/invite-dialog.js поставляемый
Просмотреть файл

@ -15,6 +15,7 @@ function pad(num, size) {
export default class InviteDialog extends Component {
static propTypes = {
entryCode: PropTypes.number,
hubId: PropTypes.string,
allowShare: PropTypes.bool,
onClose: PropTypes.func
};
@ -28,7 +29,7 @@ export default class InviteDialog extends Component {
this.setState({ shareButtonActive: true });
setTimeout(() => this.setState({ shareButtonActive: false }), 5000);
navigator.share({ title: document.title, url: link });
navigator.share({ title: "Join me now in #hubs!", url: link });
};
copyClicked = link => {
@ -42,7 +43,13 @@ export default class InviteDialog extends Component {
const { entryCode } = this.props;
const entryCodeString = pad(entryCode, 6);
const shareLink = `hub.link/${entryCodeString}`;
const shortLinkText = `hub.link/${this.props.hubId}`;
const shortLink = "https://" + shortLinkText;
const tweetText = `Join me now in #hubs!`;
const tweetLink = `https://twitter.com/share?url=${encodeURIComponent(shortLink)}&text=${encodeURIComponent(
tweetText
)}`;
return (
<div className={styles.dialog}>
@ -68,18 +75,24 @@ export default class InviteDialog extends Component {
<FormattedMessage id="invite.or_visit" />
</div>
<div className={styles.domain}>
<input type="text" readOnly onFocus={e => e.target.select()} value={shareLink} />
<input type="text" readOnly onFocus={e => e.target.select()} value={shortLinkText} />
</div>
<div className={styles.buttons}>
<button className={styles.linkButton} onClick={this.copyClicked.bind(this, "https://" + shareLink)}>
<button className={styles.linkButton} onClick={this.copyClicked.bind(this, shortLink)}>
<span>{this.state.copyButtonActive ? "copied!" : "copy"}</span>
</button>
{this.props.allowShare &&
navigator.share && (
<button className={styles.linkButton} onClick={this.shareClicked.bind(this, "https://" + shareLink)}>
<button className={styles.linkButton} onClick={this.shareClicked.bind(this, shortLink)}>
<span>{this.state.shareButtonActive ? "sharing..." : "share"}</span>
</button>
)}
{this.props.allowShare &&
!navigator.share && (
<a href={tweetLink} className={styles.linkButton} target="_blank" rel="noopener noreferrer">
<FormattedMessage id="invite.tweet" />
</a>
)}
</div>
</div>
);

72
src/react-components/link-root.js поставляемый
Просмотреть файл

@ -20,7 +20,8 @@ class LinkRoot extends Component {
static propTypes = {
intl: PropTypes.object,
store: PropTypes.object,
linkChannel: PropTypes.object
linkChannel: PropTypes.object,
showHeadsetLinkOption: PropTypes.bool
};
state = {
@ -178,16 +179,18 @@ class LinkRoot extends Component {
</div>
<div className={styles.enteredFooter}>
{!this.state.isAlphaMode && (
<img onClick={() => this.toggleMode()} src={HeadsetIcon} className={styles.headsetIcon} />
)}
{!this.state.isAlphaMode && (
<span>
<a href="#" onClick={() => this.toggleMode()}>
<FormattedMessage id="link.linking_a_headset" />
</a>
</span>
)}
{!this.state.isAlphaMode &&
this.props.showHeadsetLinkOption && (
<img onClick={() => this.toggleMode()} src={HeadsetIcon} className={styles.headsetIcon} />
)}
{!this.state.isAlphaMode &&
this.props.showHeadsetLinkOption && (
<span>
<a href="#" onClick={() => this.toggleMode()}>
<FormattedMessage id="link.linking_a_headset" />
</a>
</span>
)}
</div>
</div>
@ -208,15 +211,19 @@ class LinkRoot extends Component {
{d}
</button>
))}
<button
className={classNames(styles.keypadButton, styles.keypadToggleMode)}
onTouchStart={() => this.toggleMode()}
onClick={() => {
if (!hasTouchEvents) this.toggleMode();
}}
>
{this.state.isAlphaMode ? "123" : "ABC"}
</button>
{this.props.showHeadsetLinkOption ? (
<button
className={classNames(styles.keypadButton, styles.keypadToggleMode)}
onTouchStart={() => this.toggleMode()}
onClick={() => {
if (!hasTouchEvents) this.toggleMode();
}}
>
{this.state.isAlphaMode ? "123" : "ABC"}
</button>
) : (
<div />
)}
{!this.state.isAlphaMode && (
<button
disabled={this.state.entered.length === this.maxAllowedChars()}
@ -242,17 +249,20 @@ class LinkRoot extends Component {
</div>
<div className={styles.footer}>
<div
className={styles.linkHeadsetFooterLink}
style={{ visibility: this.state.isAlphaMode ? "hidden" : "visible" }}
>
<img onClick={() => this.toggleMode()} src={HeadsetIcon} className={styles.headsetIcon} />
<span>
<a href="#" onClick={() => this.toggleMode()}>
<FormattedMessage id="link.linking_a_headset" />
</a>
</span>
</div>
{!this.state.isAlphaMode &&
this.props.showHeadsetLinkOption && (
<div
className={styles.linkHeadsetFooterLink}
style={{ visibility: this.state.isAlphaMode ? "hidden" : "visible" }}
>
<img onClick={() => this.toggleMode()} src={HeadsetIcon} className={styles.headsetIcon} />
<span>
<a href="#" onClick={() => this.toggleMode()}>
<FormattedMessage id="link.linking_a_headset" />
</a>
</span>
</div>
)}
</div>
</div>
</div>

61
src/react-components/presence-list.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,61 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import styles from "../assets/stylesheets/presence-list.scss";
import classNames from "classnames";
import PhoneImage from "../assets/images/presence_phone.png";
import DesktopImage from "../assets/images/presence_desktop.png";
import HMDImage from "../assets/images/presence_vr.png";
import { FormattedMessage } from "react-intl";
export default class PresenceList extends Component {
static propTypes = {
presences: PropTypes.object,
sessionId: PropTypes.string
};
domForPresence = ([sessionId, data]) => {
const meta = data.metas[0];
const context = meta.context;
const profile = meta.profile;
const image = context && context.mobile ? PhoneImage : context && context.hmd ? HMDImage : DesktopImage;
return (
<div className={styles.row} key={sessionId}>
<div className={styles.device}>
<img src={image} />
</div>
<div
className={classNames({
[styles.displayName]: true,
[styles.selfDisplayName]: sessionId === this.props.sessionId
})}
>
{profile && profile.displayName}
</div>
<div className={styles.presence}>
<FormattedMessage id={`presence.in_${meta.presence}`} />
</div>
</div>
);
};
render() {
// Draw self first
return (
<div className={styles.presenceList}>
<div className={styles.attachPoint} />
<div className={styles.contents}>
<div className={styles.rows}>
{Object.entries(this.props.presences || {})
.filter(([k]) => k === this.props.sessionId)
.map(this.domForPresence)}
{Object.entries(this.props.presences || {})
.filter(([k]) => k !== this.props.sessionId)
.map(this.domForPresence)}
</div>
</div>
</div>
);
}
}

63
src/react-components/presence-log.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,63 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import styles from "../assets/stylesheets/presence-log.scss";
import classNames from "classnames";
import Linkify from "react-linkify";
import { toArray as toEmojis } from "react-emoji-render";
import { FormattedMessage } from "react-intl";
export default class PresenceLog extends Component {
static propTypes = {
entries: PropTypes.array,
inRoom: PropTypes.bool
};
constructor(props) {
super(props);
}
domForEntry = e => {
const entryClasses = {
[styles.presenceLogEntry]: true,
[styles.expired]: !!e.expired
};
switch (e.type) {
case "join":
case "entered":
return (
<div key={e.key} className={classNames(entryClasses)}>
<b>{e.name}</b> <FormattedMessage id={`presence.${e.type}_${e.presence}`} />
</div>
);
case "leave":
return (
<div key={e.key} className={classNames(entryClasses)}>
<b>{e.name}</b> <FormattedMessage id={`presence.${e.type}`} />
</div>
);
case "display_name_changed":
return (
<div key={e.key} className={classNames(entryClasses)}>
<b>{e.oldName}</b> <FormattedMessage id="presence.name_change" /> <b>{e.newName}</b>.
</div>
);
case "message":
return (
<div key={e.key} className={classNames(entryClasses)}>
<b>{e.name}</b>:{" "}
<Linkify properties={{ target: "_blank", rel: "noopener referrer" }}>{toEmojis(e.body)}</Linkify>
</div>
);
}
};
render() {
const presenceClasses = {
[styles.presenceLog]: true,
[styles.presenceLogInRoom]: this.props.inRoom
};
return <div className={classNames(presenceClasses)}>{this.props.entries.map(this.domForEntry)}</div>;
}
}

12
src/react-components/scene-ui.js поставляемый
Просмотреть файл

@ -67,6 +67,12 @@ class SceneUI extends Component {
};
render() {
const sceneUrl = [location.protocol, "//", location.host, location.pathname].join("");
const tweetText = `${this.props.sceneName} in #hubs`;
const tweetLink = `https://twitter.com/share?url=${encodeURIComponent(sceneUrl)}&text=${encodeURIComponent(
tweetText
)}`;
return (
<IntlProvider locale={lang} messages={messages}>
<div className={styles.ui}>
@ -90,6 +96,12 @@ class SceneUI extends Component {
<button onClick={this.createRoom}>
<FormattedMessage id="scene.create_button" />
</button>
<a href={tweetLink} rel="noopener noreferrer" target="_blank" className={styles.tweetButton}>
<img src="../assets/images/twitter.svg" />
<div>
<FormattedMessage id="scene.tweet_button" />
</div>
</a>
</div>
</div>
<div className={styles.info}>

86
src/react-components/ui-root.js поставляемый
Просмотреть файл

@ -27,6 +27,8 @@ import InviteTeamDialog from "./invite-team-dialog.js";
import InviteDialog from "./invite-dialog.js";
import LinkDialog from "./link-dialog.js";
import CreateObjectDialog from "./create-object-dialog.js";
import PresenceLog from "./presence-log.js";
import PresenceList from "./presence-list.js";
import TwoDHUD from "./2d-hud";
import { faUsers } from "@fortawesome/free-solid-svg-icons/faUsers";
@ -67,6 +69,7 @@ class UIRoot extends Component {
static propTypes = {
enterScene: PropTypes.func,
exitScene: PropTypes.func,
onSendMessage: PropTypes.func,
concurrentLoadDetector: PropTypes.object,
disableAutoExitOnConcurrentLoad: PropTypes.bool,
forcedVREntryType: PropTypes.string,
@ -83,8 +86,10 @@ class UIRoot extends Component {
platformUnsupportedReason: PropTypes.string,
hubId: PropTypes.string,
hubName: PropTypes.string,
occupantCount: PropTypes.number,
isSupportAvailable: PropTypes.bool
isSupportAvailable: PropTypes.bool,
presenceLogEntries: PropTypes.array,
presences: PropTypes.object,
sessionId: PropTypes.string
};
state = {
@ -93,6 +98,7 @@ class UIRoot extends Component {
dialog: null,
showInviteDialog: false,
showLinkDialog: false,
showPresenceList: false,
linkCode: null,
linkCodeCancel: null,
miniInviteActivated: false,
@ -123,7 +129,8 @@ class UIRoot extends Component {
exited: false,
showProfileEntry: false
showProfileEntry: false,
pendingMessage: ""
};
componentDidMount() {
@ -426,6 +433,7 @@ class UIRoot extends Component {
onProfileFinished = () => {
this.setState({ showProfileEntry: false });
this.props.hubChannel.sendProfileUpdate();
};
beginOrSkipAudioSetup = () => {
@ -553,7 +561,7 @@ class UIRoot extends Component {
};
onMiniInviteClicked = () => {
const link = "https://hub.link/" + this.props.hubEntryCode;
const link = "https://hub.link/" + this.props.hubId;
this.setState({ miniInviteActivated: true });
setTimeout(() => {
@ -567,6 +575,16 @@ class UIRoot extends Component {
}
};
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;
};
renderExitedPane = () => {
let subtitle = null;
if (this.props.roomUnavailableReason === "closed") {
@ -657,13 +675,26 @@ class UIRoot extends Component {
renderEntryStartPanel = () => {
return (
<div className={entryStyles.entryPanel}>
<div className={entryStyles.title}>{this.props.hubName}</div>
<div className={entryStyles.name}>{this.props.hubName}</div>
<div className={entryStyles.center}>
<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>
</div>
<form onSubmit={this.sendMessage}>
<div className={styles.messageEntry}>
<input
className={styles.messageEntryInput}
value={this.state.pendingMessage}
onFocus={e => e.target.select()}
onChange={e => this.setState({ pendingMessage: e.target.value })}
placeholder="Send a message..."
/>
<input className={styles.messageEntrySubmit} type="submit" value="send" />
</div>
</form>
</div>
<div className={entryStyles.buttonContainer}>
@ -949,10 +980,34 @@ class UIRoot extends Component {
{(!entryFinished || this.isWaitingForAutoExit()) && (
<div className={styles.uiDialog}>
<PresenceLog entries={this.props.presenceLogEntries || []} />
<div className={dialogBoxContentsClassNames}>{dialogContents}</div>
</div>
)}
{entryFinished && <PresenceLog inRoom={true} entries={this.props.presenceLogEntries || []} />}
{entryFinished && (
<form onSubmit={this.sendMessage}>
<div className={styles.messageEntryInRoom}>
<input
className={classNames([styles.messageEntryInput, styles.messageEntryInputInRoom])}
value={this.state.pendingMessage}
onFocus={e => e.target.select()}
onChange={e => {
e.stopPropagation();
this.setState({ pendingMessage: e.target.value });
}}
placeholder="Send a message..."
/>
<input
className={classNames([styles.messageEntrySubmit, styles.messageEntrySubmitInRoom])}
type="submit"
value="send"
/>
</div>
</form>
)}
<div
className={classNames({
[styles.inviteContainer]: true,
@ -962,14 +1017,14 @@ class UIRoot extends Component {
>
{!showVREntryButton && (
<button
className={classNames({ [styles.hideSmallScreens]: this.props.occupantCount > 1 && entryFinished })}
className={classNames({ [styles.hideSmallScreens]: this.occupantCount() > 1 && entryFinished })}
onClick={() => this.toggleInviteDialog()}
>
<FormattedMessage id="entry.invite-others-nag" />
</button>
)}
{!showVREntryButton &&
this.props.occupantCount > 1 &&
this.occupantCount() > 1 &&
entryFinished && (
<button onClick={this.onMiniInviteClicked} className={styles.inviteMiniButton}>
<span>
@ -977,7 +1032,7 @@ class UIRoot extends Component {
? navigator.share
? "sharing..."
: "copied!"
: "hub.link/" + this.props.hubEntryCode}
: "hub.link/" + this.props.hubId}
</span>
</button>
)}
@ -990,6 +1045,7 @@ class UIRoot extends Component {
<InviteDialog
allowShare={!this.props.availableVREntryTypes.isInHMD}
entryCode={this.props.hubEntryCode}
hubId={this.props.hubId}
onClose={() => this.setState({ showInviteDialog: false })}
/>
)}
@ -1011,11 +1067,21 @@ class UIRoot extends Component {
</i>
</button>
<div className={styles.presenceInfo}>
<div
onClick={() => this.setState({ showPresenceList: !this.state.showPresenceList })}
className={classNames({
[styles.presenceInfo]: true,
[styles.presenceInfoSelected]: this.state.showPresenceList
})}
>
<FontAwesomeIcon icon={faUsers} />
<span className={styles.occupantCount}>{this.props.occupantCount || "-"}</span>
<span className={styles.occupantCount}>{this.occupantCount()}</span>
</div>
{this.state.showPresenceList && (
<PresenceList presences={this.props.presences} sessionId={this.props.sessionId} />
)}
{this.state.entryStep === ENTRY_STEPS.finished ? (
<div>
<TwoDHUD.TopHUD

Просмотреть файл

@ -24,6 +24,7 @@ export default class SceneEntryManager {
this.scene = document.querySelector("a-scene");
this.cursorController = document.querySelector("#cursor-controller");
this.playerRig = document.querySelector("#player-rig");
this._entered = false;
}
init = () => {
@ -32,6 +33,10 @@ export default class SceneEntryManager {
});
};
hasEntered = () => {
return this._entered;
};
enterScene = async (mediaStream, enterInVR) => {
const playerCamera = document.querySelector("#player-camera");
playerCamera.removeAttribute("scene-preview-camera");
@ -82,6 +87,7 @@ export default class SceneEntryManager {
const cursor = this.cursorController.components["cursor-controller"];
cursor.enable();
cursor.setCursorVisibility(true);
this._entered = true;
// Delay sending entry event telemetry until VR display is presenting.
(async () => {
@ -215,7 +221,7 @@ export default class SceneEntryManager {
});
document.addEventListener("paste", e => {
if (e.target.nodeName === "INPUT") return;
if (e.target.nodeName === "INPUT" && document.activeElement === e.target) return;
const url = e.clipboardData.getData("text");
const files = e.clipboardData.files && e.clipboardData.files;

Просмотреть файл

@ -87,6 +87,15 @@ export default class HubChannel {
this.channel.push("events:object_spawned", spawnEvent);
};
sendProfileUpdate = () => {
this.channel.push("events:profile_updated", { profile: this.store.state.profile });
};
sendMessage = body => {
if (body === "") return;
this.channel.push("message", { body });
};
requestSupport = () => {
this.channel.push("events:request_support", {});
};

Просмотреть файл

@ -22,6 +22,11 @@ function isMaybeDaydreamCompatibleDevice(ua) {
// that can be entered into as a "generic" entry flow.
const GENERIC_ENTRY_TYPE_DEVICE_BLACKLIST = [/cardboard/i];
export function detectInHMD() {
const isOculusBrowser = /Oculus/.test(navigator.userAgent);
return isOculusBrowser;
}
// Tries to determine VR entry compatibility regardless of the current browser.
//
// For each VR "entry type", returns VR_DEVICE_AVAILABILITY.yes if that type can be launched into directly from this browser
@ -45,7 +50,6 @@ const GENERIC_ENTRY_TYPE_DEVICE_BLACKLIST = [/cardboard/i];
export async function getAvailableVREntryTypes() {
const ua = navigator.userAgent;
const isSamsungBrowser = browser.name === "chrome" && /SamsungBrowser/.test(ua);
const isOculusBrowser = /Oculus/.test(ua);
// This needs to be kept up-to-date with the latest browsers that can support VR and Hubs.
// Checking for navigator.getVRDisplays always passes b/c of polyfill.
@ -63,7 +67,9 @@ export async function getAvailableVREntryTypes() {
: VR_DEVICE_AVAILABILITY.no;
const displays = isWebVRCapableBrowser ? await navigator.getVRDisplays() : [];
const isInHMD = isOculusBrowser;
const isOculusBrowser = /Oculus/.test(ua);
const isInHMD = detectInHMD();
const screen = isInHMD
? VR_DEVICE_AVAILABILITY.no