diff --git a/.babelrc b/.babelrc
index f1f71f8b36..a2e1b77b2f 100644
--- a/.babelrc
+++ b/.babelrc
@@ -34,9 +34,6 @@
"transform-react-jsx-img-import",
["@babel/proposal-class-properties", { "loose": true }],
"@babel/proposal-object-rest-spread",
- // Samsung Internet on the Oculus Go version is stuck at version 5.2, which is a
- // Chromium 51, as of this writing. It needs babel to transpile async/await.
- "@babel/plugin-transform-async-to-generator",
"@babel/plugin-proposal-optional-chaining"
]
}
diff --git a/.defaults.env b/.defaults.env
index 5f5a0da627..5a0293b808 100644
--- a/.defaults.env
+++ b/.defaults.env
@@ -29,3 +29,7 @@ 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"
+IMMERS_SCOPE="modAdditive"
+IMMERS_ALLOW_GUESTS="true"
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/.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/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000..7319276401
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,18 @@
+FROM node:14
+
+WORKDIR /usr/src/hubs
+COPY package*.json ./
+
+WORKDIR /usr/src/hubs/admin
+COPY admin/package*.json ./
+RUN npm ci
+
+WORKDIR /usr/src/hubs
+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
new file mode 100755
index 0000000000..8cd6e13800
--- /dev/null
+++ b/dockerdeploy.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -e
+echo "Logging into to hub $hub as $email"
+npm run login -- --host $hub --email $email
+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/package-lock.json b/package-lock.json
index 2573babc75..2f5b04f53f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "hubs",
- "version": "0.0.1",
+ "version": "1.10.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -6971,6 +6971,11 @@
"react-lifecycles-compat": "^3.0.4"
}
},
+ "@socket.io/component-emitter": {
+ "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",
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.1.15.tgz",
@@ -19954,7 +19959,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",
@@ -21332,6 +21339,11 @@
"domelementtype": "1"
}
},
+ "dompurify": {
+ "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",
"resolved": "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz",
@@ -21571,23 +21583,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",
@@ -21746,6 +21741,43 @@
"objectorarray": "^1.0.4"
}
},
+ "engine.io-client": {
+ "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.1.0",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.0.3",
+ "ws": "~8.2.3",
+ "xmlhttprequest-ssl": "~2.0.0"
+ },
+ "dependencies": {
+ "debug": {
+ "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"
+ }
+ },
+ "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.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",
"resolved": "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz",
@@ -24618,6 +24650,23 @@
"integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==",
"dev": true
},
+ "immers-client": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/immers-client/-/immers-client-2.12.0.tgz",
+ "integrity": "sha512-GAbg7vCDyJ66Cm13TZrwC/Hwx8uxYCc22mhplRQ9JFfrjnlW8zpFckEoab9/Bf3jawfXcXh46fZgYn8lW1mSUg==",
+ "requires": {
+ "core-js": "^3.17.2",
+ "dompurify": "^2.3.6",
+ "socket.io-client": "^4.0.0"
+ },
+ "dependencies": {
+ "core-js": {
+ "version": "3.26.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz",
+ "integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw=="
+ }
+ }
+ },
"immutable": {
"version": "3.7.6",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz",
@@ -25031,6 +25080,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",
@@ -26482,6 +26537,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",
@@ -26541,6 +26605,11 @@
"uc.micro": "^1.0.1"
}
},
+ "linkifyjs": {
+ "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",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
@@ -27116,14 +27185,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
+ }
}
},
"memoizerific": {
@@ -27592,6 +27669,11 @@
"dev": true,
"optional": true
},
+ "nanoid": {
+ "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",
"resolved": "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz",
@@ -27691,12 +27773,12 @@
"version": "github:mozillareality/networked-aframe#a691b90cd1c817283fbd4ce32f63b0b184311f4c",
"from": "github:mozillareality/networked-aframe#master",
"requires": {
- "buffered-interpolation": "github:Infinitelee/buffered-interpolation#5bb18421ebf2bf11664645cdc7a15bd77ee2156b",
- "easyrtc": "1.1.0"
+ "buffered-interpolation": "github:Infinitelee/buffered-interpolation#5bb18421ebf2bf11664645cdc7a15bd77ee2156b"
},
"dependencies": {
"buffered-interpolation": {
- "version": "github:infinitelee/buffered-interpolation#5bb18421ebf2bf11664645cdc7a15bd77ee2156b"
+ "version": "github:Infinitelee/buffered-interpolation#5bb18421ebf2bf11664645cdc7a15bd77ee2156b",
+ "from": "github:Infinitelee/buffered-interpolation#5bb18421ebf2bf11664645cdc7a15bd77ee2156b"
}
}
},
@@ -28643,21 +28725,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",
@@ -28961,6 +29028,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": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="
+ },
"parse-url": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/parse-url/-/parse-url-5.0.2.tgz",
@@ -29140,6 +29212,11 @@
"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",
@@ -29296,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",
@@ -33529,6 +33601,95 @@
}
}
},
+ "sanitize-html": {
+ "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",
+ "htmlparser2": "^6.0.0",
+ "is-plain-object": "^5.0.0",
+ "parse-srcset": "^1.0.2",
+ "postcss": "^8.3.11"
+ },
+ "dependencies": {
+ "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.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",
+ "entities": "^2.0.0"
+ }
+ },
+ "domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="
+ },
+ "domhandler": {
+ "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"
+ }
+ },
+ "domutils": {
+ "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.2.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=="
+ },
+ "postcss": {
+ "version": "8.4.16",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz",
+ "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==",
+ "requires": {
+ "nanoid": "^3.3.4",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ }
+ }
+ }
+ },
"sass": {
"version": "1.26.10",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.26.10.tgz",
@@ -34018,6 +34179,56 @@
"kind-of": "^3.2.0"
}
},
+ "socket.io-client": {
+ "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.1.0",
+ "debug": "~4.3.2",
+ "engine.io-client": "~6.2.1",
+ "socket.io-parser": "~4.2.0"
+ },
+ "dependencies": {
+ "debug": {
+ "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"
+ }
+ },
+ "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": "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.1.0",
+ "debug": "~4.3.1"
+ },
+ "dependencies": {
+ "debug": {
+ "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"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ }
+ }
+ },
"sockjs": {
"version": "0.3.19",
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz",
@@ -34089,6 +34300,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",
@@ -34308,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": {
@@ -36628,7 +36843,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",
@@ -37491,6 +37707,11 @@
"defaults": "^1.0.3"
}
},
+ "web-monetization-polyfill": {
+ "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",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz",
@@ -37958,6 +38179,15 @@
"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"
+ }
+ },
"default-gateway": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-2.7.2.tgz",
@@ -37983,6 +38213,15 @@
"strip-eof": "^1.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"
+ }
+ },
"internal-ip": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-3.0.1.tgz",
@@ -37993,12 +38232,46 @@
"ipaddr.js": "^1.5.2"
}
},
+ "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",
@@ -38015,6 +38288,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"
+ }
}
}
},
@@ -38375,6 +38677,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",
@@ -38412,97 +38719,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.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"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.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"requires": {
- "p-try": "^2.0.0"
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
}
},
- "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.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"requires": {
- "p-limit": "^2.0.0"
+ "ansi-regex": "^5.0.1"
}
},
- "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.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"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.9",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+ "dev": true
}
}
},
@@ -38545,4 +38876,4 @@
"dev": true
}
}
-}
\ No newline at end of file
+}
diff --git a/package.json b/package.json
index 04621c2dc2..a63edff604 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "hubs",
- "version": "0.0.1",
+ "version": "1.10.1",
"description": "Duck-themed multi-user virtual spaces in WebVR.",
"main": "src/index.js",
"license": "MPL-2.0",
@@ -27,6 +27,9 @@
"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",
+ "immers-build:image": "docker build -t immersspace/hubs .",
+ "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",
@@ -104,11 +107,13 @@
"history": "^4.7.2",
"hls.js": "^0.14.6",
"html2canvas": "^1.0.0-rc.7",
+ "immers-client": "^2.12.0",
"js-cookie": "^2.2.0",
"jsonschema": "^1.2.2",
"jwt-decode": "^2.2.0",
"lib-hubs": "github:mozillareality/lib-hubs#master",
"linkify-it": "^2.0.3",
+ "linkifyjs": "^3.0.0-beta.3",
"markdown-it": "^8.4.2",
"moving-average": "^1.0.0",
"networked-aframe": "github:mozillareality/networked-aframe#master",
@@ -132,9 +137,11 @@
"react-textarea-autosize": "^8.2.0",
"react-use-css-breakpoints": "^1.0.4",
"resize-observer-polyfill": "^1.5.1",
+ "sanitize-html": "^2.3.2",
"screenfull": "^4.0.1",
"sdp-transform": "^2.14.1",
"semver": "^7.3.2",
+ "socket.io-client": "^4.0.0",
"three": "github:mozillareality/three.js#hubs-patches-133",
"three-ammo": "github:infinitelee/three-ammo",
"three-mesh-bvh": "^0.3.7",
@@ -143,6 +150,7 @@
"troika-three-text": "^0.45.0",
"use-clipboard-copy": "^0.1.2",
"uuid": "^3.2.1",
+ "web-monetization-polyfill": "^2.0.0",
"webrtc-adapter": "^7.7.0",
"webxr-polyfill": "^2.0.3",
"zip-loader": "^1.1.0"
@@ -219,7 +227,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 1b3c0fa4ad..d2fa80e3fd 100644
--- a/scripts/deploy.js
+++ b/scripts/deploy.js
@@ -6,13 +6,22 @@ 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, 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();
@@ -31,74 +40,123 @@ 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 of ["./dist", "./admin/dist"]) {
- rmdir(d, err => {
- if (err) {
- console.error(err);
- process.exit(1);
- }
- });
- }
+ for (const d of ["./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) => {
- 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) => {
- 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.";
diff --git a/scripts/immers-configure.js b/scripts/immers-configure.js
new file mode 100644
index 0000000000..a7ef45b2e6
--- /dev/null
+++ b/scripts/immers-configure.js
@@ -0,0 +1,70 @@
+import { readFileSync, existsSync } from "fs";
+// 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 ENV: domain, monetizationPointer");
+ 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:"
+ },
+ security: {
+ // 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 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;
+ });
+ 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`, {
+ headers,
+ method: "POST",
+ body: JSON.stringify({
+ features: {
+ // 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));
+ process.exit(0);
+})();
diff --git a/scripts/login.js b/scripts/login.js
index 6d013b9a3f..b741e916ef 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,8 +15,8 @@ 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): ");
- if (!host) {
+ const host = argv.host || (await ask("Host (eg hubs.mozilla.com): "));
+ if (!host || host === true) {
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);
diff --git a/src/assets/images/immers_logo.png b/src/assets/images/immers_logo.png
new file mode 100644
index 0000000000..d776333002
Binary files /dev/null and b/src/assets/images/immers_logo.png differ
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 0000000000..454430e803
Binary files /dev/null and b/src/assets/images/sprites/action/immers-action.png differ
diff --git a/src/assets/images/sprites/action/immers-bg-hover.png b/src/assets/images/sprites/action/immers-bg-hover.png
new file mode 100644
index 0000000000..c123567f98
Binary files /dev/null and b/src/assets/images/sprites/action/immers-bg-hover.png differ
diff --git a/src/assets/images/sprites/action/immers-bg.png b/src/assets/images/sprites/action/immers-bg.png
new file mode 100644
index 0000000000..15521bbd03
Binary files /dev/null and b/src/assets/images/sprites/action/immers-bg.png differ
diff --git a/src/assets/images/spritesheets/sprite-system-action-spritesheet.json b/src/assets/images/spritesheets/sprite-system-action-spritesheet.json
index 523e941f10..225f6a5152 100644
--- a/src/assets/images/spritesheets/sprite-system-action-spritesheet.json
+++ b/src/assets/images/spritesheets/sprite-system-action-spritesheet.json
@@ -285,7 +285,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "inspect-action.png":
+ "immers-action.png":
{
"frame": {"x":328,"y":738,"w":64,"h":64},
"rotated": false,
@@ -293,7 +293,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "mute-action.png":
+ "immers-bg-hover.png":
{
"frame": {"x":408,"y":738,"w":64,"h":64},
"rotated": false,
@@ -301,7 +301,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "next.png":
+ "immers-bg.png":
{
"frame": {"x":488,"y":738,"w":64,"h":64},
"rotated": false,
@@ -309,7 +309,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "pin-action.png":
+ "inspect-action.png":
{
"frame": {"x":568,"y":738,"w":64,"h":64},
"rotated": false,
@@ -317,7 +317,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "prev.png":
+ "mute-action.png":
{
"frame": {"x":648,"y":738,"w":64,"h":64},
"rotated": false,
@@ -325,7 +325,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "recenter-action.png":
+ "next.png":
{
"frame": {"x":728,"y":738,"w":64,"h":64},
"rotated": false,
@@ -333,7 +333,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "record-action-alpha.png":
+ "pin-action.png":
{
"frame": {"x":882,"y":8,"w":64,"h":64},
"rotated": false,
@@ -341,7 +341,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "record-action.png":
+ "prev.png":
{
"frame": {"x":882,"y":88,"w":64,"h":64},
"rotated": false,
@@ -349,7 +349,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "remove-action.png":
+ "recenter-action.png":
{
"frame": {"x":882,"y":168,"w":64,"h":64},
"rotated": false,
@@ -357,7 +357,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "rotate-action.png":
+ "record-action-alpha.png":
{
"frame": {"x":882,"y":248,"w":64,"h":64},
"rotated": false,
@@ -365,7 +365,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "scale-action.png":
+ "record-action.png":
{
"frame": {"x":882,"y":328,"w":64,"h":64},
"rotated": false,
@@ -373,7 +373,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "serialize-action.png":
+ "remove-action.png":
{
"frame": {"x":882,"y":408,"w":64,"h":64},
"rotated": false,
@@ -381,7 +381,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "snap-page.png":
+ "rotate-action.png":
{
"frame": {"x":882,"y":488,"w":64,"h":64},
"rotated": false,
@@ -389,7 +389,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "spawn_message.png":
+ "scale-action.png":
{
"frame": {"x":882,"y":568,"w":64,"h":64},
"rotated": false,
@@ -397,7 +397,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "spawn_message_dark-hover.png":
+ "serialize-action.png":
{
"frame": {"x":882,"y":648,"w":64,"h":64},
"rotated": false,
@@ -405,7 +405,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "spawn_message_dark.png":
+ "snap-page.png":
{
"frame": {"x":882,"y":728,"w":64,"h":64},
"rotated": false,
@@ -413,7 +413,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "stop-action.png":
+ "spawn_message.png":
{
"frame": {"x":8,"y":818,"w":64,"h":64},
"rotated": false,
@@ -421,7 +421,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "undo-action.png":
+ "spawn_message_dark-hover.png":
{
"frame": {"x":88,"y":818,"w":64,"h":64},
"rotated": false,
@@ -429,13 +429,37 @@
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
},
- "unmute-action.png":
+ "spawn_message_dark.png":
{
"frame": {"x":168,"y":818,"w":64,"h":64},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
"sourceSize": {"w":64,"h":64}
+ },
+ "stop-action.png":
+ {
+ "frame": {"x":248,"y":818,"w":64,"h":64},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
+ "sourceSize": {"w":64,"h":64}
+ },
+ "undo-action.png":
+ {
+ "frame": {"x":328,"y":818,"w":64,"h":64},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
+ "sourceSize": {"w":64,"h":64}
+ },
+ "unmute-action.png":
+ {
+ "frame": {"x":408,"y":818,"w":64,"h":64},
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64},
+ "sourceSize": {"w":64,"h":64}
}
}
}
\ No newline at end of file
diff --git a/src/assets/images/spritesheets/sprite-system-action-spritesheet.png b/src/assets/images/spritesheets/sprite-system-action-spritesheet.png
index c215d6e77d..36c1040b2b 100644
Binary files a/src/assets/images/spritesheets/sprite-system-action-spritesheet.png and b/src/assets/images/spritesheets/sprite-system-action-spritesheet.png differ
diff --git a/src/assets/images/spritesheets/sprite-system-notice-spritesheet.png b/src/assets/images/spritesheets/sprite-system-notice-spritesheet.png
index 46bc3764e0..dcb2df162c 100644
Binary files a/src/assets/images/spritesheets/sprite-system-notice-spritesheet.png and b/src/assets/images/spritesheets/sprite-system-notice-spritesheet.png differ
diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json
new file mode 100644
index 0000000000..9b00821f23
--- /dev/null
+++ b/src/assets/translations.data.json
@@ -0,0 +1,429 @@
+{
+ "en": {
+ "app-name": "App",
+ "editor-name": "Scene Editor",
+ "contact-email": "app@company.com",
+ "company-name": "Company",
+ "share-hashtag": "#app",
+ "app-description": "Share a virtual room with friends.\nWatch videos, play with 3D objects, or just hang out.",
+ "app-tagline": "Private social VR in your web browser",
+ "auth.verified-title": "Email Verified!",
+ "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 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.",
+ "sign-in.auth-started": "Email sent!\n\nTo continue, click on the link in the email using your phone, tablet, or PC.\n\nNo email? You may not be able to create an account.",
+ "sign-in.pin": "You'll need to sign in to pin objects.",
+ "sign-in.pin-complete": "You are now signed in.",
+ "sign-in.unpin": "You'll need to sign in to un-pin objects.",
+ "sign-in.unpin-complete": "You are now signed in.",
+ "sign-in.change-scene": "You'll need to sign in to change the scene.",
+ "sign-in.change-scene-complete": "You are now signed in.",
+ "sign-in.room-settings": "You'll need to sign in to change the room's settings.",
+ "sign-in.room-settings-complete": "You are now signed in.",
+ "sign-in.close-room": "You'll need to sign in to close the room.",
+ "sign-in.close-room-complete": "You are now signed in.",
+ "sign-in.mute-user": "You'll need to sign in to mute other users.",
+ "sign-in.mute-user-complete": "You are now signed in.",
+ "sign-in.kick-user": "You'll need to sign in to kick other users.",
+ "sign-in.kick-user-complete": "You are now signed in.",
+ "sign-in.add-owner": "You'll need to sign in to assign moderators.",
+ "sign-in.add-owner-complete": "You are now signed in.",
+ "sign-in.remove-owner": "You'll need to sign in to assign moderators.",
+ "sign-in.remove-owner-complete": "You are now signed in.",
+ "sign-in.create-avatar": "You'll need to sign in to create avatars.",
+ "sign-in.create-avatar-complete": "You are now signed in.",
+ "sign-in.favorite-room": "You'll need to sign in to add this room to your favorites.",
+ "sign-in.favorite-rooms": "You'll need to sign in to add favorite rooms.",
+ "sign-in.favorite-room-complete": "You are now signed in.",
+ "sign-in.favorite-rooms-complete": "You are now signed in.",
+ "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.out": "Sign Out",
+ "room-settings.apply": "Apply",
+ "room-settings.name-subtitle": "Room Name",
+ "room-settings.description-subtitle": "Room Description",
+ "room-settings.room-access-subtitle": "Room Access",
+ "room-settings.permissions-subtitle": "Room Member Permissions",
+ "room-settings.room-size-subtitle": "Room Size",
+ "room-settings.spawn_and_move_media": "Create and move objects",
+ "room-settings.spawn_camera": "Create cameras",
+ "room-settings.spawn_drawing": "Create drawings",
+ "room-settings.pin_objects": "Pin objects",
+ "room-settings.spawn_emoji": "Create emoji",
+ "room-settings.fly": "Allow flying",
+ "room-settings.access-private": "Private",
+ "room-settings.access-private-subtitle": "Only those with the link can join",
+ "room-settings.access-public": "Public",
+ "room-settings.access-public-subtitle": "Listed on the homepage",
+ "room-info.title": "Room & Scene Info",
+ "room-info.scene-info": "Scene Info",
+ "close-room.message": "Closing this room will remove yourself and others from the room, shutting it down permanently.\n\nAre you sure? This action cannot be undone.",
+ "close-room.confirm": "Yes, Close Room",
+ "close-room.cancel": "Cancel",
+ "promote.message": "Promoting a user will grant full access to room settings and moderation tools.\n\nAre you sure?",
+ "promote.confirm-prefix": "Yes, Promote ",
+ "promote.cancel": "Cancel",
+ "entry.room": "lobby",
+ "entry.enter-room-title": "Lobby",
+ "entry.enter-room": "Enter Room",
+ "entry.leave-room": "Leave Room",
+ "entry.change-scene": "Choose Scene",
+ "entry.in-lobby-notice": "You are viewing this room from the lobby.",
+ "entry.screen-prefix": "Enter on ",
+ "entry.desktop-screen": "Screen",
+ "entry.mobile-screen": "Phone",
+ "entry.mobile-safari": "Safari",
+ "entry.generic-prefix": " ",
+ "entry.generic-medium": "Connected VR Headset",
+ "entry.generic-subtitle-desktop": "Oculus or SteamVR",
+ "entry.gearvr-prefix": "Enter on ",
+ "entry.gearvr-medium": "Gear VR",
+ "entry.choose-device": "Choose Device",
+ "entry.device-medium": "Enter on Standalone VR",
+ "entry.device-subtitle-desktop": "Wireless VR Headsets",
+ "entry.device-subtitle-mobile": "Wireless VR Headsets",
+ "entry.device-subtitle-vr": "Phone or PC",
+ "entry.entry-disallowed": "Watch from Lobby",
+ "entry.entry-disallowed-subtitle": "Room is Full",
+ "entry.watch-from-lobby": "Watch from Lobby",
+ "entry.watch-from-lobby-subtitle": "Others will not be able to see or hear you",
+ "entry.cardboard": "Enter on Google Cardboard",
+ "entry.checkingForDeviceAvailability": "Checking for device availability...",
+ "entry.daydream-prefix": "Enter on ",
+ "entry.daydream-medium": "Daydream",
+ "entry.daydream-via-chrome": "Using Google Chrome",
+ "entry.invite-others": "invite others",
+ "entry.share-button": "Share",
+ "entry.desktop.invite-tip": "Nobody is here yet. Share this room to get together.",
+ "entry.mobile.invite-tip": "Share to get together.",
+ "entry.return-to-vr": "Return to VR",
+ "entry.open-in-window": "Open in Tab",
+ "entry.lobby": "Lobby",
+ "entry.back": "Back",
+ "entry.notify_me": "Notify me when others arrive",
+ "entry.mute-on-entry": "Mute my microphone",
+ "profile.save": "Accept",
+ "profile.display_name.validation_warning": "Alphanumerics and hyphens. At least 3 characters, no more than 32",
+ "profile.header": "Name & Avatar",
+ "profile.terms_of_use": "Terms of Use",
+ "profile.privacy_notice": "Privacy Notice",
+ "profile.choose_avatar": "Browse Avatars",
+ "profile.tabs.legacy": "Default",
+ "profile.tabs.skinnable": "Custom Skin",
+ "profile.tabs.url": "Custom Model",
+ "avatar-editor.info": "Find more custom avatar resources",
+ "avatar-editor.info-link": "here",
+ "avatar-editor.external-editor-info": "Create a custom skin for this avatar: ",
+ "avatar-preview.loading-failed": "Loading failed\nPlease choose another avatar",
+ "media-browser.search-placeholder.scenes": "Search Scenes...",
+ "media-browser.search-placeholder.avatars": "Search Avatars...",
+ "media-browser.search-placeholder.videos": "Search for Videos...",
+ "media-browser.search-placeholder.images": "Search for Images...",
+ "media-browser.search-placeholder.youtube": "Search for Youtube videos...",
+ "media-browser.search-placeholder.gifs": "Search for GIFs...",
+ "media-browser.search-placeholder.twitch": "Search for Twitch streams...",
+ "media-browser.search-placeholder.sketchfab": "Search Sketchfab Models...",
+ "media-browser.search-placeholder.poly": "Search Google Poly Models...",
+ "media-browser.search-placeholder.base": "Search...",
+ "media-browser.favorites-header": "Favorite Rooms",
+ "media-browser.add_custom_object": "Custom URL or File",
+ "media-browser.add_custom_scene": "Custom Scene",
+ "media-browser.add_custom_avatar": "Avatar GLB URL",
+ "media-browser.privacy_policy": "Privacy Policy",
+ "media-browser.report_issue": "Report Issue",
+ "media-browser.powered_by.images": "Search by Bing | ",
+ "media-browser.powered_by.videos": "Search by Bing | ",
+ "media-browser.powered_by.youtube": "Search by Google | ",
+ "media-browser.powered_by.gifs": "Search by Tenor | ",
+ "media-browser.powered_by.sketchfab": "Search by Sketchfab | ",
+ "media-browser.powered_by.poly": "Search by Google | ",
+ "media-browser.powered_by.twitch": "Search by Twitch | ",
+ "media-browser.powered_by.scenes": "Made with ",
+ "media-browser.powered_by.avatars": " ",
+ "media-browser.empty.images": "No results. Try entering a new search above.",
+ "media-browser.empty.videos": "No results. Try entering a new search above.",
+ "media-browser.empty.youtube": "No results. Try entering a new search above.",
+ "media-browser.empty.gifs": "No result. Try entering a new search above.",
+ "media-browser.empty.sketchfab": "No results. Try entering a new search above.",
+ "media-browser.empty.poly": "No results. Try entering a new search above.",
+ "media-browser.empty.twitch": "No results. Try entering a new search above.",
+ "media-browser.empty.favorites": "You don't have any favorites. Click a ⭐ to add to your favorites.",
+ "media-browser.nav_title.youtube": "YouTube",
+ "media-browser.nav_title.videos": "Videos",
+ "media-browser.nav_title.images": "Images",
+ "media-browser.nav_title.gifs": "GIFs",
+ "media-browser.nav_title.scenes": "Scenes",
+ "media-browser.nav_title.avatars": "Avatars",
+ "media-browser.nav_title.sketchfab": "Sketchfab",
+ "media-browser.nav_title.poly": "Google Poly",
+ "media-browser.nav_title.twitch": "Twitch",
+ "media-browser.create-avatar": "Create Avatar",
+ "media-browser.create-scene": "Create Scene with %editor-name%",
+ "media-browser.hub.joined-prefix": "Joined ",
+ "media-browser.hub.joined-prefix": "Visited ",
+ "media-browser.similar-to-facet": "Similar to: \"{name}\"",
+ "audio.title": "Audio Setup",
+ "audio.talk_to_test": "talk",
+ "audio.click_to_test": "click",
+ "audio.subtitle-desktop": "Confirm HMD speaker output",
+ "audio.subtitle-mobile": "Earphones are recommended",
+ "audio.enter-now": "Enter Now",
+ "audio.hmd-mic-warning": "Your HMD mic is not chosen",
+ "audio.grant-title": "Grant mic permissions",
+ "audio.grant-subtitle": "Mic access needed to be heard by others",
+ "audio.granted-title": "Mic permissions granted",
+ "audio.granted-subtitle": "You can still mute yourself in-game",
+ "audio.granted-next": "Next",
+ "exit.subtitle.exited": "Your session has ended. Refresh your browser to start a new one.",
+ "exit.subtitle.closed": "This room is no longer available.",
+ "exit.subtitle.denied": "You are not permitted to join this room. Please request permission from the room creator.",
+ "exit.subtitle.disconnected": "You have disconnected from the room. Refresh the page to try to reconnect.",
+ "exit.subtitle.left": "You have left the room.",
+ "exit.subtitle.full": "This room is full, please try again later.",
+ "exit.subtitle.scene_error": "The scene failed to load.",
+ "exit.subtitle.connect_error": "Unable to connect to this room, please try again later.",
+ "exit.subtitle.version_mismatch": "The version you deployed is not available yet. Your browser will refresh in 5 seconds.",
+ "autoexit.title": "Auto-ending session in ",
+ "autoexit.title_units": " seconds",
+ "autoexit.concurrent_subtitle": "You have started another session.",
+ "autoexit.idle_subtitle": "You have been idle for too long.",
+ "autoexit.cancel": "CANCEL",
+ "presence.entered_room": "entered the room.",
+ "presence.entered_lobby": "entered the lobby.",
+ "presence.join_lobby": "joined the lobby.",
+ "presence.join_room": "joined the room.",
+ "presence.leave": "left.",
+ "presence.name_change": "is now known as",
+ "presence.scene_change": "changed the scene to",
+ "presence.hub_name_change": "changed the name of the room to",
+ "presence.in_lobby": "Lobby",
+ "presence.entering": "Entering Room",
+ "presence.in_room": "In Room",
+ "home.create_a_room": "Create a Room",
+ "home.desktop.add_pwa": "Install Desktop App",
+ "home.mobile.add_pwa": "Add to Home Screen",
+ "home.take_a_tour": "Take a Tour",
+ "home.room_create_options": "options",
+ "home.room_create_button": "Create Room",
+ "home.create_name.validation_warning": "Invalid name, limited to 4 to 64 characters and limited symbols.",
+ "home.powered_by_prefix": "Powered by",
+ "home.powered_by_link": "Hubs Cloud",
+ "home.join_us": "Join the Conversation",
+ "home.subscribe_to_mailing_list": "Subscribe for Updates",
+ "home.have_code": "Have a room code?",
+ "home.add_to_discord_1": "Add the",
+ "home.add_to_discord_2": "%app-name% Bot",
+ "home.add_to_discord_3": "to Discord",
+ "home.create_with_spoke": "Create a Scene",
+ "home.report_issue": "Report Issues",
+ "home.source_link": "Source",
+ "home.whats_new_link": "What's New",
+ "home.about_link": "About",
+ "home.community_link": "Community",
+ "home.docs_link": "Docs",
+ "home.cloud_link": "Hubs Cloud",
+ "home.admin": "Admin",
+ "home.privacy_notice": "Privacy Notice",
+ "home.terms_of_use": "Terms of Use",
+ "home.made_with_love": "made with 🦆 by ",
+ "home.environment_author_by": " by ",
+ "dialog.close": "close",
+ "link.link_page_header_entry": "Enter your code:",
+ "link.link_page_header_headset": "Enter code:",
+ "link.linking_a_headset": "Have a letter code?",
+ "link.create_a_room": "Create a new room",
+ "link.try_again": "We couldn't find that code.\nPlease try again.",
+ "help.report_issue": "Report an Issue",
+ "scene.create_button": "Create a room with this scene",
+ "scene.tweet_button": "Share on Twitter",
+ "scene.unavailable": "This scene is no longer available.",
+ "scene.remix_button": "Remix in %editor-name%",
+ "scene.edit_button": "Edit in %editor-name%",
+ "link.in_your_browser": "In your device's web browser, go to:",
+ "link.enter_code": "Then, enter this one-time letter code:",
+ "link.do_not_close": "Your account and avatar will be transferred to the device.\nKeep this page open to use this code.",
+ "link.connect_headset": "Enter on Device",
+ "link.cancel": "cancel",
+ "invite.enter_via": "Or use expiring ",
+ "invite.enter_via_modal": "Others may enter via ",
+ "invite.tweet": "tweet",
+ "invite.and_enter_code": " code:",
+ "invite.duration_of_code" : "new code every 72 hours",
+ "invite.or_visit": "Share room link:",
+ "invite.or_visit_modal": "or by visiting permalink",
+ "invite.embed": "or embed on a page",
+ "invite.embed-tip": "Please be mindful of where you embed a %app-name% room.\nGo to Room Settings to lock down permissions for this room.",
+ "commands.fly": "Toggle fly mode.",
+ "commands.grow": "Increase your avatar's size.",
+ "commands.shrink": "Decrease your avatar's size.",
+ "commands.scene": "Change the scene.",
+ "commands.rename": "Rename the room.",
+ "commands.help": "Show help.",
+ "commands.leave": "Disconnect from the room.",
+ "commands.duck": "The duck tested well. Quack.",
+ "commands.capture": "Capture a 15 second video.",
+ "commands.debug": "Toggle physics debug rendering.",
+ "commands.vrstats": "Toggle stats in VR.",
+ "commands.audiomode": "(experimental) Toggle positional audio.",
+ "preferences.muteMicOnEntry": "Mute microphone on entry",
+ "preferences.enableOnScreenJoystickLeft": "Enable left on-screen joystick for moving around",
+ "preferences.enableOnScreenJoystickRight": "Enable right on-screen joystick for looking around",
+ "preferences.onlyShowNametagsInFreeze": "Only show nametags while frozen",
+ "preferences.maxResolution": "Max Resolution (width x height in pixels)",
+ "preferences.globalVoiceVolume": "Incoming Voice Volume",
+ "preferences.globalMediaVolume": "Media Volume",
+ "preferences.disableSoundEffects": "Disable Sound Effects",
+ "preferences.materialQualitySetting": "Material quality (requires restart)",
+ "preferences.snapRotationDegrees": "Rotation per snap (in degrees)",
+ "preferences.allowMultipleHubsInstances": "Disable auto-exit when multiple hubs instances are open",
+ "preferences.disableIdleDetection": "Disable auto-exit when idle or backgrounded",
+ "preferences.preferMobileObjectInfoPanel": "Prefer Mobile Object Info Panel",
+ "preferences.baseMovementSpeed": "Base movement speed",
+ "preferences.disableMovement": "Disable movement",
+ "preferences.disableBackwardsMovement": "Disable backwards movement",
+ "preferences.disableStrafing": "Disable strafing",
+ "preferences.disableTeleporter": "Disable teleporter",
+ "preferences.movementSpeedModifier": "Movement speed modifier",
+ "preferences.disableAutoPixelRatio": "Disable automatic pixel ratio adjustments",
+ "preferences.disableEchoCancellation": "Disable microphone echo cancellation (requires restart)",
+ "preferences.disableNoiseSuppression": "Disable microphone noise supression (requires restart)",
+ "preferences.disableAutoGainControl": "Disable microphone automatic gain control (requires restart)",
+ "settings.return-to-vr": "Return to VR",
+ "settings.change-avatar": "Set Name & Avatar",
+ "settings.change-scene": "Choose a Scene",
+ "settings.favorites": "Favorite Rooms",
+ "settings.preferences": "Preferences",
+ "settings.room-settings": "Room Settings",
+ "settings.close-room": "Close Room...",
+ "settings.room-info": "Room & Scene Info",
+ "settings.create-room": "New Room",
+ "settings.enable-streamer-mode": "Camera Mode",
+ "settings.disable-streamer-mode": "Exit Camera Mode",
+ "settings.send-feedback": "Send Feedback",
+ "settings.whats-new": "What's New",
+ "settings.controls": "Controls",
+ "settings.community": "Join Discord",
+ "settings.tips": "Start Tour",
+ "settings.report": "Report Issue",
+ "settings.terms": "Terms of Use",
+ "settings.privacy": "Privacy Notice",
+ "settings.help": "Help",
+ "settings.row-profile": "You",
+ "settings.row-room": "Room",
+ "settings.row-tools": "Tools",
+ "tips.mobile.video_share_mode": "Tap to stop sharing video.",
+ "tips.mobile.pen_mode": "Tap icon again to end drawing.",
+ "tips.mobile.mute_mode": "You are muted. Tap to unmute.",
+ "tips.mobile.freeze_mode": "Two-finger tap to hide menus.",
+ "tips.mobile.look": "Welcome! 👋 Tap and drag to look around.",
+ "tips.mobile.locomotion": "Great! To move, pinch with two fingers.",
+ "tips.mobile.spawn_menu-pre": "Use the ",
+ "tips.mobile.spawn_menu-post": "at the top to create objects.",
+ "tips.mobile.freeze_gesture": "Two-finger tap to show menus.",
+ "tips.mobile.object_grab": "Drag an object to move or throw it.",
+ "tips.mobile.object_rotate_button-pre": "Tap and drag ",
+ "tips.mobile.object_rotate_button-post": " to rotate.",
+ "tips.mobile.object_scale_button-pre": "Tap and drag ",
+ "tips.mobile.object_scale_button-post": " to scale.",
+ "tips.mobile.object_recenter_button-pre": "Tap ",
+ "tips.mobile.object_recenter_button-post": " to have the object face you.",
+ "tips.mobile.object_pin": "Pin an object to save it to the room.",
+ "tips.mobile.video_share_failed": "No permission granted.",
+ "tips.mobile.invite": "Use the Share button to share this room.",
+ "tips.mobile.feedback": "🦆 Quack! Want to help Hubs?",
+ "tips.mobile.feedback-link": "Tell the Duck.",
+ "tips.desktop.video_share_mode": "You're streaming. Select again to stop sharing video.",
+ "tips.desktop.pen_mode": "Press ESC or right click to drop pen. Ctrl-Z to undo.",
+ "tips.desktop.mute_mode": "You are muted. Click or press M to un-mute.",
+ "tips.desktop.look": "Welcome to %app-name%! Let's take a quick tour. 👋 Click and drag to look around.",
+ "tips.desktop.locomotion": "Use the W A S D keys to move. Hold shift to boost.",
+ "tips.desktop.turning": "Perfect. Use the Q and E keys to rotate.",
+ "tips.desktop.spawn_menu-pre": "To create objects, click the ",
+ "tips.desktop.spawn_menu-post": "button at the top of the screen, or press Ctrl 1 through 7.",
+ "tips.desktop.freeze_gesture": "Press and hold spacebar to show object menus.",
+ "tips.desktop.menu_hover": "Now, point the cursor at an object to show the menu.",
+ "tips.desktop.object_grab": "Click and drag an object to move it. Or, flick to throw it!",
+ "tips.desktop.object_pin": "Pin an object to save it to the room.",
+ "tips.desktop.object_zoom": "Scroll to move this object towards and away from you.",
+ "tips.desktop.object_rotate_button-pre": "Click and drag ",
+ "tips.desktop.object_rotate_button-post": " to rotate the object.",
+ "tips.desktop.object_scale_button-pre": "Click and drag ",
+ "tips.desktop.object_scale_button-post": " to scale the object.",
+ "tips.desktop.object_recenter_button-pre": "Click ",
+ "tips.desktop.object_recenter_button-post": " to rotate the object to face you. Helpful if you drop it!",
+ "tips.desktop.object_scale": "Hold shift and scroll to scale it bigger and smaller.",
+ "tips.desktop.pen_color": "Use Shift-Q and Shift-E to change the pen color.",
+ "tips.desktop.pen_size": "Hold Shift and scroll to change the pen size.",
+ "tips.desktop.invite": "Nobody else is here. Use the Share button at the top to share this room.",
+ "tips.desktop.video_share_failed": "You need to grant permissions to stream video.",
+ "tips.streaming": "Now broadcasting to the lobby. Click to exit.",
+ "tips.desktop.watching": "You're in the lobby. Others cannot see or hear you.",
+ "tips.desktop.feedback": "🦆 Quack! Want to help us make Hubs better?",
+ "tips.desktop.feedback-link": "Talk to the Feedback Duck.",
+ "tips.watching.back": "Enter Room",
+ "tips.mobile.watching": "You're in the lobby.",
+ "tips.watching-exit": "Back",
+ "lobby.watching": "Watching ",
+ "loader.entering_lobby": "Entering lobby...",
+ "loader.loading": "Loading ",
+ "loader.connecting": "Connecting...",
+ "loader.connected": "Connected.",
+ "loader.object": "object",
+ "loader.objects": "objects",
+ "change-scene-dialog.change-scene": "Change Scene",
+ "change-scene-dialog.create-in-spoke": "Or, create a new scene using %editor-name%.",
+ "change-scene-dialog.new-spoke-project": "Launch %editor-name%",
+ "invalid-scene-url": "This URL does not point to a scene or valid GLB.",
+ "avatar-url-dialog.apply": "Apply",
+ "interstitial.prompt": "Continue",
+ "feedback.prompt": "🦆 Feedback ",
+ "help.prompt": "Help",
+ "hide-ui.prompt": "Hide All",
+ "discord.primary_tagline": "Share a virtual room with your community.\nWatch videos, play with 3D objects, or just hang out.",
+ "discord.secondary_tagline": "No downloads or sign up. Full VR support too.",
+ "discord.contact_us": "Invite Bot to Server",
+ "discord.community_link": "Hubs Discord",
+ "discord.splash_tag": "Designed for serious businessing.",
+ "client-info.kick-button": "Kick",
+ "client-info.hide-button": "Hide",
+ "client-info.unhide-button": "Un-hide ",
+ "client-info.mute-button": "Mute",
+ "client-info.cancel": "Cancel",
+ "client-info.add-owner": "Promote",
+ "client-info.remove-owner": "Demote",
+ "object-info.no-media": "There is no media in the room yet",
+ "object-info.remove-button": "Remove",
+ "object-info.open-link": "Open Link",
+ "object-info.waypoint": "Go To",
+ "object-info.pin-button": "Pin",
+ "object-info.raise-lights": "Show Background",
+ "object-info.lower-lights": "Hide Background",
+ "object-info.unpin-button": "Unpin",
+ "avatar-landing.select": "Select",
+ "avatar-landing.selected": "This is your current avatar",
+ "leave-room-dialog.join-room.message": "Joining a new room will leave this one. Are you sure?",
+ "leave-room-dialog.join-room.confirm": "Join Room",
+ "leave-room-dialog.create-room.message": "Creating a new room will leave this one. Are you sure?",
+ "leave-room-dialog.create-room.confirm": "Leave Room",
+ "embed.load-button": "Load Room",
+ "embed.presence-warning": "This room is embedded, so may be visible to visitors on other websites.",
+ "tweet-dialog.tweet": "Tweet",
+ "tweet-dialog.posted": "Your tweet has been posted.",
+ "tweet-dialog.close": "close",
+ "oauth-dialog.sign-in.twitter": "Connect to Twitter",
+ "oauth-dialog.sign-in.discord": "Sign in to Discord",
+ "oauth-dialog.message.twitter": "Connect to Twitter to send tweets from %app-name%.",
+ "cloud.primary_tagline": "Hubs Cloud deploys server infrastructure for private,\ncollaborative 3D rooms that can be accessed on your\ndesktop, mobile phone, or VR headset.",
+ "cloud.secondary_tagline": "Get it today on the AWS Marketplace.",
+ "cloud.call_to_action_personal": "Get Hubs Cloud Personal",
+ "cloud.call_to_action_enterprise": "Get Hubs Cloud Enterprise",
+ "cloud.aws_quick_start": "Quick Start Guide",
+ "cloud.splash_tag": "Configurable infrastructure for your own Hubs stack"
+ }
+}
diff --git a/src/components/block-button.js b/src/components/block-button.js
index 5d70870187..6a9b8b0ddb 100644
--- a/src/components/block-button.js
+++ b/src/components/block-button.js
@@ -5,20 +5,44 @@
*/
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.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();
},
play() {
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/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/immers-follow-button.js b/src/components/immers/immers-follow-button.js
new file mode 100644
index 0000000000..5f533bed57
--- /dev/null
+++ b/src/components/immers/immers-follow-button.js
@@ -0,0 +1,91 @@
+/**
+ * Registers a click handler publishes a follow request
+ * @namespace immers
+ * @component immers-follow-button
+ */
+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);
+ 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.showIfLoggedIn();
+ this.playerEl.addEventListener("immers-id-changed", this.showIfLoggedIn);
+ });
+ this.textEl = this.el.querySelector("[text]");
+ // avoid accidental double clicks
+ let lastClickTime = 0;
+ this.onClick = () => {
+ 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":
+ this.action("immers-follow-reject", "none");
+ break;
+ }
+ };
+ this.onState = event => {
+ const friendState = event.detail.split("immers-follow-")[1];
+ if (friendState) {
+ this.el.setAttribute("immers-follow-button", { relation: friendState });
+ }
+ };
+ },
+
+ play() {
+ this.el.object3D.addEventListener("interact", this.onClick);
+ if (this.playerEl) {
+ this.playerEl.addEventListener("stateadded", this.onState);
+ this.playerEl.addEventListener("immers-id-changed", this.showIfLoggedIn);
+ }
+ },
+
+ update() {
+ let newText;
+ switch (this.data.relation) {
+ case "request":
+ newText = "Accept friend";
+ break;
+ case "pending":
+ newText = "Request 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) {
+ this.playerEl.removeEventListener("stateadded", this.onState);
+ this.playerEl.removeEventListener("immers-id-changed", this.showIfLoggedIn);
+ }
+ },
+
+ 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/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/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
new file mode 100644
index 0000000000..271048bda4
--- /dev/null
+++ b/src/components/immers/index.js
@@ -0,0 +1,9 @@
+import "./immers-follow-button";
+import "./immers-share-button";
+import "./spoke-tagger";
+import "./monetization-visible";
+import "./monetization-invisible";
+import "./monetization-required";
+import "./monetization-interactable";
+import "./monetization-networked";
+import "./immers-visible-if-permitted";
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/immers/monetization-networked.js b/src/components/immers/monetization-networked.js
new file mode 100644
index 0000000000..8cfc75ced8
--- /dev/null
+++ b/src/components/immers/monetization-networked.js
@@ -0,0 +1,58 @@
+/* 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-networked", {
+ init() {
+ 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);
+ },
+ 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-networked"].shareMonetization(numMonetized);
+ }
+ }
+});
+
+AFRAME.registerComponent("monetization-networked", {
+ shareMonetization(count) {
+ const monetized = !!count;
+ if (monetized !== this.lastMonetized) {
+ this.el.emit(`immers-monetization-${monetized ? "started" : "stopped"}`, undefined, false);
+ }
+ this.lastMonetized = monetized;
+ },
+ init() {
+ this.lastMonetized = false;
+ this.system.registerMe(this.el);
+ },
+ remove() {
+ this.system.unregisterMe(this.el);
+ }
+});
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/components/in-world-hud.js b/src/components/in-world-hud.js
index 738bd0e82d..54a260b4e2 100644
--- a/src/components/in-world-hud.js
+++ b/src/components/in-world-hud.js
@@ -10,8 +10,10 @@ AFRAME.registerComponent("in-world-hud", {
this.spawn = this.el.querySelector(".spawn");
this.pen = this.el.querySelector(".penhud");
this.cameraBtn = this.el.querySelector(".camera-btn");
+ this.immersBtn = this.el.querySelector(".immers-btn");
this.inviteBtn = this.el.querySelector(".invite-btn");
this.background = this.el.querySelector(".bg");
+ this.notificationText = this.el.querySelector("#hud-presence-notification");
this.onMicStateChanged = () => {
this.mic.setAttribute("mic-button", "active", APP.dialog.isMicEnabled);
@@ -22,15 +24,17 @@ AFRAME.registerComponent("in-world-hud", {
this.mic.setAttribute("mic-button", "active", APP.dialog.isMicEnabled);
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"));
this.cameraBtn.setAttribute("icon-button", "disabled", !window.APP.hubChannel.can("spawn_camera"));
}
+ this.immersBtn.object3D.visible = !this.el.sceneEl.is("immers-connected");
};
this.onStateChange = evt => {
- if (!(evt.detail === "frozen" || evt.detail === "pen" || evt.detail === "camera")) return;
+ if (!(evt.detail === "frozen" || evt.detail === "pen" || evt.detail === "camera" || evt.detail === 'notification')) return;
this.updateButtonStates();
};
@@ -54,6 +58,10 @@ AFRAME.registerComponent("in-world-hud", {
this.el.emit("action_toggle_camera");
};
+ this.onImmersClick = () => {
+ this.el.emit("action_immers_register");
+ };
+
this.onInviteClick = () => {
this.el.emit("action_invite");
};
@@ -75,6 +83,7 @@ AFRAME.registerComponent("in-world-hud", {
this.pen.object3D.addEventListener("interact", this.onPenClick);
this.cameraBtn.object3D.addEventListener("interact", this.onCameraClick);
this.inviteBtn.object3D.addEventListener("interact", this.onInviteClick);
+ this.immersBtn.object3D.addEventListener("interact", this.onImmersClick);
},
pause() {
@@ -88,5 +97,6 @@ AFRAME.registerComponent("in-world-hud", {
this.pen.object3D.removeEventListener("interact", this.onPenClick);
this.cameraBtn.object3D.removeEventListener("interact", this.onCameraClick);
this.inviteBtn.object3D.removeEventListener("interact", this.onInviteClick);
+ this.immersBtn.object3D.removeEventListener("interact", this.onImmersClick);
}
});
diff --git a/src/components/player-info.js b/src/components/player-info.js
index 70753f36c9..deedba5881 100644
--- a/src/components/player-info.js
+++ b/src/components/player-info.js
@@ -36,6 +36,8 @@ AFRAME.registerComponent("player-info", {
avatarSrc: { type: "string" },
avatarType: { type: "string", default: AVATAR_TYPES.SKINNABLE },
muted: { default: false },
+ immersId: { type: "string" },
+ monetized: { type: "boolean" },
isSharingAvatarCamera: { default: false }
},
init() {
@@ -64,6 +66,10 @@ AFRAME.registerComponent("player-info", {
},
remove() {
+ this.el.sceneEl.emit("immers-player-monetization", {
+ monetized: false,
+ immersId: this.data.immersId
+ });
const avatarEl = this.el.querySelector("[avatar-audio-source]");
APP.isAudioPaused.delete(avatarEl);
deregisterComponentInstance(this, "player-info");
@@ -148,6 +154,23 @@ AFRAME.registerComponent("player-info", {
this.el.emit("remote_mute_updated", { muted: this.data.muted });
}
this.applyProperties();
+ 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,
+ 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
+ });
+ }
},
can(perm) {
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;
diff --git a/src/hub.html b/src/hub.html
index ecda3e2365..a67972d580 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -181,6 +181,11 @@
+
+
+
+
+
@@ -317,6 +322,7 @@