From be79e491e170ac3348d5a4c5b8e0879ff4bbd9ac Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 10 May 2020 14:05:48 -0500 Subject: [PATCH 001/167] immers integration: login, arrive/leave, follow requests and friends list with locations --- .vscode/settings.json | 3 - package-lock.json | 2158 ++++++++++++++++--- package.json | 7 + src/assets/stylesheets/presence-list.scss | 27 + src/assets/translations.data.json | 2 +- src/components/immers-follow-button.js | 50 + src/components/in-world-hud.js | 4 +- src/components/player-info.js | 8 +- src/hub.html | 7 +- src/hub.js | 8 +- src/react-components/presence-list.js | 34 + src/react-components/profile-entry-panel.js | 6 +- src/react-components/sign-in-dialog.js | 3 +- src/react-components/ui-root.js | 32 +- src/scene-entry-manager.js | 3 +- src/storage/store.js | 5 + src/utils/configs.js | 4 +- src/utils/immers.js | 224 ++ 18 files changed, 2298 insertions(+), 287 deletions(-) create mode 100644 src/components/immers-follow-button.js create mode 100644 src/utils/immers.js diff --git a/.vscode/settings.json b/.vscode/settings.json index a722e4fd69..0e949af89a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,4 @@ { - // Format on save for Prettier - "editor.formatOnSave": true, - // Disable html formatting for now "html.format.enable": false, // Disable the default javascript formatter "javascript.format.enable": false, diff --git a/package-lock.json b/package-lock.json index 1aab4e7a43..3e65a48d8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4636,6 +4636,27 @@ "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "dev": true }, + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, + "requires": { + "defer-to-connect": "^1.0.1" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, "@types/events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", @@ -4944,13 +4965,44 @@ "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", "dev": true }, + "activitypub-express": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/activitypub-express/-/activitypub-express-0.0.3.tgz", + "integrity": "sha512-CSHQ+mqaY52HvvpR3aQSgAT/AFptNEjiR3scwyROHgfqBGVYBlhVnkHv7oQ2AYylmvcgkzjMyBdCt7LSMSY1tA==", + "requires": { + "deepmerge": "^4.2.2", + "express": "^4.17.1", + "http-signature": "github:wmurphyrd/node-http-signature#9c02eeb", + "jsonld": "^3.0.1", + "mongodb": "^3.3.2", + "on-finished": "^2.3.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.7" + }, + "dependencies": { + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + }, + "http-signature": { + "version": "github:wmurphyrd/node-http-signature#9c02eeb45af7cbfc59dab927a2577a27c4fe7683", + "from": "github:wmurphyrd/node-http-signature#9c02eeb", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + } + } + }, "add-px-to-style": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/add-px-to-style/-/add-px-to-style-1.0.0.tgz", "integrity": "sha1-0ME1RB+oAUqBN5BFMQlvZ/KPJjo=" }, "aframe": { - "version": "github:mozillareality/aframe#74afe5ad5f2ca6a357ba3f5dfa65fae82f1c2cc2", + "version": "github:mozillareality/aframe#3fc6fdfe09cdc6eec381ca70068dbd35270e019e", "from": "github:mozillareality/aframe#hubs/master", "requires": { "custom-event-polyfill": "^1.0.6", @@ -4980,6 +5032,11 @@ "resolved": "https://registry.yarnpkg.com/aframe-slice9-component/-/aframe-slice9-component-1.0.0.tgz", "integrity": "sha1-+w+EQdrdHosRzCRRK6eqaS1iK+E=" }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, "ajv": { "version": "6.5.2", "resolved": "https://registry.yarnpkg.com/ajv/-/ajv-6.5.2.tgz", @@ -5179,6 +5236,11 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz", @@ -5199,7 +5261,6 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, "requires": { "safer-buffer": "~2.1.0" } @@ -5245,8 +5306,7 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "assign-symbols": { "version": "1.0.0", @@ -5281,8 +5341,7 @@ "async-limiter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", - "dev": true + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" }, "asynckit": { "version": "0.4.0", @@ -6658,14 +6717,12 @@ "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", - "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", - "dev": true + "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" }, "babel-code-frame": { "version": "6.26.0", @@ -6898,6 +6955,11 @@ "to-fast-properties": "^1.0.3" } }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "bail": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.3.tgz", @@ -6936,6 +6998,11 @@ } } }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, "base64-js": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", @@ -6952,11 +7019,18 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, "requires": { "tweetnacl": "^0.14.3" } }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, "bfj": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.1.tgz", @@ -6986,6 +7060,20 @@ "integrity": "sha1-6C5D6OsXBkaB5D+cjbxzHjF9GJI=", "dev": true }, + "bl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz", + "integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -7008,28 +7096,56 @@ "dev": true }, "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", - "dev": true, + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", "requires": { - "bytes": "3.0.0", + "bytes": "3.1.0", "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "~1.1.1", - "http-errors": "~1.6.2", - "iconv-lite": "0.4.19", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", "on-finished": "~2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "~1.6.15" + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" }, "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", - "dev": true + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" } } }, @@ -7198,6 +7314,11 @@ "node-releases": "^1.1.3" } }, + "bson": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.4.tgz", + "integrity": "sha512-S/yKGU1syOMzO86+dGpg2qGoDL0zvzcb262G+gqEy6TgP6rt6z6qxSFX/8X6vLC91P7G7C3nLs0+bvDzmvBA3Q==" + }, "buffer": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", @@ -7309,6 +7430,54 @@ "unset-value": "^1.0.0" } }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, "call-matcher": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/call-matcher/-/call-matcher-1.1.0.tgz", @@ -7359,6 +7528,11 @@ "caller-callsite": "^2.0.0" } }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -7397,6 +7571,11 @@ "integrity": "sha512-ekW8NQ3/FvokviDxhdKLZZAx7PptXNwxKgXtnR5y+PR3hckwuP3yJ1Ir+4/c97dsHNqtAyfKUGdw8P4EYzBNgw==", "dev": true }, + "canonicalize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-1.0.1.tgz", + "integrity": "sha512-N3cmB3QLhS5TJ5smKFf1w42rJXWe6C1qP01z4dxJiI5v269buii4fLHWETDyf7yEd0azGLNC63VxNMiPd2u0Cg==" + }, "capture-stack-trace": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", @@ -7416,8 +7595,7 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "ccount": { "version": "1.0.3", @@ -7703,6 +7881,15 @@ "is-supported-regexp-flag": "^1.0.0" } }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, "code-excerpt": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-2.1.1.tgz", @@ -7774,7 +7961,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -7797,11 +7983,20 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, "component-emitter": { "version": "1.2.1", "resolved": "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" }, "compressible": { "version": "2.0.16", @@ -7922,16 +8117,17 @@ "dev": true }, "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", - "dev": true + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } }, "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, "convert-source-map": { "version": "1.5.1", @@ -7946,16 +8142,14 @@ "dev": true }, "cookie": { - "version": "0.3.1", - "resolved": "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", - "dev": true + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "copy-concurrently": { "version": "1.0.5", @@ -8075,8 +8269,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cors": { "version": "2.8.4", @@ -8335,7 +8528,6 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, "requires": { "assert-plus": "^1.0.0" } @@ -8386,7 +8578,6 @@ "version": "2.6.9", "resolved": "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz", "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", - "dev": true, "requires": { "ms": "2.0.0" } @@ -8492,11 +8683,16 @@ "clone": "^1.0.2" } }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true + }, "define-properties": { "version": "1.1.2", "resolved": "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz", "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", - "dev": true, "requires": { "foreach": "^2.0.5", "object-keys": "^1.0.8" @@ -8618,11 +8814,15 @@ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true }, + "denque": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", + "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" + }, "depd": { "version": "1.1.2", "resolved": "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, "des.js": { "version": "1.0.0", @@ -8637,8 +8837,7 @@ "destroy": { "version": "1.0.4", "resolved": "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, "detect-browser": { "version": "3.0.1", @@ -8947,7 +9146,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -8956,8 +9154,7 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "ejs": { "version": "2.6.1", @@ -9020,8 +9217,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, "encoding": { "version": "0.1.12", @@ -9040,6 +9236,59 @@ "once": "^1.4.0" } }, + "engine.io-client": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.1.tgz", + "integrity": "sha512-RJNmA+A9Js+8Aoq815xpGAsgWH1VoSYM//2VgIiu9lNOaHFfLpTjH4tOzktBpjIs5lvOfiNY1dwf+NuU6D38Mw==", + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "~4.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~6.1.0", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "ws": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", + "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "engine.io-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz", + "integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, "enhanced-resolve": { "version": "4.1.0", "resolved": "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", @@ -9146,11 +9395,16 @@ } } }, + "escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "dev": true + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "escape-string-regexp": { "version": "1.0.5", @@ -9470,8 +9724,7 @@ "etag": { "version": "1.8.1", "resolved": "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, "event-target-shim": { "version": "3.0.2", @@ -9577,62 +9830,100 @@ } }, "express": { - "version": "4.16.3", - "resolved": "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz", - "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", - "dev": true, + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", "requires": { - "accepts": "~1.3.5", + "accepts": "~1.3.7", "array-flatten": "1.1.1", - "body-parser": "1.18.2", - "content-disposition": "0.5.2", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", "content-type": "~1.0.4", - "cookie": "0.3.1", + "cookie": "0.4.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "~1.1.2", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.1.1", + "finalhandler": "~1.1.2", "fresh": "0.5.2", "merge-descriptors": "1.0.1", "methods": "~1.1.2", "on-finished": "~2.3.0", - "parseurl": "~1.3.2", + "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.3", - "qs": "6.5.1", - "range-parser": "~1.2.0", - "safe-buffer": "5.1.1", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" }, "dependencies": { + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" } } }, "extend": { "version": "3.0.2", "resolved": "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz", - "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=", - "dev": true + "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=" }, "extend-shallow": { "version": "2.0.1", @@ -9695,8 +9986,7 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { "version": "2.0.1", @@ -9846,18 +10136,29 @@ } }, "finalhandler": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", - "dev": true, + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "requires": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.4.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", "unpipe": "~1.0.0" + }, + "dependencies": { + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + } } }, "find-cache-dir": { @@ -9983,14 +10284,12 @@ "foreach": { "version": "2.0.5", "resolved": "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", - "dev": true + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { "version": "3.0.0", @@ -10020,8 +10319,7 @@ "forwarded": { "version": "0.1.2", "resolved": "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", - "dev": true + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" }, "fragment-cache": { "version": "0.2.1", @@ -10035,8 +10333,7 @@ "fresh": { "version": "0.5.2", "resolved": "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, "from2": { "version": "2.3.0", @@ -10704,8 +11001,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=", - "dev": true + "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=" }, "functional-red-black-tree": { "version": "1.0.1", @@ -10794,7 +11090,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, "requires": { "assert-plus": "^1.0.0" } @@ -11027,14 +11322,12 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" @@ -11044,7 +11337,6 @@ "version": "6.12.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", - "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -11055,8 +11347,7 @@ "fast-deep-equal": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", - "dev": true + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" } } }, @@ -11064,7 +11355,6 @@ "version": "1.0.3", "resolved": "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz", "integrity": "sha1-ci18v8H2qoJB8W3YFOAR4fQeh5Y=", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -11078,6 +11368,26 @@ "ansi-regex": "^2.0.0" } }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "requires": { + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz", @@ -11087,8 +11397,7 @@ "has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" }, "has-unicode": { "version": "2.0.1", @@ -11128,6 +11437,12 @@ } } }, + "has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "dev": true + }, "hash-base": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", @@ -11470,6 +11785,12 @@ } } }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, "http-deceiver": { "version": "1.2.7", "resolved": "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -11538,7 +11859,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -11724,8 +12044,7 @@ "indexof": { "version": "0.0.1", "resolved": "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" }, "inflight": { "version": "1.0.6", @@ -11986,8 +12305,7 @@ "is-date-object": { "version": "1.0.1", "resolved": "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" }, "is-decimal": { "version": "1.0.2", @@ -12216,8 +12534,7 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, "is-url": { "version": "1.2.4", @@ -12255,6 +12572,12 @@ "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", "dev": true }, + "is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "dev": true + }, "isarray": { "version": "0.0.1", "resolved": "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz", @@ -12295,8 +12618,7 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "jest-docblock": { "version": "21.2.0", @@ -12340,8 +12662,7 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jsdom": { "version": "15.2.0", @@ -12450,6 +12771,12 @@ } } }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -12459,8 +12786,7 @@ "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { "version": "0.4.1", @@ -12476,8 +12802,7 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, "json3": { "version": "3.3.2", @@ -12490,16 +12815,44 @@ "resolved": "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" }, - "jsonschema": { - "version": "1.2.4", - "resolved": "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.2.4.tgz", - "integrity": "sha1-pGusXTUGolRGW8VIh24mfG0NZGQ=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, + "jsonld": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-3.1.0.tgz", + "integrity": "sha512-9x/AbUsXMMZBPxGy98Y8qMz7CU3WCq1n0KcNfR1P4RZml5oZiEQM+53/VtStOHUTUyC6fX9Sml5olUOZRARTZw==", + "requires": { + "canonicalize": "^1.0.1", + "lru-cache": "^5.1.1", + "object.fromentries": "^2.0.2", + "rdf-canonize": "^1.0.2", + "request": "^2.88.0", + "semver": "^6.3.0", + "xmldom": "0.1.19" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "jsonschema": { + "version": "1.2.4", + "resolved": "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.2.4.tgz", + "integrity": "sha1-pGusXTUGolRGW8VIh24mfG0NZGQ=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -12521,6 +12874,15 @@ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=" }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, + "requires": { + "json-buffer": "3.0.0" + } + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -12679,8 +13041,7 @@ "lodash": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, "lodash.camelcase": { "version": "4.3.0", @@ -12985,8 +13346,7 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "mem": { "version": "4.1.0", @@ -13009,6 +13369,12 @@ "readable-stream": "^2.0.1" } }, + "memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "optional": true + }, "meow": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", @@ -13030,8 +13396,7 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, "merge2": { "version": "1.2.3", @@ -13042,8 +13407,7 @@ "methods": { "version": "1.1.2", "resolved": "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, "micromatch": { "version": "3.1.10", @@ -13326,6 +13690,19 @@ } } }, + "mongodb": { + "version": "3.5.7", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.5.7.tgz", + "integrity": "sha512-lMtleRT+vIgY/JhhTn1nyGwnSMmJkJELp+4ZbrjctrnBxuLbj6rmLuJFz8W2xUzUqWmqoyVxJLYuC58ZKpcTYQ==", + "requires": { + "bl": "^2.2.0", + "bson": "^1.1.4", + "denque": "^1.4.1", + "require_optional": "^1.0.1", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -13348,8 +13725,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "multicast-dns": { "version": "6.2.3", @@ -13733,6 +14109,614 @@ } } }, + "nodemon": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.3.tgz", + "integrity": "sha512-lLQLPS90Lqwc99IHe0U94rDgvjo+G9I4uEIxRG3evSLROcqQ9hwc0AxlSHKS4T1JW/IMj/7N5mthiN58NL/5kw==", + "dev": true, + "requires": { + "chokidar": "^3.2.2", + "debug": "^3.2.6", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.7", + "semver": "^5.7.1", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.2", + "update-notifier": "^4.0.0" + }, + "dependencies": { + "ansi-align": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", + "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", + "dev": true, + "requires": { + "string-width": "^3.0.0" + }, + "dependencies": { + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "dev": true + }, + "boxen": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", + "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "dev": true, + "requires": { + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "cli-boxes": "^2.2.0", + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.8.1", + "widest-line": "^3.1.0" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "chokidar": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", + "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.4.0" + } + }, + "cli-boxes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.0.tgz", + "integrity": "sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + } + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "dot-prop": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", + "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "global-dirs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz", + "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==", + "dev": true, + "requires": { + "ini": "^1.3.5" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-installed-globally": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", + "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "dev": true, + "requires": { + "global-dirs": "^2.0.1", + "is-path-inside": "^3.0.1" + } + }, + "is-npm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", + "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==", + "dev": true + }, + "latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "dev": true, + "requires": { + "package-json": "^6.3.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "dev": true, + "requires": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "readdirp": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", + "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "registry-auth-token": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.1.1.tgz", + "integrity": "sha512-9bKS7nTl9+/A1s7tnPeGrUpRcVY+LUh7bfFgzpndALdPfXQBfQV77rQVtqgUV3ti4vc/Ik81Ex8UJDWDQ12zQA==", + "dev": true, + "requires": { + "rc": "^1.2.8" + } + }, + "registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "dev": true, + "requires": { + "rc": "^1.2.8" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "dev": true, + "requires": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "term-size": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", + "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "update-notifier": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.0.tgz", + "integrity": "sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew==", + "dev": true, + "requires": { + "boxen": "^4.2.0", + "chalk": "^3.0.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.3.1", + "is-npm": "^4.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "pupa": "^2.0.1", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + } + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dev": true, + "requires": { + "prepend-http": "^2.0.0" + } + }, + "widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dev": true, + "requires": { + "string-width": "^4.0.0" + } + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true + } + } + }, "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", @@ -13855,14 +14839,18 @@ "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz", @@ -13874,11 +14862,15 @@ "kind-of": "^3.0.3" } }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==" + }, "object-keys": { "version": "1.0.12", "resolved": "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha1-CcU4VTd1dTEMymL1W7M0q/97PtI=", - "dev": true + "integrity": "sha1-CcU4VTd1dTEMymL1W7M0q/97PtI=" }, "object-visit": { "version": "1.0.1", @@ -13893,7 +14885,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", @@ -13901,6 +14892,83 @@ "object-keys": "^1.0.11" } }, + "object.fromentries": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.2.tgz", + "integrity": "sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + }, + "dependencies": { + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + }, + "dependencies": { + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + } + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "requires": { + "has": "^1.0.3" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + } + } + }, "object.getownpropertydescriptors": { "version": "2.0.3", "resolved": "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", @@ -13959,7 +15027,6 @@ "version": "2.3.0", "resolved": "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, "requires": { "ee-first": "1.1.1" } @@ -14225,6 +15292,12 @@ "os-tmpdir": "^1.0.0" } }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true + }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", @@ -14411,6 +15484,14 @@ "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", "dev": true }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "~1.0.0" + } + }, "parserlib": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parserlib/-/parserlib-1.1.1.tgz", @@ -14418,6 +15499,14 @@ "dev": true, "optional": true }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "~1.0.0" + } + }, "parseurl": { "version": "1.3.2", "resolved": "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz", @@ -14478,8 +15567,7 @@ "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, "path-type": { "version": "3.0.0", @@ -14530,6 +15618,12 @@ "websocket": "^1.0.24" } }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, "pify": { "version": "3.0.0", "resolved": "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz", @@ -15251,8 +16345,7 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=", - "dev": true + "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=" }, "progress": { "version": "2.0.3", @@ -15289,13 +16382,19 @@ } }, "proxy-addr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", - "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", - "dev": true, + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", "requires": { "forwarded": "~0.1.2", - "ipaddr.js": "1.8.0" + "ipaddr.js": "1.9.1" + }, + "dependencies": { + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + } } }, "prr": { @@ -15313,7 +16412,12 @@ "psl": { "version": "1.1.31", "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + }, + "pstree.remy": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz", + "integrity": "sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==", "dev": true }, "public-encrypt": { @@ -15354,14 +16458,21 @@ "punycode": { "version": "1.4.1", "resolved": "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "pupa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", + "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==", + "dev": true, + "requires": { + "escape-goat": "^2.0.0" + } }, "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", - "dev": true + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, "quad-indices": { "version": "2.0.1", @@ -15446,46 +16557,50 @@ "integrity": "sha1-MqrjpjqTFEZ6RTyUyJo2TqQ3B74=" }, "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", - "dev": true, + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", "unpipe": "1.0.0" }, "dependencies": { - "depd": { - "version": "1.1.1", - "resolved": "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", - "dev": true + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "dev": true, + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", "requires": { - "depd": "1.1.1", + "depd": "~1.1.2", "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": ">= 1.3.1 < 2" + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" } }, "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", - "dev": true + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } }, "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" } } }, @@ -15507,6 +16622,27 @@ "strip-json-comments": "~2.0.1" } }, + "rdf-canonize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-1.1.0.tgz", + "integrity": "sha512-DV06OnhVfl2zcZJQCt+YvU+hoZVgpyQpNFLeAmghq8RJybUxD3B4LRzlBquYS5k+LLd8/c3g5Gnhkqjw5qRMvg==", + "requires": { + "node-forge": "^0.9.1", + "semver": "^6.3.0" + }, + "dependencies": { + "node-forge": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz", + "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, "react": { "version": "16.4.1", "resolved": "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz", @@ -15686,7 +16822,6 @@ "version": "2.3.6", "resolved": "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -15700,14 +16835,12 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -16031,7 +17164,6 @@ "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -16059,7 +17191,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -16069,20 +17200,17 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, "requires": { "psl": "^1.1.28", "punycode": "^2.1.1" @@ -16094,7 +17222,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", - "dev": true, "requires": { "lodash": "^4.17.11" } @@ -16103,7 +17230,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", - "dev": true, "requires": { "request-promise-core": "1.1.2", "stealthy-require": "^1.1.1", @@ -16128,6 +17254,22 @@ "integrity": "sha1-WhtS63Dr7UPrmC6XTIWrWVceVvo=", "dev": true }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + }, + "dependencies": { + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + } + } + }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz", @@ -16187,6 +17329,15 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, + "requires": { + "lowercase-keys": "^1.0.0" + } + }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -16260,8 +17411,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=", - "dev": true + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" }, "safe-regex": { "version": "1.1.0", @@ -16277,6 +17427,15 @@ "resolved": "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=" }, + "saslprep": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", + "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "optional": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, "sass-graph": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", @@ -16453,8 +17612,7 @@ "semver": { "version": "5.5.0", "resolved": "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz", - "integrity": "sha1-3Eu8emyp2Rbe5dQ1FvAJK1j3uKs=", - "dev": true + "integrity": "sha1-3Eu8emyp2Rbe5dQ1FvAJK1j3uKs=" }, "semver-diff": { "version": "2.1.0", @@ -16466,10 +17624,9 @@ } }, "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "dev": true, + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", "requires": { "debug": "2.6.9", "depd": "~1.1.2", @@ -16478,19 +17635,55 @@ "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" + "range-parser": "~1.2.1", + "statuses": "~1.5.0" }, "dependencies": { + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", - "dev": true + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" } } }, @@ -16522,15 +17715,21 @@ } }, "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "dev": true, + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "dependencies": { + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + } } }, "set-blocking": { @@ -16739,6 +17938,67 @@ "kind-of": "^3.2.0" } }, + "socket.io-client": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", + "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "engine.io-client": "~3.4.0", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", + "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", + "requires": { + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, "sockjs": { "version": "0.3.19", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", @@ -16847,6 +18107,15 @@ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", "dev": true }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "optional": true, + "requires": { + "memory-pager": "^1.0.2" + } + }, "spdx-correct": { "version": "3.0.0", "resolved": "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz", @@ -17053,7 +18322,6 @@ "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -17115,8 +18383,7 @@ "stealthy-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", - "dev": true + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" }, "stream-browserify": { "version": "2.0.2", @@ -17199,6 +18466,308 @@ } } }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + }, + "dependencies": { + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + } + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "requires": { + "has": "^1.0.3" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + } + } + }, + "string.prototype.trimleft": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", + "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimstart": "^1.0.0" + }, + "dependencies": { + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + }, + "dependencies": { + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + } + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "requires": { + "has": "^1.0.3" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + } + } + }, + "string.prototype.trimright": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", + "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimend": "^1.0.0" + }, + "dependencies": { + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + }, + "dependencies": { + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + } + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "requires": { + "has": "^1.0.3" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + } + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + }, + "dependencies": { + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + } + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "requires": { + "has": "^1.0.3" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + } + } + }, "string_decoder": { "version": "0.10.31", "resolved": "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz", @@ -18299,11 +19868,11 @@ "dev": true }, "three": { - "version": "github:mozillareality/three.js#c0b3d3d9c08406dcbe38ed21618dc09350c0bbbc", + "version": "github:mozillareality/three.js#6dc1886802c936054ee5f48e8b39cc1be2e6e45f", "from": "github:mozillareality/three.js#hubs/master" }, "three-ammo": { - "version": "github:infinitelee/three-ammo#92e8faa025fefd9385a1174f8c5c538b5f65c7f2", + "version": "github:infinitelee/three-ammo#e00920e8a618b13df04eaf93f397296015f4671e", "from": "github:infinitelee/three-ammo#master" }, "three-bmfont-text": { @@ -18410,6 +19979,11 @@ "os-tmpdir": "~1.0.2" } }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, "to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", @@ -18444,6 +20018,12 @@ "kind-of": "^3.0.2" } }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true + }, "to-regex": { "version": "3.0.2", "resolved": "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz", @@ -18515,17 +20095,41 @@ "resolved": "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz", "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, "toposort": { "version": "1.0.7", "resolved": "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz", "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", "dev": true }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + }, + "dependencies": { + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1" + } + } + } + }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, "requires": { "psl": "^1.1.24", "punycode": "^1.4.1" @@ -18615,7 +20219,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, "requires": { "safe-buffer": "^5.0.1" } @@ -18623,8 +20226,7 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "type-check": { "version": "0.3.2", @@ -18642,13 +20244,27 @@ "dev": true }, "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", - "dev": true, + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "requires": { "media-typer": "0.3.0", - "mime-types": "~2.1.18" + "mime-types": "~2.1.24" + }, + "dependencies": { + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + } } }, "typedarray": { @@ -18700,6 +20316,15 @@ "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=", "dev": true }, + "undefsafe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", + "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", + "dev": true, + "requires": { + "debug": "^2.2.0" + } + }, "underscore": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.5.2.tgz", @@ -18885,8 +20510,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, "unset-value": { "version": "1.0.0", @@ -19090,8 +20714,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { "version": "1.0.0", @@ -19112,8 +20735,7 @@ "utils-merge": { "version": "1.0.1", "resolved": "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { "version": "3.3.2", @@ -19144,14 +20766,12 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -19996,6 +21616,16 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "xmldom": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.19.tgz", + "integrity": "sha1-Yx/Ad3bv2EEYvyUXGzftTQdaCrw=" + }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, "xregexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", @@ -20022,8 +21652,7 @@ "yallist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" }, "yargs": { "version": "12.0.2", @@ -20138,6 +21767,11 @@ } } }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + }, "zip-loader": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/zip-loader/-/zip-loader-1.1.0.tgz", diff --git a/package.json b/package.json index b4d1d9f779..be40e52c69 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "url": "https://github.com/mozilla/hubs/issues" }, "scripts": { + "immers-build": "webpack -w --mode=development", + "immers-dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 nodemon --inspect server/index.js --watch server", "start": "webpack-dev-server --mode=development --env.loadAppConfig", "dev": "webpack-dev-server --mode=development", "local": "webpack-dev-server --mode=development --env.localDev", @@ -52,6 +54,7 @@ "@fortawesome/react-fontawesome": "^0.1.0", "@mozillareality/easing-functions": "^0.1.1", "@mozillareality/three-batch-manager": "github:mozillareality/three-batch-manager#master", + "activitypub-express": "0.0.3", "aframe": "github:mozillareality/aframe#hubs/master", "aframe-rounded": "^1.0.3", "aframe-slice9-component": "^1.0.0", @@ -72,6 +75,7 @@ "draft-js-linkify-plugin": "^2.0.1", "draft-js-plugins-editor": "^2.1.1", "event-target-shim": "^3.0.1", + "express": "^4.17.1", "form-data": "^3.0.0", "form-urlencoded": "^2.0.4", "history": "^4.7.2", @@ -82,6 +86,7 @@ "lib-hubs": "github:mozillareality/lib-hubs#master", "linkify-it": "^2.0.3", "markdown-it": "^8.4.2", + "mongodb": "^3.5.6", "moving-average": "^1.0.0", "naf-janus-adapter": "^3.0.20", "networked-aframe": "github:mozillareality/networked-aframe#master", @@ -99,6 +104,7 @@ "react-router": "^4.3.1", "react-router-dom": "^4.3.1", "screenfull": "^4.0.1", + "socket.io-client": "^2.3.0", "three": "github:mozillareality/three.js#hubs/master", "three-ammo": "github:infinitelee/three-ammo#master", "three-bmfont-text": "github:mozillareality/three-bmfont-text#hubs/master", @@ -144,6 +150,7 @@ "ncp": "^2.0.0", "node-fetch": "^2.6.0", "node-sass": "^4.13.0", + "nodemon": "^2.0.3", "ora": "^4.0.2", "phoenix-channels": "^1.0.0", "prettier": "^1.7.0", diff --git a/src/assets/stylesheets/presence-list.scss b/src/assets/stylesheets/presence-list.scss index 4377e6e331..3041948081 100644 --- a/src/assets/stylesheets/presence-list.scss +++ b/src/assets/stylesheets/presence-list.scss @@ -79,3 +79,30 @@ margin-left: 4px; text-decoration: none; } + +:local(.friends) { + border-top: 1px solid var(--panel-rule-color); + margin-top: 12px; + padding-top: 12px; + + +} + +:local(.location) { + font-size: 0.8em; + color: $grey-text; + a { + text-decoration: underline; + text-decoration-color: var(--panel-link-underline-color); + font-weight: bold; + cursor: pointer; + margin-left: 3px; + } +} + +:local(.notifier) { + color: #FF3464; + font-size: 1.1em; + position: relative; + top: -3px; +} \ No newline at end of file diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index a2aeda7e1e..a033c35771 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -11,7 +11,7 @@ "auth.verify-failed": "Unable to sign in with this link. It may have already been used or has expired.", "auth.verified": "Your email has been verified!\nYou can now close this browser tab and return to %app-name%.", "auth.spoke-verified": "Your email has been verified!\nYou can now close this browser tab and return to %editor-name%.", - "sign-in.prompt": "Sign in to pin objects in rooms.", + "sign-in.prompt": "Sign in to your Immers profile.", "sign-in.admin": "Check your email for a verification email. Once verified, enter your email to create your account or sign in.", "sign-in.admin-no-permission": "You don't have access to admin tools. Sign into another account or ask an administrator to grant you permission.", "sign-in.hub": "An account is required to join rooms.\n\nEnter your email to create your account or sign in.", diff --git a/src/components/immers-follow-button.js b/src/components/immers-follow-button.js new file mode 100644 index 0000000000..9367dc2dd4 --- /dev/null +++ b/src/components/immers-follow-button.js @@ -0,0 +1,50 @@ +/** + * Registers a click handler publishes a follow request + * @namespace immers + * @component immers-follow-button + */ +AFRAME.registerComponent("immers-follow-button", { + init() { + NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { + this.playerEl = networkedEl; + this.playerEl.addEventListener("stateadded", this.onState); + if (this.playerEl.is("friend")) { + this.setFriend(); + } + }); + this.textEl = this.el.querySelector("[text]"); + this.onClick = () => { + this.follow(this.playerEl.components["player-info"].data.immersId); + }; + this.onState = event => { + if (event.detail === "friend") { + this.setFriend(); + } + }; + }, + + play() { + this.el.object3D.addEventListener("interact", this.onClick); + if (this.playerEl) { + this.playerEl.addEventListener("stateadded", this.onState); + } + }, + + pause() { + this.el.object3D.removeEventListener("interact", this.onClick); + if (this.playerEl) { + this.playerEl.removeEventListener("stateadded", this.onState); + } + }, + + follow(targetId) { + if (!this.playerEl.is("friend")) { + this.el.emit("immers-follow", targetId); + this.textEl.setAttribute("text", "value", "Pending"); + } + }, + + setFriend() { + this.textEl.setAttribute("text", "value", "Unfollow"); + } +}); diff --git a/src/components/in-world-hud.js b/src/components/in-world-hud.js index 33e4ffc772..08635ea8bd 100644 --- a/src/components/in-world-hud.js +++ b/src/components/in-world-hud.js @@ -12,11 +12,13 @@ AFRAME.registerComponent("in-world-hud", { this.cameraBtn = this.el.querySelector(".camera-btn"); this.inviteBtn = this.el.querySelector(".invite-btn"); this.background = this.el.querySelector(".bg"); + this.notificationText = this.el.querySelector("#hud-presence-notification"); this.updateButtonStates = () => { this.mic.setAttribute("mic-button", "active", this.el.sceneEl.is("muted")); this.pen.setAttribute("icon-button", "active", this.el.sceneEl.is("pen")); this.cameraBtn.setAttribute("icon-button", "active", this.el.sceneEl.is("camera")); + this.notificationText.setAttribute("text", "value", this.el.sceneEl.is("notification") ? "*" : ""); if (window.APP.hubChannel) { this.spawn.setAttribute("icon-button", "disabled", !window.APP.hubChannel.can("spawn_and_move_media")); this.pen.setAttribute("icon-button", "disabled", !window.APP.hubChannel.can("spawn_drawing")); @@ -25,7 +27,7 @@ AFRAME.registerComponent("in-world-hud", { }; this.onStateChange = evt => { - if (!(evt.detail === "muted" || evt.detail === "frozen" || evt.detail === "pen" || evt.detail === "camera")) + if (!(evt.detail === "muted" || evt.detail === "frozen" || evt.detail === "pen" || evt.detail === "camera" || evt.detail === 'notification')) return; this.updateButtonStates(); }; diff --git a/src/components/player-info.js b/src/components/player-info.js index b0a17ebff4..1773b08863 100644 --- a/src/components/player-info.js +++ b/src/components/player-info.js @@ -36,7 +36,8 @@ function ensureAvatarNodes(json) { AFRAME.registerComponent("player-info", { schema: { avatarSrc: { type: "string" }, - avatarType: { type: "string", default: AVATAR_TYPES.SKINNABLE } + avatarType: { type: "string", default: AVATAR_TYPES.SKINNABLE }, + immersId: { type: "string" } }, init() { this.displayName = null; @@ -88,8 +89,11 @@ AFRAME.registerComponent("player-info", { window.APP.store.removeEventListener("statechanged", this.update); }, - update() { + update(oldData) { this.applyProperties(); + if (this.data.immersId !== oldData.immersId) { + this.el.emit("immers-id-changed", this.data.immersId); + } }, updateDisplayName(e) { if (!this.playerSessionId && this.isLocalPlayerInfo) { diff --git a/src/hub.html b/src/hub.html index 02b2bfb899..f35cfce737 100644 --- a/src/hub.html +++ b/src/hub.html @@ -154,6 +154,9 @@ + + + @@ -1077,7 +1080,9 @@ - + + + diff --git a/src/hub.js b/src/hub.js index 687d132075..d8aa45cca1 100644 --- a/src/hub.js +++ b/src/hub.js @@ -3,6 +3,7 @@ import configs from "./utils/configs"; import "./utils/theme"; import "@babel/polyfill"; import "./utils/debug-log"; +import * as immers from "./utils/immers"; console.log(`App version: ${process.env.BUILD_VERSION || "?"}`); @@ -217,6 +218,8 @@ import detectConcurrentLoad from "./utils/concurrent-load-detector"; import qsTruthy from "./utils/qs_truthy"; +import "./components/immers-follow-button"; + const PHOENIX_RELIABLE_NAF = "phx-reliable"; NAF.options.firstSyncSource = PHOENIX_RELIABLE_NAF; NAF.options.syncSource = PHOENIX_RELIABLE_NAF; @@ -885,7 +888,8 @@ document.addEventListener("DOMContentLoaded", async () => { remountUI({ performConditionalSignIn, embed: isEmbed, - showPreload: isEmbed + showPreload: isEmbed, + showSignInDialog: !store.state.profile.handle }); entryManager.performConditionalSignIn = performConditionalSignIn; entryManager.init(); @@ -1573,4 +1577,6 @@ document.addEventListener("DOMContentLoaded", async () => { authChannel.setSocket(socket); linkChannel.setSocket(socket); + + immers.initialize(store, scene, remountUI); }); diff --git a/src/react-components/presence-list.js b/src/react-components/presence-list.js index 391b11017a..6145880186 100644 --- a/src/react-components/presence-list.js +++ b/src/react-components/presence-list.js @@ -47,6 +47,8 @@ export function navigateToClientInfo(history, clientId) { export default class PresenceList extends Component { static propTypes = { presences: PropTypes.object, + friends: PropTypes.array, + friendsUpdated: PropTypes.bool, history: PropTypes.object, sessionId: PropTypes.string, signedIn: PropTypes.bool, @@ -117,6 +119,34 @@ export default class PresenceList extends Component { ); }; + domForFriend = (locActivity) => { + const profile = locActivity.actor + const place = locActivity.target + return ( + +
+
+ +
+
+
+ {profile.name} +
+
+
+ {locActivity.type === 'Arrive' ? ( + Online at {place.name} + ) : ({locActivity.type === 'Leave' ? 'Offline' : ''})} +
+
+
+ ); + }; + componentDidMount() { document.querySelector(".a-canvas").addEventListener( "mouseup", @@ -140,6 +170,9 @@ export default class PresenceList extends Component { .filter(([k]) => k !== this.props.sessionId) .map(this.domForPresence)} +
+ {this.props.friends.map(this.domForFriend)} +
{this.props.signedIn ? (
@@ -178,6 +211,7 @@ export default class PresenceList extends Component { > {occupantCount} + {this.props.friendsUpdated && (*)} {this.props.expanded && this.renderExpandedList()}
diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js index b0660ef158..97c7c062ba 100644 --- a/src/react-components/profile-entry-panel.js +++ b/src/react-components/profile-entry-panel.js @@ -56,13 +56,9 @@ class ProfileEntryPanel extends Component { saveStateAndFinish = e => { e && e.preventDefault(); - const { displayName } = this.props.store.state.profile; - const { hasChangedName } = this.props.store.state.activity; - - const hasChangedNowOrPreviously = hasChangedName || this.state.displayName !== displayName; this.props.store.update({ activity: { - hasChangedName: hasChangedNowOrPreviously, + hasChangedName: true, hasAcceptedProfile: true }, profile: { diff --git a/src/react-components/sign-in-dialog.js b/src/react-components/sign-in-dialog.js index 7a226d74f5..def5d39b8d 100644 --- a/src/react-components/sign-in-dialog.js +++ b/src/react-components/sign-in-dialog.js @@ -66,9 +66,8 @@ export default class SignInDialog extends Component { {this.props.message} handleTextFieldFocus(e.target)} onBlur={() => handleTextFieldBlur()} diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 877734435c..588a5cd902 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -81,8 +81,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import qsTruthy from "../utils/qs_truthy"; import { CAMERA_MODE_INSPECT } from "../systems/camera-system"; + const avatarEditorDebug = qsTruthy("avatarEditorDebug"); +import * as immers from "../utils/immers"; + addLocaleData([...en]); // This is a list of regexes that match the microphone labels of HMDs. @@ -140,6 +143,7 @@ class UIRoot extends Component { isSupportAvailable: PropTypes.bool, presenceLogEntries: PropTypes.array, presences: PropTypes.object, + friends: PropTypes.array, sessionId: PropTypes.string, subscriptions: PropTypes.object, initialIsSubscribed: PropTypes.bool, @@ -214,7 +218,8 @@ class UIRoot extends Component { objectInfo: null, objectSrc: "", isObjectListExpanded: false, - isPresenceListExpanded: false + isPresenceListExpanded: false, + hasUnreadFriendUpdate: false, }; constructor(props) { @@ -268,6 +273,12 @@ class UIRoot extends Component { }, 0); }); } + if (prevProps.friends !== this.props.friends && !this.state.isPresenceListExpanded) { + this.setState({ hasUnreadFriendUpdate: true }); + this.props.scene.addState("notification"); + } else if (!this.state.hasUnreadFriendUpdate) { + this.props.scene.removeState("notification"); + } } onConcurrentLoad = () => { @@ -371,6 +382,9 @@ class UIRoot extends Component { } this.playerRig = scene.querySelector("#avatar-rig"); + if (this.props.showSignInDialog) { + this.showSignInDialog(); + } } UNSAFE_componentWillMount() { @@ -863,12 +877,14 @@ class UIRoot extends Component { this.showNonHistoriedDialog(SignInDialog, { message: messages["sign-in.prompt"], onSignIn: async email => { - const { authComplete } = await this.props.authChannel.startAuthentication(email, this.props.hubChannel); - this.showNonHistoriedDialog(SignInDialog, { authStarted: true }); - - await authComplete; - + try { + await immers.signIn(email, this.props.store); + } catch (err) { + console.error("Error signing in to immers profile:", err.message); + this.showNonHistoriedDialog(SignInDialog, { authStarted: false }); + return; + } this.setState({ signedIn: true }); this.closeDialog(); } @@ -2065,6 +2081,8 @@ class UIRoot extends Component { { if (expand) { - this.setState({ isPresenceListExpanded: expand, isObjectListExpanded: false }); + this.setState({ isPresenceListExpanded: expand, isObjectListExpanded: false, hasUnreadFriendUpdate: false }); } else { this.setState({ isPresenceListExpanded: expand }); } diff --git a/src/scene-entry-manager.js b/src/scene-entry-manager.js index 404edce94f..c7ea3de907 100644 --- a/src/scene-entry-manager.js +++ b/src/scene-entry-manager.js @@ -169,12 +169,13 @@ export default class SceneEntryManager { _setPlayerInfoFromProfile = async (force = false) => { const avatarId = this.store.state.profile.avatarId; + const immersId = this.store.state.profile.id; if (!force && this._lastFetchedAvatarId === avatarId) return; // Avoid continually refetching based upon state changing this._lastFetchedAvatarId = avatarId; const avatarSrc = await getAvatarSrc(avatarId); - this.avatarRig.setAttribute("player-info", { avatarSrc, avatarType: getAvatarType(avatarId) }); + this.avatarRig.setAttribute("player-info", { avatarSrc, avatarType: getAvatarType(avatarId), immersId }); }; _setupKicking = () => { diff --git a/src/storage/store.js b/src/storage/store.js index 4c964f55ff..9fdf0dbd5e 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -20,8 +20,13 @@ export const SCHEMA = { type: "object", additionalProperties: false, properties: { + handle: { type: "string" }, displayName: { type: "string", pattern: "^[A-Za-z0-9 -]{3,32}$" }, avatarId: { type: "string" }, + id: { type: "string" }, + outbox: { type: "string" }, + inbox: { type: "string" }, + followers: { type: "string" }, // personalAvatarId is obsolete, but we need it here for backwards compatibility. personalAvatarId: { type: "string" } } diff --git a/src/utils/configs.js b/src/utils/configs.js index 0bf1cc6217..4e97c796ee 100644 --- a/src/utils/configs.js +++ b/src/utils/configs.js @@ -5,7 +5,9 @@ import sceneEditorLogo from "../assets/images/editor-logo.png"; import pdfjs from "pdfjs-dist"; // Read configs from global variable if available, otherwise use the process.env injected from build. -const configs = {}; +const configs = { + IMMERS_SERVER: "https://localhost:8081" +}; let isAdmin = false; [ diff --git a/src/utils/immers.js b/src/utils/immers.js new file mode 100644 index 0000000000..56109aec8e --- /dev/null +++ b/src/utils/immers.js @@ -0,0 +1,224 @@ +import io from "socket.io-client"; +import configs from "./configs"; +const host = configs.IMMERS_SERVER; +let place; + +export function getAvatarFromActor(actorObj) { + if (!actorObj.attachment) { + return null; + } + const attachments = Array.isArray(actorObj.attachment) ? actorObj.attachment : [actorObj.attachment]; + const avi = attachments.find(obj => obj.type === "Avatar"); + if (avi) { + return avi.url || avi.content; + } + return null; +} + +export async function getObject(IRI) { + if (IRI.startsWith(host)) { + const result = await window.fetch(IRI, { + headers: { Accept: "application/activity+json" } + }); + if (!result.ok) { + throw new Error(`Object fetch error ${result.message}`); + } + return result.json(); + } +} + +export async function getLocalActor(name) { + const response = await window.fetch(`${host}/u/${name}`, { + headers: { + Accept: "application/activity+json" + } + }); + if (!response.ok) { + return null; + } + return response.json(); +} + +export async function createLocalActor(name) { + const response = await window.fetch(`${host}/u/${name}`, { + method: "POST", + headers: { + Accept: ["application/activity+json"] + } + }); + if (!response.ok) { + throw new Error("Error creating actor"); + } + return response.json(); +} + +export function postActivity(outbox, activity) { + return window.fetch(outbox, { + method: "POST", + headers: { + "Content-Type": "application/activity+json" + }, + body: JSON.stringify(activity) + }); +} + +export function updateProfile(actorObj, update) { + update.id = actorObj.id; + const activity = { + type: "Update", + actor: actorObj.id, + object: update, + to: actorObj.followers + }; + return postActivity(actorObj.outbox, activity); +} + +export function follow(actorObj, targetId) { + return postActivity(actorObj.outbox, { + type: "Follow", + actor: actorObj.id, + object: targetId, + to: targetId + }); +} + +export function arrive(actorObj) { + return postActivity(actorObj.outbox, { + type: "Arrive", + actor: actorObj.id, + target: place, + to: actorObj.followers + }); +} + +export function leave(actorObj) { + return postActivity(actorObj.outbox, { + type: "Leave", + actor: actorObj.id, + target: place, + to: actorObj.followers + }); +} + +export async function getFriends(actorObj) { + const response = await window.fetch(`${actorObj.id}/friends`, { + headers: { + Accept: "application/activity+json" + } + }); + if (!response.ok) { + throw new Error("Unable to fech friends"); + } + return response.json(); +} + +getObject(`${host}/o/immer`).then(immer => { + place = immer; + place.url = window.location.href; // adds room id +}); + +export function initialize(store, scene, remountUI) { + const immerSocket = io(host); + // arrive/leave activities + scene.addEventListener( + "entered", + () => { + const profile = store.state.profile; + if (!profile.id) return; + arrive(profile); + immerSocket.emit("entered", { + outbox: profile.outbox, + // prepare a leave activity to be fired on disconnect + leave: { + type: "Leave", + actor: profile.id, + target: window.location.href, + to: profile.followers + } + }); + }, + { once: true } + ); + + // friends list + let friendsCol; + const updateFriends = async () => { + if (store.state.profile.id) { + const profile = store.state.profile; + friendsCol = await getFriends(profile); + remountUI({ friends: friendsCol.orderedItems }); + // update follow button for new friends + const players = window.APP.componentRegistry["player-info"]; + if (players) { + players.forEach(infoComp => { + if (friendsCol.orderedItems.some(act => act.actor.id === infoComp.data.immersId)) { + infoComp.el.addState("friend"); + } + }); + } + } + }; + immerSocket.on("friends-update", updateFriends); + // profile + const onImmersProfileChange = () => { + immerSocket.emit("profile", store.state.profile.id); + updateFriends(); + }; + store.addEventListener("profilechanged", onImmersProfileChange); + scene.addEventListener("avatar_updated", () => { + updateProfile(store.state.profile, { + name: store.state.displayName, + attachment: [ + { + type: "Avatar", + content: store.state.avatarId + } + ] + }).catch(err => console.error("Error updating profile:", err.message)); + }); + // send profile id if it was cached, pull initial friends list + onImmersProfileChange(); + + // entity interactions + scene.addEventListener("immers-id-changed", event => { + if (!friendsCol) { + return; + } + if (friendsCol.orderedItems.some(act => act.actor.id === event.detail)) { + event.target.addState("friend"); + } + }); + + scene.addEventListener("immers-follow", event => { + if (!event.detail) { + return; + } + follow(store.state.profile, event.detail) + .catch(err => console.err("Error sending follow request:", err.message)); + }); +} + +export async function signIn(handle, store) { + const handleParts = handle.split("@"); + // todo: login to remote user's hosts + // if (handleParts[1] !== window.location.host) {} + let actorObj = await getLocalActor(handleParts[0]); + if (!actorObj && !handleParts[1]) { + actorObj = await createLocalActor(handleParts[0]); + handle = `${handleParts[0]}@${window.location.host}`; + } + if (actorObj) { + const initialAvi = store.state.profile.avatarId; + store.update({ + profile: { + handle, + id: actorObj.id, + avatarId: getAvatarFromActor(actorObj) || initialAvi, + displayName: actorObj.name, + inbox: actorObj.inbox, + outbox: actorObj.outbox, + followers: actorObj.followers + } + }); + } +} From 15b99fbbee5b61d0d6f17f5b0ec3cb1fb1fb8480 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 14 May 2020 08:16:19 -0500 Subject: [PATCH 002/167] fix broken profile update --- src/utils/immers.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 56109aec8e..f6f4bb4ec3 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -166,12 +166,13 @@ export function initialize(store, scene, remountUI) { }; store.addEventListener("profilechanged", onImmersProfileChange); scene.addEventListener("avatar_updated", () => { - updateProfile(store.state.profile, { - name: store.state.displayName, + const profile = store.state.profile; + updateProfile(profile, { + name: profile.displayName, attachment: [ { type: "Avatar", - content: store.state.avatarId + content: profile.avatarId } ] }).catch(err => console.error("Error updating profile:", err.message)); @@ -193,8 +194,7 @@ export function initialize(store, scene, remountUI) { if (!event.detail) { return; } - follow(store.state.profile, event.detail) - .catch(err => console.err("Error sending follow request:", err.message)); + follow(store.state.profile, event.detail).catch(err => console.err("Error sending follow request:", err.message)); }); } From 40bd40058f1aff2a221d885c98c7ba67158b7b41 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 14 May 2020 08:18:01 -0500 Subject: [PATCH 003/167] authorize with immer on load, fetch authorized profile as part of initialization --- src/hub.js | 2 + src/react-components/ui-root.js | 3 -- src/storage/store.js | 11 +++++ src/utils/immers.js | 79 ++++++++++++++++++++++++++++++--- 4 files changed, 87 insertions(+), 8 deletions(-) diff --git a/src/hub.js b/src/hub.js index d8aa45cca1..364c557d07 100644 --- a/src/hub.js +++ b/src/hub.js @@ -179,6 +179,8 @@ if (isEmbed && !qs.get("embed_token")) { throw new Error("no embed token"); } +immers.auth(store); + THREE.Object3D.DefaultMatrixAutoUpdate = false; window.APP.quality = window.APP.store.state.preferences.materialQualitySetting === "low" diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 588a5cd902..e20e9ea063 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -382,9 +382,6 @@ class UIRoot extends Component { } this.playerRig = scene.querySelector("#avatar-rig"); - if (this.props.showSignInDialog) { - this.showSignInDialog(); - } } UNSAFE_componentWillMount() { diff --git a/src/storage/store.js b/src/storage/store.js index 9fdf0dbd5e..2d86b5b690 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -41,6 +41,15 @@ export const SCHEMA = { } }, + immerCredentials: { + type: "object", + additionalProperties: false, + properties: { + token: { type: ["null", "string"] }, + home: { type: ["null", "string"] } + } + }, + activity: { type: "object", additionalProperties: false, @@ -158,6 +167,7 @@ export const SCHEMA = { properties: { profile: { $ref: "#/definitions/profile" }, credentials: { $ref: "#/definitions/credentials" }, + immerCredentials: { $ref: "#/definitions/immerCredentials" }, activity: { $ref: "#/definitions/activity" }, settings: { $ref: "#/definitions/settings" }, preferences: { $ref: "#/definitions/preferences" }, @@ -191,6 +201,7 @@ export default class Store extends EventTarget { activity: {}, settings: {}, credentials: {}, + immerCredentials: {}, profile: {}, confirmedDiscordRooms: [], confirmedBroadcastedRooms: [], diff --git a/src/utils/immers.js b/src/utils/immers.js index f6f4bb4ec3..494c4bd672 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -2,6 +2,7 @@ import io from "socket.io-client"; import configs from "./configs"; const host = configs.IMMERS_SERVER; let place; +let token; export function getAvatarFromActor(actorObj) { if (!actorObj.attachment) { @@ -38,6 +39,18 @@ export async function getLocalActor(name) { } return response.json(); } +export async function getActor() { + const response = await window.fetch(`${host}/me`, { + headers: { + Accept: "application/activity+json", + Authorization: `Bearer ${token}` + } + }); + if (!response.ok) { + return null; + } + return response.json(); +} export async function createLocalActor(name) { const response = await window.fetch(`${host}/u/${name}`, { @@ -112,12 +125,68 @@ export async function getFriends(actorObj) { return response.json(); } -getObject(`${host}/o/immer`).then(immer => { - place = immer; - place.url = window.location.href; // adds room id -}); +// perform oauth flow to get access token for local or remote user +export async function auth(store) { + const loc = new URL(window.location); + const params = loc.searchParams; + const hashParams = new URLSearchParams(loc.hash.substring(1)); + const hubUri = new URL(window.location); + hubUri.seach = new URLSearchParams({ hub_id: params.get("hub_id") }).toString(); + hubUri.hash = ""; + place = await getObject(`${host}/o/immer`); + place.url = hubUri; // include room id + + if (hashParams.has("access_token")) { + // record user's home server in case redirected during auth + let home; + try { + home = new URL(document.referrer); + home = `${home.protocol}//${home.host}`; + } catch (ignore) { + home = null; + } + store.update({ + immerCredentials: { + token: hashParams.get("access_token"), + home + } + }); + window.location.hash = ""; + } + + if (!store.state.immerCredentials.token) { + const redirect = new URL(`${host}/dialog/authorize`); + redirect.search = new URLSearchParams({ + client_id: place.id, + redirect_uri: hubUri, + response_type: "token" + }).toString(); + window.location = redirect; + return; + } else { + token = store.state.immerCredentials.token; + } +} -export function initialize(store, scene, remountUI) { +export async function initialize(store, scene, remountUI) { + // immers profile + const actorObj = await getActor(); + if (actorObj) { + const initialAvi = store.state.profile.avatarId; + store.update({ + profile: { + id: actorObj.id, + avatarId: getAvatarFromActor(actorObj) || initialAvi, + displayName: actorObj.name, + inbox: actorObj.inbox, + outbox: actorObj.outbox, + followers: actorObj.followers + }, + activity: { + hasChangedName: true + } + }); + } const immerSocket = io(host); // arrive/leave activities scene.addEventListener( From 467149c2ab371c5c2cadb54892444fb192488be3 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 14 May 2020 18:25:25 -0500 Subject: [PATCH 004/167] undo obsolete changes to hubs source --- src/react-components/profile-entry-panel.js | 6 +++++- src/react-components/sign-in-dialog.js | 3 ++- src/react-components/ui-root.js | 15 +++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js index 97c7c062ba..b0660ef158 100644 --- a/src/react-components/profile-entry-panel.js +++ b/src/react-components/profile-entry-panel.js @@ -56,9 +56,13 @@ class ProfileEntryPanel extends Component { saveStateAndFinish = e => { e && e.preventDefault(); + const { displayName } = this.props.store.state.profile; + const { hasChangedName } = this.props.store.state.activity; + + const hasChangedNowOrPreviously = hasChangedName || this.state.displayName !== displayName; this.props.store.update({ activity: { - hasChangedName: true, + hasChangedName: hasChangedNowOrPreviously, hasAcceptedProfile: true }, profile: { diff --git a/src/react-components/sign-in-dialog.js b/src/react-components/sign-in-dialog.js index def5d39b8d..7a226d74f5 100644 --- a/src/react-components/sign-in-dialog.js +++ b/src/react-components/sign-in-dialog.js @@ -66,8 +66,9 @@ export default class SignInDialog extends Component { {this.props.message} handleTextFieldFocus(e.target)} onBlur={() => handleTextFieldBlur()} diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index e20e9ea063..c3879c2c82 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -81,11 +81,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import qsTruthy from "../utils/qs_truthy"; import { CAMERA_MODE_INSPECT } from "../systems/camera-system"; - const avatarEditorDebug = qsTruthy("avatarEditorDebug"); -import * as immers from "../utils/immers"; - addLocaleData([...en]); // This is a list of regexes that match the microphone labels of HMDs. @@ -874,14 +871,12 @@ class UIRoot extends Component { this.showNonHistoriedDialog(SignInDialog, { message: messages["sign-in.prompt"], onSignIn: async email => { + const { authComplete } = await this.props.authChannel.startAuthentication(email, this.props.hubChannel); + this.showNonHistoriedDialog(SignInDialog, { authStarted: true }); - try { - await immers.signIn(email, this.props.store); - } catch (err) { - console.error("Error signing in to immers profile:", err.message); - this.showNonHistoriedDialog(SignInDialog, { authStarted: false }); - return; - } + + await authComplete; + this.setState({ signedIn: true }); this.closeDialog(); } From f97603edebae87268f5b95e4bb2c48c4eabd058a Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 14 May 2020 18:43:58 -0500 Subject: [PATCH 005/167] connect to home Immer for token actor lookup and update streaming --- src/utils/immers.js | 54 +++++++++------------------------------------ 1 file changed, 11 insertions(+), 43 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 494c4bd672..163facbcbf 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -1,6 +1,7 @@ import io from "socket.io-client"; import configs from "./configs"; -const host = configs.IMMERS_SERVER; +const localImmer = configs.IMMERS_SERVER; +let homeImmer; let place; let token; @@ -17,7 +18,7 @@ export function getAvatarFromActor(actorObj) { } export async function getObject(IRI) { - if (IRI.startsWith(host)) { + if (IRI.startsWith(localImmer) || IRI.startsWith(homeImmer)) { const result = await window.fetch(IRI, { headers: { Accept: "application/activity+json" } }); @@ -25,22 +26,13 @@ export async function getObject(IRI) { throw new Error(`Object fetch error ${result.message}`); } return result.json(); + } else { + throw new Error("Object fetch proxy not implemented"); } } -export async function getLocalActor(name) { - const response = await window.fetch(`${host}/u/${name}`, { - headers: { - Accept: "application/activity+json" - } - }); - if (!response.ok) { - return null; - } - return response.json(); -} export async function getActor() { - const response = await window.fetch(`${host}/me`, { + const response = await window.fetch(`${homeImmer}/me`, { headers: { Accept: "application/activity+json", Authorization: `Bearer ${token}` @@ -53,7 +45,7 @@ export async function getActor() { } export async function createLocalActor(name) { - const response = await window.fetch(`${host}/u/${name}`, { + const response = await window.fetch(`${localImmer}/u/${name}`, { method: "POST", headers: { Accept: ["application/activity+json"] @@ -133,7 +125,7 @@ export async function auth(store) { const hubUri = new URL(window.location); hubUri.seach = new URLSearchParams({ hub_id: params.get("hub_id") }).toString(); hubUri.hash = ""; - place = await getObject(`${host}/o/immer`); + place = await getObject(`${localImmer}/o/immer`); place.url = hubUri; // include room id if (hashParams.has("access_token")) { @@ -155,7 +147,7 @@ export async function auth(store) { } if (!store.state.immerCredentials.token) { - const redirect = new URL(`${host}/dialog/authorize`); + const redirect = new URL(`${localImmer}/dialog/authorize`); redirect.search = new URLSearchParams({ client_id: place.id, redirect_uri: hubUri, @@ -165,6 +157,7 @@ export async function auth(store) { return; } else { token = store.state.immerCredentials.token; + homeImmer = store.state.immerCredentials.home; } } @@ -187,7 +180,7 @@ export async function initialize(store, scene, remountUI) { } }); } - const immerSocket = io(host); + const immerSocket = io(homeImmer); // arrive/leave activities scene.addEventListener( "entered", @@ -266,28 +259,3 @@ export async function initialize(store, scene, remountUI) { follow(store.state.profile, event.detail).catch(err => console.err("Error sending follow request:", err.message)); }); } - -export async function signIn(handle, store) { - const handleParts = handle.split("@"); - // todo: login to remote user's hosts - // if (handleParts[1] !== window.location.host) {} - let actorObj = await getLocalActor(handleParts[0]); - if (!actorObj && !handleParts[1]) { - actorObj = await createLocalActor(handleParts[0]); - handle = `${handleParts[0]}@${window.location.host}`; - } - if (actorObj) { - const initialAvi = store.state.profile.avatarId; - store.update({ - profile: { - handle, - id: actorObj.id, - avatarId: getAvatarFromActor(actorObj) || initialAvi, - displayName: actorObj.name, - inbox: actorObj.inbox, - outbox: actorObj.outbox, - followers: actorObj.followers - } - }); - } -} From 3fca521b169b8b29dbb96a24740e6b35e5f29114 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 15 May 2020 17:55:17 -0500 Subject: [PATCH 006/167] add authorization to friends request --- src/utils/immers.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 163facbcbf..d6d736b879 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -108,7 +108,8 @@ export function leave(actorObj) { export async function getFriends(actorObj) { const response = await window.fetch(`${actorObj.id}/friends`, { headers: { - Accept: "application/activity+json" + Accept: "application/activity+json", + Authorization: `Bearer ${token}` } }); if (!response.ok) { From 143386b42cc40475f1ce798777553e27e578a185 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 17 May 2020 16:56:19 -0500 Subject: [PATCH 007/167] updated immers auth paths and us tokens in requests --- src/utils/immers.js | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index d6d736b879..1a7e8a1885 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -19,9 +19,11 @@ export function getAvatarFromActor(actorObj) { export async function getObject(IRI) { if (IRI.startsWith(localImmer) || IRI.startsWith(homeImmer)) { - const result = await window.fetch(IRI, { - headers: { Accept: "application/activity+json" } - }); + const headers = { Accept: "application/activity+json" }; + if (token) { + headers.Authorization = `Bearer ${token}`; + } + const result = await window.fetch(IRI, { headers }); if (!result.ok) { throw new Error(`Object fetch error ${result.message}`); } @@ -32,7 +34,7 @@ export async function getObject(IRI) { } export async function getActor() { - const response = await window.fetch(`${homeImmer}/me`, { + const response = await window.fetch(`${homeImmer}/auth/me`, { headers: { Accept: "application/activity+json", Authorization: `Bearer ${token}` @@ -44,24 +46,12 @@ export async function getActor() { return response.json(); } -export async function createLocalActor(name) { - const response = await window.fetch(`${localImmer}/u/${name}`, { - method: "POST", - headers: { - Accept: ["application/activity+json"] - } - }); - if (!response.ok) { - throw new Error("Error creating actor"); - } - return response.json(); -} - export function postActivity(outbox, activity) { return window.fetch(outbox, { method: "POST", headers: { - "Content-Type": "application/activity+json" + "Content-Type": "application/activity+json", + Authorization: `Bearer ${token}` }, body: JSON.stringify(activity) }); @@ -148,7 +138,7 @@ export async function auth(store) { } if (!store.state.immerCredentials.token) { - const redirect = new URL(`${localImmer}/dialog/authorize`); + const redirect = new URL(`${localImmer}/auth/authorize`); redirect.search = new URLSearchParams({ client_id: place.id, redirect_uri: hubUri, From ade011cc763e41aafeb8dfbac47c2c65d7d92155 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 17 May 2020 19:25:13 -0500 Subject: [PATCH 008/167] authenticate socketio connections --- src/utils/immers.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 1a7e8a1885..3df2586c3f 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -151,7 +151,8 @@ export async function auth(store) { homeImmer = store.state.immerCredentials.home; } } - +// TODO: ensure socket.profile happens even if profile cached +// TODO: arrive activity should happen earlier; on authorized connection export async function initialize(store, scene, remountUI) { // immers profile const actorObj = await getActor(); @@ -171,7 +172,15 @@ export async function initialize(store, scene, remountUI) { } }); } - const immerSocket = io(homeImmer); + const immerSocket = io(homeImmer, { + transportOptions: { + polling: { + extraHeaders: { + Authorization: `Bearer ${token}` + } + } + } + }); // arrive/leave activities scene.addEventListener( "entered", From d6d08030cceda6fadb6cb464b9f4c10bd7c62fab Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 17 May 2020 21:43:37 -0500 Subject: [PATCH 009/167] arrive after socket connection, authorize prepared Leave --- src/utils/immers.js | 51 +++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 3df2586c3f..b3459e9ebd 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -151,8 +151,6 @@ export async function auth(store) { homeImmer = store.state.immerCredentials.home; } } -// TODO: ensure socket.profile happens even if profile cached -// TODO: arrive activity should happen earlier; on authorized connection export async function initialize(store, scene, remountUI) { // immers profile const actorObj = await getActor(); @@ -181,26 +179,25 @@ export async function initialize(store, scene, remountUI) { } } }); - // arrive/leave activities - scene.addEventListener( - "entered", - () => { - const profile = store.state.profile; - if (!profile.id) return; - arrive(profile); - immerSocket.emit("entered", { - outbox: profile.outbox, - // prepare a leave activity to be fired on disconnect - leave: { - type: "Leave", - actor: profile.id, - target: window.location.href, - to: profile.followers - } - }); - }, - { once: true } - ); + let hasArrived; + immerSocket.on("connect", () => { + if (hasArrived) { + return; + } + hasArrived = true; + arrive(actorObj); + immerSocket.emit("entered", { + // prepare a leave activity to be fired on disconnect + outbox: actorObj.outbox, + authorization: `Bearer ${token}`, + leave: { + type: "Leave", + actor: actorObj.id, + target: window.location.href, + to: actorObj.followers + } + }); + }); // friends list let friendsCol; @@ -220,13 +217,9 @@ export async function initialize(store, scene, remountUI) { } } }; + updateFriends(); immerSocket.on("friends-update", updateFriends); - // profile - const onImmersProfileChange = () => { - immerSocket.emit("profile", store.state.profile.id); - updateFriends(); - }; - store.addEventListener("profilechanged", onImmersProfileChange); + scene.addEventListener("avatar_updated", () => { const profile = store.state.profile; updateProfile(profile, { @@ -239,8 +232,6 @@ export async function initialize(store, scene, remountUI) { ] }).catch(err => console.error("Error updating profile:", err.message)); }); - // send profile id if it was cached, pull initial friends list - onImmersProfileChange(); // entity interactions scene.addEventListener("immers-id-changed", event => { From bdda3699ecfb765a55aa149f538d8b3987524c9a Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 21 May 2020 17:48:10 -0500 Subject: [PATCH 010/167] wait until after phoenix connection for immers initialization to avoide a raced error with ui-root state change handler --- src/hub.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hub.js b/src/hub.js index 364c557d07..fa06faeaea 100644 --- a/src/hub.js +++ b/src/hub.js @@ -678,6 +678,8 @@ function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data) } else { connectToScene(); } + + immers.initialize(store, scene, remountUI); } async function runBotMode(scene, entryManager) { @@ -1579,6 +1581,4 @@ document.addEventListener("DOMContentLoaded", async () => { authChannel.setSocket(socket); linkChannel.setSocket(socket); - - immers.initialize(store, scene, remountUI); }); From 23618e4f945fdc9cde7c1e72977818c40aea4b5c Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 22 May 2020 08:37:18 -0500 Subject: [PATCH 011/167] save avatar urls to profile so they can be fetched from other immers --- src/utils/immers.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index b3459e9ebd..2566403adc 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -1,5 +1,6 @@ import io from "socket.io-client"; import configs from "./configs"; +import { fetchAvatar } from "./avatar-utils"; const localImmer = configs.IMMERS_SERVER; let homeImmer; let place; @@ -12,7 +13,7 @@ export function getAvatarFromActor(actorObj) { const attachments = Array.isArray(actorObj.attachment) ? actorObj.attachment : [actorObj.attachment]; const avi = attachments.find(obj => obj.type === "Avatar"); if (avi) { - return avi.url || avi.content; + return avi.url; } return null; } @@ -220,14 +221,16 @@ export async function initialize(store, scene, remountUI) { updateFriends(); immerSocket.on("friends-update", updateFriends); - scene.addEventListener("avatar_updated", () => { + scene.addEventListener("avatar_updated", async () => { const profile = store.state.profile; + const avatar = await fetchAvatar(profile.avatarId); updateProfile(profile, { name: profile.displayName, attachment: [ { type: "Avatar", - content: profile.avatarId + content: profile.avatarId, + url: avatar.gltf_url } ] }).catch(err => console.error("Error updating profile:", err.message)); From 79299e95cfb36db6a0155a9dba2efed61350f5a5 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 23 May 2020 08:47:41 -0500 Subject: [PATCH 012/167] Revert obosolete package.json changes --- package.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/package.json b/package.json index be40e52c69..02ed7f1988 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,6 @@ "url": "https://github.com/mozilla/hubs/issues" }, "scripts": { - "immers-build": "webpack -w --mode=development", - "immers-dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 nodemon --inspect server/index.js --watch server", "start": "webpack-dev-server --mode=development --env.loadAppConfig", "dev": "webpack-dev-server --mode=development", "local": "webpack-dev-server --mode=development --env.localDev", @@ -54,7 +52,6 @@ "@fortawesome/react-fontawesome": "^0.1.0", "@mozillareality/easing-functions": "^0.1.1", "@mozillareality/three-batch-manager": "github:mozillareality/three-batch-manager#master", - "activitypub-express": "0.0.3", "aframe": "github:mozillareality/aframe#hubs/master", "aframe-rounded": "^1.0.3", "aframe-slice9-component": "^1.0.0", @@ -75,7 +72,6 @@ "draft-js-linkify-plugin": "^2.0.1", "draft-js-plugins-editor": "^2.1.1", "event-target-shim": "^3.0.1", - "express": "^4.17.1", "form-data": "^3.0.0", "form-urlencoded": "^2.0.4", "history": "^4.7.2", @@ -86,7 +82,6 @@ "lib-hubs": "github:mozillareality/lib-hubs#master", "linkify-it": "^2.0.3", "markdown-it": "^8.4.2", - "mongodb": "^3.5.6", "moving-average": "^1.0.0", "naf-janus-adapter": "^3.0.20", "networked-aframe": "github:mozillareality/networked-aframe#master", @@ -150,7 +145,6 @@ "ncp": "^2.0.0", "node-fetch": "^2.6.0", "node-sass": "^4.13.0", - "nodemon": "^2.0.3", "ora": "^4.0.2", "phoenix-channels": "^1.0.0", "prettier": "^1.7.0", From ec9be50a2be43566aaf525e56257bc7c73894676 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 23 May 2020 10:35:59 -0500 Subject: [PATCH 013/167] refactor local immer config to be compatible with hubs could admin panel config options --- .defaults.env | 2 ++ src/utils/configs.js | 5 ++--- webpack.config.js | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.defaults.env b/.defaults.env index debda9f08b..45c4db04d4 100644 --- a/.defaults.env +++ b/.defaults.env @@ -29,3 +29,5 @@ DEFAULT_SCENE_SID="JGLt8DP" # Uncomment to load the app config from the reticulum server in development. # Useful when testing the admin panel. # LOAD_APP_CONFIG=true + +IMMERS_SERVER="https://localhost:8081" diff --git a/src/utils/configs.js b/src/utils/configs.js index 4e97c796ee..9eec998d0f 100644 --- a/src/utils/configs.js +++ b/src/utils/configs.js @@ -5,9 +5,7 @@ import sceneEditorLogo from "../assets/images/editor-logo.png"; import pdfjs from "pdfjs-dist"; // Read configs from global variable if available, otherwise use the process.env injected from build. -const configs = { - IMMERS_SERVER: "https://localhost:8081" -}; +const configs = {}; let isAdmin = false; [ @@ -18,6 +16,7 @@ let isAdmin = false; "SENTRY_DSN", "GA_TRACKING_ID", "SHORTLINK_DOMAIN", + "IMMERS_SERVER", "BASE_ASSETS_PATH" ].forEach(x => { const el = document.querySelector(`meta[name='env:${x.toLowerCase()}']`); diff --git a/webpack.config.js b/webpack.config.js index c22d5bd3c7..7f87760c45 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -481,6 +481,7 @@ module.exports = async (env, argv) => { SENTRY_DSN: process.env.SENTRY_DSN, GA_TRACKING_ID: process.env.GA_TRACKING_ID, POSTGREST_SERVER: process.env.POSTGREST_SERVER, + IMMERS_SERVER: process.env.IMMERS_SERVER, APP_CONFIG: appConfig }) }) From cceb8c8917909c878ba6a5af5dc914a52d89519f Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 23 May 2020 10:36:57 -0500 Subject: [PATCH 014/167] hub redirect uri compatible with both query param and route params forms --- src/utils/immers.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 2566403adc..8db4adcc55 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -112,10 +112,8 @@ export async function getFriends(actorObj) { // perform oauth flow to get access token for local or remote user export async function auth(store) { const loc = new URL(window.location); - const params = loc.searchParams; const hashParams = new URLSearchParams(loc.hash.substring(1)); const hubUri = new URL(window.location); - hubUri.seach = new URLSearchParams({ hub_id: params.get("hub_id") }).toString(); hubUri.hash = ""; place = await getObject(`${localImmer}/o/immer`); place.url = hubUri; // include room id From 13caeeac384e4ad746f63c1d6c3d9e2690d123a9 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 23 May 2020 10:41:21 -0500 Subject: [PATCH 015/167] add socket.io client --- package-lock.json | 235 ++++++++++++++++++++++++++++++++++++++++++++-- package.json | 1 + 2 files changed, 228 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 218e4a31fa..ce2574a280 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4980,6 +4980,11 @@ "resolved": "https://registry.yarnpkg.com/aframe-slice9-component/-/aframe-slice9-component-1.0.0.tgz", "integrity": "sha1-+w+EQdrdHosRzCRRK6eqaS1iK+E=" }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, "ajv": { "version": "6.5.2", "resolved": "https://registry.yarnpkg.com/ajv/-/ajv-6.5.2.tgz", @@ -5179,6 +5184,11 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz", @@ -5279,8 +5289,7 @@ "async-limiter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", - "dev": true + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" }, "asynckit": { "version": "0.4.0", @@ -6894,6 +6903,11 @@ "to-fast-properties": "^1.0.3" } }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "bail": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.3.tgz", @@ -6932,6 +6946,11 @@ } } }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, "base64-js": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", @@ -6952,6 +6971,14 @@ "tweetnacl": "^0.14.3" } }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, "bfj": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.1.tgz", @@ -6981,6 +7008,11 @@ "integrity": "sha1-6C5D6OsXBkaB5D+cjbxzHjF9GJI=", "dev": true }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -7353,6 +7385,11 @@ "caller-callsite": "^2.0.0" } }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -7794,11 +7831,20 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, "component-emitter": { "version": "1.2.1", "resolved": "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" }, "compressible": { "version": "2.0.16", @@ -9048,6 +9094,64 @@ "once": "^1.4.0" } }, + "engine.io-client": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.2.tgz", + "integrity": "sha512-AWjc1Xg06a6UPFOBAzJf48W1UR/qKYmv/ubgSCumo9GXgvL/xGIvo05dXoBL+2NTLMipDI7in8xK61C17L25xg==", + "requires": { + "component-emitter": "~1.3.0", + "component-inherit": "0.0.3", + "debug": "~4.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~6.1.0", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "ws": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", + "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "engine.io-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz", + "integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, "enhanced-resolve": { "version": "4.1.0", "resolved": "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", @@ -11078,6 +11182,26 @@ "ansi-regex": "^2.0.0" } }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "requires": { + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz", @@ -11735,8 +11859,7 @@ "indexof": { "version": "0.0.1", "resolved": "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" }, "inflight": { "version": "1.0.6", @@ -13369,8 +13492,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "multicast-dns": { "version": "6.2.3", @@ -13883,6 +14005,11 @@ "resolved": "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz", @@ -14431,6 +14558,14 @@ "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", "dev": true }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "~1.0.0" + } + }, "parserlib": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parserlib/-/parserlib-1.1.1.tgz", @@ -14438,6 +14573,14 @@ "dev": true, "optional": true }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "~1.0.0" + } + }, "parseurl": { "version": "1.3.2", "resolved": "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz", @@ -16782,6 +16925,67 @@ "kind-of": "^3.2.0" } }, + "socket.io-client": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", + "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "engine.io-client": "~3.4.0", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", + "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", + "requires": { + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, "sockjs": { "version": "0.3.19", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", @@ -18451,6 +18655,11 @@ "os-tmpdir": "~1.0.2" } }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, "to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", @@ -20033,6 +20242,11 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, "xregexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", @@ -20175,6 +20389,11 @@ } } }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + }, "zip-loader": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/zip-loader/-/zip-loader-1.1.0.tgz", diff --git a/package.json b/package.json index 464090f7e9..4d8d89ba40 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "react-router": "^4.3.1", "react-router-dom": "^4.3.1", "screenfull": "^4.0.1", + "socket.io-client": "^2.3.0", "three": "github:mozillareality/three.js#hubs/master", "three-ammo": "github:infinitelee/three-ammo#master", "three-bmfont-text": "github:mozillareality/three-bmfont-text#hubs/master", From 40afe21331dae8ce1df1b7eceef73e7d05e44b36 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 23 May 2020 10:42:46 -0500 Subject: [PATCH 016/167] immers integration squash merge --- .defaults.env | 2 + .vscode/settings.json | 3 - src/assets/stylesheets/presence-list.scss | 27 +++ src/assets/translations.data.json | 2 +- src/components/immers-follow-button.js | 50 +++++ src/components/in-world-hud.js | 4 +- src/components/player-info.js | 8 +- src/hub.html | 7 +- src/hub.js | 10 +- src/react-components/presence-list.js | 34 +++ src/react-components/ui-root.js | 14 +- src/scene-entry-manager.js | 3 +- src/storage/store.js | 16 ++ src/utils/configs.js | 1 + src/utils/immers.js | 253 ++++++++++++++++++++++ webpack.config.js | 1 + 16 files changed, 423 insertions(+), 12 deletions(-) create mode 100644 src/components/immers-follow-button.js create mode 100644 src/utils/immers.js diff --git a/.defaults.env b/.defaults.env index debda9f08b..45c4db04d4 100644 --- a/.defaults.env +++ b/.defaults.env @@ -29,3 +29,5 @@ DEFAULT_SCENE_SID="JGLt8DP" # Uncomment to load the app config from the reticulum server in development. # Useful when testing the admin panel. # LOAD_APP_CONFIG=true + +IMMERS_SERVER="https://localhost:8081" diff --git a/.vscode/settings.json b/.vscode/settings.json index a722e4fd69..0e949af89a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,4 @@ { - // Format on save for Prettier - "editor.formatOnSave": true, - // Disable html formatting for now "html.format.enable": false, // Disable the default javascript formatter "javascript.format.enable": false, diff --git a/src/assets/stylesheets/presence-list.scss b/src/assets/stylesheets/presence-list.scss index 4377e6e331..3041948081 100644 --- a/src/assets/stylesheets/presence-list.scss +++ b/src/assets/stylesheets/presence-list.scss @@ -79,3 +79,30 @@ margin-left: 4px; text-decoration: none; } + +:local(.friends) { + border-top: 1px solid var(--panel-rule-color); + margin-top: 12px; + padding-top: 12px; + + +} + +:local(.location) { + font-size: 0.8em; + color: $grey-text; + a { + text-decoration: underline; + text-decoration-color: var(--panel-link-underline-color); + font-weight: bold; + cursor: pointer; + margin-left: 3px; + } +} + +:local(.notifier) { + color: #FF3464; + font-size: 1.1em; + position: relative; + top: -3px; +} \ No newline at end of file diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 02e0746672..724d0473e2 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -11,7 +11,7 @@ "auth.verify-failed": "Unable to sign in with this link. It may have already been used or has expired.", "auth.verified": "Your email has been verified!\nYou can now close this browser tab and return to %app-name%.", "auth.spoke-verified": "Your email has been verified!\nYou can now close this browser tab and return to %editor-name%.", - "sign-in.prompt": "Sign in to pin objects in rooms.", + "sign-in.prompt": "Sign in to your Immers profile.", "sign-in.admin": "Check your email for a verification email. Once verified, enter your email to create your account or sign in.", "sign-in.admin-no-permission": "You don't have access to admin tools. Sign into another account or ask an administrator to grant you permission.", "sign-in.hub": "An account is required to join rooms.\n\nEnter your email to create your account or sign in.", diff --git a/src/components/immers-follow-button.js b/src/components/immers-follow-button.js new file mode 100644 index 0000000000..9367dc2dd4 --- /dev/null +++ b/src/components/immers-follow-button.js @@ -0,0 +1,50 @@ +/** + * Registers a click handler publishes a follow request + * @namespace immers + * @component immers-follow-button + */ +AFRAME.registerComponent("immers-follow-button", { + init() { + NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { + this.playerEl = networkedEl; + this.playerEl.addEventListener("stateadded", this.onState); + if (this.playerEl.is("friend")) { + this.setFriend(); + } + }); + this.textEl = this.el.querySelector("[text]"); + this.onClick = () => { + this.follow(this.playerEl.components["player-info"].data.immersId); + }; + this.onState = event => { + if (event.detail === "friend") { + this.setFriend(); + } + }; + }, + + play() { + this.el.object3D.addEventListener("interact", this.onClick); + if (this.playerEl) { + this.playerEl.addEventListener("stateadded", this.onState); + } + }, + + pause() { + this.el.object3D.removeEventListener("interact", this.onClick); + if (this.playerEl) { + this.playerEl.removeEventListener("stateadded", this.onState); + } + }, + + follow(targetId) { + if (!this.playerEl.is("friend")) { + this.el.emit("immers-follow", targetId); + this.textEl.setAttribute("text", "value", "Pending"); + } + }, + + setFriend() { + this.textEl.setAttribute("text", "value", "Unfollow"); + } +}); diff --git a/src/components/in-world-hud.js b/src/components/in-world-hud.js index 33e4ffc772..08635ea8bd 100644 --- a/src/components/in-world-hud.js +++ b/src/components/in-world-hud.js @@ -12,11 +12,13 @@ AFRAME.registerComponent("in-world-hud", { this.cameraBtn = this.el.querySelector(".camera-btn"); this.inviteBtn = this.el.querySelector(".invite-btn"); this.background = this.el.querySelector(".bg"); + this.notificationText = this.el.querySelector("#hud-presence-notification"); this.updateButtonStates = () => { this.mic.setAttribute("mic-button", "active", this.el.sceneEl.is("muted")); this.pen.setAttribute("icon-button", "active", this.el.sceneEl.is("pen")); this.cameraBtn.setAttribute("icon-button", "active", this.el.sceneEl.is("camera")); + this.notificationText.setAttribute("text", "value", this.el.sceneEl.is("notification") ? "*" : ""); if (window.APP.hubChannel) { this.spawn.setAttribute("icon-button", "disabled", !window.APP.hubChannel.can("spawn_and_move_media")); this.pen.setAttribute("icon-button", "disabled", !window.APP.hubChannel.can("spawn_drawing")); @@ -25,7 +27,7 @@ AFRAME.registerComponent("in-world-hud", { }; this.onStateChange = evt => { - if (!(evt.detail === "muted" || evt.detail === "frozen" || evt.detail === "pen" || evt.detail === "camera")) + if (!(evt.detail === "muted" || evt.detail === "frozen" || evt.detail === "pen" || evt.detail === "camera" || evt.detail === 'notification')) return; this.updateButtonStates(); }; diff --git a/src/components/player-info.js b/src/components/player-info.js index b0a17ebff4..1773b08863 100644 --- a/src/components/player-info.js +++ b/src/components/player-info.js @@ -36,7 +36,8 @@ function ensureAvatarNodes(json) { AFRAME.registerComponent("player-info", { schema: { avatarSrc: { type: "string" }, - avatarType: { type: "string", default: AVATAR_TYPES.SKINNABLE } + avatarType: { type: "string", default: AVATAR_TYPES.SKINNABLE }, + immersId: { type: "string" } }, init() { this.displayName = null; @@ -88,8 +89,11 @@ AFRAME.registerComponent("player-info", { window.APP.store.removeEventListener("statechanged", this.update); }, - update() { + update(oldData) { this.applyProperties(); + if (this.data.immersId !== oldData.immersId) { + this.el.emit("immers-id-changed", this.data.immersId); + } }, updateDisplayName(e) { if (!this.playerSessionId && this.isLocalPlayerInfo) { diff --git a/src/hub.html b/src/hub.html index bbc1b7250b..939a5deccc 100644 --- a/src/hub.html +++ b/src/hub.html @@ -154,6 +154,9 @@ + + + @@ -1076,7 +1079,9 @@ - + + + diff --git a/src/hub.js b/src/hub.js index 99ad6147b2..c3b1d5645c 100644 --- a/src/hub.js +++ b/src/hub.js @@ -3,6 +3,7 @@ import configs from "./utils/configs"; import "./utils/theme"; import "@babel/polyfill"; import "./utils/debug-log"; +import * as immers from "./utils/immers"; console.log(`App version: ${process.env.BUILD_VERSION || "?"}`); @@ -179,6 +180,8 @@ if (isEmbed && !qs.get("embed_token")) { throw new Error("no embed token"); } +immers.auth(store); + THREE.Object3D.DefaultMatrixAutoUpdate = false; window.APP.quality = window.APP.store.state.preferences.materialQualitySetting === "low" @@ -218,6 +221,8 @@ import detectConcurrentLoad from "./utils/concurrent-load-detector"; import qsTruthy from "./utils/qs_truthy"; +import "./components/immers-follow-button"; + const PHOENIX_RELIABLE_NAF = "phx-reliable"; NAF.options.firstSyncSource = PHOENIX_RELIABLE_NAF; NAF.options.syncSource = PHOENIX_RELIABLE_NAF; @@ -674,6 +679,8 @@ function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data) } else { connectToScene(); } + + immers.initialize(store, scene, remountUI); } async function runBotMode(scene, entryManager) { @@ -886,7 +893,8 @@ document.addEventListener("DOMContentLoaded", async () => { remountUI({ performConditionalSignIn, embed: isEmbed, - showPreload: isEmbed + showPreload: isEmbed, + showSignInDialog: !store.state.profile.handle }); entryManager.performConditionalSignIn = performConditionalSignIn; entryManager.init(); diff --git a/src/react-components/presence-list.js b/src/react-components/presence-list.js index 391b11017a..6145880186 100644 --- a/src/react-components/presence-list.js +++ b/src/react-components/presence-list.js @@ -47,6 +47,8 @@ export function navigateToClientInfo(history, clientId) { export default class PresenceList extends Component { static propTypes = { presences: PropTypes.object, + friends: PropTypes.array, + friendsUpdated: PropTypes.bool, history: PropTypes.object, sessionId: PropTypes.string, signedIn: PropTypes.bool, @@ -117,6 +119,34 @@ export default class PresenceList extends Component { ); }; + domForFriend = (locActivity) => { + const profile = locActivity.actor + const place = locActivity.target + return ( + +
+
+ +
+
+
+ {profile.name} +
+
+
+ {locActivity.type === 'Arrive' ? ( + Online at {place.name} + ) : ({locActivity.type === 'Leave' ? 'Offline' : ''})} +
+
+
+ ); + }; + componentDidMount() { document.querySelector(".a-canvas").addEventListener( "mouseup", @@ -140,6 +170,9 @@ export default class PresenceList extends Component { .filter(([k]) => k !== this.props.sessionId) .map(this.domForPresence)}
+
+ {this.props.friends.map(this.domForFriend)} +
{this.props.signedIn ? (
@@ -178,6 +211,7 @@ export default class PresenceList extends Component { > {occupantCount} + {this.props.friendsUpdated && (*)} {this.props.expanded && this.renderExpandedList()}
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 79a1f03073..381e6497d3 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -141,6 +141,7 @@ class UIRoot extends Component { isSupportAvailable: PropTypes.bool, presenceLogEntries: PropTypes.array, presences: PropTypes.object, + friends: PropTypes.array, sessionId: PropTypes.string, subscriptions: PropTypes.object, initialIsSubscribed: PropTypes.bool, @@ -215,7 +216,8 @@ class UIRoot extends Component { objectInfo: null, objectSrc: "", isObjectListExpanded: false, - isPresenceListExpanded: false + isPresenceListExpanded: false, + hasUnreadFriendUpdate: false, }; constructor(props) { @@ -269,6 +271,12 @@ class UIRoot extends Component { }, 0); }); } + if (prevProps.friends !== this.props.friends && !this.state.isPresenceListExpanded) { + this.setState({ hasUnreadFriendUpdate: true }); + this.props.scene.addState("notification"); + } else if (!this.state.hasUnreadFriendUpdate) { + this.props.scene.removeState("notification"); + } } onConcurrentLoad = () => { @@ -2091,6 +2099,8 @@ class UIRoot extends Component { { if (expand) { - this.setState({ isPresenceListExpanded: expand, isObjectListExpanded: false }); + this.setState({ isPresenceListExpanded: expand, isObjectListExpanded: false, hasUnreadFriendUpdate: false }); } else { this.setState({ isPresenceListExpanded: expand }); } diff --git a/src/scene-entry-manager.js b/src/scene-entry-manager.js index 0e221c927c..18c94c4dc4 100644 --- a/src/scene-entry-manager.js +++ b/src/scene-entry-manager.js @@ -170,12 +170,13 @@ export default class SceneEntryManager { _setPlayerInfoFromProfile = async (force = false) => { const avatarId = this.store.state.profile.avatarId; + const immersId = this.store.state.profile.id; if (!force && this._lastFetchedAvatarId === avatarId) return; // Avoid continually refetching based upon state changing this._lastFetchedAvatarId = avatarId; const avatarSrc = await getAvatarSrc(avatarId); - this.avatarRig.setAttribute("player-info", { avatarSrc, avatarType: getAvatarType(avatarId) }); + this.avatarRig.setAttribute("player-info", { avatarSrc, avatarType: getAvatarType(avatarId), immersId }); }; _setupKicking = () => { diff --git a/src/storage/store.js b/src/storage/store.js index 9bfbaa4fdc..92b492ac05 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -20,8 +20,13 @@ export const SCHEMA = { type: "object", additionalProperties: false, properties: { + handle: { type: "string" }, displayName: { type: "string", pattern: "^[A-Za-z0-9 -]{3,32}$" }, avatarId: { type: "string" }, + id: { type: "string" }, + outbox: { type: "string" }, + inbox: { type: "string" }, + followers: { type: "string" }, // personalAvatarId is obsolete, but we need it here for backwards compatibility. personalAvatarId: { type: "string" } } @@ -36,6 +41,15 @@ export const SCHEMA = { } }, + immerCredentials: { + type: "object", + additionalProperties: false, + properties: { + token: { type: ["null", "string"] }, + home: { type: ["null", "string"] } + } + }, + activity: { type: "object", additionalProperties: false, @@ -157,6 +171,7 @@ export const SCHEMA = { properties: { profile: { $ref: "#/definitions/profile" }, credentials: { $ref: "#/definitions/credentials" }, + immerCredentials: { $ref: "#/definitions/immerCredentials" }, activity: { $ref: "#/definitions/activity" }, settings: { $ref: "#/definitions/settings" }, preferences: { $ref: "#/definitions/preferences" }, @@ -190,6 +205,7 @@ export default class Store extends EventTarget { activity: {}, settings: {}, credentials: {}, + immerCredentials: {}, profile: {}, confirmedDiscordRooms: [], confirmedBroadcastedRooms: [], diff --git a/src/utils/configs.js b/src/utils/configs.js index 0bf1cc6217..9eec998d0f 100644 --- a/src/utils/configs.js +++ b/src/utils/configs.js @@ -16,6 +16,7 @@ let isAdmin = false; "SENTRY_DSN", "GA_TRACKING_ID", "SHORTLINK_DOMAIN", + "IMMERS_SERVER", "BASE_ASSETS_PATH" ].forEach(x => { const el = document.querySelector(`meta[name='env:${x.toLowerCase()}']`); diff --git a/src/utils/immers.js b/src/utils/immers.js new file mode 100644 index 0000000000..8db4adcc55 --- /dev/null +++ b/src/utils/immers.js @@ -0,0 +1,253 @@ +import io from "socket.io-client"; +import configs from "./configs"; +import { fetchAvatar } from "./avatar-utils"; +const localImmer = configs.IMMERS_SERVER; +let homeImmer; +let place; +let token; + +export function getAvatarFromActor(actorObj) { + if (!actorObj.attachment) { + return null; + } + const attachments = Array.isArray(actorObj.attachment) ? actorObj.attachment : [actorObj.attachment]; + const avi = attachments.find(obj => obj.type === "Avatar"); + if (avi) { + return avi.url; + } + return null; +} + +export async function getObject(IRI) { + if (IRI.startsWith(localImmer) || IRI.startsWith(homeImmer)) { + const headers = { Accept: "application/activity+json" }; + if (token) { + headers.Authorization = `Bearer ${token}`; + } + const result = await window.fetch(IRI, { headers }); + if (!result.ok) { + throw new Error(`Object fetch error ${result.message}`); + } + return result.json(); + } else { + throw new Error("Object fetch proxy not implemented"); + } +} + +export async function getActor() { + const response = await window.fetch(`${homeImmer}/auth/me`, { + headers: { + Accept: "application/activity+json", + Authorization: `Bearer ${token}` + } + }); + if (!response.ok) { + return null; + } + return response.json(); +} + +export function postActivity(outbox, activity) { + return window.fetch(outbox, { + method: "POST", + headers: { + "Content-Type": "application/activity+json", + Authorization: `Bearer ${token}` + }, + body: JSON.stringify(activity) + }); +} + +export function updateProfile(actorObj, update) { + update.id = actorObj.id; + const activity = { + type: "Update", + actor: actorObj.id, + object: update, + to: actorObj.followers + }; + return postActivity(actorObj.outbox, activity); +} + +export function follow(actorObj, targetId) { + return postActivity(actorObj.outbox, { + type: "Follow", + actor: actorObj.id, + object: targetId, + to: targetId + }); +} + +export function arrive(actorObj) { + return postActivity(actorObj.outbox, { + type: "Arrive", + actor: actorObj.id, + target: place, + to: actorObj.followers + }); +} + +export function leave(actorObj) { + return postActivity(actorObj.outbox, { + type: "Leave", + actor: actorObj.id, + target: place, + to: actorObj.followers + }); +} + +export async function getFriends(actorObj) { + const response = await window.fetch(`${actorObj.id}/friends`, { + headers: { + Accept: "application/activity+json", + Authorization: `Bearer ${token}` + } + }); + if (!response.ok) { + throw new Error("Unable to fech friends"); + } + return response.json(); +} + +// perform oauth flow to get access token for local or remote user +export async function auth(store) { + const loc = new URL(window.location); + const hashParams = new URLSearchParams(loc.hash.substring(1)); + const hubUri = new URL(window.location); + hubUri.hash = ""; + place = await getObject(`${localImmer}/o/immer`); + place.url = hubUri; // include room id + + if (hashParams.has("access_token")) { + // record user's home server in case redirected during auth + let home; + try { + home = new URL(document.referrer); + home = `${home.protocol}//${home.host}`; + } catch (ignore) { + home = null; + } + store.update({ + immerCredentials: { + token: hashParams.get("access_token"), + home + } + }); + window.location.hash = ""; + } + + if (!store.state.immerCredentials.token) { + const redirect = new URL(`${localImmer}/auth/authorize`); + redirect.search = new URLSearchParams({ + client_id: place.id, + redirect_uri: hubUri, + response_type: "token" + }).toString(); + window.location = redirect; + return; + } else { + token = store.state.immerCredentials.token; + homeImmer = store.state.immerCredentials.home; + } +} +export async function initialize(store, scene, remountUI) { + // immers profile + const actorObj = await getActor(); + if (actorObj) { + const initialAvi = store.state.profile.avatarId; + store.update({ + profile: { + id: actorObj.id, + avatarId: getAvatarFromActor(actorObj) || initialAvi, + displayName: actorObj.name, + inbox: actorObj.inbox, + outbox: actorObj.outbox, + followers: actorObj.followers + }, + activity: { + hasChangedName: true + } + }); + } + const immerSocket = io(homeImmer, { + transportOptions: { + polling: { + extraHeaders: { + Authorization: `Bearer ${token}` + } + } + } + }); + let hasArrived; + immerSocket.on("connect", () => { + if (hasArrived) { + return; + } + hasArrived = true; + arrive(actorObj); + immerSocket.emit("entered", { + // prepare a leave activity to be fired on disconnect + outbox: actorObj.outbox, + authorization: `Bearer ${token}`, + leave: { + type: "Leave", + actor: actorObj.id, + target: window.location.href, + to: actorObj.followers + } + }); + }); + + // friends list + let friendsCol; + const updateFriends = async () => { + if (store.state.profile.id) { + const profile = store.state.profile; + friendsCol = await getFriends(profile); + remountUI({ friends: friendsCol.orderedItems }); + // update follow button for new friends + const players = window.APP.componentRegistry["player-info"]; + if (players) { + players.forEach(infoComp => { + if (friendsCol.orderedItems.some(act => act.actor.id === infoComp.data.immersId)) { + infoComp.el.addState("friend"); + } + }); + } + } + }; + updateFriends(); + immerSocket.on("friends-update", updateFriends); + + scene.addEventListener("avatar_updated", async () => { + const profile = store.state.profile; + const avatar = await fetchAvatar(profile.avatarId); + updateProfile(profile, { + name: profile.displayName, + attachment: [ + { + type: "Avatar", + content: profile.avatarId, + url: avatar.gltf_url + } + ] + }).catch(err => console.error("Error updating profile:", err.message)); + }); + + // entity interactions + scene.addEventListener("immers-id-changed", event => { + if (!friendsCol) { + return; + } + if (friendsCol.orderedItems.some(act => act.actor.id === event.detail)) { + event.target.addState("friend"); + } + }); + + scene.addEventListener("immers-follow", event => { + if (!event.detail) { + return; + } + follow(store.state.profile, event.detail).catch(err => console.err("Error sending follow request:", err.message)); + }); +} diff --git a/webpack.config.js b/webpack.config.js index a62bc5663f..59e5d556c6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -481,6 +481,7 @@ module.exports = async (env, argv) => { SENTRY_DSN: process.env.SENTRY_DSN, GA_TRACKING_ID: process.env.GA_TRACKING_ID, POSTGREST_SERVER: process.env.POSTGREST_SERVER, + IMMERS_SERVER: process.env.IMMERS_SERVER, APP_CONFIG: appConfig }) }) From d11b3c0d3fed484c6d7e615cdc6eb9fa01a98c4e Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Mon, 25 May 2020 11:21:28 -0500 Subject: [PATCH 017/167] fix home immer not identified correctly when tokens requested for already logged in users on trusted clients --- src/utils/immers.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 8db4adcc55..f79dc045c0 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -119,18 +119,11 @@ export async function auth(store) { place.url = hubUri; // include room id if (hashParams.has("access_token")) { - // record user's home server in case redirected during auth - let home; - try { - home = new URL(document.referrer); - home = `${home.protocol}//${home.host}`; - } catch (ignore) { - home = null; - } store.update({ immerCredentials: { token: hashParams.get("access_token"), - home + // record user's home server in case redirected during auth + home: hashParams.get("issuer") } }); window.location.hash = ""; From 5f759aae71863f39e17594fb543f6595aef48bd4 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Tue, 26 May 2020 17:44:55 -0500 Subject: [PATCH 018/167] fix profile entry panel to show one time per immer --- src/utils/immers.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index f79dc045c0..992a75cada 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -156,9 +156,6 @@ export async function initialize(store, scene, remountUI) { inbox: actorObj.inbox, outbox: actorObj.outbox, followers: actorObj.followers - }, - activity: { - hasChangedName: true } }); } @@ -224,7 +221,15 @@ export async function initialize(store, scene, remountUI) { url: avatar.gltf_url } ] - }).catch(err => console.error("Error updating profile:", err.message)); + }) + .then(() => { + store.update({ + activity: { + hasChangedName: true + } + }); + }) + .catch(err => console.error("Error updating profile:", err.message)); }); // entity interactions From 029e3c51eb2339df41b492a81666f42e8ff84cd4 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Tue, 26 May 2020 18:49:54 -0500 Subject: [PATCH 019/167] move immer credentials into store.state.credentials so hubs.link will sync them, resolve races in auth v. initalize and re-auth on bad/expired tokens --- src/storage/store.js | 15 ++-------- src/utils/immers.js | 71 +++++++++++++++++++++++++++----------------- 2 files changed, 47 insertions(+), 39 deletions(-) diff --git a/src/storage/store.js b/src/storage/store.js index 2d86b5b690..358d0434f6 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -37,16 +37,9 @@ export const SCHEMA = { additionalProperties: false, properties: { token: { type: ["null", "string"] }, - email: { type: ["null", "string"] } - } - }, - - immerCredentials: { - type: "object", - additionalProperties: false, - properties: { - token: { type: ["null", "string"] }, - home: { type: ["null", "string"] } + email: { type: ["null", "string"] }, + immerToken: { type: ["null", "string"] }, + immerHome: { type: ["null", "string"] } } }, @@ -167,7 +160,6 @@ export const SCHEMA = { properties: { profile: { $ref: "#/definitions/profile" }, credentials: { $ref: "#/definitions/credentials" }, - immerCredentials: { $ref: "#/definitions/immerCredentials" }, activity: { $ref: "#/definitions/activity" }, settings: { $ref: "#/definitions/settings" }, preferences: { $ref: "#/definitions/preferences" }, @@ -201,7 +193,6 @@ export default class Store extends EventTarget { activity: {}, settings: {}, credentials: {}, - immerCredentials: {}, profile: {}, confirmedDiscordRooms: [], confirmedBroadcastedRooms: [], diff --git a/src/utils/immers.js b/src/utils/immers.js index 992a75cada..1e4858dc6b 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -2,6 +2,13 @@ import io from "socket.io-client"; import configs from "./configs"; import { fetchAvatar } from "./avatar-utils"; const localImmer = configs.IMMERS_SERVER; +// avoid race between auth and initialize code +let resolveAuth; +let rejectAuth; +const authPromise = new Promise((resolve, reject) => { + resolveAuth = resolve; + rejectAuth = reject; +}); let homeImmer; let place; let token; @@ -42,7 +49,7 @@ export async function getActor() { } }); if (!response.ok) { - return null; + throw new Error(`Error fetching actor ${response.status} ${response.statusText}`); } return response.json(); } @@ -119,17 +126,16 @@ export async function auth(store) { place.url = hubUri; // include room id if (hashParams.has("access_token")) { - store.update({ - immerCredentials: { - token: hashParams.get("access_token"), - // record user's home server in case redirected during auth - home: hashParams.get("issuer") - } - }); + // not safe to update store here, will be saved later in initialize() + token = hashParams.get("access_token"); + homeImmer = hashParams.get("issuer"); window.location.hash = ""; + } else { + token = store.state.credentials.immerToken; + homeImmer = store.state.credentials.homeImmer; } - if (!store.state.immerCredentials.token) { + const redirectToAuth = () => { const redirect = new URL(`${localImmer}/auth/authorize`); redirect.search = new URLSearchParams({ client_id: place.id, @@ -137,28 +143,39 @@ export async function auth(store) { response_type: "token" }).toString(); window.location = redirect; - return; - } else { - token = store.state.immerCredentials.token; - homeImmer = store.state.immerCredentials.home; + }; + + // will cause re-auth when expired/invalid tokens are rejected + authPromise.catch(redirectToAuth); + // check token validity & get actor object + if (!token) { + return redirectToAuth(); + } + try { + resolveAuth(await getActor()); + } catch (err) { + rejectAuth(err); } } export async function initialize(store, scene, remountUI) { // immers profile - const actorObj = await getActor(); - if (actorObj) { - const initialAvi = store.state.profile.avatarId; - store.update({ - profile: { - id: actorObj.id, - avatarId: getAvatarFromActor(actorObj) || initialAvi, - displayName: actorObj.name, - inbox: actorObj.inbox, - outbox: actorObj.outbox, - followers: actorObj.followers - } - }); - } + const actorObj = await authPromise; + const initialAvi = store.state.profile.avatarId; + store.update({ + profile: { + id: actorObj.id, + avatarId: getAvatarFromActor(actorObj) || initialAvi, + displayName: actorObj.name, + inbox: actorObj.inbox, + outbox: actorObj.outbox, + followers: actorObj.followers + }, + credentials: { + immerToken: token, + // record user's home server in case redirected during auth + immerHome: homeImmer + } + }); const immerSocket = io(homeImmer, { transportOptions: { polling: { From 8740b3236ac0c439e1ce73e09c4e9e88588069ad Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Tue, 26 May 2020 22:23:38 -0500 Subject: [PATCH 020/167] update ui-root to avoid errors with early state updates; move initialize back to safe one-time only event handler now that race with hub channel join no longer an issue --- src/hub.js | 3 ++- src/react-components/ui-root.js | 3 ++- src/utils/immers.js | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/hub.js b/src/hub.js index c3b1d5645c..e30d497937 100644 --- a/src/hub.js +++ b/src/hub.js @@ -680,7 +680,6 @@ function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data) connectToScene(); } - immers.initialize(store, scene, remountUI); } async function runBotMode(scene, entryManager) { @@ -1603,4 +1602,6 @@ document.addEventListener("DOMContentLoaded", async () => { authChannel.setSocket(socket); linkChannel.setSocket(socket); + + immers.initialize(store, scene, remountUI); }); diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 59ca13f4da..d9ade53010 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -217,7 +217,7 @@ class UIRoot extends Component { objectSrc: "", isObjectListExpanded: false, isPresenceListExpanded: false, - hasUnreadFriendUpdate: false, + hasUnreadFriendUpdate: false }; constructor(props) { @@ -928,6 +928,7 @@ class UIRoot extends Component { }; onStoreChanged = () => { + if (!this.props.hub) return; const broadcastedRoomConfirmed = this.props.store.state.confirmedBroadcastedRooms.includes(this.props.hub.hub_id); if (broadcastedRoomConfirmed !== this.state.broadcastTipDismissed) { this.setState({ broadcastTipDismissed: broadcastedRoomConfirmed }); diff --git a/src/utils/immers.js b/src/utils/immers.js index 1e4858dc6b..4ca0322850 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -2,6 +2,7 @@ import io from "socket.io-client"; import configs from "./configs"; import { fetchAvatar } from "./avatar-utils"; const localImmer = configs.IMMERS_SERVER; +console.log("immers.space client v0.0.5"); // avoid race between auth and initialize code let resolveAuth; let rejectAuth; From cfb2f74bdd50ff646500b89e37cd0acd0b4bb29a Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Tue, 26 May 2020 23:15:49 -0500 Subject: [PATCH 021/167] fix: tokens not getting reused --- src/utils/immers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 4ca0322850..a35fcd74f4 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -133,7 +133,7 @@ export async function auth(store) { window.location.hash = ""; } else { token = store.state.credentials.immerToken; - homeImmer = store.state.credentials.homeImmer; + homeImmer = store.state.credentials.immerHome; } const redirectToAuth = () => { From 75741179e117f4077e378e10766e7ef4c5bab7d8 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 28 May 2020 21:01:59 -0500 Subject: [PATCH 022/167] bump client version to match server --- src/utils/immers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index a35fcd74f4..f2d605deaf 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -2,7 +2,7 @@ import io from "socket.io-client"; import configs from "./configs"; import { fetchAvatar } from "./avatar-utils"; const localImmer = configs.IMMERS_SERVER; -console.log("immers.space client v0.0.5"); +console.log("immers.space client v0.1.0"); // avoid race between auth and initialize code let resolveAuth; let rejectAuth; From 1a6a34894d5bfce915841ddc27f0a6534c967bcb Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Wed, 28 Oct 2020 22:27:05 -0500 Subject: [PATCH 023/167] api for communicating monetization status to the A-Frame scene --- src/utils/immers.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/utils/immers.js b/src/utils/immers.js index f2d605deaf..c27bc4871e 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -13,6 +13,12 @@ const authPromise = new Promise((resolve, reject) => { let homeImmer; let place; let token; +let hubScene; +const monetization = { + amountPaid: 0, + currency: undefined, + state: undefined +}; export function getAvatarFromActor(actorObj) { if (!actorObj.attachment) { @@ -158,7 +164,30 @@ export async function auth(store) { rejectAuth(err); } } + +function onMonetizationStart() { + monetization.state = "started"; + hubScene.emit("monetizationstarted"); +} +function onMonetizationStop() { + monetization.state = "stopped"; + hubScene.emit("monetizationstopped"); +} +function onMonetizationProgress(event) { + const amount = Number.parseInt(event.detail.amount) * Math.pow(10, -event.detail.assetScale); + if (amount) { + monetization.amountPaid += amount; + monetization.currency = event.detail.assetCode; + hubScene.emit("monetizationprogress", { + amount, + totalAmount: monetization.amountPaid, + currency: monetization.currency + }); + } +} + export async function initialize(store, scene, remountUI) { + hubScene = scene; // immers profile const actorObj = await authPromise; const initialAvi = store.state.profile.avatarId; @@ -266,4 +295,14 @@ export async function initialize(store, scene, remountUI) { } follow(store.state.profile, event.detail).catch(err => console.err("Error sending follow request:", err.message)); }); + + // monetization + if (document.monetization) { + if (document.monetization.state === "started") { + onMonetizationStart(); + } + document.monetization.addEventListener("monetizationstart", onMonetizationStart); + document.monetization.addEventListener("monetizationstop", onMonetizationStop); + document.monetization.addEventListener("monetizationprogress", onMonetizationProgress); + } } From b6dc5307900b71f288164b68005846ca7f7cb293 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 29 Oct 2020 20:28:59 -0500 Subject: [PATCH 024/167] delay monetization scene events until loading finished to simplify api use --- src/utils/immers.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index c27bc4871e..51d16d92a8 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -185,6 +185,14 @@ function onMonetizationProgress(event) { }); } } +function setupMonetization() { + if (document.monetization.state === "started") { + onMonetizationStart(); + } + document.monetization.addEventListener("monetizationstart", onMonetizationStart); + document.monetization.addEventListener("monetizationstop", onMonetizationStop); + document.monetization.addEventListener("monetizationprogress", onMonetizationProgress); +} export async function initialize(store, scene, remountUI) { hubScene = scene; @@ -296,13 +304,11 @@ export async function initialize(store, scene, remountUI) { follow(store.state.profile, event.detail).catch(err => console.err("Error sending follow request:", err.message)); }); - // monetization - if (document.monetization) { - if (document.monetization.state === "started") { - onMonetizationStart(); - } - document.monetization.addEventListener("monetizationstart", onMonetizationStart); - document.monetization.addEventListener("monetizationstop", onMonetizationStop); - document.monetization.addEventListener("monetizationprogress", onMonetizationProgress); + // wait until scene is fully loaded to trigger monetization events so creators don't + // have to worry about whether entities are loaded + if (document.monetization && hubScene.is("loaded")) { + setupMonetization(); + } else if (document.monetization) { + hubScene.addEventListener("loading_finished", () => setupMonetization(), { once: true }); } } From c8a3118f33702411d82178414316cdf52c3bc4d7 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 31 Oct 2020 09:49:31 -0500 Subject: [PATCH 025/167] add system for adding monetization logic to elements from spoke scene --- src/components/monetization-visible.js | 46 ++++++++++++++++++++++++++ src/hub.html | 1 + src/hub.js | 1 + 3 files changed, 48 insertions(+) create mode 100644 src/components/monetization-visible.js diff --git a/src/components/monetization-visible.js b/src/components/monetization-visible.js new file mode 100644 index 0000000000..c468ed76ad --- /dev/null +++ b/src/components/monetization-visible.js @@ -0,0 +1,46 @@ +AFRAME.registerSystem("monetization-visible-system", { + init() { + this.mo = new MutationObserver(this.onMutation); + this.mo.observe(this.el, { subtree: true, childList: true }); + }, + onMutation(records) { + const mv = "monetization-visible"; + for (const record of records) { + for (const node of record.addedNodes) { + if (!node.nodeType === document.ELEMENT_NODE) continue; + if (node.classList.contains(mv)) node.setAttribute(mv, {}); + for (const descendant of node.querySelectorAll(`.${mv}`)) { + descendant.setAttribute(mv, {}); + } + } + } + } +}); + +AFRAME.registerComponent("monetization-visible", { + schema: { + monetized: { type: "boolean", default: false } + }, + init() { + this.monetize = this.monetize.bind(this); + this.unmonetize = this.unmonetize.bind(this); + }, + play() { + this.el.sceneEl.addEventListener("monetizationstarted", this.monetize); + this.el.sceneEl.addEventListener("monetizationstopped", this.unmonetize); + this.el.setAttribute("visible", this.monetized); + }, + pause() { + this.el.sceneEl.removeEventListener("monetizationstarted", this.monetize); + this.el.sceneEl.removeEventListener("monetizationstopped", this.unmonetize); + this.el.setAttribute("visible", this.monetized); + }, + monetize() { + this.monetized = true; + this.el.setAttribute("visible", true); + }, + unmonetize() { + this.monetized = false; + this.el.setAttribute("visible", false); + } +}); diff --git a/src/hub.html b/src/hub.html index f335b0ff10..51bd51340a 100644 --- a/src/hub.html +++ b/src/hub.html @@ -54,6 +54,7 @@ environment-map set-max-resolution light="defaultLightsEnabled: false" + monetization-visible > diff --git a/src/hub.js b/src/hub.js index cf15a2757a..970024e20c 100644 --- a/src/hub.js +++ b/src/hub.js @@ -217,6 +217,7 @@ import detectConcurrentLoad from "./utils/concurrent-load-detector"; import qsTruthy from "./utils/qs_truthy"; import "./components/immers-follow-button"; +import "./components/monetization-visible"; const PHOENIX_RELIABLE_NAF = "phx-reliable"; NAF.options.firstSyncSource = PHOENIX_RELIABLE_NAF; From cd9d2f6931750c3109e10b45412ea8739bdc8e6b Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 31 Oct 2020 14:34:00 -0500 Subject: [PATCH 026/167] network monetization status and refactor monetization-visible system to match --- src/components/monetization-visible.js | 55 ++++++++++++++++---------- src/components/player-info.js | 21 +++++++++- src/hub.html | 1 - src/utils/immers.js | 6 ++- 4 files changed, 59 insertions(+), 24 deletions(-) diff --git a/src/components/monetization-visible.js b/src/components/monetization-visible.js index c468ed76ad..1cf925080d 100644 --- a/src/components/monetization-visible.js +++ b/src/components/monetization-visible.js @@ -1,8 +1,28 @@ -AFRAME.registerSystem("monetization-visible-system", { +const players = {}; + +AFRAME.registerSystem("monetization-visible", { init() { this.mo = new MutationObserver(this.onMutation); this.mo.observe(this.el, { subtree: true, childList: true }); + this.onMonetizationChange = this.onMonetizationChange.bind(this); + this.entities = []; + this.el.addEventListener("immers-player-monetization", this.onMonetizationChange); + }, + play() { + this.el.addEventListener("immers-player-monetization", this.onMonetizationChange); + }, + pause() { + this.el.removeEventListener("immers-player-monetization", this.onMonetizationChange); }, + registerMe(el) { + this.entities.push(el); + }, + + unregisterMe(el) { + const index = this.entities.indexOf(el); + this.entities.splice(index, 1); + }, + // inject component into spoke scene entities (spoke saves objet names as classes) onMutation(records) { const mv = "monetization-visible"; for (const record of records) { @@ -14,6 +34,13 @@ AFRAME.registerSystem("monetization-visible-system", { } } } + }, + onMonetizationChange(event) { + players[event.detail.immersId] = event.detail.monetized; + const numMonetized = Object.values(players).reduce((a, b) => a + b, 0); + for (const entity of this.entities) { + entity.components["monetization-visible"].checkMonetization(numMonetized); + } } }); @@ -21,26 +48,14 @@ AFRAME.registerComponent("monetization-visible", { schema: { monetized: { type: "boolean", default: false } }, - init() { - this.monetize = this.monetize.bind(this); - this.unmonetize = this.unmonetize.bind(this); + checkMonetization(count) { + this.el.setAttribute("visible", count > 0); }, - play() { - this.el.sceneEl.addEventListener("monetizationstarted", this.monetize); - this.el.sceneEl.addEventListener("monetizationstopped", this.unmonetize); - this.el.setAttribute("visible", this.monetized); - }, - pause() { - this.el.sceneEl.removeEventListener("monetizationstarted", this.monetize); - this.el.sceneEl.removeEventListener("monetizationstopped", this.unmonetize); - this.el.setAttribute("visible", this.monetized); - }, - monetize() { - this.monetized = true; - this.el.setAttribute("visible", true); - }, - unmonetize() { - this.monetized = false; + init() { this.el.setAttribute("visible", false); + this.system.registerMe(this.el); + }, + remove() { + this.system.unregisterMe(this.el); } }); diff --git a/src/components/player-info.js b/src/components/player-info.js index 72c174c58b..15d61cf436 100644 --- a/src/components/player-info.js +++ b/src/components/player-info.js @@ -37,7 +37,8 @@ AFRAME.registerComponent("player-info", { avatarSrc: { type: "string" }, avatarType: { type: "string", default: AVATAR_TYPES.SKINNABLE }, muted: { default: false }, - immersId: { type: "string" } + immersId: { type: "string" }, + monetized: { type: "boolean" } }, init() { this.displayName = null; @@ -67,6 +68,10 @@ AFRAME.registerComponent("player-info", { registerComponentInstance(this, "player-info"); }, remove() { + this.el.sceneEl.emit("immers-player-monetization", { + monetized: false, + immersId: this.data.immersId + }); deregisterComponentInstance(this, "player-info"); }, play() { @@ -105,6 +110,20 @@ AFRAME.registerComponent("player-info", { this.applyProperties(); if (this.data.immersId !== oldData.immersId) { this.el.emit("immers-id-changed", this.data.immersId); + this.el.sceneEl.emit("immers-player-monetization", { + monetized: false, + immersId: oldData.immersId + }); + this.el.sceneEl.emit("immers-player-monetization", { + monetized: this.data.monetized, + immersId: this.data.immersId + }); + } + if (this.data.monetized !== oldData.monetized) { + this.el.sceneEl.emit("immers-player-monetization", { + monetized: this.data.monetized, + immersId: this.data.immersId + }); } }, updateDisplayName(e) { diff --git a/src/hub.html b/src/hub.html index 51bd51340a..f335b0ff10 100644 --- a/src/hub.html +++ b/src/hub.html @@ -54,7 +54,6 @@ environment-map set-max-resolution light="defaultLightsEnabled: false" - monetization-visible > diff --git a/src/utils/immers.js b/src/utils/immers.js index 51d16d92a8..37bfb05594 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -14,6 +14,7 @@ let homeImmer; let place; let token; let hubScene; +let localPlayer; const monetization = { amountPaid: 0, currency: undefined, @@ -167,11 +168,11 @@ export async function auth(store) { function onMonetizationStart() { monetization.state = "started"; - hubScene.emit("monetizationstarted"); + localPlayer.setAttribute("player-info", { monetized: true }); } function onMonetizationStop() { monetization.state = "stopped"; - hubScene.emit("monetizationstopped"); + localPlayer.setAttribute("player-info", { monetized: false }); } function onMonetizationProgress(event) { const amount = Number.parseInt(event.detail.amount) * Math.pow(10, -event.detail.assetScale); @@ -196,6 +197,7 @@ function setupMonetization() { export async function initialize(store, scene, remountUI) { hubScene = scene; + localPlayer = document.getElementById("avatar-rig"); // immers profile const actorObj = await authPromise; const initialAvi = store.state.profile.avatarId; From 3acfc002e0261d89fc2590d54d0c3020a154a5d5 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 1 Nov 2020 10:28:30 -0600 Subject: [PATCH 027/167] bump version --- src/utils/immers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 37bfb05594..7a35ccd253 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -2,7 +2,7 @@ import io from "socket.io-client"; import configs from "./configs"; import { fetchAvatar } from "./avatar-utils"; const localImmer = configs.IMMERS_SERVER; -console.log("immers.space client v0.1.0"); +console.log("immers.space client v0.1.1"); // avoid race between auth and initialize code let resolveAuth; let rejectAuth; From 68a5740ba40868ac9e3eef04b259b77fe4956052 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 1 Nov 2020 11:23:27 -0600 Subject: [PATCH 028/167] improve code structure and do umentation --- src/components/monetization-visible.js | 13 ++++- src/utils/immers.js | 43 +-------------- src/utils/immers/monetization.js | 76 ++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 42 deletions(-) create mode 100644 src/utils/immers/monetization.js diff --git a/src/components/monetization-visible.js b/src/components/monetization-visible.js index 1cf925080d..a06b59d209 100644 --- a/src/components/monetization-visible.js +++ b/src/components/monetization-visible.js @@ -1,3 +1,14 @@ +/* Simple use case for room monetization status. Certain Spoke entities are + * shown or hiden based on whether anyone in the room is monetized. + * + * To use without additional client customisation, add entities or groups in Spoke + * with the name "monetization-visible", and this + * component will attach to it and make it invisible unless someone in the + * room is monetized. + * + * For elements created via custom client extension, + * give them the "monetization-visible" component to enable to same behavior. + */ const players = {}; AFRAME.registerSystem("monetization-visible", { @@ -22,7 +33,7 @@ AFRAME.registerSystem("monetization-visible", { const index = this.entities.indexOf(el); this.entities.splice(index, 1); }, - // inject component into spoke scene entities (spoke saves objet names as classes) + // inject component into spoke scene entities (spoke saves names as classes) onMutation(records) { const mv = "monetization-visible"; for (const record of records) { diff --git a/src/utils/immers.js b/src/utils/immers.js index 7a35ccd253..1fddab2d6c 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -1,6 +1,7 @@ import io from "socket.io-client"; import configs from "./configs"; import { fetchAvatar } from "./avatar-utils"; +import { setupMonetization } from "./immers/monetization"; const localImmer = configs.IMMERS_SERVER; console.log("immers.space client v0.1.1"); // avoid race between auth and initialize code @@ -15,11 +16,6 @@ let place; let token; let hubScene; let localPlayer; -const monetization = { - amountPaid: 0, - currency: undefined, - state: undefined -}; export function getAvatarFromActor(actorObj) { if (!actorObj.attachment) { @@ -166,35 +162,6 @@ export async function auth(store) { } } -function onMonetizationStart() { - monetization.state = "started"; - localPlayer.setAttribute("player-info", { monetized: true }); -} -function onMonetizationStop() { - monetization.state = "stopped"; - localPlayer.setAttribute("player-info", { monetized: false }); -} -function onMonetizationProgress(event) { - const amount = Number.parseInt(event.detail.amount) * Math.pow(10, -event.detail.assetScale); - if (amount) { - monetization.amountPaid += amount; - monetization.currency = event.detail.assetCode; - hubScene.emit("monetizationprogress", { - amount, - totalAmount: monetization.amountPaid, - currency: monetization.currency - }); - } -} -function setupMonetization() { - if (document.monetization.state === "started") { - onMonetizationStart(); - } - document.monetization.addEventListener("monetizationstart", onMonetizationStart); - document.monetization.addEventListener("monetizationstop", onMonetizationStop); - document.monetization.addEventListener("monetizationprogress", onMonetizationProgress); -} - export async function initialize(store, scene, remountUI) { hubScene = scene; localPlayer = document.getElementById("avatar-rig"); @@ -306,11 +273,5 @@ export async function initialize(store, scene, remountUI) { follow(store.state.profile, event.detail).catch(err => console.err("Error sending follow request:", err.message)); }); - // wait until scene is fully loaded to trigger monetization events so creators don't - // have to worry about whether entities are loaded - if (document.monetization && hubScene.is("loaded")) { - setupMonetization(); - } else if (document.monetization) { - hubScene.addEventListener("loading_finished", () => setupMonetization(), { once: true }); - } + setupMonetization(hubScene, localPlayer); } diff --git a/src/utils/immers/monetization.js b/src/utils/immers/monetization.js new file mode 100644 index 0000000000..a823ce189a --- /dev/null +++ b/src/utils/immers/monetization.js @@ -0,0 +1,76 @@ +/* Handles monetization feature-checking, api, and initialization race. + * Immers creators can listen for events on the scene that are guaranteed to + * occur after initial scene and entity load. Monetization status is synced to + * the room currently via player-info. This has two limitations, 1) easy to spoof + * and 2) doesn't sync while players are still in lobby. A better implemenation + * would be to extend the reticulum server to support this over hubChannel. + * + * Events (emitted from scene element): + * immers-monetization-started - local user monetization began/resumed + * immers-monetization-stopped - local user monetization ceased + * immers-monetization-progress - local user micropayment received. + * detail: + * amount: Number, amount received on this transation + * totalAmoint: Number, amount received so far during this session + * currency: String, currency of the transation + */ +const monetization = { + amountPaid: 0, + currency: undefined, + state: undefined +}; +let localPlayer; +let hubScene; + +// sync player's monetization status with room via player-info component +function onMonetizationStart() { + monetization.state = "started"; + localPlayer.setAttribute("player-info", { monetized: true }); + hubScene.emit("immers-monetization-started"); +} +function onMonetizationStop() { + monetization.state = "stopped"; + localPlayer.setAttribute("player-info", { monetized: false }); + hubScene.emit("immers-monetization-stopped"); +} + +// tallies total amount paid during curent session +// (no cross-session tracking is permitted) +function onMonetizationProgress(event) { + const amount = Number.parseInt(event.detail.amount) * Math.pow(10, -event.detail.assetScale); + if (amount) { + monetization.amountPaid += amount; + monetization.currency = event.detail.assetCode; + hubScene.emit("immers-monetization-progress", { + amount, + totalAmount: monetization.amountPaid, + currency: monetization.currency + }); + } +} + +// checks availability and adds event handlers +function onSceneLoaded() { + if (!document.monetization) { + hubScene.emit("immers-monetization-unavailable"); + return; + } + if (document.monetization.state === "started") { + onMonetizationStart(); + } + document.monetization.addEventListener("monetizationstart", onMonetizationStart); + document.monetization.addEventListener("monetizationstop", onMonetizationStop); + document.monetization.addEventListener("monetizationprogress", onMonetizationProgress); +} + +// wait until scene is fully loaded to trigger monetization events so creators don't +// have to worry about whether entities are loaded +export function setupMonetization(scene, player) { + hubScene = scene; + localPlayer = player; + if (hubScene.is("loaded")) { + onSceneLoaded(); + } else { + hubScene.addEventListener("loading_finished", onSceneLoaded, { once: true }); + } +} From c94ed673222f3c438bfc4ea8b442fbe8740c264c Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 13 Nov 2020 16:01:46 -0600 Subject: [PATCH 029/167] move auth after hubChannel connection and pass hub entry code to immers auth request for use in VR login instructions --- src/hub.js | 3 +-- src/utils/immers.js | 8 +++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/hub.js b/src/hub.js index 970024e20c..efbf14f56c 100644 --- a/src/hub.js +++ b/src/hub.js @@ -183,8 +183,6 @@ if (isEmbed && !qs.get("embed_token")) { throw new Error("no embed token"); } -immers.auth(store); - THREE.Object3D.DefaultMatrixAutoUpdate = false; import "./components/owned-object-limiter"; @@ -498,6 +496,7 @@ function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data) } } + immers.auth(store, hub); console.log(`Janus host: ${hub.host}:${hub.port}`); remountUI({ diff --git a/src/utils/immers.js b/src/utils/immers.js index 1fddab2d6c..051ff44b34 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -3,7 +3,7 @@ import configs from "./configs"; import { fetchAvatar } from "./avatar-utils"; import { setupMonetization } from "./immers/monetization"; const localImmer = configs.IMMERS_SERVER; -console.log("immers.space client v0.1.1"); +console.log("immers.space client v0.2.0"); // avoid race between auth and initialize code let resolveAuth; let rejectAuth; @@ -121,7 +121,7 @@ export async function getFriends(actorObj) { } // perform oauth flow to get access token for local or remote user -export async function auth(store) { +export async function auth(store, hub) { const loc = new URL(window.location); const hashParams = new URLSearchParams(loc.hash.substring(1)); const hubUri = new URL(window.location); @@ -144,7 +144,9 @@ export async function auth(store) { redirect.search = new URLSearchParams({ client_id: place.id, redirect_uri: hubUri, - response_type: "token" + response_type: "token", + shortlink_domain: configs.SHORTLINK_DOMAIN, + entry_code: hub.entry_code }).toString(); window.location = redirect; }; From 1ea964a575790e78863e8c3b2f8bc62ef3afc2d6 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Mon, 7 Dec 2020 21:39:51 -0600 Subject: [PATCH 030/167] switch to web monetization polyfill for support in CSP contexts --- package-lock.json | 33 ++++++++++++++++++-------------- package.json | 3 ++- src/utils/immers/monetization.js | 7 ++----- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index ecb766d7cc..bab8f4f32d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7264,12 +7264,6 @@ "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "dev": true }, - "bowser": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.9.0.tgz", - "integrity": "sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA==", - "dev": true - }, "boxen": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", @@ -12929,9 +12923,9 @@ "requires": { "@types/debug": "^4.1.5", "@types/events": "^3.0.0", - "awaitqueue": "^2.2.3", + "awaitqueue": "^2.3.3", "bowser": "^2.11.0", - "debug": "^4.1.1", + "debug": "^4.3.1", "events": "^3.2.0", "h264-profile-level-id": "^1.0.1", "sdp-transform": "^2.14.0", @@ -12944,6 +12938,12 @@ "integrity": "sha512-vKY8hHHt1FT05UBxaTKWoA8+A3APnyzcOO1UBY5wQ7ENzXCFi3Yy4wSKsqrO0ncDD/5CI6IZDN1nVZQKziWIqg==", "dev": true }, + "bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "dev": true + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -12954,9 +12954,9 @@ } }, "events": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", - "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", "dev": true }, "has-flag": { @@ -12972,9 +12972,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -19446,6 +19446,11 @@ "defaults": "^1.0.3" } }, + "web-monetization-polyfill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/web-monetization-polyfill/-/web-monetization-polyfill-1.0.0.tgz", + "integrity": "sha512-lNEkyOtXXdSppvrfTes2/x5LyTCxe7X33at559QC+UM8lHWmEK+6i00iSr78geaM7KCw3paXH16ZR2uFpLEXbw==" + }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/package.json b/package.json index fbbe748705..17ea4a337a 100644 --- a/package.json +++ b/package.json @@ -101,8 +101,8 @@ "react-router": "^5.1.2", "react-router-dom": "^5.1.2", "screenfull": "^4.0.1", - "socket.io-client": "^2.3.0", "semver": "^7.3.2", + "socket.io-client": "^2.3.0", "three": "github:mozillareality/three.js#hubs/master", "three-ammo": "^1.0.11", "three-bmfont-text": "github:mozillareality/three-bmfont-text#hubs/master", @@ -110,6 +110,7 @@ "three-pathfinding": "github:MozillaReality/three-pathfinding#hubs/master2", "three-to-ammo": "github:infinitelee/three-to-ammo", "uuid": "^3.2.1", + "web-monetization-polyfill": "^1.0.0", "webrtc-adapter": "^6.0.2", "zip-loader": "^1.1.0" }, diff --git a/src/utils/immers/monetization.js b/src/utils/immers/monetization.js index a823ce189a..bf7a511de8 100644 --- a/src/utils/immers/monetization.js +++ b/src/utils/immers/monetization.js @@ -14,6 +14,8 @@ * totalAmoint: Number, amount received so far during this session * currency: String, currency of the transation */ +import "web-monetization-polyfill"; + const monetization = { amountPaid: 0, currency: undefined, @@ -49,12 +51,7 @@ function onMonetizationProgress(event) { } } -// checks availability and adds event handlers function onSceneLoaded() { - if (!document.monetization) { - hubScene.emit("immers-monetization-unavailable"); - return; - } if (document.monetization.state === "started") { onMonetizationStart(); } From 425d59cdcd79455d3f94c6f5c63a9e708a8d0bde Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Mon, 7 Dec 2020 21:56:45 -0600 Subject: [PATCH 031/167] prevent hubs from displaying error messages from interrupting scene load when redirecting to authorization --- src/utils/immers.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utils/immers.js b/src/utils/immers.js index 051ff44b34..e740587fc4 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -148,6 +148,12 @@ export async function auth(store, hub) { shortlink_domain: configs.SHORTLINK_DOMAIN, entry_code: hub.entry_code }).toString(); + // hide error messages caused by interrupting loading to redirect + try { + document.getElementById("ui-root").style.display = "none"; + } catch (ignore) { + /* ignore */ + } window.location = redirect; }; From 20b8eb3c1b43cf822751b7530e00b0bab309cf0c Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Mon, 7 Dec 2020 22:09:40 -0600 Subject: [PATCH 032/167] revert changes related to passing room link info to now-removed VR instructions in immer login --- src/hub.js | 3 ++- src/utils/immers.js | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/hub.js b/src/hub.js index efbf14f56c..970024e20c 100644 --- a/src/hub.js +++ b/src/hub.js @@ -183,6 +183,8 @@ if (isEmbed && !qs.get("embed_token")) { throw new Error("no embed token"); } +immers.auth(store); + THREE.Object3D.DefaultMatrixAutoUpdate = false; import "./components/owned-object-limiter"; @@ -496,7 +498,6 @@ function handleHubChannelJoined(entryManager, hubChannel, messageDispatch, data) } } - immers.auth(store, hub); console.log(`Janus host: ${hub.host}:${hub.port}`); remountUI({ diff --git a/src/utils/immers.js b/src/utils/immers.js index e740587fc4..b6cf7c335f 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -121,7 +121,7 @@ export async function getFriends(actorObj) { } // perform oauth flow to get access token for local or remote user -export async function auth(store, hub) { +export async function auth(store) { const loc = new URL(window.location); const hashParams = new URLSearchParams(loc.hash.substring(1)); const hubUri = new URL(window.location); @@ -144,9 +144,7 @@ export async function auth(store, hub) { redirect.search = new URLSearchParams({ client_id: place.id, redirect_uri: hubUri, - response_type: "token", - shortlink_domain: configs.SHORTLINK_DOMAIN, - entry_code: hub.entry_code + response_type: "token" }).toString(); // hide error messages caused by interrupting loading to redirect try { From c14398f4fac8f627ee80445e59e3446cd844212e Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Mon, 7 Dec 2020 22:43:02 -0600 Subject: [PATCH 033/167] temporarily disable all reticulum sign in buttons by blanking out the translation text --- src/assets/locales/en.json | 2 +- src/assets/locales/zh.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index f2bb02bc9e..09454e0dc5 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -42,7 +42,7 @@ "sign-in.tweet": "You'll need to sign in to send tweets.", "sign-in.tweet-complete": "You are now signed in.", "sign-in.as": "Signed in as", - "sign-in.in": "Sign In", + "sign-in.in": " ", "sign-in.out": "Sign Out", "room-settings.apply": "Apply", "room-settings.name-subtitle": "Room Name", diff --git a/src/assets/locales/zh.json b/src/assets/locales/zh.json index 85181810fe..2f133bdd40 100644 --- a/src/assets/locales/zh.json +++ b/src/assets/locales/zh.json @@ -42,7 +42,7 @@ "sign-in.tweet": "只有登陆才可以发送信息。", "sign-in.tweet-complete": "你已经登陆。", "sign-in.as": "登录为", - "sign-in.in": "登录", + "sign-in.in": " ", "sign-in.out": "退出", "room-settings.apply": "递交", "room-settings.name-subtitle": "房间名称", From 244ae1eafe1d7b3d7bc8a23cd93b5e69ad4bb3fb Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 11 Dec 2020 19:08:55 -0600 Subject: [PATCH 034/167] add users handle to friends list link queries and auth requests --- src/hub.js | 2 +- src/react-components/presence-list.js | 38 ++++++++++++++++++++------- src/react-components/ui-root.js | 2 ++ src/utils/immers.js | 6 ++++- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/hub.js b/src/hub.js index 970024e20c..a85b07fd9d 100644 --- a/src/hub.js +++ b/src/hub.js @@ -895,7 +895,7 @@ document.addEventListener("DOMContentLoaded", async () => { performConditionalSignIn, embed: isEmbed, showPreload: isEmbed, - showSignInDialog: !store.state.profile.handle + showSignInDialog: false }); entryManager.performConditionalSignIn = performConditionalSignIn; entryManager.init(); diff --git a/src/react-components/presence-list.js b/src/react-components/presence-list.js index ddec8c988f..4696369384 100644 --- a/src/react-components/presence-list.js +++ b/src/react-components/presence-list.js @@ -66,6 +66,7 @@ export default class PresenceList extends Component { presences: PropTypes.object, friends: PropTypes.array, friendsUpdated: PropTypes.bool, + handle: PropTypes.string, history: PropTypes.object, sessionId: PropTypes.string, signedIn: PropTypes.bool, @@ -178,14 +179,29 @@ export default class PresenceList extends Component { ); }; - domForFriend = (locActivity) => { - const profile = locActivity.actor - const place = locActivity.target + domForFriend = locActivity => { + const profile = locActivity.actor; + const place = locActivity.target; + let placeUrl = place ? place.url : undefined; + // inject user handle into desintation url so they don't have to type it + if (placeUrl) { + try { + const url = new URL(placeUrl); + const search = new URLSearchParams(url.search); + search.set("me", this.props.handle); + url.search = search.toString(); + placeUrl = url.toString(); + } catch (ignore) { + /* if fail, leave original url unchanged */ + } + } return (
- + + +
- {locActivity.type === 'Arrive' ? ( - Online at {place.name} - ) : ({locActivity.type === 'Leave' ? 'Offline' : ''})} + {locActivity.type === "Arrive" ? ( + + Online at {place.name} + + ) : ( + {locActivity.type === "Leave" ? "Offline" : ""} + )}
@@ -245,7 +265,7 @@ export default class PresenceList extends Component { .map(this.domForPresence)}
- {this.props.friends.map(this.domForFriend)} + {this.props.friends && this.props.friends.map(this.domForFriend)}
{this.props.signedIn ? ( @@ -285,7 +305,7 @@ export default class PresenceList extends Component { > {occupantCount} - {this.props.friendsUpdated && (*)} + {this.props.friendsUpdated && *} {this.props.expanded && this.renderExpandedList()}
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 38f1928b50..6a68134ebe 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -138,6 +138,7 @@ class UIRoot extends Component { presenceLogEntries: PropTypes.array, presences: PropTypes.object, friends: PropTypes.array, + handle: PropTypes.string, sessionId: PropTypes.string, subscriptions: PropTypes.object, initialIsSubscribed: PropTypes.bool, @@ -2068,6 +2069,7 @@ class UIRoot extends Component { presences={this.props.presences} friends={this.props.friends} friendsUpdated={this.state.hasUnreadFriendUpdate} + handle={this.props.handle} sessionId={this.props.sessionId} signedIn={this.state.signedIn} email={this.props.store.state.credentials.email} diff --git a/src/utils/immers.js b/src/utils/immers.js index b6cf7c335f..cd0f350dd1 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -140,9 +140,12 @@ export async function auth(store) { } const redirectToAuth = () => { + // send to token endpoint at local immer, it handles + // detecting remote users and sending them on to their home to login const redirect = new URL(`${localImmer}/auth/authorize`); redirect.search = new URLSearchParams({ client_id: place.id, + // hubUri may contain user's handle (me param) when linking between immers redirect_uri: hubUri, response_type: "token" }).toString(); @@ -179,6 +182,7 @@ export async function initialize(store, scene, remountUI) { id: actorObj.id, avatarId: getAvatarFromActor(actorObj) || initialAvi, displayName: actorObj.name, + handle: `${actorObj.preferredUsername}[${new URL(homeImmer).host}]`, inbox: actorObj.inbox, outbox: actorObj.outbox, followers: actorObj.followers @@ -224,7 +228,7 @@ export async function initialize(store, scene, remountUI) { if (store.state.profile.id) { const profile = store.state.profile; friendsCol = await getFriends(profile); - remountUI({ friends: friendsCol.orderedItems }); + remountUI({ friends: friendsCol.orderedItems, handle: profile.handle }); // update follow button for new friends const players = window.APP.componentRegistry["player-info"]; if (players) { From cd82369e7e6a31d5385704fb98fbf3f98c6a7e2e Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 12 Dec 2020 16:29:16 -0600 Subject: [PATCH 035/167] Move me param to auth request query so it can be added either by hub or by a destination immer depending on user arrival flow. In presence list, detect friends in current room and don't show link --- src/react-components/presence-list.js | 36 +++++++++++++++++++-------- src/utils/immers.js | 26 +++++++++++++++---- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/react-components/presence-list.js b/src/react-components/presence-list.js index 4696369384..1eec51842f 100644 --- a/src/react-components/presence-list.js +++ b/src/react-components/presence-list.js @@ -105,6 +105,13 @@ export default class PresenceList extends Component { } }; + actorIsPresent = actorId => { + return Object.values(this.props.presences).find(data => { + const meta = data.metas[data.metas.length - 1]; + return meta.profile && meta.profile.id === actorId; + }); + }; + domForPresence = ([sessionId, data]) => { const meta = data.metas[data.metas.length - 1]; const context = meta.context; @@ -183,8 +190,12 @@ export default class PresenceList extends Component { const profile = locActivity.actor; const place = locActivity.target; let placeUrl = place ? place.url : undefined; - // inject user handle into desintation url so they don't have to type it - if (placeUrl) { + let status = ""; + let isHere; + if (this.actorIsPresent(profile.id)) { + isHere = true; + } else if (placeUrl) { + // inject user handle into desintation url so they don't have to type it try { const url = new URL(placeUrl); const search = new URLSearchParams(url.search); @@ -195,6 +206,17 @@ export default class PresenceList extends Component { /* if fail, leave original url unchanged */ } } + if (isHere) { + status = "Online here"; + } else if (locActivity.type === "Arrive") { + status = ( + + Online at {place.name} + + ); + } else if (locActivity.type === "Leave") { + status = "Offline"; + } return (
@@ -212,15 +234,7 @@ export default class PresenceList extends Component { {profile.name}
-
- {locActivity.type === "Arrive" ? ( - - Online at {place.name} - - ) : ( - {locActivity.type === "Leave" ? "Offline" : ""} - )} -
+
{status}
); diff --git a/src/utils/immers.js b/src/utils/immers.js index cd0f350dd1..5781e56324 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -122,10 +122,21 @@ export async function getFriends(actorObj) { // perform oauth flow to get access token for local or remote user export async function auth(store) { - const loc = new URL(window.location); - const hashParams = new URLSearchParams(loc.hash.substring(1)); + // copy of URL used for sharing/authorization request const hubUri = new URL(window.location); + const hashParams = new URLSearchParams(hubUri.hash.substring(1)); + const searchParams = new URLSearchParams(hubUri.search); + let handle; + + // don't share your token! hubUri.hash = ""; + // users handle may be passed from previous immer + if (searchParams.has("me")) { + handle = searchParams.get("me"); + // remove your handle before sharing with friends + searchParams.delete("me"); + hubUri.search = searchParams.toString(); + } place = await getObject(`${localImmer}/o/immer`); place.url = hubUri; // include room id @@ -143,12 +154,17 @@ export async function auth(store) { // send to token endpoint at local immer, it handles // detecting remote users and sending them on to their home to login const redirect = new URL(`${localImmer}/auth/authorize`); - redirect.search = new URLSearchParams({ + const redirectParams = new URLSearchParams({ client_id: place.id, - // hubUri may contain user's handle (me param) when linking between immers + // hub link with room id redirect_uri: hubUri, response_type: "token" - }).toString(); + }); + if (handle) { + // pass to auth to prefill login form + redirectParams.set("me", handle); + } + redirect.search = redirectParams.toString(); // hide error messages caused by interrupting loading to redirect try { document.getElementById("ui-root").style.display = "none"; From 529384c00179c66e07495818085da9db8393cb34 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 12 Dec 2020 16:34:54 -0600 Subject: [PATCH 036/167] bump client version --- src/utils/immers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 5781e56324..3419b80df5 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -3,7 +3,7 @@ import configs from "./configs"; import { fetchAvatar } from "./avatar-utils"; import { setupMonetization } from "./immers/monetization"; const localImmer = configs.IMMERS_SERVER; -console.log("immers.space client v0.2.0"); +console.log("immers.space client v0.3.0"); // avoid race between auth and initialize code let resolveAuth; let rejectAuth; From 83bf22db0ced39d72056910204aafbc021f33f3f Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 26 Dec 2020 09:11:17 -0600 Subject: [PATCH 037/167] sync lockfile with package --- package-lock.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index bab8f4f32d..b47e5e87dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6846,6 +6846,12 @@ } } }, + "awaitqueue": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/awaitqueue/-/awaitqueue-2.3.3.tgz", + "integrity": "sha512-RbzQg6VtPUtyErm55iuQLTrBJ2uihy5BKBOEkyBwv67xm5Fn2o/j+Bz+a5BmfSoe2oZ5dcz9Z3fExS8pL+LLhw==", + "dev": true + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -12928,16 +12934,10 @@ "debug": "^4.3.1", "events": "^3.2.0", "h264-profile-level-id": "^1.0.1", - "sdp-transform": "^2.14.0", + "sdp-transform": "^2.14.1", "supports-color": "^7.2.0" }, "dependencies": { - "awaitqueue": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/awaitqueue/-/awaitqueue-2.2.3.tgz", - "integrity": "sha512-vKY8hHHt1FT05UBxaTKWoA8+A3APnyzcOO1UBY5wQ7ENzXCFi3Yy4wSKsqrO0ncDD/5CI6IZDN1nVZQKziWIqg==", - "dev": true - }, "bowser": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", @@ -12945,12 +12945,12 @@ "dev": true }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "events": { From fa9f2aa6acd0135ae0aa56713003b6100650263d Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 17 Jan 2021 11:45:38 -0600 Subject: [PATCH 038/167] consume new avatar format, add new avatars to personal avatar collection --- src/utils/immers.js | 71 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 3419b80df5..d2a4091c02 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -18,15 +18,18 @@ let hubScene; let localPlayer; export function getAvatarFromActor(actorObj) { - if (!actorObj.attachment) { + if (!actorObj.avatar) { return null; } - const attachments = Array.isArray(actorObj.attachment) ? actorObj.attachment : [actorObj.attachment]; - const avi = attachments.find(obj => obj.type === "Avatar"); - if (avi) { - return avi.url; + const avatar = Array.isArray(actorObj.avatar) ? actorObj.avatar[0] : actorObj.avatar; + const links = Array.isArray(avatar.url) ? avatar.url : [avatar.url]; + // prefer gltf + const gltfUrl = links.find(link => link.mediaType === "model/gltf+json" || link.mediaType === "model/gltf-binary"); + if (gltfUrl) { + return gltfUrl.href; } - return null; + // gamble on a url of unkown type + return links.find(link => typeof link === "string"); } export async function getObject(IRI) { @@ -107,6 +110,48 @@ export function leave(actorObj) { }); } +// Adds a new avatar to an immerser's inventory +export async function createAvatar(actorObj, hubsAvatarId) { + const hubsAvatar = await fetchAvatar(hubsAvatarId); + const immersAvatar = { + type: "Model", + name: hubsAvatar.name, + url: { + type: "Link", + href: hubsAvatar.gltf_url, + mediaType: hubsAvatar.gltf_url.includes(".glb") ? "model/gltf-binary" : "model/gltf+json" + }, + to: actorObj.followers + }; + if (hubsAvatar.files.thumbnail) { + immersAvatar.icon = hubsAvatar.files.thumbnail; + } + if (hubsAvatar.attributions) { + immersAvatar.attributedTo = Object.values(hubsAvatar.attributions).map(name => ({ + name, + type: "Person" + })); + } + const createResult = await postActivity(actorObj.outbox, immersAvatar); + if (!createResult.ok) { + throw new Error("Error creating avatar", createResult.status, createResult.body); + } + const created = await getObject(createResult.headers.get("Location")); + if (actorObj.streams?.avatars) { + const addResult = await postActivity(actorObj.outbox, { + type: "Add", + actor: actorObj.id, + to: actorObj.followers, + object: created.id, + target: actorObj.streams.avatars + }); + if (!addResult.ok) { + throw new Error("Error adding avatar to collection", addResult.status, addResult.body); + } + } + return created; +} + export async function getFriends(actorObj) { const response = await window.fetch(`${actorObj.id}/friends`, { headers: { @@ -261,16 +306,12 @@ export async function initialize(store, scene, remountUI) { scene.addEventListener("avatar_updated", async () => { const profile = store.state.profile; - const avatar = await fetchAvatar(profile.avatarId); - updateProfile(profile, { + // const avatar = await fetchAvatar(profile.avatarId); + const created = await createAvatar(actorObj, profile.avatarId); + updateProfile(actorObj, { name: profile.displayName, - attachment: [ - { - type: "Avatar", - content: profile.avatarId, - url: avatar.gltf_url - } - ] + avatar: created.object, + icon: created.object.icon }) .then(() => { store.update({ From ac2889105b03dbf14579d6d70007de666f069f5a Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 17 Jan 2021 11:46:19 -0600 Subject: [PATCH 039/167] fix occasional, inconsequential error in monetization-visible --- src/components/monetization-visible.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/monetization-visible.js b/src/components/monetization-visible.js index a06b59d209..8670c96ff4 100644 --- a/src/components/monetization-visible.js +++ b/src/components/monetization-visible.js @@ -38,7 +38,7 @@ AFRAME.registerSystem("monetization-visible", { const mv = "monetization-visible"; for (const record of records) { for (const node of record.addedNodes) { - if (!node.nodeType === document.ELEMENT_NODE) continue; + if (!(node.nodeType === document.ELEMENT_NODE)) continue; if (node.classList.contains(mv)) node.setAttribute(mv, {}); for (const descendant of node.querySelectorAll(`.${mv}`)) { descendant.setAttribute(mv, {}); From 90b482cb497e4361132d58ac99ebf550eb449b93 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 17 Jan 2021 11:47:30 -0600 Subject: [PATCH 040/167] bump client version --- src/utils/immers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index d2a4091c02..f2633b1fc6 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -3,7 +3,7 @@ import configs from "./configs"; import { fetchAvatar } from "./avatar-utils"; import { setupMonetization } from "./immers/monetization"; const localImmer = configs.IMMERS_SERVER; -console.log("immers.space client v0.3.0"); +console.log("immers.space client v0.4.0"); // avoid race between auth and initialize code let resolveAuth; let rejectAuth; From 2e88cbc1ace3d0769a911026cbc425b84939b8c4 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 22 Jan 2021 13:56:29 -0600 Subject: [PATCH 041/167] hijack my avatars tab in hubs UI for immers personal avatar collection --- src/react-components/media-tiles.js | 2 +- src/storage/media-search-store.js | 8 ++- src/utils/immers.js | 98 ++++++++++++++++++++++++----- 3 files changed, 92 insertions(+), 16 deletions(-) diff --git a/src/react-components/media-tiles.js b/src/react-components/media-tiles.js index 2777d54705..4cefd5c7e3 100644 --- a/src/react-components/media-tiles.js +++ b/src/react-components/media-tiles.js @@ -198,7 +198,7 @@ class MediaTiles extends Component { {thumbnailElement}
- {entry.type === "avatar" && ( + {/*disabled*/ entry.type === "__avatar" && ( link.mediaType === "model/gltf+json" || link.mediaType === "model/gltf-binary"); @@ -32,9 +34,17 @@ export function getAvatarFromActor(actorObj) { return links.find(link => typeof link === "string"); } +export function getAvatarFromActor(actorObj) { + if (!actorObj.avatar) { + return null; + } + const avatar = Array.isArray(actorObj.avatar) ? actorObj.avatar[0] : actorObj.avatar; + getUrlFromAvatar(avatar); +} + export async function getObject(IRI) { if (IRI.startsWith(localImmer) || IRI.startsWith(homeImmer)) { - const headers = { Accept: "application/activity+json" }; + const headers = { Accept: jsonldMime }; if (token) { headers.Authorization = `Bearer ${token}`; } @@ -51,7 +61,7 @@ export async function getObject(IRI) { export async function getActor() { const response = await window.fetch(`${homeImmer}/auth/me`, { headers: { - Accept: "application/activity+json", + Accept: jsonldMime, Authorization: `Bearer ${token}` } }); @@ -65,7 +75,7 @@ export function postActivity(outbox, activity) { return window.fetch(outbox, { method: "POST", headers: { - "Content-Type": "application/activity+json", + "Content-Type": jsonldMime, Authorization: `Bearer ${token}` }, body: JSON.stringify(activity) @@ -121,7 +131,8 @@ export async function createAvatar(actorObj, hubsAvatarId) { href: hubsAvatar.gltf_url, mediaType: hubsAvatar.gltf_url.includes(".glb") ? "model/gltf-binary" : "model/gltf+json" }, - to: actorObj.followers + to: actorObj.followers, + generator: place.id }; if (hubsAvatar.files.thumbnail) { immersAvatar.icon = hubsAvatar.files.thumbnail; @@ -152,10 +163,69 @@ export async function createAvatar(actorObj, hubsAvatarId) { return created; } +export async function fetchMyImmersAvatars(page) { + let collectionPage; + let items; + const hubsResult = { + meta: { + source: "avatar", + next_cursor: null + }, + entries: [], + suggestions: null + }; + if (!actorObj.streams?.avatars) { + return hubsResult; + } + try { + if (!avatarsCollection) { + // cache base collection object + avatarsCollection = await getObject(actorObj.streams.avatars); + } + // check if the collection is not paginated + items = avatarsCollection.orderedItems; + if (!items && avatarsCollection.first) { + // otherwise get page + collectionPage = await getObject(page || avatarsCollection.first); + items = collectionPage.orderedItems; + hubsResult.meta.next_cursor = collectionPage.next; + } + items.forEach(createActivity => { + const avatar = createActivity.object; + const avatarGltfUrl = getUrlFromAvatar(avatar); + // cache results for lookup by url when donned + myAvatars[avatarGltfUrl] = avatar; + // form object for Hubs MediaBrowser + let preview = Array.isArray(avatar.icon) ? avatar.icon[0] : avatar.icon; + // if link/image object instead of direct link + if (typeof preview === "object") { + preview = preview.href || preview.url; + } + hubsResult.entries.push({ + type: "avatar", + name: avatar.name, + // id used by hubs to set the avatar, put model url here + id: avatarGltfUrl, + images: { + preview: { + // width/height ignored for avatar media + url: preview + } + }, + // the url displayed on hover is the immers link + url: avatar.id + }); + }); + } catch (err) { + console.error("Cannot fetch avatar collection", err); + } + return hubsResult; +} + export async function getFriends(actorObj) { const response = await window.fetch(`${actorObj.id}/friends`, { headers: { - Accept: "application/activity+json", + Accept: jsonldMime, Authorization: `Bearer ${token}` } }); @@ -236,7 +306,7 @@ export async function initialize(store, scene, remountUI) { hubScene = scene; localPlayer = document.getElementById("avatar-rig"); // immers profile - const actorObj = await authPromise; + actorObj = await authPromise; const initialAvi = store.state.profile.avatarId; store.update({ profile: { @@ -307,11 +377,11 @@ export async function initialize(store, scene, remountUI) { scene.addEventListener("avatar_updated", async () => { const profile = store.state.profile; // const avatar = await fetchAvatar(profile.avatarId); - const created = await createAvatar(actorObj, profile.avatarId); + const avatar = myAvatars[profile.avatarId] || (await createAvatar(actorObj, profile.avatarId)).object; updateProfile(actorObj, { name: profile.displayName, - avatar: created.object, - icon: created.object.icon + avatar, + icon: avatar.icon }) .then(() => { store.update({ From eca830d9820a16d691ce6867c18be4465ade75d1 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 22 Jan 2021 16:45:05 -0600 Subject: [PATCH 042/167] bugfix not loading actor's avatar --- src/utils/immers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 7a3286bf02..b67d28a7dd 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -39,7 +39,7 @@ export function getAvatarFromActor(actorObj) { return null; } const avatar = Array.isArray(actorObj.avatar) ? actorObj.avatar[0] : actorObj.avatar; - getUrlFromAvatar(avatar); + return getUrlFromAvatar(avatar); } export async function getObject(IRI) { From d7e60b5a00fac5ef3855e77a2ca91de0f44566ef Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 5 Feb 2021 13:36:59 -0600 Subject: [PATCH 043/167] add spoke tagger and more monetization features --- src/components/immers/README.md | 38 +++++++++++ .../{ => immers}/immers-follow-button.js | 0 src/components/immers/index.js | 7 ++ .../immers/monetization-interactable.js | 20 ++++++ .../immers/monetization-invisible.js | 20 ++++++ .../monetization-networked.js} | 33 +++------- .../immers/monetization-required.js | 64 +++++++++++++++++++ src/components/immers/monetization-visible.js | 20 ++++++ src/components/immers/spoke-tagger.js | 34 ++++++++++ src/components/immers/utils.js | 15 +++++ src/hub.js | 3 +- src/utils/immers/monetization.js | 2 + 12 files changed, 230 insertions(+), 26 deletions(-) create mode 100644 src/components/immers/README.md rename src/components/{ => immers}/immers-follow-button.js (100%) create mode 100644 src/components/immers/index.js create mode 100644 src/components/immers/monetization-interactable.js create mode 100644 src/components/immers/monetization-invisible.js rename src/components/{monetization-visible.js => immers/monetization-networked.js} (59%) create mode 100644 src/components/immers/monetization-required.js create mode 100644 src/components/immers/monetization-visible.js create mode 100644 src/components/immers/spoke-tagger.js create mode 100644 src/components/immers/utils.js diff --git a/src/components/immers/README.md b/src/components/immers/README.md new file mode 100644 index 0000000000..6aaa6da752 --- /dev/null +++ b/src/components/immers/README.md @@ -0,0 +1,38 @@ +# Immers Space Hubs components + +## spoke-tagger +System allows you to encode interactivity when editing scenes in Spoke. +Add names to entities in spoke that begin with `st-` followed by a component name +and `spoke-tagger` will add that component to the entity in Hubs. +Separate multiple components with a space, prefixing each with `st-`. +Any terms in the name not starting with `st-` will be ignored. + +E.g. name an object `bonus-content st-monetization-visible st-monetization-networked` +and `spoke-tagger` will add `monetization-visible` and `monetization-networked` +to the entity, making it visible for everyone in the room when at least one immerser +is monetized. + +## monetization-interactable + +Add this to an entity that would normally be interactable (e.g. a link) to make it +only interactable for users that are monetized + +## monetization-visible + +Only appear for users that are monetized + +## monetization-invisible + +Only appear for uses that are not monetized + +## monetization-required + +Add to an object to turn it into a monetization explainer. +It adds a hover menu that shows either "payment required" button +with a link to info about how to sign up +or a "thanks for paying!" button that does nothing. + +## monetization-networked + +Add this to an entity to make any of the above monetization features apply +for everyone in the room if at least one immerser is monetized. \ No newline at end of file diff --git a/src/components/immers-follow-button.js b/src/components/immers/immers-follow-button.js similarity index 100% rename from src/components/immers-follow-button.js rename to src/components/immers/immers-follow-button.js diff --git a/src/components/immers/index.js b/src/components/immers/index.js new file mode 100644 index 0000000000..cdeea63c42 --- /dev/null +++ b/src/components/immers/index.js @@ -0,0 +1,7 @@ +import "./immers-follow-button"; +import "./spoke-tagger"; +import "./monetization-visible"; +import "./monetization-invisible"; +import "./monetization-required"; +import "./monetization-interactable"; +import "./monetization-networked"; diff --git a/src/components/immers/monetization-interactable.js b/src/components/immers/monetization-interactable.js new file mode 100644 index 0000000000..6cd8d38fec --- /dev/null +++ b/src/components/immers/monetization-interactable.js @@ -0,0 +1,20 @@ +import { listenForMonetization, unlistenForMonetization } from "./utils"; + +AFRAME.registerComponent("monetization-interactable", { + init() { + this.onMonetizationStart = this.onMonetizationStart.bind(this); + this.onMonetizationStop = this.onMonetizationStop.bind(this); + }, + play() { + listenForMonetization(this.el, this.onMonetizationStart, this.onMonetizationStop); + }, + pause() { + unlistenForMonetization(this.el, this.onMonetizationStart, this.onMonetizationStop); + }, + onMonetizationStart() { + this.el.classList.add("interactable"); + }, + onMonetizationStop() { + this.el.classList.remove("interactable"); + } +}); diff --git a/src/components/immers/monetization-invisible.js b/src/components/immers/monetization-invisible.js new file mode 100644 index 0000000000..470f76a437 --- /dev/null +++ b/src/components/immers/monetization-invisible.js @@ -0,0 +1,20 @@ +import { listenForMonetization, unlistenForMonetization } from "./utils"; + +AFRAME.registerComponent("monetization-invisible", { + init() { + this.onMonetizationStart = this.onMonetizationStart.bind(this); + this.onMonetizationStop = this.onMonetizationStop.bind(this); + }, + play() { + listenForMonetization(this.el, this.onMonetizationStart, this.onMonetizationStop); + }, + pause() { + unlistenForMonetization(this.el, this.onMonetizationStart, this.onMonetizationStop); + }, + onMonetizationStart() { + this.el.setAttribute("visible", false); + }, + onMonetizationStop() { + this.el.setAttribute("visible", true); + } +}); diff --git a/src/components/monetization-visible.js b/src/components/immers/monetization-networked.js similarity index 59% rename from src/components/monetization-visible.js rename to src/components/immers/monetization-networked.js index 8670c96ff4..27d8044670 100644 --- a/src/components/monetization-visible.js +++ b/src/components/immers/monetization-networked.js @@ -11,10 +11,8 @@ */ const players = {}; -AFRAME.registerSystem("monetization-visible", { +AFRAME.registerSystem("monetization-networked", { init() { - this.mo = new MutationObserver(this.onMutation); - this.mo.observe(this.el, { subtree: true, childList: true }); this.onMonetizationChange = this.onMonetizationChange.bind(this); this.entities = []; this.el.addEventListener("immers-player-monetization", this.onMonetizationChange); @@ -33,37 +31,24 @@ AFRAME.registerSystem("monetization-visible", { const index = this.entities.indexOf(el); this.entities.splice(index, 1); }, - // inject component into spoke scene entities (spoke saves names as classes) - onMutation(records) { - const mv = "monetization-visible"; - for (const record of records) { - for (const node of record.addedNodes) { - if (!(node.nodeType === document.ELEMENT_NODE)) continue; - if (node.classList.contains(mv)) node.setAttribute(mv, {}); - for (const descendant of node.querySelectorAll(`.${mv}`)) { - descendant.setAttribute(mv, {}); - } - } - } - }, onMonetizationChange(event) { players[event.detail.immersId] = event.detail.monetized; const numMonetized = Object.values(players).reduce((a, b) => a + b, 0); for (const entity of this.entities) { - entity.components["monetization-visible"].checkMonetization(numMonetized); + entity.components["monetization-networked"].shareMonetization(numMonetized); } } }); -AFRAME.registerComponent("monetization-visible", { - schema: { - monetized: { type: "boolean", default: false } - }, - checkMonetization(count) { - this.el.setAttribute("visible", count > 0); +AFRAME.registerComponent("monetization-networked", { + shareMonetization(count) { + const monetized = !!count; + if (monetized !== this.lastMonetized) { + this.el.emit(`immers-monetization-${monetized ? "started" : "stopped"}`, undefined, false); + } }, init() { - this.el.setAttribute("visible", false); + this.lastMonetized = false; this.system.registerMe(this.el); }, remove() { diff --git a/src/components/immers/monetization-required.js b/src/components/immers/monetization-required.js new file mode 100644 index 0000000000..d196297fee --- /dev/null +++ b/src/components/immers/monetization-required.js @@ -0,0 +1,64 @@ +import { handleExitTo2DInterstitial } from "../../utils/vr-interstitial"; +import { listenForMonetization, unlistenForMonetization } from "./utils"; + +// inject the hover menu template so we don't have to alter hub.html +document.addEventListener( + "DOMContentLoaded", + () => { + const t = document.createElement("template"); + t.setAttribute("id", "monetization-required-hover-menu"); + t.innerHTML = ` + + + + `; + document.querySelector("a-assets").appendChild(t); + }, + { once: true } +); + +AFRAME.registerComponent("monetization-required", { + init() { + this.el.setAttribute("hover-menu", { + template: "#monetization-required-hover-menu", + isFlat: true + }); + this.el.classList.add("interactable"); + this.el.setAttribute("body-helper", "type: static; mass: 1; collisionFilterGroup: 1; collisionFilterMask: 1;"); + this.el.setAttribute("is-remote-hover-target", true); + this.el.setAttribute("tags", "isStatic: true; togglesHoveredActionSet: true; inspectable: true;"); + } +}); + +AFRAME.registerComponent("monetization-required-button", { + init() { + this.onMonetizationStart = this.onMonetizationStart.bind(this); + this.onMonetizationStop = this.onMonetizationStop.bind(this); + this.label = this.el.querySelector("[text]"); + this.monetized = false; + this.onClick = async () => { + if (this.monetized) { + return; + } + await handleExitTo2DInterstitial(false, () => {}, true); + window.open("https://web.immers.space/monetization-required/"); + }; + }, + play() { + listenForMonetization(this.el, this.onMonetizationStart, this.onMonetizationStop); + this.el.object3D.addEventListener("interact", this.onClick); + }, + + pause() { + unlistenForMonetization(this.el, this.onMonetizationStart, this.onMonetizationStop); + this.el.object3D.removeEventListener("interact", this.onClick); + }, + onMonetizationStart() { + this.monetized = true; + this.label.setAttribute("text", "value", "thanks for paying!"); + }, + onMonetizationStop() { + this.monetized = false; + this.label.setAttribute("text", "value", "payment required"); + } +}); diff --git a/src/components/immers/monetization-visible.js b/src/components/immers/monetization-visible.js new file mode 100644 index 0000000000..594035c954 --- /dev/null +++ b/src/components/immers/monetization-visible.js @@ -0,0 +1,20 @@ +import { listenForMonetization, unlistenForMonetization } from "./utils"; + +AFRAME.registerComponent("monetization-visible", { + init() { + this.onMonetizationStart = this.onMonetizationStart.bind(this); + this.onMonetizationStop = this.onMonetizationStop.bind(this); + }, + play() { + listenForMonetization(this.el, this.onMonetizationStart, this.onMonetizationStop); + }, + pause() { + unlistenForMonetization(this.el, this.onMonetizationStart, this.onMonetizationStop); + }, + onMonetizationStart() { + this.el.setAttribute("visible", true); + }, + onMonetizationStop() { + this.el.setAttribute("visible", false); + } +}); diff --git a/src/components/immers/spoke-tagger.js b/src/components/immers/spoke-tagger.js new file mode 100644 index 0000000000..3ea932cfa5 --- /dev/null +++ b/src/components/immers/spoke-tagger.js @@ -0,0 +1,34 @@ +AFRAME.registerSystem("spoke-tagger", { + init() { + this.tag = this.tag.bind(this); + this.onMutation = this.onMutation.bind(this); + + this.mo = new MutationObserver(this.onMutation); + this.mo.observe(this.el, { subtree: true, childList: true }); + }, + + // inject components into spoke scene entities (spoke saves names as classes) + onMutation(records) { + for (const record of records) { + for (const node of record.addedNodes) { + this.tag(node); + } + } + }, + + tag(el) { + if (!(el.nodeType === document.ELEMENT_NODE) || !el.classList.length) { + return; + } + // spoke converts spaces in names to _ in class + const tags = el.classList.value.split("_"); + for (const tag of tags) { + if (!tag.startsWith("st-")) { + continue; + } + el.setAttribute(tag.substring(3), ""); + } + // recurse children of added node + el.childNodes.forEach(this.tag); + } +}); diff --git a/src/components/immers/utils.js b/src/components/immers/utils.js new file mode 100644 index 0000000000..46a831d841 --- /dev/null +++ b/src/components/immers/utils.js @@ -0,0 +1,15 @@ +export function listenForMonetization (el, onMonetizationStart, onMonetizationStop) { + el.sceneEl.addEventListener("immers-monetization-started", onMonetizationStart); + el.sceneEl.addEventListener("immers-monetization-stopped", onMonetizationStop); + // listen on self for monetization-networked events + el.addEventListener("immers-monetization-started", onMonetizationStart); + el.addEventListener("immers-monetization-stopped", onMonetizationStop); + +} + +export function unlistenForMonetization(el, onMonetizationStart, onMonetizationStop) { + el.sceneEl.removeEventListener("immers-monetization-started", onMonetizationStart); + el.sceneEl.removeEventListener("immers-monetization-stopped", onMonetizationStop); + el.removeEventListener("immers-monetization-started", onMonetizationStart); + el.removeEventListener("immers-monetization-stopped", onMonetizationStop); +} diff --git a/src/hub.js b/src/hub.js index 046f8dd0d1..862cdd0662 100644 --- a/src/hub.js +++ b/src/hub.js @@ -220,8 +220,7 @@ import detectConcurrentLoad from "./utils/concurrent-load-detector"; import qsTruthy from "./utils/qs_truthy"; -import "./components/immers-follow-button"; -import "./components/monetization-visible"; +import "./components/immers/index"; const PHOENIX_RELIABLE_NAF = "phx-reliable"; NAF.options.firstSyncSource = PHOENIX_RELIABLE_NAF; diff --git a/src/utils/immers/monetization.js b/src/utils/immers/monetization.js index bf7a511de8..d3d6470299 100644 --- a/src/utils/immers/monetization.js +++ b/src/utils/immers/monetization.js @@ -54,6 +54,8 @@ function onMonetizationProgress(event) { function onSceneLoaded() { if (document.monetization.state === "started") { onMonetizationStart(); + } else { + onMonetizationStop(); } document.monetization.addEventListener("monetizationstart", onMonetizationStart); document.monetization.addEventListener("monetizationstop", onMonetizationStop); From e267049fc588e7be5cec0866ddaeffcac48221a9 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 5 Feb 2021 14:49:35 -0600 Subject: [PATCH 044/167] fix networked monetization not turning off --- src/components/immers/monetization-networked.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/immers/monetization-networked.js b/src/components/immers/monetization-networked.js index 27d8044670..8cfc75ced8 100644 --- a/src/components/immers/monetization-networked.js +++ b/src/components/immers/monetization-networked.js @@ -46,6 +46,7 @@ AFRAME.registerComponent("monetization-networked", { if (monetized !== this.lastMonetized) { this.el.emit(`immers-monetization-${monetized ? "started" : "stopped"}`, undefined, false); } + this.lastMonetized = monetized; }, init() { this.lastMonetized = false; From e9e7cb6c870cdec8febf40b307c803a212745ff0 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 7 Feb 2021 14:51:00 -0600 Subject: [PATCH 045/167] friends update notification --- src/react-components/room/ContentMenu.js | 1 + src/react-components/room/ContentMenu.scss | 8 +++++++- src/react-components/ui-root.js | 7 ++++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/react-components/room/ContentMenu.js b/src/react-components/room/ContentMenu.js index b5eceb382d..b2f212c639 100644 --- a/src/react-components/room/ContentMenu.js +++ b/src/react-components/room/ContentMenu.js @@ -38,6 +38,7 @@ export function PeopleMenuButton(props) { + {props.notification && *} ); } diff --git a/src/react-components/room/ContentMenu.scss b/src/react-components/room/ContentMenu.scss index abcdefd1e6..c8ddccb7d4 100644 --- a/src/react-components/room/ContentMenu.scss +++ b/src/react-components/room/ContentMenu.scss @@ -65,4 +65,10 @@ width: 1px; margin: 0 8px; background-color: theme.$lightgrey; -} \ No newline at end of file +} + +:local(.notifier) { + color: #FF3464; + font-size: 1.5em; + margin-left: -8px; +} diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index e1085290a7..635d517629 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -248,8 +248,8 @@ class UIRoot extends Component { }, 0); }); } - // TOOD state.isPresenceListExpanded is gone - if (prevProps.friends !== this.props.friends && !this.state.isPresenceListExpanded) { + + if (prevProps.friends !== this.props.friends && this.state.sidebarId !== "people") { this.setState({ hasUnreadFriendUpdate: true }); this.props.scene.addState("notification"); } else if (!this.state.hasUnreadFriendUpdate) { @@ -1364,7 +1364,8 @@ class UIRoot extends Component { )} this.toggleSidebar("people")} + onClick={() => this.toggleSidebar("people", { hasUnreadFriendUpdate: false })} + notification={this.state.hasUnreadFriendUpdate} /> )} From df20078191c9d789047f07481c7ce9b0f55aecf8 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 12 Feb 2021 13:38:36 -0600 Subject: [PATCH 046/167] immers friends in people menu --- src/react-components/icons/immers_logo.png | Bin 0 -> 22558 bytes src/react-components/layout/List.scss | 8 ++- src/react-components/room/PeopleSidebar.js | 41 +++++++++++++-- src/react-components/room/PeopleSidebar.scss | 10 ++++ .../room/PeopleSidebarContainer.js | 48 ++++++++++++++++-- src/react-components/ui-root.js | 1 + 6 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 src/react-components/icons/immers_logo.png diff --git a/src/react-components/icons/immers_logo.png b/src/react-components/icons/immers_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d7763330023d8e3b614913f97d1fb02b19546501 GIT binary patch literal 22558 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 K!`|pR#s3!{q?XA5 literal 0 HcmV?d00001 diff --git a/src/react-components/layout/List.scss b/src/react-components/layout/List.scss index 21f8a290dc..31a6872fd3 100644 --- a/src/react-components/layout/List.scss +++ b/src/react-components/layout/List.scss @@ -24,11 +24,11 @@ border: none; width: 100%; - &:hover { + &:hover:not([disabled]) { background-color: theme.$white-hover; } - &:active { + &:active:not([disabled]) { background-color: theme.$white-pressed; } @@ -36,6 +36,10 @@ box-shadow: inset 0 0 0 3px theme.$black; } + &[disabled] { + cursor: default; + } + &:local(.selected) { color: theme.$white; background-color: theme.$blue; diff --git a/src/react-components/room/PeopleSidebar.js b/src/react-components/room/PeopleSidebar.js index 5951dc0ff2..f03f5a7b43 100644 --- a/src/react-components/room/PeopleSidebar.js +++ b/src/react-components/room/PeopleSidebar.js @@ -12,8 +12,10 @@ import { ReactComponent as VRIcon } from "../icons/VR.svg"; import { ReactComponent as VolumeOffIcon } from "../icons/VolumeOff.svg"; import { ReactComponent as VolumeHighIcon } from "../icons/VolumeHigh.svg"; import { ReactComponent as VolumeMutedIcon } from "../icons/VolumeMuted.svg"; +import immersLogo from "../icons/immers_logo.png"; import { List, ButtonListItem } from "../layout/List"; import { FormattedMessage, useIntl } from "react-intl"; +import { proxiedUrlFor } from "../../utils/media-url-utils"; function getDeviceLabel(ctx, intl) { if (ctx) { @@ -80,6 +82,32 @@ function getPresenceMessage(presence, intl) { } } +function getLocationMessage(activity, intl) { + switch (activity.type) { + case "Arrive": { + const onlineMsg = intl.formatMessage({ id: "people-sidebar.immers.online", defaultMessage: "Online at" }); + const placeUrl = activity.target?.url; + return ( + + {onlineMsg} {activity.target?.name ?? "unkown"} + + ); + } + case "Leave": + return intl.formatMessage({ id: "people-sidebar.immers.offline", defaultMessage: "Offline" }); + default: + return ""; + } +} + +function imageIcon(src) { + return ( + + {src && } + + ); +} + function getPersonName(person, intl) { const you = intl.formatMessage({ id: "people-sidebar.person-name.you", @@ -123,9 +151,14 @@ export function PeopleSidebar({ people, onSelectPerson, onClose, showMuteAll, on key={person.id} type="button" onClick={e => onSelectPerson(person, e)} + disabled={person.remote} > - {} - {!person.context.discord && VoiceIcon && } + {person.remote ? imageIcon(immersLogo) : } + {person.remote + ? imageIcon(person.friendStatus.actor.icon) + : !person.context.discord && + !person.remote && + VoiceIcon && }

{getPersonName(person, intl)}

{person.roles.owner && ( )} -

{getPresenceMessage(person.presence, intl)}

+

+ {getPresenceMessage(person.presence, intl) ?? getLocationMessage(person.friendStatus, intl)} +

); })} diff --git a/src/react-components/room/PeopleSidebar.scss b/src/react-components/room/PeopleSidebar.scss index 8b9ed0ba50..22c8c4def5 100644 --- a/src/react-components/room/PeopleSidebar.scss +++ b/src/react-components/room/PeopleSidebar.scss @@ -27,3 +27,13 @@ flex: 1; justify-content: flex-end; } + +:local(.image-icon-wrapper) { + width: 20px; + height: 20px; + overflow: hidden; +} + +:local(.image-icon) { + width: 20px; +} diff --git a/src/react-components/room/PeopleSidebarContainer.js b/src/react-components/room/PeopleSidebarContainer.js index 4e7c1df725..0c68b82090 100644 --- a/src/react-components/room/PeopleSidebarContainer.js +++ b/src/react-components/room/PeopleSidebarContainer.js @@ -11,18 +11,54 @@ export function userFromPresence(sessionId, presence, micPresences, mySessionId) return { id: sessionId, isMe: mySessionId === sessionId, micPresence, ...meta }; } -function usePeopleList(presences, mySessionId, micUpdateFrequency = 500) { +function usePeopleList(presences, mySessionId, friends, micUpdateFrequency = 500) { const [people, setPeople] = useState([]); - useEffect( () => { let timeout; + const friendsAndPresences = Object.assign({}, presences); + const presenceFriendLookup = Object.fromEntries( + Object.entries(presences).map(([, presence]) => [ + presence.metas[presence.metas.length - 1].profile.id, + presence + ]) + ); + friends.sort((a, b) => { + if (a.type === b.type) { + return 0; + } + if (a.type === "Leave") { + return 1; + } + return -1; + }); + friends.forEach(friend => { + const localPresence = presenceFriendLookup[friend.actor.id]; + if (localPresence) { + localPresence.metas[localPresence.metas.length - 1].friendStatus = friend; + } else { + friendsAndPresences[friend.id] = { + id: friend.actor.id, + metas: [ + { + context: {}, + profile: { + displayName: friend.actor.name + }, + roles: {}, + friendStatus: friend, + remote: true + } + ] + }; + } + }); function updateMicrophoneState() { const micPresences = getMicrophonePresences(); setPeople( - Object.entries(presences).map(([id, presence]) => { + Object.entries(friendsAndPresences).map(([id, presence]) => { return userFromPresence(id, presence, micPresences, mySessionId); }) ); @@ -36,7 +72,7 @@ function usePeopleList(presences, mySessionId, micUpdateFrequency = 500) { clearTimeout(timeout); }; }, - [presences, micUpdateFrequency, setPeople, mySessionId] + [presences, friends, micUpdateFrequency, setPeople, mySessionId] ); return people; @@ -75,6 +111,7 @@ PeopleListContainer.propTypes = { export function PeopleSidebarContainer({ hubChannel, presences, + friends, mySessionId, displayNameOverride, store, @@ -84,7 +121,7 @@ export function PeopleSidebarContainer({ showNonHistoriedDialog, onClose }) { - const people = usePeopleList(presences, mySessionId); + const people = usePeopleList(presences, mySessionId, friends); const [selectedPersonId, setSelectedPersonId] = useState(null); const selectedPerson = people.find(person => person.id === selectedPersonId); const setSelectedPerson = useCallback( @@ -137,6 +174,7 @@ PeopleSidebarContainer.propTypes = { onClose: PropTypes.func.isRequired, mySessionId: PropTypes.string.isRequired, presences: PropTypes.object.isRequired, + friends: PropTypes.array.isRequired, performConditionalSignIn: PropTypes.func.isRequired, onCloseDialog: PropTypes.func.isRequired, showNonHistoriedDialog: PropTypes.func.isRequired diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 635d517629..f5c07decbc 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -1442,6 +1442,7 @@ class UIRoot extends Component { history={this.props.history} mySessionId={this.props.sessionId} presences={this.props.presences} + friends={this.props.friends} onClose={() => this.setSidebar(null)} onCloseDialog={() => this.closeDialog()} showNonHistoriedDialog={this.showNonHistoriedDialog} From 100711ff42e1cbb278cea1d0ec3a5adc44c62de7 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 12 Feb 2021 14:52:04 -0600 Subject: [PATCH 047/167] disable edit button on avatar collection avatars, add in source immer as publisher for descript & link in metdia tile --- src/react-components/room/MediaTiles.js | 2 +- src/utils/immers.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/react-components/room/MediaTiles.js b/src/react-components/room/MediaTiles.js index 14f821a691..aaa9d03cc8 100644 --- a/src/react-components/room/MediaTiles.js +++ b/src/react-components/room/MediaTiles.js @@ -204,7 +204,7 @@ export function MediaTile({ entry, processThumbnailUrl, onClick, onEdit, onShowS
)}
- {entry.type === "avatar" && ( + {entry.type === "__disabled:avatar" && ( Date: Fri, 12 Feb 2021 15:07:32 -0600 Subject: [PATCH 048/167] alter sign-in message to clarify difference from immers profile login --- src/react-components/auth/SignInModal.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/react-components/auth/SignInModal.js b/src/react-components/auth/SignInModal.js index 5a68e7334c..583a1b9ff5 100644 --- a/src/react-components/auth/SignInModal.js +++ b/src/react-components/auth/SignInModal.js @@ -96,14 +96,15 @@ export function SubmitEmail({ onSubmitEmail, initialEmail, privacyUrl, termsUrl, }, [setEmail] ); - + // immers: disable signin prompt + message = undefined; return (

{message ? ( intl.formatMessage(message) ) : ( - + )}

Date: Fri, 12 Feb 2021 15:57:26 -0600 Subject: [PATCH 049/167] restore handle injection in friends links --- src/react-components/room/PeopleSidebar.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/react-components/room/PeopleSidebar.js b/src/react-components/room/PeopleSidebar.js index f03f5a7b43..b1eaee9e96 100644 --- a/src/react-components/room/PeopleSidebar.js +++ b/src/react-components/room/PeopleSidebar.js @@ -82,11 +82,21 @@ function getPresenceMessage(presence, intl) { } } -function getLocationMessage(activity, intl) { +function getLocationMessage(activity, myHandle, intl) { switch (activity.type) { case "Arrive": { const onlineMsg = intl.formatMessage({ id: "people-sidebar.immers.online", defaultMessage: "Online at" }); - const placeUrl = activity.target?.url; + let placeUrl = activity.target?.url; + // inject user handle into desintation url so they don't have to type it + try { + const url = new URL(placeUrl); + const search = new URLSearchParams(url.search); + search.set("me", myHandle); + url.search = search.toString(); + placeUrl = url.toString(); + } catch (ignore) { + /* if fail, leave original url unchanged */ + } return ( {onlineMsg} {activity.target?.name ?? "unkown"} @@ -119,7 +129,7 @@ function getPersonName(person, intl) { export function PeopleSidebar({ people, onSelectPerson, onClose, showMuteAll, onMuteAll }) { const intl = useIntl(); - + const myHandle = people.find(person => person.isMe)?.profile.handle; return ( )}

- {getPresenceMessage(person.presence, intl) ?? getLocationMessage(person.friendStatus, intl)} + {getPresenceMessage(person.presence, intl) ?? getLocationMessage(person.friendStatus, myHandle, intl)}

); From 6adade547993e9b5bd382d303136de99398e0785 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 12 Feb 2021 15:58:10 -0600 Subject: [PATCH 050/167] update scene loaded event for monetization setup --- src/utils/immers/monetization.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/immers/monetization.js b/src/utils/immers/monetization.js index d3d6470299..890b9f8ea5 100644 --- a/src/utils/immers/monetization.js +++ b/src/utils/immers/monetization.js @@ -70,6 +70,6 @@ export function setupMonetization(scene, player) { if (hubScene.is("loaded")) { onSceneLoaded(); } else { - hubScene.addEventListener("loading_finished", onSceneLoaded, { once: true }); + hubScene.addEventListener("environment-scene-loaded", onSceneLoaded, { once: true }); } } From 6553ba609a963d8ae9ac275a3a6ccc7de059e082 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 12 Feb 2021 15:58:21 -0600 Subject: [PATCH 051/167] bump version --- src/utils/immers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 7a0ba47258..7282832a2d 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -3,7 +3,7 @@ import configs from "./configs"; import { fetchAvatar } from "./avatar-utils"; import { setupMonetization } from "./immers/monetization"; const localImmer = configs.IMMERS_SERVER; -console.log("immers.space client v0.4.0"); +console.log("immers.space client v0.4.1"); const jsonldMime = "application/activity+json"; // avoid race between auth and initialize code let resolveAuth; From 4d9f9a03264d83c8feede634b7c15c86aee085ff Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 13 Feb 2021 08:39:31 -0600 Subject: [PATCH 052/167] fix static asset location --- .../icons => assets/images}/immers_logo.png | Bin src/react-components/room/PeopleSidebar.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{react-components/icons => assets/images}/immers_logo.png (100%) diff --git a/src/react-components/icons/immers_logo.png b/src/assets/images/immers_logo.png similarity index 100% rename from src/react-components/icons/immers_logo.png rename to src/assets/images/immers_logo.png diff --git a/src/react-components/room/PeopleSidebar.js b/src/react-components/room/PeopleSidebar.js index b1eaee9e96..a66e1387a9 100644 --- a/src/react-components/room/PeopleSidebar.js +++ b/src/react-components/room/PeopleSidebar.js @@ -12,7 +12,7 @@ import { ReactComponent as VRIcon } from "../icons/VR.svg"; import { ReactComponent as VolumeOffIcon } from "../icons/VolumeOff.svg"; import { ReactComponent as VolumeHighIcon } from "../icons/VolumeHigh.svg"; import { ReactComponent as VolumeMutedIcon } from "../icons/VolumeMuted.svg"; -import immersLogo from "../icons/immers_logo.png"; +import immersLogo from "../../assets/images/immers_logo.png"; import { List, ButtonListItem } from "../layout/List"; import { FormattedMessage, useIntl } from "react-intl"; import { proxiedUrlFor } from "../../utils/media-url-utils"; From 087a28f2e7cb96597bee1de3e43d523986e0fcc3 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 13 Feb 2021 09:29:49 -0600 Subject: [PATCH 053/167] extra check to prevent conflicting updates in people list --- src/react-components/room/PeopleSidebarContainer.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/react-components/room/PeopleSidebarContainer.js b/src/react-components/room/PeopleSidebarContainer.js index 0c68b82090..28b653c2bf 100644 --- a/src/react-components/room/PeopleSidebarContainer.js +++ b/src/react-components/room/PeopleSidebarContainer.js @@ -10,11 +10,13 @@ export function userFromPresence(sessionId, presence, micPresences, mySessionId) const micPresence = micPresences.get(sessionId); return { id: sessionId, isMe: mySessionId === sessionId, micPresence, ...meta }; } - +// sometimes the mic presence timeout fails to clear +let lastTimeout; function usePeopleList(presences, mySessionId, friends, micUpdateFrequency = 500) { const [people, setPeople] = useState([]); useEffect( () => { + clearTimeout(lastTimeout); let timeout; const friendsAndPresences = Object.assign({}, presences); @@ -64,6 +66,7 @@ function usePeopleList(presences, mySessionId, friends, micUpdateFrequency = 500 ); timeout = setTimeout(updateMicrophoneState, micUpdateFrequency); + lastTimeout = timeout; } updateMicrophoneState(); From cdef6fb115027e681623db2a3cc297285778c30d Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 13 Feb 2021 09:30:47 -0600 Subject: [PATCH 054/167] avoid error when loading into room without changing avatar, avoid unnecessary profile update when entering new immer for first time --- src/utils/immers.js | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index b67d28a7dd..b984d048c1 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -308,10 +308,13 @@ export async function initialize(store, scene, remountUI) { // immers profile actorObj = await authPromise; const initialAvi = store.state.profile.avatarId; + const actorAvi = getAvatarFromActor(actorObj); + // cache current avatar so doesn't get recreated during a profile update + myAvatars[actorAvi] = Array.isArray(actorObj.avatar) ? actorObj.avatar[0] : actorObj.avatar; store.update({ profile: { id: actorObj.id, - avatarId: getAvatarFromActor(actorObj) || initialAvi, + avatarId: actorAvi || initialAvi, displayName: actorObj.name, handle: `${actorObj.preferredUsername}[${new URL(homeImmer).host}]`, inbox: actorObj.inbox, @@ -376,21 +379,24 @@ export async function initialize(store, scene, remountUI) { scene.addEventListener("avatar_updated", async () => { const profile = store.state.profile; - // const avatar = await fetchAvatar(profile.avatarId); - const avatar = myAvatars[profile.avatarId] || (await createAvatar(actorObj, profile.avatarId)).object; - updateProfile(actorObj, { - name: profile.displayName, - avatar, - icon: avatar.icon - }) - .then(() => { - store.update({ - activity: { - hasChangedName: true - } - }); - }) - .catch(err => console.error("Error updating profile:", err.message)); + const update = {}; + if (profile.displayName !== actorObj.name) { + update.name = profile.displayName; + } + if (getAvatarFromActor(actorObj) !== profile.avatarId) { + update.avatar = myAvatars[profile.avatarId] || (await createAvatar(actorObj, profile.avatarId)).object; + update.icon = update.avatar.icon; + } + // only publish update if something changed + if (Object.keys(update).length) { + await updateProfile(actorObj, update).catch(err => console.error("Error updating profile:", err.message)); + } + // disable the first-time entry name & avatar prompt + store.update({ + activity: { + hasChangedName: true + } + }); }); // entity interactions From d6d28a98735737038e104ee98f73fcb7f7470d2d Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 13 Feb 2021 09:31:05 -0600 Subject: [PATCH 055/167] fix wrong target in leave activity --- src/utils/immers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index b984d048c1..e4bd0ac72e 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -350,7 +350,7 @@ export async function initialize(store, scene, remountUI) { leave: { type: "Leave", actor: actorObj.id, - target: window.location.href, + target: place, to: actorObj.followers } }); From e4e6cb9faa8f17fbc591cb411800ad91c552e7e3 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 14 Feb 2021 12:51:45 -0600 Subject: [PATCH 056/167] fix react warning about untyped prop --- src/react-components/room/ContentMenu.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/react-components/room/ContentMenu.js b/src/react-components/room/ContentMenu.js index b2f212c639..c3d98ed3fd 100644 --- a/src/react-components/room/ContentMenu.js +++ b/src/react-components/room/ContentMenu.js @@ -7,17 +7,19 @@ import { ReactComponent as ObjectsIcon } from "../icons/Objects.svg"; import { ReactComponent as PeopleIcon } from "../icons/People.svg"; import { FormattedMessage } from "react-intl"; -export function ContentMenuButton({ active, children, ...props }) { +export function ContentMenuButton({ active, children, notification, ...props }) { return ( ); } ContentMenuButton.propTypes = { children: PropTypes.node, - active: PropTypes.bool + active: PropTypes.bool, + notification: PropTypes.bool }; export function ObjectsMenuButton(props) { @@ -38,7 +40,6 @@ export function PeopleMenuButton(props) { - {props.notification && *} ); } From c555cc6ec0bf810934cf7cd9e15099816810e69d Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 7 Feb 2021 16:34:07 -0600 Subject: [PATCH 057/167] clear state on load if invalid --- src/storage/store.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/storage/store.js b/src/storage/store.js index 4e19639128..f7de03e042 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -216,7 +216,8 @@ export default class Store extends EventTarget { constructor() { super(); - if (localStorage.getItem(LOCAL_STORE_KEY) === null) { + const savedState = localStorage.getItem(LOCAL_STORE_KEY) + if (savedState === null || !validator.validate(JSON.parse(savedState), SCHEMA).valid) { localStorage.setItem(LOCAL_STORE_KEY, JSON.stringify({})); } From 8c33c4e6a91368106e4e1345272cb9663521cd21 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Tue, 2 Mar 2021 21:09:23 -0600 Subject: [PATCH 058/167] News feed & chat integration - new Activities class to start consolidating ActivityPub API code - Add summary text to Arrive/Leave activities to make them easier to display - Remove obsolete Leave method (leave is posted via socket close event) - Change icon format to be Image object for fediverse compat Chat & history items behind feature switch (needs new UI): - fetch inbox & outbox history and send into hubs messaging system for display in chat - intercept outgoing chat messages and convert to Immers activity delivered to room occupants and friends - stream new inbox activities while connected and send to hubs messaging --- src/utils/immers.js | 89 ++++++++++++--- src/utils/immers/activities.js | 199 +++++++++++++++++++++++++++++++++ 2 files changed, 273 insertions(+), 15 deletions(-) create mode 100644 src/utils/immers/activities.js diff --git a/src/utils/immers.js b/src/utils/immers.js index e4bd0ac72e..0071b74a28 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -2,8 +2,9 @@ import io from "socket.io-client"; import configs from "./configs"; import { fetchAvatar } from "./avatar-utils"; import { setupMonetization } from "./immers/monetization"; +import Activities from "./immers/activities"; const localImmer = configs.IMMERS_SERVER; -console.log("immers.space client v0.4.0"); +console.log("immers.space client v0.4.1"); const jsonldMime = "application/activity+json"; // avoid race between auth and initialize code let resolveAuth; @@ -12,6 +13,7 @@ const authPromise = new Promise((resolve, reject) => { resolveAuth = resolve; rejectAuth = reject; }); +const activities = new Activities(localImmer); let homeImmer; let place; let token; @@ -107,16 +109,8 @@ export function arrive(actorObj) { type: "Arrive", actor: actorObj.id, target: place, - to: actorObj.followers - }); -} - -export function leave(actorObj) { - return postActivity(actorObj.outbox, { - type: "Leave", - actor: actorObj.id, - target: place, - to: actorObj.followers + to: actorObj.followers, + summary: `${actorObj.name} arrived at ${place.name}.` }); } @@ -135,7 +129,11 @@ export async function createAvatar(actorObj, hubsAvatarId) { generator: place.id }; if (hubsAvatar.files.thumbnail) { - immersAvatar.icon = hubsAvatar.files.thumbnail; + immersAvatar.icon = { + type: "Image", + mediaType: "image/png", + url: hubsAvatar.files.thumbnail + }; } if (hubsAvatar.attributions) { immersAvatar.attributedTo = Object.values(hubsAvatar.attributions).map(name => ({ @@ -254,7 +252,7 @@ export async function auth(store) { } place = await getObject(`${localImmer}/o/immer`); place.url = hubUri; // include room id - + activities.place = place; if (hashParams.has("access_token")) { // not safe to update store here, will be saved later in initialize() token = hashParams.get("access_token"); @@ -264,6 +262,8 @@ export async function auth(store) { token = store.state.credentials.immerToken; homeImmer = store.state.credentials.immerHome; } + activities.token = token; + activities.homeImmer = homeImmer; const redirectToAuth = () => { // send to token endpoint at local immer, it handles @@ -302,11 +302,12 @@ export async function auth(store) { } } -export async function initialize(store, scene, remountUI) { +export async function initialize(store, scene, remountUI, messageDispatch) { hubScene = scene; localPlayer = document.getElementById("avatar-rig"); // immers profile actorObj = await authPromise; + activities.actor = actorObj; const initialAvi = store.state.profile.avatarId; const actorAvi = getAvatarFromActor(actorObj); // cache current avatar so doesn't get recreated during a profile update @@ -351,7 +352,8 @@ export async function initialize(store, scene, remountUI) { type: "Leave", actor: actorObj.id, target: place, - to: actorObj.followers + to: actorObj.followers, + summary: `${actorObj.name} left ${place.name}.` } }); }); @@ -362,6 +364,7 @@ export async function initialize(store, scene, remountUI) { if (store.state.profile.id) { const profile = store.state.profile; friendsCol = await getFriends(profile); + activities.friends = friendsCol.orderedItems; remountUI({ friends: friendsCol.orderedItems, handle: profile.handle }); // update follow button for new friends const players = window.APP.componentRegistry["player-info"]; @@ -417,4 +420,60 @@ export async function initialize(store, scene, remountUI) { }); setupMonetization(hubScene, localPlayer); + + // news feed and chat integration, behind a feature switch as it needs the new hubs ui + if (messageDispatch) { + // fetch news feed + const updateFeed = async () => { + const { messages, more } = await activities.feedAsChat(); + messages.forEach(detail => { + messageDispatch.dispatchEvent(new CustomEvent("message", { detail })); + }); + return more; + }; + updateFeed(); + // event from "load more" button at top of in chat sidebar + window.addEventListener("immers-load-more-history", async () => { + const more = await updateFeed(); + window.dispatchEvent(new CustomEvent("immers-more-history-loaded", { detail: more })); + }); + + // stream new activity while in room + immerSocket.on("inbox-update", activity => { + activity = JSON.parse(activity); + if (activity.type === "Create") { + const detail = activities.activityAsChat(activity); + messageDispatch.dispatchEvent(new CustomEvent("message", { detail })); + } + }); + // intercept outgoing messages and post to immers space feed + messageDispatch.addEventListener("message", ({ detail: message }) => { + // skip if incoming message or loaded from history + if (!message.sent || message.isImmersFeed) { + return; + } + // Send to immers id of everyone in room so all chat goes through immers and + // we don't have to worry about duplicate messages appearing in chat + const localAudience = Object.values(window.APP.hubChannel.presence.state) + .map(presence => presence.metas[presence.metas.length - 1]?.profile.id) + .filter(id => id && id !== actorObj.id); + // send activity + let task; + switch (message.type) { + case "chat": + task = activities.note(message.body, localAudience, true, null); + break; + case "image": + case "photo": + task = activities.image(message.body.src, localAudience, true, null); + break; + case "video": + task = activities.video(message.body.src, localAudience, true, null); + break; + default: + console.log("Chat message not shared", message); + } + task.catch(err => console.error(`Error sharing chat: ${err.message}`)); + }); + } } diff --git a/src/utils/immers/activities.js b/src/utils/immers/activities.js new file mode 100644 index 0000000000..cbe5a99052 --- /dev/null +++ b/src/utils/immers/activities.js @@ -0,0 +1,199 @@ +export default class Activities { + static JSONLDMime = "application/activity+json"; + static PublicAddress = "as:Public"; + constructor(localImmer) { + this.localImmer = localImmer; + this.homeImmer = null; + this.token = null; + this.actor = null; + this.place = null; + this.nextInboxPage = null; + this.nextOutboxPage = null; + this.inboxStartDate = new Date(); + this.outboxStartDate = this.inboxStartDate; + this.friends = []; + } + + trustedIRI(IRI) { + return IRI.startsWith(this.localImmer) || IRI.startsWith(this.homeImmer); + } + + async getObject(IRI) { + if (this.trustedIRI(IRI)) { + const headers = { Accept: Activities.JSONLDMime }; + if (this.token) { + headers.Authorization = `Bearer ${this.token}`; + } + const result = await window.fetch(IRI, { headers }); + if (!result.ok) { + throw new Error(`Object fetch error ${result.message}`); + } + return result.json(); + } else { + throw new Error("Object fetch proxy not implemented"); + } + } + + async inbox() { + let col; + if (this.nextInboxPage === null) { + col = await this.getObject(this.actor.inbox); + if (!col.orderedItems && col.first) { + col = await this.getObject(col.first); + } + } else if (this.nextInboxPage) { + col = await this.getObject(this.nextInboxPage); + } + this.nextInboxPage = col?.next; + return col; + } + + async outbox() { + let col; + if (this.nextOutboxPage === null) { + col = await this.getObject(this.actor.outbox); + if (!col.orderedItems && col.first) { + col = await this.getObject(col.first); + } + } else if (this.nextOutboxPage) { + col = await this.getObject(this.nextOutboxPage); + } + this.nextOutboxPage = col?.next; + return col; + } + + async inboxAsChat() { + const inbox = await this.inbox(); + if (!inbox?.orderedItems?.length) { + return []; + } + this.inboxStartDate = new Date(inbox.orderedItems[inbox.orderedItems.length - 1].published); + return inbox.orderedItems.map(act => this.activityAsChat(act)).filter(message => message.body); + } + + async outboxAsChat() { + const outbox = await this.outbox(); + if (!outbox?.orderedItems?.length) { + return []; + } + this.outboxStartDate = new Date(outbox.orderedItems[outbox.orderedItems.length - 1].published); + return outbox.orderedItems.map(act => this.activityAsChat(act, true)).filter(message => message.body); + } + + async feedAsChat() { + const messages = (await this.inboxAsChat()).concat(await this.outboxAsChat()); + // try to balance amount of time covered by inbox & outbox feeds + if (this.inboxStartDate > this.outboxStartDate) { + while (this.inboxStartDate > this.outboxStartDate && this.nextInboxPage) { + messages.push(...(await this.inboxAsChat())); + } + } else { + while (this.outboxStartDate > this.inboxStartDate && this.nextOutboxPage) { + messages.push(...(await this.outboxAsChat())); + } + } + return { + messages, + more: this.nextOutboxPage || this.nextInboxPage + }; + } + + postActivity(activity) { + if (!this.trustedIRI(this.actor.outbox)) { + throw new Error("Inavlid outbox address"); + } + return window.fetch(this.actor.outbox, { + method: "POST", + headers: { + "Content-Type": Activities.JSONLDMime, + Authorization: `Bearer ${this.token}` + }, + body: JSON.stringify(activity) + }); + } + + note(content, to, isPublic, summary) { + const obj = { + content, + type: "Note", + attributedTo: this.actor.id, + context: this.place, + to: [this.actor.followers, ...to] + }; + if (summary) { + obj.summary = summary; + } + if (isPublic) { + obj.to.push(Activities.PublicAddress); + } + return this.postActivity(obj); + } + + image(url, to, isPublic, summary) { + const obj = { + url, + type: "Image", + attributedTo: this.actor.id, + context: this.place, + to: [this.actor.followers, ...to] + }; + if (summary) { + obj.summary = summary; + } + if (isPublic) { + obj.to.push(Activities.PublicAddress); + } + return this.postActivity(obj); + } + + video(url, to, isPublic, summary) { + const obj = { + url, + type: "Video", + attributedTo: this.actor.id, + context: this.place, + to: [this.actor.followers, ...to] + }; + if (summary) { + obj.summary = summary; + } + if (isPublic) { + obj.to.push(Activities.PublicAddress); + } + return this.postActivity(obj); + } + + activityAsChat(activity, outbox = false) { + const message = { + isImmersFeed: true, + isFriend: this.friends.some(status => status.actor.id === activity.actor.id), + sent: outbox, + context: activity.object?.context, + timestamp: new Date(activity.published).getTime(), + name: activity.actor.name, + sessionId: activity.actor.id, + icon: activity.actor.icon, + immer: new URL(activity.actor.id).hostname + }; + switch (activity.object?.type) { + case "Note": + message.type = "chat"; + message.body = activity.object.content; + break; + case "Image": + message.type = "photo"; + message.body = { src: activity.object.url }; + break; + case "Video": + message.type = "video"; + message.body = { src: activity.object.url }; + break; + default: + if (activity.summary) { + message.type = "activity"; + message.body = activity.summary; + } + } + return message; + } +} From 5d10f5088ac706a2a42abac7e9d49bbf541bb203 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Tue, 2 Mar 2021 21:20:26 -0600 Subject: [PATCH 059/167] bump client version to match immers server --- src/utils/immers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 0071b74a28..8d8a8eabd3 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -4,7 +4,7 @@ import { fetchAvatar } from "./avatar-utils"; import { setupMonetization } from "./immers/monetization"; import Activities from "./immers/activities"; const localImmer = configs.IMMERS_SERVER; -console.log("immers.space client v0.4.1"); +console.log("immers.space client v0.5.0"); const jsonldMime = "application/activity+json"; // avoid race between auth and initialize code let resolveAuth; From c6a020accb55b269d3dfbe461b55bf6d07b8a068 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Tue, 2 Mar 2021 21:28:13 -0600 Subject: [PATCH 060/167] Immers feed & chat integration in React UI - Consolidate immers react code - Add immers space friend icon to in-room friends in people list and add tooltip Chat features (will not appear until core-side of code is merged) - Chat components to show immers space messages swith sender profile icon and location - Processing for immers messages in hubs chat system with time-ordered insertion - Load more button at top of chat scroll to fetch additional history --- src/hub.js | 2 +- src/react-components/room/ChatSidebar.js | 2 +- .../room/ChatSidebarContainer.js | 30 +++- src/react-components/room/ImmersReact.js | 133 ++++++++++++++++++ src/react-components/room/ImmersReact.scss | 49 +++++++ src/react-components/room/PeopleSidebar.js | 39 ++--- 6 files changed, 224 insertions(+), 31 deletions(-) create mode 100644 src/react-components/room/ImmersReact.js create mode 100644 src/react-components/room/ImmersReact.scss diff --git a/src/hub.js b/src/hub.js index 62014cf061..1f8bcbb67c 100644 --- a/src/hub.js +++ b/src/hub.js @@ -1554,5 +1554,5 @@ document.addEventListener("DOMContentLoaded", async () => { authChannel.setSocket(socket); linkChannel.setSocket(socket); - immers.initialize(store, scene, remountUI); + immers.initialize(store, scene, remountUI, messageDispatch); }); diff --git a/src/react-components/room/ChatSidebar.js b/src/react-components/room/ChatSidebar.js index 23960e0bbf..2a15a4cc9b 100644 --- a/src/react-components/room/ChatSidebar.js +++ b/src/react-components/room/ChatSidebar.js @@ -246,7 +246,7 @@ MessageBubble.propTypes = { children: PropTypes.node }; -function getMessageComponent(message) { +export function getMessageComponent(message) { switch (message.type) { case "chat": { const { formattedBody, monospace, emoji } = formatMessageBody(message.body); diff --git a/src/react-components/room/ChatSidebarContainer.js b/src/react-components/room/ChatSidebarContainer.js index a5c423b4e2..32b997edcb 100644 --- a/src/react-components/room/ChatSidebarContainer.js +++ b/src/react-components/room/ChatSidebarContainer.js @@ -15,6 +15,7 @@ import { useMaintainScrollPosition } from "../misc/useMaintainScrollPosition"; import { spawnChatMessage } from "../chat-message"; import { discordBridgesForPresences } from "../../utils/phoenix-utils"; import { useIntl } from "react-intl"; +import { ImmersChatMessage, ImmersMoreHistoryButton } from "./ImmersReact"; const ChatContext = createContext({ messageGroups: [], sendMessage: () => {} }); @@ -42,6 +43,29 @@ function processChatMessage(messageGroups, newMessage) { const now = Date.now(); const { name, sent, sessionId, ...messageProps } = newMessage; + if (messageProps.isImmersFeed) { + // insert according to timestamp + const newMessageGroups = messageGroups.slice(); + const i = newMessageGroups.findIndex(group => messageProps.timestamp < group.timestamp); + newMessageGroups.splice(i === -1 ? newMessageGroups.length : i, 0, { + id: uniqueMessageId++, + isImmersFeed: messageProps.isImmersFeed, + isFriend: messageProps.isFriend, + timestamp: messageProps.timestamp, + sent: sent, + sender: name, + icon: messageProps.icon, + senderSessionId: sessionId, + context: messageProps.context, + immer: messageProps.immer, + messages: [{ id: uniqueMessageId++, ...messageProps }] + }); + return newMessageGroups; + } + // local chat is ignored in favor of immers feed which includes it + if (!sent) { + return messageGroups; + } if (shouldCreateNewMessageGroup(messageGroups, newMessage, now)) { return [ ...messageGroups, @@ -89,6 +113,7 @@ function updateMessageGroups(messageGroups, newMessage) { case "image": case "photo": case "video": + case "activity": return processChatMessage(messageGroups, newMessage); default: return messageGroups; @@ -251,9 +276,12 @@ export function ChatSidebarContainer({ scene, canSpawnMessages, presences, occup return ( - {messageGroups.map(({ id, systemMessage, ...rest }) => { + + {messageGroups.map(({ id, systemMessage, isImmersFeed, ...rest }) => { if (systemMessage) { return ; + } else if (isImmersFeed) { + return ; } else { return ; } diff --git a/src/react-components/room/ImmersReact.js b/src/react-components/room/ImmersReact.js new file mode 100644 index 0000000000..0837144d00 --- /dev/null +++ b/src/react-components/room/ImmersReact.js @@ -0,0 +1,133 @@ +import React, { useEffect, useState } from "react"; +import PropTypes from "prop-types"; +import classNames from "classnames"; +import { getMessageComponent } from "./ChatSidebar"; +import chatStyles from "./ChatSidebar.scss"; +import styles from "./ImmersReact.scss"; +import { FormattedRelativeTime } from "react-intl"; +import { proxiedUrlFor } from "../../utils/media-url-utils"; +import immersLogo from "../../assets/images/immers_logo.png"; + +export function ImmerLink({ place }) { + if (!place) { + return; + } + let placeUrl = place.url; + // inject user handle into desintation url so they don't have to type it + try { + const url = new URL(placeUrl); + if ( + `${url.host}${url.pathname}${url.search}` === + `${window.location.host}${window.location.pathname}${window.location.search}` + ) { + placeUrl = null; + } else { + const search = new URLSearchParams(url.search); + search.set("me", window.APP.store.state.profile.handle); + url.search = search.toString(); + placeUrl = url.toString(); + } + } catch (ignore) { + /* if fail, leave original url unchanged */ + } + return placeUrl ? {place.name ?? "unkown"} : "here"; +} + +ImmerLink.propTypes = { + place: PropTypes.object +}; + +export function ImmersChatMessage({ sent, sender, timestamp, isFriend, icon, immer, context, messages }) { + if (messages[0].type === "activity") { + return ( +
  • +

    + {messages[0].body} |{" "} + +

    +
  • + ); + } + return ( +
  • +

    + {isFriend && } + {icon && } + {sender} + [{immer}] |  |{" "} + +

    +
      {messages.map(message => getMessageComponent(message))}
    +
  • + ); +} + +ImmersChatMessage.propTypes = { + sent: PropTypes.bool, + sender: PropTypes.string, + timestamp: PropTypes.any, + messages: PropTypes.array, + immer: PropTypes.string, + icon: PropTypes.string, + isFriend: PropTypes.bool, + context: PropTypes.object +}; + +export function ImmersImageIcon({ src, title }) { + return ( + + {src && } + + ); +} +ImmersImageIcon.propTypes = { + src: PropTypes.string, + title: PropTypes.string +}; + +export function ImmersFriendIcon() { + return ; +} + +export function ImmersAvatarIcon({ avi }) { + // support both Image objects & direct url + const src = avi.url || avi; + return ; +} +ImmersAvatarIcon.propTypes = { + avi: PropTypes.any +}; + +export function ImmersMoreHistoryButton() { + const [isLoading, setIsLoading] = useState(false); + const [hasMore, setHasMore] = useState(true); + useEffect( + () => { + const onMoreHistory = evt => { + setIsLoading(false); + setHasMore(evt.detail); + }; + window.addEventListener("immers-more-history-loaded", onMoreHistory); + return () => window.removeEventListener("immers-more-history-loaded", onMoreHistory); + }, + [setIsLoading, setHasMore] + ); + const handleClick = evt => { + evt.preventDefault(); + setIsLoading(true); + window.dispatchEvent(new CustomEvent("immers-load-more-history")); + }; + return ( + hasMore && ( +
    + {isLoading ? ( + Loading... + ) : ( + + Load more + + )} +
    + ) + ); +} diff --git a/src/react-components/room/ImmersReact.scss b/src/react-components/room/ImmersReact.scss new file mode 100644 index 0000000000..5184e6e9e0 --- /dev/null +++ b/src/react-components/room/ImmersReact.scss @@ -0,0 +1,49 @@ +@use "../styles/theme.scss"; + +:local(.image-icon-wrapper) { + width: 20px; + height: 20px; + overflow: hidden; +} + +:local(.image-icon) { + width: 20px; +} + +:local(.immer-name) { + color: theme.$grey; +} + +:local(.immer-chat-label) { + align-items: center; +} + +:local(.sent) { + :local(.message-group-label) { + align-self: flex-end; + } + + :local(.message-bubble) { + background-color: theme.$blue; + color: theme.$white; + align-self: flex-end; + + a { + color: theme.$white; + + &:hover { + color: theme.$white-hover; + } + + &:active { + color: theme.$white-pressed; + } + } + } +} + +:local(.history-button) { + text-align: center; + padding-top: 5px; + font-size: theme.$font-size-sm; +} diff --git a/src/react-components/room/PeopleSidebar.js b/src/react-components/room/PeopleSidebar.js index a66e1387a9..f96289bcd2 100644 --- a/src/react-components/room/PeopleSidebar.js +++ b/src/react-components/room/PeopleSidebar.js @@ -12,10 +12,9 @@ import { ReactComponent as VRIcon } from "../icons/VR.svg"; import { ReactComponent as VolumeOffIcon } from "../icons/VolumeOff.svg"; import { ReactComponent as VolumeHighIcon } from "../icons/VolumeHigh.svg"; import { ReactComponent as VolumeMutedIcon } from "../icons/VolumeMuted.svg"; -import immersLogo from "../../assets/images/immers_logo.png"; import { List, ButtonListItem } from "../layout/List"; import { FormattedMessage, useIntl } from "react-intl"; -import { proxiedUrlFor } from "../../utils/media-url-utils"; +import { ImmerLink, ImmersAvatarIcon, ImmersFriendIcon } from "./ImmersReact"; function getDeviceLabel(ctx, intl) { if (ctx) { @@ -86,20 +85,9 @@ function getLocationMessage(activity, myHandle, intl) { switch (activity.type) { case "Arrive": { const onlineMsg = intl.formatMessage({ id: "people-sidebar.immers.online", defaultMessage: "Online at" }); - let placeUrl = activity.target?.url; - // inject user handle into desintation url so they don't have to type it - try { - const url = new URL(placeUrl); - const search = new URLSearchParams(url.search); - search.set("me", myHandle); - url.search = search.toString(); - placeUrl = url.toString(); - } catch (ignore) { - /* if fail, leave original url unchanged */ - } return ( - {onlineMsg} {activity.target?.name ?? "unkown"} + {onlineMsg} ); } @@ -110,14 +98,6 @@ function getLocationMessage(activity, myHandle, intl) { } } -function imageIcon(src) { - return ( - - {src && } - - ); -} - function getPersonName(person, intl) { const you = intl.formatMessage({ id: "people-sidebar.person-name.you", @@ -163,12 +143,15 @@ export function PeopleSidebar({ people, onSelectPerson, onClose, showMuteAll, on onClick={e => onSelectPerson(person, e)} disabled={person.remote} > - {person.remote ? imageIcon(immersLogo) : } - {person.remote - ? imageIcon(person.friendStatus.actor.icon) - : !person.context.discord && - !person.remote && - VoiceIcon && } + {person.friendStatus && } + {!person.remote && } + {person.remote ? ( + + ) : ( + !person.context.discord && + !person.remote && + VoiceIcon && + )}

    {getPersonName(person, intl)}

    {person.roles.owner && ( Date: Tue, 2 Mar 2021 21:50:49 -0600 Subject: [PATCH 061/167] Revert "bump version" This reverts commit 6553ba609a963d8ae9ac275a3a6ccc7de059e082. --- src/utils/immers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 7282832a2d..7a0ba47258 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -3,7 +3,7 @@ import configs from "./configs"; import { fetchAvatar } from "./avatar-utils"; import { setupMonetization } from "./immers/monetization"; const localImmer = configs.IMMERS_SERVER; -console.log("immers.space client v0.4.1"); +console.log("immers.space client v0.4.0"); const jsonldMime = "application/activity+json"; // avoid race between auth and initialize code let resolveAuth; From 8983b466880a09345381599c9890332ef7cf14f5 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 5 Mar 2021 11:27:27 -0600 Subject: [PATCH 062/167] also push live feed to VR mode users --- src/utils/immers.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 8d8a8eabd3..ca5c016438 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -302,7 +302,7 @@ export async function auth(store) { } } -export async function initialize(store, scene, remountUI, messageDispatch) { +export async function initialize(store, scene, remountUI, messageDispatch, createInWorldLogMessage) { hubScene = scene; localPlayer = document.getElementById("avatar-rig"); // immers profile @@ -441,9 +441,12 @@ export async function initialize(store, scene, remountUI, messageDispatch) { // stream new activity while in room immerSocket.on("inbox-update", activity => { activity = JSON.parse(activity); - if (activity.type === "Create") { - const detail = activities.activityAsChat(activity); - messageDispatch.dispatchEvent(new CustomEvent("message", { detail })); + const message = activities.activityAsChat(activity); + if (message.body) { + messageDispatch.dispatchEvent(new CustomEvent("message", { detail: message })); + if (scene.is("vr-mode")) { + createInWorldLogMessage(message); + } } }); // intercept outgoing messages and post to immers space feed From 3038d7e5d32e4841d9f61245e32eab0856015f9c Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 5 Mar 2021 11:28:59 -0600 Subject: [PATCH 063/167] hook into in-world message system for VR users --- src/hub.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/hub.js b/src/hub.js index 1f8bcbb67c..f59ced224d 100644 --- a/src/hub.js +++ b/src/hub.js @@ -1478,9 +1478,10 @@ document.addEventListener("DOMContentLoaded", async () => { sent: session_id === socket.params().session_id }; - if (scene.is("vr-mode")) { - createInWorldLogMessage(incomingMessage); - } + // replaced by immers feed + // if (scene.is("vr-mode")) { + // createInWorldLogMessage(incomingMessage); + // } messageDispatch.receive(incomingMessage); }); @@ -1554,5 +1555,5 @@ document.addEventListener("DOMContentLoaded", async () => { authChannel.setSocket(socket); linkChannel.setSocket(socket); - immers.initialize(store, scene, remountUI, messageDispatch); + immers.initialize(store, scene, remountUI, messageDispatch, createInWorldLogMessage); }); From e2eda91f05f73485074acde86f7d270929aaded9 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 5 Mar 2021 15:11:43 -0600 Subject: [PATCH 064/167] friend behavior: require manual acceptance of follow requests via immers-follow-button --- src/components/immers/immers-follow-button.js | 61 ++++++++++++++----- src/components/player-info.js | 2 +- src/utils/immers.js | 58 ++++++++++-------- src/utils/immers/activities.js | 18 ++++++ 4 files changed, 98 insertions(+), 41 deletions(-) diff --git a/src/components/immers/immers-follow-button.js b/src/components/immers/immers-follow-button.js index 9367dc2dd4..c2f9b0c288 100644 --- a/src/components/immers/immers-follow-button.js +++ b/src/components/immers/immers-follow-button.js @@ -4,21 +4,41 @@ * @component immers-follow-button */ AFRAME.registerComponent("immers-follow-button", { + schema: { relation: { type: "string", default: "none" } }, init() { NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { this.playerEl = networkedEl; this.playerEl.addEventListener("stateadded", this.onState); - if (this.playerEl.is("friend")) { - this.setFriend(); + if (this.playerEl.is("immers-follow-friend")) { + this.el.setAttribute("immers-follow-button", { relation: "friend" }); } }); this.textEl = this.el.querySelector("[text]"); + // avoid accidental double clicks + let lastClickTime = 0; this.onClick = () => { - this.follow(this.playerEl.components["player-info"].data.immersId); + const now = Date.now(); + if (now - lastClickTime < 500) { + return; + } + lastClickTime = now; + switch (this.data.relation) { + case "none": + this.action("immers-follow", "pending"); + break; + case "request": + this.action("immers-follow-accept", "friend"); + break; + case "friend": + // TODO: unfriend + // this.action("immers-follow-reject", "none"); + break; + } }; this.onState = event => { - if (event.detail === "friend") { - this.setFriend(); + const friendState = event.detail.split("immers-follow-")[1]; + if (friendState) { + this.el.setAttribute("immers-follow-button", { relation: friendState }); } }; }, @@ -30,6 +50,24 @@ AFRAME.registerComponent("immers-follow-button", { } }, + update() { + let newText; + switch (this.data.relation) { + case "request": + newText = "Accept friend"; + break; + case "pending": + newText = "Reqest sent"; + break; + case "friend": + newText = "Unfriend"; + break; + default: + newText = "Add friend"; + } + this.textEl.setAttribute("text", "value", newText); + }, + pause() { this.el.object3D.removeEventListener("interact", this.onClick); if (this.playerEl) { @@ -37,14 +75,9 @@ AFRAME.registerComponent("immers-follow-button", { } }, - follow(targetId) { - if (!this.playerEl.is("friend")) { - this.el.emit("immers-follow", targetId); - this.textEl.setAttribute("text", "value", "Pending"); - } - }, - - setFriend() { - this.textEl.setAttribute("text", "value", "Unfollow"); + action(eventName, newRelation) { + const targetId = this.playerEl.getAttribute("player-info").immersId; + this.el.emit(eventName, targetId); + this.el.setAttribute("immers-follow-button", { relation: newRelation }); } }); diff --git a/src/components/player-info.js b/src/components/player-info.js index c015e5d0ec..b6e8142546 100644 --- a/src/components/player-info.js +++ b/src/components/player-info.js @@ -114,7 +114,7 @@ AFRAME.registerComponent("player-info", { update(oldData) { this.applyProperties(); - if (this.data.immersId !== oldData.immersId) { + if (oldData.immersId !== undefined && this.data.immersId !== oldData.immersId) { this.el.emit("immers-id-changed", this.data.immersId); this.el.sceneEl.emit("immers-player-monetization", { monetized: false, diff --git a/src/utils/immers.js b/src/utils/immers.js index ca5c016438..69dba20591 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -4,7 +4,7 @@ import { fetchAvatar } from "./avatar-utils"; import { setupMonetization } from "./immers/monetization"; import Activities from "./immers/activities"; const localImmer = configs.IMMERS_SERVER; -console.log("immers.space client v0.5.0"); +console.log("immers.space client v0.6.0"); const jsonldMime = "application/activity+json"; // avoid race between auth and initialize code let resolveAuth; @@ -95,15 +95,6 @@ export function updateProfile(actorObj, update) { return postActivity(actorObj.outbox, activity); } -export function follow(actorObj, targetId) { - return postActivity(actorObj.outbox, { - type: "Follow", - actor: actorObj.id, - object: targetId, - to: targetId - }); -} - export function arrive(actorObj) { return postActivity(actorObj.outbox, { type: "Arrive", @@ -360,6 +351,24 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat // friends list let friendsCol; + const setFriendState = (immersId, el) => { + // friends not loaded or is myself + if (!friendsCol || immersId === actorObj.id) { + return; + } + const friendStatus = friendsCol.orderedItems.find(act => act.actor.id === immersId); + // inReplyTo on a follow means it is a followback, don't show accept prompt (already a friend) + if (friendStatus?.type === "Follow" && !friendStatus?.inReplyTo) { + el.removeState("immers-follow-friend"); + el.addState("immers-follow-request"); + } else if (friendStatus) { + el.removeState("immers-follow-request"); + el.addState("immers-follow-friend"); + } else { + el.removeState("immers-follow-request"); + el.removeState("immers-follow-friend"); + } + }; const updateFriends = async () => { if (store.state.profile.id) { const profile = store.state.profile; @@ -368,13 +377,7 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat remountUI({ friends: friendsCol.orderedItems, handle: profile.handle }); // update follow button for new friends const players = window.APP.componentRegistry["player-info"]; - if (players) { - players.forEach(infoComp => { - if (friendsCol.orderedItems.some(act => act.actor.id === infoComp.data.immersId)) { - infoComp.el.addState("friend"); - } - }); - } + players?.forEach(infoComp => setFriendState(infoComp.data.immersId, infoComp.el)); } }; updateFriends(); @@ -403,21 +406,24 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat }); // entity interactions - scene.addEventListener("immers-id-changed", event => { - if (!friendsCol) { - return; - } - if (friendsCol.orderedItems.some(act => act.actor.id === event.detail)) { - event.target.addState("friend"); - } - }); + scene.addEventListener("immers-id-changed", event => setFriendState(event.detail, event.target)); scene.addEventListener("immers-follow", event => { if (!event.detail) { return; } - follow(store.state.profile, event.detail).catch(err => console.err("Error sending follow request:", err.message)); + activities.follow(event.detail).catch(err => console.err("Error sending follow request:", err.message)); + }); + scene.addEventListener("immers-follow-accept", event => { + const follow = friendsCol.orderedItems.find(act => act.actor.id === event.detail && act.type === "Follow"); + if (!follow) { + return; + } + activities.accept(follow).catch(err => console.err("Error sending follow accept:", err.message)); }); + // TODO: unfriend + // scene.addEventListener("immers-follow-reject", event => { + // }); setupMonetization(hubScene, localPlayer); diff --git a/src/utils/immers/activities.js b/src/utils/immers/activities.js index cbe5a99052..8da7634b51 100644 --- a/src/utils/immers/activities.js +++ b/src/utils/immers/activities.js @@ -112,6 +112,24 @@ export default class Activities { }); } + accept(follow) { + return this.postActivity({ + type: "Accept", + actor: this.actor.id, + object: follow.id, + to: follow.actor + }); + } + + follow(targetId) { + return this.postActivity({ + type: "Follow", + actor: this.actor.id, + object: targetId, + to: targetId + }); + } + note(content, to, isPublic, summary) { const obj = { content, From 9ddd3841d55b6a8995fed1c1b085f99ad4bf9071 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 5 Mar 2021 16:06:14 -0600 Subject: [PATCH 065/167] Load new players into pending request state if they have one, fix text button overflow --- src/components/immers/immers-follow-button.js | 2 ++ src/hub.html | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/immers/immers-follow-button.js b/src/components/immers/immers-follow-button.js index c2f9b0c288..387d3e13aa 100644 --- a/src/components/immers/immers-follow-button.js +++ b/src/components/immers/immers-follow-button.js @@ -11,6 +11,8 @@ AFRAME.registerComponent("immers-follow-button", { this.playerEl.addEventListener("stateadded", this.onState); if (this.playerEl.is("immers-follow-friend")) { this.el.setAttribute("immers-follow-button", { relation: "friend" }); + } else if (this.playerEl.is("immers-follow-request")) { + this.el.setAttribute("immers-follow-button", { relation: "request" }); } }); this.textEl = this.el.querySelector("[text]"); diff --git a/src/hub.html b/src/hub.html index 62ffe62355..af6923facb 100644 --- a/src/hub.html +++ b/src/hub.html @@ -160,7 +160,7 @@ - + From 0c7b596de754fc1c42fda6f09eac5bfc0f5bbda0 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 6 Mar 2021 22:36:12 -0600 Subject: [PATCH 066/167] unfriend action & update friends status in response to unfriend --- src/components/immers/immers-follow-button.js | 5 ++--- src/utils/immers.js | 17 +++++++++++------ src/utils/immers/activities.js | 9 +++++++++ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/components/immers/immers-follow-button.js b/src/components/immers/immers-follow-button.js index 387d3e13aa..d2790268bf 100644 --- a/src/components/immers/immers-follow-button.js +++ b/src/components/immers/immers-follow-button.js @@ -4,7 +4,7 @@ * @component immers-follow-button */ AFRAME.registerComponent("immers-follow-button", { - schema: { relation: { type: "string", default: "none" } }, + schema: { relation: { type: "string", default: "none", oneOf: ["none", "request", "friend", "pending"] } }, init() { NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { this.playerEl = networkedEl; @@ -32,8 +32,7 @@ AFRAME.registerComponent("immers-follow-button", { this.action("immers-follow-accept", "friend"); break; case "friend": - // TODO: unfriend - // this.action("immers-follow-reject", "none"); + this.action("immers-follow-reject", "none"); break; } }; diff --git a/src/utils/immers.js b/src/utils/immers.js index 69dba20591..79bff3d3ba 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -360,13 +360,16 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat // inReplyTo on a follow means it is a followback, don't show accept prompt (already a friend) if (friendStatus?.type === "Follow" && !friendStatus?.inReplyTo) { el.removeState("immers-follow-friend"); + el.removeState("immers-follow-none"); el.addState("immers-follow-request"); - } else if (friendStatus) { + } else if (friendStatus && friendStatus.type !== "Reject") { el.removeState("immers-follow-request"); + el.removeState("immers-follow-none"); el.addState("immers-follow-friend"); } else { el.removeState("immers-follow-request"); el.removeState("immers-follow-friend"); + el.addState("immers-follow-none"); } }; const updateFriends = async () => { @@ -374,7 +377,7 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat const profile = store.state.profile; friendsCol = await getFriends(profile); activities.friends = friendsCol.orderedItems; - remountUI({ friends: friendsCol.orderedItems, handle: profile.handle }); + remountUI({ friends: friendsCol.orderedItems.filter(act => act.type !== "Reject"), handle: profile.handle }); // update follow button for new friends const players = window.APP.componentRegistry["player-info"]; players?.forEach(infoComp => setFriendState(infoComp.data.immersId, infoComp.el)); @@ -412,7 +415,7 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat if (!event.detail) { return; } - activities.follow(event.detail).catch(err => console.err("Error sending follow request:", err.message)); + activities.follow(event.detail).catch(err => console.error("Error sending follow request:", err.message)); }); scene.addEventListener("immers-follow-accept", event => { const follow = friendsCol.orderedItems.find(act => act.actor.id === event.detail && act.type === "Follow"); @@ -421,9 +424,11 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat } activities.accept(follow).catch(err => console.err("Error sending follow accept:", err.message)); }); - // TODO: unfriend - // scene.addEventListener("immers-follow-reject", event => { - // }); + // unfriend + scene.addEventListener("immers-follow-reject", event => { + // server converts actorId to followId for reject object + activities.reject(event.detail, event.detail).catch(err => console.error("Error sending unfollow:", err.message)); + }); setupMonetization(hubScene, localPlayer); diff --git a/src/utils/immers/activities.js b/src/utils/immers/activities.js index 8da7634b51..f800177c7a 100644 --- a/src/utils/immers/activities.js +++ b/src/utils/immers/activities.js @@ -121,6 +121,15 @@ export default class Activities { }); } + reject(objectId, recipientId) { + return this.postActivity({ + type: "Reject", + actor: this.actor.id, + object: objectId, + to: recipientId + }); + } + follow(targetId) { return this.postActivity({ type: "Follow", From e89ffacc4ffefdc6250edf59051a1ce9ac5b7a7e Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 7 Mar 2021 17:54:13 -0600 Subject: [PATCH 067/167] hotfix UI crash when trying to render posts from Mastodon --- src/react-components/room/ImmersReact.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-components/room/ImmersReact.js b/src/react-components/room/ImmersReact.js index 0837144d00..9bb6a9ffe5 100644 --- a/src/react-components/room/ImmersReact.js +++ b/src/react-components/room/ImmersReact.js @@ -10,7 +10,7 @@ import immersLogo from "../../assets/images/immers_logo.png"; export function ImmerLink({ place }) { if (!place) { - return; + return null; } let placeUrl = place.url; // inject user handle into desintation url so they don't have to type it From 5b8d110155d9d5d622ceb16ca59b91cc8648df26 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 7 Mar 2021 22:06:02 -0600 Subject: [PATCH 068/167] hotfix people list ui crash if friend has no icon --- src/react-components/room/PeopleSidebar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-components/room/PeopleSidebar.js b/src/react-components/room/PeopleSidebar.js index f96289bcd2..9e0d532700 100644 --- a/src/react-components/room/PeopleSidebar.js +++ b/src/react-components/room/PeopleSidebar.js @@ -146,7 +146,7 @@ export function PeopleSidebar({ people, onSelectPerson, onClose, showMuteAll, on {person.friendStatus && } {!person.remote && } {person.remote ? ( - + person.friendStatus?.actor?.icon && ) : ( !person.context.discord && !person.remote && From bf4cdcc3013c1cba71df2fe64fe390a52cd51a6b Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 7 Mar 2021 23:13:45 -0600 Subject: [PATCH 069/167] hotfix cross-immer media chats need proxy --- src/react-components/room/ImmersReact.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/react-components/room/ImmersReact.js b/src/react-components/room/ImmersReact.js index 9bb6a9ffe5..fc7461ad2f 100644 --- a/src/react-components/room/ImmersReact.js +++ b/src/react-components/room/ImmersReact.js @@ -7,6 +7,7 @@ import styles from "./ImmersReact.scss"; import { FormattedRelativeTime } from "react-intl"; import { proxiedUrlFor } from "../../utils/media-url-utils"; import immersLogo from "../../assets/images/immers_logo.png"; +import merge from "deepmerge"; export function ImmerLink({ place }) { if (!place) { @@ -57,7 +58,7 @@ export function ImmersChatMessage({ sent, sender, timestamp, isFriend, icon, imm [{immer}] |  |{" "}

    -
      {messages.map(message => getMessageComponent(message))}
    +
      {messages.map(message => proxyAndGetMessageComponent(message))}
    ); } @@ -68,7 +69,10 @@ ImmersChatMessage.propTypes = { timestamp: PropTypes.any, messages: PropTypes.array, immer: PropTypes.string, - icon: PropTypes.string, + icon: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object + ]), isFriend: PropTypes.bool, context: PropTypes.object }; @@ -131,3 +135,12 @@ export function ImmersMoreHistoryButton() { ) ); } + +function proxyAndGetMessageComponent(message) { + // media urls need proxy to pass CSP & CORS + if (message.body?.src) { + message = merge({}, message); + message.body.src = proxiedUrlFor(message.body.src); + } + return getMessageComponent(message); +} \ No newline at end of file From 5e89b3bea76da02c74dff196ee862779b9b9f06d Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 20 Mar 2021 17:37:49 -0500 Subject: [PATCH 070/167] reconnection reliability: bump socketio for fewer reconnects (longer + more accurate timeout) and re-arrive when when reconnection occurs --- package-lock.json | 311 +++++++++++++++----------------------------- package.json | 2 +- src/utils/immers.js | 6 +- 3 files changed, 106 insertions(+), 213 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ad994c1f0..d43d3571bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4549,6 +4549,11 @@ "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", "dev": true }, + "@types/component-emitter": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", + "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==" + }, "@types/debug": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", @@ -4611,9 +4616,9 @@ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" }, "@types/react": { - "version": "16.9.44", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.44.tgz", - "integrity": "sha512-BtLoJrXdW8DVZauKP+bY4Kmiq7ubcJq+H/aCpRfvPF7RAT3RwR73Sg8szdc2YasbAlWBDrQ6Q+AFM0KwtQY+WQ==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz", + "integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==", "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4991,11 +4996,6 @@ "resolved": "https://registry.yarnpkg.com/aframe-slice9-component/-/aframe-slice9-component-1.0.0.tgz", "integrity": "sha1-+w+EQdrdHosRzCRRK6eqaS1iK+E=" }, - "after": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" - }, "ajv": { "version": "6.5.2", "resolved": "https://registry.yarnpkg.com/ajv/-/ajv-6.5.2.tgz", @@ -5179,11 +5179,6 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, - "arraybuffer.slice": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" - }, "arrify": { "version": "1.0.1", "resolved": "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz", @@ -5286,7 +5281,8 @@ "async-limiter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true }, "asynckit": { "version": "0.4.0", @@ -6951,11 +6947,6 @@ } } }, - "base64-arraybuffer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" - }, "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", @@ -6976,14 +6967,6 @@ "tweetnacl": "^0.14.3" } }, - "better-assert": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", - "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", - "requires": { - "callsite": "1.0.0" - } - }, "bfj": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.1.tgz", @@ -7023,11 +7006,6 @@ "integrity": "sha1-6C5D6OsXBkaB5D+cjbxzHjF9GJI=", "dev": true }, - "blob": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", - "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" - }, "bluebird": { "version": "3.5.1", "resolved": "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz", @@ -7085,12 +7063,6 @@ "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "dev": true }, - "bowser": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", - "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", - "dev": true - }, "boxen": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", @@ -7441,11 +7413,6 @@ "caller-callsite": "^2.0.0" } }, - "callsite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", - "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" - }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -7892,20 +7859,11 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, - "component-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" - }, "component-emitter": { "version": "1.2.1", "resolved": "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" - }, - "component-inherit": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true }, "compressible": { "version": "2.0.16", @@ -9227,64 +9185,6 @@ "once": "^1.4.0" } }, - "engine.io-client": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.3.tgz", - "integrity": "sha512-0NGY+9hioejTEJCaSJZfWZLk4FPI9dN+1H1C4+wj2iuFba47UgZbJzfWs4aNFajnX/qAaYKbe2lLTfEEWzCmcw==", - "requires": { - "component-emitter": "~1.3.0", - "component-inherit": "0.0.3", - "debug": "~4.1.0", - "engine.io-parser": "~2.2.0", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "ws": "~6.1.0", - "xmlhttprequest-ssl": "~1.5.4", - "yeast": "0.1.2" - }, - "dependencies": { - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "ws": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", - "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", - "requires": { - "async-limiter": "~1.0.0" - } - } - } - }, - "engine.io-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz", - "integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==", - "requires": { - "after": "0.8.2", - "arraybuffer.slice": "~0.0.7", - "base64-arraybuffer": "0.1.5", - "blob": "0.0.5", - "has-binary2": "~1.0.2" - } - }, "enhanced-resolve": { "version": "4.1.0", "resolved": "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", @@ -10709,21 +10609,6 @@ "ansi-regex": "^2.0.0" } }, - "has-binary2": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", - "requires": { - "isarray": "2.0.1" - }, - "dependencies": { - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" - } - } - }, "has-cors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", @@ -11285,7 +11170,7 @@ }, "http-deceiver": { "version": "1.2.7", - "resolved": "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", "dev": true }, @@ -11537,11 +11422,6 @@ "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", "dev": true }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" - }, "infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", @@ -11937,8 +11817,8 @@ }, "is-path-in-cwd": { "version": "1.0.1", - "resolved": "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha1-WsSLNF72dTOb1sekipEhELJBz1I=", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", "dev": true, "requires": { "is-path-inside": "^1.0.0" @@ -11955,14 +11835,14 @@ }, "is-plain-obj": { "version": "1.1.0", - "resolved": "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", "dev": true }, "is-plain-object": { "version": "2.0.4", - "resolved": "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, "requires": { "isobject": "^3.0.1" @@ -12785,7 +12665,7 @@ "debug": "^4.2.0", "events": "^3.2.0", "h264-profile-level-id": "^1.0.1", - "sdp-transform": "^2.14.1", + "sdp-transform": "^2.14.0", "supports-color": "^7.2.0" }, "dependencies": { @@ -13191,7 +13071,8 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "multicast-dns": { "version": "6.2.3", @@ -13609,11 +13490,6 @@ "resolved": "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, - "object-component": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", - "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" - }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz", @@ -14155,14 +14031,6 @@ "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", "dev": true }, - "parseqs": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", - "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", - "requires": { - "better-assert": "~1.0.0" - } - }, "parserlib": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parserlib/-/parserlib-1.1.1.tgz", @@ -14170,14 +14038,6 @@ "dev": true, "optional": true }, - "parseuri": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", - "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", - "requires": { - "better-assert": "~1.0.0" - } - }, "parseurl": { "version": "1.3.2", "resolved": "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz", @@ -16617,63 +16477,110 @@ } }, "socket.io-client": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", - "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", - "requires": { - "backo2": "1.0.2", - "base64-arraybuffer": "0.1.5", - "component-bind": "1.0.0", - "component-emitter": "1.2.1", - "debug": "~4.1.0", - "engine.io-client": "~3.4.0", - "has-binary2": "~1.0.2", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "object-component": "0.0.3", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "socket.io-parser": "~3.3.0", - "to-array": "0.1.4" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.0.0.tgz", + "integrity": "sha512-27yQxmXJAEYF19Ygyl8FPJ0if0wegpSmkIIbrWJeI7n7ST1JyH8bbD5v3fjjGY5cfCanACJ3dARUAyiVFNrlTQ==", + "requires": { + "@types/component-emitter": "^1.2.10", + "backo2": "~1.0.2", + "component-emitter": "~1.3.0", + "debug": "~4.3.1", + "engine.io-client": "~5.0.0", + "parseuri": "0.0.6", + "socket.io-parser": "~4.0.4" }, "dependencies": { + "base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" + } + }, + "engine.io-client": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.0.0.tgz", + "integrity": "sha512-e6GK0Fqvq45Nu/j7YdIVqXtDPvlsggAcfml3QiEiGdJ1qeh7IQU6knxSN3+yy9BmbnXtIfjo1hK4MFyHKdc9mQ==", + "requires": { + "base64-arraybuffer": "0.1.4", + "component-emitter": "~1.3.0", + "debug": "~4.3.1", + "engine.io-parser": "~4.0.1", + "has-cors": "1.1.0", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~7.4.2", + "yeast": "0.1.2" + } + }, + "engine.io-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz", + "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==", + "requires": { + "base64-arraybuffer": "0.1.4" } }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" + }, + "parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" + }, + "ws": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", + "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==" } } }, "socket.io-parser": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", - "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", "requires": { - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "isarray": "2.0.1" + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" }, "dependencies": { + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "2.0.0" + "ms": "2.1.2" } }, - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -18332,11 +18239,6 @@ "os-tmpdir": "~1.0.2" } }, - "to-array": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" - }, "to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", @@ -20192,11 +20094,6 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, - "xmlhttprequest-ssl": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", - "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" - }, "xregexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", diff --git a/package.json b/package.json index c45e5dccb4..0b05844c8e 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "react-router-dom": "^5.1.2", "screenfull": "^4.0.1", "semver": "^7.3.2", - "socket.io-client": "^2.3.0", + "socket.io-client": "^4.0.0", "three": "github:mozillareality/three.js#hubs/master", "three-ammo": "^1.0.12", "three-bmfont-text": "github:mozillareality/three-bmfont-text#hubs/master", diff --git a/src/utils/immers.js b/src/utils/immers.js index 79bff3d3ba..f4cd78f758 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -328,12 +328,8 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat } } }); - let hasArrived; immerSocket.on("connect", () => { - if (hasArrived) { - return; - } - hasArrived = true; + // will also send on reconnect to ensure you show as online arrive(actorObj); immerSocket.emit("entered", { // prepare a leave activity to be fired on disconnect From 9cb2c27a09c12f609b46f319e4880f7a150e9dab Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 21 Mar 2021 21:32:48 -0500 Subject: [PATCH 071/167] bump version --- src/utils/immers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index f4cd78f758..0ec329fff3 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -4,7 +4,7 @@ import { fetchAvatar } from "./avatar-utils"; import { setupMonetization } from "./immers/monetization"; import Activities from "./immers/activities"; const localImmer = configs.IMMERS_SERVER; -console.log("immers.space client v0.6.0"); +console.log("immers.space client v0.7.0"); const jsonldMime = "application/activity+json"; // avoid race between auth and initialize code let resolveAuth; From 9756cec199db6518a54ef77a2baf56159d645f5f Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Mon, 29 Mar 2021 22:14:22 -0500 Subject: [PATCH 072/167] hotfix high quality level by default on Quest due to importing immers before AFRAME, leading to changing the import order of hubs dependencies and trying to determine device before AFRAME is loaded --- src/hub.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hub.js b/src/hub.js index f59ced224d..353a9c363b 100644 --- a/src/hub.js +++ b/src/hub.js @@ -3,7 +3,6 @@ import "./webxr-bypass-hacks"; import configs from "./utils/configs"; import "./utils/theme"; import "@babel/polyfill"; -import * as immers from "./utils/immers"; console.log(`App version: ${process.env.BUILD_VERSION || "?"}`); @@ -165,6 +164,7 @@ import "./gltf-component-mappings"; import { App } from "./App"; import MediaDevicesManager from "./utils/media-devices-manager"; import { platformUnsupported } from "./support"; +import * as immers from "./utils/immers"; window.APP = new App(); window.APP.RENDER_ORDER = { From 47519f3985bf67e97063fd0b60da5017a5713f3f Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 2 Apr 2021 14:42:44 -0500 Subject: [PATCH 073/167] connect monetization state to UI, allow bypass room size when monetized, explain monetization on entry modal --- src/utils/immers.js | 4 ++-- src/utils/immers/monetization.js | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 0ec329fff3..f558acd72d 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -4,7 +4,7 @@ import { fetchAvatar } from "./avatar-utils"; import { setupMonetization } from "./immers/monetization"; import Activities from "./immers/activities"; const localImmer = configs.IMMERS_SERVER; -console.log("immers.space client v0.7.0"); +console.log("immers.space client v0.7.1"); const jsonldMime = "application/activity+json"; // avoid race between auth and initialize code let resolveAuth; @@ -426,7 +426,7 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat activities.reject(event.detail, event.detail).catch(err => console.error("Error sending unfollow:", err.message)); }); - setupMonetization(hubScene, localPlayer); + setupMonetization(hubScene, localPlayer, remountUI); // news feed and chat integration, behind a feature switch as it needs the new hubs ui if (messageDispatch) { diff --git a/src/utils/immers/monetization.js b/src/utils/immers/monetization.js index d3d6470299..f8ab36b6fb 100644 --- a/src/utils/immers/monetization.js +++ b/src/utils/immers/monetization.js @@ -23,17 +23,20 @@ const monetization = { }; let localPlayer; let hubScene; +let updateUI; // sync player's monetization status with room via player-info component function onMonetizationStart() { monetization.state = "started"; localPlayer.setAttribute("player-info", { monetized: true }); hubScene.emit("immers-monetization-started"); + updateUI({ isMonetized: true }); } function onMonetizationStop() { monetization.state = "stopped"; localPlayer.setAttribute("player-info", { monetized: false }); hubScene.emit("immers-monetization-stopped"); + updateUI({ isMonetized: false }); } // tallies total amount paid during curent session @@ -64,9 +67,10 @@ function onSceneLoaded() { // wait until scene is fully loaded to trigger monetization events so creators don't // have to worry about whether entities are loaded -export function setupMonetization(scene, player) { +export function setupMonetization(scene, player, remountUI) { hubScene = scene; localPlayer = player; + updateUI = remountUI; if (hubScene.is("loaded")) { onSceneLoaded(); } else { From e3ecd960837ef8560b436dc8ce3c650863232d88 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 2 Apr 2021 14:42:44 -0500 Subject: [PATCH 074/167] connect monetization state to UI, allow bypass room size when monetized, explain monetization on entry modal --- src/react-components/icons/wm-icon.svg | 6 +++++ src/react-components/room/RoomEntryModal.js | 26 ++++++++++++++++++- src/react-components/room/RoomEntryModal.scss | 9 +++++++ src/react-components/ui-root.js | 12 ++++++--- 4 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 src/react-components/icons/wm-icon.svg diff --git a/src/react-components/icons/wm-icon.svg b/src/react-components/icons/wm-icon.svg new file mode 100644 index 0000000000..6b0d436039 --- /dev/null +++ b/src/react-components/icons/wm-icon.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/react-components/room/RoomEntryModal.js b/src/react-components/room/RoomEntryModal.js index fc74e457bf..8dee2c870e 100644 --- a/src/react-components/room/RoomEntryModal.js +++ b/src/react-components/room/RoomEntryModal.js @@ -7,6 +7,7 @@ import { ReactComponent as EnterIcon } from "../icons/Enter.svg"; import { ReactComponent as VRIcon } from "../icons/VR.svg"; import { ReactComponent as ShowIcon } from "../icons/Show.svg"; import { ReactComponent as SettingsIcon } from "../icons/Settings.svg"; +import { ReactComponent as WMIcon } from "../icons/wm-icon.svg"; import styles from "./RoomEntryModal.scss"; import styleUtils from "../styles/style-utils.scss"; import { useCssBreakpoints } from "react-use-css-breakpoints"; @@ -26,6 +27,8 @@ export function RoomEntryModal({ onSpectate, showOptions, onOptions, + showMonetizationRequired, + showMonetized, ...rest }) { const breakpoint = useCssBreakpoints(); @@ -69,6 +72,25 @@ export function RoomEntryModal({
    )} +
    + {!showJoinRoom &&

    This space has no more free slots available.

    } + + {showMonetized && ( +
    + Thanks for paying! You can join this space even if it is full. Search for this icon in the space to find + other premium features. +
    + )} + {showMonetizationRequired && ( +
    + + Sign up for Web Monetization + {" "} + to unlock premium features and join spaces even when they are full. +
    + )} +
    + {showOptions && breakpoint !== "sm" && ( <> @@ -99,7 +121,9 @@ RoomEntryModal.propTypes = { showSpectate: PropTypes.bool, onSpectate: PropTypes.func, showOptions: PropTypes.bool, - onOptions: PropTypes.func + onOptions: PropTypes.func, + showMonetizationRequired: PropTypes.bool, + showMonetized: PropTypes.bool }; RoomEntryModal.defaultProps = { diff --git a/src/react-components/room/RoomEntryModal.scss b/src/react-components/room/RoomEntryModal.scss index 0e50c90cdb..7d5392fc92 100644 --- a/src/react-components/room/RoomEntryModal.scss +++ b/src/react-components/room/RoomEntryModal.scss @@ -50,3 +50,12 @@ } } } + +:local(.webmon) { + display: flex; + flex-direction: column; + align-items: center; + svg { + flex-shrink: 0; + } +} \ No newline at end of file diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index f5c07decbc..42eee27b15 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -141,6 +141,7 @@ class UIRoot extends Component { presences: PropTypes.object, friends: PropTypes.array, handle: PropTypes.string, + isMonetized: PropTypes.bool, sessionId: PropTypes.string, subscriptions: PropTypes.object, initialIsSubscribed: PropTypes.bool, @@ -792,7 +793,8 @@ class UIRoot extends Component { renderEntryStartPanel = () => { const { hasAcceptedProfile, hasChangedName } = this.props.store.state.activity; const promptForNameAndAvatarBeforeEntry = this.props.hubIsBound ? !hasAcceptedProfile : !hasChangedName; - + // monetized users can bypass room limit + const canEnter = !this.props.entryDisallowed || this.props.isMonetized; // TODO: What does onEnteringCanceled do? return ( <> @@ -800,7 +802,7 @@ class UIRoot extends Component { appName={configs.translation("app-name")} logoSrc={configs.image("logo")} roomName={this.props.hub.name} - showJoinRoom={!this.state.waitingOnAudio && !this.props.entryDisallowed} + showJoinRoom={!this.state.waitingOnAudio && canEnter} onJoinRoom={() => { if (promptForNameAndAvatarBeforeEntry || !this.props.forcedVREntryType) { this.setState({ entering: true }); @@ -816,9 +818,9 @@ class UIRoot extends Component { this.handleForceEntry(); } }} - showEnterOnDevice={!this.state.waitingOnAudio && !this.props.entryDisallowed && !isMobileVR} + showEnterOnDevice={!this.state.waitingOnAudio && canEnter && !isMobileVR} onEnterOnDevice={() => this.attemptLink()} - showSpectate={!this.state.waitingOnAudio && !this.props.entryDisallowed} + showSpectate={!this.state.waitingOnAudio} onSpectate={() => this.setState({ watching: true })} showOptions={this.props.hubChannel.canOrWillIfCreator("update_hub")} onOptions={() => { @@ -828,6 +830,8 @@ class UIRoot extends Component { SignInMessages.roomSettings ); }} + showMonetizationRequired={!this.props.isMonetized} + showMonetized={this.props.isMonetized} /> {!this.state.waitingOnAudio && ( Date: Fri, 2 Apr 2021 16:32:41 -0500 Subject: [PATCH 075/167] add premium scene section to homepage that searches reticulum for scenes with premium tag and allows direct room creation from them if monetized --- src/react-components/home/HomePage.js | 60 ++++++++++++++++++- src/react-components/home/HomePage.scss | 19 ++++++ src/react-components/home/usePremiumScenes.js | 14 +++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 src/react-components/home/usePremiumScenes.js diff --git a/src/react-components/home/HomePage.js b/src/react-components/home/HomePage.js index a0b2a7425f..d6f8c53b5a 100644 --- a/src/react-components/home/HomePage.js +++ b/src/react-components/home/HomePage.js @@ -1,11 +1,13 @@ -import React, { useContext, useEffect } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import classNames from "classnames"; +import "web-monetization-polyfill"; import configs from "../../utils/configs"; import { CreateRoomButton } from "./CreateRoomButton"; import { PWAButton } from "./PWAButton"; import { useFavoriteRooms } from "./useFavoriteRooms"; import { usePublicRooms } from "./usePublicRooms"; +import { usePremiumScenes } from "./usePremiumScenes"; import styles from "./HomePage.scss"; import { AuthContext } from "../auth/AuthContext"; import { createAndRedirectToNewHub } from "../../utils/phoenix-utils"; @@ -16,13 +18,16 @@ import { scaledThumbnailUrlFor } from "../../utils/media-url-utils"; import { Column } from "../layout/Column"; import { Button } from "../input/Button"; import { Container } from "../layout/Container"; +import { ReactComponent as WMIcon } from "../icons/wm-icon.svg"; export function HomePage() { const auth = useContext(AuthContext); const intl = useIntl(); + const [isMonetized, setIsMonetized] = useState(false); const { results: favoriteRooms } = useFavoriteRooms(); const { results: publicRooms } = usePublicRooms(); + const { results: premiumScenes } = usePremiumScenes(); const sortedFavoriteRooms = Array.from(favoriteRooms).sort((a, b) => b.member_count - a.member_count); const sortedPublicRooms = Array.from(publicRooms).sort((a, b) => b.member_count - a.member_count); @@ -46,6 +51,19 @@ export function HomePage() { } }, []); + useEffect(() => { + // save in closure in case this changes between renders + const monetization = document.monetization; + const onMonetizationStart = () => setIsMonetized(true); + const onMonetizationStop = () => setIsMonetized(false); + monetization.addEventListener("monetizationstart", onMonetizationStart); + monetization.addEventListener("monetizationstop", onMonetizationStop); + return () => { + monetization.removeEventListener("monetizationstart", onMonetizationStart); + monetization.removeEventListener("monetizationstop", onMonetizationStop); + }; + }); + const canCreateRooms = !configs.feature("disable_room_creation") || auth.isAdmin; return ( @@ -74,6 +92,46 @@ export function HomePage() {
    + {premiumScenes.length > 0 && ( + +

    + + +

    + + {isMonetized ? ( +
    Thanks for paying! You can also create rooms with these exclusive scenes:
    + ) : ( +
    + + Sign up for Web Monetization + {" "} + to unlock exclusive scenes. +
    + )} + + {premiumScenes.map(scene => { + // obfuscate the scene url as you can create a room from there + scene.url = "#"; + const onClick = isMonetized + ? () => createAndRedirectToNewHub(null, scene.id, false) + : e => e.preventDefault(); + return ( + + scaledThumbnailUrlFor(entry.images.preview.url, width, height) + } + onClick={onClick} + /> + ); + })} + +
    +
    + )} {configs.feature("show_feature_panels") && ( diff --git a/src/react-components/home/HomePage.scss b/src/react-components/home/HomePage.scss index 78c26b586e..bd01796246 100644 --- a/src/react-components/home/HomePage.scss +++ b/src/react-components/home/HomePage.scss @@ -143,6 +143,25 @@ font-size: 24px; margin-bottom: 16px; } + +:local(.scenes-heading) { + margin-left: 10px; + font-size: 24px; + margin-bottom: 16px; + display: flex; + align-items: center; + svg { + margin-right: 5px; + } +} + +:local(.scene-disabled) { + filter: blur(2px); + cursor: not-allowed !important; + a { + cursor: not-allowed !important; + } +} :local(.rooms) { background-color: theme.$recessed-bg; diff --git a/src/react-components/home/usePremiumScenes.js b/src/react-components/home/usePremiumScenes.js new file mode 100644 index 0000000000..45de5b277c --- /dev/null +++ b/src/react-components/home/usePremiumScenes.js @@ -0,0 +1,14 @@ +import { useCallback, useContext } from "react"; +import { usePaginatedAPI } from "./usePaginatedAPI"; +import { fetchReticulumAuthenticated } from "../../utils/phoenix-utils"; +import { AuthContext } from "../auth/AuthContext"; + +export function usePremiumScenes() { + const auth = useContext(AuthContext); // Re-render when you log in/out. + const getMoreScenes = useCallback( + cursor => fetchReticulumAuthenticated(`/api/v1/media/search?filter=premium&source=scene_listings&cursor=${cursor}`), + // eslint-disable-next-line react-hooks/exhaustive-deps + [auth.isSignedIn] + ); + return usePaginatedAPI(getMoreScenes); +} From 6ccd222cadf9afd0f4f9ec9034dc937f80ce5dc6 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 9 Apr 2021 13:40:16 -0500 Subject: [PATCH 076/167] docker container that will deploy immers client to a hubs cloud and set key configs automatically --- .dockerignore | 12 ++ Dockerfile | 13 ++ dockerdeploy.sh | 8 ++ package-lock.json | 279 +++++++++++++++++++++++++----------- package.json | 6 +- scripts/deploy.js | 9 ++ scripts/immers-configure.js | 58 ++++++++ scripts/login.js | 8 +- 8 files changed, 309 insertions(+), 84 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100755 dockerdeploy.sh create mode 100644 scripts/immers-configure.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..98f45f6e69 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +certs +dist +admin/node_modules +admin/certs +admin/dist +npm-debug.log +.vscode +.cache +.parcel-cache +.env +.ret.credentials diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..d0c0c43bf1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM node:14 + +WORKDIR /usr/src/hubs/admin +COPY package*.json ./ +RUN npm ci + +WORKDIR /usr/src/hubs +COPY package*.json ./ +RUN npm ci + +COPY . . + +CMD [ "dockerdeploy.sh" ] diff --git a/dockerdeploy.sh b/dockerdeploy.sh new file mode 100755 index 0000000000..4ac34bc9ed --- /dev/null +++ b/dockerdeploy.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +echo "Logging into to hub $hub as $email" +npm run login -- --host $hub --email $email +echo "Deploying Immers Space hubs client" +npm run deploy -- --skipCI +echo "Updating hubs config for immer $domain" +npm run immers-configure -- --immer $domain --wallet $monetizationPointer +echo "Done" diff --git a/package-lock.json b/package-lock.json index d43d3571bc..9a08b8de6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9323,9 +9323,15 @@ } } }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, "escape-html": { "version": "1.0.3", - "resolved": "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", "dev": true }, @@ -11553,6 +11559,12 @@ "loose-envify": "^1.0.0" } }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, "ip": { "version": "1.1.5", "resolved": "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz", @@ -12240,6 +12252,15 @@ "xtend": "^4.0.0" } }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, "leven": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", @@ -12714,14 +12735,22 @@ } }, "mem": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.1.0.tgz", - "integrity": "sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", "dev": true, "requires": { "map-age-cleaner": "^0.1.1", - "mimic-fn": "^1.0.0", + "mimic-fn": "^2.0.0", "p-is-promise": "^2.0.0" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + } } }, "memory-fs": { @@ -13803,21 +13832,6 @@ "pump": "^3.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -19761,12 +19775,64 @@ "ms": "^2.1.1" } }, + "decamelize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz", + "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==", + "dev": true, + "requires": { + "xregexp": "4.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -19783,6 +19849,35 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", "dev": true + }, + "yargs": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.2.tgz", + "integrity": "sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^2.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^10.1.0" + } + }, + "yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "dev": true, + "requires": { + "camelcase": "^4.1.0" + } } } }, @@ -20124,97 +20219,121 @@ "dev": true }, "yargs": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.2.tgz", - "integrity": "sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "cliui": "^4.0.0", - "decamelize": "^2.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^10.1.0" + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" }, "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, - "decamelize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz", - "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "xregexp": "4.0.0" + "color-convert": "^2.0.1" } }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "color-name": "~1.1.4" } }, - "p-limit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", - "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { - "p-try": "^2.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "p-limit": "^2.0.0" + "ansi-regex": "^5.0.0" } }, - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", - "dev": true + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "y18n": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", + "integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==", "dev": true }, "yargs-parser": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", - "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", - "dev": true, - "requires": { - "camelcase": "^4.1.0" - } + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "dev": true } } }, diff --git a/package.json b/package.json index 0b05844c8e..880fdf83c9 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "login": "node -r @babel/register -r esm -r ./scripts/shim scripts/login.js", "logout": "node -r @babel/register -r esm -r ./scripts/shim scripts/logout.js", "deploy": "node -r @babel/register -r esm -r ./scripts/shim scripts/deploy.js", + "immers-configure": "node -r @babel/register -r esm -r ./scripts/shim scripts/immers-configure.js", "undeploy": "node -r @babel/register -r esm -r ./scripts/shim scripts/undeploy.js", "test": "npm run lint && npm run test:unit && npm run build", "test:unit": "ava", @@ -133,11 +134,11 @@ "@babel/core": "^7.3.3", "@babel/plugin-proposal-class-properties": "^7.3.3", "@babel/plugin-proposal-object-rest-spread": "^7.3.2", + "@babel/plugin-proposal-optional-chaining": "7.12.1", "@babel/polyfill": "^7.4.4", "@babel/preset-env": "^7.9.0", "@babel/preset-react": "^7.0.0", "@babel/register": "^7.0.0", - "@babel/plugin-proposal-optional-chaining": "7.12.1", "@iarna/toml": "^2.2.3", "acorn": "^6.4.1", "ava": "^1.4.1", @@ -190,7 +191,8 @@ "webpack-bundle-analyzer": "^3.3.2", "webpack-cli": "^3.2.3", "webpack-dev-server": "^3.1.14", - "worker-loader": "^2.0.0" + "worker-loader": "^2.0.0", + "yargs": "^16.2.0" }, "optionalDependencies": { "fsevents": "^2.2.1" diff --git a/scripts/deploy.js b/scripts/deploy.js index cc79ee396e..5f7d9c5ff8 100644 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -6,6 +6,9 @@ import tar from "tar"; import ora from "ora"; import FormData from "form-data"; import path from "path"; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; +const skipCI = yargs(hideBin(process.argv)).argv.skipCI; if (!existsSync(".ret.credentials")) { console.log("Not logged in, so cannot deploy. To log in, run npm run login."); @@ -60,6 +63,9 @@ const getTs = (() => { step.text = "Building Client."; await new Promise((resolve, reject) => { + if (skipCI) { + return resolve(); + } exec("npm ci", {}, err => { if (err) reject(err); resolve(); @@ -76,6 +82,9 @@ const getTs = (() => { step.text = "Building Admin Console."; await new Promise((resolve, reject) => { + if (skipCI) { + return resolve(); + } exec("npm ci", { cwd: "./admin" }, err => { if (err) reject(err); resolve(); diff --git a/scripts/immers-configure.js b/scripts/immers-configure.js new file mode 100644 index 0000000000..1ce30efb99 --- /dev/null +++ b/scripts/immers-configure.js @@ -0,0 +1,58 @@ +import { readFileSync, existsSync } from "fs"; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; +const { immer, wallet } = yargs(hideBin(process.argv)).argv; +if (!immer || !wallet) { + console.log("Missing required CLI arguments: immer, wallet"); + process.exit(1); +} +if (!existsSync(".ret.credentials")) { + console.log("Not logged in, so cannot configure. To log in, run npm run login."); + process.exit(1); +} +const { host, token } = JSON.parse(readFileSync(".ret.credentials")); + +(async () => { + const headers = { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json" + }; + // server settings + const cfg = { + extra_csp: { + // connect to home immer + connect_src: "https: wss:", + // allow Coil WebMon browser plugin + script_src: + "'sha256-W5yaJ6UM3/kOJa12aRVSLOEOKdAUYAWZPM1bUuaTJYQ=' 'sha256-XpyxuqRQmj1o8ovYZlIA71UXSYTvYdV8kOb55p+lrNo=' 'sha256-tie542PGbiDGOm9MefVIzDBZf4Nt5wTagAHT/BKEB94=' 'sha256-9QLzkf1LE5s0CtnpqUvwkWr7DV4GRfQLGt/tFNT19h0='" + }, + security: { + // fetch remote avatars + cors_origins: "*" + }, + extra_html: {} + }; + // add local immers server env variable and web monetizatoin payment pointer to all pages + const extraHeader = ``; + ["extra_avatar_html", "extra_index_html", "extra_room_html", "extra_scene_html"].forEach(setting => { + cfg[setting] = extraHeader; + }); + + await fetch(`https://${host}/api/ita/configs/reticulum`, { + headers, + method: "PATCH", + body: JSON.stringify(cfg) + }); + + // App Settings + await fetch(`https://${host}/api/v1/app_configs`, { + headers, + method: "POST", + body: JSON.stringify({ + features: { + // disallow hubs/reticulum accounts to enfore monetized features and avoid confusion with immers accounts + disable_sign_up: true + } + }) + }); +})(); diff --git a/scripts/login.js b/scripts/login.js index 6d013b9a3f..d69794c082 100644 --- a/scripts/login.js +++ b/scripts/login.js @@ -5,6 +5,9 @@ import AuthChannel from "../src/utils/auth-channel"; import configs from "../src/utils/configs.js"; import { Socket } from "phoenix-channels"; import { writeFileSync } from "fs"; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; +const argv = yargs(hideBin(process.argv)).argv; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); @@ -12,7 +15,7 @@ const ask = q => new Promise(res => rl.question(q, res)); (async () => { console.log("Logging into Hubs Cloud.\n"); - const host = await ask("Host (eg hubs.mozilla.com): "); + const host = argv.host || (await ask("Host (eg hubs.mozilla.com): ")); if (!host) { console.log("Invalid host."); process.exit(1); @@ -37,8 +40,9 @@ const ask = q => new Promise(res => rl.question(q, res)); const socket = await connectToReticulum(false, null, Socket); const store = new Store(); - const email = await ask("Your admin account email (eg admin@yoursite.com): "); + const email = argv.email || (await ask("Your admin account email (eg admin@yoursite.com): ")); console.log(`Logging into ${host} as ${email}. Click on the link in your email to continue.`); + const authChannel = new AuthChannel(store); authChannel.setSocket(socket); const { authComplete } = await authChannel.startAuthentication(email); From 7f8f0d02fd309fef8e9ad8a8ba219d843aec12c8 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 9 Apr 2021 15:20:33 -0500 Subject: [PATCH 077/167] scripts for docker tasks --- dockerdeploy.sh | 3 ++- package.json | 2 ++ scripts/immers-configure.js | 24 +++++++++++++++--------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/dockerdeploy.sh b/dockerdeploy.sh index 4ac34bc9ed..263dd1f30d 100755 --- a/dockerdeploy.sh +++ b/dockerdeploy.sh @@ -4,5 +4,6 @@ npm run login -- --host $hub --email $email echo "Deploying Immers Space hubs client" npm run deploy -- --skipCI echo "Updating hubs config for immer $domain" -npm run immers-configure -- --immer $domain --wallet $monetizationPointer +# this one reads from env because of issues with dollar sign in payment pointer +npm run immers-configure echo "Done" diff --git a/package.json b/package.json index 880fdf83c9..5e5f683a56 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "logout": "node -r @babel/register -r esm -r ./scripts/shim scripts/logout.js", "deploy": "node -r @babel/register -r esm -r ./scripts/shim scripts/deploy.js", "immers-configure": "node -r @babel/register -r esm -r ./scripts/shim scripts/immers-configure.js", + "immers-build:image": "docker build -t immersspace/hubs .", + "immers-publish:image": "docker push immersspace/hubs:latest", "undeploy": "node -r @babel/register -r esm -r ./scripts/shim scripts/undeploy.js", "test": "npm run lint && npm run test:unit && npm run build", "test:unit": "ava", diff --git a/scripts/immers-configure.js b/scripts/immers-configure.js index 1ce30efb99..577858d448 100644 --- a/scripts/immers-configure.js +++ b/scripts/immers-configure.js @@ -1,9 +1,8 @@ import { readFileSync, existsSync } from "fs"; -import yargs from "yargs"; -import { hideBin } from "yargs/helpers"; -const { immer, wallet } = yargs(hideBin(process.argv)).argv; +// use env due to complications of reading $ in payment pointer via cli +const { domain: immer, monetizationPointer: wallet } = process.env if (!immer || !wallet) { - console.log("Missing required CLI arguments: immer, wallet"); + console.log("Missing required ENV: domain, monetizationPointer"); process.exit(1); } if (!existsSync(".ret.credentials")) { @@ -30,19 +29,24 @@ const { host, token } = JSON.parse(readFileSync(".ret.credentials")); // fetch remote avatars cors_origins: "*" }, + uploads: { + // keep media for 6 months so it remains in chat history + ttl: 15778476 + }, extra_html: {} }; // add local immers server env variable and web monetizatoin payment pointer to all pages const extraHeader = ``; ["extra_avatar_html", "extra_index_html", "extra_room_html", "extra_scene_html"].forEach(setting => { - cfg[setting] = extraHeader; + cfg.extra_html[setting] = extraHeader; }); - await fetch(`https://${host}/api/ita/configs/reticulum`, { headers, method: "PATCH", body: JSON.stringify(cfg) - }); + }) + .then(res => { if (!res.ok) { throw new Error(`Response ${res.status}`) } }) + .catch(err => console.log("Error updating server config: ", err.message)); // App Settings await fetch(`https://${host}/api/v1/app_configs`, { @@ -50,9 +54,11 @@ const { host, token } = JSON.parse(readFileSync(".ret.credentials")); method: "POST", body: JSON.stringify({ features: { - // disallow hubs/reticulum accounts to enfore monetized features and avoid confusion with immers accounts + // disallow hubs/reticulum accounts to enforce monetized features and avoid confusion with immers accounts disable_sign_up: true } }) - }); + }) + .then(res => { if (!res.ok) { throw new Error(`Response ${res.status}`) } }) + .catch(err => console.log("Error updating server config: ", err.message)); })(); From 8f994430cdb65bf90e8d6832a8709d47a02a203a Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 9 Apr 2021 18:10:29 -0500 Subject: [PATCH 078/167] fix admin install, use correct package json and use same install order as deploy script --- Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index d0c0c43bf1..f06b400c45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,14 @@ FROM node:14 -WORKDIR /usr/src/hubs/admin +WORKDIR /usr/src/hubs COPY package*.json ./ RUN npm ci -WORKDIR /usr/src/hubs -COPY package*.json ./ +WORKDIR /usr/src/hubs/admin +COPY admin/package*.json ./ RUN npm ci +WORKDIR /usr/src/hubs COPY . . CMD [ "dockerdeploy.sh" ] From 0946826ae82e1f9c370c6cb272600808eda7d126 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 10 Apr 2021 22:55:59 -0500 Subject: [PATCH 079/167] fix immers server meta tag --- scripts/immers-configure.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/immers-configure.js b/scripts/immers-configure.js index 577858d448..007f7bc7d2 100644 --- a/scripts/immers-configure.js +++ b/scripts/immers-configure.js @@ -36,7 +36,7 @@ const { host, token } = JSON.parse(readFileSync(".ret.credentials")); extra_html: {} }; // add local immers server env variable and web monetizatoin payment pointer to all pages - const extraHeader = ``; + const extraHeader = ``; ["extra_avatar_html", "extra_index_html", "extra_room_html", "extra_scene_html"].forEach(setting => { cfg.extra_html[setting] = extraHeader; }); From a6fea21855b7b9662cc10958229bdd42aea86a36 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 11 Apr 2021 18:02:46 -0500 Subject: [PATCH 080/167] separate immers feed into its own message dispatch --- src/utils/immers.js | 42 +++++++++++++-------- src/utils/immers/activities.js | 31 ++++++++++----- src/utils/immers/immers-message-dispatch.js | 11 ++++++ 3 files changed, 59 insertions(+), 25 deletions(-) create mode 100644 src/utils/immers/immers-message-dispatch.js diff --git a/src/utils/immers.js b/src/utils/immers.js index f558acd72d..875fcc2bc7 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -1,7 +1,9 @@ import io from "socket.io-client"; import configs from "./configs"; import { fetchAvatar } from "./avatar-utils"; +import { SOUND_CHAT_MESSAGE } from "../systems/sound-effects-system"; import { setupMonetization } from "./immers/monetization"; +import immersMessageDispatch from "./immers/immers-message-dispatch"; import Activities from "./immers/activities"; const localImmer = configs.IMMERS_SERVER; console.log("immers.space client v0.7.1"); @@ -429,12 +431,12 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat setupMonetization(hubScene, localPlayer, remountUI); // news feed and chat integration, behind a feature switch as it needs the new hubs ui - if (messageDispatch) { + if (createInWorldLogMessage) { // fetch news feed const updateFeed = async () => { const { messages, more } = await activities.feedAsChat(); messages.forEach(detail => { - messageDispatch.dispatchEvent(new CustomEvent("message", { detail })); + immersMessageDispatch.dispatchEvent(new CustomEvent("message", { detail })); }); return more; }; @@ -450,20 +452,18 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat activity = JSON.parse(activity); const message = activities.activityAsChat(activity); if (message.body) { - messageDispatch.dispatchEvent(new CustomEvent("message", { detail: message })); + if (message.type !== "activity") { + // play sound for chat/image/video updates + scene.systems["hubs-systems"].soundEffectsSystem.playSoundOneShot(SOUND_CHAT_MESSAGE); + } + immersMessageDispatch.dispatchEvent(new CustomEvent("message", { detail: message })); if (scene.is("vr-mode")) { createInWorldLogMessage(message); } } }); - // intercept outgoing messages and post to immers space feed - messageDispatch.addEventListener("message", ({ detail: message }) => { - // skip if incoming message or loaded from history - if (!message.sent || message.isImmersFeed) { - return; - } - // Send to immers id of everyone in room so all chat goes through immers and - // we don't have to worry about duplicate messages appearing in chat + immersMessageDispatch.setDispatchHandler(message => { + // include local room occupants const localAudience = Object.values(window.APP.hubChannel.presence.state) .map(presence => presence.metas[presence.metas.length - 1]?.profile.id) .filter(id => id && id !== actorObj.id); @@ -471,19 +471,29 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat let task; switch (message.type) { case "chat": - task = activities.note(message.body, localAudience, true, null); + task = activities.note(message.body, localAudience, message.audience, null); break; case "image": case "photo": - task = activities.image(message.body.src, localAudience, true, null); + task = activities.image(message.body.src, localAudience, message.audience, null); break; case "video": - task = activities.video(message.body.src, localAudience, true, null); + task = activities.video(message.body.src, localAudience, message.audience, null); break; default: - console.log("Chat message not shared", message); + return console.log("Chat message not shared", message); } - task.catch(err => console.error(`Error sharing chat: ${err.message}`)); + task + .then(async postResult => { + if (!postResult.ok) { + throw new Error(postResult.status); + } + // fetch the newly created activity and feed it back into chat system so your outgoing messages appear in panel + const chat = activities.activityAsChat(await getObject(postResult.headers.get("Location")), true); + immersMessageDispatch.dispatchEvent(new CustomEvent("message", { detail: chat })); + }) + .catch(err => console.error(`Error sharing chat: ${err.message}`)); }); + remountUI({ immersMessageDispatch }); } } diff --git a/src/utils/immers/activities.js b/src/utils/immers/activities.js index f800177c7a..3fcab5f9a4 100644 --- a/src/utils/immers/activities.js +++ b/src/utils/immers/activities.js @@ -139,58 +139,71 @@ export default class Activities { }); } - note(content, to, isPublic, summary) { + note(content, to, audience, summary) { const obj = { content, type: "Note", attributedTo: this.actor.id, context: this.place, - to: [this.actor.followers, ...to] + to: to.slice() }; if (summary) { obj.summary = summary; } - if (isPublic) { + if (audience === "friends" || audience === "public") { + obj.to.push(this.actor.followers); + } + if (audience === "public") { obj.to.push(Activities.PublicAddress); } return this.postActivity(obj); } - image(url, to, isPublic, summary) { + image(url, to, audience, summary) { const obj = { url, type: "Image", attributedTo: this.actor.id, context: this.place, - to: [this.actor.followers, ...to] + to: to.slice() }; if (summary) { obj.summary = summary; } - if (isPublic) { + if (audience === "friends" || audience === "public") { + obj.to.push(this.actor.followers); + } + if (audience === "public") { obj.to.push(Activities.PublicAddress); } return this.postActivity(obj); } - video(url, to, isPublic, summary) { + video(url, to, audience, summary) { const obj = { url, type: "Video", attributedTo: this.actor.id, context: this.place, - to: [this.actor.followers, ...to] + to: to.slice() }; if (summary) { obj.summary = summary; } - if (isPublic) { + if (audience === "friends" || audience === "public") { + obj.to.push(this.actor.followers); + } + if (audience === "public") { obj.to.push(Activities.PublicAddress); } return this.postActivity(obj); } activityAsChat(activity, outbox = false) { + if (outbox) { + // avoid apex api inconsistency that returns actor as id string for direct activity fetch + activity.actor = this.actor; + } const message = { isImmersFeed: true, isFriend: this.friends.some(status => status.actor.id === activity.actor.id), diff --git a/src/utils/immers/immers-message-dispatch.js b/src/utils/immers/immers-message-dispatch.js new file mode 100644 index 0000000000..c3e7d2edd4 --- /dev/null +++ b/src/utils/immers/immers-message-dispatch.js @@ -0,0 +1,11 @@ +class ImmersMessageDispatch extends EventTarget { + setDispatchHandler(dispatchHandler) { + this.dispatchHandler = dispatchHandler; + } + + dispatch(message) { + this.dispatchHandler(message); + } +} + +export default new ImmersMessageDispatch(); From cd468f1486df57c1b3354ea35315e25f7d14b245 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 11 Apr 2021 18:04:49 -0500 Subject: [PATCH 081/167] repalace the Tweet button on selgies images/videos with Immers Space share buttons (two privacy level options: friends, public) --- .../images/sprites/action/immers-action.png | Bin 0 -> 11921 bytes .../sprite-system-action-spritesheet.json | 46 ++++++++------ .../sprite-system-action-spritesheet.png | Bin 116526 -> 118561 bytes .../sprite-system-notice-spritesheet.png | Bin 259405 -> 259405 bytes src/components/immers/immers-share-button.js | 39 ++++++++++++ src/components/immers/index.js | 1 + src/hub.html | 56 ++++++++++++++++-- 7 files changed, 119 insertions(+), 23 deletions(-) create mode 100644 src/assets/images/sprites/action/immers-action.png create mode 100644 src/components/immers/immers-share-button.js diff --git a/src/assets/images/sprites/action/immers-action.png b/src/assets/images/sprites/action/immers-action.png new file mode 100644 index 0000000000000000000000000000000000000000..454430e8034d109d43dfeb14cfb2fdd6202a291c GIT binary patch literal 11921 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
  • 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 >1_6l$h%=wm`$YW%Y8QYtRF&F-KIRC$c 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 AG5`Po literal 116526 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 diff --git a/src/components/immers/immers-share-button.js b/src/components/immers/immers-share-button.js new file mode 100644 index 0000000000..8c92753565 --- /dev/null +++ b/src/components/immers/immers-share-button.js @@ -0,0 +1,39 @@ +import immersMessageDispatch from "../../utils/immers/immers-message-dispatch"; + +AFRAME.registerComponent("immers-share-button", { + schema: { type: "string", oneOf: ["public", "friends", "local"], default: "public" }, + init() { + this.textEl = this.el.querySelector("[text]"); + NAF.utils + .getNetworkedEntity(this.el) + .then(networkedEl => { + this.targetEl = networkedEl; + }) + .catch(() => { + // Non-networked, do not handle for now, and hide button. + this.el.object3D.visible = false; + }); + + this.onClick = () => { + if (this.shared) { + return; + } + const { src, contentSubtype } = this.targetEl.components["media-loader"].data; + immersMessageDispatch.dispatch({ + type: contentSubtype.split(/[-/ ]/)[0], + body: { src }, + audience: this.data + }); + this.shared = true; + this.textEl.setAttribute("text", "value", "shared"); + }; + }, + + play() { + this.el.object3D.addEventListener("interact", this.onClick); + }, + + pause() { + this.el.object3D.removeEventListener("interact", this.onClick); + } +}); diff --git a/src/components/immers/index.js b/src/components/immers/index.js index cdeea63c42..1dbac2d746 100644 --- a/src/components/immers/index.js +++ b/src/components/immers/index.js @@ -1,4 +1,5 @@ import "./immers-follow-button"; +import "./immers-share-button"; import "./spoke-tagger"; import "./monetization-visible"; import "./monetization-invisible"; diff --git a/src/hub.html b/src/hub.html index af6923facb..76e335d4ff 100644 --- a/src/hub.html +++ b/src/hub.html @@ -829,9 +829,24 @@ position="0 0.225 0" class="video-snap-button" > - - + + + + + + + - - + + + + + + + @@ -1005,6 +1035,24 @@ opacity: 1.0; src: #button" > + From 7fe9cded50b4c61679e0e1c56c1c220001f5ac35 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 10 Apr 2021 13:21:33 -0500 Subject: [PATCH 082/167] sanitize & display html in federated chats --- package-lock.json | 121 +++++++++++++++++++++++++++++++++++++- package.json | 1 + src/utils/chat-message.js | 6 +- 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 63dcf93778..0af7fab982 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27792,6 +27792,11 @@ "dev": true, "optional": true }, + "nanoid": { + "version": "3.1.22", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz", + "integrity": "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==" + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz", @@ -29181,6 +29186,11 @@ "integrity": "sha1-dLkdLLhnXRG5mXagBl9s4X+hvMg=", "dev": true }, + "parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=" + }, "parse-url": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-5.0.2.tgz", @@ -32282,9 +32292,9 @@ } }, "react-popper-2": { - "version": "npm:react-popper@2.2.4", - "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.4.tgz", - "integrity": "sha512-NacOu4zWupdQjVXq02XpTD3yFPSfg5a7fex0wa3uGKVkFK7UN6LvVxgcb+xYr56UCuWiNPMH20tntdVdJRwYew==", + "version": "npm:react-popper@2.2.5", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.5.tgz", + "integrity": "sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw==", "requires": { "react-fast-compare": "^3.0.1", "warning": "^4.0.2" @@ -33794,6 +33804,111 @@ } } }, + "sanitize-html": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.3.3.tgz", + "integrity": "sha512-DCFXPt7Di0c6JUnlT90eIgrjs6TsJl/8HYU3KLdmrVclFN4O0heTcVbJiMa23OKVr6aR051XYtsgd8EWwEBwUA==", + "requires": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^6.0.0", + "is-plain-object": "^5.0.0", + "klona": "^2.0.3", + "parse-srcset": "^1.0.2", + "postcss": "^8.0.2" + }, + "dependencies": { + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + }, + "dom-serializer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.1.tgz", + "integrity": "sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + }, + "domhandler": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.1.0.tgz", + "integrity": "sha512-/6/kmsGlMY4Tup/nGVutdrK9yQi4YjWVcVeoQmixpzjOUK1U7pQkvAPHBJeUxOgxF0J8f8lwCJSlCfD0V4CMGQ==", + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.5.2.tgz", + "integrity": "sha512-MHTthCb1zj8f1GVfRpeZUbohQf/HdBos0oX5gZcQFepOZPLLRyj6Wn7XS7EMnY7CVpwv8863u2vyE83Hfu28HQ==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.1.0" + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, + "klona": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", + "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==" + }, + "postcss": { + "version": "8.2.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.9.tgz", + "integrity": "sha512-b+TmuIL4jGtCHtoLi+G/PisuIl9avxs8IZMSmlABRwNz5RLUUACrC+ws81dcomz1nRezm5YPdXiMEzBEKgYn+Q==", + "requires": { + "colorette": "^1.2.2", + "nanoid": "^3.1.22", + "source-map": "^0.6.1" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, "sass": { "version": "1.26.10", "resolved": "https://registry.npmjs.org/sass/-/sass-1.26.10.tgz", diff --git a/package.json b/package.json index 2bb204cd3d..fa725f4e45 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "react-textarea-autosize": "^8.2.0", "react-use-css-breakpoints": "^1.0.3", "resize-observer-polyfill": "^1.5.1", + "sanitize-html": "^2.3.2", "screenfull": "^4.0.1", "semver": "^7.3.2", "socket.io-client": "^2.3.0", diff --git a/src/utils/chat-message.js b/src/utils/chat-message.js index 357ca85245..79d608fbaf 100644 --- a/src/utils/chat-message.js +++ b/src/utils/chat-message.js @@ -1,6 +1,7 @@ import React from "react"; import Linkify from "react-linkify"; import { toArray as toEmojis } from "react-emoji-render"; +import sanitize from "sanitize-html"; const emojiRegex = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c[\ude32-\ude3a]|[\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/; @@ -12,7 +13,10 @@ export function formatMessageBody(body, { emojiClassName } = {}) { const messages = cleanedBody.split("\n").map((message, i) => (

    - {toEmojis(message, { className: emojiClassName })} + {toEmojis(message, { className: emojiClassName }).map( + (node, i) => + typeof node === "string" ? : node + )}

    )); From 822cae372198831421409d1e965748d24dc2c0aa Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 10 Apr 2021 18:07:57 -0500 Subject: [PATCH 083/167] separate local chat from immers posts in two different sidebars, local chat is once again ephemeral, add privacy level controls to immers chat --- src/hub.js | 7 +- src/react-components/input/ToolbarButton.js | 4 +- src/react-components/input/ToolbarButton.scss | 8 + src/react-components/room/ChatSidebar.js | 9 +- .../room/ChatSidebarContainer.js | 29 +-- .../room/ImmersFeedSidebarContainer.js | 236 ++++++++++++++++++ src/react-components/room/ImmersReact.js | 31 +-- src/react-components/ui-root.js | 26 +- 8 files changed, 295 insertions(+), 55 deletions(-) create mode 100644 src/react-components/room/ImmersFeedSidebarContainer.js diff --git a/src/hub.js b/src/hub.js index 353a9c363b..78eb012a0c 100644 --- a/src/hub.js +++ b/src/hub.js @@ -1478,10 +1478,9 @@ document.addEventListener("DOMContentLoaded", async () => { sent: session_id === socket.params().session_id }; - // replaced by immers feed - // if (scene.is("vr-mode")) { - // createInWorldLogMessage(incomingMessage); - // } + if (scene.is("vr-mode")) { + createInWorldLogMessage(incomingMessage); + } messageDispatch.receive(incomingMessage); }); diff --git a/src/react-components/input/ToolbarButton.js b/src/react-components/input/ToolbarButton.js index f9fcafb0cc..2c9da420d4 100644 --- a/src/react-components/input/ToolbarButton.js +++ b/src/react-components/input/ToolbarButton.js @@ -9,7 +9,7 @@ export const statusColors = ["red", "orange", "green"]; export const ToolbarButton = forwardRef( ( - { preset, className, iconContainerClassName, children, icon, label, selected, large, statusColor, ...rest }, + { preset, className, iconContainerClassName, children, icon, label, selected, large, small, statusColor, ...rest }, ref ) => { return ( @@ -18,7 +18,7 @@ export const ToolbarButton = forwardRef( className={classNames( styles.toolbarButton, styles[preset], - { [styles.selected]: selected, [styles.large]: large }, + { [styles.selected]: selected, [styles.large]: large, [styles.small]: small }, className )} {...rest} diff --git a/src/react-components/input/ToolbarButton.scss b/src/react-components/input/ToolbarButton.scss index 799d734a2a..21072fc7f7 100644 --- a/src/react-components/input/ToolbarButton.scss +++ b/src/react-components/input/ToolbarButton.scss @@ -42,6 +42,14 @@ } } +:local(.small) { + width: 48px; +} + +:local(.small) :local(.icon-container) { + flex-shrink: 0; +} + :local(.large) :local(.icon-container) { width: 96px; height: 96px; diff --git a/src/react-components/room/ChatSidebar.js b/src/react-components/room/ChatSidebar.js index 2a15a4cc9b..210a8344d0 100644 --- a/src/react-components/room/ChatSidebar.js +++ b/src/react-components/room/ChatSidebar.js @@ -302,10 +302,10 @@ ChatMessageList.propTypes = { children: PropTypes.node }; -export function ChatSidebar({ onClose, children, ...rest }) { +export function ChatSidebar({ onClose, title, children, ...rest }) { return ( } + title={title || } beforeTitle={} contentClassName={styles.content} {...rest} @@ -317,6 +317,7 @@ export function ChatSidebar({ onClose, children, ...rest }) { ChatSidebar.propTypes = { onClose: PropTypes.func, + title: PropTypes.any, onScrollList: PropTypes.func, children: PropTypes.node, listRef: PropTypes.func @@ -328,7 +329,9 @@ export function ChatToolbarButton(props) { {...props} icon={} preset="blue" - label={} + small + title="Chat with room occupants that is not saved" + label={} /> ); } diff --git a/src/react-components/room/ChatSidebarContainer.js b/src/react-components/room/ChatSidebarContainer.js index 32b997edcb..69a4908dfb 100644 --- a/src/react-components/room/ChatSidebarContainer.js +++ b/src/react-components/room/ChatSidebarContainer.js @@ -15,7 +15,6 @@ import { useMaintainScrollPosition } from "../misc/useMaintainScrollPosition"; import { spawnChatMessage } from "../chat-message"; import { discordBridgesForPresences } from "../../utils/phoenix-utils"; import { useIntl } from "react-intl"; -import { ImmersChatMessage, ImmersMoreHistoryButton } from "./ImmersReact"; const ChatContext = createContext({ messageGroups: [], sendMessage: () => {} }); @@ -43,29 +42,6 @@ function processChatMessage(messageGroups, newMessage) { const now = Date.now(); const { name, sent, sessionId, ...messageProps } = newMessage; - if (messageProps.isImmersFeed) { - // insert according to timestamp - const newMessageGroups = messageGroups.slice(); - const i = newMessageGroups.findIndex(group => messageProps.timestamp < group.timestamp); - newMessageGroups.splice(i === -1 ? newMessageGroups.length : i, 0, { - id: uniqueMessageId++, - isImmersFeed: messageProps.isImmersFeed, - isFriend: messageProps.isFriend, - timestamp: messageProps.timestamp, - sent: sent, - sender: name, - icon: messageProps.icon, - senderSessionId: sessionId, - context: messageProps.context, - immer: messageProps.immer, - messages: [{ id: uniqueMessageId++, ...messageProps }] - }); - return newMessageGroups; - } - // local chat is ignored in favor of immers feed which includes it - if (!sent) { - return messageGroups; - } if (shouldCreateNewMessageGroup(messageGroups, newMessage, now)) { return [ ...messageGroups, @@ -276,12 +252,9 @@ export function ChatSidebarContainer({ scene, canSpawnMessages, presences, occup return ( - - {messageGroups.map(({ id, systemMessage, isImmersFeed, ...rest }) => { + {messageGroups.map(({ id, systemMessage, ...rest }) => { if (systemMessage) { return ; - } else if (isImmersFeed) { - return ; } else { return ; } diff --git a/src/react-components/room/ImmersFeedSidebarContainer.js b/src/react-components/room/ImmersFeedSidebarContainer.js new file mode 100644 index 0000000000..a54ad0b193 --- /dev/null +++ b/src/react-components/room/ImmersFeedSidebarContainer.js @@ -0,0 +1,236 @@ +import React, { createContext, useCallback, useContext, useEffect, useState } from "react"; +import PropTypes from "prop-types"; +import { ChatSidebar, ChatMessageList, ChatInput, SendMessageButton } from "./ChatSidebar"; +import { useMaintainScrollPosition } from "../misc/useMaintainScrollPosition"; +import { FormattedMessage, useIntl } from "react-intl"; +import { ImmersChatMessage, ImmersIcon, ImmersMoreHistoryButton } from "./ImmersReact"; +import { ToolbarButton } from "../input/ToolbarButton"; +import { ReactComponent as PublicIcon } from "../icons/Scene.svg"; +import { ReactComponent as FriendsIcon } from "../icons/People.svg"; +import { ReactComponent as LocalIcon } from "../icons/Home.svg"; +import { IconButton } from "../input/IconButton"; +import styles from "./ChatSidebar.scss"; + +const ImmersFeedContext = createContext({ messageGroups: [], sendMessage: () => {} }); + +let uniqueMessageId = 0; + +function processChatMessage(messageGroups, newMessage) { + const { name, sent, sessionId, ...messageProps } = newMessage; + + if (messageProps.isImmersFeed) { + // insert according to timestamp + const newMessageGroups = messageGroups.slice(); + const i = newMessageGroups.findIndex(group => messageProps.timestamp < group.timestamp); + newMessageGroups.splice(i === -1 ? newMessageGroups.length : i, 0, { + id: uniqueMessageId++, + isImmersFeed: messageProps.isImmersFeed, + isFriend: messageProps.isFriend, + timestamp: messageProps.timestamp, + sent: sent, + sender: name, + icon: messageProps.icon, + senderSessionId: sessionId, + context: messageProps.context, + immer: messageProps.immer, + messages: [{ id: uniqueMessageId++, ...messageProps }] + }); + return newMessageGroups; + } +} + +// Returns the new message groups array when we receive a message. +// If the message is ignored, we return the original message group array. +function updateMessageGroups(messageGroups, newMessage) { + switch (newMessage.type) { + case "chat": + case "image": + case "photo": + case "video": + case "activity": + return processChatMessage(messageGroups, newMessage); + default: + return messageGroups; + } +} + +export function ImmersFeedContextProvider({ messageDispatch, children }) { + const [messageGroups, setMessageGroups] = useState([]); + const [unreadMessages, setUnreadMessages] = useState(false); + const [audience, setAudience] = useState("public"); + + useEffect( + () => { + function onReceiveMessage(event) { + const newMessage = event.detail; + if (!newMessage.isImmersFeed) { + return; + } + setMessageGroups(messages => updateMessageGroups(messages, newMessage)); + if ( + newMessage.type === "chat" || + newMessage.type === "image" || + newMessage.type === "photo" || + newMessage.type === "video" + ) { + setUnreadMessages(true); + } + } + + if (messageDispatch) { + messageDispatch.addEventListener("message", onReceiveMessage); + } + + return () => { + if (messageDispatch) { + messageDispatch.removeEventListener("message", onReceiveMessage); + } + }; + }, + [messageDispatch, setMessageGroups, setUnreadMessages] + ); + + const sendMessage = useCallback( + body => { + if (messageDispatch) { + messageDispatch.dispatch({ type: "chat", body, audience }); + } + }, + [messageDispatch, audience] + ); + + const setMessagesRead = useCallback( + () => { + setUnreadMessages(false); + }, + [setUnreadMessages] + ); + + return ( + + {children} + + ); +} + +ImmersFeedContextProvider.propTypes = { + children: PropTypes.node, + messageDispatch: PropTypes.object +}; + +export function ImmersFeedSidebarContainer({ onClose }) { + const { messageGroups, sendMessage, setMessagesRead, audience, setAudience } = useContext(ImmersFeedContext); + const [onScrollList, listRef, scrolledToBottom] = useMaintainScrollPosition(messageGroups); + const [message, setMessage] = useState(""); + const intl = useIntl(); + + const onKeyDown = useCallback( + e => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + sendMessage(e.target.value); + setMessage(""); + } + }, + [sendMessage, setMessage] + ); + + const onSendMessage = useCallback( + () => { + sendMessage(message); + setMessage(""); + }, + [message, sendMessage, setMessage] + ); + + useEffect( + () => { + if (scrolledToBottom) { + setMessagesRead(); + } + }, + [messageGroups, scrolledToBottom, setMessagesRead] + ); + + let placeholder; + switch (audience) { + case "local": + placeholder = intl.formatMessage({ + id: "immersfeed-sidebar-container.input-placeholder.local", + defaultMessage: "Post to room" + }); + break; + case "friends": + placeholder = intl.formatMessage({ + id: "immersfeed-sidebar-container.input-placeholder.friends", + defaultMessage: "Post to room and friends" + }); + break; + case "public": + placeholder = intl.formatMessage({ + id: "immersfeed-sidebar-container.input-placeholder.public", + defaultMessage: "Post publicly" + }); + break; + } + const audiences = ["local", "friends", "public"]; + const audienceButton = ( + setAudience(audiences[(audiences.indexOf(audience) + 1) % 3])} + title="Change audience" + > + {audience === "public" && } + {audience === "friends" && } + {audience === "local" && } + + ); + return ( + + + + {messageGroups.map(({ id, ...rest }) => { + return ; + })} + + setMessage(e.target.value)} + placeholder={placeholder} + value={message} + afterInput={ + <> + + {audienceButton} + + } + /> + + ); +} + +ImmersFeedSidebarContainer.propTypes = { + canSpawnMessages: PropTypes.bool, + presences: PropTypes.object.isRequired, + occupantCount: PropTypes.number.isRequired, + scene: PropTypes.object.isRequired, + onClose: PropTypes.func.isRequired +}; + +export function ImmersFeedToolbarButtonContainer(props) { + const { unreadMessages } = useContext(ImmersFeedContext); + return ( + } + statusColor={unreadMessages ? "orange" : undefined} + preset="basic" + small + title="Chat across the metaverse that is saved in your profile" + label={} + /> + ); +} diff --git a/src/react-components/room/ImmersReact.js b/src/react-components/room/ImmersReact.js index fc7461ad2f..85f60fc7f6 100644 --- a/src/react-components/room/ImmersReact.js +++ b/src/react-components/room/ImmersReact.js @@ -9,6 +9,15 @@ import { proxiedUrlFor } from "../../utils/media-url-utils"; import immersLogo from "../../assets/images/immers_logo.png"; import merge from "deepmerge"; +function proxyAndGetMessageComponent(message) { + // media urls need proxy to pass CSP & CORS + if (message.body?.src) { + message = merge({}, message); + message.body.src = proxiedUrlFor(message.body.src); + } + return getMessageComponent(message); +} + export function ImmerLink({ place }) { if (!place) { return null; @@ -58,7 +67,9 @@ export function ImmersChatMessage({ sent, sender, timestamp, isFriend, icon, imm [{immer}] |  |{" "}

    -
      {messages.map(message => proxyAndGetMessageComponent(message))}
    +
      + {messages.map(message => proxyAndGetMessageComponent(message))} +
  • ); } @@ -69,10 +80,7 @@ ImmersChatMessage.propTypes = { timestamp: PropTypes.any, messages: PropTypes.array, immer: PropTypes.string, - icon: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.object - ]), + icon: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), isFriend: PropTypes.bool, context: PropTypes.object }; @@ -93,6 +101,10 @@ export function ImmersFriendIcon() { return ; } +export function ImmersIcon() { + return ; +} + export function ImmersAvatarIcon({ avi }) { // support both Image objects & direct url const src = avi.url || avi; @@ -135,12 +147,3 @@ export function ImmersMoreHistoryButton() { ) ); } - -function proxyAndGetMessageComponent(message) { - // media urls need proxy to pass CSP & CORS - if (message.body?.src) { - message = merge({}, message); - message.body.src = proxiedUrlFor(message.body.src); - } - return getMessageComponent(message); -} \ No newline at end of file diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index f5c07decbc..8ae2169cab 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -94,6 +94,11 @@ import { TweetModalContainer } from "./room/TweetModalContainer"; import { TipContainer, FullscreenTip } from "./room/TipContainer"; import { SpectatingLabel } from "./room/SpectatingLabel"; import { SignInMessages } from "./auth/SignInModal"; +import { + ImmersFeedContextProvider, + ImmersFeedSidebarContainer, + ImmersFeedToolbarButtonContainer +} from "./room/ImmersFeedSidebarContainer"; const avatarEditorDebug = qsTruthy("avatarEditorDebug"); @@ -1427,6 +1432,15 @@ class UIRoot extends Component { onClose={() => this.setSidebar(null)} /> )} + {this.state.sidebarId === "feed" && ( + this.setSidebar(null)} + /> + )} {this.state.sidebarId === "objects" && ( )} this.toggleSidebar("chat")} /> + this.toggleSidebar("feed")} /> {entered && isMobileVR && ( - - - + + + + + ); } UIRootHooksWrapper.propTypes = { scene: PropTypes.object.isRequired, - messageDispatch: PropTypes.object + messageDispatch: PropTypes.object, + immersMessageDispatch: PropTypes.object }; export default UIRootHooksWrapper; From 2e46ad2498d957acc62911a2d2c9da4d07349454 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 11 Apr 2021 18:08:46 -0500 Subject: [PATCH 084/167] update packagelock --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0af7fab982..c32842f6f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22059,7 +22059,7 @@ }, "escape-html": { "version": "1.0.3", - "resolved": "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", "dev": true }, @@ -24609,7 +24609,7 @@ }, "http-deceiver": { "version": "1.2.7", - "resolved": "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", "dev": true }, @@ -25530,7 +25530,7 @@ }, "is-plain-obj": { "version": "1.1.0", - "resolved": "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", "dev": true }, From e53883821e0595c68b180827fa07a10726ce9350 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Mon, 12 Apr 2021 11:10:22 -0500 Subject: [PATCH 085/167] correct executable in docker cmd, improve error handling, lint --- Dockerfile | 2 +- dockerdeploy.sh | 1 + scripts/immers-configure.js | 15 ++++++++++++--- scripts/login.js | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index f06b400c45..565aa12ed7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,4 +11,4 @@ RUN npm ci WORKDIR /usr/src/hubs COPY . . -CMD [ "dockerdeploy.sh" ] +CMD [ "/bin/bash", "dockerdeploy.sh" ] diff --git a/dockerdeploy.sh b/dockerdeploy.sh index 263dd1f30d..8d3415863b 100755 --- a/dockerdeploy.sh +++ b/dockerdeploy.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +set -e echo "Logging into to hub $hub as $email" npm run login -- --host $hub --email $email echo "Deploying Immers Space hubs client" diff --git a/scripts/immers-configure.js b/scripts/immers-configure.js index 007f7bc7d2..239b98cbca 100644 --- a/scripts/immers-configure.js +++ b/scripts/immers-configure.js @@ -1,6 +1,6 @@ import { readFileSync, existsSync } from "fs"; // use env due to complications of reading $ in payment pointer via cli -const { domain: immer, monetizationPointer: wallet } = process.env +const { domain: immer, monetizationPointer: wallet } = process.env; if (!immer || !wallet) { console.log("Missing required ENV: domain, monetizationPointer"); process.exit(1); @@ -45,7 +45,11 @@ const { host, token } = JSON.parse(readFileSync(".ret.credentials")); method: "PATCH", body: JSON.stringify(cfg) }) - .then(res => { if (!res.ok) { throw new Error(`Response ${res.status}`) } }) + .then(res => { + if (!res.ok) { + throw new Error(`Response ${res.status}`); + } + }) .catch(err => console.log("Error updating server config: ", err.message)); // App Settings @@ -59,6 +63,11 @@ const { host, token } = JSON.parse(readFileSync(".ret.credentials")); } }) }) - .then(res => { if (!res.ok) { throw new Error(`Response ${res.status}`) } }) + .then(res => { + if (!res.ok) { + throw new Error(`Response ${res.status}`); + } + }) .catch(err => console.log("Error updating server config: ", err.message)); + process.exit(0); })(); diff --git a/scripts/login.js b/scripts/login.js index d69794c082..b741e916ef 100644 --- a/scripts/login.js +++ b/scripts/login.js @@ -16,7 +16,7 @@ const ask = q => new Promise(res => rl.question(q, res)); (async () => { console.log("Logging into Hubs Cloud.\n"); const host = argv.host || (await ask("Host (eg hubs.mozilla.com): ")); - if (!host) { + if (!host || host === true) { console.log("Invalid host."); process.exit(1); } From 1d60d5281da2688c9f94cf0f31ffa8445a8df022 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 17 Apr 2021 11:27:52 -0500 Subject: [PATCH 086/167] change Hubs hide button to block. Adds user your blocklist. Automatically hide any users in room or joining room who are in your blocklist --- src/components/block-button.js | 1 + src/hub.html | 2 +- src/utils/immers.js | 26 ++++++++++++++++++++++++ src/utils/immers/activities.js | 36 ++++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/components/block-button.js b/src/components/block-button.js index 5d70870187..4073e82042 100644 --- a/src/components/block-button.js +++ b/src/components/block-button.js @@ -7,6 +7,7 @@ AFRAME.registerComponent("block-button", { init() { this.onClick = () => { this.block(this.owner); + this.el.emit("immers-block", { clientId: this.owner }); }; NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { this.owner = networkedEl.components.networked.data.owner; diff --git a/src/hub.html b/src/hub.html index af6923facb..4a077e1956 100644 --- a/src/hub.html +++ b/src/hub.html @@ -163,7 +163,7 @@
    - + diff --git a/src/utils/immers.js b/src/utils/immers.js index 0ec329fff3..cd503a3dc7 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -21,6 +21,7 @@ let hubScene; let localPlayer; let actorObj; let avatarsCollection; +let blockList; // map of avatar urls to model objects to avoid recreating their AP representation // when donned from personal avatars collection const myAvatars = {}; @@ -382,6 +383,21 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat updateFriends(); immerSocket.on("friends-update", updateFriends); + blockList = await activities.blockList(); + // hide any blocked users currently in the room + Object.entries(window.APP.hubChannel.presence.state).forEach(([clientId, presence]) => { + const immersId = presence.metas[presence.metas.length - 1]?.profile.id; + if (blockList.includes(immersId)) { + window.APP.hubChannel.hide(clientId); + } + }); + // hide blocked users as soon as they connect + scene.addEventListener("presence_updated", ({ detail: { sessionId, profile } }) => { + if (blockList.includes(profile.id)) { + window.APP.hubChannel.hide(sessionId); + } + }); + scene.addEventListener("avatar_updated", async () => { const profile = store.state.profile; const update = {}; @@ -425,6 +441,16 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat // server converts actorId to followId for reject object activities.reject(event.detail, event.detail).catch(err => console.error("Error sending unfollow:", err.message)); }); + // blocked + scene.addEventListener("immers-block", ({ detail: { clientId } }) => { + const presence = window.APP.hubChannel.presence.state[clientId]; + const immersId = presence?.metas[presence.metas.length - 1]?.profile.id; + if (immersId) { + activities.block(immersId); + // update local copy in case blocked user reconnects + blockList.push(immersId); + } + }); setupMonetization(hubScene, localPlayer); diff --git a/src/utils/immers/activities.js b/src/utils/immers/activities.js index f800177c7a..518172b197 100644 --- a/src/utils/immers/activities.js +++ b/src/utils/immers/activities.js @@ -62,6 +62,34 @@ export default class Activities { return col; } + async blockList() { + const blocked = []; + // use blocklist IRI if specified, fallback to immers default + const blockedIRI = this.actor.streams?.blocked || `${this.homeImmer}/blocked/${this.actor.preferredUsername}`; + let col; + try { + col = await this.getObject(blockedIRI); + } catch (err) { + console.warn("Unable to fetch blocklist: ", err.message); + return blocked; + } + if (col.orderedItems?.length) { + blocked.push(...col.orderedItems); + } else { + col = await this.getObject(col.first); + blocked.push(...col.orderedItems); + } + // fetch entire collection + while (col.next) { + col = await this.getObject(col.next); + if (!col.orderedItems?.length) { + break; + } + blocked.push(...col.orderedItems); + } + return blocked.map(b => (typeof b === "object" ? b.id : b)); + } + async inboxAsChat() { const inbox = await this.inbox(); if (!inbox?.orderedItems?.length) { @@ -190,6 +218,14 @@ export default class Activities { return this.postActivity(obj); } + block(blockeeId) { + return this.postActivity({ + type: "Block", + actor: this.actor.id, + object: blockeeId + }); + } + activityAsChat(activity, outbox = false) { const message = { isImmersFeed: true, From 5b06cd6bbac4ee1509e44e2af873f5eb151b458a Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 23 Apr 2021 17:30:14 -0500 Subject: [PATCH 087/167] udpate color names for hubs cloud theme update, fix toolbar getting squished due to extra content in room entry modal --- src/react-components/modal/Modal.scss | 2 +- src/react-components/room/ContentMenu.scss | 2 +- src/react-components/room/ImmersFeedSidebarContainer.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/react-components/modal/Modal.scss b/src/react-components/modal/Modal.scss index c4c58c962f..5a04e54355 100644 --- a/src/react-components/modal/Modal.scss +++ b/src/react-components/modal/Modal.scss @@ -8,7 +8,7 @@ background-color: theme.$background1-color; border: 1px solid theme.$border1-color; border-radius: theme.$border-radius-regular; - margin: 24px; + margin: 2px; z-index: 1; width: 100%; max-width: 360px; diff --git a/src/react-components/room/ContentMenu.scss b/src/react-components/room/ContentMenu.scss index 17ba0c63a6..5345252d5b 100644 --- a/src/react-components/room/ContentMenu.scss +++ b/src/react-components/room/ContentMenu.scss @@ -87,7 +87,7 @@ /* TODO: change to theme color */ :local(.notifier) { - color: #FF3464; + color: theme.$status-unread-color; font-size: 1.5em; margin-left: -8px; } diff --git a/src/react-components/room/ImmersFeedSidebarContainer.js b/src/react-components/room/ImmersFeedSidebarContainer.js index a54ad0b193..fcb5bc2163 100644 --- a/src/react-components/room/ImmersFeedSidebarContainer.js +++ b/src/react-components/room/ImmersFeedSidebarContainer.js @@ -226,7 +226,7 @@ export function ImmersFeedToolbarButtonContainer(props) { } - statusColor={unreadMessages ? "orange" : undefined} + statusColor={unreadMessages ? "unread" : undefined} preset="basic" small title="Chat across the metaverse that is saved in your profile" From 6aa29c14d040e22f872eba4312e2bbc0fbe9da1f Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Tue, 27 Apr 2021 21:34:58 -0500 Subject: [PATCH 088/167] hotfix broken immers share buttons appearing on webcam/screenshare --- src/hub.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hub.html b/src/hub.html index b678c0a59d..9f82bd922a 100644 --- a/src/hub.html +++ b/src/hub.html @@ -855,7 +855,7 @@ position="0 0.225 0" class="video-snap-button" > - + - + Date: Fri, 23 Apr 2021 17:38:30 -0500 Subject: [PATCH 089/167] basic support for oauth scopes, default requested level, and caching the actually granted scopes --- .defaults.env | 1 + src/storage/store.js | 3 ++- src/utils/configs.js | 1 + src/utils/immers.js | 11 +++++++++-- webpack.config.js | 1 + 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.defaults.env b/.defaults.env index 45c4db04d4..1978f94e2c 100644 --- a/.defaults.env +++ b/.defaults.env @@ -31,3 +31,4 @@ DEFAULT_SCENE_SID="JGLt8DP" # LOAD_APP_CONFIG=true IMMERS_SERVER="https://localhost:8081" +IMMERS_SCOPE="modAdditive" diff --git a/src/storage/store.js b/src/storage/store.js index 487b250ef6..0ab544980a 100644 --- a/src/storage/store.js +++ b/src/storage/store.js @@ -63,7 +63,8 @@ export const SCHEMA = { token: { type: ["null", "string"] }, email: { type: ["null", "string"] }, immerToken: { type: ["null", "string"] }, - immerHome: { type: ["null", "string"] } + immerHome: { type: ["null", "string"] }, + immerScopes: { type: ["null", "array"] } } }, diff --git a/src/utils/configs.js b/src/utils/configs.js index 05b46bc4cf..f69e795e1a 100644 --- a/src/utils/configs.js +++ b/src/utils/configs.js @@ -17,6 +17,7 @@ let isAdmin = false; "GA_TRACKING_ID", "SHORTLINK_DOMAIN", "IMMERS_SERVER", + "IMMERS_SCOPE", "BASE_ASSETS_PATH" ].forEach(x => { const el = document.querySelector(`meta[name='env:${x.toLowerCase()}']`); diff --git a/src/utils/immers.js b/src/utils/immers.js index 3d074fe63b..c6efaee343 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -6,6 +6,8 @@ import { setupMonetization } from "./immers/monetization"; import immersMessageDispatch from "./immers/immers-message-dispatch"; import Activities from "./immers/activities"; const localImmer = configs.IMMERS_SERVER; +// immer can set a requested scope, but user can override +const preferredScope = configs.IMMERS_SCOPE; console.log("immers.space client v0.7.1"); const jsonldMime = "application/activity+json"; // avoid race between auth and initialize code @@ -19,6 +21,7 @@ const activities = new Activities(localImmer); let homeImmer; let place; let token; +let authorizedScopes; let hubScene; let localPlayer; let actorObj; @@ -251,10 +254,12 @@ export async function auth(store) { // not safe to update store here, will be saved later in initialize() token = hashParams.get("access_token"); homeImmer = hashParams.get("issuer"); + authorizedScopes = hashParams.get("scope")?.split(" ") || []; window.location.hash = ""; } else { token = store.state.credentials.immerToken; homeImmer = store.state.credentials.immerHome; + authorizedScopes = store.state.credentials.immerScopes; } activities.token = token; activities.homeImmer = homeImmer; @@ -267,7 +272,8 @@ export async function auth(store) { client_id: place.id, // hub link with room id redirect_uri: hubUri, - response_type: "token" + response_type: "token", + scope: preferredScope }); if (handle) { // pass to auth to prefill login form @@ -319,7 +325,8 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat credentials: { immerToken: token, // record user's home server in case redirected during auth - immerHome: homeImmer + immerHome: homeImmer, + immerScopes: authorizedScopes } }); const immerSocket = io(homeImmer, { diff --git a/webpack.config.js b/webpack.config.js index 1a68cd588f..9478c5d050 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -633,6 +633,7 @@ module.exports = async (env, argv) => { GA_TRACKING_ID: process.env.GA_TRACKING_ID, POSTGREST_SERVER: process.env.POSTGREST_SERVER, IMMERS_SERVER: process.env.IMMERS_SERVER, + IMMERS_SCOPE: process.env.IMMERS_SCOPE, APP_CONFIG: appConfig }) }) From 811c173952c45edef61517ec4dd8476826ac2c66 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Mon, 26 Apr 2021 12:40:21 -0500 Subject: [PATCH 090/167] don't crash the people list sidebar if friends list is not loaded/authorized --- src/react-components/ui-root.js | 4 ++++ src/utils/immers.js | 13 +++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 63a5ca92aa..5e1a3cd669 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -172,6 +172,10 @@ class UIRoot extends Component { breakpoint: PropTypes.string }; + static defaultProps = { + friends: [] + }; + state = { enterInVR: false, entered: false, diff --git a/src/utils/immers.js b/src/utils/immers.js index c6efaee343..6671854658 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -225,7 +225,7 @@ export async function getFriends(actorObj) { } }); if (!response.ok) { - throw new Error("Unable to fech friends"); + throw new Error(`Unable to fech friends: ${response.statusText}`); } return response.json(); } @@ -381,9 +381,14 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat const updateFriends = async () => { if (store.state.profile.id) { const profile = store.state.profile; - friendsCol = await getFriends(profile); - activities.friends = friendsCol.orderedItems; - remountUI({ friends: friendsCol.orderedItems.filter(act => act.type !== "Reject"), handle: profile.handle }); + try { + friendsCol = await getFriends(profile); + activities.friends = friendsCol.orderedItems; + remountUI({ friends: friendsCol.orderedItems.filter(act => act.type !== "Reject"), handle: profile.handle }); + } catch (err) { + console.warn(err.message); + remountUI({ friends: [], handle: profile.handle }); + } // update follow button for new friends const players = window.APP.componentRegistry["player-info"]; players?.forEach(infoComp => setFriendState(infoComp.data.immersId, infoComp.el)); From 75098c80f6f1e2f06646d3e01a02efab830a22e7 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 30 Apr 2021 13:24:50 -0500 Subject: [PATCH 091/167] show explanations & prompts ot upgrade when permissions denied for react ui options: post, view friends, update profile --- .../room/AvatarSettingsContent.js | 4 ++ .../room/ImmersFeedSidebarContainer.js | 45 ++++++++++++++----- src/react-components/room/ImmersReact.js | 37 ++++++++++++++- src/react-components/room/ImmersReact.scss | 14 ++++++ src/react-components/room/PeopleSidebar.js | 7 ++- src/react-components/ui-root.js | 14 ++++-- src/utils/immers.js | 38 ++++++++++++---- 7 files changed, 135 insertions(+), 24 deletions(-) diff --git a/src/react-components/room/AvatarSettingsContent.js b/src/react-components/room/AvatarSettingsContent.js index 0a8f11235a..80465776d0 100644 --- a/src/react-components/room/AvatarSettingsContent.js +++ b/src/react-components/room/AvatarSettingsContent.js @@ -5,6 +5,7 @@ import styles from "./AvatarSettingsContent.scss"; import { TextInputField } from "../input/TextInputField"; import { Column } from "../layout/Column"; import { FormattedMessage } from "react-intl"; +import { ImmersPermissionUpgrade } from "./ImmersReact"; export function AvatarSettingsContent({ displayName, @@ -41,6 +42,9 @@ export function AvatarSettingsContent({ + + Changes will only apply in this Immer. Need permission to update profile or save new avatars + ); } diff --git a/src/react-components/room/ImmersFeedSidebarContainer.js b/src/react-components/room/ImmersFeedSidebarContainer.js index fcb5bc2163..4b2d673884 100644 --- a/src/react-components/room/ImmersFeedSidebarContainer.js +++ b/src/react-components/room/ImmersFeedSidebarContainer.js @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import { ChatSidebar, ChatMessageList, ChatInput, SendMessageButton } from "./ChatSidebar"; import { useMaintainScrollPosition } from "../misc/useMaintainScrollPosition"; import { FormattedMessage, useIntl } from "react-intl"; -import { ImmersChatMessage, ImmersIcon, ImmersMoreHistoryButton } from "./ImmersReact"; +import { ImmersChatMessage, ImmersIcon, ImmersMoreHistoryButton, ImmersPermissionUpgradeButton } from "./ImmersReact"; import { ToolbarButton } from "../input/ToolbarButton"; import { ReactComponent as PublicIcon } from "../icons/Scene.svg"; import { ReactComponent as FriendsIcon } from "../icons/People.svg"; @@ -11,7 +11,7 @@ import { ReactComponent as LocalIcon } from "../icons/Home.svg"; import { IconButton } from "../input/IconButton"; import styles from "./ChatSidebar.scss"; -const ImmersFeedContext = createContext({ messageGroups: [], sendMessage: () => {} }); +export const ImmersFeedContext = createContext({ messageGroups: [], sendMessage: () => {} }); let uniqueMessageId = 0; @@ -54,7 +54,7 @@ function updateMessageGroups(messageGroups, newMessage) { } } -export function ImmersFeedContextProvider({ messageDispatch, children }) { +export function ImmersFeedContextProvider({ messageDispatch, children, permissions, reAuthorize }) { const [messageGroups, setMessageGroups] = useState([]); const [unreadMessages, setUnreadMessages] = useState(false); const [audience, setAudience] = useState("public"); @@ -108,7 +108,16 @@ export function ImmersFeedContextProvider({ messageDispatch, children }) { return ( {children} @@ -117,11 +126,15 @@ export function ImmersFeedContextProvider({ messageDispatch, children }) { ImmersFeedContextProvider.propTypes = { children: PropTypes.node, - messageDispatch: PropTypes.object + messageDispatch: PropTypes.object, + permissions: PropTypes.array, + reAuthorize: PropTypes.func }; export function ImmersFeedSidebarContainer({ onClose }) { - const { messageGroups, sendMessage, setMessagesRead, audience, setAudience } = useContext(ImmersFeedContext); + const { messageGroups, sendMessage, setMessagesRead, audience, setAudience, permissions } = useContext( + ImmersFeedContext + ); const [onScrollList, listRef, scrolledToBottom] = useMaintainScrollPosition(messageGroups); const [message, setMessage] = useState(""); const intl = useIntl(); @@ -187,6 +200,13 @@ export function ImmersFeedSidebarContainer({ onClose }) { {audience === "local" && } ); + const canPost = permissions.includes("creative"); + if (!canPost) { + placeholder = intl.formatMessage({ + id: "immersfeed-sidebar-container.input-placeholder.forbidden", + defaultMessage: "Need permission to post" + }); + } return ( @@ -199,13 +219,18 @@ export function ImmersFeedSidebarContainer({ onClose }) { id="chat-input" onKeyDown={onKeyDown} onChange={e => setMessage(e.target.value)} + disabled={!canPost} placeholder={placeholder} value={message} afterInput={ - <> - - {audienceButton} - + canPost ? ( + <> + + {audienceButton} + + ) : ( + + ) } /> diff --git a/src/react-components/room/ImmersReact.js b/src/react-components/room/ImmersReact.js index 85f60fc7f6..4b05b75f87 100644 --- a/src/react-components/room/ImmersReact.js +++ b/src/react-components/room/ImmersReact.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import PropTypes from "prop-types"; import classNames from "classnames"; import { getMessageComponent } from "./ChatSidebar"; @@ -8,6 +8,7 @@ import { FormattedRelativeTime } from "react-intl"; import { proxiedUrlFor } from "../../utils/media-url-utils"; import immersLogo from "../../assets/images/immers_logo.png"; import merge from "deepmerge"; +import { ImmersFeedContext } from "./ImmersFeedSidebarContainer"; function proxyAndGetMessageComponent(message) { // media urls need proxy to pass CSP & CORS @@ -147,3 +148,37 @@ export function ImmersMoreHistoryButton() { ) ); } + +export function ImmersPermissionUpgrade({ scope, role, children }) { + const { permissions } = useContext(ImmersFeedContext); + if (permissions.includes(scope)) { + return null; + } + return ( +
    + +

    + {children}. +

    +
    + ); +} + +ImmersPermissionUpgrade.propTypes = { + children: PropTypes.node, + scope: PropTypes.string, + role: PropTypes.string +}; + +export function ImmersPermissionUpgradeButton({ role }) { + const { reAuthorize } = useContext(ImmersFeedContext); + return ( +
    reAuthorize(role)}> + Reload & change + + ); +} + +ImmersPermissionUpgradeButton.propTypes = { + role: PropTypes.string +}; diff --git a/src/react-components/room/ImmersReact.scss b/src/react-components/room/ImmersReact.scss index 5184e6e9e0..531a125f86 100644 --- a/src/react-components/room/ImmersReact.scss +++ b/src/react-components/room/ImmersReact.scss @@ -47,3 +47,17 @@ padding-top: 5px; font-size: theme.$font-size-sm; } + +:local(.permissions) { + display: flex; + font-size: theme.$font-size-sm; + padding-left: 5px; + + p { + padding-left: 5px; + } +} + +:local(.permissions-button) { + padding-right: 5px; +} \ No newline at end of file diff --git a/src/react-components/room/PeopleSidebar.js b/src/react-components/room/PeopleSidebar.js index 06ee20c492..f80892ea0c 100644 --- a/src/react-components/room/PeopleSidebar.js +++ b/src/react-components/room/PeopleSidebar.js @@ -14,7 +14,7 @@ import { ReactComponent as VolumeHighIcon } from "../icons/VolumeHigh.svg"; import { ReactComponent as VolumeMutedIcon } from "../icons/VolumeMuted.svg"; import { List, ButtonListItem } from "../layout/List"; import { FormattedMessage, useIntl } from "react-intl"; -import { ImmerLink, ImmersAvatarIcon, ImmersFriendIcon } from "./ImmersReact"; +import { ImmerLink, ImmersAvatarIcon, ImmersFriendIcon, ImmersPermissionUpgrade } from "./ImmersReact"; function getDeviceLabel(ctx, intl) { if (ctx) { @@ -167,6 +167,11 @@ export function PeopleSidebar({ people, onSelectPerson, onClose, showMuteAll, on ); })} +
  • + + Need permission to load friends + +
  • ); diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 5e1a3cd669..e5fcdb7f81 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -145,6 +145,7 @@ class UIRoot extends Component { presences: PropTypes.object, friends: PropTypes.array, handle: PropTypes.string, + immersScopes: PropTypes.array, isMonetized: PropTypes.bool, sessionId: PropTypes.string, subscriptions: PropTypes.object, @@ -173,7 +174,8 @@ class UIRoot extends Component { }; static defaultProps = { - friends: [] + friends: [], + immersScopes: [] }; state = { @@ -1649,7 +1651,11 @@ function UIRootHooksWrapper(props) { return ( - + @@ -1662,7 +1668,9 @@ UIRootHooksWrapper.propTypes = { scene: PropTypes.object.isRequired, messageDispatch: PropTypes.object, store: PropTypes.object.isRequired, - immersMessageDispatch: PropTypes.object + immersMessageDispatch: PropTypes.object, + immersScopes: PropTypes.array, + immersReAuth: PropTypes.func }; export default UIRootHooksWrapper; diff --git a/src/utils/immers.js b/src/utils/immers.js index 6671854658..50aa4b2546 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -231,7 +231,7 @@ export async function getFriends(actorObj) { } // perform oauth flow to get access token for local or remote user -export async function auth(store) { +export async function auth(store, scope) { // copy of URL used for sharing/authorization request const hubUri = new URL(window.location); const hashParams = new URLSearchParams(hubUri.hash.substring(1)); @@ -246,6 +246,9 @@ export async function auth(store) { // remove your handle before sharing with friends searchParams.delete("me"); hubUri.search = searchParams.toString(); + } else if (store.state.profile.handle) { + // can prefill login if known user but needs new token + handle = store.state.profile.handle; } place = await getObject(`${localImmer}/o/immer`); place.url = hubUri; // include room id @@ -273,7 +276,7 @@ export async function auth(store) { // hub link with room id redirect_uri: hubUri, response_type: "token", - scope: preferredScope + scope: scope || preferredScope }); if (handle) { // pass to auth to prefill login form @@ -302,6 +305,19 @@ export async function auth(store) { } } +// force re-login to change authorized scopes +function resetAuth(store, scope) { + token = undefined; + store.update({ + credentials: { + immerToken: null, + immerHome: null, + immerScopes: null + } + }); + return auth(store, scope); +} + export async function initialize(store, scene, remountUI, messageDispatch, createInWorldLogMessage) { hubScene = scene; localPlayer = document.getElementById("avatar-rig"); @@ -400,6 +416,15 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat scene.addEventListener("avatar_updated", async () => { const profile = store.state.profile; const update = {}; + // disable the first-time entry name & avatar prompt + store.update({ + activity: { + hasChangedName: true + } + }); + if (!authorizedScopes.includes("creative")) { + return; + } if (profile.displayName !== actorObj.name) { update.name = profile.displayName; } @@ -411,12 +436,6 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat if (Object.keys(update).length) { await updateProfile(actorObj, update).catch(err => console.error("Error updating profile:", err.message)); } - // disable the first-time entry name & avatar prompt - store.update({ - activity: { - hasChangedName: true - } - }); }); // entity interactions @@ -507,6 +526,7 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat }) .catch(err => console.error(`Error sharing chat: ${err.message}`)); }); - remountUI({ immersMessageDispatch }); + const immersReAuth = scope => resetAuth(store, scope); + remountUI({ immersMessageDispatch, immersScopes: authorizedScopes, immersReAuth }); } } From b3991d42f92b02f1fd402a92b3d0721d221bc9cc Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 30 Apr 2021 13:28:33 -0500 Subject: [PATCH 092/167] hide in-world buttons that lack required permissions --- .../immers/immers-visible-if-permitted.js | 19 +++++++++ src/components/immers/index.js | 1 + src/hub.html | 40 ++++++++++--------- src/utils/immers.js | 1 + 4 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 src/components/immers/immers-visible-if-permitted.js diff --git a/src/components/immers/immers-visible-if-permitted.js b/src/components/immers/immers-visible-if-permitted.js new file mode 100644 index 0000000000..c337fe015b --- /dev/null +++ b/src/components/immers/immers-visible-if-permitted.js @@ -0,0 +1,19 @@ +AFRAME.registerComponent("immers-visible-if-permitted", { + schema: { + type: "string", + oneOf: ["viewFriends", "postLocation", "viewPrivate", "creative", "addFriends", "addBlocks", "destructive"] + }, + init() { + this.updateVisibility = this.updateVisibility.bind(this); + this.el.sceneEl.addEventListener("stateadded", this.updateVisibility); + this.el.sceneEl.addEventListener("stateremoved", this.updateVisibility); + this.updateVisibility(); + }, + updateVisibility() { + this.el.object3D.visible = this.el.sceneEl.states.includes(`immers-scope-${this.data}`); + }, + remove() { + this.el.sceneEl.removeEventListener("stateadded", this.updateVisibility); + this.el.sceneEl.removeEventListener("stateremoved", this.updateVisibility); + } +}); diff --git a/src/components/immers/index.js b/src/components/immers/index.js index 1dbac2d746..271048bda4 100644 --- a/src/components/immers/index.js +++ b/src/components/immers/index.js @@ -6,3 +6,4 @@ import "./monetization-invisible"; import "./monetization-required"; import "./monetization-interactable"; import "./monetization-networked"; +import "./immers-visible-if-permitted"; diff --git a/src/hub.html b/src/hub.html index 9f82bd922a..0545b296ee 100644 --- a/src/hub.html +++ b/src/hub.html @@ -185,7 +185,7 @@ - + @@ -855,24 +855,26 @@ position="0 0.225 0" class="video-snap-button" > - - - + + + + + + + + + - - - - + + diff --git a/src/utils/immers.js b/src/utils/immers.js index 50aa4b2546..c32094aa85 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -345,6 +345,7 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat immerScopes: authorizedScopes } }); + authorizedScopes.forEach(scope => hubScene.addState(`immers-scope-${scope}`)); const immerSocket = io(homeImmer, { transportOptions: { polling: { From c6d75f10209f50cc70aa0449498e0428b35d56d5 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 30 Apr 2021 13:45:04 -0500 Subject: [PATCH 093/167] toggle label on block button between hide & block based on permissions --- src/components/block-button.js | 12 ++++++++++++ src/hub.html | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/block-button.js b/src/components/block-button.js index 4073e82042..23c7391086 100644 --- a/src/components/block-button.js +++ b/src/components/block-button.js @@ -5,21 +5,33 @@ */ AFRAME.registerComponent("block-button", { init() { + this.textEl = this.el.querySelector("[text]"); this.onClick = () => { this.block(this.owner); this.el.emit("immers-block", { clientId: this.owner }); }; + this.onScopeChange = () => { + if (this.el.sceneEl.states.includes("immers-scope-addBlocks")) { + this.textEl.setAttribute("text", "value", "Block"); + } else { + this.textEl.setAttribute("text", "value", "Hide"); + } + }; NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { this.owner = networkedEl.components.networked.data.owner; }); + this.onScopeChange(); }, play() { this.el.object3D.addEventListener("interact", this.onClick); + this.el.sceneEl.addEventListener("stateadded", this.onScopeChange); + this.el.sceneEl.addEventListener("stateremoved", this.onScopeChange); }, pause() { this.el.object3D.removeEventListener("interact", this.onClick); + this.el.sceneEl.removeEventListener("stateremoved", this.onScopeChange); }, block(clientId) { diff --git a/src/hub.html b/src/hub.html index 7d165c2ba3..0545b296ee 100644 --- a/src/hub.html +++ b/src/hub.html @@ -189,7 +189,7 @@ - + From 9dd31e3e9ad1f11bd58f47de147c8a211e466bfb Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 30 Apr 2021 13:45:44 -0500 Subject: [PATCH 094/167] avoid errors from race on authorizedScopes --- src/react-components/room/ImmersFeedSidebarContainer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/react-components/room/ImmersFeedSidebarContainer.js b/src/react-components/room/ImmersFeedSidebarContainer.js index 4b2d673884..7694da1f71 100644 --- a/src/react-components/room/ImmersFeedSidebarContainer.js +++ b/src/react-components/room/ImmersFeedSidebarContainer.js @@ -11,7 +11,7 @@ import { ReactComponent as LocalIcon } from "../icons/Home.svg"; import { IconButton } from "../input/IconButton"; import styles from "./ChatSidebar.scss"; -export const ImmersFeedContext = createContext({ messageGroups: [], sendMessage: () => {} }); +export const ImmersFeedContext = createContext({ messageGroups: [], sendMessage: () => {}, permissions: [] }); let uniqueMessageId = 0; @@ -54,7 +54,7 @@ function updateMessageGroups(messageGroups, newMessage) { } } -export function ImmersFeedContextProvider({ messageDispatch, children, permissions, reAuthorize }) { +export function ImmersFeedContextProvider({ messageDispatch, children, permissions = [], reAuthorize }) { const [messageGroups, setMessageGroups] = useState([]); const [unreadMessages, setUnreadMessages] = useState(false); const [audience, setAudience] = useState("public"); From c9ceb00004a89a670ec4cbf156d8041b89e4b446 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 30 Apr 2021 13:46:15 -0500 Subject: [PATCH 095/167] bump client version --- package.json | 2 +- src/utils/immers.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d645564c23..fb1e1525ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "0.0.1", + "version": "1.0.0", "description": "Duck-themed multi-user virtual spaces in WebVR.", "main": "src/index.js", "license": "MPL-2.0", diff --git a/src/utils/immers.js b/src/utils/immers.js index 886f60241f..ba67de4ae4 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -8,7 +8,7 @@ import Activities from "./immers/activities"; const localImmer = configs.IMMERS_SERVER; // immer can set a requested scope, but user can override const preferredScope = configs.IMMERS_SCOPE; -console.log("immers.space client v0.7.1"); +console.log("immers.space client v1.1.0"); const jsonldMime = "application/activity+json"; // avoid race between auth and initialize code let resolveAuth; From 003f32a7bc62e349a8f871dd8946aa7c55568034 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 30 Apr 2021 13:52:55 -0500 Subject: [PATCH 096/167] 1.1.0-alpha.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index df5d2df7f6..89be49b7e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "0.0.1", + "version": "1.1.0-alpha.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fb1e1525ed..85385ccff8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.0.0", + "version": "1.1.0-alpha.0", "description": "Duck-themed multi-user virtual spaces in WebVR.", "main": "src/index.js", "license": "MPL-2.0", From b722d2da1efae3680c38ff820bef0388e5e45255 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 7 May 2021 17:12:45 -0500 Subject: [PATCH 097/167] Popup Auth & scope bugfixes - Change from redirect-based OAuth to a popup window - Update react UX to show login prompt and hide immers chat panel before login - Refactor auth & initialization flow to work with popup - Update scope upgrade flow to work with popup - Fix duplicate copies of scope accumulating in store - Fix home immer showing inaccurate scope upgrade notices - Change pass-your-handle-through-links to use hash because we no longer update the query during redirect - Prefill handle in login form when renewing expired token on remote immer --- src/hub.js | 2 - src/index.js | 4 + src/react-components/room/ImmersReact.js | 17 +- src/react-components/room/ImmersReact.scss | 5 + src/react-components/room/RoomEntryModal.js | 15 +- src/react-components/ui-root.js | 11 +- src/utils/immers.js | 205 +++++++++++--------- src/utils/immers/activities.js | 1 + src/utils/immers/authUtils.js | 32 +++ 9 files changed, 183 insertions(+), 109 deletions(-) create mode 100644 src/utils/immers/authUtils.js diff --git a/src/hub.js b/src/hub.js index 6625fa29d1..15eeb462bc 100644 --- a/src/hub.js +++ b/src/hub.js @@ -191,8 +191,6 @@ if (isEmbed && !qs.get("embed_token")) { throw new Error("no embed token"); } -immers.auth(store); - THREE.Object3D.DefaultMatrixAutoUpdate = false; import "./components/owned-object-limiter"; diff --git a/src/index.js b/src/index.js index 7d2c6f0d8e..1902ba18d2 100644 --- a/src/index.js +++ b/src/index.js @@ -8,6 +8,10 @@ import { HomePage } from "./react-components/home/HomePage"; import { AuthContextProvider } from "./react-components/auth/AuthContext"; import "./react-components/styles/global.scss"; import { ThemeProvider } from "./react-components/styles/theme"; +import { catchToken } from "./utils/immers/authUtils"; + +// homepage is used as redirectURI in popup OAuth flow (because using the hub uri causes duplicate session disconnect) +catchToken(); registerTelemetry("/home", "Hubs Home Page"); diff --git a/src/react-components/room/ImmersReact.js b/src/react-components/room/ImmersReact.js index 4b05b75f87..9b3c9e79f1 100644 --- a/src/react-components/room/ImmersReact.js +++ b/src/react-components/room/ImmersReact.js @@ -33,9 +33,9 @@ export function ImmerLink({ place }) { ) { placeUrl = null; } else { - const search = new URLSearchParams(url.search); - search.set("me", window.APP.store.state.profile.handle); - url.search = search.toString(); + const hashParams = new URLSearchParams(); + hashParams.set("me", window.APP.store.state.profile.handle); + url.hash = hashParams.toString(); placeUrl = url.toString(); } } catch (ignore) { @@ -86,24 +86,25 @@ ImmersChatMessage.propTypes = { context: PropTypes.object }; -export function ImmersImageIcon({ src, title }) { +export function ImmersImageIcon({ src, title, button }) { return ( - + {src && } ); } ImmersImageIcon.propTypes = { src: PropTypes.string, - title: PropTypes.string + title: PropTypes.string, + button: PropTypes.bool }; export function ImmersFriendIcon() { return ; } -export function ImmersIcon() { - return ; +export function ImmersIcon(props) { + return ; } export function ImmersAvatarIcon({ avi }) { diff --git a/src/react-components/room/ImmersReact.scss b/src/react-components/room/ImmersReact.scss index 531a125f86..998ab91b91 100644 --- a/src/react-components/room/ImmersReact.scss +++ b/src/react-components/room/ImmersReact.scss @@ -6,6 +6,11 @@ overflow: hidden; } +:local(.button-icon) { + margin-right: 8px; + flex-shrink: 0; +} + :local(.image-icon) { width: 20px; } diff --git a/src/react-components/room/RoomEntryModal.js b/src/react-components/room/RoomEntryModal.js index 35599a92bb..af889b9178 100644 --- a/src/react-components/room/RoomEntryModal.js +++ b/src/react-components/room/RoomEntryModal.js @@ -13,12 +13,15 @@ import styleUtils from "../styles/style-utils.scss"; import { useCssBreakpoints } from "react-use-css-breakpoints"; import { Column } from "../layout/Column"; import { FormattedMessage } from "react-intl"; +import { ImmersIcon } from "./ImmersReact"; export function RoomEntryModal({ appName, logoSrc, className, roomName, + showLoginToImmers, + onLoginToImmers, showJoinRoom, onJoinRoom, showEnterOnDevice, @@ -48,7 +51,15 @@ export function RoomEntryModal({

    {roomName}

    - {showJoinRoom && ( + {showLoginToImmers && ( + + )} + {!showLoginToImmers && showJoinRoom && ( )} - {showEnterOnDevice && ( + {!showLoginToImmers && showEnterOnDevice && ( + <> + + +

    Login or create a free account to join the room

    + )} - {!showLoginToImmers && showJoinRoom && ( + {showJoinRoom && ( )} - {!showLoginToImmers && showEnterOnDevice && ( + {showEnterOnDevice && ( )}
    - {!showJoinRoom &&

    This space has no more free slots available.

    } - {showMonetized && ( -
    - Thanks for paying! You can join this space even if it is full. Search for this icon in the space to find - other premium features. -
    + <> + +

    + Thanks for paying! {showRoomFull && You can join this space even though it is full.}{" "} + Search for this icon in the space to find other premium features. +

    + )} {showMonetizationRequired && ( -
    - - Sign up for Web Monetization - {" "} - to unlock premium features and join spaces even when they are full. -
    + <> + +

    + This space has no more free slots available.{" "} + + Sign up for Web Monetization + {" "} + to join anyway. +

    + )}
    diff --git a/src/react-components/room/RoomEntryModal.scss b/src/react-components/room/RoomEntryModal.scss index 7d5392fc92..4bab1f4fee 100644 --- a/src/react-components/room/RoomEntryModal.scss +++ b/src/react-components/room/RoomEntryModal.scss @@ -10,6 +10,10 @@ @media(min-width: theme.$breakpoint-lg) and (min-height: theme.$breakpoint-vr) { padding: 24px; } + p { + font-size: theme.$font-size-sm; + max-width: 275px; + } } :local(.logo-container) { @@ -53,9 +57,9 @@ :local(.webmon) { display: flex; - flex-direction: column; + flex-direction: row; align-items: center; svg { flex-shrink: 0; } -} \ No newline at end of file +} diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index baa89ef70d..9b72b6d29d 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -801,8 +801,10 @@ class UIRoot extends Component { renderEntryStartPanel = () => { const { hasAcceptedProfile, hasChangedName } = this.props.store.state.activity; const promptForNameAndAvatarBeforeEntry = this.props.hubIsBound ? !hasAcceptedProfile : !hasChangedName; + const pageIsMonetized = !!document.querySelector("meta[name=monetization]"); + const showLogin = !this.props.isImmersConnected; // monetized users can bypass room limit - const canEnter = !this.props.entryDisallowed || this.props.isMonetized; + const canEnter = this.props.isImmersConnected && (!this.props.entryDisallowed || this.props.isMonetized); // TODO: What does onEnteringCanceled do? return ( <> @@ -810,7 +812,7 @@ class UIRoot extends Component { appName={configs.translation("app-name")} logoSrc={configs.image("logo")} roomName={this.props.hub.name} - showLoginToImmers={!this.props.isImmersConnected} + showLoginToImmers={showLogin} onLoginToImmers={this.props.startImmersAuth} showJoinRoom={!this.state.waitingOnAudio && canEnter} onJoinRoom={() => { @@ -830,7 +832,7 @@ class UIRoot extends Component { }} showEnterOnDevice={!this.state.waitingOnAudio && canEnter && !isMobileVR} onEnterOnDevice={() => this.attemptLink()} - showSpectate={!this.state.waitingOnAudio} + showSpectate={false} onSpectate={() => this.setState({ watching: true })} showOptions={this.props.hubChannel.canOrWillIfCreator("update_hub")} onOptions={() => { @@ -840,8 +842,11 @@ class UIRoot extends Component { SignInMessages.roomSettings ); }} - showMonetizationRequired={!this.props.isMonetized} - showMonetized={this.props.isMonetized} + showRoomFull={!this.state.waitingOnAudio && !showLogin && this.props.entryDisallowed} + showMonetizationRequired={ + !this.state.waitingOnAudio && pageIsMonetized && !showLogin && !this.props.isMonetized && !canEnter + } + showMonetized={!this.state.waitingOnAudio && !showLogin && this.props.isMonetized} /> {!this.state.waitingOnAudio && ( { + startImmersAuth: evt => { + // send to token endpoint at local immer, it handles + // detecting remote users and sending them on to their home to login + const redirect = new URL(`${localImmer}/auth/authorize`); + const redirectParams = new URLSearchParams({ + client_id: place.id, + // redirect to homepage to catch token + redirect_uri: hubUri.origin, + response_type: "token", + scope: scope || preferredScope + }); + // users handle may be passed from previous immer or cached but with expired token + if (handle || store.state.profile.handle) { + // pass to auth to prefill login form + redirectParams.set("me", handle || store.state.profile.handle); + } + if (evt.currentTarget.classList.contains("registration")) { + redirectParams.set("tab", "Register"); + } + redirect.search = redirectParams.toString(); popup = window.open(redirect, "immersLoginPopup", features); } }; From a6fbcd203ca1bfd53f388a961f462c54ac627c26 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 23 May 2021 16:38:15 -0500 Subject: [PATCH 105/167] don't show avatar settings or scope upgrade prompts prior to login --- src/react-components/room/ImmersReact.js | 8 ++++++-- src/react-components/ui-root.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/react-components/room/ImmersReact.js b/src/react-components/room/ImmersReact.js index 9b3c9e79f1..c1c9333730 100644 --- a/src/react-components/room/ImmersReact.js +++ b/src/react-components/room/ImmersReact.js @@ -151,8 +151,8 @@ export function ImmersMoreHistoryButton() { } export function ImmersPermissionUpgrade({ scope, role, children }) { - const { permissions } = useContext(ImmersFeedContext); - if (permissions.includes(scope)) { + const { permissions, reAuthorize } = useContext(ImmersFeedContext); + if (permissions.includes(scope) || !reAuthorize) { return null; } return ( @@ -173,6 +173,10 @@ ImmersPermissionUpgrade.propTypes = { export function ImmersPermissionUpgradeButton({ role }) { const { reAuthorize } = useContext(ImmersFeedContext); + if (!reAuthorize) { + // initial auth has not occurred + return null; + } return ( reAuthorize(role)}> Reload & change diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 9b72b6d29d..0deb48143a 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -1140,7 +1140,7 @@ class UIRoot extends Component { reason: LeaveReason.createRoom }) }, - { + this.props.isImmersConnected && { id: "user-profile", label: , icon: AvatarIcon, From e8731aa371df6203b5359299efd91030a20b966c Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 23 May 2021 16:45:44 -0500 Subject: [PATCH 106/167] cleanup UX - do not prompt users with options that require Hubs account signup since we have that disabled --- src/hub.html | 1 + src/react-components/media-browser.js | 20 +++++++++++++------- src/react-components/room/MediaTiles.js | 2 ++ src/react-components/ui-root.js | 8 ++++---- src/utils/hub-channel.js | 6 ++++-- 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/hub.html b/src/hub.html index 0545b296ee..6cfa6d316e 100644 --- a/src/hub.html +++ b/src/hub.html @@ -326,6 +326,7 @@ 0 || !showEmptyStringOnNoResult ? ( <> - {urlSource === "avatars" && ( - } - /> - )} + {this.props.hubChannel.signedIn && + urlSource === "avatars" && ( + } + /> + )} {urlSource === "scenes" && + this.props.hubChannel.signedIn && configs.feature("enable_spoke") && ( this.handleCopyScene(e, entry); } + if (!this.props.hubChannel.signedIn) { + onCopy = null; + } + return ( )} {entry.type === "avatar_listing" && + onCopy && entry.allow_remixing && ( )} {entry.type === "scene_listing" && + onCopy && entry.allow_remixing && ( , + label: , icon: EnterIcon, onClick: () => this.showContextualSignInDialog() }, @@ -1146,7 +1146,7 @@ class UIRoot extends Component { icon: AvatarIcon, onClick: () => this.setSidebar("profile") }, - { + this.state.signedIn && { id: "favorite-rooms", label: , icon: FavoritesIcon, @@ -1185,14 +1185,14 @@ class UIRoot extends Component { icon: InviteIcon, onClick: () => this.props.scene.emit("action_invite") }, - this.isFavorited() + this.state.signedIn && this.isFavorited() ? { id: "unfavorite-room", label: , icon: StarIcon, onClick: () => this.toggleFavorited() } - : { + : this.state.signedIn && { id: "favorite-room", label: , icon: StarOutlineIcon, diff --git a/src/utils/hub-channel.js b/src/utils/hub-channel.js index 45e680f001..02751df9e8 100644 --- a/src/utils/hub-channel.js +++ b/src/utils/hub-channel.js @@ -51,7 +51,8 @@ export default class HubChannel extends EventTarget { // Returns true if the current session has the given permission, *or* will get the permission // if they sign in and become the creator. canOrWillIfCreator(permission) { - if (this._getCreatorAssignmentToken() && HUB_CREATOR_PERMISSIONS.includes(permission)) return true; + // immers: just show current perms; avoid showing options that aren't available + // if (this._getCreatorAssignmentToken() && HUB_CREATOR_PERMISSIONS.includes(permission)) return true; return this.can(permission); } @@ -105,6 +106,7 @@ export default class HubChannel extends EventTarget { setPermissionsFromToken = token => { // Note: token is not verified. this._permissions = jwtDecode(token); + this._permissions.pin_objects = this._permissions.pin_objects && this._signedIn; configs.setIsAdmin(this._permissions.postgrest_role === "ret_admin"); this.dispatchEvent(new CustomEvent("permissions_updated")); @@ -272,8 +274,8 @@ export default class HubChannel extends EventTarget { this.channel .push("sign_in", { token, creator_assignment_token }) .receive("ok", ({ perms_token }) => { - this.setPermissionsFromToken(perms_token); this._signedIn = true; + this.setPermissionsFromToken(perms_token); resolve(); }) .receive("error", err => { From 67edc0b78879cf08ad7b7b046e5ace6a61fd2f4a Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sun, 23 May 2021 16:46:45 -0500 Subject: [PATCH 107/167] fix showing create room option on servers with it disabled --- src/react-components/ui-root.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 947a48c44d..73b8246637 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -1103,7 +1103,7 @@ class UIRoot extends Component { const renderEntryFlow = (!enteredOrWatching && this.props.hub) || this.isWaitingForAutoExit(); - const canCreateRoom = !configs.feature("disable_room_creation") || configs.isAdmin; + const canCreateRoom = !configs.feature("disable_room_creation") || configs.isAdmin(); const canCloseRoom = this.props.hubChannel && !!this.props.hubChannel.canOrWillIfCreator("close_hub"); const isModerator = this.props.hubChannel && this.props.hubChannel.canOrWillIfCreator("kick_users") && !isMobileVR; From 9fc16c89cb2a2f7b5c5d5163cafc8c48b99d3785 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Mon, 24 May 2021 08:25:08 -0500 Subject: [PATCH 108/167] consistent use of 'space' to decsribe a room in entry panel tips --- src/react-components/room/RoomEntryModal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react-components/room/RoomEntryModal.js b/src/react-components/room/RoomEntryModal.js index 1eccf7e1bc..961bdfd3f6 100644 --- a/src/react-components/room/RoomEntryModal.js +++ b/src/react-components/room/RoomEntryModal.js @@ -66,7 +66,7 @@ export function RoomEntryModal({
    -

    Login or create a free account to join the room

    +

    Login or create a free account to join this space

    )} {showJoinRoom && ( From 4ca539cd083b1195cee9cc4d0f4d49b64b188683 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 28 May 2021 16:33:22 -0500 Subject: [PATCH 109/167] don't show custom avatar import link if not logged in --- src/react-components/media-browser.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/react-components/media-browser.js b/src/react-components/media-browser.js index 24103b6f66..bb687292ea 100644 --- a/src/react-components/media-browser.js +++ b/src/react-components/media-browser.js @@ -370,8 +370,7 @@ class MediaBrowserContainer extends Component { const urlSource = this.getUrlSource(searchParams); const isSceneApiType = urlSource === "scenes"; const isFavorites = urlSource === "favorites"; - const showCustomOption = - !isFavorites && (!isSceneApiType || this.props.hubChannel.canOrWillIfCreator("update_hub")); + const showCustomOption = !isFavorites && this.props.hubChannel.canOrWillIfCreator("update_hub"); const entries = (this.state.result && this.state.result.entries) || []; const hideSearch = urlSource === "favorites"; const showEmptyStringOnNoResult = urlSource !== "avatars" && urlSource !== "scenes"; From 7a10481ebbff67adb659f8b454ba075c6a9765f6 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 4 Jun 2021 16:13:21 -0500 Subject: [PATCH 110/167] Revert "don't show custom avatar import link if not logged in" This reverts commit 4ca539cd083b1195cee9cc4d0f4d49b64b188683. --- src/react-components/media-browser.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/react-components/media-browser.js b/src/react-components/media-browser.js index bb687292ea..24103b6f66 100644 --- a/src/react-components/media-browser.js +++ b/src/react-components/media-browser.js @@ -370,7 +370,8 @@ class MediaBrowserContainer extends Component { const urlSource = this.getUrlSource(searchParams); const isSceneApiType = urlSource === "scenes"; const isFavorites = urlSource === "favorites"; - const showCustomOption = !isFavorites && this.props.hubChannel.canOrWillIfCreator("update_hub"); + const showCustomOption = + !isFavorites && (!isSceneApiType || this.props.hubChannel.canOrWillIfCreator("update_hub")); const entries = (this.state.result && this.state.result.entries) || []; const hideSearch = urlSource === "favorites"; const showEmptyStringOnNoResult = urlSource !== "avatars" && urlSource !== "scenes"; From 2301e8608f63b1ab131d700411f4699f78398b6d Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 4 Jun 2021 16:16:17 -0500 Subject: [PATCH 111/167] support avatars from URL by filling in default icon when creating avatar object, fix unnecessary avatar creations/profile updates due to incorrect comparison of old v new avatar --- src/utils/immers.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 075a144413..0c61ecba3c 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -122,7 +122,7 @@ export async function createAvatar(actorObj, hubsAvatarId) { const hubsAvatar = await fetchAvatar(hubsAvatarId); const immersAvatar = { type: "Model", - name: hubsAvatar.name, + name: hubsAvatar.name ?? "Imported avatar", url: { type: "Link", href: hubsAvatar.gltf_url, @@ -131,13 +131,12 @@ export async function createAvatar(actorObj, hubsAvatarId) { to: actorObj.followers, generator: place }; - if (hubsAvatar.files.thumbnail) { - immersAvatar.icon = { - type: "Image", - mediaType: "image/png", - url: hubsAvatar.files.thumbnail - }; - } + immersAvatar.icon = { + type: "Image", + mediaType: "image/png", + // direct URL avatars won't have a preview image, fill in default + url: hubsAvatar.files?.thumbnail || configs.image("logo") + }; if (hubsAvatar.attributions) { immersAvatar.attributedTo = Object.values(hubsAvatar.attributions).map(name => ({ name, @@ -464,13 +463,15 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat if (profile.displayName !== actorObj.name) { update.name = profile.displayName; } - if (getAvatarFromActor(actorObj) !== profile.avatarId) { + if (getAvatarFromActor(actorObj) !== (await fetchAvatar(profile.avatarId)).gltf_url) { update.avatar = myAvatars[profile.avatarId] || (await createAvatar(actorObj, profile.avatarId)).object; update.icon = update.avatar.icon; } // only publish update if something changed if (Object.keys(update).length) { await updateProfile(actorObj, update).catch(err => console.error("Error updating profile:", err.message)); + // update cached copy of profile + Object.assign(actorObj, update); } }); From 8b732c1dc67d15b071d4b96bd1728b2233f2d5ca Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 4 Jun 2021 16:17:02 -0500 Subject: [PATCH 112/167] fix hubs disconnecting while doing auth on mobile/standalone --- src/systems/exit-on-blur.js | 2 +- src/utils/immers.js | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/systems/exit-on-blur.js b/src/systems/exit-on-blur.js index c224055387..15a37be4c5 100644 --- a/src/systems/exit-on-blur.js +++ b/src/systems/exit-on-blur.js @@ -42,7 +42,7 @@ AFRAME.registerSystem("exit-on-blur", { }, onBlur() { - if (this.el.isMobile) { + if (this.el.isMobile && !this.el.is("immers-authorizing")) { clearTimeout(this.exitTimeout); this.exitTimeout = setTimeout(this.onTimeout, 30 * 1000); } diff --git a/src/utils/immers.js b/src/utils/immers.js index 0c61ecba3c..c96ac857db 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -298,6 +298,12 @@ export async function auth(store, scope) { } redirect.search = redirectParams.toString(); popup = window.open(redirect, "immersLoginPopup", features); + if (!popup) { + alert("Could not open login window. Please check if popup was blocked and allow it"); + } else { + hubScene?.addState("immers-authorizing"); + popup.onunload = () => hubScene?.removeState("immers-authorizing"); + } } }; } From 7ccd515d3275ffab768c41f322395d1584ba0f15 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 4 Jun 2021 16:47:00 -0500 Subject: [PATCH 113/167] 1.2.1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index a69da65606..1611a82ffe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 97919ba257..abf4f9157e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.2.0", + "version": "1.2.1", "description": "Duck-themed multi-user virtual spaces in WebVR.", "main": "src/index.js", "license": "MPL-2.0", From ee8cea721bca883981cd8a64b4f7e6d6a13df28d Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 18 Jun 2021 13:31:16 -0500 Subject: [PATCH 114/167] insert placeholders for build vars and update via string replacement on deploy --- Dockerfile | 2 + dockerdeploy.sh | 5 +- scripts/deploy.js | 152 +++++++++++++++++++++++++++++----------------- 3 files changed, 101 insertions(+), 58 deletions(-) diff --git a/Dockerfile b/Dockerfile index 565aa12ed7..447edaed4a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,4 +11,6 @@ RUN npm ci WORKDIR /usr/src/hubs COPY . . +RUN npm run deploy -- --skipCI --noUpload --envPlaceholders + CMD [ "/bin/bash", "dockerdeploy.sh" ] diff --git a/dockerdeploy.sh b/dockerdeploy.sh index 8d3415863b..8cd6e13800 100755 --- a/dockerdeploy.sh +++ b/dockerdeploy.sh @@ -2,9 +2,10 @@ set -e echo "Logging into to hub $hub as $email" npm run login -- --host $hub --email $email -echo "Deploying Immers Space hubs client" -npm run deploy -- --skipCI echo "Updating hubs config for immer $domain" # this one reads from env because of issues with dollar sign in payment pointer npm run immers-configure +echo "Deploying Immers Space hubs client" +npm run deploy -- --noBuild --replacePlaceholders + echo "Done" diff --git a/scripts/deploy.js b/scripts/deploy.js index 5f7d9c5ff8..4e2d6296fd 100644 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -8,14 +8,20 @@ import FormData from "form-data"; import path from "path"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; -const skipCI = yargs(hideBin(process.argv)).argv.skipCI; +const { skipCI, noBuild, noUpload, envPlaceholders, replacePlaceholders } = yargs(hideBin(process.argv)).argv; + +let host; +let token; if (!existsSync(".ret.credentials")) { - console.log("Not logged in, so cannot deploy. To log in, run npm run login."); - process.exit(0); + if (!noUpload && !envPlaceholders) { + console.log("Not logged in, so cannot deploy. To log in, run npm run login."); + process.exit(1); + } +} else { + ({ host, token } = JSON.parse(readFileSync(".ret.credentials"))); } -const { host, token } = JSON.parse(readFileSync(".ret.credentials")); console.log(`Deploying to ${host}.`); const step = ora({ indent: 2 }).start(); @@ -34,80 +40,114 @@ const getTs = (() => { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }; - - const res = await fetch(`https://${host}/api/ita/configs/hubs`, { headers }); - const hubsConfigs = await res.json(); - const buildEnv = {}; - for (const [k, v] of Object.entries(hubsConfigs.general)) { - buildEnv[k.toUpperCase()] = v; + let buildEnv = {}; + let hubsConfigs + if (envPlaceholders) { + buildEnv = { + CORS_PROXY_SERVER: "__IMMERS_PLACEHOLDER_CORS_PROXY_SERVER", + BASE_ASSETS_PATH: "__IMMERS_PLACEHOLDER_BASE_ASSETS_PATH", + SHORTLINK_DOMAIN: "__IMMERS_PLACEHOLDER_SHORTLINK_DOMAIN", + SENTRY_DSN: "__IMMERS_PLACEHOLDER_SENTRY_DSN", + GA_TRACKING_ID: "__IMMERS_PLACEHOLDER_GA_TRACKING_ID", + RETICULUM_SERVER: "__IMMERS_PLACEHOLDER_RETICULUM_SERVER", + THUMBNAIL_SERVER: "__IMMERS_PLACEHOLDER_THUMBNAIL_SERVER", + NON_CORS_PROXY_DOMAINS: "__IMMERS_PLACEHOLDER_NON_CORS_PROXY_DOMAINS", + }; + } else { + const res = await fetch(`https://${host}/api/ita/configs/hubs`, { headers }); + hubsConfigs = await res.json(); + for (const [k, v] of Object.entries(hubsConfigs.general)) { + buildEnv[k.toUpperCase()] = v; + } } const version = getTs(); - buildEnv.BUILD_VERSION = `1.0.0.${version}`; + buildEnv.BUILD_VERSION = `${process.env.npm_package_version}.${version}`; buildEnv.ITA_SERVER = ""; buildEnv.POSTGREST_SERVER = ""; buildEnv.CONFIGURABLE_SERVICES = "janus-gateway,reticulum,hubs,spoke"; const env = Object.assign(process.env, buildEnv); + if (!noBuild) { + for (const d in ["./dist", "./admin/dist"]) { + rmdir(d, err => { + if (err) { + console.error(err); + process.exit(1); + } + }); + } - for (const d in ["./dist", "./admin/dist"]) { - rmdir(d, err => { - if (err) { - console.error(err); - process.exit(1); - } - }); - } - - step.text = "Building Client."; + step.text = "Building Client."; - await new Promise((resolve, reject) => { - if (skipCI) { - return resolve(); - } - exec("npm ci", {}, err => { - if (err) reject(err); - resolve(); + await new Promise((resolve, reject) => { + if (skipCI) { + return resolve(); + } + exec("npm ci", {}, err => { + if (err) reject(err); + resolve(); + }); }); - }); - await new Promise((resolve, reject) => { - exec("npm run build", { env }, err => { - if (err) reject(err); - resolve(); + await new Promise((resolve, reject) => { + exec("npm run build", { env }, err => { + if (err) reject(err); + resolve(); + }); }); - }); - step.text = "Building Admin Console."; + step.text = "Building Admin Console."; - await new Promise((resolve, reject) => { - if (skipCI) { - return resolve(); - } - exec("npm ci", { cwd: "./admin" }, err => { - if (err) reject(err); - resolve(); + await new Promise((resolve, reject) => { + if (skipCI) { + return resolve(); + } + exec("npm ci", { cwd: "./admin" }, err => { + if (err) reject(err); + resolve(); + }); }); - }); - await new Promise((resolve, reject) => { - exec("npm run build", { cwd: "./admin", env }, err => { - if (err) reject(err); - resolve(); + await new Promise((resolve, reject) => { + exec("npm run build", { cwd: "./admin", env }, err => { + if (err) reject(err); + resolve(); + }); }); - }); - await new Promise(res => { - ncp("./admin/dist", "./dist", err => { - if (err) { - console.error(err); - process.exit(1); - } + await new Promise(res => { + ncp("./admin/dist", "./dist", err => { + if (err) { + console.error(err); + process.exit(1); + } - res(); + res(); + }); }); - }); + } + if (replacePlaceholders) { + // update prebuilt bundle placeholders with server-specific values + for (const [k, v] of Object.entries(hubsConfigs.general)) { + await new Promise((resolve, reject) => { + const ph = v.endsWith("/") + // avoid double slash on substitution + ? `__IMMERS_PLACEHOLDER_${k.toUpperCase()}/\\?` + : `__IMMERS_PLACEHOLDER_${k.toUpperCase()}`; + exec(`find ./dist -iregex ".*\\.\\(js\\|html\\|css\\|map\\)" -print0 | xargs -0 sed -i -e 's|${ph}|${v}|g'`, { env }, err => { + if (err) reject(err); + resolve(); + }); + }); + } + } + if (noUpload) { + step.text = `Skipping deploy.`; + step.succeed(); + process.exit(0); + } step.text = "Preparing Deploy."; step.text = "Packaging Build."; From 836b93e78a1636cd932c7c36c4595dbae1045862 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 25 Jun 2021 10:05:00 -0500 Subject: [PATCH 115/167] bump web monetization polyfill, remove now-obsolete script-src CSP config --- package-lock.json | 6 +++--- package.json | 2 +- scripts/immers-configure.js | 7 ++----- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1611a82ffe..91d5abc6e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38341,9 +38341,9 @@ } }, "web-monetization-polyfill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/web-monetization-polyfill/-/web-monetization-polyfill-1.0.0.tgz", - "integrity": "sha512-lNEkyOtXXdSppvrfTes2/x5LyTCxe7X33at559QC+UM8lHWmEK+6i00iSr78geaM7KCw3paXH16ZR2uFpLEXbw==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/web-monetization-polyfill/-/web-monetization-polyfill-2.0.0.tgz", + "integrity": "sha512-qrt1PawK4pKtc+aZtu2rxubm8pF25QOcHaU04K3sGMcyqJYr7WwGdXlfK7fA0TkQI0niMO7ZhaqyQ1T2tnVH6A==" }, "web-namespaces": { "version": "1.1.4", diff --git a/package.json b/package.json index abf4f9157e..1b99fe7a9a 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "three-to-ammo": "github:infinitelee/three-to-ammo", "use-clipboard-copy": "^0.1.2", "uuid": "^3.2.1", - "web-monetization-polyfill": "^1.0.0", + "web-monetization-polyfill": "^2.0.0", "webrtc-adapter": "^7.7.0", "zip-loader": "^1.1.0" }, diff --git a/scripts/immers-configure.js b/scripts/immers-configure.js index 239b98cbca..a7ef45b2e6 100644 --- a/scripts/immers-configure.js +++ b/scripts/immers-configure.js @@ -20,10 +20,7 @@ const { host, token } = JSON.parse(readFileSync(".ret.credentials")); const cfg = { extra_csp: { // connect to home immer - connect_src: "https: wss:", - // allow Coil WebMon browser plugin - script_src: - "'sha256-W5yaJ6UM3/kOJa12aRVSLOEOKdAUYAWZPM1bUuaTJYQ=' 'sha256-XpyxuqRQmj1o8ovYZlIA71UXSYTvYdV8kOb55p+lrNo=' 'sha256-tie542PGbiDGOm9MefVIzDBZf4Nt5wTagAHT/BKEB94=' 'sha256-9QLzkf1LE5s0CtnpqUvwkWr7DV4GRfQLGt/tFNT19h0='" + connect_src: "https: wss:" }, security: { // fetch remote avatars @@ -35,7 +32,7 @@ const { host, token } = JSON.parse(readFileSync(".ret.credentials")); }, extra_html: {} }; - // add local immers server env variable and web monetizatoin payment pointer to all pages + // add local immers server env variable and web monetization payment pointer to all pages const extraHeader = ``; ["extra_avatar_html", "extra_index_html", "extra_room_html", "extra_scene_html"].forEach(setting => { cfg.extra_html[setting] = extraHeader; From f225f40c539db6c40a6b2cfa021c403e521f760b Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 25 Jun 2021 10:41:56 -0500 Subject: [PATCH 116/167] fix immers monetization sometimes never initializing --- src/utils/immers/monetization.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/utils/immers/monetization.js b/src/utils/immers/monetization.js index 0a606ff3e4..e5f4e24e92 100644 --- a/src/utils/immers/monetization.js +++ b/src/utils/immers/monetization.js @@ -74,6 +74,12 @@ export function setupMonetization(scene, player, remountUI) { if (hubScene.is("loaded")) { onSceneLoaded(); } else { - hubScene.addEventListener("environment-scene-loaded", onSceneLoaded, { once: true }); + const sceneStateListener = ({ detail }) => { + if (detail === "loaded") { + onSceneLoaded(); + hubScene.removeEventListener("stateadded", sceneStateListener); + } + }; + hubScene.addEventListener("stateadded", sceneStateListener); } } From 914441b6355ca8b10e7bd17a8523b9b31b638f72 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 25 Jun 2021 10:55:27 -0500 Subject: [PATCH 117/167] 1.3.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 91d5abc6e9..ce940c7e80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.2.1", + "version": "1.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 1b99fe7a9a..8b735c4a7e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.2.1", + "version": "1.3.0", "description": "Duck-themed multi-user virtual spaces in WebVR.", "main": "src/index.js", "license": "MPL-2.0", From f6dbb4e5a936e6da0e0ca497716f32540d0f4dde Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 25 Jun 2021 11:17:05 -0500 Subject: [PATCH 118/167] add version tag to docker deploy script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8b735c4a7e..217278bf27 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "deploy": "node -r @babel/register -r esm -r ./scripts/shim scripts/deploy.js", "immers-configure": "node -r @babel/register -r esm -r ./scripts/shim scripts/immers-configure.js", "immers-build:image": "docker build -t immersspace/hubs .", - "immers-publish:image": "docker push immersspace/hubs:latest", + "immers-publish:image": "docker tag immersspace/hubs:latest immersspace/hubs:v$npm_package_version && docker push immersspace/hubs:latest && docker push immersspace/hubs:v$npm_package_version", "undeploy": "node -r @babel/register -r esm -r ./scripts/shim scripts/undeploy.js", "test": "npm run lint && npm run test:unit && npm run build", "test:unit": "ava", From 13519d2f1db683dc3821f71c87573183b5bc62ab Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 14 May 2021 16:25:39 -0500 Subject: [PATCH 119/167] fix bug in touchscreen grab calculations --- .../devices/app-aware-touchscreen.js | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/systems/userinput/devices/app-aware-touchscreen.js b/src/systems/userinput/devices/app-aware-touchscreen.js index b1efee09a2..54c00000da 100644 --- a/src/systems/userinput/devices/app-aware-touchscreen.js +++ b/src/systems/userinput/devices/app-aware-touchscreen.js @@ -35,19 +35,14 @@ const getPlayerCamera = (() => { }; })(); -function shouldMoveCursor(touch, raycaster) { +function shouldMoveCursor(cursorPose, raycaster) { const isCursorGrabbing = !!AFRAME.scenes[0].systems.interaction.state.rightRemote.held; if (isCursorGrabbing) { return true; } const rawIntersections = []; - raycaster.setFromCamera( - { - x: (touch.clientX / window.innerWidth) * 2 - 1, - y: -(touch.clientY / window.innerHeight) * 2 + 1 - }, - getPlayerCamera() - ); + raycaster.ray.origin = cursorPose.position; + raycaster.ray.direction = cursorPose.direction; raycaster.intersectObjects( AFRAME.scenes[0].systems["hubs-systems"].cursorTargettingSystem.targets, true, @@ -212,9 +207,14 @@ export class AppAwareTouchscreenDevice { if (isFirstTouch || (isThirdTouch && hasSecondPinch)) { let assignment; + const cursorPose = new Pose().fromCameraProjection( + getPlayerCamera(), + (touch.clientX / this.canvas.clientWidth) * 2 - 1, + -(touch.clientY / this.canvas.clientHeight) * 2 + 1 + ); // First touch or third touch and other two fingers were pinching - if (shouldMoveCursor(touch, this.raycaster)) { + if (shouldMoveCursor(cursorPose, this.raycaster)) { assignment = assign(touch, MOVE_CURSOR_JOB, this.assignments); // Grabbing objects is delayed by several frames: @@ -231,11 +231,7 @@ export class AppAwareTouchscreenDevice { // On touch down, we always move the cursor on the first frame, but the MOVE_CURSOR_JOB indicates // the touch will then track the cursor (instead of the camera) - assignment.cursorPose = new Pose().fromCameraProjection( - getPlayerCamera(), - (touch.clientX / this.canvas.clientWidth) * 2 - 1, - -(touch.clientY / this.canvas.clientHeight) * 2 + 1 - ); + assignment.cursorPose = cursorPose; } else if (isSecondTouch || isThirdTouch) { const cursorJob = findByJob(MOVE_CURSOR_JOB, this.assignments); From 4a483aed34a8db11315cdf245bb0334eeb0c60ab Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 24 Sep 2021 12:04:26 -0500 Subject: [PATCH 120/167] update buttons on players to reflect whether the remote player is logged into immers --- src/components/block-button.js | 13 ++++++++++++- src/components/immers/immers-follow-button.js | 7 +++++++ src/hub.html | 6 ++++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/components/block-button.js b/src/components/block-button.js index 23c7391086..6a9b8b0ddb 100644 --- a/src/components/block-button.js +++ b/src/components/block-button.js @@ -11,14 +11,19 @@ AFRAME.registerComponent("block-button", { this.el.emit("immers-block", { clientId: this.owner }); }; this.onScopeChange = () => { - if (this.el.sceneEl.states.includes("immers-scope-addBlocks")) { + if ( + this.playerEl?.getAttribute("player-info").immersId && + this.el.sceneEl.states.includes("immers-scope-addBlocks") + ) { this.textEl.setAttribute("text", "value", "Block"); } else { this.textEl.setAttribute("text", "value", "Hide"); } }; NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { + this.playerEl = networkedEl; this.owner = networkedEl.components.networked.data.owner; + this.playerEl.addEventListener("immers-id-changed", this.onScopeChange); }); this.onScopeChange(); }, @@ -27,11 +32,17 @@ AFRAME.registerComponent("block-button", { this.el.object3D.addEventListener("interact", this.onClick); this.el.sceneEl.addEventListener("stateadded", this.onScopeChange); this.el.sceneEl.addEventListener("stateremoved", this.onScopeChange); + if (this.playerEl) { + this.playerEl.addEventListener("immers-id-changed", this.onScopeChange); + } }, pause() { this.el.object3D.removeEventListener("interact", this.onClick); this.el.sceneEl.removeEventListener("stateremoved", this.onScopeChange); + if (this.playerEl) { + this.playerEl.removeEventListener("immers-id-changed", this.onScopeChange); + } }, block(clientId) { diff --git a/src/components/immers/immers-follow-button.js b/src/components/immers/immers-follow-button.js index ad47ac6641..5f533bed57 100644 --- a/src/components/immers/immers-follow-button.js +++ b/src/components/immers/immers-follow-button.js @@ -6,6 +6,9 @@ AFRAME.registerComponent("immers-follow-button", { schema: { relation: { type: "string", default: "none", oneOf: ["none", "request", "friend", "pending"] } }, init() { + this.showIfLoggedIn = () => { + this.el.object3D.visible = !!this.playerEl?.getAttribute("player-info").immersId; + }; NAF.utils.getNetworkedEntity(this.el).then(networkedEl => { this.playerEl = networkedEl; this.playerEl.addEventListener("stateadded", this.onState); @@ -14,6 +17,8 @@ AFRAME.registerComponent("immers-follow-button", { } else if (this.playerEl.is("immers-follow-request")) { this.el.setAttribute("immers-follow-button", { relation: "request" }); } + this.showIfLoggedIn(); + this.playerEl.addEventListener("immers-id-changed", this.showIfLoggedIn); }); this.textEl = this.el.querySelector("[text]"); // avoid accidental double clicks @@ -48,6 +53,7 @@ AFRAME.registerComponent("immers-follow-button", { this.el.object3D.addEventListener("interact", this.onClick); if (this.playerEl) { this.playerEl.addEventListener("stateadded", this.onState); + this.playerEl.addEventListener("immers-id-changed", this.showIfLoggedIn); } }, @@ -73,6 +79,7 @@ AFRAME.registerComponent("immers-follow-button", { this.el.object3D.removeEventListener("interact", this.onClick); if (this.playerEl) { this.playerEl.removeEventListener("stateadded", this.onState); + this.playerEl.removeEventListener("immers-id-changed", this.showIfLoggedIn); } }, diff --git a/src/hub.html b/src/hub.html index 6cfa6d316e..02844ef2e6 100644 --- a/src/hub.html +++ b/src/hub.html @@ -185,8 +185,10 @@ - - + + + + From 9538528090ef367f3d9af1d5c51c103b141f035d Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 24 Sep 2021 13:44:34 -0500 Subject: [PATCH 121/167] update room entry flow to allow joining without logging in, if feature enabled --- .defaults.env | 1 + src/react-components/room/RoomEntryModal.js | 63 +++++++++++++-------- src/react-components/ui-root.js | 9 ++- src/utils/configs.js | 1 + webpack.config.js | 1 + 5 files changed, 49 insertions(+), 26 deletions(-) diff --git a/.defaults.env b/.defaults.env index 1978f94e2c..fc3f779160 100644 --- a/.defaults.env +++ b/.defaults.env @@ -32,3 +32,4 @@ DEFAULT_SCENE_SID="JGLt8DP" IMMERS_SERVER="https://localhost:8081" IMMERS_SCOPE="modAdditive" +IMMERS_ALLOW_GUESTS="true" diff --git a/src/react-components/room/RoomEntryModal.js b/src/react-components/room/RoomEntryModal.js index 961bdfd3f6..da70d4ee77 100644 --- a/src/react-components/room/RoomEntryModal.js +++ b/src/react-components/room/RoomEntryModal.js @@ -21,7 +21,9 @@ export function RoomEntryModal({ className, roomName, showLoginToImmers, + showGuestEntry, onLoginToImmers, + onGuestEntry, showJoinRoom, onJoinRoom, showEnterOnDevice, @@ -52,7 +54,7 @@ export function RoomEntryModal({

    {roomName}

    - {showLoginToImmers && ( + {showLoginToImmers ? ( <> - -

    Login or create a free account to join this space

    + {showGuestEntry ? ( + + ) : ( + <> + +

    Login or create a free account to join this space

    + + )} + + ) : ( + <> + {showJoinRoom && ( + + )} + {showEnterOnDevice && ( + + )} - )} - {showJoinRoom && ( - - )} - {showEnterOnDevice && ( - )} {showSpectate && (
    + + + ); +} + +ImmersClaimAccountModal.propTypes = { + scene: PropTypes.object, + startImmersAuth: PropTypes.func, + onClose: PropTypes.func +}; diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index 35e4fbb8ff..fe5650b2cc 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -99,6 +99,7 @@ import { ImmersFeedToolbarButtonContainer, ImmersRegisterToolbarButtonContainer } from "./room/ImmersFeedSidebarContainer"; +import { ImmersClaimAccountModal } from "./room/ImmersReact"; const avatarEditorDebug = qsTruthy("avatarEditorDebug"); @@ -379,6 +380,7 @@ class UIRoot extends Component { this.playerRig = scene.querySelector("#avatar-rig"); scene.addEventListener("action_media_tweet", this.onTweet); + scene.addEventListener("action_immers_register", this.onImmersRegister); } UNSAFE_componentWillMount() { @@ -392,6 +394,7 @@ class UIRoot extends Component { this.props.scene.removeEventListener("share_video_disabled", this.onShareVideoDisabled); this.props.scene.removeEventListener("share_video_failed", this.onShareVideoFailed); this.props.scene.removeEventListener("action_media_tweet", this.onTweet); + this.props.scene.removeEventListener("action_immers_register", this.onImmersRegister); this.props.store.removeEventListener("statechanged", this.storeUpdated); window.removeEventListener("concurrentload", this.onConcurrentLoad); window.removeEventListener("idle_detected", this.onIdleDetected); @@ -747,6 +750,19 @@ class UIRoot extends Component { }); }; + showImmersRegister = () => { + this.showNonHistoriedDialog(ImmersClaimAccountModal, { + startImmersAuth: this.props.startImmersAuth, + scene: this.props.scene + }); + }; + + onImmersRegister = () => { + handleExitTo2DInterstitial(true, () => {}).then(() => { + this.showImmersRegister(); + }); + }; + onChangeScene = () => { this.props.performConditionalSignIn( () => this.props.hubChannel.can("update_hub"), @@ -1593,7 +1609,7 @@ class UIRoot extends Component { {this.props.isImmersConnected ? ( this.toggleSidebar("feed")} /> ) : ( - + )} {entered && isMobileVR && ( diff --git a/src/systems/exit-on-blur.js b/src/systems/exit-on-blur.js index 15a37be4c5..70b2513367 100644 --- a/src/systems/exit-on-blur.js +++ b/src/systems/exit-on-blur.js @@ -29,6 +29,7 @@ AFRAME.registerSystem("exit-on-blur", { if ( this.isOculusBrowser && this.enteredVR && + !this.el.is("immers-authorizing") && (this.lastTimeoutCheck === 0 || t - this.lastTimeoutCheck >= 1000.0) // Don't do this clear every frame, slow. ) { this.lastTimeoutCheck = t; diff --git a/src/utils/immers.js b/src/utils/immers.js index 7e21ad52ba..94517125dd 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -590,6 +590,7 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat .catch(err => console.error(`Error sharing chat: ${err.message}`)); }); const immersReAuth = scope => resetAuth(store, remountUI, scope); + hubScene.addState("immers-connected"); remountUI({ immersMessageDispatch, immersScopes: authorizedScopes, isImmersConnected: true, immersReAuth }); } } From f2fd2efe65908a43042f00e6fd5e059159e1b1a9 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 25 Sep 2021 23:49:08 -0500 Subject: [PATCH 128/167] fix detection of when auth popup closes, avoiding cross-origin error --- src/utils/immers.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 94517125dd..fe327ddd7d 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -306,8 +306,13 @@ export async function auth(store, scope) { if (!popup) { alert("Could not open login window. Please check if popup was blocked and allow it"); } else { - hubScene?.addState("immers-authorizing"); - popup.onunload = () => hubScene?.removeState("immers-authorizing"); + hubScene.addState("immers-authorizing"); + const closedCheckInterval = window.setInterval(() => { + if (popup.closed) { + window.clearInterval(closedCheckInterval); + hubScene.removeState("immers-authorizing"); + } + }, 100); } } }; From 8207e5fcd20ace03f74ae033b61a5d856269c5b3 Mon Sep 17 00:00:00 2001 From: Takahiro Date: Thu, 23 Sep 2021 12:38:26 -0700 Subject: [PATCH 129/167] Bump A-Frame for safari15 fixes --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index ce940c7e80..8ea08ba1d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15840,7 +15840,7 @@ "dev": true }, "aframe": { - "version": "github:mozillareality/aframe#6fdea0cdaae21eb6ad779c089eb005011191c29a", + "version": "github:mozillareality/aframe#1f77f3d9328f00faff76e94dcc190d4278ddb3c6", "from": "github:mozillareality/aframe#hubs/master", "requires": { "custom-event-polyfill": "^1.0.6", From 8337011fdd697d32dd96fb2cf0e686b3be55e663 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Wed, 29 Sep 2021 22:17:11 -0500 Subject: [PATCH 130/167] bring back spectate, but only when joining is impossible --- src/react-components/ui-root.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js index fe5650b2cc..24dfcaa0d3 100644 --- a/src/react-components/ui-root.js +++ b/src/react-components/ui-root.js @@ -824,7 +824,9 @@ class UIRoot extends Component { const showLogin = !this.props.isImmersConnected && !this.state.guestEntry; const showGuestEntry = configs.IMMERS_ALLOW_GUESTS !== "false"; // monetized users can bypass room limit - const canEnter = !this.props.entryDisallowed || this.props.isMonetized; + const canEnter = !this.props.entryDisallowed || !!this.props.isMonetized; + // only show when joining is not possible to reduce number of choices shown + const canSpectate = !showLogin && !canEnter; // TODO: What does onEnteringCanceled do? return ( <> @@ -854,7 +856,7 @@ class UIRoot extends Component { }} showEnterOnDevice={!this.state.waitingOnAudio && canEnter && !isMobileVR} onEnterOnDevice={() => this.attemptLink()} - showSpectate={false} + showSpectate={!this.state.waitingOnAudio && canSpectate} onSpectate={() => this.setState({ watching: true })} showOptions={this.props.hubChannel.canOrWillIfCreator("update_hub")} onOptions={() => { From 5812ba55938f4482f2db922bc0d60a3727be42c5 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 1 Oct 2021 10:52:14 -0500 Subject: [PATCH 131/167] current hubs-cloud version of A-Frame patched for Safari 15 crashes, text display --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5e3a03a5a7..1912d5aa65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15795,8 +15795,8 @@ "dev": true }, "aframe": { - "version": "github:mozillareality/aframe#6fdea0cdaae21eb6ad779c089eb005011191c29a", - "from": "github:mozillareality/aframe#hubs/master", + "version": "github:immers-space/aframe#70412c4b4c9ee3bc60ef1cfb20efdd012572a045", + "from": "github:immers-space/aframe#hubs-cloud-safari-15-patches", "requires": { "custom-event-polyfill": "^1.0.6", "debug": "github:ngokevin/debug#noTimestamp", diff --git a/package.json b/package.json index 3296fd7d30..d8d41f7427 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@mozillareality/easing-functions": "^0.1.1", "@mozillareality/three-batch-manager": "github:mozillareality/three-batch-manager#master", "@popperjs/core": "^2.4.4", - "aframe": "github:mozillareality/aframe#hubs/master", + "aframe": "github:immers-space/aframe#hubs-cloud-safari-15-patches", "aframe-rounded": "^1.0.3", "aframe-slice9-component": "^1.0.0", "ammo-debug-drawer": "github:infinitelee/ammo-debug-drawer", From 5986033d9c497da66088a09a488d9b11e2e54432 Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Tue, 7 Sep 2021 17:10:17 -0400 Subject: [PATCH 132/167] Tweak media query and avatar settings heights to avoid cutoff and scrolling --- src/react-components/input/InputField.scss | 4 ++++ src/react-components/room/AvatarSettingsContent.scss | 6 +++--- src/react-components/styles/theme.scss | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/react-components/input/InputField.scss b/src/react-components/input/InputField.scss index d63d83072c..c287a8a940 100644 --- a/src/react-components/input/InputField.scss +++ b/src/react-components/input/InputField.scss @@ -28,3 +28,7 @@ color: theme.$text3-color; align-self: flex-start; } + +:local(.info), :local(.error) { + font-size: 10px; +} diff --git a/src/react-components/room/AvatarSettingsContent.scss b/src/react-components/room/AvatarSettingsContent.scss index ab0c339fec..08d73c568d 100644 --- a/src/react-components/room/AvatarSettingsContent.scss +++ b/src/react-components/room/AvatarSettingsContent.scss @@ -2,7 +2,7 @@ :local(.content) { align-items: center; - padding: 24px; + padding: 10px; text-align: center; } @@ -16,8 +16,8 @@ /* TODO: This styling into AvatarPreview */ & > :first-child { width: 168px; - height: 300px; - min-height: 300px; + height: 250px; + min-height: 250px; border-radius: 8px; background-color: theme.$tile-bg-color; } diff --git a/src/react-components/styles/theme.scss b/src/react-components/styles/theme.scss index 2a0b9bb5b6..6786915f47 100644 --- a/src/react-components/styles/theme.scss +++ b/src/react-components/styles/theme.scss @@ -4,7 +4,7 @@ $breakpoint-md: 768px; // Tablets $breakpoint-lg: 992px; // Desktops $breakpoint-xl: 1200px; // Large Desktops $breakpoint-xxl: 1600px; // Extra Large Desktops -$breakpoint-vr: 500px; // Standalone VR Browsers +$breakpoint-vr: 600px; // Standalone VR Browsers $transparent: transparent; $transparent-hover: rgba(0, 0, 0, 0.08); From a378bf23701a04c20224b09d55e1200b40480169 Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Tue, 7 Sep 2021 17:23:14 -0400 Subject: [PATCH 133/167] Only reduce avatar settings padding on top edge --- src/react-components/room/AvatarSettingsContent.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/react-components/room/AvatarSettingsContent.scss b/src/react-components/room/AvatarSettingsContent.scss index 08d73c568d..60bdb42bca 100644 --- a/src/react-components/room/AvatarSettingsContent.scss +++ b/src/react-components/room/AvatarSettingsContent.scss @@ -2,7 +2,8 @@ :local(.content) { align-items: center; - padding: 10px; + padding: 24px; + padding-top: 10px; text-align: center; } From 1bd87f9252c89cd35ad790b85ae619e86b1bb459 Mon Sep 17 00:00:00 2001 From: Brian Peiris Date: Tue, 7 Sep 2021 20:04:29 -0400 Subject: [PATCH 134/167] Remove redundant min-height and add a comment explaining AvatarPreview's behavior --- src/react-components/room/AvatarSettingsContent.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/react-components/room/AvatarSettingsContent.scss b/src/react-components/room/AvatarSettingsContent.scss index 60bdb42bca..f119857db7 100644 --- a/src/react-components/room/AvatarSettingsContent.scss +++ b/src/react-components/room/AvatarSettingsContent.scss @@ -16,9 +16,12 @@ /* TODO: This styling into AvatarPreview */ & > :first-child { + /* + We need to set dimensions explicitly here, since AvatarPreview + resizes itself to its container's dimensions. + */ width: 168px; height: 250px; - min-height: 250px; border-radius: 8px; background-color: theme.$tile-bg-color; } From 73125c2c3b7fc9ad892ef177d380540c084c4604 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Sat, 25 Sep 2021 23:07:23 -0500 Subject: [PATCH 135/167] do not fail to get media if last device no longer available --- src/utils/media-devices-manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/media-devices-manager.js b/src/utils/media-devices-manager.js index 0858788df5..3a11d95a61 100644 --- a/src/utils/media-devices-manager.js +++ b/src/utils/media-devices-manager.js @@ -110,7 +110,7 @@ export default class MediaDevicesManager { async startMicShare(deviceId) { let constraints = { audio: {} }; if (deviceId) { - constraints = { audio: { deviceId: { exact: [deviceId] } } }; + constraints = { audio: { deviceId: { ideal: [deviceId] } } }; } const result = await this._startMicShare(constraints); From 176dd925a063598a0564d363f775cf94af7fb174 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 3 Feb 2022 08:25:03 -0600 Subject: [PATCH 136/167] bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c960d07ab2..d4bdc66110 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.3.0", + "version": "1.4.0", "description": "Duck-themed multi-user virtual spaces in WebVR.", "main": "src/index.js", "license": "MPL-2.0", From 01140660226f9f2fb761e91647b79b775f9bbd0a Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 3 Feb 2022 08:25:06 -0600 Subject: [PATCH 137/167] 1.5.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index d88e572e2f..aa22b5fa7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.3.0", + "version": "1.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d4bdc66110..e10a1bc339 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.4.0", + "version": "1.5.0", "description": "Duck-themed multi-user virtual spaces in WebVR.", "main": "src/index.js", "license": "MPL-2.0", From 0c417783352a4d4482b28a6106bcacd737a234a0 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 3 Feb 2022 08:28:48 -0600 Subject: [PATCH 138/167] 1.6.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index aa22b5fa7d..19f370ee2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.5.0", + "version": "1.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e10a1bc339..828ccda793 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.5.0", + "version": "1.6.0", "description": "Duck-themed multi-user virtual spaces in WebVR.", "main": "src/index.js", "license": "MPL-2.0", From e0c0fc7d57586e7edaeac23e1524367940865c97 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Thu, 3 Feb 2022 11:18:35 -0600 Subject: [PATCH 139/167] fix indeterminate node_modules in docker --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 447edaed4a..7319276401 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,12 +2,14 @@ FROM node:14 WORKDIR /usr/src/hubs COPY package*.json ./ -RUN npm ci WORKDIR /usr/src/hubs/admin COPY admin/package*.json ./ RUN npm ci +WORKDIR /usr/src/hubs +RUN npm ci + WORKDIR /usr/src/hubs COPY . . From b6fb25dde73361bcb14b3723a007062e6079b138 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 11 Feb 2022 17:43:34 -0600 Subject: [PATCH 140/167] resync aframe version with upstream --- package-lock.json | 1557 ++++++++++++++++----------------------------- package.json | 2 +- 2 files changed, 547 insertions(+), 1012 deletions(-) diff --git a/package-lock.json b/package-lock.json index 026ab21750..f22a6b035a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5602,15 +5602,6 @@ "typescript": "^4.0" }, "dependencies": { - "@formatjs/ecma402-abstract": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.5.0.tgz", - "integrity": "sha512-wXv36yo+mfWllweN0Fq7sUs7PUiNopn7I0JpLTe3hGu6ZMR4CV7LqK1llhB18pndwpKoafQKb1et2DCJAOW20Q==", - "dev": true, - "requires": { - "tslib": "^2.0.1" - } - }, "@nodelib/fs.stat": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", @@ -5710,16 +5701,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "intl-messageformat-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-6.1.2.tgz", - "integrity": "sha512-4GQDEPhl/ZMNDKwMsLqyw1LG2IAWjmLJXdmnRcHKeLQzpgtNYZI6lVw1279pqIkRk2MfKb9aDsVFzm565azK5A==", - "dev": true, - "requires": { - "@formatjs/ecma402-abstract": "1.5.0", - "tslib": "^2.0.1" - } - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -5965,25 +5946,6 @@ "typescript": "^4.0" }, "dependencies": { - "@formatjs/ecma402-abstract": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.5.0.tgz", - "integrity": "sha512-wXv36yo+mfWllweN0Fq7sUs7PUiNopn7I0JpLTe3hGu6ZMR4CV7LqK1llhB18pndwpKoafQKb1et2DCJAOW20Q==", - "dev": true, - "requires": { - "tslib": "^2.0.1" - } - }, - "intl-messageformat-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-6.1.2.tgz", - "integrity": "sha512-4GQDEPhl/ZMNDKwMsLqyw1LG2IAWjmLJXdmnRcHKeLQzpgtNYZI6lVw1279pqIkRk2MfKb9aDsVFzm565azK5A==", - "dev": true, - "requires": { - "@formatjs/ecma402-abstract": "1.5.0", - "tslib": "^2.0.1" - } - }, "tslib": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", @@ -6189,10 +6151,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "debug": { - "version": "github:ngokevin/debug#ef5f8e66d49ce8bc64c6f282c15f8b7164409e3a", - "from": "github:ngokevin/debug#noTimestamp" - }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -6203,9 +6161,9 @@ } }, "graceful-fs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", "dev": true }, "has-flag": { @@ -6417,12 +6375,12 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, "requires": { - "@babel/highlight": "^7.12.13" + "@babel/highlight": "^7.10.4" } }, "@babel/core": { @@ -6450,113 +6408,114 @@ } }, "@babel/generator": { - "version": "7.13.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.9.tgz", - "integrity": "sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", + "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", "dev": true, "requires": { - "@babel/types": "^7.13.0", + "@babel/types": "^7.12.11", "jsesc": "^2.5.1", "source-map": "^0.5.0" } }, "@babel/helper-function-name": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", - "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", + "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/helper-get-function-arity": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/types": "^7.12.11" } }, "@babel/helper-get-function-arity": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", - "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", + "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.12.10" } }, "@babel/helper-member-expression-to-functions": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", - "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", "dev": true, "requires": { - "@babel/types": "^7.13.12" + "@babel/types": "^7.12.7" } }, "@babel/helper-module-imports": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", - "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", + "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", "dev": true, "requires": { - "@babel/types": "^7.13.12" + "@babel/types": "^7.12.5" } }, "@babel/helper-module-transforms": { - "version": "7.13.14", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.13.14.tgz", - "integrity": "sha512-QuU/OJ0iAOSIatyVZmfqB0lbkVP0kDRiKj34xy+QNsnVZi/PA6BoSoreeqnxxa9EHFAIL0R9XOaAR/G9WlIy5g==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz", + "integrity": "sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.13.12", - "@babel/helper-replace-supers": "^7.13.12", - "@babel/helper-simple-access": "^7.13.12", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/helper-validator-identifier": "^7.12.11", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.13.13", - "@babel/types": "^7.13.14" + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-simple-access": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/helper-validator-identifier": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.1", + "@babel/types": "^7.12.1", + "lodash": "^4.17.19" } }, "@babel/helper-optimise-call-expression": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", - "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", + "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.12.10" } }, "@babel/helper-plugin-utils": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", - "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", "dev": true }, "@babel/helper-replace-supers": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz", - "integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz", + "integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==", "dev": true, "requires": { - "@babel/helper-member-expression-to-functions": "^7.13.12", - "@babel/helper-optimise-call-expression": "^7.12.13", - "@babel/traverse": "^7.13.0", - "@babel/types": "^7.13.12" + "@babel/helper-member-expression-to-functions": "^7.12.7", + "@babel/helper-optimise-call-expression": "^7.12.10", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.11" } }, "@babel/helper-simple-access": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz", - "integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz", + "integrity": "sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==", "dev": true, "requires": { - "@babel/types": "^7.13.12" + "@babel/types": "^7.12.1" } }, "@babel/helper-split-export-declaration": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", - "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", + "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.12.11" } }, "@babel/helper-validator-identifier": { @@ -6566,31 +6525,31 @@ "dev": true }, "@babel/helpers": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.13.10.tgz", - "integrity": "sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz", + "integrity": "sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==", "dev": true, "requires": { - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.13.0", - "@babel/types": "^7.13.0" + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.5", + "@babel/types": "^7.12.5" } }, "@babel/highlight": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", - "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.12.11", + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.13.13", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.13.tgz", - "integrity": "sha512-OhsyMrqygfk5v8HmWwOzlYjJrtLaFhF34MrfG/Z73DgYCI6ojNUTUp2TYbtnjo8PegeJp12eamsNettCQjKjVw==", + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", "dev": true }, "@babel/plugin-syntax-jsx": { @@ -6612,36 +6571,37 @@ } }, "@babel/template": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", - "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", "dev": true, "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" } }, "@babel/traverse": { - "version": "7.13.13", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.13.tgz", - "integrity": "sha512-CblEcwmXKR6eP43oQGG++0QMTtCjAsa3frUuzHoiIJWpaIIi8dwMyEFUJoXRLxagGqCK+jALRwIO+o3R9p/uUg==", + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.12.tgz", + "integrity": "sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.13.9", - "@babel/helper-function-name": "^7.12.13", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.13.13", - "@babel/types": "^7.13.13", + "@babel/code-frame": "^7.12.11", + "@babel/generator": "^7.12.11", + "@babel/helper-function-name": "^7.12.11", + "@babel/helper-split-export-declaration": "^7.12.11", + "@babel/parser": "^7.12.11", + "@babel/types": "^7.12.12", "debug": "^4.1.0", - "globals": "^11.1.0" + "globals": "^11.1.0", + "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.13.14", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", - "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.12.11", @@ -6686,9 +6646,9 @@ "dev": true }, "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", "dev": true, "requires": { "minimist": "^1.2.5" @@ -6771,9 +6731,9 @@ } }, "unist-util-is": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", - "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.0.4.tgz", + "integrity": "sha512-3dF39j/u423v4BBQrk1AQ2Ve1FxY5W3JKwXxVFzBODQ6WEvccguhgp802qQLKSnxPODE6WuRZtV+ohlUg4meBA==", "dev": true }, "unist-util-remove-position": { @@ -7011,6 +6971,16 @@ "react-lifecycles-compat": "^3.0.4" } }, + "@socket.io/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==" + }, + "@socket.io/component-emitter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.0.0.tgz", + "integrity": "sha512-2pTGuibAXJswAPJjaKisthqS/NOK5ypG4LYT6tEAV0S/mxW0zOIvYvGK0V8w8+SHxAm6vRMSjqSalFXeBAqs+Q==" + }, "@storybook/addon-actions": { "version": "6.1.15", "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.1.15.tgz", @@ -14996,11 +14966,6 @@ "integrity": "sha512-TbH79tcyi9FHwbyboOKeRachRq63mSuWYXOflsNO9ZyE5ClQ/JaozNKl+aWUq87qPNsXasXxi2AbgfwIJ+8GQw==", "dev": true }, - "@types/component-emitter": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", - "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==" - }, "@types/debug": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", @@ -15840,8 +15805,8 @@ "dev": true }, "aframe": { - "version": "github:immers-space/aframe#70412c4b4c9ee3bc60ef1cfb20efdd012572a045", - "from": "github:immers-space/aframe#hubs-cloud-safari-15-patches", + "version": "github:mozillareality/aframe#1f77f3d9328f00faff76e94dcc190d4278ddb3c6", + "from": "github:mozillareality/aframe#hubs/master", "requires": { "custom-event-polyfill": "^1.0.6", "debug": "github:ngokevin/debug#noTimestamp", @@ -15988,6 +15953,7 @@ "version": "3.2.1", "resolved": "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", + "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -17631,16 +17597,6 @@ "path-exists": "^3.0.0" } }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -18685,15 +18641,6 @@ "to-fast-properties": "^2.0.0" } }, - "@formatjs/ecma402-abstract": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.5.0.tgz", - "integrity": "sha512-wXv36yo+mfWllweN0Fq7sUs7PUiNopn7I0JpLTe3hGu6ZMR4CV7LqK1llhB18pndwpKoafQKb1et2DCJAOW20Q==", - "dev": true, - "requires": { - "tslib": "^2.0.1" - } - }, "@types/json-schema": { "version": "7.0.7", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", @@ -18742,16 +18689,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "intl-messageformat-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-6.1.2.tgz", - "integrity": "sha512-4GQDEPhl/ZMNDKwMsLqyw1LG2IAWjmLJXdmnRcHKeLQzpgtNYZI6lVw1279pqIkRk2MfKb9aDsVFzm565azK5A==", - "dev": true, - "requires": { - "@formatjs/ecma402-abstract": "1.5.0", - "tslib": "^2.0.1" - } - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -19159,10 +19096,11 @@ "widest-line": "^2.0.0" }, "dependencies": { - "get-stdin": { - "version": "5.0.1", - "resolved": "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz", - "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=" + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true } } }, @@ -19384,8 +19322,14 @@ "dev": true }, "buffered-interpolation": { - "version": "0.2.5", - "resolved": "github:Infinitelee/buffered-interpolation#5bb18421ebf2bf11664645cdc7a15bd77ee2156b" + "version": "github:Infinitelee/buffered-interpolation#5bb18421ebf2bf11664645cdc7a15bd77ee2156b", + "from": "github:Infinitelee/buffered-interpolation" + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true }, "builtin-status-codes": { "version": "3.0.0", @@ -19449,6 +19393,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -21823,6 +21768,7 @@ "version": "1.4.1", "resolved": "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz", "integrity": "sha1-7SljTRm6ukY7bOa4CjchPqtx7EM=", + "dev": true, "requires": { "once": "^1.4.0" } @@ -21838,6 +21784,50 @@ "objectorarray": "^1.0.4" } }, + "engine.io-client": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.1.1.tgz", + "integrity": "sha512-V05mmDo4gjimYW+FGujoGmmmxRaDsrVr7AXA3ZIfa04MWM1jOfZfUwou0oNqhNwy/votUDvGDt4JA4QF4e0b4g==", + "requires": { + "@socket.io/component-emitter": "~3.0.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.0", + "has-cors": "1.1.0", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~8.2.3", + "xmlhttprequest-ssl": "~2.0.0", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==" + } + } + }, + "engine.io-parser": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", + "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", + "requires": { + "@socket.io/base64-arraybuffer": "~1.0.2" + } + }, "enhanced-resolve": { "version": "4.1.0", "resolved": "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", @@ -21847,21 +21837,6 @@ "graceful-fs": "^4.1.2", "memory-fs": "^0.4.0", "tapable": "^1.0.0" - }, - "dependencies": { - "@formatjs/ecma402-abstract": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.5.0.tgz", - "integrity": "sha512-wXv36yo+mfWllweN0Fq7sUs7PUiNopn7I0JpLTe3hGu6ZMR4CV7LqK1llhB18pndwpKoafQKb1et2DCJAOW20Q==", - "requires": { - "tslib": "^2.0.1" - } - }, - "tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" - } } }, "entities": { @@ -21946,25 +21921,14 @@ } }, "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.1.1", + "resolved": "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz", + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", "dev": true, "requires": { - "is-callable": "^1.1.4", + "is-callable": "^1.1.1", "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "dependencies": { - "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.1" - } - } + "is-symbol": "^1.0.1" } }, "es5-ext": { @@ -22068,7 +22032,8 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true }, "escodegen": { "version": "1.12.0", @@ -22237,12 +22202,20 @@ } }, "eslint-config-prettier": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-2.10.0.tgz", - "integrity": "sha512-Mhl90VLucfBuhmcWBgbUNtgBiK955iCDK1+aHAz7QfDQF6wuzWZ6JjihZ3ejJoGlJWIuko7xLqNm8BA5uenKhA==", + "version": "2.9.0", + "resolved": "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-2.9.0.tgz", + "integrity": "sha1-Xs1lF01IbCLf84n+A2/r9QLUaKM=", "dev": true, "requires": { "get-stdin": "^5.0.1" + }, + "dependencies": { + "get-stdin": { + "version": "5.0.1", + "resolved": "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz", + "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=", + "dev": true + } } }, "eslint-plugin-prettier": { @@ -22977,13 +22950,13 @@ } }, "flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "version": "1.0.3", + "resolved": "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz", + "integrity": "sha1-xdWG7zivYJdlC0m8QbVfq7GfNb0=", "dev": true, "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" + "inherits": "^2.0.1", + "readable-stream": "^2.0.4" } }, "follow-redirects": { @@ -23196,7 +23169,8 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=" + "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=", + "dev": true }, "function.prototype.name": { "version": "1.1.3", @@ -23387,6 +23361,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz", "integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==", + "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -23405,12 +23380,6 @@ "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==", "dev": true }, - "get-stdin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", - "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=", - "dev": true - }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz", @@ -23786,6 +23755,7 @@ "version": "1.0.3", "resolved": "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz", "integrity": "sha1-ci18v8H2qoJB8W3YFOAR4fQeh5Y=", + "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -23799,12 +23769,6 @@ "ansi-regex": "^2.0.0" } }, - "has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", - "dev": true - }, "has-cors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", @@ -23813,7 +23777,8 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true }, "has-glob": { "version": "1.0.0", @@ -23844,7 +23809,8 @@ "has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true }, "has-unicode": { "version": "2.0.1", @@ -24998,33 +24964,103 @@ } }, "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz", + "integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==", "dev": true, "requires": { - "get-intrinsic": "^1.1.0", + "es-abstract": "^1.17.0-next.1", "has": "^1.0.3", - "side-channel": "^1.0.4" + "side-channel": "^1.0.2" }, "dependencies": { - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", "dev": true, "requires": { + "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { "has-symbols": "^1.0.1" } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } } } }, "interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "version": "1.1.0", + "resolved": "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", "dev": true }, "intl-messageformat": { @@ -25037,6 +25073,15 @@ "tslib": "^2.0.1" }, "dependencies": { + "intl-messageformat-parser": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-6.1.3.tgz", + "integrity": "sha512-rQTtrVTFy/Z6Lg0ieHkkhdFfi/47BKv1P9+wMWlKWaAxpdDP0FIsp2LRyLPpIVKTwUfL3xf26QT25d69cSkZgQ==", + "requires": { + "@formatjs/ecma402-abstract": "1.5.1", + "tslib": "^2.0.1" + } + }, "tslib": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", @@ -25045,18 +25090,29 @@ } }, "intl-messageformat-parser": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-6.1.3.tgz", - "integrity": "sha512-rQTtrVTFy/Z6Lg0ieHkkhdFfi/47BKv1P9+wMWlKWaAxpdDP0FIsp2LRyLPpIVKTwUfL3xf26QT25d69cSkZgQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-6.1.2.tgz", + "integrity": "sha512-4GQDEPhl/ZMNDKwMsLqyw1LG2IAWjmLJXdmnRcHKeLQzpgtNYZI6lVw1279pqIkRk2MfKb9aDsVFzm565azK5A==", + "dev": true, "requires": { - "@formatjs/ecma402-abstract": "1.5.1", + "@formatjs/ecma402-abstract": "1.5.0", "tslib": "^2.0.1" }, "dependencies": { + "@formatjs/ecma402-abstract": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.5.0.tgz", + "integrity": "sha512-wXv36yo+mfWllweN0Fq7sUs7PUiNopn7I0JpLTe3hGu6ZMR4CV7LqK1llhB18pndwpKoafQKb1et2DCJAOW20Q==", + "dev": true, + "requires": { + "tslib": "^2.0.1" + } + }, "tslib": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "dev": true } } }, @@ -25068,6 +25124,12 @@ "loose-envify": "^1.0.0" } }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, "ip": { "version": "1.1.5", "resolved": "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz", @@ -25152,12 +25214,6 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, - "is-bigint": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz", - "integrity": "sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==", - "dev": true - }, "is-binary-path": { "version": "1.0.1", "resolved": "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz", @@ -25167,20 +25223,20 @@ "binary-extensions": "^1.0.0" } }, - "is-boolean-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz", - "integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==", - "dev": true, - "requires": { - "call-bind": "^1.0.0" - } - }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "^1.0.0" + } + }, "is-callable": { "version": "1.1.4", "resolved": "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz", @@ -25225,7 +25281,8 @@ "is-date-object": { "version": "1.0.1", "resolved": "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true }, "is-decimal": { "version": "1.0.2", @@ -25366,7 +25423,8 @@ "is-negative-zero": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==" + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true }, "is-npm": { "version": "1.0.0", @@ -25383,12 +25441,6 @@ "kind-of": "^3.0.2" } }, - "is-number-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", - "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==", - "dev": true - }, "is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", @@ -25417,98 +25469,11 @@ }, "is-path-in-cwd": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "resolved": "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha1-WsSLNF72dTOb1sekipEhELJBz1I=", "dev": true, "requires": { "is-path-inside": "^1.0.0" - }, - "dependencies": { - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "requires": { - "object-keys": "^1.0.12" - } - }, - "es-abstract": { - "version": "1.18.0-next.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz", - "integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==", - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.1", - "object-inspect": "^1.9.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.3", - "string.prototype.trimstart": "^1.0.3" - }, - "dependencies": { - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - } - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "is-callable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", - "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" - }, - "is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "requires": { - "has-symbols": "^1.0.1" - } - }, - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - }, - "dependencies": { - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - } - } - } } }, "is-path-inside": { @@ -25528,8 +25493,8 @@ }, "is-plain-object": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "resolved": "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", "dev": true, "requires": { "isobject": "^3.0.1" @@ -25606,6 +25571,12 @@ "integrity": "sha512-3vcJecUUrpgCqc/ca0aWeNu64UGgxcvO60K/Fkr1N6RSvfGCTU60UKN68JDmKokgba0rFFJs12EnzOQa14ubKQ==", "dev": true }, + "is-symbol": { + "version": "1.0.1", + "resolved": "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz", + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", + "dev": true + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -25661,7 +25632,8 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true }, "isobject": { "version": "3.0.1", @@ -26311,7 +26283,8 @@ "js-tokens": { "version": "3.0.2", "resolved": "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true }, "js-yaml": { "version": "3.13.1", @@ -26616,6 +26589,15 @@ } } }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, "leven": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", @@ -26636,11 +26618,6 @@ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" } } }, @@ -26655,7 +26632,7 @@ } }, "lib-hubs": { - "version": "github:mozillareality/lib-hubs#433df560dd14d476ce0a7ec84d86e40f18ba072c", + "version": "github:mozillareality/lib-hubs#592695f0fd098adbf7cc88d14391bd97a482f78a", "from": "github:mozillareality/lib-hubs#master" }, "lie": { @@ -26681,9 +26658,9 @@ } }, "linkifyjs": { - "version": "3.0.0-beta.3", - "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-3.0.0-beta.3.tgz", - "integrity": "sha512-aXq4WJs91NsETo5f9dQrt8Vx+OxAvzJAtR8lLgpum8PDjtCgstycwYbIkAjDGRV/YF1LlKKdbWyOpgMYgwgOvQ==" + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-3.0.5.tgz", + "integrity": "sha512-1Y9XQH65eQKA9p2xtk+zxvnTeQBG7rdAXSkUG97DmuI/Xhji9uaUzaWxRj6rf9YC0v8KKHkxav7tnLX82Sz5Fg==" }, "load-json-file": { "version": "4.0.0", @@ -26848,33 +26825,40 @@ } }, "loglevel": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", - "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==", + "version": "1.6.1", + "resolved": "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz", + "integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=", "dev": true }, "longest-streak": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", - "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.2.tgz", + "integrity": "sha512-TmYTeEYxiAmSVdpbnQDXGtvYOIRsCMg89CVZzwzc2o7GFL1CjoiRPjH5ec0NFAVlAx3fVof9dX/t6KKRAo2OWA==", "dev": true }, "loose-envify": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "resolved": "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha1-ce5R+nvkyuwaY4OffmgtgTLTDK8=", "requires": { "js-tokens": "^3.0.0 || ^4.0.0" + }, + "dependencies": { + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha1-GSA/tZmR35jjoocFDUZHzerzJJk=" + } } }, "loud-rejection": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-2.2.0.tgz", - "integrity": "sha512-S0FayMXku80toa5sZ6Ro4C+s+EtFDCsyJNG/AzFMfX3AxD5Si4dZsgzm/kKnbOxHl5Cv8jBlno8+3XYIh2pNjQ==", + "version": "1.6.0", + "resolved": "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", "dev": true, "requires": { "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.2" + "signal-exit": "^3.0.0" } }, "lower-case": { @@ -27323,18 +27307,6 @@ "read-pkg-up": "^3.0.0", "redent": "^2.0.0", "trim-newlines": "^2.0.0" - }, - "dependencies": { - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - } } }, "merge-class-names": { @@ -27546,8 +27518,8 @@ }, "minimalistic-assert": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "resolved": "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha1-LhlN4ERibUoQ5/f7wAznPoPk1cc=", "dev": true }, "minimalistic-crypto-utils": { @@ -27749,51 +27721,6 @@ "array-union": "^1.0.2", "arrify": "^1.0.1", "minimatch": "^3.0.4" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", - "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "requires": { - "has-flag": "^3.0.0" - } - } } }, "mustache": { @@ -27816,9 +27743,9 @@ "optional": true }, "nanoid": { - "version": "3.1.22", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz", - "integrity": "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", + "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==" }, "nanomatch": { "version": "1.2.13", @@ -27921,6 +27848,13 @@ "requires": { "buffered-interpolation": "^0.2.5", "easyrtc": "1.1.0" + }, + "dependencies": { + "buffered-interpolation": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/buffered-interpolation/-/buffered-interpolation-0.2.5.tgz", + "integrity": "sha512-LauOWiVXZppapDpc8y2yzcf3/pKU1Exal/+H0EZeNVxttnWHqtyhEaJ2J4U5yia71FWqZvywnhx8xneh073xbA==" + } } }, "new-array": { @@ -28068,33 +28002,17 @@ } }, "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "version": "2.4.0", + "resolved": "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", "dev": true, "requires": { "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", + "is-builtin-module": "^1.0.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" }, "dependencies": { - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -28241,12 +28159,14 @@ "object-inspect": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", - "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "dev": true }, "object-keys": { "version": "1.0.12", "resolved": "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha1-CcU4VTd1dTEMymL1W7M0q/97PtI=" + "integrity": "sha1-CcU4VTd1dTEMymL1W7M0q/97PtI=", + "dev": true }, "object-visit": { "version": "1.0.1", @@ -28291,27 +28211,25 @@ } }, "es-abstract": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz", - "integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==", + "version": "1.18.0-next.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz", + "integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==", "dev": true, "requires": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", + "get-intrinsic": "^1.0.2", "has": "^1.0.3", - "has-symbols": "^1.0.2", - "is-callable": "^1.2.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.2", - "is-string": "^1.0.5", + "is-regex": "^1.1.1", "object-inspect": "^1.9.0", "object-keys": "^1.1.1", "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.0" + "string.prototype.trimend": "^1.0.3", + "string.prototype.trimstart": "^1.0.3" }, "dependencies": { "object-keys": { @@ -28322,36 +28240,38 @@ } } }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" } }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true - }, "is-callable": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", - "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", "dev": true }, "is-regex": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", - "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", "has-symbols": "^1.0.1" } }, @@ -28374,26 +28294,6 @@ "dev": true } } - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } } } }, @@ -28506,130 +28406,13 @@ } }, "object.getownpropertydescriptors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz", - "integrity": "sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ==", + "version": "2.0.3", + "resolved": "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2" - }, - "dependencies": { - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "es-abstract": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz", - "integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.2", - "is-string": "^1.0.5", - "object-inspect": "^1.9.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.0" - }, - "dependencies": { - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - } - } - }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true - }, - "is-callable": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", - "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", - "dev": true - }, - "is-regex": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", - "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-symbols": "^1.0.1" - } - }, - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - }, - "dependencies": { - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - } - } - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - } + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" } }, "object.pick": { @@ -29032,21 +28815,6 @@ "pump": "^3.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -29386,6 +29154,11 @@ "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", "dev": true }, + "parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" + }, "parserlib": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parserlib/-/parserlib-1.1.1.tgz", @@ -29393,6 +29166,11 @@ "dev": true, "optional": true }, + "parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" + }, "parseurl": { "version": "1.3.2", "resolved": "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz", @@ -29413,155 +29191,26 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", + "dev": true, "requires": { "tslib": "^1.10.0" } }, "no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", + "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", "dev": true, "requires": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - }, - "dependencies": { - "lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, - "tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", - "dev": true - } - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "requires": { - "find-up": "^3.0.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==" - } - } - }, - "readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "requires": { - "picomatch": "^2.2.1" - } - }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "requires": { - "signal-exit": "^3.0.2" - } - }, - "rxjs": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", - "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", - "requires": { - "tslib": "^1.9.0" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "requires": { - "strip-ansi": "^6.0.0" + "lower-case": "^2.0.1", + "tslib": "^1.10.0" } }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==" - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==" - }, "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "type-fest": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==" - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", + "dev": true } } }, @@ -29668,10 +29317,16 @@ "websocket": "^1.0.24" } }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true }, "pify": { "version": "3.0.0", @@ -30516,9 +30171,9 @@ } }, "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", + "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -30585,15 +30240,6 @@ "supports-color": "^6.1.0" } }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -31804,9 +31450,9 @@ "dev": true }, "node-releases": { - "version": "1.1.71", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz", - "integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==", + "version": "1.1.70", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.70.tgz", + "integrity": "sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw==", "dev": true }, "normalize-path": { @@ -31894,9 +31540,9 @@ } }, "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", "dev": true, "requires": { "tslib": "^1.9.0" @@ -31924,9 +31570,9 @@ "dev": true }, "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "dev": true, "requires": { "emoji-regex": "^8.0.0", @@ -34095,36 +33741,30 @@ } }, "sanitize-html": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.3.3.tgz", - "integrity": "sha512-DCFXPt7Di0c6JUnlT90eIgrjs6TsJl/8HYU3KLdmrVclFN4O0heTcVbJiMa23OKVr6aR051XYtsgd8EWwEBwUA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.0.tgz", + "integrity": "sha512-jfQelabOn5voO7FAfnQF7v+jsA6z9zC/O4ec0z3E35XPEtHYJT/OdUziVWlKW4irCr2kXaQAyXTXDHWAibg1tA==", "requires": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", "htmlparser2": "^6.0.0", "is-plain-object": "^5.0.0", - "klona": "^2.0.3", "parse-srcset": "^1.0.2", - "postcss": "^8.0.2" + "postcss": "^8.3.11" }, "dependencies": { - "colorette": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", - "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" - }, "deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" }, "dom-serializer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.1.tgz", - "integrity": "sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", "requires": { "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", + "domhandler": "^4.2.0", "entities": "^2.0.0" } }, @@ -34134,21 +33774,21 @@ "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" }, "domhandler": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.1.0.tgz", - "integrity": "sha512-/6/kmsGlMY4Tup/nGVutdrK9yQi4YjWVcVeoQmixpzjOUK1U7pQkvAPHBJeUxOgxF0J8f8lwCJSlCfD0V4CMGQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.0.tgz", + "integrity": "sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==", "requires": { "domelementtype": "^2.2.0" } }, "domutils": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.5.2.tgz", - "integrity": "sha512-MHTthCb1zj8f1GVfRpeZUbohQf/HdBos0oX5gZcQFepOZPLLRyj6Wn7XS7EMnY7CVpwv8863u2vyE83Hfu28HQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", "requires": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", - "domhandler": "^4.1.0" + "domhandler": "^4.2.0" } }, "entities": { @@ -34177,25 +33817,15 @@ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" }, - "klona": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", - "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==" - }, "postcss": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.9.tgz", - "integrity": "sha512-b+TmuIL4jGtCHtoLi+G/PisuIl9avxs8IZMSmlABRwNz5RLUUACrC+ws81dcomz1nRezm5YPdXiMEzBEKgYn+Q==", + "version": "8.4.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz", + "integrity": "sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==", "requires": { - "colorette": "^1.2.2", - "nanoid": "^3.1.22", - "source-map": "^0.6.1" + "nanoid": "^3.2.0", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, @@ -34602,7 +34232,8 @@ "signal-exit": { "version": "3.0.2", "resolved": "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true }, "simple-concat": { "version": "1.0.1", @@ -34703,102 +34334,46 @@ } }, "socket.io-client": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.0.0.tgz", - "integrity": "sha512-27yQxmXJAEYF19Ygyl8FPJ0if0wegpSmkIIbrWJeI7n7ST1JyH8bbD5v3fjjGY5cfCanACJ3dARUAyiVFNrlTQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.4.1.tgz", + "integrity": "sha512-N5C/L5fLNha5Ojd7Yeb/puKcPWWcoB/A09fEjjNsg91EDVr5twk/OEyO6VT9dlLSUNY85NpW6KBhVMvaLKQ3vQ==", "requires": { - "@types/component-emitter": "^1.2.10", + "@socket.io/component-emitter": "~3.0.0", "backo2": "~1.0.2", - "component-emitter": "~1.3.0", - "debug": "~4.3.1", - "engine.io-client": "~5.0.0", + "debug": "~4.3.2", + "engine.io-client": "~6.1.1", "parseuri": "0.0.6", - "socket.io-parser": "~4.0.4" + "socket.io-parser": "~4.1.1" }, "dependencies": { - "base64-arraybuffer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", - "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } }, - "engine.io-client": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.0.0.tgz", - "integrity": "sha512-e6GK0Fqvq45Nu/j7YdIVqXtDPvlsggAcfml3QiEiGdJ1qeh7IQU6knxSN3+yy9BmbnXtIfjo1hK4MFyHKdc9mQ==", - "requires": { - "base64-arraybuffer": "0.1.4", - "component-emitter": "~1.3.0", - "debug": "~4.3.1", - "engine.io-parser": "~4.0.1", - "has-cors": "1.1.0", - "parseqs": "0.0.6", - "parseuri": "0.0.6", - "ws": "~7.4.2", - "yeast": "0.1.2" - } - }, - "engine.io-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz", - "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==", - "requires": { - "base64-arraybuffer": "0.1.4" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "parseqs": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", - "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" - }, - "parseuri": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", - "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" - }, - "ws": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", - "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==" } } }, "socket.io-parser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", - "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.1.1.tgz", + "integrity": "sha512-USQVLSkDWE5nbcY760ExdKaJxCE65kcsG/8k5FDGZVVxpD1pA7hABYXYkCUvxUuYYh/+uQw0N/fvBzfT8o07KA==", "requires": { - "@types/component-emitter": "^1.2.10", - "component-emitter": "~1.3.0", + "@socket.io/component-emitter": "~3.0.0", "debug": "~4.3.1" }, "dependencies": { - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } @@ -34881,6 +34456,11 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + }, "source-map-resolve": { "version": "0.5.2", "resolved": "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz", @@ -35639,6 +35219,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", + "dev": true, "requires": { "call-bind": "^1.0.0", "define-properties": "^1.1.3" @@ -35648,6 +35229,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -35658,6 +35240,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", + "dev": true, "requires": { "call-bind": "^1.0.0", "define-properties": "^1.1.3" @@ -35667,6 +35250,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -35967,16 +35551,6 @@ } } }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, "map-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", @@ -36982,7 +36556,7 @@ "dev": true }, "three": { - "version": "github:mozillareality/three.js#17375caff569ad48e1bef3e673e33f0bdd5d9bab", + "version": "github:mozillareality/three.js#2ea3755b4873db768c7b9b0b30e488e97e3bb613", "from": "github:mozillareality/three.js#hubs-patches-133" }, "three-ammo": { @@ -36990,7 +36564,7 @@ "from": "github:infinitelee/three-ammo" }, "three-bmfont-text": { - "version": "github:mozillareality/three-bmfont-text#3cbce0b90403d7ca6e690e9174c650f4606b53d8", + "version": "github:mozillareality/three-bmfont-text#f6aa2ad7aca998d8fe448cde119ff7af498f4f99", "from": "github:mozillareality/three-bmfont-text#hubs/master", "requires": { "array-shuffle": "^1.0.1", @@ -37020,7 +36594,7 @@ "integrity": "sha512-lBXt+GLlgCz/ppakGDj6L/JJH2JmZ8ZFiFHqey4v47V8SrzhKRuXfrsHo6IPF1Z/MDki/ahIf7uquW+jy/3hAg==" }, "three-to-ammo": { - "version": "github:infinitelee/three-to-ammo#92fd0e8300e693d4d48a603a8c446e520f9568ab", + "version": "github:infinitelee/three-to-ammo#9e68a3bbe500988f2dc0096f78c12dabbdaf5548", "from": "github:infinitelee/three-to-ammo" }, "throttle-debounce": { @@ -37421,26 +36995,6 @@ "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=", "dev": true }, - "unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - }, - "dependencies": { - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true - } - } - }, "underscore": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.5.2.tgz", @@ -39128,30 +38682,6 @@ "isexe": "^2.0.0" } }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "dependencies": { - "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.1" - } - } - } - }, "which-module": { "version": "2.0.0", "resolved": "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz", @@ -39335,6 +38865,11 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" + }, "xregexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", @@ -39386,9 +38921,9 @@ }, "dependencies": { "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "ansi-styles": { @@ -39445,23 +38980,23 @@ "dev": true }, "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "strip-ansi": "^6.0.1" } }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" } }, "wrap-ansi": { @@ -39476,15 +39011,15 @@ } }, "y18n": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", - "integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, "yargs-parser": { - "version": "20.2.7", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", - "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true } } diff --git a/package.json b/package.json index e11632dfce..3cbe263ca4 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "@mozillareality/easing-functions": "^0.1.1", "@mozillareality/three-batch-manager": "github:mozillareality/three-batch-manager#master", "@popperjs/core": "^2.4.4", - "aframe": "github:immers-space/aframe#hubs-cloud-safari-15-patches", + "aframe": "github:mozillareality/aframe#hubs/master", "aframe-rounded": "^1.0.3", "aframe-slice9-component": "github:takahirox/aframe-slice9-component#AlphaTest", "ammo-debug-drawer": "github:infinitelee/ammo-debug-drawer", From 97bf4118ce868e5153b0ef668f6c4ec6652fdec7 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 11 Feb 2022 18:29:17 -0600 Subject: [PATCH 141/167] include the new BASE_ASSETS_PATH env var in build environments --- admin/webpack.config.js | 3 ++- webpack.config.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/admin/webpack.config.js b/admin/webpack.config.js index ccf6e4d0bd..584cfad214 100644 --- a/admin/webpack.config.js +++ b/admin/webpack.config.js @@ -225,7 +225,8 @@ module.exports = (env, argv) => { RETICULUM_SERVER: process.env.RETICULUM_SERVER, CORS_PROXY_SERVER: process.env.CORS_PROXY_SERVER, POSTGREST_SERVER: process.env.POSTGREST_SERVER, - UPLOADS_HOST: process.env.UPLOADS_HOST + UPLOADS_HOST: process.env.UPLOADS_HOST, + BASE_ASSETS_PATH: process.env.BASE_ASSETS_PATH }) }) ] diff --git a/webpack.config.js b/webpack.config.js index c536e46b18..acc114db1d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -645,6 +645,7 @@ module.exports = async (env, argv) => { GA_TRACKING_ID: process.env.GA_TRACKING_ID, POSTGREST_SERVER: process.env.POSTGREST_SERVER, UPLOADS_HOST: process.env.UPLOADS_HOST, + BASE_ASSETS_PATH: process.env.BASE_ASSETS_PATH, APP_CONFIG: appConfig }) }) From 78d5832ad2c2e8be5ba4826a6d6b40f2426345f3 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 11 Feb 2022 19:38:46 -0600 Subject: [PATCH 142/167] 1.7.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f22a6b035a..822a723346 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.6.0", + "version": "1.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3cbe263ca4..1924bf5ff6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.6.0", + "version": "1.7.0", "description": "Duck-themed multi-user virtual spaces in WebVR.", "main": "src/index.js", "license": "MPL-2.0", From ddeddbbdd64a0901a66acf84a27451e660ff0ff1 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 3 Jun 2022 12:24:27 -0500 Subject: [PATCH 143/167] loosen avatar import requirements to support nice.freetreasures.com avatars and start immers-client library integration --- package-lock.json | 54 +++++++++++++++++++++++++-------------------- package.json | 1 + src/utils/immers.js | 13 +++++------ 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index c2cd6d9373..97050ea400 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19969,7 +19969,9 @@ "colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "optional": true }, "combined-stream": { "version": "1.0.8", @@ -21347,6 +21349,11 @@ "domelementtype": "1" } }, + "dompurify": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz", + "integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==" + }, "domutils": { "version": "1.5.1", "resolved": "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz", @@ -21586,23 +21593,6 @@ "stream-shift": "^1.0.0" } }, - "easyrtc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/easyrtc/-/easyrtc-1.1.0.tgz", - "integrity": "sha1-9Ek39xMsuLW6jgvBzD48zEcqPvQ=", - "requires": { - "async": "0.2.x", - "colors": "*", - "underscore": "1.5.x" - }, - "dependencies": { - "async": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" - } - } - }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -24682,6 +24672,23 @@ "integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==", "dev": true }, + "immers-client": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/immers-client/-/immers-client-2.5.0.tgz", + "integrity": "sha512-9GEYOherZBIs2Y7OEy9Ecf+YX9CsP4MH8EZnoL5KO5FL9/0vcc6HbD/8SzGcgRYYOrHmi8L9FXObQThxOqWnYw==", + "requires": { + "core-js": "^3.17.2", + "dompurify": "^2.3.6", + "socket.io-client": "^4.0.0" + }, + "dependencies": { + "core-js": { + "version": "3.22.8", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.8.tgz", + "integrity": "sha512-UoGQ/cfzGYIuiq6Z7vWL1HfkE9U9IZ4Ub+0XSiJTCzvbZzgPA69oDF2f+lgJ6dFFLEdjW5O6svvoKzXX23xFkA==" + } + } + }, "immutable": { "version": "3.7.6", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", @@ -27788,14 +27795,12 @@ "version": "github:mozillareality/networked-aframe#a691b90cd1c817283fbd4ce32f63b0b184311f4c", "from": "github:mozillareality/networked-aframe#master", "requires": { - "buffered-interpolation": "^0.2.5", - "easyrtc": "1.1.0" + "buffered-interpolation": "buffered-interpolation@0.2.5" }, "dependencies": { "buffered-interpolation": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/buffered-interpolation/-/buffered-interpolation-0.2.5.tgz", - "integrity": "sha512-LauOWiVXZppapDpc8y2yzcf3/pKU1Exal/+H0EZeNVxttnWHqtyhEaJ2J4U5yia71FWqZvywnhx8xneh073xbA==" + "version": "https://registry.npmjs.org/buffered-interpolation/-/buffered-interpolation-0.2.5.tgz", + "from": "buffered-interpolation@0.2.5" } } }, @@ -36893,7 +36898,8 @@ "underscore": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.5.2.tgz", - "integrity": "sha1-EzXF5PXm0zu7SwBrqMhqAPVW3gg=" + "integrity": "sha1-EzXF5PXm0zu7SwBrqMhqAPVW3gg=", + "dev": true }, "unfetch": { "version": "4.2.0", diff --git a/package.json b/package.json index 6e7aa25fb9..74b63b3c9c 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "history": "^4.7.2", "hls.js": "^0.14.6", "html2canvas": "^1.0.0-rc.7", + "immers-client": "^2.5.0", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", diff --git a/src/utils/immers.js b/src/utils/immers.js index fe327ddd7d..35c6004ca0 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -1,4 +1,5 @@ import io from "socket.io-client"; +import { ImmersClient } from "immers-client"; import configs from "./configs"; import { fetchAvatar } from "./avatar-utils"; import { SOUND_CHAT_MESSAGE } from "../systems/sound-effects-system"; @@ -40,12 +41,12 @@ const hubUri = new URL(window.location); export function getUrlFromAvatar(avatar) { const links = Array.isArray(avatar.url) ? avatar.url : [avatar.url]; // prefer gltf - const gltfUrl = links.find(link => link.mediaType === "model/gltf+json" || link.mediaType === "model/gltf-binary"); + const gltfUrl = links.find(link => link.mediaType.includes("gltf")); if (gltfUrl) { return gltfUrl.href; } // gamble on a url of unkown type - return links.find(link => typeof link === "string"); + return ImmersClient.URLFromProperty(links[0]); } export function getAvatarFromActor(actorObj) { @@ -196,11 +197,7 @@ export async function fetchMyImmersAvatars(page) { // cache results for lookup by url when donned myAvatars[avatarGltfUrl] = avatar; // form object for Hubs MediaBrowser - let preview = Array.isArray(avatar.icon) ? avatar.icon[0] : avatar.icon; - // if link/image object instead of direct link - if (typeof preview === "object") { - preview = preview.href || preview.url; - } + const icon = Array.isArray(avatar.icon) ? avatar.icon[0] : avatar.icon; hubsResult.entries.push({ type: "avatar", name: avatar.name, @@ -209,7 +206,7 @@ export async function fetchMyImmersAvatars(page) { images: { preview: { // width/height ignored for avatar media - url: preview + url: ImmersClient.URLFromProperty(icon) } }, // display source immer name & link in description field From ba264d98507fc3c4f2b3f8d10e6271fb9ce2628e Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 3 Jun 2022 12:47:29 -0500 Subject: [PATCH 144/167] update for immers v3 breaking change on friends API - ignore outgoing friend requests in response until a full immers client integration can be completed --- src/utils/immers.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 35c6004ca0..4eb3a8dcc3 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -433,7 +433,10 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat try { friendsCol = await getFriends(profile); activities.friends = friendsCol.orderedItems; - remountUI({ friends: friendsCol.orderedItems.filter(act => act.type !== "Reject"), handle: profile.handle }); + remountUI({ + friends: friendsCol.orderedItems.filter(act => act.type !== "Reject" && act.actor.id), + handle: profile.handle + }); } catch (err) { console.warn(err.message); remountUI({ friends: [], handle: profile.handle }); From 0707efdf56b96ecbb23af7ef0e9d93f2e38dc562 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 3 Jun 2022 12:58:23 -0500 Subject: [PATCH 145/167] clarfiy homepage sign-in is only for admins --- src/react-components/home/SignInButton.js | 4 ++-- src/react-components/home/SignInButton.scss | 1 + src/react-components/layout/Header.scss | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/react-components/home/SignInButton.js b/src/react-components/home/SignInButton.js index e6d2304dcd..9560cb6c7e 100644 --- a/src/react-components/home/SignInButton.js +++ b/src/react-components/home/SignInButton.js @@ -6,8 +6,8 @@ import { Button } from "../input/Button"; export function SignInButton({ mobile }) { return ( - ); } diff --git a/src/react-components/home/SignInButton.scss b/src/react-components/home/SignInButton.scss index 748e67d36e..4d75efd10c 100644 --- a/src/react-components/home/SignInButton.scss +++ b/src/react-components/home/SignInButton.scss @@ -8,6 +8,7 @@ } :local(.mobile-sign-in) { + z-index: 5; /* prevent menu header from blocking button */ display: flex; @media(min-width: theme.$breakpoint-lg) { display: none; diff --git a/src/react-components/layout/Header.scss b/src/react-components/layout/Header.scss index ecbaac12ee..82901e93ce 100644 --- a/src/react-components/layout/Header.scss +++ b/src/react-components/layout/Header.scss @@ -94,7 +94,7 @@ header { a { margin-left: 8px; - color: theme.$link-color; + // color: theme.$link-color; } @media(min-width: theme.$breakpoint-lg) { From 8f77a46529df9b60b197a59ca935e7f6b058c27d Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 17 Jun 2022 11:31:43 -0500 Subject: [PATCH 146/167] Begin migration to immers-client, making a connected immersClient instance available in parallel to existing process --- src/utils/immers.js | 36 +++++++++--------------------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 4eb3a8dcc3..3c36350e40 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -1,4 +1,3 @@ -import io from "socket.io-client"; import { ImmersClient } from "immers-client"; import configs from "./configs"; import { fetchAvatar } from "./avatar-utils"; @@ -13,7 +12,10 @@ const localImmer = configs.IMMERS_SERVER; const preferredScope = configs.IMMERS_SCOPE; console.log("immers.space client v1.2.0"); const jsonldMime = "application/activity+json"; - +export const immersClient = new ImmersClient(`${localImmer}/o/immer`, { + allowStorage: true, + localImmer +}); const activities = new Activities(localImmer); let homeImmer; let place; @@ -352,6 +354,10 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat activities.homeImmer = homeImmer; activities.authorizedScopes = authorizedScopes; activities.actor = actorObj; + // during incremental migration to immers client, connect client in parallel + await immersClient.loginWithToken(token, homeImmer, authorizedScopes); + // set online status to here + immersClient.enter(); const initialAvi = store.state.profile.avatarId; const actorAvi = getAvatarFromActor(actorObj); // cache current avatar so doesn't get recreated during a profile update @@ -378,31 +384,7 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat replaceArrays ); authorizedScopes.forEach(scope => hubScene.addState(`immers-scope-${scope}`)); - const immerSocket = io(homeImmer, { - transportOptions: { - polling: { - extraHeaders: { - Authorization: `Bearer ${token}` - } - } - } - }); - immerSocket.on("connect", () => { - // will also send on reconnect to ensure you show as online - arrive(actorObj); - immerSocket.emit("entered", { - // prepare a leave activity to be fired on disconnect - outbox: actorObj.outbox, - authorization: `Bearer ${token}`, - leave: { - type: "Leave", - actor: actorObj.id, - target: place, - to: actorObj.followers, - summary: `${actorObj.name} left ${place.name}.` - } - }); - }); + const immerSocket = immersClient.streaming.socket; // friends list let friendsCol; From 274f6b53b6e1ff1c25830389205228168dbe4c68 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 17 Jun 2022 12:31:15 -0500 Subject: [PATCH 147/167] 1.8.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a699a9bf50..8dc6b9d026 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.7.0", + "version": "1.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -38921,4 +38921,4 @@ "dev": true } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 9905136a02..12b1a9b6ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.7.0", + "version": "1.8.0", "description": "Duck-themed multi-user virtual spaces in WebVR.", "main": "src/index.js", "license": "MPL-2.0", From 61a1f66c4cfc354a89069ae061880506a1b7a593 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 17 Jun 2022 12:31:35 -0500 Subject: [PATCH 148/167] version --- src/utils/immers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 3c36350e40..b6e452e0f6 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -10,7 +10,7 @@ const replaceArrays = { arrayMerge: (destinationArray, sourceArray) => sourceArr const localImmer = configs.IMMERS_SERVER; // immer can set a requested scope, but user can override const preferredScope = configs.IMMERS_SCOPE; -console.log("immers.space client v1.2.0"); +console.log("immers.space client v1.8.0"); const jsonldMime = "application/activity+json"; export const immersClient = new ImmersClient(`${localImmer}/o/immer`, { allowStorage: true, From 4cb4d0b4d7a4925c890d6613d593466d477e4313 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 17 Jun 2022 14:18:33 -0500 Subject: [PATCH 149/167] fix troika text ignoring newlines --- src/components/troika-text.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/troika-text.js b/src/components/troika-text.js index aa32aaba32..4c4ea4845c 100644 --- a/src/components/troika-text.js +++ b/src/components/troika-text.js @@ -95,7 +95,7 @@ AFRAME.registerComponent("text", { const mesh = this.troikaTextMesh; // Update the text mesh - mesh.text = data.value || ""; + mesh.text = (data.value || "").replace(/\\n/g, "\n").replace(/\\t/g, "\t"); mesh.textAlign = data.textAlign; mesh.anchorX = data.anchorX; mesh.anchorY = data.anchorY; From b1b5682223162c35b52cf07b1e7d6ca0e08e6126 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 17 Jun 2022 14:21:21 -0500 Subject: [PATCH 150/167] fix text align & size on media share buttons --- src/hub.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hub.html b/src/hub.html index 58931b2c41..a67972d580 100644 --- a/src/hub.html +++ b/src/hub.html @@ -848,7 +848,7 @@ >
    - + - + - + - + Date: Fri, 17 Jun 2022 14:21:44 -0500 Subject: [PATCH 151/167] 1.8.1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8dc6b9d026..59ce79e2b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.8.0", + "version": "1.8.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 12b1a9b6ee..39201c774e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.8.0", + "version": "1.8.1", "description": "Duck-themed multi-user virtual spaces in WebVR.", "main": "src/index.js", "license": "MPL-2.0", From 351ac18c2ec92c741efde99eab4af8fd72ae6619 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 17 Jun 2022 14:22:20 -0500 Subject: [PATCH 152/167] version --- src/utils/immers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index b6e452e0f6..dab973ea97 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -10,7 +10,7 @@ const replaceArrays = { arrayMerge: (destinationArray, sourceArray) => sourceArr const localImmer = configs.IMMERS_SERVER; // immer can set a requested scope, but user can override const preferredScope = configs.IMMERS_SCOPE; -console.log("immers.space client v1.8.0"); +console.log("immers.space client v1.8.1"); const jsonldMime = "application/activity+json"; export const immersClient = new ImmersClient(`${localImmer}/o/immer`, { allowStorage: true, From 46338262758180e4d065905a842add296f90af46 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 15 Jul 2022 17:58:21 -0500 Subject: [PATCH 153/167] fix room id omitted from shared location --- src/utils/immers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index dab973ea97..8807b33fc7 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -345,7 +345,7 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat hubScene = scene; localPlayer = document.getElementById("avatar-rig"); place = await getObject(`${localImmer}/o/immer`); - place.url = hubUri; // include room id + place.url = hubUri.href; // include room id activities.place = place; const { authPromise, startImmersAuth } = await auth(store); remountUI({ isImmersConnected: false, startImmersAuth }); @@ -357,7 +357,7 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat // during incremental migration to immers client, connect client in parallel await immersClient.loginWithToken(token, homeImmer, authorizedScopes); // set online status to here - immersClient.enter(); + immersClient.enter(place); const initialAvi = store.state.profile.avatarId; const actorAvi = getAvatarFromActor(actorObj); // cache current avatar so doesn't get recreated during a profile update From 9c29108bf07edc83330c126f1dd908c6054da8e2 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 15 Jul 2022 18:03:40 -0500 Subject: [PATCH 154/167] 1.8.2 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 59ce79e2b8..8abc7baa92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.8.1", + "version": "1.8.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 39201c774e..e7715f9510 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.8.1", + "version": "1.8.2", "description": "Duck-themed multi-user virtual spaces in WebVR.", "main": "src/index.js", "license": "MPL-2.0", From 92f701078bedc6f79974f91ce72cee1fd1ea4b0b Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 26 Aug 2022 17:30:28 -0500 Subject: [PATCH 155/167] bump immers client, fixup package-lock --- package-lock.json | 177 +++++++++++++++++----------------------------- package.json | 2 +- 2 files changed, 67 insertions(+), 112 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8abc7baa92..09cfcf662c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6971,15 +6971,10 @@ "react-lifecycles-compat": "^3.0.4" } }, - "@socket.io/base64-arraybuffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==" - }, "@socket.io/component-emitter": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.0.0.tgz", - "integrity": "sha512-2pTGuibAXJswAPJjaKisthqS/NOK5ypG4LYT6tEAV0S/mxW0zOIvYvGK0V8w8+SHxAm6vRMSjqSalFXeBAqs+Q==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, "@storybook/addon-actions": { "version": "6.1.15", @@ -18881,11 +18876,6 @@ "to-fast-properties": "^1.0.3" } }, - "backo2": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" - }, "bail": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.3.tgz", @@ -21350,9 +21340,9 @@ } }, "dompurify": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz", - "integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz", + "integrity": "sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA==" }, "domutils": { "version": "1.5.1", @@ -21752,25 +21742,21 @@ } }, "engine.io-client": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.1.1.tgz", - "integrity": "sha512-V05mmDo4gjimYW+FGujoGmmmxRaDsrVr7AXA3ZIfa04MWM1jOfZfUwou0oNqhNwy/votUDvGDt4JA4QF4e0b4g==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz", + "integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==", "requires": { - "@socket.io/component-emitter": "~3.0.0", + "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", - "engine.io-parser": "~5.0.0", - "has-cors": "1.1.0", - "parseqs": "0.0.6", - "parseuri": "0.0.6", + "engine.io-parser": "~5.0.3", "ws": "~8.2.3", - "xmlhttprequest-ssl": "~2.0.0", - "yeast": "0.1.2" + "xmlhttprequest-ssl": "~2.0.0" }, "dependencies": { "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { "ms": "2.1.2" } @@ -21788,12 +21774,9 @@ } }, "engine.io-parser": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", - "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", - "requires": { - "@socket.io/base64-arraybuffer": "~1.0.2" - } + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz", + "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==" }, "enhanced-resolve": { "version": "4.1.0", @@ -21992,7 +21975,7 @@ }, "escape-html": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "resolved": "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", "dev": true }, @@ -23729,11 +23712,6 @@ "ansi-regex": "^2.0.0" } }, - "has-cors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", - "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" - }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz", @@ -24525,7 +24503,7 @@ }, "http-deceiver": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "resolved": "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz", "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", "dev": true }, @@ -24673,9 +24651,9 @@ "dev": true }, "immers-client": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/immers-client/-/immers-client-2.5.0.tgz", - "integrity": "sha512-9GEYOherZBIs2Y7OEy9Ecf+YX9CsP4MH8EZnoL5KO5FL9/0vcc6HbD/8SzGcgRYYOrHmi8L9FXObQThxOqWnYw==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/immers-client/-/immers-client-2.8.0.tgz", + "integrity": "sha512-GFevNHD7Ln3Rj5lwC7tJ68/czZvbUTKPJgYXp1EtDeHTkaL8ZPPNBlvgnsYQy6H2eSln+TfQwNhocC8J8F8lCA==", "requires": { "core-js": "^3.17.2", "dompurify": "^2.3.6", @@ -24683,9 +24661,9 @@ }, "dependencies": { "core-js": { - "version": "3.22.8", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.8.tgz", - "integrity": "sha512-UoGQ/cfzGYIuiq6Z7vWL1HfkE9U9IZ4Ub+0XSiJTCzvbZzgPA69oDF2f+lgJ6dFFLEdjW5O6svvoKzXX23xFkA==" + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.25.0.tgz", + "integrity": "sha512-CVU1xvJEfJGhyCpBrzzzU1kjCfgsGUxhEvwUV2e/cOedYWHdmluamx+knDnmhqALddMG16fZvIqvs9aijsHHaA==" } } }, @@ -25467,7 +25445,7 @@ }, "is-plain-obj": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "resolved": "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", "dev": true }, @@ -27692,9 +27670,9 @@ "optional": true }, "nanoid": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", - "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==" + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" }, "nanomatch": { "version": "1.2.13", @@ -27795,12 +27773,12 @@ "version": "github:mozillareality/networked-aframe#a691b90cd1c817283fbd4ce32f63b0b184311f4c", "from": "github:mozillareality/networked-aframe#master", "requires": { - "buffered-interpolation": "buffered-interpolation@0.2.5" + "buffered-interpolation": "github:Infinitelee/buffered-interpolation#5bb18421ebf2bf11664645cdc7a15bd77ee2156b" }, "dependencies": { "buffered-interpolation": { - "version": "https://registry.npmjs.org/buffered-interpolation/-/buffered-interpolation-0.2.5.tgz", - "from": "buffered-interpolation@0.2.5" + "version": "github:Infinitelee/buffered-interpolation#5bb18421ebf2bf11664645cdc7a15bd77ee2156b", + "from": "github:Infinitelee/buffered-interpolation#5bb18421ebf2bf11664645cdc7a15bd77ee2156b" } } }, @@ -29053,7 +29031,7 @@ "parse-srcset": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", - "integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=" + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" }, "parse-url": { "version": "5.0.2", @@ -29081,11 +29059,6 @@ "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", "dev": true }, - "parseqs": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", - "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" - }, "parserlib": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parserlib/-/parserlib-1.1.1.tgz", @@ -29093,11 +29066,6 @@ "dev": true, "optional": true }, - "parseuri": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", - "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" - }, "parseurl": { "version": "1.3.2", "resolved": "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz", @@ -29405,11 +29373,6 @@ "find-up": "^2.1.0" } }, - "platform-command": { - "version": "git+https://gitlab.com/gitlabdev/platform-command.git#3f02478b2cbe88d516c0d17b2c221d3c3b96e16f", - "from": "git+https://gitlab.com/gitlabdev/platform-command.git", - "dev": true - }, "plur": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/plur/-/plur-3.1.1.tgz", @@ -33639,9 +33602,9 @@ } }, "sanitize-html": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.0.tgz", - "integrity": "sha512-jfQelabOn5voO7FAfnQF7v+jsA6z9zC/O4ec0z3E35XPEtHYJT/OdUziVWlKW4irCr2kXaQAyXTXDHWAibg1tA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.1.tgz", + "integrity": "sha512-oOpe8l4J8CaBk++2haoN5yNI5beekjuHv3JRPKUx/7h40Rdr85pemn4NkvUB3TcBP7yjat574sPlcMAyv4UQig==", "requires": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", @@ -33657,9 +33620,9 @@ "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" }, "dom-serializer": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", - "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", "requires": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", @@ -33667,14 +33630,14 @@ } }, "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" }, "domhandler": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.0.tgz", - "integrity": "sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", "requires": { "domelementtype": "^2.2.0" } @@ -33716,11 +33679,11 @@ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" }, "postcss": { - "version": "8.4.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz", - "integrity": "sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==", + "version": "8.4.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", + "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", "requires": { - "nanoid": "^3.2.0", + "nanoid": "^3.3.4", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } @@ -34217,22 +34180,20 @@ } }, "socket.io-client": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.4.1.tgz", - "integrity": "sha512-N5C/L5fLNha5Ojd7Yeb/puKcPWWcoB/A09fEjjNsg91EDVr5twk/OEyO6VT9dlLSUNY85NpW6KBhVMvaLKQ3vQ==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.1.tgz", + "integrity": "sha512-e6nLVgiRYatS+AHXnOnGi4ocOpubvOUCGhyWw8v+/FxW8saHkinG6Dfhi9TU0Kt/8mwJIAASxvw6eujQmjdZVA==", "requires": { - "@socket.io/component-emitter": "~3.0.0", - "backo2": "~1.0.2", + "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", - "engine.io-client": "~6.1.1", - "parseuri": "0.0.6", - "socket.io-parser": "~4.1.1" + "engine.io-client": "~6.2.1", + "socket.io-parser": "~4.2.0" }, "dependencies": { "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { "ms": "2.1.2" } @@ -34245,18 +34206,18 @@ } }, "socket.io-parser": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.1.1.tgz", - "integrity": "sha512-USQVLSkDWE5nbcY760ExdKaJxCE65kcsG/8k5FDGZVVxpD1pA7hABYXYkCUvxUuYYh/+uQw0N/fvBzfT8o07KA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz", + "integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==", "requires": { - "@socket.io/component-emitter": "~3.0.0", + "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" }, "dependencies": { "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { "ms": "2.1.2" } @@ -34563,7 +34524,6 @@ "glob": "~3.2.6", "mustache": "~0.7.2", "optimist": "~0.6.0", - "platform-command": "git+https://gitlab.com/gitlabdev/platform-command.git", "underscore": "~1.5.2" }, "dependencies": { @@ -38895,11 +38855,6 @@ } } }, - "yeast": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", - "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" - }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index e7715f9510..dfdca84f62 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "history": "^4.7.2", "hls.js": "^0.14.6", "html2canvas": "^1.0.0-rc.7", - "immers-client": "^2.5.0", + "immers-client": "^2.8.0", "js-cookie": "^2.2.0", "jsonschema": "^1.2.2", "jwt-decode": "^2.2.0", From c9f556f35c1623d589e49ce61702459c740255b6 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 26 Aug 2022 21:29:59 -0500 Subject: [PATCH 156/167] update shared hub metadata, handle HTML in location status updates --- src/react-components/room/ImmersReact.js | 10 +++++--- src/react-components/room/ImmersReact.scss | 7 ++++++ src/utils/immers.js | 29 +++++++++++----------- src/utils/immers/activities.js | 2 +- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/react-components/room/ImmersReact.js b/src/react-components/room/ImmersReact.js index f9afa4333c..4a1a2cacb1 100644 --- a/src/react-components/room/ImmersReact.js +++ b/src/react-components/room/ImmersReact.js @@ -13,6 +13,7 @@ import { Modal } from "../modal/Modal"; import { Button } from "../input/Button"; import { Column } from "../layout/Column"; import { CloseButton } from "../input/CloseButton"; +import { formatMessageBody } from "../../utils/chat-message"; function proxyAndGetMessageComponent(message) { // media urls need proxy to pass CSP & CORS @@ -56,10 +57,13 @@ export function ImmersChatMessage({ sent, sender, timestamp, isFriend, icon, imm if (messages[0].type === "activity") { return (
  • -

    - {messages[0].body} |{" "} +

    + {sent ? "You" : sender} + + {formatMessageBody(messages[0].body).formattedBody} + -

    +
  • ); } diff --git a/src/react-components/room/ImmersReact.scss b/src/react-components/room/ImmersReact.scss index 998ab91b91..ea0b07f0c1 100644 --- a/src/react-components/room/ImmersReact.scss +++ b/src/react-components/room/ImmersReact.scss @@ -65,4 +65,11 @@ :local(.permissions-button) { padding-right: 5px; +} + +:local(.divider) { + &:after { + content: "|"; + } + padding: 0 1ch; } \ No newline at end of file diff --git a/src/utils/immers.js b/src/utils/immers.js index 8807b33fc7..21a0c279bb 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -110,16 +110,6 @@ export function updateProfile(actorObj, update) { return postActivity(actorObj.outbox, activity); } -export function arrive(actorObj) { - return postActivity(actorObj.outbox, { - type: "Arrive", - actor: actorObj.id, - target: place, - to: actorObj.followers, - summary: `${actorObj.name} arrived at ${place.name}.` - }); -} - // Adds a new avatar to an immerser's inventory export async function createAvatar(actorObj, hubsAvatarId) { const hubsAvatar = await fetchAvatar(hubsAvatarId); @@ -356,8 +346,19 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat activities.actor = actorObj; // during incremental migration to immers client, connect client in parallel await immersClient.loginWithToken(token, homeImmer, authorizedScopes); + // wait for hubs room metadata to load + if (!window.APP?.hub?.hub_id) { + await new Promise(resolve => { + hubScene.addEventListener("hub_updated", resolve, { once: true }); + }); + } // set online status to here - immersClient.enter(place); + immersClient.enter({ + name: APP.hub.name, + url: hubUri.href, + privacy: "friends", + previewImage: APP.hub.scene.screenshot_url + }); const initialAvi = store.state.profile.avatarId; const actorAvi = getAvatarFromActor(actorObj); // cache current avatar so doesn't get recreated during a profile update @@ -365,10 +366,10 @@ export async function initialize(store, scene, remountUI, messageDispatch, creat store.update( { profile: { - id: actorObj.id, + id: immersClient.profile.id, avatarId: actorAvi || initialAvi, - displayName: actorObj.name, - handle: `${actorObj.preferredUsername}[${new URL(homeImmer).host}]`, + displayName: immersClient.profile.displayName, + handle: immersClient.profile.handle, inbox: actorObj.inbox, outbox: actorObj.outbox, followers: actorObj.followers diff --git a/src/utils/immers/activities.js b/src/utils/immers/activities.js index b21baa2a1b..efbbe24ab0 100644 --- a/src/utils/immers/activities.js +++ b/src/utils/immers/activities.js @@ -266,7 +266,7 @@ export default class Activities { message.body = { src: activity.object.url }; break; default: - if (activity.summary) { + if (activity.type === "Arrive" || activity.type === "Leave") { message.type = "activity"; message.body = activity.summary; } From 7e4bcdbd3a6d73e9dfee731b97fc17585a9743de Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 26 Aug 2022 21:35:41 -0500 Subject: [PATCH 157/167] version --- src/utils/immers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/immers.js b/src/utils/immers.js index 21a0c279bb..4d9205f853 100644 --- a/src/utils/immers.js +++ b/src/utils/immers.js @@ -10,7 +10,7 @@ const replaceArrays = { arrayMerge: (destinationArray, sourceArray) => sourceArr const localImmer = configs.IMMERS_SERVER; // immer can set a requested scope, but user can override const preferredScope = configs.IMMERS_SCOPE; -console.log("immers.space client v1.8.1"); +console.log("immers.space client v1.9.0"); const jsonldMime = "application/activity+json"; export const immersClient = new ImmersClient(`${localImmer}/o/immer`, { allowStorage: true, From 7630143a2643c06c4fb397a65945fcb237ce868c Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 26 Aug 2022 21:35:48 -0500 Subject: [PATCH 158/167] 1.9.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 09cfcf662c..0facb9c23d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.8.2", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index dfdca84f62..640898f43f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hubs", - "version": "1.8.2", + "version": "1.9.0", "description": "Duck-themed multi-user virtual spaces in WebVR.", "main": "src/index.js", "license": "MPL-2.0", From b8e0c49fc6a75ab8ae3997e4bc9710442fee1b0e Mon Sep 17 00:00:00 2001 From: Quinn Madson Date: Sat, 27 Aug 2022 13:55:58 -0500 Subject: [PATCH 159/167] On branch hubs-cloud-aug-2022 Changes to be committed: modified: .circleci/config.yml modified: .defaults.env new file: .github/workflows/browser-stack.yml modified: README.md modified: admin/package-lock.json modified: doc/best-practices.md modified: habitat/plan.sh modified: package-lock.json modified: package.json modified: scripts/deploy.js modified: scripts/docker/run.sh modified: src/App.js modified: src/assets/images/app-logo-dark.png modified: src/assets/images/app-logo.png deleted: src/assets/images/cursor.svg modified: src/components/ambient-light.js modified: src/components/animation.js modified: src/components/audio-feedback.js modified: src/components/audio-params.js modified: src/components/avatar-audio-source.js modified: src/components/avatar-volume-controls.js modified: src/components/bone-visibility.js modified: src/components/camera-tool.js modified: src/components/cursor-controller.js modified: src/components/cylinder-texture.js modified: src/components/directional-light.js modified: src/components/emoji-hud.js modified: src/components/environment-map.js modified: src/components/follow-in-fov.js modified: src/components/gltf-model-plus.js modified: src/components/hoverable-visuals.js modified: src/components/hud-controller.js modified: src/components/ik-controller.js modified: src/components/layers.js modified: src/components/media-image.js modified: src/components/media-loader.js modified: src/components/media-pdf.js modified: src/components/media-video.js modified: src/components/name-tag.js modified: src/components/point-light.js modified: src/components/position-at-border.js deleted: src/components/scale-in-screen-space.js modified: src/components/scene-components.js new file: src/components/slice9.js modified: src/components/stats-plus.js modified: src/components/super-spawner.js modified: src/components/teleporter.js modified: src/components/text-button.js modified: src/components/tools/networked-drawing.js modified: src/components/troika-text.js modified: src/components/video-texture-target.js modified: src/components/virtual-gamepad-controls.css modified: src/components/virtual-gamepad-controls.js modified: src/gltf-component-mappings.js modified: src/hub.html modified: src/hub.js modified: src/loaders/HubsTextureLoader.js modified: src/react-components/avatar-preview.js modified: src/react-components/home/HomePage.js new file: src/react-components/input/ToolbarMicButton.js modified: src/react-components/layout/Header.js modified: src/react-components/layout/LoadingScreenLayout.js modified: src/react-components/layout/MobileNav.js modified: src/react-components/layout/Page.js modified: src/react-components/layout/PageContainer.js new file: src/react-components/misc/AppLogo.js modified: src/react-components/misc/LevelBar.js new file: src/react-components/misc/VolumeLevelBar.js modified: src/react-components/misc/useVolumeMeter.js modified: src/react-components/preferences-screen.js modified: src/react-components/room/AudioPopover.stories.js modified: src/react-components/room/AudioPopoverButton.js modified: src/react-components/room/AudioPopoverButtonContainer.js modified: src/react-components/room/AudioPopoverContent.js modified: src/react-components/room/AudioPopoverContentContainer.js modified: src/react-components/room/ExitedRoomScreen.js modified: src/react-components/room/ExitedRoomScreen.stories.js modified: src/react-components/room/ExitedRoomScreenContainer.js modified: src/react-components/room/LoadingScreen.js modified: src/react-components/room/LoadingScreen.stories.js modified: src/react-components/room/LoadingScreenContainer.js modified: src/react-components/room/MicSetupModal.js modified: src/react-components/room/MicSetupModal.stories.js modified: src/react-components/room/MicSetupModalContainer.js modified: src/react-components/room/RoomEntryModal.js modified: src/react-components/room/RoomEntryModal.stories.js modified: src/react-components/room/SharePopoverContainer.js modified: src/react-components/room/useResizeViewport.js modified: src/react-components/room/useSound.js modified: src/react-components/scene-ui.js modified: src/react-components/ui-root.js modified: src/storage/store.js modified: src/systems/audio-debug-system.js modified: src/systems/camera-system.js modified: src/systems/character-controller-system.js deleted: src/systems/enter-vr-button-system.js modified: src/systems/environment-system.js modified: src/systems/haptic-feedback-system.js modified: src/systems/hubs-systems.js modified: src/systems/menu-animation-system.js modified: src/systems/physics-system.js deleted: src/systems/render-manager-system.js deleted: src/systems/render-manager/hubs-batch-raw-uniform-group.js deleted: src/systems/scale-in-screen-space.js modified: src/systems/shadow-system.js modified: src/systems/sound-effects-system.js modified: src/systems/userinput/devices/vive-controller.js modified: src/systems/userinput/devices/windows-mixed-reality-controller.js modified: src/update-audio-settings.js modified: src/utils/detect-safari.js modified: src/utils/dpad.js modified: src/utils/fullscreen.js modified: src/utils/get-app-logo.js new file: src/utils/isHmc.js modified: src/utils/material-utils.js modified: src/utils/media-utils.js new file: src/utils/screen-orientation-utils.js modified: src/utils/three-utils.js modified: src/vendor/Water.js new file: test/browser-stack/common/browserstack.js new file: test/browser-stack/common/keep-alive.js new file: test/browser-stack/package-lock.json new file: test/browser-stack/package.json new file: test/browser-stack/tests/ie11.js modified: webpack.config.js --- .circleci/config.yml | 8 +- .defaults.env | 3 - .github/workflows/browser-stack.yml | 28 ++ README.md | 13 +- admin/package-lock.json | 36 +- doc/best-practices.md | 250 ++++++++--- habitat/plan.sh | 10 +- package-lock.json | 86 ++-- package.json | 9 +- scripts/deploy.js | 2 +- scripts/docker/run.sh | 15 +- src/App.js | 3 + src/assets/images/app-logo-dark.png | Bin 9033 -> 3982 bytes src/assets/images/app-logo.png | Bin 7680 -> 3497 bytes src/assets/images/cursor.svg | 1 - src/components/ambient-light.js | 3 +- src/components/animation.js | 71 ++- src/components/audio-feedback.js | 8 +- src/components/audio-params.js | 8 + src/components/avatar-audio-source.js | 21 +- src/components/avatar-volume-controls.js | 4 +- src/components/bone-visibility.js | 12 + src/components/camera-tool.js | 13 +- src/components/cursor-controller.js | 73 +++- src/components/cylinder-texture.js | 2 +- src/components/directional-light.js | 3 +- src/components/emoji-hud.js | 15 - src/components/environment-map.js | 2 +- src/components/follow-in-fov.js | 2 +- src/components/gltf-model-plus.js | 20 +- src/components/hoverable-visuals.js | 20 - src/components/hud-controller.js | 8 +- src/components/ik-controller.js | 2 +- src/components/layers.js | 7 +- src/components/media-image.js | 28 +- src/components/media-loader.js | 37 +- src/components/media-pdf.js | 13 +- src/components/media-video.js | 22 +- src/components/name-tag.js | 54 ++- src/components/point-light.js | 3 +- src/components/position-at-border.js | 2 +- src/components/scale-in-screen-space.js | 36 -- src/components/scene-components.js | 1 + src/components/slice9.js | 261 +++++++++++ src/components/stats-plus.js | 105 +++-- src/components/super-spawner.js | 11 +- src/components/teleporter.js | 211 ++++----- src/components/text-button.js | 4 +- src/components/tools/networked-drawing.js | 7 +- src/components/troika-text.js | 10 +- src/components/video-texture-target.js | 9 +- src/components/virtual-gamepad-controls.css | 6 +- src/components/virtual-gamepad-controls.js | 27 +- src/gltf-component-mappings.js | 33 +- src/hub.html | 125 ++---- src/hub.js | 10 - src/loaders/HubsTextureLoader.js | 2 +- src/react-components/avatar-preview.js | 5 +- src/react-components/home/HomePage.js | 13 +- .../input/ToolbarMicButton.js | 45 ++ src/react-components/layout/Header.js | 12 +- .../layout/LoadingScreenLayout.js | 9 +- src/react-components/layout/MobileNav.js | 8 - src/react-components/layout/Page.js | 6 - src/react-components/layout/PageContainer.js | 7 +- src/react-components/misc/AppLogo.js | 20 + src/react-components/misc/LevelBar.js | 23 +- src/react-components/misc/VolumeLevelBar.js | 37 ++ src/react-components/misc/useVolumeMeter.js | 17 +- src/react-components/preferences-screen.js | 97 +++-- .../room/AudioPopover.stories.js | 85 ++-- .../room/AudioPopoverButton.js | 49 +-- .../room/AudioPopoverButtonContainer.js | 22 +- .../room/AudioPopoverContent.js | 26 +- .../room/AudioPopoverContentContainer.js | 15 +- src/react-components/room/ExitedRoomScreen.js | 5 +- .../room/ExitedRoomScreen.stories.js | 4 +- .../room/ExitedRoomScreenContainer.js | 1 - src/react-components/room/LoadingScreen.js | 4 +- .../room/LoadingScreen.stories.js | 5 +- .../room/LoadingScreenContainer.js | 3 +- src/react-components/room/MicSetupModal.js | 33 +- .../room/MicSetupModal.stories.js | 97 +++-- .../room/MicSetupModalContainer.js | 15 +- src/react-components/room/RoomEntryModal.js | 10 +- .../room/RoomEntryModal.stories.js | 3 +- .../room/SharePopoverContainer.js | 2 +- .../room/useResizeViewport.js | 78 ++-- src/react-components/room/useSound.js | 6 +- src/react-components/scene-ui.js | 12 +- src/react-components/ui-root.js | 2 - src/storage/store.js | 8 +- src/systems/audio-debug-system.js | 32 +- src/systems/camera-system.js | 57 ++- src/systems/character-controller-system.js | 2 +- src/systems/enter-vr-button-system.js | 34 -- src/systems/environment-system.js | 40 +- src/systems/haptic-feedback-system.js | 2 +- src/systems/hubs-systems.js | 6 - src/systems/menu-animation-system.js | 6 +- src/systems/physics-system.js | 2 +- src/systems/render-manager-system.js | 29 -- .../hubs-batch-raw-uniform-group.js | 140 ------ src/systems/scale-in-screen-space.js | 39 -- src/systems/shadow-system.js | 8 +- src/systems/sound-effects-system.js | 10 +- .../userinput/devices/vive-controller.js | 2 +- .../windows-mixed-reality-controller.js | 2 +- src/update-audio-settings.js | 33 +- src/utils/detect-safari.js | 6 +- src/utils/dpad.js | 4 +- src/utils/fullscreen.js | 2 +- src/utils/get-app-logo.js | 4 +- src/utils/isHmc.js | 7 + src/utils/material-utils.js | 3 +- src/utils/media-utils.js | 14 +- src/utils/screen-orientation-utils.js | 105 +++++ src/utils/three-utils.js | 21 +- src/vendor/Water.js | 4 +- test/browser-stack/common/browserstack.js | 52 +++ test/browser-stack/common/keep-alive.js | 11 + test/browser-stack/package-lock.json | 408 ++++++++++++++++++ test/browser-stack/package.json | 9 + test/browser-stack/tests/ie11.js | 32 ++ webpack.config.js | 139 +++--- 125 files changed, 2412 insertions(+), 1412 deletions(-) create mode 100644 .github/workflows/browser-stack.yml delete mode 100644 src/assets/images/cursor.svg delete mode 100644 src/components/scale-in-screen-space.js create mode 100644 src/components/slice9.js create mode 100644 src/react-components/input/ToolbarMicButton.js create mode 100644 src/react-components/misc/AppLogo.js create mode 100644 src/react-components/misc/VolumeLevelBar.js delete mode 100644 src/systems/enter-vr-button-system.js delete mode 100644 src/systems/render-manager-system.js delete mode 100644 src/systems/render-manager/hubs-batch-raw-uniform-group.js delete mode 100644 src/systems/scale-in-screen-space.js create mode 100644 src/utils/isHmc.js create mode 100644 src/utils/screen-orientation-utils.js create mode 100644 test/browser-stack/common/browserstack.js create mode 100644 test/browser-stack/common/keep-alive.js create mode 100644 test/browser-stack/package-lock.json create mode 100644 test/browser-stack/package.json create mode 100644 test/browser-stack/tests/ie11.js diff --git a/.circleci/config.yml b/.circleci/config.yml index b6b55feced..fd2bd05cea 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,9 +14,9 @@ jobs: - checkout - restore_cache: keys: - - v1-dependencies-{{ checksum "package-lock.json" }} - - v1-dependencies- - - run: npm ci + - v1-dependencies-{{ checksum "package-lock.json" }} + - v1-dependencies- + - run: npm ci --loglevel verbose - save_cache: paths: - node_modules @@ -46,4 +46,4 @@ workflows: - build_and_test filters: branches: - only: room-ui-redesign \ No newline at end of file + only: room-ui-redesign diff --git a/.defaults.env b/.defaults.env index 5a0293b808..a3f9f9fa5f 100644 --- a/.defaults.env +++ b/.defaults.env @@ -23,9 +23,6 @@ NON_CORS_PROXY_DOMAINS="hubs.local,dev.reticulum.io" # The root URL under which Hubs expects static assets to be served. BASE_ASSETS_PATH=/ -# The default scene to use. Note the example scene id is only available on dev.reticulum.io -DEFAULT_SCENE_SID="JGLt8DP" - # Uncomment to load the app config from the reticulum server in development. # Useful when testing the admin panel. # LOAD_APP_CONFIG=true diff --git a/.github/workflows/browser-stack.yml b/.github/workflows/browser-stack.yml new file mode 100644 index 0000000000..139f6a0eb4 --- /dev/null +++ b/.github/workflows/browser-stack.yml @@ -0,0 +1,28 @@ +name: browser-stack + +on: + schedule: + - cron: '0 18 * * *' + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup node.js + uses: actions/setup-node@v3 + with: + node-version: 16.x + + - name: Run browser-stack test + env: + BROWSER_STACK_USER_NAME: ${{ secrets.BROWSER_STACK_USER_NAME }} + BROWSER_STACK_ACCESS_KEY: ${{ secrets.BROWSER_STACK_ACCESS_KEY }} + run: | + cd test/browser-stack + npm ci + npm test diff --git a/README.md b/README.md index bc41de9e3d..9d48d0d7b4 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,14 @@ npm ci npm run dev ``` -Then visit https://localhost:8080 (note: HTTPS is required, you'll need to accept the warning for the self-signed SSL certificate) +The backend dev server is configured with CORS to only accept connections from "hubs.local:8080", so you will need to access it from that host. To do this, you likely want to add "hubs.local" and "hubs-proxy.local" to the [local "hosts" file](https://phoenixnap.com/kb/how-to-edit-hosts-file-in-windows-mac-or-linux) on your computer: + +``` +127.0.0.1 hubs.local +127.0.0.1 hubs-proxy.local +``` + +Then visit https://hubs.local:8080 (note: HTTPS is required, you'll need to accept the warning for the self-signed SSL certificate) > Note: When running the Hubs client locally, you will still connect to the development versions of our [Janus WebRTC](https://github.com/mozilla/janus-plugin-sfu) and [reticulum](https://github.com/mozilla/reticulum) servers. These servers do not allow being accessed outside of localhost. If you want to host your own Hubs servers, please check out [Hubs Cloud](https://hubs.mozilla.com/docs/hubs-cloud-intro.html). @@ -47,7 +54,7 @@ Read our [contributor guide](./CONTRIBUTING.md) to learn how you can submit bug We're also looking for help with localization. The Hubs redesign has a lot of new text and we need help from people like you to translate it. Follow the [localization docs](./src/assets/locales/README.md) to get started. -Contributors are expected to abide by the project's [Code of Conduct](./CODE_OF_CONDUCT.md) and to be respectful of the project and people working on it. +Contributors are expected to abide by the project's [Code of Conduct](./CODE_OF_CONDUCT.md) and to be respectful of the project and people working on it. ## Additional Resources @@ -63,4 +70,4 @@ Mozilla and Hubs believe that privacy is fundamental to a healthy internet. Read ## License -Hubs is licensed with the [Mozilla Public License 2.0](./LICENSE) \ No newline at end of file +Hubs is licensed with the [Mozilla Public License 2.0](./LICENSE) diff --git a/admin/package-lock.json b/admin/package-lock.json index 461f7dd643..704009ff96 100644 --- a/admin/package-lock.json +++ b/admin/package-lock.json @@ -1504,7 +1504,7 @@ "dev": true }, "aframe": { - "version": "github:mozillareality/aframe#05b980199e5e79e7dde1f6acb63ca74660ceee33", + "version": "github:mozillareality/aframe#deba2d4eedda664bb7d87bf4b3c9fcf25f5298e1", "from": "github:mozillareality/aframe#hubs/master", "requires": { "custom-event-polyfill": "^1.0.6", @@ -6703,8 +6703,6 @@ "@mozillareality/easing-functions": "^0.1.1", "@mozillareality/three-batch-manager": "github:mozillareality/three-batch-manager#master", "aframe": "github:mozillareality/aframe#hubs/master", - "aframe-rounded": "^1.0.3", - "aframe-slice9-component": "^1.0.0", "ammo-debug-drawer": "github:infinitelee/ammo-debug-drawer", "ammo.js": "github:mozillareality/ammo.js#hubs/master", "animejs": "github:mozillareality/anime#hubs/master", @@ -11236,16 +11234,6 @@ } } }, - "aframe-rounded": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/aframe-rounded/-/aframe-rounded-1.0.3.tgz", - "integrity": "sha512-OQlwyK/U5ISEoEaZCOxCzkfqq6F1coUKXQuwkWMAmRmvKyAfrEyX63G8c87aPvZO35z+PItObcpYNtBmjk4SBQ==" - }, - "aframe-slice9-component": { - "version": "1.0.0", - "resolved": "https://registry.yarnpkg.com/aframe-slice9-component/-/aframe-slice9-component-1.0.0.tgz", - "integrity": "sha1-+w+EQdrdHosRzCRRK6eqaS1iK+E=" - }, "ajv": { "version": "6.5.2", "resolved": "https://registry.yarnpkg.com/ajv/-/ajv-6.5.2.tgz", @@ -14979,23 +14967,6 @@ "stream-shift": "^1.0.0" } }, - "easyrtc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/easyrtc/-/easyrtc-1.1.0.tgz", - "integrity": "sha1-9Ek39xMsuLW6jgvBzD48zEcqPvQ=", - "requires": { - "async": "0.2.x", - "colors": "*", - "underscore": "1.5.x" - }, - "dependencies": { - "async": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" - } - } - }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -18734,11 +18705,10 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "networked-aframe": { - "version": "github:mozillareality/networked-aframe#f4b1f1ccc8871149be2e3aaa24ca665e481c716a", + "version": "github:mozillareality/networked-aframe#50184f495ff1f77d0e6fe3777d73092784e3aee3", "from": "github:mozillareality/networked-aframe#master", "requires": { - "buffered-interpolation": "^0.2.5", - "easyrtc": "1.1.0" + "buffered-interpolation": "^0.2.5" } }, "new-array": { diff --git a/doc/best-practices.md b/doc/best-practices.md index b6de1b1dec..20a337c982 100644 --- a/doc/best-practices.md +++ b/doc/best-practices.md @@ -1,74 +1,64 @@ This is a set of random little best practices we have come to endorse over the years. They are examples of things that have often come up in PR reviews or patterns we have started to solidify on. There are plenty of counter examples of these in the codebase, they are probably mostly unintentional. Also, most of these are more suggestions than hard rules. -These pertain mostly to the "in room experince" or "game" code. For best practices in the React UI code, see [UI Best Practices](ui-best-practices.md) +These pertain mostly to the "in room experience" or "engine" code. For best practices in the React UI code, see [UI Best Practices](ui-best-practices.md) -### If a method doesn't use `this` it shouldn't be a method +## Avoid Misdirection +### if you have a setter/getter that just directly sets/gets a value, consider not having it instead -Prefer a standalone function instead. +This avoids an extra layer of indirection and makes the code easier to interpret. If the goal is "in case we need to do other things when this changes later", see [YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it). Setters and getters also have added overhead in javascript vs raw values. Don't: ```js class Foo { - plusOne(x) { - return x + 1; + constructor() { + this._val = 1; } - bar() { - console.log(this.plusOne(3)); + + get val() { + return this._val; + } + + set val(newVal) { + this._val = newVal; } } ``` - Do: ```js -function plusOne(x) { - return x + 1; -} class Foo { - bar() { - console.log(plusOne(3)); + constructor() { + this.val = 1; } } ``` -### Don't allocate objects every frame. +### If a method doesn't use `this` it shouldn't be a method -If you see `new` in a tick function, you should be suspicious. This is especially common for ThreeJS objects like Vectors, Quaternions, and Matrices, try to re-use them instead to avoid GC. If you find yourself allocating a whole bunch of something, consider using a pool. +Prefer a standalone function instead. Doing so instantly lets the reader know they don't have to be looking for some hidden state. Don't: ```js -AFRAME.registerComponent("bad-example", { - tick(dt) { - const tempVec = new THREE.Vector3(); - this.el.object3D.getWorldPosition(tempVec); - // do something with tempVec +class Foo { + plusOne(x) { + return x + 1; + } + bar() { + console.log(this.plusOne(3)); } } ``` + Do: ```js -AFRAME.registerComponent("example", { - tick: (function { - const tempVec = new THREE.Vector3(); - return function(dt) { - this.el.object3D.getWorldPosition(tempVec); - // do something with tempVec - }; - })() +function plusOne(x) { + return x + 1; } -``` - -or - -```js -const tempVec = new THREE.Vector3(1, 2, 3); - -AFRAME.registerComponent("example", { - tick() { - this.el.object3D.getWorldPosition(tempVec); - // do something with tempVec +class Foo { + bar() { + console.log(plusOne(3)); } } ``` @@ -76,7 +66,7 @@ AFRAME.registerComponent("example", { ### Only store things on `this` that are actually persistent state. -If you don't care about the state between frames and are only re-using it for GC reasons (see above point) use temp reusable variables, use a closure or variable defined at the root of the file instead. +If you don't care about the state between frames and are only re-using it for [GC reasons](#dont-allocate-objects-every-frame) use a closure or variable defined at the root of the file instead. Don't: ```js @@ -118,43 +108,95 @@ class Foo { } ``` -### if you have a setter/getter that just directly sets/gets a value, consider not having it instead +### Prefer calling functions to emitting events -Don't +Events obfuscate code flow and coupling and should only be used when the emitter of the event is truly decoupled from things wishing ot subscribe to the event. Prefer direct function calls when the emitter and subscriber are inherently coupled. + +Don't: ```js -class Foo { - constructor() { - this._val = 1; - } - - get val() { - return this._val; - } +function onKeyDown(e) + clearTimeout(keyTimout); + keyTimout = setTimeout(() => scene.emit("action_typing_ended"), 500); + scene.emit("action_typing_started"); +} - set val(newVal) { - this._val = newVal; +scene.addEventListener("action_typing_started", () => window.APP.hubChannel.beginTyping()); +scene.addEventListener("action_typing_ended", () => window.APP.hubChannel.endTyping()); +``` + +Do: +```js +function onKeyDown(e) + clearTimeout(keyTimout); + keyTimout = setTimeout(() => window.APP.hubChannel.endTyping(), 500); + window.APP.hubChannel.beginTyping() +} +``` + +### Don’t fail silently. + +Always at least log a warning or error to the console when something unexpected happens + +### Avoid defensive coding + +Related to no failing silently; Do not guard against conditions that should not occur or indicate problems that are the responsibility of some other code. Adding unnecessary guards will force future readers of the code to have to try and figure out what cases that guard is intending to handle. Also, as with failing silently, unnecessary guards can lead to masking of serious issues. + + +Don't: +```js +if(window.APP && window.APP.shouldAlwaysExist && window.APP.shouldAlwaysExist.doSomething) { + window.APP.shouldAlwaysExist.doSomething(); +} +``` + +Do: +```js +window.APP.shouldAlwaysExist.doSomething(); +``` + + +### Prefer timestamps to timeouts + +Prefer saving and comparing timestamps to using `setTimeout`, especially for code that is already running in a tick loop. This allows for a much more predictable code flow. + +Don't: +```js + +tick() { + if (this.canDoThingNow && userinput.get(DO_THING_PATH)) { + this.canDoThingNow = false; + doThing(); + setTimeout(() => { + this.canDoThingNow = true; + }, DO_THING_COOLDOWN); } } + ``` -Do +Do: ```js -class Foo { - constructor() { - this.val = 1; + +tick(time) { + if (time > this.lastDidThing + DO_THING_COOLDOWN && userinput.get(DO_THING_PATH)) { + this.lastDidThing = time; + doThing(); } } + ``` -### Avoid unnecessary AFrame components. -AFrame component instantiation can be tricky to deal with. When possible try and limit entities and components to the outer edge of the behavior you are working on. The only things that HAVE to be in components right now things with networked state. Things serialized in a glTF are generally components as well, but that is less of a strict requirement. For everything else, consider if that behavior or state might better live elsewhere. Store state in associative collections. Mutate state in regular functions. +## Avoid aframe's pitfalls +### Avoid unnecessary AFrame components and entities + +AFrame component instantiation can be tricky to deal with. When possible try and limit entities and components to the outer edge of the behavior you are working on. The only things that HAVE to be in components right now things with networked state. Things serialized in a glTF are generally components as well, but that is less of a strict requirement. For everything else, consider if that behavior or state might better live elsewhere. Store state in associative collections. Mutate state in regular functions. Also, consider setting up object hierarchies directly in a function using ThreeJS objects rather than in hub.html. ### Avoid repeatedly running querySelectors every frame. Prefer storing a reference when possible. -don't +Don't: ```js AFRAME.registerComponent("bad-example", { tick() { @@ -164,7 +206,7 @@ AFRAME.registerComponent("bad-example", { } ``` -do +Do: ```js AFRAME.registerComponent("bad-example", { init() { @@ -177,6 +219,90 @@ AFRAME.registerComponent("bad-example", { ``` -### Don’t fail silently. +### Avoid using aframe's THREE property components -Always at least log a warning or error to the console when something unexpected happens +Do not use aframe's `position`, `rotation`, `scale`, or `visible` components, and instead just directly modify the Object3D properties. These add an additional layer of indirection and performance overhead. + +Don't: +```js +el.setAttribute("visible", false); +el.setAttribute("position", {x: 0, y: 1, z: 0}); +``` + +Do: +```js +el.object3D.visible = false; +el.object3D.position.set(0,1,0); +``` + +### Prefer storing Object3D references to aframe entities + +As we continue to work to phase out our use of aframe we should prefer storing things in terms of Object3D references rather than aframe entity references. This is especially true if we only ever use the entity reference to get at the Object3D (which can often be the case when abiding by the above point). If the reference is heavily being used to access aframe functionality then it may make sense to keep as an entity reference. By convention all entity references should have the `El` suffix. + +Don't: +```js +const head = document.querySelector("#avatar-head"); +head.object3D.visible = false; +head.object3D.position.set(0,1,0); +head.setAttribute("some-component", "value"); + +const onlyUsingAframe = document.querySelector("#foo"); +onlyUsingAframe.setAttribute("some-component", "value"); +console.log(onlyUsingAframe.components["some-component"].data.value); +``` + +Do: +```js +const head = document.querySelector("#avatar-head").object3D; +head.visible = false; +head.position.set(0,1,0); +head.el.setAttribute("some-component", "value"); + +const onlyUsingAframeEl = document.querySelector("#foo"); +onlyUsingAframeEl.setAttribute("some-component", "value"); +console.log(onlyUsingAframeEl.components["some-component"].data.value); +``` + +## Avoid JS pitfalls +### Don't allocate objects every frame. + +If you see `new` in a tick function, you should be suspicious. This is especially common for ThreeJS objects like Vectors, Quaternions, and Matrices, try to re-use them instead to avoid GC. If you find yourself allocating a whole bunch of something, consider using a pool. + +Don't: +```js +AFRAME.registerComponent("bad-example", { + tick(dt) { + const tempVec = new THREE.Vector3(); + this.el.object3D.getWorldPosition(tempVec); + // do something with tempVec + } +} +``` + +Do: +```js +AFRAME.registerComponent("example", { + tick: (function { + const tempVec = new THREE.Vector3(); + return function(dt) { + this.el.object3D.getWorldPosition(tempVec); + // do something with tempVec + }; + })() +} +``` + +or + +```js +const tempVec = new THREE.Vector3(1, 2, 3); + +AFRAME.registerComponent("example", { + tick() { + this.el.object3D.getWorldPosition(tempVec); + // do something with tempVec + } +} +``` + + diff --git a/habitat/plan.sh b/habitat/plan.sh index a0de478616..8b1c0ae4c8 100644 --- a/habitat/plan.sh +++ b/habitat/plan.sh @@ -7,10 +7,10 @@ pkg_license=('MPLv2') pkg_description="Duck-powered web-based social VR." pkg_upstream_url="https://hubs.mozilla.com/" pkg_build_deps=( - core/coreutils - core/bash + core/coreutils/8.32/20210826054709 + core/bash/5.1/20210826055113 core/node10/10.16.1/20190801173856 # Latest node10 fails during npm ci due to a permissions error creating tmp dir - core/git + core/git/2.31.0/20211016175551 ) pkg_deps=( @@ -20,6 +20,10 @@ pkg_deps=( do_build() { ln -s "$(hab pkg path core/coreutils)/bin/env" /usr/bin/env + # Avoid using git:// and ssh + git config --global url."https://github.com/".insteadOf git@github.com: + git config --global url."https://".insteadOf git:// + # main client npm ci --verbose --no-progress diff --git a/package-lock.json b/package-lock.json index 0facb9c23d..ea70551981 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5686,6 +5686,18 @@ "to-regex-range": "^5.0.1" } }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, "glob-parent": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", @@ -5695,6 +5707,12 @@ "is-glob": "^4.0.1" } }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -6822,10 +6840,6 @@ "resolved": "https://registry.npmjs.org/@mozillareality/easing-functions/-/easing-functions-0.1.1.tgz", "integrity": "sha512-rApZ0OOqreMULwHZOy+fB9wRLnikelE8pMbqUmtLBhT6OSKp5dz/7pA/EjnFXJDQBR/EgZUa3vwbFlTZnUgSlw==" }, - "@mozillareality/three-batch-manager": { - "version": "github:mozillareality/three-batch-manager#0696524807536840eba156e84feef78e83fb297a", - "from": "github:mozillareality/three-batch-manager#master" - }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -10446,6 +10460,26 @@ } } }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + } + } + }, "fsevents": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz", @@ -15800,7 +15834,7 @@ "dev": true }, "aframe": { - "version": "github:mozillareality/aframe#033ec6571ff6ec2c9162e26ff4e23ee2e65afc12", + "version": "github:mozillareality/aframe#2cd12e184bbf7e1ff7d2100d66064f50403405c1", "from": "github:mozillareality/aframe#hubs/master", "requires": { "custom-event-polyfill": "^1.0.6", @@ -15820,15 +15854,6 @@ } } }, - "aframe-rounded": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/aframe-rounded/-/aframe-rounded-1.0.3.tgz", - "integrity": "sha512-OQlwyK/U5ISEoEaZCOxCzkfqq6F1coUKXQuwkWMAmRmvKyAfrEyX63G8c87aPvZO35z+PItObcpYNtBmjk4SBQ==" - }, - "aframe-slice9-component": { - "version": "github:takahirox/aframe-slice9-component#2ec0810cbf23f487a10c767d24dee6d5b7ede77f", - "from": "github:takahirox/aframe-slice9-component#AlphaTest" - }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -23056,21 +23081,20 @@ } }, "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", + "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", "dev": true, "requires": { - "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "dependencies": { "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true } } @@ -26580,7 +26604,7 @@ } }, "lib-hubs": { - "version": "github:mozillareality/lib-hubs#592695f0fd098adbf7cc88d14391bd97a482f78a", + "version": "github:mozillareality/lib-hubs#f897132e88ba0c554424d911b2a56a47b6732c7c", "from": "github:mozillareality/lib-hubs#master" }, "lie": { @@ -27745,12 +27769,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", - "dev": true - }, "negotiator": { "version": "0.6.1", "resolved": "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz", @@ -27770,7 +27788,7 @@ "dev": true }, "networked-aframe": { - "version": "github:mozillareality/networked-aframe#a691b90cd1c817283fbd4ce32f63b0b184311f4c", + "version": "github:mozillareality/networked-aframe#2c3c394e3f12a7d2cc68b193c0f7c94d709a46e8", "from": "github:mozillareality/networked-aframe#master", "requires": { "buffered-interpolation": "github:Infinitelee/buffered-interpolation#5bb18421ebf2bf11664645cdc7a15bd77ee2156b" @@ -36400,8 +36418,8 @@ "dev": true }, "three": { - "version": "github:mozillareality/three.js#ab5d5824770dbcf6921b21a4a2faaae961f67328", - "from": "github:mozillareality/three.js#hubs-patches-133" + "version": "github:mozillareality/three.js#d96f3cf69c7303db807cabaa2acba09d648e5e96", + "from": "github:mozillareality/three.js#hubs-patches-141" }, "three-ammo": { "version": "github:infinitelee/three-ammo#db3ad1104b258d963ca66165f9fbed4013faa32a", @@ -36413,9 +36431,9 @@ "integrity": "sha512-veRJRj0mY2rBj9yRZVg/E98wd6e7AzqzTq/+6ispY6m50gYuGitUNih2dRQzbkMcwRECLURvuudb9BFW3/swAw==" }, "three-pathfinding": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/three-pathfinding/-/three-pathfinding-0.14.1.tgz", - "integrity": "sha512-lBXt+GLlgCz/ppakGDj6L/JJH2JmZ8ZFiFHqey4v47V8SrzhKRuXfrsHo6IPF1Z/MDki/ahIf7uquW+jy/3hAg==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/three-pathfinding/-/three-pathfinding-1.1.0.tgz", + "integrity": "sha512-GkQ5/HAVZZ/BT1kLvqf0KOmyDYrBtGk6krv0YX/0qU2sCv6a9epJZXNgczzZDQzX8+s676Uc4SKTo5djKpF3TA==" }, "three-to-ammo": { "version": "github:infinitelee/three-to-ammo#9e68a3bbe500988f2dc0096f78c12dabbdaf5548", diff --git a/package.json b/package.json index 640898f43f..dbe1c16981 100644 --- a/package.json +++ b/package.json @@ -77,11 +77,8 @@ "@fortawesome/free-solid-svg-icons": "^5.2.0", "@fortawesome/react-fontawesome": "^0.1.0", "@mozillareality/easing-functions": "^0.1.1", - "@mozillareality/three-batch-manager": "github:mozillareality/three-batch-manager#master", "@popperjs/core": "^2.4.4", "aframe": "github:mozillareality/aframe#hubs/master", - "aframe-rounded": "^1.0.3", - "aframe-slice9-component": "github:takahirox/aframe-slice9-component#AlphaTest", "ammo-debug-drawer": "github:infinitelee/ammo-debug-drawer", "ammo.js": "github:mozillareality/ammo.js#hubs/master", "animejs": "github:mozillareality/anime#hubs/master", @@ -142,10 +139,10 @@ "sdp-transform": "^2.14.1", "semver": "^7.3.2", "socket.io-client": "^4.0.0", - "three": "github:mozillareality/three.js#hubs-patches-133", + "three": "github:mozillareality/three.js#hubs-patches-141", "three-ammo": "github:infinitelee/three-ammo", "three-mesh-bvh": "^0.3.7", - "three-pathfinding": "^0.14.1", + "three-pathfinding": "^1.1.0", "three-to-ammo": "github:infinitelee/three-to-ammo", "troika-three-text": "^0.45.0", "use-clipboard-copy": "^0.1.2", @@ -191,6 +188,7 @@ "esm": "^3.2.5", "fast-plural-rules": "0.0.3", "file-loader": "^1.1.10", + "fs-extra": "^10.0.1", "glob": "^7.1.6", "html-loader": "^0.5.5", "html-webpack-plugin": "^4.2.0", @@ -200,7 +198,6 @@ "localstorage-memory": "^1.0.3", "mediasoup-client": "^3.6.29", "mini-css-extract-plugin": "^0.8.0", - "ncp": "^2.0.0", "node-fetch": "^2.6.1", "npm-scripts-info": "0.3.9", "ora": "^4.0.2", diff --git a/scripts/deploy.js b/scripts/deploy.js index d2fa80e3fd..7a6ce3c880 100644 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -1,7 +1,7 @@ import { createReadStream, readFileSync, existsSync, unlinkSync } from "fs"; import { exec } from "child_process"; import rmdir from "rimraf"; -import ncp from "ncp"; +import { copy } from "fs-extra"; import tar from "tar"; import ora from "ora"; import FormData from "form-data"; diff --git a/scripts/docker/run.sh b/scripts/docker/run.sh index 6e465d29ed..40cf745cf4 100644 --- a/scripts/docker/run.sh +++ b/scripts/docker/run.sh @@ -1,8 +1,14 @@ -# TODO: need a better one -healthcheck(){ - while true; do (echo -e 'HTTP/1.1 200 OK\r\n\r\n 1') | nc -lp 1111 > /dev/null; done -} +export turkeyCfg_postgrest_server="" +export turkeyCfg_thumbnail_server="nearspark.reticulum.io" +export turkeyCfg_base_assets_path="https://$SUB_DOMAIN-assets.$DOMAIN/hubs/" +export turkeyCfg_non_cors_proxy_domains="$SUB_DOMAIN.$DOMAIN,$SUB_DOMAIN-assets.$DOMAIN" +export turkeyCfg_reticulum_server="$SUB_DOMAIN.$DOMAIN" +export turkeyCfg_cors_proxy_server="$SUB_DOMAIN-cors.$DOMAIN" +export turkeyCfg_ga_tracking_id="" +export turkeyCfg_shortlink_domain="$SUB_DOMAIN.$DOMAIN" +export turkeyCfg_ita_server="" +export turkeyCfg_sentry_dsn="" find /www/hubs/ -type f -name *.html -exec sed -i "s/{{rawhubs-base-assets-path}}\//https:\/\/${SUB_DOMAIN}-assets.${DOMAIN}\/hubs\//g" {} \; find /www/hubs/ -type f -name *.html -exec sed -i "s/{{rawhubs-base-assets-path}}/https:\/\/${SUB_DOMAIN}-assets.${DOMAIN}\/hubs\//g" {} \; @@ -15,5 +21,4 @@ for f in /www/hubs/pages/*.html; do [[ $var == $prefix* ]] && sed -i "s/$anchor/ $anchor/" $f; done done -healthcheck & nginx -g "daemon off;" diff --git a/src/App.js b/src/App.js index f6b2dbdb69..de56893bcc 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,6 @@ import Store from "./storage/store"; import MediaSearchStore from "./storage/media-search-store"; +import qsTruthy from "./utils/qs_truthy"; export class App { constructor() { @@ -54,6 +55,8 @@ export class App { renderer.setPixelRatio(window.devicePixelRatio); + renderer.debug.checkShaderErrors = qsTruthy("checkShaderErrors"); + // These get overridden by environment-system but setting to the highly expected defaults to avoid any extra work renderer.physicallyCorrectLights = true; renderer.outputEncoding = THREE.sRGBEncoding; diff --git a/src/assets/images/app-logo-dark.png b/src/assets/images/app-logo-dark.png index 3f04a2e1f3d52437628abdccbee745b1f7e6a257..0eadb340674b51a44910f955b0961eea2e38afba 100644 GIT binary patch literal 3982 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 Uxzc0y_nG731&9s7`c~rq0NhvS+yDRo literal 9033 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 diff --git a/src/assets/images/app-logo.png b/src/assets/images/app-logo.png index 75a349290be1c412adf007e2b0303ea8c2234659..f36fbaf3f28ea6bca0fbc5e3578c385e55555c28 100644 GIT binary patch literal 3497 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 XDa!Af|HBkk00000NkvXXu0mjf)`PX{ literal 7680 zcmZWu2{@GB+kQoqHI#k%Wr>D_>_m&m-eAO#2HD1%B+;N`4$L(riB zaGZ365&Soer7l8{aHoOpMa#gfm2r%33YCuO`%FNM*vi$otEc3176LjHT&MWbN8?vA zrp~C|Wmpr+xFe^z_2qX}ckZg9ZxvH?Lb>f{9$|X!dylF<6+9ObGB!U6PjNV8KBj)b zzT>gjk;B!;-F8+JCpONV?d^YCeg9x{btCH1?pvjwJM&wZoXN7%h6X`h@MpQRXxKV^w1Pkmxa#m!D66oM{-~7^qIHF1L?FSXyt_I#P^+{)7bRa@f0Fi)VVc@g9#%#~ z6+%m-H|Vc3MX2pDcPNa7qPVA&_ zo=m(dk(ZhMZd(313!bNsb(QLgw?tL=FNo+Sf#$_hQRbmmN zqoev`0SV}Gi&?)qU5sG7p7N4y*--bNHn22vd{Co(4^mkmJ{CzlX5vB$zX2Ppp`pUY zd;>q9|BWPfV#yZ7fMaMcMQp%-po$`?XE-@IQ_a-}a68i7sPx!B=O@h72XQ;a(WrD7 zu?g|Q;w(>Gj5^hNl8ciQE+(nZ5Pp%2R2E50e{0pFe}z#~*$^Xmgh6R(^qRE9;;5g= z2o3)FEYqT5#O2(lE(C!i3|%5Oq|cdKDfVi;0f@X3>Q*Qz4>>Y@3!|t#(XqNM)a~rMe@6R{} z?aQuLS;gz+@1aau9n15xPE!fJU;9gZ#FL_nebjGDa>6yzyCu;Gf@Ix)r!eS1AY3@! zddB=*#-gp9@?~8j?DoQYVTtlJ7k8TTsmKk?`FNgsL$c_46CSpzH)&u)$k57g`xnppn!!yTddKzrbLze4`J zWzZ--xs51J{wFDUVYm~tlx&U<)cQosjnMZ~57t=TvBqFD-p1nddN+;NULfHI>EsJ!s;E#jj2`q7aYrLNuZfK0I&++vw6;~%L;z1N zZv9gBAp5Gxq7hBkqhTDNU8;dlL?6z_se(`60S!>j8)-%9Thd!kTIm4L21dHTSqeDQ z24@A4q=aa241wckY>b*>=Iy(0{GYvAp$36&-ITgsndrLnEk{es7-E(-c7Wtul@MFB zHMM+k{{JSUm;g_>Fz1})ZFel-RC=0WTJ1Bt*T__(51ep0(1qK8gdAhpKj}!w*i`|^ zD4wmW#Kjt*Ybj5(o~*Qz%rkc^7thPXcI>7RVVtSlAR-vn5a(lq=YrQcfm@a4rc?r# z2JO5MMLSPJPU3p7ym9SqSwZ4hpVXCq#8QfkE7Vri)dl>EE}oGVcYk={LRAx6#HG9@ zgrETpODi%mSw|pj6esF4o{YiMW~ zHZ|+&=060O!R+OTi&qx`t^GP_&q<{hA7gTedEK@Pxej2^(=d^W@LEG>X` zm%!ZTRnGi7C-1@ME4p7dQ3JJTeEXXgW&zjroXB#k* zzw(l49po7h5adU2Es-(`(eYn7>e0>xKS_&~^vr^Ql>L`trZRvjiO)2|A>twcjZbjX zW~flWgX{=0Qa1w7Th|5)G}9VTs?QDdFNRoIS;@0PywEhoA~ZEL(66n|q}eUmCPZ>2 zJp#klefs{jmGe{S__kNs*e2)Cr*JpG>?vQxB7TQ71LI+jxnHi8x`+f)LU9*B|L~ISf2Zj*P$apv6M+2W zcyRAsxm97`yWO`~JjJD>}=^na+Q zf->A4z>|7y=8Jg-8Xw6E}2gVAzk(K5>38)kSst7lK za2I(~*u z!J6D_R|jf@)Qga9D1BwAbYvec~`Bm<;jdNUS&K$G06X;MYg_HIx-&92`<;Oob|Ln=?@!IO+IOY4CY zhHExJ!Yh;1xErkqBV;nLolm-Ey6vw@lxBenr0d4%irKkGb{z^APSr&daGP<8z^_-X ziouLxJTBwOHRI#s20-+q%_~3N;Wssq|5W6(}?8Dw+u1tN|x3?L>Y1 zCm%zjb#}loNCw2v{m;=+O1-G!*vflPO~ZSB15#yg>ZtC2BDqe;I>0FJbU#*fVn6 zn>Vx;epE|2A^jb96q?sRR83-?p{%Ap6he{AwJ5Y9|wN8hTP%UG1clw5hV^XBd zX1){}sRbv7fI;NP3(2yIC_W9$3#8~i7gI{h)@9HlW>RU~*>K}l7$ z)v=<$tJAS!>h%lncady@pZoj6_xD4D)O?}t^t1XF7TicAQqNx~SzSLR4Nq;yC8DC{qb?SIRs&?;h(}oEM3a-CnISWC9r8ihoK1kS$8*qUF z4r+0!x>(dtx~1r_OsSyM-WG?Z`t(I3WV>gCBy!T*UOsG{zKRQ5T3kda3AFbvb>G}q z?0CW--mqFPvxNHUtM;mYVQOQ?cW?W)?2URQr}Cw>HD2YiO!EVgSA8i)m~K)MsKuOL zv5GVO&+3O}=o2?#CH3_hdo#Sv7hGMX*hRIU#POcZtEu7D3|MYy+?rr9NN$TTulsrw z(h}il+9j9Gkgwlt&p7A${e#p?6o7ww^?l`ZgFZd6ho<&?=I z5-Lo#TmD6lV_OfJcq+chMwW;C>zk*FVADqS8U)n?|F3m-cP9aA&_NCN)=ZhvDlgao zp*p6f{z?$mjNIJ~;;hur)TD!&cY1u1C%BU(9EK~Mp)l&w(B-nB)@k1s#|rR!Rakgf zh(sZa9tzr+*%jc|=^~mAHY5%L5myPLI^44H%U@$GR0OgO7-<5ng z!y&p5zhd~^yLW4Ys4!560LrOrt+#PoH;0@z&CxmvZLj!*%IH~ewO)}ve#yt@EU-;L z?Z~@JgSlm89Q5?`<<_z@hthjJBG1{Cr$`)?xn9ZuWog~#@)#_6*uT)$eDK=`YN1iD zqeGALxP-yGUFs4Y6vZLhfkWWS{pJjXv;vRiUOVF2n`6RqzRvq{&(+@9&6QCaajaz{ zl|2ThL=U!^7#SJm)zqTZ@61aa#sknjUiP@0gQg;Q?Joe?E&!C*zqfMGLtqCZNhx_~WP~}}G-|xz9zEpU zwBw>C#9m!p{i?2xUq4agN@%E#%g5(*Rl}?{X$tpN?O55_XU6YMlHaPHO-H#Bkw8U1 zz0^tkniI?p0JJ&W0QAcVL_ouCWMt$!U34_s zzUk|91yyI_(iM-i?pob2IjlK|Q!)oY@WdTv45tHH&!-cjb;=}@q0v6EJ&ugx zv|K*&{oLHlD=O0W)O_hQAl9vz6SC_LwY0QAG#`6k`6M!uaV%(wKzZXO7?z7ftWbc& zwxtwx4FTQ(XbKAcat0aF*GC5d>bZ-Dfgy(Obl-#^ot+P6$*WhC_*g;4Kw)fxD$$tT zo!(Gw&%iDf3O_5O){pn%P?+Xw?d2yIT$lrze~qun`%Gwo5{CvX(6&yR+mc8ZSy@?8 zpR&}|$6XDH52-6fI28)mEx06~Vfd}hHz3f%PTPM za1QMwD7WK={QTWi6R-M7vz_-j1)PGW+> z)@M5}Crd1kH}FEA$HuY(>^tHG(|LmY#v=#SMHlWbFE1Mh$4eMcYJ5cHZqz@kZmtu_ z=dnrao0wpSYW?PzSsBBR!~zBF>FJrFxb|eGs-TOsYUKggU~sZ_wa4@mgh(;mQu24^ zjn^CXxR#$WNxtzOhfOpHs(K&aBC~C6yg$Ay%fsT`NVWR8wKbdJn!`D*`~u~wB#^RM z1w#+!|a}g9?@>n%A_1r-w`fEqyp$xp@sr7n|D zhJL-dCwlpO&Yte>`~eJIz$H9YoOXXMK#_|SU?~;Z4{+8Ipn@w4p~|f1WSkEU=m=J= zIvN1jL8)@(0O|ZVGBUED$dfz##-eUJ^5GZnnpvZpyIj!J&xQWa3;vP^m^(M1CBdg5uaHhKKZ%<7J+W30dP6&#C(d%-GN;R8ckT894Qj31U6}J*ggJlQ!*F*Y>O3Q^ob|&B~@GkCP+zHIhUy1 z5G6IUt`=q{nsaUcBbqoPb90AT&aO7>oa-l)ukW$ot=0DJ_1P$lx%vxK5!K=HipO~U zU4GG6zyUWj`jEVTSvHqvjGoxg?i=&@0em#->;GDzUy<^H-AtAMTg&v2fPg^qzHw2% z!>`SiOj7C7EecyrFOtLTx$&uWjJbXgaBtud^VPx2`p7jq zq}5E1Qd0fhCCf(Ype+>n+SR7UJ3G*a_1>OK6+ckCme1j5vbqzPPyX4!ede`sJ&h!H z+Xelv?b+l!Y^9|AHm|JpU-Odv3;wlhO~2ZXJVut&54@az`(VfKlUe)gCYM@_$SRiN z6Z&fu3JYiAaD4apsDW7t;~_ezC0WwwuWrQ>D;Z!hQis}zL-sN`e8M^*Z=e6AO2ty_ z-t>)(z~q(#8ia>#hGLLcyT59=2rmCn?=W6Vzixm3F!Xt3B(u7=>kG2TH2cz>J5|q* z58qOpelzOLonH`pl>1c6*9|%-V40HSuNA|6T<3cWy#e=e8S#%@4DDSWKZ6eEQ!z+Np(xBFp9;2qHIcVrN~NP-+%Xx8>m~ z1$p@}uw2nP*xRtK_dmTCq5~?EHj`yE$-8!Eh)34?>4sK7Oc}rF>-)?5KZ|te_>kdM z1OgkVi`p92nphx8-5u8vt$?ogl-$cvQ~nwiv=T_o_rk~ZfN!633y{h$y=WCt%je67 z0qb9GJohT^-r^%li@SiDiBVpu(ZJSL5bUlpGBMrW`QiNWrOwUA#|sk}!nP(uMavpD z1?)q1oF890307WoP=crqdpPM;eStM7-M|yHWt+~yPVdZq`&Ef#rDRYwmZM0A)?GoV z5I*i5MMk#|U@g``oj(m0d9~{ug6)l=m9n#-EaWr_ZUi_(k@LG2`;(w{tE}I;wlsaz zChZF0!S|_)dmAqg+GNV!is_x8LxHu^b46$7ua)`926im=gr=3%Rk`~Z+n=aTWGXB} zk$eABw&v~CG4(9Xd+eh%KAkHylXCNV7AyfVGN-c62lD{$KN4}N_V?y!74p7g?GUTf zi&UwQy;;#&uo`TMVB!5dIhlz0wdwF7Sp*p}H9)r?6)VeW9IWsfU zcdje7I#YJY>lSmsPEMz1_zmq&H6c7X_Ct!)EVd};U6Q!IFTgQN8zW#}##`mmV<>PU z#hJfm%#X#2fQ+b_3=stR`40GVP;fA3yxuy+DKQs|HMkq19@qZpg?If?U?FW$q9M^n z>2lM*w?eFbCVWGd5Zn}AT9$#}U z#F2NqHgl7wAB{iZKUW^Gf~2{iDCTF<^OwG(Z@g*`_J8H}FDRB;Ua~a)v+vhXvN-yP z2~;N$O@jgD<#*S)+XSt!HR^FTYQL2zF0O?6kmTDaQlg# z_|G8Bm!MMQSEH52{I^!g5;Q#&Xc*6V!(K9STf*CRc+!|-;Z{SW_u`fTYXG1-Pn_h+ zb}IEQ@s#l?nAF_0N$`CQ80Al_alT$YHPbQ4nSbcUz3kd3qdKV)fTXeauU650-+{9& z?OIZW8hTy(>LfVk16}8-06!quk7bXG6 diff --git a/src/components/ambient-light.js b/src/components/ambient-light.js index 13112b6a17..5a107b2c4b 100644 --- a/src/components/ambient-light.js +++ b/src/components/ambient-light.js @@ -7,13 +7,12 @@ AFRAME.registerComponent("ambient-light", { init() { this.light = new THREE.AmbientLight(); this.el.setObject3D("ambient-light", this.light); - this.rendererSystem = this.el.sceneEl.systems.renderer; }, update(prevData) { if (this.data.color !== prevData.color) { this.light.color.set(this.data.color); - this.rendererSystem.applyColorCorrection(this.light.color); + this.light.color.convertSRGBToLinear(); } if (this.data.intensity !== prevData.intensity) { diff --git a/src/components/animation.js b/src/components/animation.js index acc0b43f60..8a9942269e 100644 --- a/src/components/animation.js +++ b/src/components/animation.js @@ -19,8 +19,67 @@ delete AFRAME.components.animation; // hubs var colorHelperFrom = new THREE.Color(); var colorHelperTo = new THREE.Color(); -var getComponentProperty = utils.entity.getComponentProperty; -var setComponentProperty = utils.entity.setComponentProperty; +/** + * Split a delimited component property string (e.g., `material.color`) to an object + * containing `component` name and `property` name. If there is no delimiter, just return the + * string back. + * + * Cache arrays from splitting strings via delimiter to save on memory. + * + * @param {string} str - e.g., `material.opacity`. + * @param {string} delimiter - e.g., `.`. + * @returns {array} e.g., `['material', 'opacity']`. + */ +var propertyPathCache = {}; +function getComponentPropertyPath(str, delimiter) { + delimiter = delimiter || "."; + if (!propertyPathCache[delimiter]) { + propertyPathCache[delimiter] = {}; + } + if (str.indexOf(delimiter) !== -1) { + propertyPathCache[delimiter][str] = str.split(delimiter); + } else { + propertyPathCache[delimiter][str] = str; + } + return propertyPathCache[delimiter][str]; +} + +/** + * Get component property using encoded component name + component property name with a + * delimiter. + */ +const getComponentProperty = function(el, name, delimiter) { + var splitName; + delimiter = delimiter || "."; + if (name.indexOf(delimiter) !== -1) { + splitName = getComponentPropertyPath(name, delimiter); + if (splitName.constructor === String) { + return el.getAttribute(splitName); + } + return el.getAttribute(splitName[0])[splitName[1]]; + } + return el.getAttribute(name); +}; + +/** + * Set component property using encoded component name + component property name with a + * delimiter. + */ +const setComponentProperty = function(el, name, value, delimiter) { + var splitName; + delimiter = delimiter || "."; + if (name.indexOf(delimiter) !== -1) { + splitName = getComponentPropertyPath(name, delimiter); + if (splitName.constructor === String) { + el.setAttribute(splitName, value); + } else { + el.setAttribute(splitName[0], splitName[1], value); + } + return; + } + el.setAttribute(name, value); +}; + var splitCache = {}; var TYPE_COLOR = "color"; @@ -606,9 +665,9 @@ function getPropertyType(el, property) { * Convert object to radians. */ function toRadians(obj) { - obj.x = THREE.Math.degToRad(obj.x); - obj.y = THREE.Math.degToRad(obj.y); - obj.z = THREE.Math.degToRad(obj.z); + obj.x = THREE.MathUtils.degToRad(obj.x); + obj.y = THREE.MathUtils.degToRad(obj.y); + obj.z = THREE.MathUtils.degToRad(obj.z); } function addEventListeners(el, eventNames, handler) { @@ -644,7 +703,7 @@ function setRawProperty(el, path, value, type) { var targetValue; if (path.startsWith("object3D.rotation")) { - value = THREE.Math.degToRad(value); + value = THREE.MathUtils.degToRad(value); } // Walk. diff --git a/src/components/audio-feedback.js b/src/components/audio-feedback.js index 005564fb1c..dc0199d2e2 100644 --- a/src/components/audio-feedback.js +++ b/src/components/audio-feedback.js @@ -28,7 +28,7 @@ const calculateVolume = (analyser, levels) => { function updateVolume(component) { const newRawVolume = calculateVolume(component.analyser, component.levels); - const newPerceivedVolume = Math.log(THREE.Math.mapLinear(newRawVolume, 0, 1, 1, Math.E)); + const newPerceivedVolume = Math.log(THREE.MathUtils.mapLinear(newRawVolume, 0, 1, 1, Math.E)); component.volume = newPerceivedVolume < MIN_VOLUME_THRESHOLD ? 0 : newPerceivedVolume; @@ -180,7 +180,7 @@ AFRAME.registerComponent("scale-audio-feedback", { if (!this.analyser) this.analyser = getAnalyser(this.el); this.el.object3D.scale.setScalar( - THREE.Math.mapLinear(this.analyser?.volume || 0, 0, 1, this.data.minScale, this.data.maxScale) + THREE.MathUtils.mapLinear(this.analyser?.volume || 0, 0, 1, this.data.minScale, this.data.maxScale) ); this.el.object3D.matrixNeedsUpdate = true; } @@ -219,7 +219,7 @@ AFRAME.registerComponent("morph-audio-feedback", { if (!this.analyser) this.analyser = getAnalyser(this.el); const { minValue, maxValue } = this.data; - const morphValue = THREE.Math.mapLinear( + const morphValue = THREE.MathUtils.mapLinear( easeOutQuadratic(this.analyser ? this.analyser.volume : 0), 0, 1, @@ -267,7 +267,7 @@ const SPRITE_NAMES = { }; export function micLevelForVolume(volume) { - return THREE.Math.clamp(Math.ceil(THREE.Math.mapLinear(volume - 0.05, 0, 1, 0, 7)), 0, 7); + return THREE.MathUtils.clamp(Math.ceil(THREE.MathUtils.mapLinear(volume - 0.05, 0, 1, 0, 7)), 0, 7); } AFRAME.registerComponent("mic-button", { diff --git a/src/components/audio-params.js b/src/components/audio-params.js index 41f2552c9e..ecf1cec2cd 100644 --- a/src/components/audio-params.js +++ b/src/components/audio-params.js @@ -22,9 +22,15 @@ export const DistanceModelType = { Exponential: "exponential" }; +export const PanningModelType = Object.freeze({ + HRTF: "HRTF", + EqualPower: "equalpower" +}); + export const AvatarAudioDefaults = Object.freeze({ audioType: AudioType.PannerNode, distanceModel: DistanceModelType.Inverse, + panningModel: PanningModelType.HRTF, rolloffFactor: 5, refDistance: 5, maxDistance: 10000, @@ -37,6 +43,7 @@ export const AvatarAudioDefaults = Object.freeze({ export const MediaAudioDefaults = Object.freeze({ audioType: AudioType.PannerNode, distanceModel: DistanceModelType.Inverse, + panningModel: PanningModelType.HRTF, rolloffFactor: 5, refDistance: 5, maxDistance: 10000, @@ -49,6 +56,7 @@ export const MediaAudioDefaults = Object.freeze({ export const TargetAudioDefaults = Object.freeze({ audioType: AudioType.PannerNode, distanceModel: DistanceModelType.Inverse, + panningModel: PanningModelType.HRTF, rolloffFactor: 5, refDistance: 8, maxDistance: 10000, diff --git a/src/components/avatar-audio-source.js b/src/components/avatar-audio-source.js index 0c921cf5f9..0c17639e31 100644 --- a/src/components/avatar-audio-source.js +++ b/src/components/avatar-audio-source.js @@ -96,13 +96,24 @@ AFRAME.registerComponent("avatar-audio-source", { APP.dialog.on("stream_updated", this._onStreamUpdated, this); this.createAudio(); - let disableLeftRightPanningPref = APP.store.state.preferences.disableLeftRightPanning; + let { disableLeftRightPanning, audioPanningQuality } = APP.store.state.preferences; this.onPreferenceChanged = () => { - const newPref = APP.store.state.preferences.disableLeftRightPanning; - const shouldRecreateAudio = disableLeftRightPanningPref !== newPref && !this.isCreatingAudio; - disableLeftRightPanningPref = newPref; + const newDisableLeftRightPanning = APP.store.state.preferences.disableLeftRightPanning; + const newAudioPanningQuality = APP.store.state.preferences.audioPanningQuality; + + const shouldRecreateAudio = disableLeftRightPanning !== newDisableLeftRightPanning && !this.isCreatingAudio; + const shouldUpdateAudioSettings = audioPanningQuality !== newAudioPanningQuality; + + disableLeftRightPanning = newDisableLeftRightPanning; + audioPanningQuality = newAudioPanningQuality; + if (shouldRecreateAudio) { this.createAudio(); + } else if (shouldUpdateAudioSettings) { + // updateAudioSettings() is called in this.createAudio() + // so no need to call it if shouldRecreateAudio is true. + const audio = this.el.getObject3D(this.attrName); + updateAudioSettings(this.el, audio); } }; APP.store.addEventListener("statechanged", this.onPreferenceChanged); @@ -308,7 +319,7 @@ AFRAME.registerComponent("audio-target", { if (this.data.maxDelay > 0) { const delayNode = audio.context.createDelay(this.data.maxDelay); - delayNode.delayTime.value = THREE.Math.randFloat(this.data.minDelay, this.data.maxDelay); + delayNode.delayTime.value = THREE.MathUtils.randFloat(this.data.minDelay, this.data.maxDelay); audio.setFilters([delayNode]); } diff --git a/src/components/avatar-volume-controls.js b/src/components/avatar-volume-controls.js index 8c90981693..15a3e70d35 100644 --- a/src/components/avatar-volume-controls.js +++ b/src/components/avatar-volume-controls.js @@ -71,14 +71,14 @@ AFRAME.registerComponent("avatar-volume-controls", { volumeUp() { let gainMultiplier = APP.gainMultipliers.get(this.audioEl); const step = calcGainStepUp(gainMultiplier); - gainMultiplier = THREE.Math.clamp(gainMultiplier + step, 0, MAX_GAIN_MULTIPLIER); + gainMultiplier = THREE.MathUtils.clamp(gainMultiplier + step, 0, MAX_GAIN_MULTIPLIER); this.updateGainMultiplier(gainMultiplier, true); }, volumeDown() { let gainMultiplier = APP.gainMultipliers.get(this.audioEl); const step = -calcGainStepDown(gainMultiplier); - gainMultiplier = THREE.Math.clamp(gainMultiplier + step, 0, MAX_GAIN_MULTIPLIER); + gainMultiplier = THREE.MathUtils.clamp(gainMultiplier + step, 0, MAX_GAIN_MULTIPLIER); this.updateGainMultiplier(gainMultiplier, true); }, diff --git a/src/components/bone-visibility.js b/src/components/bone-visibility.js index 81f3f69a76..9134c5143b 100644 --- a/src/components/bone-visibility.js +++ b/src/components/bone-visibility.js @@ -31,6 +31,18 @@ export class BoneVisibilitySystem { cmp.lastVisible = visible; } } + + // Called only from camera-tool.tock() to update the matrices + // of objects and their children whose are updated in tick(). + updateMatrices() { + for (let i = 0; i < components.length; i++) { + const cmp = components[i]; + const obj = cmp.el.object3D; + if (obj.matrixNeedsUpdate) { + obj.updateMatrixWorld(); + } + } + } } AFRAME.registerComponent("bone-visibility", { diff --git a/src/components/camera-tool.js b/src/components/camera-tool.js index 48c3f088c5..b4ef237559 100644 --- a/src/components/camera-tool.js +++ b/src/components/camera-tool.js @@ -109,7 +109,7 @@ AFRAME.registerComponent("camera-tool", { format: THREE.RGBAFormat, minFilter: THREE.LinearFilter, magFilter: THREE.NearestFilter, - encoding: THREE.GammaEncoding, + encoding: THREE.sRGBEncoding, depth: false, stencil: false }); @@ -584,6 +584,8 @@ AFRAME.registerComponent("camera-tool", { if (this.trackTarget) { if (this.trackTarget.parentNode) { this.lookAt(this.trackTarget); + // scene.autoUpdate will be false so explicitly update the world matrices + this.object3D.updateMatrixWorld(); } else { this.trackTarget = null; // Target removed } @@ -620,13 +622,20 @@ AFRAME.registerComponent("camera-tool", { // HACK, bone visibility typically takes a tick to update, but since we want to be able // to have enable() and disable() be reflected this frame, we need to do it immediately. boneVisibilitySystem.tick(); + // scene.autoUpdate will be false so explicitly update the world matrices + boneVisibilitySystem.updateMatrices(); } const tmpVRFlag = renderer.xr.enabled; const tmpOnAfterRender = sceneEl.object3D.onAfterRender; + const tmpAutoUpdate = sceneEl.object3D.autoUpdate; delete sceneEl.object3D.onAfterRender; renderer.xr.enabled = false; + // The entire scene graph matrices should already be updated + // in tick(). They don't need to be recomputed again in tock(). + sceneEl.object3D.autoUpdate = false; + if (allowVideo && this.videoRecorder && !this.videoRenderTarget) { // Create a separate render target for video because we need to flip and (sometimes) downscale it before // encoding it to video. @@ -649,6 +658,7 @@ AFRAME.registerComponent("camera-tool", { renderer.xr.enabled = tmpVRFlag; sceneEl.object3D.onAfterRender = tmpOnAfterRender; + sceneEl.object3D.autoUpdate = tmpAutoUpdate; if (this.playerHud) { this.playerHud.visible = playerHudWasVisible; @@ -666,6 +676,7 @@ AFRAME.registerComponent("camera-tool", { // HACK, bone visibility typically takes a tick to update, but since we want to be able // to have enable() and disable() be reflected this frame, we need to do it immediately. boneVisibilitySystem.tick(); + boneVisibilitySystem.updateMatrices(); } this.lastUpdate = now; diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js index 9bba275395..ae8896b05c 100644 --- a/src/components/cursor-controller.js +++ b/src/components/cursor-controller.js @@ -9,7 +9,6 @@ const TRANSFORM_COLOR_2 = new THREE.Color(23 / 255, 64 / 255, 118 / 255); AFRAME.registerComponent("cursor-controller", { schema: { cursor: { type: "selector" }, - cursorVisual: { type: "selector" }, camera: { type: "selector" }, far: { default: 100 }, near: { default: 0.01 }, @@ -20,19 +19,70 @@ AFRAME.registerComponent("cursor-controller", { init: function() { this.enabled = false; - this.data.cursorVisual.addEventListener( - "loaded", - () => { - this.data.cursorVisual.object3DMap.mesh.renderOrder = window.APP.RENDER_ORDER.CURSOR; - }, - { once: true } + this.cursorVisual = new THREE.Mesh( + new THREE.PlaneBufferGeometry(), + new THREE.ShaderMaterial({ + depthTest: false, + uniforms: { + color: { value: new THREE.Color(0x2f80ed) } + }, + vertexShader: ` + varying vec2 vPos; + void main() { + vPos = position.xy; + + vec4 mvPosition = modelViewMatrix * vec4( 0.0, 0.0, 0.0, 1.0 ); + + vec2 scale = vec2( + length( vec3( modelMatrix[ 0 ].x, modelMatrix[ 0 ].y, modelMatrix[ 0 ].z ) ), + length( vec3( modelMatrix[ 1 ].x, modelMatrix[ 1 ].y, modelMatrix[ 1 ].z ) ) + ); + + float distance = -mvPosition.z; + scale *= distance; // negates projection scale + scale += min(1.0/distance, 0.3); // scale in screen space + + float radius = 0.02; + mvPosition.xy += position.xy * radius * scale; + gl_Position = projectionMatrix * mvPosition; + }`, + fragmentShader: ` + uniform vec3 color; + varying vec2 vPos; + + void main() { + float distance = length(vPos); + if (distance > 0.5) { + discard; + } + + gl_FragColor = vec4( + mix(color, vec3(0.0), step(0.35, distance)), + 0.8 + ); + + // #include + #include + }` + }) ); + const setCursorScale = () => { + this.cursorVisual.scale.setScalar(APP.store.state.preferences["cursorSize"] || 1); + this.cursorVisual.matrixNeedsUpdate = true; + }; + APP.store.addEventListener("statechanged", setCursorScale); + setCursorScale(); + + this.cursorVisual.renderOrder = window.APP.RENDER_ORDER.CURSOR; + this.cursorVisual.material.transparent = true; + this.data.cursor.object3D.add(this.cursorVisual); + this.intersection = null; this.raycaster = new THREE.Raycaster(); this.raycaster.firstHitOnly = true; // flag specific to three-mesh-bvh this.distance = this.data.far; - this.color = new THREE.Color(0, 0, 0); + this.color = this.cursorVisual.material.uniforms.color.value; const lineGeometry = new THREE.BufferGeometry(); lineGeometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(2 * 3), 3)); @@ -104,7 +154,7 @@ AFRAME.registerComponent("cursor-controller", { const cursorModDelta = userinput.get(left ? paths.actions.cursor.left.modDelta : paths.actions.cursor.right.modDelta) || 0; if (isGrabbing && !userinput.activeSets.includes(left ? sets.leftCursorHoldingUI : sets.rightCursorHoldingUI)) { - this.distance = THREE.Math.clamp(this.distance - cursorModDelta, minDistance, far * playerScale); + this.distance = THREE.MathUtils.clamp(this.distance - cursorModDelta, minDistance, far * playerScale); } cursor.object3D.position.copy(cursorPose.position).addScaledVector(cursorPose.direction, this.distance); // The cursor will always be oriented towards the player about its Y axis, so objects held by the cursor will rotate towards the player. @@ -127,11 +177,6 @@ AFRAME.registerComponent("cursor-controller", { this.color.copy(NO_HIGHLIGHT); } - if (!this.data.cursorVisual.object3DMap.mesh.material.color.equals(this.color)) { - this.data.cursorVisual.object3DMap.mesh.material.color.copy(this.color); - this.data.cursorVisual.object3DMap.mesh.material.needsUpdate = true; - } - if (this.line.material.visible) { const posePosition = cursorPose.position; const cursorPosition = cursor.object3D.position; diff --git a/src/components/cylinder-texture.js b/src/components/cylinder-texture.js index 666c23533b..70ab5fdaa9 100644 --- a/src/components/cylinder-texture.js +++ b/src/components/cylinder-texture.js @@ -1,2 +1,2 @@ -export const CYLINDER_TEXTURE = +export const cylinderTextureSrc = "data:image/png;base64,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"; diff --git a/src/components/directional-light.js b/src/components/directional-light.js index 83c1dd5d39..bedb70a859 100644 --- a/src/components/directional-light.js +++ b/src/components/directional-light.js @@ -14,7 +14,6 @@ AFRAME.registerComponent("directional-light", { this.light.target.position.set(0, 0, 1); this.light.add(this.light.target); this.el.setObject3D("directional-light", this.light); - this.rendererSystem = this.el.sceneEl.systems.renderer; }, update(prevData) { @@ -22,7 +21,7 @@ AFRAME.registerComponent("directional-light", { if (this.data.color !== prevData.color) { const color = new THREE.Color(this.data.color); - this.rendererSystem.applyColorCorrection(color); + color.convertSRGBToLinear(); light.color.copy(color); } diff --git a/src/components/emoji-hud.js b/src/components/emoji-hud.js index 188bc362df..bca0b904b6 100644 --- a/src/components/emoji-hud.js +++ b/src/components/emoji-hud.js @@ -87,27 +87,12 @@ AFRAME.registerComponent("emoji-hud", { spawnScale: { x: this.data.spawnedScale, y: this.data.spawnedScale, z: this.data.spawnedScale } }); - const cylinder = document.createElement("a-cylinder"); - cylinder.setAttribute("visibility-while-frozen", { - requireHoverOnNonMobile: false, - withPermission: "spawn_emoji" - }); - cylinder.setAttribute("material", { opacity: 0.2, color: "#2f7fee" }); - cylinder.setAttribute("segments-height", 1); - cylinder.setAttribute("segments-radial", 16); - cylinder.setAttribute("scale", { x: width / 2, y: width / 20, z: width / 5 }); - cylinder.setAttribute("rotation", { x: 45, y: 0, z: 0 }); - setOffsetVector(i, emojis.length, width, spacing, offsetVector); spawnerEntity.object3D.position.copy(offsetVector); spawnerEntity.object3D.matrixNeedsUpdate = true; - cylinder.object3D.position.set(offsetVector.x, -width / 2, offsetVector.z + 0.01); //move back to avoid transparency issues with emojis - cylinder.object3D.matrixNeedsUpdate = true; - this.el.appendChild(spawnerEntity); - this.el.appendChild(cylinder); this.spawnerEntities.push(spawnerEntity); } diff --git a/src/components/environment-map.js b/src/components/environment-map.js index 6d14271d08..3096d34de8 100644 --- a/src/components/environment-map.js +++ b/src/components/environment-map.js @@ -10,6 +10,6 @@ export async function createDefaultEnvironmentMap() { const texture = await new Promise((resolve, reject) => new THREE.CubeTextureLoader().load(urls, resolve, undefined, reject) ); - texture.format = THREE.RGBFormat; + texture.format = THREE.RGBAFormat; return texture; } diff --git a/src/components/follow-in-fov.js b/src/components/follow-in-fov.js index 18b0c5d4f6..94b2b9d167 100644 --- a/src/components/follow-in-fov.js +++ b/src/components/follow-in-fov.js @@ -53,7 +53,7 @@ AFRAME.registerComponent("follow-in-fov", { // Compute position + rotation by projecting offset along a downward ray in target space, // and mask out Z rotation. this._applyMaskedTargetRotation( - -this.data.angle * THREE.Math.DEG2RAD, + -this.data.angle * THREE.MathUtils.DEG2RAD, target.rotation.y, 0, this.snappedXFormWorld diff --git a/src/components/gltf-model-plus.js b/src/components/gltf-model-plus.js index 48d38add9f..5ef87c08be 100644 --- a/src/components/gltf-model-plus.js +++ b/src/components/gltf-model-plus.js @@ -210,7 +210,7 @@ const inflateEntities = function(indexToEntityMap, node, templates, isRoot, mode // the group. See `PropertyBinding.findNode`: // https://github.com/mrdoob/three.js/blob/dev/src/animation/PropertyBinding.js#L211 el.object3D.uuid = node.uuid; - node.uuid = THREE.Math.generateUUID(); + node.uuid = THREE.MathUtils.generateUUID(); if (node.animations) { // Pass animations up to the group object so that when we can pass the group as @@ -562,6 +562,12 @@ class GLTFHubsLightMapExtension { const lightMap = results[1]; material.lightMap = lightMap; material.lightMapIntensity = extensionDef.intensity !== undefined ? extensionDef.intensity : 1; + + // See https://github.com/mrdoob/three.js/pull/23613 + if (material.isMeshBasicMaterial) { + material.lightMapIntensity *= Math.PI; + } + return material; }); } @@ -595,7 +601,7 @@ class GLTFHubsTextureBasisExtension { console.warn(`The ${this.name} extension is deprecated, you should use KHR_texture_basisu instead.`); const extensionDef = textureDef.extensions[this.name]; - const source = json.images[extensionDef.source]; + const source = extensionDef.source; return parser.loadTextureImage(textureIndex, source, this.basisLoader); } @@ -618,7 +624,7 @@ class GLTFMozTextureRGBE { } const extensionDef = textureDef.extensions[this.name]; - const source = json.images[extensionDef.source]; + const source = extensionDef.source; return parser.loadTextureImage(textureIndex, source, this.loader).then(t => { // TODO pretty severe artifacting when using mipmaps, disable for now if (t.minFilter == THREE.NearestMipmapNearestFilter || t.minFilter == THREE.NearestMipmapLinearFilter) { @@ -725,7 +731,6 @@ AFRAME.registerComponent("gltf-model-plus", { contentType: { type: "string" }, useCache: { default: true }, inflate: { default: false }, - batch: { default: false }, modelToWorldScale: { type: "number", default: 1 } }, @@ -745,9 +750,6 @@ AFRAME.registerComponent("gltf-model-plus", { }, remove() { - if (this.data.batch && this.model) { - this.el.sceneEl.systems["hubs-systems"].batchManagerSystem.removeObject(this.el.object3DMap.mesh); - } if (this.data.useCache) { const src = resolveAsset(this.data.src); if (src) { @@ -791,10 +793,6 @@ AFRAME.registerComponent("gltf-model-plus", { this.model = gltf.scene; - if (this.data.batch) { - this.el.sceneEl.systems["hubs-systems"].batchManagerSystem.addObject(this.model); - } - if (gltf.animations.length > 0) { this.el.setAttribute("animation-mixer", {}); this.el.components["animation-mixer"].initMixer(this.model.animations); diff --git a/src/components/hoverable-visuals.js b/src/components/hoverable-visuals.js index cdb4a8a4cf..9c39b890ac 100644 --- a/src/components/hoverable-visuals.js +++ b/src/components/hoverable-visuals.js @@ -1,4 +1,3 @@ -import { forEachMaterial } from "../utils/material-utils"; import { showHoverEffect } from "../utils/permissions-utils"; const interactorOneTransform = []; @@ -25,25 +24,6 @@ AFRAME.registerComponent("hoverable-visuals", { this.uniforms = null; this.boundingBox = null; - // Used when the object is batched - const batchManagerSystem = this.el.sceneEl.systems["hubs-systems"].batchManagerSystem; - this.el.object3D.traverse(object => { - if (!object.material) return; - forEachMaterial(object, material => { - if ( - !validMaterials.includes(material.type) || - object.el.classList.contains("ui") || - object.el.classList.contains("hud") || - object.el.getAttribute("text-button") - ) - return; - - if (batchManagerSystem.batchingEnabled) { - batchManagerSystem.meshToEl.delete(object); - } - }); - }); - const isMobile = AFRAME.utils.device.isMobile(); const isMobileVR = AFRAME.utils.device.isMobileVR(); this.isTouchscreen = isMobile && !isMobileVR; diff --git a/src/components/hud-controller.js b/src/components/hud-controller.js index f47ff20083..a45c0e0d7a 100644 --- a/src/components/hud-controller.js +++ b/src/components/hud-controller.js @@ -35,14 +35,14 @@ AFRAME.registerComponent("hud-controller", { const { offset, lookCutoff, animRange, yawCutoff } = this.data; - const pitch = head.rotation.x * THREE.Math.RAD2DEG; - const yawDif = deltaAngle(head.rotation.y, hud.rotation.y) * THREE.Math.RAD2DEG; + const pitch = head.rotation.x * THREE.MathUtils.RAD2DEG; + const yawDif = deltaAngle(head.rotation.y, hud.rotation.y) * THREE.MathUtils.RAD2DEG; // HUD is always visible until first hover, to increase discoverability. const forceHudVisible = !this.store.state.activity.hasHoveredInWorldHud; // animate the hud into place over animRange degrees as the user aproaches the lookCutoff angle - let t = 1 - THREE.Math.clamp(lookCutoff - pitch, 0, animRange) / animRange; + let t = 1 - THREE.MathUtils.clamp(lookCutoff - pitch, 0, animRange) / animRange; // HUD is locked down while showing tooltip or if forced. if (forceHudVisible) { @@ -80,7 +80,7 @@ AFRAME.registerComponent("hud-controller", { (!hudOutOfView || forceHudVisible) && this.el.sceneEl.systems["hubs-systems"].cameraSystem.mode === CAMERA_MODE_FIRST_PERSON; hud.position.y = (this.isYLocked ? this.lockedHeadPositionY : head.position.y) + offset + (1 - t) * offset; - hud.rotation.x = (1 - t) * THREE.Math.DEG2RAD * 90; + hud.rotation.x = (1 - t) * THREE.MathUtils.DEG2RAD * 90; hud.matrixNeedsUpdate = true; }, diff --git a/src/components/ik-controller.js b/src/components/ik-controller.js index c2549a46d6..d92d13f76b 100644 --- a/src/components/ik-controller.js +++ b/src/components/ik-controller.js @@ -79,7 +79,7 @@ AFRAME.registerComponent("ik-controller", { rightHand: { type: "string", default: "RightHand" }, chest: { type: "string", default: "Spine" }, rotationSpeed: { default: 8 }, - maxLerpAngle: { default: 90 * THREE.Math.DEG2RAD }, + maxLerpAngle: { default: 90 * THREE.MathUtils.DEG2RAD }, alwaysUpdate: { type: "boolean", default: false } }, diff --git a/src/components/layers.js b/src/components/layers.js index 25aaea606d..27929fa946 100644 --- a/src/components/layers.js +++ b/src/components/layers.js @@ -6,11 +6,10 @@ export const Layers = { CAMERA_LAYER_REFLECTION: 3, CAMERA_LAYER_INSPECT: 4, - CAMERA_LAYER_BATCH_INSPECT: 5, - CAMERA_LAYER_VIDEO_TEXTURE_TARGET: 6, + CAMERA_LAYER_VIDEO_TEXTURE_TARGET: 5, - CAMERA_LAYER_THIRD_PERSON_ONLY: 7, - CAMERA_LAYER_FIRST_PERSON_ONLY: 8 + CAMERA_LAYER_THIRD_PERSON_ONLY: 6, + CAMERA_LAYER_FIRST_PERSON_ONLY: 7 }; /** diff --git a/src/components/media-image.js b/src/components/media-image.js index 1e008e760b..687e82c851 100644 --- a/src/components/media-image.js +++ b/src/components/media-image.js @@ -17,7 +17,6 @@ AFRAME.registerComponent("media-image", { version: { type: "number" }, projection: { type: "string", default: "flat" }, contentType: { type: "string" }, - batch: { default: false }, alphaMode: { type: "string", default: undefined }, alphaCutoff: { type: "number" } }, @@ -27,9 +26,6 @@ AFRAME.registerComponent("media-image", { }, remove() { - if (this.data.batch && this.mesh) { - this.el.sceneEl.systems["hubs-systems"].batchManagerSystem.removeObject(this.mesh); - } if (this.currentSrcIsRetained) { textureCache.release(this.data.src, this.data.version); this.currentSrcIsRetained = false; @@ -40,8 +36,6 @@ AFRAME.registerComponent("media-image", { let texture; let ratio = 1; - const batchManagerSystem = this.el.sceneEl.systems["hubs-systems"].batchManagerSystem; - try { const { src, version, contentType } = this.data; if (!src) return; @@ -110,12 +104,6 @@ AFRAME.registerComponent("media-image", { const projection = this.data.projection; - if (this.mesh && this.data.batch) { - // This is a no-op if the mesh was just created. - // Otherwise we want to ensure the texture gets updated. - batchManagerSystem.removeObject(this.mesh); - } - if (!this.mesh || projection !== oldData.projection) { const material = new THREE.MeshBasicMaterial(); material.toneMapped = false; @@ -147,24 +135,18 @@ AFRAME.registerComponent("media-image", { if (texture == errorTexture) { this.mesh.material.transparent = true; } else { - // if transparency setting isnt explicitly defined, default to on for all non batched things, gifs, and basis textures with alpha + // if transparency setting isnt explicitly defined, default to on for all gifs, and basis textures with alpha switch (this.data.alphaMode) { case "opaque": this.mesh.material.transparent = false; break; - case "blend": - this.mesh.material.transparent = true; - this.mesh.material.alphaTest = 0; - break; case "mask": this.mesh.material.transparent = false; this.mesh.material.alphaTest = this.data.alphaCutoff; break; + case "blend": default: - this.mesh.material.transparent = - !this.data.batch || - this.data.contentType.includes("image/gif") || - !!(texture.image && texture.image.hasAlpha); + this.mesh.material.transparent = true; this.mesh.material.alphaTest = 0; } } @@ -176,10 +158,6 @@ AFRAME.registerComponent("media-image", { scaleToAspectRatio(this.el, ratio); } - if (texture !== errorTexture && this.data.batch && !texture.isCompressedTexture) { - batchManagerSystem.addObject(this.mesh); - } - this.el.emit("image-loaded", { src: this.data.src, projection: projection }); } }); diff --git a/src/components/media-loader.js b/src/components/media-loader.js index b1b9420a25..21241befa7 100644 --- a/src/components/media-loader.js +++ b/src/components/media-loader.js @@ -15,7 +15,6 @@ import { isLocalHubsAvatarUrl } from "../utils/media-url-utils"; import { addAnimationComponents } from "../utils/animation"; -import qsTruthy from "../utils/qs_truthy"; import loadingObjectSrc from "../assets/models/LoadingObject_Atom.glb"; import { SOUND_MEDIA_LOADING, SOUND_MEDIA_LOADED } from "../systems/sound-effects-system"; @@ -37,10 +36,6 @@ const fetchContentType = url => { return fetch(url, { method: "HEAD" }).then(r => r.headers.get("content-type")); }; -const forceMeshBatching = qsTruthy("batchMeshes"); -const forceImageBatching = qsTruthy("batchImages"); -const disableBatching = qsTruthy("disableBatching"); - AFRAME.registerComponent("media-loader", { schema: { playSoundEffect: { default: true }, @@ -482,17 +477,12 @@ AFRAME.registerComponent("media-loader", { { once: true } ); this.el.setAttribute("floaty-object", { reduceAngularFloat: true, releaseGravity: -1 }); - let batch = !disableBatching && forceImageBatching; - if (this.data.mediaOptions.hasOwnProperty("batch") && !this.data.mediaOptions.batch) { - batch = false; - } this.el.setAttribute( "media-image", Object.assign({}, this.data.mediaOptions, { src: accessibleUrl, version, - contentType, - batch + contentType }) ); @@ -511,8 +501,7 @@ AFRAME.registerComponent("media-loader", { "media-pdf", Object.assign({}, this.data.mediaOptions, { src: accessibleUrl, - contentType, - batch: false // Batching disabled until atlas is updated properly + contentType }) ); this.el.setAttribute("media-pager", {}); @@ -551,10 +540,6 @@ AFRAME.registerComponent("media-loader", { { once: true } ); this.el.addEventListener("model-error", this.onError, { once: true }); - let batch = !disableBatching && forceMeshBatching; - if (this.data.mediaOptions.hasOwnProperty("batch") && !this.data.mediaOptions.batch) { - batch = false; - } if (this.data.mediaOptions.hasOwnProperty("applyGravity")) { this.el.setAttribute("floaty-object", { modifyGravityOnRelease: !this.data.mediaOptions.applyGravity @@ -566,7 +551,6 @@ AFRAME.registerComponent("media-loader", { src: accessibleUrl, contentType: contentType, inflate: true, - batch, modelToWorldScale: this.data.fitToBox ? 0.0001 : 1.0 }) ); @@ -599,17 +583,12 @@ AFRAME.registerComponent("media-loader", { { once: true } ); this.el.setAttribute("floaty-object", { reduceAngularFloat: true, releaseGravity: -1 }); - let batch = !disableBatching && forceImageBatching; - if (this.data.mediaOptions.hasOwnProperty("batch") && !this.data.mediaOptions.batch) { - batch = false; - } this.el.setAttribute( "media-image", Object.assign({}, this.data.mediaOptions, { src: thumbnail, version, - contentType: guessContentType(thumbnail) || "image/png", - batch + contentType: guessContentType(thumbnail) || "image/png" }) ); if (this.el.components["position-at-border__freeze"]) { @@ -675,9 +654,7 @@ AFRAME.registerComponent("media-pager", { }) .catch(() => {}); //ignore exception, entity might not be networked - this.el.addEventListener("pdf-loaded", async () => { - this.update(); - }); + this.el.addEventListener("pdf-loaded", this.update); }, async update(oldData) { @@ -723,6 +700,12 @@ AFRAME.registerComponent("media-pager", { this.networkedEl.removeEventListener("unpinned", this.update); } + this.nextButton.object3D.removeEventListener("interact", this.onNext); + this.prevButton.object3D.removeEventListener("interact", this.onPrev); + this.snapButton.object3D.removeEventListener("interact", this.onSnap); + window.APP.hubChannel.removeEventListener("permissions_updated", this.update); + + this.el.removeEventListener("pdf-loaded", this.update); } }); diff --git a/src/components/media-pdf.js b/src/components/media-pdf.js index 1769d3a736..44d1a4054e 100644 --- a/src/components/media-pdf.js +++ b/src/components/media-pdf.js @@ -25,8 +25,7 @@ AFRAME.registerComponent("media-pdf", { src: { type: "string" }, projection: { type: "string", default: "flat" }, contentType: { type: "string" }, - index: { default: 0 }, - batch: { default: false } + index: { default: 0 } }, init() { @@ -61,12 +60,6 @@ AFRAME.registerComponent("media-pdf", { entity.addEventListener("image-loaded", this.onSnapImageLoaded, ONCE_TRUE); }, - remove() { - if (this.data.batch && this.mesh) { - this.el.sceneEl.systems["hubs-systems"].batchManagerSystem.removeObject(this.mesh); - } - }, - async update(oldData) { let texture; let ratio = 1; @@ -131,10 +124,6 @@ AFRAME.registerComponent("media-pdf", { scaleToAspectRatio(this.el, ratio); - if (texture !== errorTexture && this.data.batch) { - this.el.sceneEl.systems["hubs-systems"].batchManagerSystem.addObject(this.mesh); - } - if (this.el.components["media-pager"] && this.el.components["media-pager"].data.index !== this.data.index) { this.el.setAttribute("media-pager", { index: this.data.index }); } diff --git a/src/components/media-video.js b/src/components/media-video.js index f3fa243fbf..049a85b668 100644 --- a/src/components/media-video.js +++ b/src/components/media-video.js @@ -157,15 +157,27 @@ AFRAME.registerComponent("media-video", { this.updatePlaybackState(); }); - let disableLeftRightPanningPref = APP.store.state.preferences.disableLeftRightPanning; + let { disableLeftRightPanning, audioPanningQuality } = APP.store.state.preferences; this.onPreferenceChanged = () => { - const newPref = APP.store.state.preferences.disableLeftRightPanning; - const shouldRecreateAudio = disableLeftRightPanningPref !== newPref && this.audio && this.mediaElementAudioSource; - disableLeftRightPanningPref = newPref; + const newDisableLeftRightPanning = APP.store.state.preferences.disableLeftRightPanning; + const newAudioPanningQuality = APP.store.state.preferences.audioPanningQuality; + + const shouldRecreateAudio = + disableLeftRightPanning !== newDisableLeftRightPanning && this.audio && this.mediaElementAudioSource; + const shouldUpdateAudioSettings = audioPanningQuality !== newAudioPanningQuality; + + disableLeftRightPanning = newDisableLeftRightPanning; + audioPanningQuality = newAudioPanningQuality; + if (shouldRecreateAudio) { this.setupAudio(); + } else if (shouldUpdateAudioSettings) { + // updateAudioSettings() is called in this.setupAudio() + // so no need to call it if shouldRecreateAudio is true. + updateAudioSettings(this.el, this.audio); } }; + APP.store.addEventListener("statechanged", this.onPreferenceChanged); this.el.addEventListener("audio_type_changed", this.setupAudio); }, @@ -201,7 +213,7 @@ AFRAME.registerComponent("media-video", { changeVolumeBy(v) { let gainMultiplier = APP.gainMultipliers.get(this.el); - gainMultiplier = THREE.Math.clamp(gainMultiplier + v, 0, MAX_GAIN_MULTIPLIER); + gainMultiplier = THREE.MathUtils.clamp(gainMultiplier + v, 0, MAX_GAIN_MULTIPLIER); APP.gainMultipliers.set(this.el, gainMultiplier); this.updateVolumeLabel(); const audio = APP.audios.get(this.el); diff --git a/src/components/name-tag.js b/src/components/name-tag.js index 712d8539b7..e2e0a7e7d9 100644 --- a/src/components/name-tag.js +++ b/src/components/name-tag.js @@ -4,8 +4,11 @@ import { getThemeColor } from "../utils/theme"; import qsTruthy from "../utils/qs_truthy"; import { findAncestorWithComponent } from "../utils/scene-graph"; import { THREE } from "aframe"; -import { setMatrixWorld } from "../utils/three-utils"; import nextTick from "../utils/next-tick"; +import { createPlaneBufferGeometry, setMatrixWorld } from "../utils/three-utils"; +import { textureLoader } from "../utils/media-utils"; + +import handRaisedIconSrc from "../assets/hud/hand-raised.png"; const DEBUG = qsTruthy("debug"); const NAMETAG_BACKGROUND_PADDING = 0.05; @@ -24,6 +27,15 @@ const ANIM_CONFIG = { round: false }; +const nametagVolumeGeometry = new THREE.PlaneBufferGeometry(1, 0.025); +const nametagVolumeMaterial = new THREE.MeshBasicMaterial({ color: "#7ED320" }); + +const nametagTypingGeometry = new THREE.CircleBufferGeometry(0.01, 6); + +const handRaisedTexture = textureLoader.load(handRaisedIconSrc); +const handRaisedGeometry = createPlaneBufferGeometry(0.2, 0.2, 1, 1, handRaisedTexture.flipY); +const handRaisedMaterial = new THREE.MeshBasicMaterial({ transparent: true, map: handRaisedTexture }); + AFRAME.registerComponent("name-tag", { schema: {}, init() { @@ -54,14 +66,33 @@ AFRAME.registerComponent("name-tag", { this.nametag = this.el.object3D; this.nametagIdentityName = this.el.querySelector(".identityName").object3D; this.nametagBackground = this.el.querySelector(".nametag-background").object3D; - this.nametagVolume = this.el.querySelector(".nametag-volume").object3D; this.nametagStatusBorder = this.el.querySelector(".nametag-status-border").object3D; this.recordingBadge = this.el.querySelector(".recordingBadge").object3D; this.modBadge = this.el.querySelector(".modBadge").object3D; - this.handRaised = this.el.querySelector(".hand-raised-id").object3D; - this.nametagTyping = this.el.querySelector(".nametag-typing").object3D; this.nametagText = this.el.querySelector(".nametag-text").object3D; + this.handRaised = new THREE.Mesh(handRaisedGeometry, handRaisedMaterial); + this.handRaised.position.set(0, -0.3, 0.001); + this.el.object3D.add(this.handRaised); + + this.nametagVolume = new THREE.Mesh(nametagVolumeGeometry, nametagVolumeMaterial); + this.nametagVolume.position.set(0, -0.075, 0.001); + this.nametagVolume.visible = false; + this.el.object3D.add(this.nametagVolume); + + // TODO this is horribly inneficient draw call and geometry wise. Replace with custom shader code or at least a uv-croll image + this.nametagTyping = new THREE.Group(); + this.nametagTyping.position.set(0, -0.075, 0.001); + for (let i = 0; i < 5; i++) { + const dot = new THREE.Mesh( + nametagTypingGeometry, + new THREE.MeshBasicMaterial({ transparent: true, color: 0xffffff, depthWrite: false }) + ); + dot.position.x = i * 0.035 - 0.07; + this.nametagTyping.add(dot); + } + this.el.object3D.add(this.nametagTyping); + this.updateTheme(); NAF.utils.getNetworkedEntity(this.el).then(networkedEntity => { @@ -241,12 +272,13 @@ AFRAME.registerComponent("name-tag", { }, updateTheme() { - this.nametagStatusBorder.el.object3DMap.mesh.material.color.set( + this.nametagStatusBorder.el.setAttribute( + "slice9", + "color", getThemeColor(this.isHandRaised ? "nametag-border-color-raised-hand" : "nametag-border-color") ); - this.nametagVolume.el.object3DMap.mesh.material.color.set(getThemeColor("nametag-volume-color")); - this.nametagBackground.el.object3DMap.mesh.material.color.set(getThemeColor("nametag-color")); - this.nametagStatusBorder.el.object3DMap.mesh.material.color.set(getThemeColor("nametag-border-color")); + nametagVolumeMaterial.color.set(getThemeColor("nametag-volume-color")); + this.nametagBackground.el.setAttribute("slice9", "color", getThemeColor("nametag-color")); this.nametagText.el.setAttribute("text", "color", getThemeColor("nametag-text-color")); }, @@ -266,10 +298,12 @@ AFRAME.registerComponent("name-tag", { }, updateHandRaised() { - this.nametagStatusBorder.el.object3DMap.mesh.material.color.set( + this.nametagStatusBorder.el.setAttribute( + "slice9", + "color", getThemeColor(this.isHandRaised ? "nametag-border-color-raised-hand" : "nametag-border-color") ); - const targetScale = this.isHandRaised ? 0.2 : 0; + const targetScale = this.isHandRaised ? 1 : 0; anime({ ...ANIM_CONFIG, targets: { diff --git a/src/components/point-light.js b/src/components/point-light.js index 29c16d5ec6..7f1d558143 100644 --- a/src/components/point-light.js +++ b/src/components/point-light.js @@ -14,7 +14,6 @@ AFRAME.registerComponent("point-light", { this.light = new THREE.PointLight(); this.light.shadow.camera.matrixAutoUpdate = true; this.el.setObject3D("point-light", this.light); - this.rendererSystem = this.el.sceneEl.systems.renderer; }, update(prevData) { @@ -22,7 +21,7 @@ AFRAME.registerComponent("point-light", { if (this.data.color !== prevData.color) { const color = new THREE.Color(this.data.color); - this.rendererSystem.applyColorCorrection(color); + color.convertSRGBToLinear(); light.color.copy(color); } diff --git a/src/components/position-at-border.js b/src/components/position-at-border.js index f243467a6c..4c263744fa 100644 --- a/src/components/position-at-border.js +++ b/src/components/position-at-border.js @@ -216,7 +216,7 @@ AFRAME.registerComponent("position-at-border", { } if (this.data.scale) { const distanceToCenter = centerToCamera.subVectors(cameraPosition, desiredCenterPoint).length(); - desiredTargetScale.setScalar(THREE.Math.clamp(0.45 * distanceToCenter, MIN_SCALE, MAX_SCALE)); + desiredTargetScale.setScalar(THREE.MathUtils.clamp(0.45 * distanceToCenter, MIN_SCALE, MAX_SCALE)); } else { desiredTargetScale.setFromMatrixScale(this.target.matrixWorld); } diff --git a/src/components/scale-in-screen-space.js b/src/components/scale-in-screen-space.js deleted file mode 100644 index f73693060e..0000000000 --- a/src/components/scale-in-screen-space.js +++ /dev/null @@ -1,36 +0,0 @@ -AFRAME.registerComponent("scale-in-screen-space", { - schema: { - baseScale: { type: "vec3", default: { x: 1, y: 1, z: 1 } }, - addedScale: { type: "vec3", default: { x: 1, y: 1, z: 1 } }, - scalePreferenceName: { type: "string" } - }, - - init() { - this.preferredScale = new THREE.Vector3(1, 1, 1); - this.storeUpdated = this.storeUpdated.bind(this); - }, - play() { - if (!this.didRegister) { - this.didRegister = true; - this.el.sceneEl.systems["hubs-systems"].scaleInScreenSpaceSystem.register(this); - if (this.data.scalePreferenceName) { - window.APP.store.addEventListener("statechanged", this.storeUpdated); - } - } - }, - remove() { - if (this.didRegister) { - this.el.sceneEl.systems["hubs-systems"].scaleInScreenSpaceSystem.unregister(this); - if (this.data.scalePreferenceName) { - window.APP.store.removeEventListener("statechanged", this.storeUpdated); - } - } - }, - storeUpdated() { - if (!this.data.scalePreferenceName) return; - const preferenceValue = window.APP.store.state.preferences[this.data.scalePreferenceName] || 1; - this.preferredScale.x = preferenceValue * this.data.addedScale.x; - this.preferredScale.y = preferenceValue * this.data.addedScale.y; - this.preferredScale.z = preferenceValue * this.data.addedScale.z; - } -}); diff --git a/src/components/scene-components.js b/src/components/scene-components.js index 3366d027f9..62b4b893f4 100644 --- a/src/components/scene-components.js +++ b/src/components/scene-components.js @@ -3,6 +3,7 @@ import "./ambient-light"; import "./animation-mixer"; import "./audio-feedback"; import "./billboard"; +import "./slice9"; import "./css-class"; import "./directional-light"; import "./duck"; diff --git a/src/components/slice9.js b/src/components/slice9.js new file mode 100644 index 0000000000..ea6828bc0a --- /dev/null +++ b/src/components/slice9.js @@ -0,0 +1,261 @@ +/* global AFRAME */ +import { textureLoader } from "../utils/media-utils"; + +import actionButtonSrc from "../assets/hud/action_button.9.png"; +import buttonSrc from "../assets/hud/button.9.png"; +import nametagSrc from "../assets/hud/nametag.9.png"; +import nametagBorderSrc from "../assets/hud/nametag-border.9.png"; + +const textures = { + "action-button": { + tex: textureLoader.load(actionButtonSrc), + width: 128, + height: 128 + }, + button: { + tex: textureLoader.load(buttonSrc), + width: 128, + height: 128 + }, + nametag: { + tex: textureLoader.load(nametagSrc), + width: 128, + height: 128 + }, + "nametag-border": { + tex: textureLoader.load(nametagBorderSrc), + width: 128, + height: 128 + } +}; + +function parseSide(side) { + switch (side) { + case "back": { + return THREE.BackSide; + } + case "double": { + return THREE.DoubleSide; + } + default: { + // Including case `front`. + return THREE.FrontSide; + } + } +} + +/** + * Slice9 component for A-Frame. + */ +AFRAME.registerComponent("slice9", { + schema: { + alphaTest: { default: 0.0 }, + bottom: { default: 0, min: 0 }, + color: { type: "color", default: "#fff" }, + debug: { default: false }, + height: { default: 1, min: 0 }, + left: { default: 0, min: 0 }, + opacity: { default: 1.0, min: 0, max: 1 }, + padding: { default: 0.1, min: 0.01 }, + right: { default: 0, min: 0 }, + side: { default: "front", oneOf: ["front", "back", "double"] }, + src: { oneOf: Object.keys(textures) }, + top: { default: 0, min: 0 }, + transparent: { default: true }, + width: { default: 1, min: 0 }, + usingCustomMaterial: { default: false }, + usingAtlas: { default: false }, + uvAtlasMin: { type: "vec2" }, + uvAtlasMax: { type: "vec2" } + }, + + init: function() { + const data = this.data; + + this.textureSrc = null; + + const geometry = (this.geometry = new THREE.PlaneBufferGeometry(data.width, data.height, 3, 3)); + + // Create mesh. + if (data.usingCustomMaterial) { + this.plane = new THREE.Mesh(geometry); + } else { + const material = new THREE.MeshBasicMaterial({ + alphaTest: data.alphaTest, + color: data.color, + opacity: data.opacity, + transparent: data.transparent, + wireframe: data.debug + }); + this.plane = new THREE.Mesh(geometry, material); + } + this.el.setObject3D("mesh", this.plane); + }, + + regenerateMesh: function() { + const data = this.data; + let height; + const pos = this.geometry.attributes.position.array; + const uvs = this.geometry.attributes.uv.array; + let width; + + if (this.plane.material && !this.plane.material.map) { + return; + } + + /* + 0--1------------------------------2--3 + | | | | + 4--5------------------------------6--7 + | | | | + | | | | + | | | | + 8--9-----------------------------10--11 + | | | | + 12-13----------------------------14--15 + */ + function setPos(id, x, y) { + pos[3 * id] = x; + pos[3 * id + 1] = y; + } + + function setUV(id, u, v) { + if (data.usingAtlas) { + u = data.uvAtlasMin.x + u * (data.uvAtlasMax.x - data.uvAtlasMin.x); + v = data.uvAtlasMin.y + v * (data.uvAtlasMax.y - data.uvAtlasMin.y); + } + uvs[2 * id] = u; + uvs[2 * id + 1] = v; + } + + // Update UVS + if (data.usingCustomMaterial) { + height = 1; + width = 1; + } else { + height = textures[data.src].height; + width = textures[data.src].width; + } + const uv = { + left: data.left / width, + right: data.right / width, + top: data.top / height, + bottom: data.bottom / height + }; + + setUV(1, uv.left, 1); + setUV(2, uv.right, 1); + + setUV(4, 0, uv.bottom); + setUV(5, uv.left, uv.bottom); + setUV(6, uv.right, uv.bottom); + setUV(7, 1, uv.bottom); + + setUV(8, 0, uv.top); + setUV(9, uv.left, uv.top); + setUV(10, uv.right, uv.top); + setUV(11, 1, uv.top); + + setUV(13, uv.left, 0); + setUV(14, uv.right, 0); + + if (data.usingAtlas) { + setUV(0, 0, 1); + setUV(3, 1, 1); + setUV(12, 0, 0); + setUV(15, 1, 0); + } + + // Update vertex positions + const w2 = data.width / 2; + const h2 = data.height / 2; + const left = -w2 + data.padding; + const right = w2 - data.padding; + const top = h2 - data.padding; + const bottom = -h2 + data.padding; + + setPos(0, -w2, h2); + setPos(1, left, h2); + setPos(2, right, h2); + setPos(3, w2, h2); + + setPos(4, -w2, top); + setPos(5, left, top); + setPos(6, right, top); + setPos(7, w2, top); + + setPos(8, -w2, bottom); + setPos(9, left, bottom); + setPos(10, right, bottom); + setPos(11, w2, bottom); + + setPos(13, left, -h2); + setPos(14, right, -h2); + setPos(12, -w2, -h2); + setPos(15, w2, -h2); + + this.geometry.attributes.position.needsUpdate = true; + this.geometry.attributes.uv.needsUpdate = true; + }, + + update: function(oldData) { + const data = this.data; + const diff = AFRAME.utils.diff(data, oldData); + + // Update material if using built-in material. + if (!data.usingCustomMaterial) { + this.plane.material.alphaTest = data.alphaTest; + this.plane.material.color.setStyle(data.color); + this.plane.material.opacity = data.opacity; + this.plane.material.transparent = data.transparent; + this.plane.material.wireframe = data.debug; + this.plane.material.side = parseSide(data.side); + this.plane.material.needsUpdate = true; + if ("src" in diff) { + this.updateMap(); + } + } + + if ( + "width" in diff || + "height" in diff || + "padding" in diff || + "left" in diff || + "top" in diff || + "bottom" in diff || + "right" in diff + ) { + this.regenerateMesh(); + } + }, + + setMap(texture) { + this.plane.material.map = texture; + this.plane.material.needsUpdate = true; + this.regenerateMesh(); + }, + + /** + * Update `src` if using built-in material. + */ + updateMap: function() { + const src = this.data.src; + + if (src) { + if (src === this.textureSrc) { + return; + } + // Texture added or changed. + this.textureSrc = src; + this.setMap(textures[src].tex); + return; + } + + // Texture removed. + if (!this.plane.material.map) { + return; + } + + this.setMap(null); + } +}); diff --git a/src/components/stats-plus.js b/src/components/stats-plus.js index 6e3c37e75a..e1c8e0c0bf 100644 --- a/src/components/stats-plus.js +++ b/src/components/stats-plus.js @@ -1,24 +1,81 @@ import "./stats-plus.css"; import qsTruthy from "../utils/qs_truthy"; -// Adapted from https://github.com/aframevr/aframe/blob/master/src/components/scene/stats.js +function ThreeStats(renderer) { + let _rS = null; + + const _values = { + "renderer.info.memory.geometries": { + caption: "Geometries" + }, + "renderer.info.memory.textures": { + caption: "Textures" + }, + "renderer.info.programs": { + caption: "Programs" + }, + "renderer.info.render.calls": { + caption: "Calls" + }, + "renderer.info.render.triangles": { + caption: "Triangles", + over: 100000 + }, + "renderer.info.render.points": { + caption: "Points" + } + }; + + const _groups = [ + { + caption: "Three.js - Memory", + values: ["renderer.info.memory.geometries", "renderer.info.programs", "renderer.info.memory.textures"] + }, + { + caption: "Three.js - Render", + values: ["renderer.info.render.calls", "renderer.info.render.triangles", "renderer.info.render.points"] + } + ]; + + const _fractions = []; + function _update() { + _rS("renderer.info.memory.geometries").set(renderer.info.memory.geometries); + _rS("renderer.info.programs").set(renderer.info.programs.length); + _rS("renderer.info.memory.textures").set(renderer.info.memory.textures); + _rS("renderer.info.render.calls").set(renderer.info.render.calls); + _rS("renderer.info.render.triangles").set(renderer.info.render.triangles); + _rS("renderer.info.render.points").set(renderer.info.render.points); + } + + function _start() {} + + function _end() {} + + function _attach(r) { + _rS = r; + } + + return { + update: _update, + start: _start, + end: _end, + attach: _attach, + values: _values, + groups: _groups, + fractions: _fractions + }; +} + +// Adapted from https://github.com/aframevr/aframe/blob/master/src/components/scene/stats.js function createStats(scene) { - const threeStats = new window.threeStats(scene.renderer); - const aframeStats = new window.aframeStats(scene); - const plugins = scene.isMobile ? [] : [threeStats, aframeStats]; + const plugins = scene.isMobile ? [] : [new ThreeStats(scene.renderer)]; return new window.rStats({ - css: [], // Our stylesheet is injected from AFrame. + css: [], values: { - fps: { caption: "fps", below: 30 }, - batchdraws: { caption: "Draws" }, - batchinstances: { caption: "Instances" }, - batchatlassize: { caption: "Atlas Size" } + fps: { caption: "fps", below: 30 } }, - groups: [ - { caption: "Framerate", values: ["fps", "raf", "physics"] }, - { caption: "Batching", values: ["batchdraws", "batchinstances", "batchatlassize"] } - ], + groups: [{ caption: "Framerate", values: ["fps", "raf", "physics"] }], plugins: plugins }); } @@ -95,14 +152,12 @@ AFRAME.registerComponent("stats-plus", { { once: true } ); this.el.append(this.vrPanel); - const background = document.createElement("a-plane"); - background.setAttribute("color", "#333333"); - background.setAttribute("material", "shader", "flat"); - background.setAttribute("material", "depthTest", false); - background.setAttribute("width", 0.1); - background.setAttribute("height", 0.12); - background.setAttribute("position", "-0.2 0.055 0"); - this.el.append(background); + const background = new THREE.Mesh( + new THREE.PlaneBufferGeometry(0.1, 0.12), + new THREE.MeshBasicMaterial({ color: 0x333333, depthTest: false }) + ); + background.position.set(-0.2, 0.055, 0); + this.el.object3D.add(background); }, toggleVRStats() { if (this.vrStatsEnabled) { @@ -143,14 +198,6 @@ AFRAME.registerComponent("stats-plus", { stats("FPS").frame(); stats("physics").set(this.el.sceneEl.systems["hubs-systems"].physicsSystem.stepDuration); - const batchManagerSystem = this.el.sceneEl.systems["hubs-systems"].batchManagerSystem; - if (batchManagerSystem.batchingEnabled) { - const batchManager = batchManagerSystem.batchManager; - stats("batchdraws").set(batchManager.batches.length); - stats("batchinstances").set(batchManager.instanceCount); - stats("batchatlassize").set(batchManager.atlas.arrayDepth); - } - stats().update(); } else if (!this.inVR) { // Update the fps counter diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js index e1143b1d36..af17427d0a 100644 --- a/src/components/super-spawner.js +++ b/src/components/super-spawner.js @@ -77,8 +77,6 @@ AFRAME.registerComponent("super-spawner", { this.handleMediaLoaded = this.handleMediaLoaded.bind(this); this.spawnedMediaScale = null; - - this.physicsSystem = this.el.sceneEl.systems["hubs-systems"].physicsSystem; }, play() { @@ -191,7 +189,14 @@ AFRAME.registerComponent("super-spawner", { } } - this.physicsSystem.resetDynamicBody(spawnedEntity.components["body-helper"].uuid); + // We skip this in the scene preview because + // 1. hubs-systems is not initialized in the scene preview + // 2. physics is not needed in the scene preview + if (this.el.sceneEl.systems["hubs-systems"]) { + this.el.sceneEl.systems["hubs-systems"].physicsSystem.resetDynamicBody( + spawnedEntity.components["body-helper"].uuid + ); + } spawnedEntity.addEventListener( "media-loaded", diff --git a/src/components/teleporter.js b/src/components/teleporter.js index cb8bc13417..f57b051aff 100644 --- a/src/components/teleporter.js +++ b/src/components/teleporter.js @@ -1,7 +1,11 @@ -import { CYLINDER_TEXTURE } from "./cylinder-texture"; +import { cylinderTextureSrc } from "./cylinder-texture"; import { SOUND_TELEPORT_START, SOUND_TELEPORT_END } from "../systems/sound-effects-system"; import { getMeshes } from "../utils/aframe-utils"; +import { textureLoader } from "../utils/media-utils"; + +const CYLINDER_TEXTURE = textureLoader.load(cylinderTextureSrc); + function easeIn(t) { return t * t; } @@ -11,42 +15,46 @@ function easeOutIn(t) { return 0.5 * (t = t * 2 - 1) * t * t + 0.5; } -const RayCurve = function(numPoints, width) { - this.geometry = new THREE.BufferGeometry(); - this.vertices = new Float32Array(numPoints * 3 * 6); - this.width = width; - - this.geometry.setAttribute("position", new THREE.BufferAttribute(this.vertices, 3).setUsage(THREE.DynamicDrawUsage)); +const UP = new THREE.Vector3(0, 1, 0); - this.material = new THREE.MeshBasicMaterial({ - side: THREE.DoubleSide, - transparent: true - }); +class RayCurve extends THREE.Mesh { + constructor(numPoints, width) { + super( + new THREE.BufferGeometry(), + new THREE.MeshBasicMaterial({ + side: THREE.DoubleSide, + toneMapped: false, + transparent: true + }) + ); + + this.vertices = new Float32Array(numPoints * 3 * 6); + this.width = width; - this.mesh = new THREE.Mesh(this.geometry, this.material); + this.geometry.setAttribute( + "position", + new THREE.BufferAttribute(this.vertices, 3).setUsage(THREE.DynamicDrawUsage) + ); - this.mesh.frustumCulled = false; - this.mesh.vertices = this.vertices; + this.frustumCulled = false; - this.direction = new THREE.Vector3(); - this.numPoints = numPoints; -}; + this.direction = new THREE.Vector3(); + this.numPoints = numPoints; + } -const UP = new THREE.Vector3(0, 1, 0); -RayCurve.prototype = { - setDirection: function(direction) { + setDirection(direction) { this.direction .copy(direction) .cross(UP) .normalize() .multiplyScalar(this.width / 2); - }, + } - setWidth: function(width) { + setWidth(width) { this.width = width; - }, + } - setPoint: (function() { + setPoint = (function() { const A = new THREE.Vector3(); const B = new THREE.Vector3(); const C = new THREE.Vector3(); @@ -107,8 +115,8 @@ RayCurve.prototype = { this.geometry.attributes.position.needsUpdate = true; }; - })() -}; + })(); +} function parabolicCurve(p0, v0, t, out) { out.x = p0.x + v0.x * t; @@ -119,7 +127,7 @@ function parabolicCurve(p0, v0, t, out) { function isValidNormalsAngle(collisionNormal, referenceNormal, landingMaxAngle) { const angleNormals = referenceNormal.angleTo(collisionNormal); - return THREE.Math.RAD2DEG * angleNormals <= landingMaxAngle; + return THREE.MathUtils.RAD2DEG * angleNormals <= landingMaxAngle; } const checkLineIntersection = (function() { @@ -141,7 +149,7 @@ const checkLineIntersection = (function() { const MISS_OPACITY = 0.1; const HIT_OPACITY = 0.3; const MISS_COLOR = 0xff0000; -const HIT_COLOR = 0x00ff00; +const HIT_COLOR = 0x99ff99; const FORWARD = new THREE.Vector3(0, 0, -1); const LANDING_NORMAL = new THREE.Vector3(0, 1, 0); const MAX_LANDING_ANGLE = 45; @@ -167,10 +175,8 @@ AFRAME.registerComponent("teleporter", { this.characterController = this.el.sceneEl.systems["hubs-systems"].characterController; this.isTeleporting = false; this.rayCurve = new RayCurve(20, 0.025); - this.rayCurve.mesh.visible = false; - this.teleportEntity = document.createElement("a-entity"); - this.teleportEntity.setObject3D("mesh", this.rayCurve.mesh); - this.el.sceneEl.appendChild(this.teleportEntity); + this.rayCurve.visible = false; + this.el.sceneEl.object3D.add(this.rayCurve); this.p0 = new THREE.Vector3(); this.v0 = new THREE.Vector3(); @@ -191,8 +197,8 @@ AFRAME.registerComponent("teleporter", { this.prevHitHeight = 0; this.direction = new THREE.Vector3(); this.hitEntity = this.createHitEntity(); - this.hitEntity.object3D.visible = false; - this.el.sceneEl.appendChild(this.hitEntity); + this.hitEntity.visible = false; + this.el.sceneEl.object3D.add(this.hitEntity); this.queryCollisionEntities(); }, @@ -232,11 +238,11 @@ AFRAME.registerComponent("teleporter", { this.isTeleporting = true; this.timeTeleporting = 0; this.hit = false; - this.rayCurve.mesh.visible = true; - this.rayCurve.mesh.updateMatrixWorld(); - this.rayCurve.mesh.material.opacity = MISS_OPACITY; - this.rayCurve.mesh.material.color.set(MISS_COLOR); - this.rayCurve.mesh.material.needsUpdate = true; + this.rayCurve.visible = true; + this.rayCurve.updateMatrixWorld(); + this.rayCurve.material.opacity = MISS_OPACITY; + this.rayCurve.material.color.set(MISS_COLOR); + this.rayCurve.material.needsUpdate = true; this.teleportingSound = sfx.playSoundLoopedWithGain(SOUND_TELEPORT_START); if (this.teleportingSound) { this.teleportingSound.gain.gain.value = 0.005; @@ -251,9 +257,9 @@ AFRAME.registerComponent("teleporter", { } if (userinput.get(confirm)) { - this.hitEntity.setAttribute("visible", false); + this.hitEntity.visible = false; this.isTeleporting = false; - this.rayCurve.mesh.visible = false; + this.rayCurve.visible = false; if (this.teleportingSound) { this.stopPlayingTeleportSound(); @@ -326,85 +332,82 @@ AFRAME.registerComponent("teleporter", { this.rayCurve.material.color.set(color); this.rayCurve.material.opacity = opacity; - this.hitEntity.setAttribute("visible", this.hit); + this.hitEntity.visible = this.hit; if (this.hit) { - this.hitEntity.setAttribute("position", this.hitPoint); - this.hitEntity.object3D.traverse(o => { - o.matrixNeedsUpdate = true; - }); - const hitEntityOpacity = HIT_OPACITY * easeOutIn(percentToDraw); + this.hitEntity.position.copy(this.hitPoint); + this.hitEntity.matrixNeedsUpdate = true; + const dRadii = this.data.outerRadius - this.data.hitCylinderRadius; const outerScale = (this.data.outerRadius - easeIn(percentToDraw) * dRadii) / this.data.outerRadius; - this.outerTorus.object3D.scale.set(outerScale, outerScale, 1); - this.torus.setAttribute("material", "opacity", hitEntityOpacity); - this.cylinder.setAttribute("material", "opacity", hitEntityOpacity); + this.outerTorus.scale.set(outerScale, outerScale, 1); + this.outerTorus.matrixNeedsUpdate = true; + + const hitEntityOpacity = HIT_OPACITY * easeOutIn(percentToDraw); + this.torus.material.opacity = hitEntityOpacity; + this.cylinder.material.opacity = hitEntityOpacity; } }, + // TODO the use of toruses here is a bit wasteful. createHitEntity() { const data = this.data; // Parent. - const hitEntity = document.createElement("a-entity"); - hitEntity.className = "hitEntity"; + const hitEntity = new THREE.Group(); // Torus. - this.torus = document.createElement("a-entity"); - this.torus.setAttribute("geometry", { - primitive: "torus", - radius: data.hitCylinderRadius, - radiusTubular: 0.01, - segmentsRadial: 16, - segmentsTubular: 18 - }); - this.torus.setAttribute("rotation", { x: 90, y: 0, z: 0 }); - this.torus.setAttribute("material", { - shader: "flat", - color: data.hitCylinderColor, - side: "double", - depthTest: false - }); - hitEntity.appendChild(this.torus); + this.torus = new THREE.Mesh( + new THREE.TorusBufferGeometry(data.hitCylinderRadius, 0.01, 16, 18, 360 * THREE.MathUtils.DEG2RAD), + new THREE.MeshBasicMaterial({ + color: data.hitCylinderColor, + side: THREE.DoubleSide, + transparent: true, + toneMapped: false, + depthTest: false + }) + ); + this.torus.rotation.x = 90 * THREE.MathUtils.DEG2RAD; + hitEntity.add(this.torus); // Cylinder. - this.cylinder = document.createElement("a-entity"); - this.cylinder.setAttribute("position", { x: 0, y: data.hitCylinderHeight / 2, z: 0 }); - this.cylinder.setAttribute("geometry", { - primitive: "cylinder", - segmentsHeight: 1, - radius: data.hitCylinderRadius, - height: data.hitCylinderHeight, - openEnded: true - }); - this.cylinder.setAttribute("material", { - shader: "flat", - color: data.hitCylinderColor, - side: "double", - src: CYLINDER_TEXTURE, - transparent: true, - depthTest: false - }); - hitEntity.appendChild(this.cylinder); + this.cylinder = new THREE.Mesh( + new THREE.CylinderBufferGeometry( + data.hitCylinderRadius, + data.hitCylinderRadius, + data.hitCylinderHeight, + 16, + 1, + true + ), + new THREE.MeshBasicMaterial({ + color: data.hitCylinderColor, + side: THREE.DoubleSide, + map: CYLINDER_TEXTURE, + toneMapped: false, + transparent: true, + depthTest: false + }) + ); + this.cylinder.position.y = data.hitCylinderHeight / 2; + // UV's for THREE Geometries assume flipY + if (!CYLINDER_TEXTURE.flipY) { + this.cylinder.rotation.z = 180 * THREE.MathUtils.DEG2RAD; + } + hitEntity.add(this.cylinder); // create another torus for animating when the hit destination is ready to go - this.outerTorus = document.createElement("a-entity"); - this.outerTorus.setAttribute("geometry", { - primitive: "torus", - radius: data.outerRadius, - radiusTubular: 0.01, - segmentsRadial: 16, - segmentsTubular: 18 - }); - this.outerTorus.setAttribute("rotation", { x: 90, y: 0, z: 0 }); - this.outerTorus.setAttribute("material", { - shader: "flat", - color: data.hitCylinderColor, - side: "double", - opacity: HIT_OPACITY, - depthTest: false - }); - this.outerTorus.setAttribute("id", "outerTorus"); - hitEntity.appendChild(this.outerTorus); + this.outerTorus = new THREE.Mesh( + new THREE.TorusBufferGeometry(data.outerRadius, 0.01, 16, 18, 360 * THREE.MathUtils.DEG2RAD), + new THREE.MeshBasicMaterial({ + color: data.hitCylinderColor, + side: THREE.DoubleSide, + opacity: HIT_OPACITY, + transparent: true, + depthTest: false + }) + ); + this.outerTorus.rotation.x = 90 * THREE.MathUtils.DEG2RAD; + hitEntity.add(this.outerTorus); return hitEntity; } diff --git a/src/components/text-button.js b/src/components/text-button.js index 799d3d7b7c..4cddb86061 100644 --- a/src/components/text-button.js +++ b/src/components/text-button.js @@ -25,12 +25,12 @@ AFRAME.registerComponent("text-button", { this.textEl = this.el.querySelector("[text]"); if (this.el.getObject3D("mesh")) { - this.el.components.slice9.material.toneMapped = false; + this.el.components.slice9.plane.material.toneMapped = false; } else { this.el.addEventListener( "object3dset", () => { - this.el.components.slice9.material.toneMapped = false; + this.el.components.slice9.plane.material.toneMapped = false; }, { once: true } ); diff --git a/src/components/tools/networked-drawing.js b/src/components/tools/networked-drawing.js index a7cd83414a..847f5066df 100644 --- a/src/components/tools/networked-drawing.js +++ b/src/components/tools/networked-drawing.js @@ -51,7 +51,7 @@ AFRAME.registerComponent("networked-drawing", { this.networkBufferInitialized = false; const options = { - vertexColors: THREE.VertexColors + vertexColors: true }; this.color = new THREE.Color(); @@ -203,8 +203,9 @@ AFRAME.registerComponent("networked-drawing", { } }; - const glb = await new Promise(resolve => { - exporter.parse(mesh, resolve, { + // TODO: Proper error handling + const glb = await new Promise((resolve, reject) => { + exporter.parse(mesh, resolve, reject, { binary: true, includeCustomExtensions: true }); diff --git a/src/components/troika-text.js b/src/components/troika-text.js index 4c4ea4845c..617d86cf79 100644 --- a/src/components/troika-text.js +++ b/src/components/troika-text.js @@ -7,6 +7,12 @@ import { Text } from "troika-three-text"; // Mark this type of object so we can filter in from our shader patching Text.prototype.isTroikaText = true; +const THREE_SIDES = { + front: THREE.FrontSide, + back: THREE.BackSide, + double: THREE.DoubleSide +}; + function numberOrPercent(defaultValue) { return { default: defaultValue, @@ -52,7 +58,7 @@ AFRAME.registerComponent("text", { depthOffset: { type: "number", default: 0 }, direction: { type: "string", default: "auto", oneOf: ["auto", "ltr", "rtl"] }, fillOpacity: { type: "number", default: 1 }, - // This is different from the Troika preoperty name, Using "fontUrl" to prevent conflict with previous "font" p=roperty and to allow us to make named fonts later + // This is different from the Troika preoperty name, Using "fontUrl" to prevent conflict with previous "font" property and to allow us to make named fonts later fontUrl: { type: "string" }, // This default value differs from the Troika default of 0.1, it most closely matches the size of our previous text component. fontSize: { type: "number", default: 0.075 }, @@ -100,7 +106,7 @@ AFRAME.registerComponent("text", { mesh.anchorX = data.anchorX; mesh.anchorY = data.anchorY; mesh.color = data.color; - mesh.material.side = data.side; + mesh.material.side = THREE_SIDES[data.side]; mesh.material.opacity = data.opacity; mesh.curveRadius = data.curveRadius; mesh.depthOffset = data.depthOffset || 0; diff --git a/src/components/video-texture-target.js b/src/components/video-texture-target.js index 454a476637..4b2df9b539 100644 --- a/src/components/video-texture-target.js +++ b/src/components/video-texture-target.js @@ -35,7 +35,7 @@ AFRAME.registerComponent("video-texture-source", { format: THREE.RGBAFormat, minFilter: THREE.LinearFilter, magFilter: THREE.NearestFilter, - encoding: THREE.GammaEncoding, + encoding: THREE.sRGBEncoding, depth: false, stencil: false }); @@ -72,10 +72,17 @@ AFRAME.registerComponent("video-texture-source", { delete sceneEl.object3D.onAfterRender; renderer.xr.enabled = false; + // The entire scene graph matrices should already be updated + // in tick(). They don't need to be recomputed again in tock(). + const tmpAutoUpdate = sceneEl.object3D.autoUpdate; + sceneEl.object3D.autoUpdate = false; + renderer.setRenderTarget(this.renderTarget); renderer.render(sceneEl.object3D, this.camera); renderer.setRenderTarget(null); + sceneEl.object3D.autoUpdate = tmpAutoUpdate; + renderer.xr.enabled = tmpXRFlag; sceneEl.object3D.onAfterRender = tmpOnAfterRender; diff --git a/src/components/virtual-gamepad-controls.css b/src/components/virtual-gamepad-controls.css index 712603d68a..05903b5dd6 100644 --- a/src/components/virtual-gamepad-controls.css +++ b/src/components/virtual-gamepad-controls.css @@ -10,13 +10,11 @@ } :local(.touchZone.left) { - left: 0; - right: 55%; + left: 80px; } :local(.touchZone.right) { - left: 55%; - right: 0; + right: 80px; } :local(.mockJoystickContainer) { diff --git a/src/components/virtual-gamepad-controls.js b/src/components/virtual-gamepad-controls.js index e359ce0aa6..39b723528c 100644 --- a/src/components/virtual-gamepad-controls.js +++ b/src/components/virtual-gamepad-controls.js @@ -5,6 +5,8 @@ function insertAfter(el, referenceEl) { referenceEl.parentNode.insertBefore(el, referenceEl.nextSibling); } +const ROTATION_SPEED = 0.025; + /** * Instantiates 2D virtual gamepads and emits associated events. * @namespace user-input @@ -79,6 +81,9 @@ AFRAME.registerComponent("virtual-gamepad-controls", { if (!isChanged) { return; } + if ((newEnableLeft || newEnableRight) && !this.mockJoystickContainer.parentNode) { + insertAfter(this.mockJoystickContainer, this.el.sceneEl.canvas); + } if (!this.enableLeft && newEnableLeft) { this.createLeftStick(); } else if (this.enableLeft && !newEnableLeft) { @@ -112,12 +117,8 @@ AFRAME.registerComponent("virtual-gamepad-controls", { this.rightMockSmall.classList.remove(styles.hidden); this.rightStick.on("start", this.onFirstInteraction); } - if ((this.enableLeft || this.enableRight) && !this.mockJoystickContainer.parentNode) { - insertAfter(this.mockJoystickContainer, this.el.sceneEl.canvas); - } - if (!this.enableLeft && !this.enableRight) { - this.mockJoystickContainer.parentNode && - this.mockJoystickContainer.parentNode.removeChild(this.mockJoystickContainer); + if (!this.enableLeft && !this.enableRight && this.mockJoystickContainer.parentNode) { + this.mockJoystickContainer.parentNode.removeChild(this.mockJoystickContainer); } }, @@ -126,10 +127,16 @@ AFRAME.registerComponent("virtual-gamepad-controls", { this.rightTouchZone.classList.add(styles.touchZone, styles.right); insertAfter(this.rightTouchZone, this.mockJoystickContainer); this.rightStick = nipplejs.create({ + mode: "static", + position: { left: "50%", top: "50%" }, zone: this.rightTouchZone, color: "white", fadeTime: 0 }); + // nipplejs sets z-index 999 but it makes the joysticks + // visible even if the scene is hidden for example by + // preference dialog. So remove z-index. + this.rightStick[0].ui.el.style.removeProperty("z-index"); this.rightStick.on("start", this.onFirstInteraction); this.rightStick.on("move", this.onLookJoystickChanged); this.rightStick.on("end", this.onLookJoystickEnd); @@ -140,10 +147,13 @@ AFRAME.registerComponent("virtual-gamepad-controls", { this.leftTouchZone.classList.add(styles.touchZone, styles.left); insertAfter(this.leftTouchZone, this.mockJoystickContainer); this.leftStick = nipplejs.create({ + mode: "static", + position: { left: "50%", top: "50%" }, zone: this.leftTouchZone, color: "white", fadeTime: 0 }); + this.leftStick[0].ui.el.style.removeProperty("z-index"); this.leftStick.on("start", this.onFirstInteraction); this.leftStick.on("move", this.onMoveJoystickChanged); this.leftStick.on("end", this.onMoveJoystickEnd); @@ -174,10 +184,9 @@ AFRAME.registerComponent("virtual-gamepad-controls", { // Set pitch and yaw angles on right stick move const angle = joystick.angle.radian; const force = joystick.force < 1 ? joystick.force : 1; - const turnStrength = 0.05; this.rotating = true; - this.lookDy = -Math.cos(angle) * force * turnStrength; - this.lookDx = Math.sin(angle) * force * turnStrength; + this.lookDy = -Math.cos(angle) * force * ROTATION_SPEED; + this.lookDx = Math.sin(angle) * force * ROTATION_SPEED; }, onLookJoystickEnd() { diff --git a/src/gltf-component-mappings.js b/src/gltf-component-mappings.js index 9fc735e53f..2cdda43587 100644 --- a/src/gltf-component-mappings.js +++ b/src/gltf-component-mappings.js @@ -6,24 +6,6 @@ const COLLISION_LAYERS = require("./constants").COLLISION_LAYERS; import { AudioType, DistanceModelType, SourceType } from "./components/audio-params"; import { updateAudioSettings } from "./update-audio-settings"; -function registerRootSceneComponent(componentName) { - AFRAME.GLTFModelPlus.registerComponent(componentName, componentName, (el, componentName, componentData) => { - const sceneEl = AFRAME.scenes[0]; - - sceneEl.setAttribute(componentName, componentData); - - sceneEl.addEventListener( - "reset_scene", - () => { - sceneEl.removeAttribute(componentName); - }, - { once: true } - ); - }); -} - -registerRootSceneComponent("fog"); - AFRAME.GLTFModelPlus.registerComponent("duck", "duck", el => { el.setAttribute("duck", ""); el.setAttribute("quack", { quackPercentage: 0.1 }); @@ -597,6 +579,21 @@ AFRAME.GLTFModelPlus.registerComponent("background", "background", (el, _compone el.setAttribute("environment-settings", { backgroundColor: new THREE.Color(componentData.color) }); }); +AFRAME.GLTFModelPlus.registerComponent("fog", "fog", (el, _componentName, componentData) => { + // TODO need to actually implement this in blender exporter before showing this warning + // console.warn( + // "The `fog` component is deprecated, use the fog properties on the `environment-settings` component instead." + // ); + // This assumes the fog component is on the root entitycoco + el.setAttribute("environment-settings", { + fogType: componentData.type, + fogColor: new THREE.Color(componentData.color), + fogNear: componentData.near, + fogFar: componentData.far, + fogDensity: componentData.density + }); +}); + AFRAME.GLTFModelPlus.registerComponent( "environment-settings", "environment-settings", diff --git a/src/hub.html b/src/hub.html index a67972d580..4197a4e050 100644 --- a/src/hub.html +++ b/src/hub.html @@ -5,7 +5,7 @@ - + @@ -55,14 +55,11 @@
    - - - - - - - - - - + + +