Merge branch 'develop' of github.com:thecodingmachine/workadventure into develop

This commit is contained in:
_Bastler 2022-05-05 12:25:09 +02:00
commit 47e5e360df
68 changed files with 1375 additions and 280 deletions

View File

@ -40,6 +40,7 @@
}, },
"homepage": "https://github.com/thecodingmachine/workadventure#readme", "homepage": "https://github.com/thecodingmachine/workadventure#readme",
"dependencies": { "dependencies": {
"@anatine/zod-openapi": "^1.3.0",
"@workadventure/tiled-map-type-guard": "^1.0.3", "@workadventure/tiled-map-type-guard": "^1.0.3",
"axios": "^0.21.2", "axios": "^0.21.2",
"busboy": "^0.3.1", "busboy": "^0.3.1",
@ -50,6 +51,7 @@
"ipaddr.js": "^2.0.1", "ipaddr.js": "^2.0.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"openapi3-ts": "^2.0.2",
"prom-client": "^12.0.0", "prom-client": "^12.0.0",
"query-string": "^6.13.3", "query-string": "^6.13.3",
"redis": "^3.1.2", "redis": "^3.1.2",

View File

@ -520,7 +520,16 @@ export class GameRoom {
this.admins.delete(admin); this.admins.delete(admin);
} }
public incrementVersion(): number { public async incrementVersion(): Promise<number> {
// Let's check if the mapUrl has changed
const mapDetails = await GameRoom.getMapDetails(this.roomUrl);
if (this.mapUrl !== mapDetails.mapUrl) {
this.mapUrl = mapDetails.mapUrl;
this.mapPromise = undefined;
// Reset the variable manager
this.variableManagerPromise = undefined;
}
this.versionNumber++; this.versionNumber++;
return this.versionNumber; return this.versionNumber;
} }

View File

@ -150,6 +150,9 @@ export class User implements Movable {
if (this.outlineColor !== undefined) { if (this.outlineColor !== undefined) {
playerDetails.setOutlinecolor(new UInt32Value().setValue(this.outlineColor)); playerDetails.setOutlinecolor(new UInt32Value().setValue(this.outlineColor));
} }
if (details.getRemoveoutlinecolor()) {
playerDetails.setRemoveoutlinecolor(new BoolValue().setValue(true));
}
if (this.voiceIndicatorShown !== undefined) { if (this.voiceIndicatorShown !== undefined) {
playerDetails.setShowvoiceindicator(new BoolValue().setValue(this.voiceIndicatorShown)); playerDetails.setShowvoiceindicator(new BoolValue().setValue(this.voiceIndicatorShown));
} }

View File

@ -852,14 +852,14 @@ export class SocketManager {
return; return;
} }
const versionNumber = room.incrementVersion(); const versionNumber = await room.incrementVersion();
room.getUsers().forEach((recipient) => { room.getUsers().forEach((recipient) => {
const worldFullMessage = new RefreshRoomMessage(); const refreshRoomMessage = new RefreshRoomMessage();
worldFullMessage.setRoomid(roomId); refreshRoomMessage.setRoomid(roomId);
worldFullMessage.setVersionnumber(versionNumber); refreshRoomMessage.setVersionnumber(versionNumber);
const clientMessage = new ServerToClientMessage(); const clientMessage = new ServerToClientMessage();
clientMessage.setRefreshroommessage(worldFullMessage); clientMessage.setRefreshroommessage(refreshRoomMessage);
recipient.socket.write(clientMessage); recipient.socket.write(clientMessage);
}); });

View File

@ -2,6 +2,14 @@
# yarn lockfile v1 # yarn lockfile v1
"@anatine/zod-openapi@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@anatine/zod-openapi/-/zod-openapi-1.3.0.tgz#b5b38c3d821b79674226aa7b327c88c371860d0d"
integrity sha512-l54DypUdDsIq1Uwjv4ib9IBkTXMKZQLUj7qvdFL51EExC5LdSSqOlTOyaVVZZGYgWPKM7ZjGklhdoknLz4EC+w==
dependencies:
ts-deepmerge "^1.1.0"
validator "^13.7.0"
"@babel/code-frame@^7.0.0": "@babel/code-frame@^7.0.0":
version "7.16.0" version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431"
@ -1563,6 +1571,13 @@ onetime@^5.1.0, onetime@^5.1.2:
dependencies: dependencies:
mimic-fn "^2.1.0" mimic-fn "^2.1.0"
openapi3-ts@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/openapi3-ts/-/openapi3-ts-2.0.2.tgz#a200dd838bf24c9086c8eedcfeb380b7eb31e82a"
integrity sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==
dependencies:
yaml "^1.10.2"
optionator@^0.9.1: optionator@^0.9.1:
version "0.9.1" version "0.9.1"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
@ -2042,6 +2057,11 @@ tree-kill@^1.2.2:
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
ts-deepmerge@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/ts-deepmerge/-/ts-deepmerge-1.1.0.tgz#4236ae102199affe2e77690dcf198a420160eef2"
integrity sha512-VvwaV/6RyYMwT9d8dClmfHIsG2PCdm6WY430QKOIbPRR50Y/1Q2ilp4i2XEZeHFcNqfaYnAQzpyUC6XA0AqqBg==
ts-node-dev@^1.1.8: ts-node-dev@^1.1.8:
version "1.1.8" version "1.1.8"
resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.1.8.tgz#95520d8ab9d45fffa854d6668e2f8f9286241066" resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.1.8.tgz#95520d8ab9d45fffa854d6668e2f8f9286241066"
@ -2153,6 +2173,11 @@ v8-compile-cache@^2.0.3:
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
validator@^13.7.0:
version "13.7.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857"
integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==
webidl-conversions@^3.0.0: webidl-conversions@^3.0.0:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
@ -2236,7 +2261,7 @@ yallist@^4.0.0:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml@^1.10.0: yaml@^1.10.0, yaml@^1.10.2:
version "1.10.2" version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==

View File

@ -166,7 +166,8 @@ return [
], ],
[ [
'title' => 'Troubleshooting', 'title' => 'Troubleshooting',
'url' => '/map-building/troubleshooting', 'url' => '/map-building/troubleshooting.md',
'view' => 'content.map.troubleshooting' 'markdown' => 'maps.troubleshooting',
'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/troubleshooting.md',
], ],
]; ];

View File

@ -18,11 +18,18 @@ In order to create a zone that opens websites:
{.alert.alert-warning} {.alert.alert-warning}
A website can explicitly forbid another website from loading it in an iFrame using A website can explicitly forbid another website from loading it in an iFrame using
the [X-Frame-Options HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options). the [X-Frame-Options HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options). You can
read more about this common issue and possible workaround the [troubleshooting guide](troubleshooting.md#embedding-an-iframe-is-forbidden).
{.alert.alert-info} {.alert.alert-info}
As an alternative, you may also put the `openWebsite` properties on a layer (rather than putting them on an "area" object) As an alternative, you may also put the `openWebsite` properties on a layer (rather than putting them on an "area" object)
but we advise to stick with "area" objects for better performance! but we advise sticking with "area" objects for better performance!
{.alert.alert-warning}
If the website you are embedding is using cookies, those cookies must be configured with the `SameSite=none` attribute. Otherwise,
they will be ignored by the browser. If you manage to see the website you embed but cannot log into it, the `SameSite` attribute is most
likely the culprit. You can read more about this common issue and possible workaround the [troubleshooting guide](troubleshooting.md#i-cannot-log-into-my-embedded-website).
## Integrating a Youtube video ## Integrating a Youtube video

View File

@ -0,0 +1,94 @@
{.section-title.accent.text-primary}
# Troubleshooting
## Look at the browser console
If your map is not displayed correctly (most notably if you are getting a black screen), open your browser console.
This is usually done by pressing the F12 key and selecting the "console" tab.
Scan the output. Towards the end, you might see a message explaining why your map cannot be loaded.
## Check webserver CORS settings
If you are hosting the map you built on your own webserver and if the map does not load, please check that
[your webserver CORS settings are correctly configured](hosting.md).
## Issues embedding a website
When you are embedding a website in WorkAdventure (whether it is using the [`openWebsite` property](opening-a-website.md) or
the [integrated website in a map](website-in-map.md) feature or the [Scripting API](scripting.md)), WorkAdventure
will open your website using an iFrame.
Browsers have various security measures in place, and website owners can use those measures to prevent websites from
being used inside iFrames (either partially or completely).
In the chapters below, we will list what can possibly prevent you from embedding a website, and see what are your options.
### Embedding an iFrame is forbidden
The worst that can happen is that the website you are trying to embed completely denies you the authorisation.
A website owner can do that using the [`X-Frame-Options` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options),
or the newer [`Content-Security-Policy` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy).
Take a look at the headers of the page you are trying to load.
{.alert.alert-info}
You can view the headers of the web page you try to load in the developer tools of your browser (usually accessible using the F12 key
of your keyboard), in the network tab. Click on the top-most request and check the "Response Headers".
Below is what you can see when opening a Youtube video page:
![](images/x-frame-options.png)
`X-Frame-Options: DENY` or `X-Frame-Options: SAMEORIGIN` will prevent WorkAdventure from loading the page.
`Content-Security-Policy` header have also the potential to prevent WorkAdventure from loading the page.
If the website you are trying to embed has one of these headers set, here are your options:
- if you have control over the website or know the owner, you can contact the owner/administrator of the website and ask for an exception
- otherwise, you can look for an "embed" option. Some websites have special pages that can be embedded. For instance,
YouTube has special "embed" links that can be used to embed a video in your website. A lot of websites have the same feature (you
can usually find those links in the "share" section)
If none of these options are available to you, as a last resort, you can use the [`openTab` property](opening-a-website.md) instead of the `openWebsite` property.
It will open your webpage in another tab instead of opening it in an iFrame.
### I cannot log into my embedded website
When you log into a website, the website is issuing a "cookie". The cookie is a unique identifier that allows the website
to recognize you and to identify you. To improve the privacy of their users, browsers can sometimes treat cookies
inside iFrames as "third-party cookies" and discard them.
Cookies can come with a `SameSite` attribute.
The `SameSite` attribute can take these values: "Lax", "Strict" or "None". The only value that allows using the
cookie inside an iFrame is "None".
{.alert.alert-info}
The `SameSite` attribute of your cookie MUST be set to "None" if you want to be able to use this cookie from an iFrame inside WorkAdventure.
**Default values**:
If the "SameSite" attribute is not explicitly set, [the behaviour depends on the browser](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#browser_compatibility).
Chrome, Edge and Opera will default to "Lax".
Firefox and Safari will default to "None" (as of 2022/04/25).
As a result, a website that does not set the `SameSite` attribute on cookies will work correctly in Firefox and Safari but
login will fail on Chrome, Edge and Opera.
If the website you are trying to embed has the `SameSite` attribute set to a value other than "None", here are your options:
- if you have control over the website or know the owner, you can contact the owner/administrator of the website and ask
the owner/administrator to change the `SameSite` settings.
- otherwise, you will have to use the [`openTab` property](opening-a-website.md) instead of the `openWebsite` property.
It will open your webpage in another tab instead of in an iFrame.
## Need some help?
<div class="card bg-red text-white"><div class="card-body">
<p>WorkAdventure is a constantly evolving project and there is plenty of room for improvement regarding map editing.</p>
<p>If you are facing any troubles, do not hesitate to seek help in
<a href="https://discord.gg/G6Xh9ZM9aR">our Discord server</a> or open an "issue" in the
<a href="https://github.com/thecodingmachine/workadventure/issues" target="_blank">GitHub WorkAdventure account</a>.
</p>
</div></div>

View File

@ -1,5 +1,3 @@
src/Messages/generated src/Messages/generated
src/Messages/JsonMessages src/Messages/JsonMessages
src/i18n/i18n-svelte.ts src/i18n/i18n-*.ts
src/i18n/i18n-types.ts
src/i18n/i18n-util.ts

View File

@ -1,5 +1,5 @@
{ {
"$schema": "https://unpkg.com/typesafe-i18n@2.59.0/schema/typesafe-i18n.json", "$schema": "https://unpkg.com/typesafe-i18n@5.4.0/schema/typesafe-i18n.json",
"baseLocale": "en-US", "baseLocale": "en-US",
"adapter": "svelte" "adapter": "svelte"
} }

View File

@ -5,7 +5,7 @@
"license": "SEE LICENSE IN LICENSE.txt", "license": "SEE LICENSE IN LICENSE.txt",
"devDependencies": { "devDependencies": {
"@geprog/vite-plugin-env-config": "^4.0.3", "@geprog/vite-plugin-env-config": "^4.0.3",
"@home-based-studio/phaser3-utils": "^0.4.2", "@home-based-studio/phaser3-utils": "^0.4.7",
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.36", "@sveltejs/vite-plugin-svelte": "^1.0.0-next.36",
"@tsconfig/svelte": "^1.0.10", "@tsconfig/svelte": "^1.0.10",
"@types/google-protobuf": "^3.7.3", "@types/google-protobuf": "^3.7.3",
@ -34,6 +34,7 @@
}, },
"dependencies": { "dependencies": {
"@16bits/nes.css": "^2.3.2", "@16bits/nes.css": "^2.3.2",
"@anatine/zod-openapi": "^1.3.0",
"@fontsource/press-start-2p": "^4.3.0", "@fontsource/press-start-2p": "^4.3.0",
"@joeattardi/emoji-button": "^4.6.2", "@joeattardi/emoji-button": "^4.6.2",
"@types/simple-peer": "^9.11.1", "@types/simple-peer": "^9.11.1",
@ -47,6 +48,7 @@
"easystarjs": "^0.4.4", "easystarjs": "^0.4.4",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"google-protobuf": "^3.13.0", "google-protobuf": "^3.13.0",
"openapi3-ts": "^2.0.2",
"phaser": "3.55.1", "phaser": "3.55.1",
"phaser-animated-tiles": "workadventure/phaser-animated-tiles#da68bbededd605925621dd4f03bd27e69284b254", "phaser-animated-tiles": "workadventure/phaser-animated-tiles#da68bbededd605925621dd4f03bd27e69284b254",
"phaser3-rex-plugins": "^1.1.42", "phaser3-rex-plugins": "^1.1.42",
@ -61,7 +63,7 @@
"standardized-audio-context": "^25.2.4", "standardized-audio-context": "^25.2.4",
"ts-deferred": "^1.0.4", "ts-deferred": "^1.0.4",
"ts-proto": "^1.96.0", "ts-proto": "^1.96.0",
"typesafe-i18n": "^2.59.0", "typesafe-i18n": "^5.4.0",
"uuidv4": "^6.2.10", "uuidv4": "^6.2.10",
"zod": "^3.14.3" "zod": "^3.14.3"
}, },

View File

@ -17,7 +17,7 @@ class AnalyticsClient {
} }
} }
identifyUser(uuid: string, email: string | null) { identifyUser(uuid: string, email: string | null): void {
this.posthogPromise this.posthogPromise
?.then((posthog) => { ?.then((posthog) => {
posthog.identify(uuid, { uuid, email, wa: true }); posthog.identify(uuid, { uuid, email, wa: true });
@ -25,7 +25,7 @@ class AnalyticsClient {
.catch((e) => console.error(e)); .catch((e) => console.error(e));
} }
loggedWithSso() { loggedWithSso(): void {
this.posthogPromise this.posthogPromise
?.then((posthog) => { ?.then((posthog) => {
posthog.capture("wa-logged-sso"); posthog.capture("wa-logged-sso");
@ -33,7 +33,7 @@ class AnalyticsClient {
.catch((e) => console.error(e)); .catch((e) => console.error(e));
} }
loggedWithToken() { loggedWithToken(): void {
this.posthogPromise this.posthogPromise
?.then((posthog) => { ?.then((posthog) => {
posthog.capture("wa-logged-token"); posthog.capture("wa-logged-token");
@ -41,7 +41,7 @@ class AnalyticsClient {
.catch((e) => console.error(e)); .catch((e) => console.error(e));
} }
enteredRoom(roomId: string, roomGroup: string | null) { enteredRoom(roomId: string, roomGroup: string | null): void {
this.posthogPromise this.posthogPromise
?.then((posthog) => { ?.then((posthog) => {
posthog.capture("$pageView", { roomId, roomGroup }); posthog.capture("$pageView", { roomId, roomGroup });
@ -50,7 +50,7 @@ class AnalyticsClient {
.catch((e) => console.error(e)); .catch((e) => console.error(e));
} }
openedMenu() { openedMenu(): void {
this.posthogPromise this.posthogPromise
?.then((posthog) => { ?.then((posthog) => {
posthog.capture("wa-opened-menu"); posthog.capture("wa-opened-menu");
@ -58,7 +58,7 @@ class AnalyticsClient {
.catch((e) => console.error(e)); .catch((e) => console.error(e));
} }
launchEmote(emote: string) { launchEmote(emote: string): void {
this.posthogPromise this.posthogPromise
?.then((posthog) => { ?.then((posthog) => {
posthog.capture("wa-emote-launch", { emote }); posthog.capture("wa-emote-launch", { emote });
@ -66,7 +66,7 @@ class AnalyticsClient {
.catch((e) => console.error(e)); .catch((e) => console.error(e));
} }
enteredJitsi(roomName: string, roomId: string) { enteredJitsi(roomName: string, roomId: string): void {
this.posthogPromise this.posthogPromise
?.then((posthog) => { ?.then((posthog) => {
posthog.capture("wa-entered-jitsi", { roomName, roomId }); posthog.capture("wa-entered-jitsi", { roomName, roomId });
@ -74,7 +74,7 @@ class AnalyticsClient {
.catch((e) => console.error(e)); .catch((e) => console.error(e));
} }
validationName() { validationName(): void {
this.posthogPromise this.posthogPromise
?.then((posthog) => { ?.then((posthog) => {
posthog.capture("wa-name-validation"); posthog.capture("wa-name-validation");
@ -82,7 +82,7 @@ class AnalyticsClient {
.catch((e) => console.error(e)); .catch((e) => console.error(e));
} }
validationWoka(scene: string) { validationWoka(scene: string): void {
this.posthogPromise this.posthogPromise
?.then((posthog) => { ?.then((posthog) => {
posthog.capture("wa-woka-validation", { scene }); posthog.capture("wa-woka-validation", { scene });
@ -90,12 +90,309 @@ class AnalyticsClient {
.catch((e) => console.error(e)); .catch((e) => console.error(e));
} }
validationVideo() { validationVideo(): void {
this.posthogPromise this.posthogPromise
?.then((posthog) => { ?.then((posthog) => {
posthog.capture("wa-video-validation"); posthog.capture("wa-video-validation");
}) })
.catch((e) => console.error(e)); .catch((e) => console.error(e));
} }
/** New feature analytics **/
openedChat(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa-opened-chat");
})
.catch((e) => console.error(e));
}
openRegister(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa-opened-register");
})
.catch((e) => console.error(e));
}
openInvite(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa-opened-invite");
})
.catch((e) => console.error(e));
}
lockDiscussion(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_lockroom");
})
.catch((e) => console.error(e));
}
screenSharing(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa-screensharing");
})
.catch((e) => console.error(e));
}
follow(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_follow");
})
.catch((e) => console.error(e));
}
camera(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_camera");
})
.catch((e) => console.error(e));
}
microphone(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_microphone");
})
.catch((e) => console.error(e));
}
settingMicrophone(value: string): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_setting_microphone", {
checkbox: value,
});
})
.catch((e) => console.error(e));
}
settingCamera(value: string): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_setting_camera", {
checkbox: value,
});
})
.catch((e) => console.error(e));
}
settingNotification(value: string): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_setting_notification", {
checkbox: value,
});
})
.catch((e) => console.error(e));
}
settingFullscreen(value: string): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_setting_fullscreen", {
checkbox: value,
});
})
.catch((e) => console.error(e));
}
settingAskWebsite(value: string): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_setting_ask_website", {
checkbox: value,
});
})
.catch((e) => console.error(e));
}
settingRequestFollow(value: string): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_setting_request_follow", {
checkbox: value,
});
})
.catch((e) => console.error(e));
}
settingDecreaseAudioVolume(value: string): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_setting_decrease_audio_volume", {
checkbox: value,
});
})
.catch((e) => console.error(e));
}
login(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_login");
})
.catch((e) => console.error(e));
}
logout(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_logout");
})
.catch((e) => console.error(e));
}
switchMultiIframe(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_multiiframe_switch");
})
.catch((e) => console.error(e));
}
closeMultiIframe(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_multiiframe_close");
})
.catch((e) => console.error(e));
}
fullScreenMultiIframe(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_multiiframe_fullscreen");
})
.catch((e) => console.error(e));
}
stackOpenCloseMultiIframe(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_multiiframe_stack_open_close");
})
.catch((e) => console.error(e));
}
menuCredit(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_menu_credit");
})
.catch((e) => console.error(e));
}
menuProfile(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_menu_profile");
})
.catch((e) => console.error(e));
}
menuSetting() {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_menu_setting");
})
.catch((e) => console.error(e));
}
menuInvite(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_menu_invite");
})
.catch((e) => console.error(e));
}
globalMessage(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_menu_globalmessage");
})
.catch((e) => console.error(e));
}
menuContact(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_menu_contact");
})
.catch((e) => console.error(e));
}
inviteCopyLink(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_menu_invite_copylink");
})
.catch((e) => console.error(e));
}
inviteCopyLinkWalk(value: string): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_menu_invite_copylink_walk", {
checkbox: value,
});
})
.catch((e) => console.error(e));
}
editCompanion(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_edit_companion");
})
.catch((e) => console.error(e));
}
editCamera(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_edit_camera");
})
.catch((e) => console.error(e));
}
editName(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_edit_name");
})
.catch((e) => console.error(e));
}
editWoka(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_edit_woka");
})
.catch((e) => console.error(e));
}
selectWoka(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_wokascene_select");
})
.catch((e) => console.error(e));
}
selectCustomWoka(): void {
this.posthogPromise
?.then((posthog) => {
posthog.capture("wa_wokascene_custom");
})
.catch((e) => console.error(e));
}
} }
export const analyticsClient = new AnalyticsClient(); export const analyticsClient = new AnalyticsClient();

View File

@ -242,7 +242,7 @@ class IframeListener {
} else if (iframeEvent.type === "cameraFollowPlayer") { } else if (iframeEvent.type === "cameraFollowPlayer") {
this._cameraFollowPlayerStream.next(iframeEvent.data); this._cameraFollowPlayerStream.next(iframeEvent.data);
} else if (iframeEvent.type === "chat") { } else if (iframeEvent.type === "chat") {
scriptUtils.sendAnonymousChat(iframeEvent.data); scriptUtils.sendAnonymousChat(iframeEvent.data, iframe.contentWindow ?? undefined);
} else if (iframeEvent.type === "openPopup") { } else if (iframeEvent.type === "openPopup") {
this._openPopupStream.next(iframeEvent.data); this._openPopupStream.next(iframeEvent.data);
} else if (iframeEvent.type === "closePopup") { } else if (iframeEvent.type === "closePopup") {
@ -404,13 +404,20 @@ class IframeListener {
this.scripts.delete(scriptUrl); this.scripts.delete(scriptUrl);
} }
sendUserInputChat(message: string) { /**
this.postMessage({ * @param message The message to dispatch
type: "userInputChat", * @param exceptOrigin Don't dispatch the message to exceptOrigin (to avoid infinite loops)
data: { */
message: message, sendUserInputChat(message: string, exceptOrigin?: Window) {
} as UserInputChatEvent, this.postMessage(
}); {
type: "userInputChat",
data: {
message: message,
} as UserInputChatEvent,
},
exceptOrigin
);
} }
sendEnterEvent(name: string) { sendEnterEvent(name: string) {
@ -528,8 +535,11 @@ class IframeListener {
/** /**
* Sends the message... to all allowed iframes. * Sends the message... to all allowed iframes.
*/ */
public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) { public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>, exceptOrigin?: Window) {
for (const iframe of this.iframes) { for (const iframe of this.iframes) {
if (exceptOrigin === iframe.contentWindow) {
continue;
}
iframe.contentWindow?.postMessage(message, "*"); iframe.contentWindow?.postMessage(message, "*");
} }
} }

View File

@ -11,9 +11,9 @@ class ScriptUtils {
window.location.href = url; window.location.href = url;
} }
public sendAnonymousChat(chatEvent: ChatEvent) { public sendAnonymousChat(chatEvent: ChatEvent, origin?: Window) {
const userId = playersStore.addFacticePlayer(chatEvent.author); const userId = playersStore.addFacticePlayer(chatEvent.author);
chatMessagesStore.addExternalMessage(userId, chatEvent.message); chatMessagesStore.addExternalMessage(userId, chatEvent.message, origin);
} }
} }

View File

@ -17,6 +17,7 @@
import { followRoleStore, followStateStore, followUsersStore } from "../Stores/FollowStore"; import { followRoleStore, followStateStore, followUsersStore } from "../Stores/FollowStore";
import { gameManager } from "../Phaser/Game/GameManager"; import { gameManager } from "../Phaser/Game/GameManager";
import { currentPlayerGroupLockStateStore } from "../Stores/CurrentPlayerGroupStore"; import { currentPlayerGroupLockStateStore } from "../Stores/CurrentPlayerGroupStore";
import { analyticsClient } from "../Administration/AnalyticsClient";
const gameScene = gameManager.getCurrentGameScene(); const gameScene = gameManager.getCurrentGameScene();
@ -89,6 +90,7 @@
class="btn-follow nes-btn is-dark" class="btn-follow nes-btn is-dark"
class:hide={($peerStore.size === 0 && $followStateStore === "off") || $silentStore} class:hide={($peerStore.size === 0 && $followStateStore === "off") || $silentStore}
class:disabled={$followStateStore !== "off"} class:disabled={$followStateStore !== "off"}
on:click={() => analyticsClient.follow()}
on:click={followClick} on:click={followClick}
> >
<img class="noselect" src={followImg} alt="" /> <img class="noselect" src={followImg} alt="" />
@ -98,6 +100,7 @@
class="btn-lock nes-btn is-dark" class="btn-lock nes-btn is-dark"
class:hide={$peerStore.size === 0 || $silentStore} class:hide={$peerStore.size === 0 || $silentStore}
class:disabled={$currentPlayerGroupLockStateStore} class:disabled={$currentPlayerGroupLockStateStore}
on:click={() => analyticsClient.lockDiscussion()}
on:click={lockClick} on:click={lockClick}
> >
<img class="noselect" src={lockImg} alt="" /> <img class="noselect" src={lockImg} alt="" />
@ -105,6 +108,7 @@
<div <div
class="btn-monitor nes-btn is-dark" class="btn-monitor nes-btn is-dark"
on:click={() => analyticsClient.screenSharing()}
on:click={screenSharingClick} on:click={screenSharingClick}
class:hide={!$screenSharingAvailableStore || $silentStore} class:hide={!$screenSharingAvailableStore || $silentStore}
class:enabled={$requestedScreenSharingState} class:enabled={$requestedScreenSharingState}
@ -116,7 +120,12 @@
{/if} {/if}
</div> </div>
<div class="btn-video nes-btn is-dark" on:click={cameraClick} class:disabled={!$requestedCameraState || $silentStore}> <div
class="btn-video nes-btn is-dark"
on:click={() => analyticsClient.camera()}
on:click={cameraClick}
class:disabled={!$requestedCameraState || $silentStore}
>
{#if $requestedCameraState && !$silentStore} {#if $requestedCameraState && !$silentStore}
<img class="noselect" src={cinemaImg} alt="Turn on webcam" /> <img class="noselect" src={cinemaImg} alt="Turn on webcam" />
{:else} {:else}
@ -124,7 +133,12 @@
{/if} {/if}
</div> </div>
<div class="btn-micro nes-btn is-dark" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState || $silentStore}> <div
class="btn-micro nes-btn is-dark"
on:click={() => analyticsClient.microphone()}
on:click={microphoneClick}
class:disabled={!$requestedMicrophoneState || $silentStore}
>
{#if $requestedMicrophoneState && !$silentStore} {#if $requestedMicrophoneState && !$silentStore}
<img class="noselect" src={microphoneImg} alt="Turn on microphone" /> <img class="noselect" src={microphoneImg} alt="Turn on microphone" />
{:else} {:else}

View File

@ -12,6 +12,7 @@
import { i18nJson } from "../../i18n/locales"; import { i18nJson } from "../../i18n/locales";
import uploadFile from "../images/jitsi.png"; import uploadFile from "../images/jitsi.png";
import { analyticsClient } from "../../Administration/AnalyticsClient";
export let index: number; export let index: number;
export let coWebsite: CoWebsite; export let coWebsite: CoWebsite;
@ -103,6 +104,7 @@
class:ready={$state === "ready"} class:ready={$state === "ready"}
class:displayed={isMain || isHighlight} class:displayed={isMain || isHighlight}
class:vertical class:vertical
on:click={() => analyticsClient.stackOpenCloseMultiIframe()}
on:click={onClick} on:click={onClick}
> >
<div class="cowebsite-thumnail-container"> <div class="cowebsite-thumnail-container">

View File

@ -19,6 +19,5 @@
padding-top: 2%; padding-top: 2%;
height: 100%; height: 100%;
position: relative; position: relative;
z-index: 200;
} }
</style> </style>

View File

@ -67,10 +67,11 @@
{/key} {/key}
{:else if $highlightedEmbedScreen.type === "cowebsite"} {:else if $highlightedEmbedScreen.type === "cowebsite"}
{#key $highlightedEmbedScreen.embed.getId()} {#key $highlightedEmbedScreen.embed.getId()}
<div <div class="highlighted-cowebsite-container nes-container is-rounded">
id={"cowebsite-slot-" + $highlightedEmbedScreen.embed.getId()} <div
class="highlighted-cowebsite nes-container is-rounded" id={"cowebsite-slot-" + $highlightedEmbedScreen.embed.getId()}
> class="highlighted-cowebsite"
/>
<div class="actions"> <div class="actions">
<button type="button" class="nes-btn is-error close" on:click={closeCoWebsite} <button type="button" class="nes-btn is-error close" on:click={closeCoWebsite}
>&times;</button >&times;</button
@ -120,20 +121,29 @@
.highlighted-cowebsite { .highlighted-cowebsite {
height: 100% !important; height: 100% !important;
width: 96%; width: 100% !important;
background-color: rgba(#000000, 0.6); position: relative;
margin: 0 !important; z-index: 200;
.actions { &-container {
z-index: 200; height: 100% !important;
position: relative; width: 96%;
display: flex; background-color: rgba(#000000, 0.6);
flex-direction: row; margin: 0 !important;
justify-content: end; padding: 0 !important;
gap: 2%; .actions {
z-index: 202;
position: absolute;
width: 100%;
top: 0;
display: flex;
flex-direction: row;
justify-content: end;
gap: 2%;
button { button {
pointer-events: all; pointer-events: all;
}
} }
} }
} }

View File

@ -53,11 +53,14 @@
background: #eceeee; background: #eceeee;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
position: absolute;
left: 0; left: 0;
right: 0; right: 0;
margin-top: 4%; margin-top: 4%;
max-height: 80vh; max-height: 80vh;
max-width: 80vw; max-width: 80vw;
margin-left: 10%;
margin-right: 10%;
z-index: 600; z-index: 600;
overflow: auto; overflow: auto;
text-align: center; text-align: center;

View File

@ -68,6 +68,11 @@
</aside> </aside>
<section id="main-layout-main"> <section id="main-layout-main">
<Lazy
when={$showDesktopCapturerSourcePicker}
component={() => import("./Video/DesktopCapturerSourcePicker.svelte")}
/>
{#if $menuVisiblilityStore} {#if $menuVisiblilityStore}
<Menu /> <Menu />
{/if} {/if}
@ -120,11 +125,6 @@
<Lazy when={$emoteMenuStore} component={() => import("./EmoteMenu/EmoteMenu.svelte")} /> <Lazy when={$emoteMenuStore} component={() => import("./EmoteMenu/EmoteMenu.svelte")} />
<Lazy
when={$showDesktopCapturerSourcePicker}
component={() => import("./Video/DesktopCapturerSourcePicker.svelte")}
/>
{#if hasEmbedScreen} {#if hasEmbedScreen}
<EmbedScreensContainer /> <EmbedScreensContainer />
{/if} {/if}

View File

@ -2,6 +2,7 @@
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
import { gameManager } from "../../Phaser/Game/GameManager"; import { gameManager } from "../../Phaser/Game/GameManager";
import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore"; import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore";
import { analyticsClient } from "../../Administration/AnalyticsClient";
let entryPoint: string = $startLayerNamesStore[0]; let entryPoint: string = $startLayerNamesStore[0];
let walkAutomatically: boolean = false; let walkAutomatically: boolean = false;
@ -54,16 +55,26 @@
class="link-url nes-input is-dark" class="link-url nes-input is-dark"
value={location.toString()} value={location.toString()}
/> />
<button type="button" class="nes-btn is-primary" on:click={copyLink}>{$LL.menu.invite.copy()}</button> <button
type="button"
class="nes-btn is-primary"
on:click={() => analyticsClient.inviteCopyLink()}
on:click={copyLink}>{$LL.menu.invite.copy()}</button
>
</section> </section>
{:else} {:else}
<section class="is-mobile"> <section class="is-mobile">
<h3>{$LL.menu.invite.description()}</h3> <h3>{$LL.menu.invite.description()}</h3>
<input type="hidden" readonly id="input-share-link" value={location.toString()} /> <input type="hidden" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={shareLink}>{$LL.menu.invite.share()}</button> <button
type="button"
class="nes-btn is-primary"
on:click={() => analyticsClient.inviteCopyLink()}
on:click={shareLink}>{$LL.menu.invite.share()}</button
>
</section> </section>
{/if} {/if}
<h3>Select an entry point</h3> <h3>{$LL.menu.invite.selectEntryPoint()}</h3>
<section class="nes-select is-dark starting-points"> <section class="nes-select is-dark starting-points">
<select <select
bind:value={entryPoint} bind:value={entryPoint}
@ -81,11 +92,12 @@
type="checkbox" type="checkbox"
class="nes-checkbox is-dark" class="nes-checkbox is-dark"
bind:checked={walkAutomatically} bind:checked={walkAutomatically}
on:change={(e) => analyticsClient.inviteCopyLinkWalk(e.currentTarget.value)}
on:change={() => { on:change={() => {
updateInputFieldValue(); updateInputFieldValue();
}} }}
/> />
<span>{$LL.menu.invite.walk_automatically_to_position()}</span> <span>{$LL.menu.invite.walkAutomaticallyToPosition()}</span>
</label> </label>
</section> </section>
</div> </div>

View File

@ -19,6 +19,7 @@
import type { Unsubscriber } from "svelte/store"; import type { Unsubscriber } from "svelte/store";
import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem"; import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem";
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
import { analyticsClient } from "../../Administration/AnalyticsClient";
let activeSubMenu: MenuItem = $subMenusStore[0]; let activeSubMenu: MenuItem = $subMenusStore[0];
let activeComponent: typeof ProfileSubMenu | typeof CustomSubMenu = ProfileSubMenu; let activeComponent: typeof ProfileSubMenu | typeof CustomSubMenu = ProfileSubMenu;
@ -48,24 +49,30 @@
activeSubMenu = menu; activeSubMenu = menu;
switch (menu.key) { switch (menu.key) {
case SubMenusInterface.settings: case SubMenusInterface.settings:
analyticsClient.menuSetting();
activeComponent = SettingsSubMenu; activeComponent = SettingsSubMenu;
break; break;
case SubMenusInterface.profile: case SubMenusInterface.profile:
analyticsClient.menuProfile();
activeComponent = ProfileSubMenu; activeComponent = ProfileSubMenu;
break; break;
case SubMenusInterface.worlds: case SubMenusInterface.worlds:
activeComponent = WorldsSubMenu; activeComponent = WorldsSubMenu;
break; break;
case SubMenusInterface.invite: case SubMenusInterface.invite:
analyticsClient.menuInvite();
activeComponent = GuestSubMenu; activeComponent = GuestSubMenu;
break; break;
case SubMenusInterface.aboutRoom: case SubMenusInterface.aboutRoom:
analyticsClient.menuCredit();
activeComponent = AboutRoomSubMenu; activeComponent = AboutRoomSubMenu;
break; break;
case SubMenusInterface.globalMessages: case SubMenusInterface.globalMessages:
analyticsClient.globalMessage();
activeComponent = (await import("./GlobalMessagesSubMenu.svelte")).default; activeComponent = (await import("./GlobalMessagesSubMenu.svelte")).default;
break; break;
case SubMenusInterface.contact: case SubMenusInterface.contact:
analyticsClient.menuContact();
activeComponent = ContactSubMenu; activeComponent = ContactSubMenu;
break; break;
} }
@ -92,16 +99,11 @@
} }
} }
function translateMenuName(menu: MenuItem) { $: subMenuTranslations = $subMenusStore.map((subMenu) =>
if (menu.type === "scripting") { subMenu.type === "scripting" ? subMenu.label : $LL.menu.sub[subMenu.key]()
return menu.label; );
} $: activeSubMenuTranslation =
activeSubMenu.type === "scripting" ? activeSubMenu.label : $LL.menu.sub[activeSubMenu.key]();
// Bypass the proxy of typesafe for getting the menu name : https://github.com/ivanhofer/typesafe-i18n/issues/156
const getMenuName = $LL.menu.sub[menu.key];
return getMenuName();
}
</script> </script>
<svelte:window on:keydown={onKeyDown} /> <svelte:window on:keydown={onKeyDown} />
@ -110,20 +112,20 @@
<div class="menu-nav-sidebar nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}> <div class="menu-nav-sidebar nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}>
<h2>{$LL.menu.title()}</h2> <h2>{$LL.menu.title()}</h2>
<nav> <nav>
{#each $subMenusStore as submenu} {#each $subMenusStore as submenu, i}
<button <button
type="button" type="button"
class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}" class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}"
on:click|preventDefault={() => void switchMenu(submenu)} on:click|preventDefault={() => void switchMenu(submenu)}
> >
{translateMenuName(submenu)} {subMenuTranslations[i]}
</button> </button>
{/each} {/each}
</nav> </nav>
</div> </div>
<div class="menu-submenu-container nes-container is-rounded" transition:fly={{ y: -1000, duration: 500 }}> <div class="menu-submenu-container nes-container is-rounded" transition:fly={{ y: -1000, duration: 500 }}>
<button type="button" class="nes-btn is-error close" on:click={closeMenu}>&times</button> <button type="button" class="nes-btn is-error close" on:click={closeMenu}>&times</button>
<h2>{translateMenuName(activeSubMenu)}</h2> <h2>{activeSubMenuTranslation}</h2>
<svelte:component this={activeComponent} {...props} /> <svelte:component this={activeComponent} {...props} />
</div> </div>
</div> </div>

View File

@ -10,6 +10,7 @@
import { ADMIN_URL } from "../../Enum/EnvironmentVariable"; import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
import { showShareLinkMapModalStore } from "../../Stores/ModalStore"; import { showShareLinkMapModalStore } from "../../Stores/ModalStore";
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
import { analyticsClient } from "../../Administration/AnalyticsClient";
function showMenu() { function showMenu() {
menuVisiblilityStore.set(!get(menuVisiblilityStore)); menuVisiblilityStore.set(!get(menuVisiblilityStore));
@ -43,7 +44,8 @@
class="nes-pointer" class="nes-pointer"
draggable="false" draggable="false"
on:dragstart|preventDefault={noDrag} on:dragstart|preventDefault={noDrag}
on:click|preventDefault={showInvite} on:click={() => analyticsClient.openInvite()}
on:click={showInvite}
/> />
</span> </span>
<span class="nes-btn is-dark"> <span class="nes-btn is-dark">
@ -53,7 +55,8 @@
class="nes-pointer" class="nes-pointer"
draggable="false" draggable="false"
on:dragstart|preventDefault={noDrag} on:dragstart|preventDefault={noDrag}
on:click|preventDefault={register} on:click={() => analyticsClient.openRegister()}
on:click={register}
/> />
</span> </span>
{:else} {:else}
@ -64,11 +67,11 @@
class="nes-pointer" class="nes-pointer"
draggable="false" draggable="false"
on:dragstart|preventDefault={noDrag} on:dragstart|preventDefault={noDrag}
on:click|preventDefault={showMenu} on:click={() => analyticsClient.openedMenu()}
on:click={showMenu}
/> />
</span> </span>
{/if} {/if}
<span class="nes-btn is-dark"> <span class="nes-btn is-dark">
<img <img
src={logoTalk} src={logoTalk}
@ -76,7 +79,8 @@
class="nes-pointer" class="nes-pointer"
draggable="false" draggable="false"
on:dragstart|preventDefault={noDrag} on:dragstart|preventDefault={noDrag}
on:click|preventDefault={showChat} on:click={() => analyticsClient.openedMenu()}
on:click={showChat}
/> />
</span> </span>
</main> </main>

View File

@ -21,6 +21,7 @@
import Woka from "../Woka/Woka.svelte"; import Woka from "../Woka/Woka.svelte";
import Companion from "../Companion/Companion.svelte"; import Companion from "../Companion/Companion.svelte";
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
import { analyticsClient } from "../../Administration/AnalyticsClient";
function disableMenuStores() { function disableMenuStores() {
menuVisiblilityStore.set(false); menuVisiblilityStore.set(false);
@ -54,21 +55,41 @@
<div class="customize-main"> <div class="customize-main">
<div class="submenu"> <div class="submenu">
<section> <section>
<!-- <!--
<button type="button" class="nes-btn" on:click|preventDefault={openEditNameScene}> <button
type="button"
class="nes-btn"
on:click={() => analyticsClient.editName()}
on:click={openEditNameScene}
>
<img src={btnProfileSubMenuIdentity} alt={$LL.menu.profile.edit.name()} /> <img src={btnProfileSubMenuIdentity} alt={$LL.menu.profile.edit.name()} />
<span class="btn-hover">{$LL.menu.profile.edit.name()}</span> <span class="btn-hover">{$LL.menu.profile.edit.name()}</span>
</button> </button>
--> -->
<button type="button" class="nes-btn" on:click|preventDefault={openEditSkinScene}> <button
type="button"
class="nes-btn"
on:click={() => analyticsClient.editWoka()}
on:click={openEditSkinScene}
>
<Woka userId={-1} placeholderSrc="" width="26px" height="26px" /> <Woka userId={-1} placeholderSrc="" width="26px" height="26px" />
<span class="btn-hover">{$LL.menu.profile.edit.woka()}</span> <span class="btn-hover">{$LL.menu.profile.edit.woka()}</span>
</button> </button>
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}> <button
type="button"
class="nes-btn"
on:click={() => analyticsClient.editCompanion()}
on:click={openEditCompanionScene}
>
<Companion userId={-1} placeholderSrc={btnProfileSubMenuCompanion} width="26px" height="26px" /> <Companion userId={-1} placeholderSrc={btnProfileSubMenuCompanion} width="26px" height="26px" />
<span class="btn-hover">{$LL.menu.profile.edit.companion()}</span> <span class="btn-hover">{$LL.menu.profile.edit.companion()}</span>
</button> </button>
<button type="button" class="nes-btn" on:click|preventDefault={openEnableCameraScene}> <button
type="button"
class="nes-btn"
on:click={() => analyticsClient.editCamera()}
on:click={openEnableCameraScene}
>
<img src={btnProfileSubMenuCamera} alt={$LL.menu.profile.edit.camera()} /> <img src={btnProfileSubMenuCamera} alt={$LL.menu.profile.edit.camera()} />
<span class="btn-hover">{$LL.menu.profile.edit.camera()}</span> <span class="btn-hover">{$LL.menu.profile.edit.camera()}</span>
</button> </button>
@ -83,13 +104,15 @@
{/if} {/if}
</section> </section>
<section> <section>
<button type="button" class="nes-btn" on:click|preventDefault={logOut} <button type="button" class="nes-btn" on:click={() => analyticsClient.logout()} on:click={logOut}
>{$LL.menu.profile.logout()}</button >{$LL.menu.profile.logout()}</button
> >
</section> </section>
{:else} {:else}
<section> <section>
<a type="button" class="nes-btn" href="/login">{$LL.menu.profile.login()}</a> <a type="button" class="nes-btn" href="/login" on:click={() => analyticsClient.login()}
>{$LL.menu.profile.login()}</a
>
</section> </section>
{/if} {/if}
</div> </div>

View File

@ -10,6 +10,7 @@
import { audioManagerVolumeStore } from "../../Stores/AudioManagerStore"; import { audioManagerVolumeStore } from "../../Stores/AudioManagerStore";
import infoImg from "../images/info.svg"; import infoImg from "../images/info.svg";
import { analyticsClient } from "../../Administration/AnalyticsClient";
let fullscreen: boolean = localUserStore.getFullscreen(); let fullscreen: boolean = localUserStore.getFullscreen();
let notification: boolean = localUserStore.getNotification() === "granted"; let notification: boolean = localUserStore.getNotification() === "granted";
@ -28,12 +29,12 @@
let previewCameraPrivacySettings = valueCameraPrivacySettings; let previewCameraPrivacySettings = valueCameraPrivacySettings;
let previewMicrophonePrivacySettings = valueMicrophonePrivacySettings; let previewMicrophonePrivacySettings = valueMicrophonePrivacySettings;
function saveSetting() { async function saveSetting() {
let change = false; let change = false;
if (valueLocale !== previewValueLocale) { if (valueLocale !== previewValueLocale) {
previewValueLocale = valueLocale; previewValueLocale = valueLocale;
setCurrentLocale(valueLocale as Locales); await setCurrentLocale(valueLocale as Locales);
} }
if (valueVideo !== previewValueVideo) { if (valueVideo !== previewValueVideo) {
@ -174,7 +175,7 @@
<div class="nes-select is-dark"> <div class="nes-select is-dark">
<select class="languages-switcher" bind:value={valueLocale}> <select class="languages-switcher" bind:value={valueLocale}>
{#each displayableLocales as locale (locale.id)} {#each displayableLocales as locale (locale.id)}
<option value={locale.id}>{`${locale.language} (${locale.country})`}</option> <option value={locale.id}>{`${locale.language} (${locale.region})`}</option>
{/each} {/each}
</select> </select>
</div> </div>
@ -191,11 +192,21 @@
</div> </div>
</div> </div>
<label> <label>
<input type="checkbox" class="nes-checkbox is-dark" bind:checked={valueCameraPrivacySettings} /> <input
type="checkbox"
class="nes-checkbox is-dark"
on:change={(event) => analyticsClient.settingCamera(event.currentTarget.value)}
bind:checked={valueCameraPrivacySettings}
/>
<span>{$LL.menu.settings.privacySettings.cameraToggle()}</span> <span>{$LL.menu.settings.privacySettings.cameraToggle()}</span>
</label> </label>
<label> <label>
<input type="checkbox" class="nes-checkbox is-dark" bind:checked={valueMicrophonePrivacySettings} /> <input
type="checkbox"
class="nes-checkbox is-dark"
on:change={(event) => analyticsClient.settingMicrophone(event.currentTarget.value)}
bind:checked={valueMicrophonePrivacySettings}
/>
<span>{$LL.menu.settings.privacySettings.microphoneToggle()}</span> <span>{$LL.menu.settings.privacySettings.microphoneToggle()}</span>
</label> </label>
</section> </section>
@ -211,6 +222,7 @@
type="checkbox" type="checkbox"
class="nes-checkbox is-dark" class="nes-checkbox is-dark"
bind:checked={fullscreen} bind:checked={fullscreen}
on:change={(event) => analyticsClient.settingFullscreen(event.currentTarget.value)}
on:change={changeFullscreen} on:change={changeFullscreen}
/> />
<span>{$LL.menu.settings.fullscreen()}</span> <span>{$LL.menu.settings.fullscreen()}</span>
@ -220,6 +232,7 @@
type="checkbox" type="checkbox"
class="nes-checkbox is-dark" class="nes-checkbox is-dark"
bind:checked={notification} bind:checked={notification}
on:change={(event) => analyticsClient.settingNotification(event.currentTarget.value)}
on:change={changeNotification} on:change={changeNotification}
/> />
<span>{$LL.menu.settings.notifications()}</span> <span>{$LL.menu.settings.notifications()}</span>
@ -229,6 +242,7 @@
type="checkbox" type="checkbox"
class="nes-checkbox is-dark" class="nes-checkbox is-dark"
bind:checked={forceCowebsiteTrigger} bind:checked={forceCowebsiteTrigger}
on:change={(event) => analyticsClient.settingAskWebsite(event.currentTarget.value)}
on:change={changeForceCowebsiteTrigger} on:change={changeForceCowebsiteTrigger}
/> />
<span>{$LL.menu.settings.cowebsiteTrigger()}</span> <span>{$LL.menu.settings.cowebsiteTrigger()}</span>
@ -238,6 +252,7 @@
type="checkbox" type="checkbox"
class="nes-checkbox is-dark" class="nes-checkbox is-dark"
bind:checked={ignoreFollowRequests} bind:checked={ignoreFollowRequests}
on:change={(event) => analyticsClient.settingRequestFollow(event.currentTarget.value)}
on:change={changeIgnoreFollowRequests} on:change={changeIgnoreFollowRequests}
/> />
<span>{$LL.menu.settings.ignoreFollowRequest()}</span> <span>{$LL.menu.settings.ignoreFollowRequest()}</span>
@ -246,6 +261,7 @@
type="checkbox" type="checkbox"
class="nes-checkbox is-dark" class="nes-checkbox is-dark"
bind:checked={decreaseAudioPlayerVolumeWhileTalking} bind:checked={decreaseAudioPlayerVolumeWhileTalking}
on:change={(event) => analyticsClient.settingDecreaseAudioVolume(event.currentTarget.value)}
on:change={changeDecreaseAudioPlayerVolumeWhileTalking} on:change={changeDecreaseAudioPlayerVolumeWhileTalking}
/> />
<span>{$LL.audio.manager.reduce()}</span> <span>{$LL.audio.manager.reduce()}</span>

View File

@ -2,6 +2,7 @@
import { fly } from "svelte/transition"; import { fly } from "svelte/transition";
import { errorScreenStore } from "../../Stores/ErrorScreenStore"; import { errorScreenStore } from "../../Stores/ErrorScreenStore";
import { gameManager } from "../../Phaser/Game/GameManager"; import { gameManager } from "../../Phaser/Game/GameManager";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { onDestroy } from "svelte"; import { onDestroy } from "svelte";
@ -11,7 +12,8 @@
let errorScreen = get(errorScreenStore); let errorScreen = get(errorScreenStore);
function click() { function click() {
window.location.reload(); if (errorScreen.type === "unauthorized") void connectionManager.logout();
else window.location.reload();
} }
let details = errorScreen.details; let details = errorScreen.details;
let timeVar = errorScreen.timeToRetry ?? 0; let timeVar = errorScreen.timeToRetry ?? 0;
@ -37,9 +39,9 @@
<p class="details"> <p class="details">
{detailsStylized}{#if $errorScreenStore.type === "retry"}<div class="loading" />{/if} {detailsStylized}{#if $errorScreenStore.type === "retry"}<div class="loading" />{/if}
</p> </p>
{#if $errorScreenStore.type === "retry" && $errorScreenStore.canRetryManual} {#if ($errorScreenStore.type === "retry" && $errorScreenStore.canRetryManual) || $errorScreenStore.type === "unauthorized"}
<button type="button" class="nes-btn is-primary button" on:click={click}> <button type="button" class="nes-btn is-primary button" on:click={click}>
<img src={reload} alt="" class="reload" /> {#if $errorScreenStore.type === "retry"}<img src={reload} alt="" class="reload" />{/if}
{$errorScreenStore.buttonTitle} {$errorScreenStore.buttonTitle}
</button> </button>
{/if} {/if}

View File

@ -3,6 +3,7 @@
import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene"; import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene";
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
import { customizeAvailableStore, selectedCollection } from "../../Stores/SelectCharacterSceneStore"; import { customizeAvailableStore, selectedCollection } from "../../Stores/SelectCharacterSceneStore";
import { analyticsClient } from "../../Administration/AnalyticsClient";
export let game: Game; export let game: Game;
@ -40,13 +41,15 @@
<button <button
type="submit" type="submit"
class="selectCharacterSceneFormSubmit nes-btn is-primary" class="selectCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={cameraScene}>{$LL.woka.selectWoka.continue()}</button on:click={() => analyticsClient.selectWoka()}
on:click={cameraScene}>{$LL.woka.selectWoka.continue()}</button
> >
{#if $customizeAvailableStore} {#if $customizeAvailableStore}
<button <button
type="submit" type="submit"
class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn" class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn"
on:click|preventDefault={customizeScene}>{$LL.woka.selectWoka.customize()}</button on:click={() => analyticsClient.selectCustomWoka()}
on:click={customizeScene}>{$LL.woka.selectWoka.customize()}</button
> >
{/if} {/if}
</section> </section>

View File

@ -364,15 +364,13 @@ class ConnectionManager {
if (locale) { if (locale) {
try { try {
if (locales.indexOf(locale) == -1) { if (locales.indexOf(locale) !== -1) {
locales.forEach((l) => { await setCurrentLocale(locale as Locales);
if (l.startsWith(locale.split("-")[0])) {
setCurrentLocale(l);
return;
}
});
} else { } else {
setCurrentLocale(locale as Locales); const nonRegionSpecificLocale = locales.find((l) => l.startsWith(locale.split("-")[0]));
if (nonRegionSpecificLocale) {
await setCurrentLocale(nonRegionSpecificLocale);
}
} }
} catch (err) { } catch (err) {
console.warn("Could not set locale", err); console.warn("Could not set locale", err);

View File

@ -483,9 +483,15 @@ export class RoomConnection implements RoomConnection {
} }
case "errorScreenMessage": { case "errorScreenMessage": {
this._errorScreenMessageStream.next(message.errorScreenMessage); this._errorScreenMessageStream.next(message.errorScreenMessage);
if (message.errorScreenMessage.code !== "retry") this.closed = true; console.error("An error occurred server side: " + JSON.stringify(message.errorScreenMessage));
console.error("An error occurred server side: " + message.errorScreenMessage.code); if (message.errorScreenMessage.code !== "retry") {
errorScreenStore.setError(message.errorScreenMessage); this.closed = true;
}
if (message.errorScreenMessage.type === "redirect" && message.errorScreenMessage.urlToRedirect) {
window.location.assign(message.errorScreenMessage.urlToRedirect);
} else {
errorScreenStore.setError(message.errorScreenMessage);
}
break; break;
} }
default: { default: {

View File

@ -61,9 +61,8 @@ export class CustomWokaPreviewer extends Phaser.GameObjects.Container {
this.frame = this.scene.add.graphics(); this.frame = this.scene.add.graphics();
this.turnIcon = this.scene.add this.turnIcon = this.scene.add
.image(this.background.displayWidth * 0.35, this.background.displayHeight * 0.35, "iconTurn") .image(this.background.displayWidth * 0.35, this.background.displayHeight * 0.35, "iconTurn")
.setScale(0.25) .setScale(0.2)
.setTintFill(0xffffff) .setAlpha(0.75);
.setAlpha(0.5);
this.drawFrame(); this.drawFrame();
this.setSize(this.SIZE, this.SIZE); this.setSize(this.SIZE, this.SIZE);
@ -130,11 +129,11 @@ export class CustomWokaPreviewer extends Phaser.GameObjects.Container {
this.changeAnimation(direction, moving); this.changeAnimation(direction, moving);
this.turnIconTween?.stop(); this.turnIconTween?.stop();
this.turnIcon.setScale(0.25); this.turnIcon.setScale(0.2);
this.turnIconTween = this.scene.tweens.add({ this.turnIconTween = this.scene.tweens.add({
targets: [this.turnIcon], targets: [this.turnIcon],
duration: 100, duration: 100,
scale: 0.2, scale: 0.15,
yoyo: true, yoyo: true,
ease: Easing.SineEaseIn, ease: Easing.SineEaseIn,
}); });

View File

@ -6,6 +6,7 @@ export interface IconButtonConfig {
hover: IconButtonAppearanceConfig; hover: IconButtonAppearanceConfig;
pressed: IconButtonAppearanceConfig; pressed: IconButtonAppearanceConfig;
selected: IconButtonAppearanceConfig; selected: IconButtonAppearanceConfig;
iconScale?: number;
} }
export interface IconButtonAppearanceConfig { export interface IconButtonAppearanceConfig {
@ -34,7 +35,7 @@ export class IconButton extends Phaser.GameObjects.Container {
this.config = config; this.config = config;
this.background = this.scene.add.graphics(); this.background = this.scene.add.graphics();
this.icon = this.scene.add.image(0, 0, this.config.iconTextureKey); this.icon = this.scene.add.image(0, 0, this.config.iconTextureKey).setScale(config.iconScale ?? 1);
this.drawBackground(this.config.idle); this.drawBackground(this.config.idle);
this.add([this.background, this.icon]); this.add([this.background, this.icon]);

View File

@ -4,7 +4,7 @@ import { Character } from "../Entity/Character";
import type { GameScene } from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import type { PointInterface } from "../../Connexion/ConnexionModels"; import type { PointInterface } from "../../Connexion/ConnexionModels";
import type { PlayerAnimationDirections } from "../Player/Animation"; import type { PlayerAnimationDirections } from "../Player/Animation";
import type { Unsubscriber } from "svelte/store"; import { get, Unsubscriber } from "svelte/store";
import type { ActivatableInterface } from "../Game/ActivatableInterface"; import type { ActivatableInterface } from "../Game/ActivatableInterface";
import type CancelablePromise from "cancelable-promise"; import type CancelablePromise from "cancelable-promise";
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
@ -113,7 +113,7 @@ export class RemotePlayer extends Character implements ActivatableInterface {
const actions: ActionsMenuAction[] = []; const actions: ActionsMenuAction[] = [];
if (this.visitCardUrl) { if (this.visitCardUrl) {
actions.push({ actions.push({
actionName: LL.woka.menu.businessCard(), actionName: get(LL).woka.menu.businessCard(),
protected: true, protected: true,
priority: 1, priority: 1,
callback: () => { callback: () => {
@ -125,8 +125,8 @@ export class RemotePlayer extends Character implements ActivatableInterface {
actions.push({ actions.push({
actionName: blackListManager.isBlackListed(this.userUuid) actionName: blackListManager.isBlackListed(this.userUuid)
? LL.report.block.unblock() ? get(LL).report.block.unblock()
: LL.report.block.block(), : get(LL).report.block.block(),
protected: true, protected: true,
priority: -1, priority: -1,
style: "is-error", style: "is-error",

View File

@ -76,6 +76,7 @@ export class ActivatablesManager {
const currentPlayerPos = this.currentPlayer.getDirectionalActivationPosition( const currentPlayerPos = this.currentPlayer.getDirectionalActivationPosition(
this.directionalActivationPositionShift this.directionalActivationPositionShift
); );
this.activatableObjectsDistances.clear();
for (const object of objects) { for (const object of objects) {
const distance = MathUtils.distanceBetween(currentPlayerPos, object.getPosition()); const distance = MathUtils.distanceBetween(currentPlayerPos, object.getPosition());
this.activatableObjectsDistances.set(object, distance); this.activatableObjectsDistances.set(object, distance);

View File

@ -1135,6 +1135,13 @@ export class GameScene extends DirtyScene {
}) })
); );
this.iframeSubscriptionList.push(
iframeListener.stopSoundStream.subscribe((stopSoundEvent) => {
const url = new URL(stopSoundEvent.url, this.MapUrlFile);
soundManager.stopSound(this.sound, url.toString());
})
);
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
iframeListener.addActionsMenuKeyToRemotePlayerStream.subscribe((data) => { iframeListener.addActionsMenuKeyToRemotePlayerStream.subscribe((data) => {
this.MapPlayersByKey.get(data.id)?.registerActionsMenuAction({ this.MapPlayersByKey.get(data.id)?.registerActionsMenuAction({

View File

@ -94,6 +94,7 @@ export class CustomizeScene extends AbstractCharacterScene {
} }
public create(): void { public create(): void {
this.tryLoadLastUsedWokaLayers();
waScaleManager.zoomModifier = 1; waScaleManager.zoomModifier = 1;
this.createSlotBackgroundTextures(); this.createSlotBackgroundTextures();
this.initializeCustomWokaPreviewer(); this.initializeCustomWokaPreviewer();
@ -149,12 +150,35 @@ export class CustomizeScene extends AbstractCharacterScene {
this.scene.run(SelectCharacterSceneName); this.scene.run(SelectCharacterSceneName);
} }
private tryLoadLastUsedWokaLayers(): void {
try {
const savedWokaLayers = gameManager.getCharacterLayers();
if (savedWokaLayers && savedWokaLayers.length !== 0) {
this.selectedLayers = [];
for (let i = 0; i < savedWokaLayers.length; i += 1) {
this.selectedLayers.push(
this.layers[i].findIndex((item) => item.id === gameManager.getCharacterLayers()[i])
);
}
}
} catch {
console.warn("Cannot load previous WOKA");
}
}
private createSlotBackgroundTextures(): void { private createSlotBackgroundTextures(): void {
for (let i = 0; i < 4; i += 1) { for (let i = 0; i < 4; i += 1) {
if (this.textures.getTextureKeys().includes(`floorTexture${i}`)) { if (this.textures.getTextureKeys().includes(`floorTexture${i}`)) {
continue; continue;
} }
TexturesHelper.createFloorRectangleTexture(this, `floorTexture${i}`, 50, 50, "floorTiles", i); TexturesHelper.createFloorRectangleTexture(
this,
`floorTexture${i}`,
WokaBodyPartSlot.SIZE,
WokaBodyPartSlot.SIZE,
"floorTiles",
i
);
} }
} }
@ -213,15 +237,16 @@ export class CustomizeScene extends AbstractCharacterScene {
), ),
[CustomWokaBodyPart.Body]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconBody")), [CustomWokaBodyPart.Body]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconBody")),
[CustomWokaBodyPart.Clothes]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconClothes")), [CustomWokaBodyPart.Clothes]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconClothes")),
[CustomWokaBodyPart.Eyes]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconEyes")), [CustomWokaBodyPart.Eyes]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconEyes", 0.7)),
[CustomWokaBodyPart.Hair]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconHair")), [CustomWokaBodyPart.Hair]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconHair")),
[CustomWokaBodyPart.Hat]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconHat")), [CustomWokaBodyPart.Hat]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconHat")),
}; };
} }
private getDefaultIconButtonConfig(iconTextureKey: string): IconButtonConfig { private getDefaultIconButtonConfig(iconTextureKey: string, iconScale?: number): IconButtonConfig {
return { return {
iconTextureKey, iconTextureKey,
iconScale,
width: 25, width: 25,
height: 25, height: 25,
idle: { idle: {
@ -327,13 +352,14 @@ export class CustomizeScene extends AbstractCharacterScene {
} }
private handleCustomWokaPreviewerOnResize(): void { private handleCustomWokaPreviewerOnResize(): void {
const ratio = innerHeight / innerWidth;
this.customWokaPreviewer.x = this.cameras.main.worldView.x + this.cameras.main.width / 2; this.customWokaPreviewer.x = this.cameras.main.worldView.x + this.cameras.main.width / 2;
this.customWokaPreviewer.y = this.customWokaPreviewer.displayHeight * 0.5 + 10; this.customWokaPreviewer.y = this.customWokaPreviewer.displayHeight * 0.5 + (ratio > 1.6 ? 40 : 10);
} }
private handleBodyPartButtonsOnResize(): void { private handleBodyPartButtonsOnResize(): void {
const ratio = innerHeight / innerWidth; const ratio = innerHeight / innerWidth;
const slotDimension = 50; const slotDimension = WokaBodyPartSlot.SIZE;
for (const part in this.bodyPartsButtons) { for (const part in this.bodyPartsButtons) {
this.bodyPartsButtons[part as CustomWokaBodyPart].setDisplaySize(slotDimension, slotDimension); this.bodyPartsButtons[part as CustomWokaBodyPart].setDisplaySize(slotDimension, slotDimension);
@ -420,7 +446,7 @@ export class CustomizeScene extends AbstractCharacterScene {
private handleRandomizeButtonOnResize(): void { private handleRandomizeButtonOnResize(): void {
const x = const x =
this.customWokaPreviewer.x + this.customWokaPreviewer.x -
(this.customWokaPreviewer.displayWidth - this.randomizeButton.displayWidth) * 0.5; (this.customWokaPreviewer.displayWidth - this.randomizeButton.displayWidth) * 0.5;
const y = const y =
this.customWokaPreviewer.y + this.customWokaPreviewer.y +
@ -431,7 +457,7 @@ export class CustomizeScene extends AbstractCharacterScene {
private handleFinishButtonOnResize(): void { private handleFinishButtonOnResize(): void {
const x = const x =
this.customWokaPreviewer.x - this.customWokaPreviewer.x +
(this.customWokaPreviewer.displayWidth - this.randomizeButton.displayWidth) * 0.5; (this.customWokaPreviewer.displayWidth - this.randomizeButton.displayWidth) * 0.5;
const y = const y =
this.customWokaPreviewer.y + this.customWokaPreviewer.y +

View File

@ -6,7 +6,6 @@ import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene";
import { localeDetector } from "../../i18n/locales"; import { localeDetector } from "../../i18n/locales";
import { errorScreenStore } from "../../Stores/ErrorScreenStore"; import { errorScreenStore } from "../../Stores/ErrorScreenStore";
import { isErrorApiData } from "../../Messages/JsonMessages/ErrorApiData"; import { isErrorApiData } from "../../Messages/JsonMessages/ErrorApiData";
import { connectionManager } from "../../Connexion/ConnectionManager";
export const EntrySceneName = "EntryScene"; export const EntrySceneName = "EntryScene";
@ -49,9 +48,7 @@ export class EntryScene extends Scene {
.catch((err) => { .catch((err) => {
const errorType = isErrorApiData.safeParse(err?.response?.data); const errorType = isErrorApiData.safeParse(err?.response?.data);
if (errorType.success) { if (errorType.success) {
if (errorType.data.type === "unauthorized") { if (errorType.data.type === "redirect") {
void connectionManager.logout();
} else if (errorType.data.type === "redirect") {
window.location.assign(errorType.data.urlToRedirect); window.location.assign(errorType.data.urlToRedirect);
} else errorScreenStore.setError(err?.response?.data); } else errorScreenStore.setError(err?.response?.data);
} else { } else {

View File

@ -18,6 +18,7 @@ import { DraggableGrid } from "@home-based-studio/phaser3-utils";
import { WokaSlot } from "../Components/SelectWoka/WokaSlot"; import { WokaSlot } from "../Components/SelectWoka/WokaSlot";
import { DraggableGridEvent } from "@home-based-studio/phaser3-utils/lib/utils/gui/containers/grids/DraggableGrid"; import { DraggableGridEvent } from "@home-based-studio/phaser3-utils/lib/utils/gui/containers/grids/DraggableGrid";
import { wokaList } from "../../Messages/JsonMessages/PlayerTextures"; import { wokaList } from "../../Messages/JsonMessages/PlayerTextures";
import { myCameraVisibilityStore } from "../../Stores/MyCameraStoreVisibility";
//todo: put this constants in a dedicated file //todo: put this constants in a dedicated file
export const SelectCharacterSceneName = "SelectCharacterScene"; export const SelectCharacterSceneName = "SelectCharacterScene";
@ -133,6 +134,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
return; return;
} }
this.selectedWoka = null; this.selectedWoka = null;
myCameraVisibilityStore.set(false);
this.scene.sleep(SelectCharacterSceneName); this.scene.sleep(SelectCharacterSceneName);
this.scene.run(CustomizeSceneName); this.scene.run(CustomizeSceneName);
selectCharacterSceneVisibleStore.set(false); selectCharacterSceneVisibleStore.set(false);

View File

@ -87,7 +87,10 @@ function createChatMessagesStore() {
return list; return list;
}); });
}, },
addExternalMessage(authorId: number, text: string) { /**
* @param origin The iframe that originated this message (if triggered from the Scripting API), or undefined otherwise.
*/
addExternalMessage(authorId: number, text: string, origin?: Window) {
update((list) => { update((list) => {
const lastMessage = list[list.length - 1]; const lastMessage = list[list.length - 1];
if ( if (
@ -106,7 +109,7 @@ function createChatMessagesStore() {
}); });
} }
iframeListener.sendUserInputChat(text); iframeListener.sendUserInputChat(text, origin);
return list; return list;
}); });
chatVisibilityStore.set(true); chatVisibilityStore.set(true);

View File

@ -2,7 +2,6 @@ import { get, writable } from "svelte/store";
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
import { userIsAdminStore } from "./GameStore"; import { userIsAdminStore } from "./GameStore";
import { CONTACT_URL, IDENTITY_URL, PROFILE_URL } from "../Enum/EnvironmentVariable"; import { CONTACT_URL, IDENTITY_URL, PROFILE_URL } from "../Enum/EnvironmentVariable";
import { analyticsClient } from "../Administration/AnalyticsClient";
import type { Translation } from "../i18n/i18n-types"; import type { Translation } from "../i18n/i18n-types";
import axios from "axios"; import axios from "axios";
import { localUserStore } from "../Connexion/LocalUserStore"; import { localUserStore } from "../Connexion/LocalUserStore";
@ -14,7 +13,6 @@ export const userIsConnected = writable(false);
export const profileAvailable = writable(true); export const profileAvailable = writable(true);
menuVisiblilityStore.subscribe((value) => { menuVisiblilityStore.subscribe((value) => {
if (value) analyticsClient.openedMenu();
if (userIsConnected && value && IDENTITY_URL != null) { if (userIsConnected && value && IDENTITY_URL != null) {
axios.get(getMeUrl()).catch((err) => { axios.get(getMeUrl()).catch((err) => {
console.error("menuVisiblilityStore => err => ", err); console.error("menuVisiblilityStore => err => ", err);

View File

@ -8,6 +8,7 @@ import { isMediaBreakpointDown } from "../Utils/BreakpointsUtils";
import { LayoutMode } from "./LayoutManager"; import { LayoutMode } from "./LayoutManager";
import type { CoWebsite } from "./CoWebsite/CoWesbite"; import type { CoWebsite } from "./CoWebsite/CoWesbite";
import type CancelablePromise from "cancelable-promise"; import type CancelablePromise from "cancelable-promise";
import { analyticsClient } from "../Administration/AnalyticsClient";
export enum iframeStates { export enum iframeStates {
closed = 1, closed = 1,
@ -126,6 +127,7 @@ class CoWebsiteManager {
const buttonCloseCoWebsite = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId); const buttonCloseCoWebsite = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
buttonCloseCoWebsite.addEventListener("click", () => { buttonCloseCoWebsite.addEventListener("click", () => {
analyticsClient.closeMultiIframe();
const coWebsite = this.getMainCoWebsite(); const coWebsite = this.getMainCoWebsite();
if (!coWebsite) { if (!coWebsite) {
@ -143,6 +145,7 @@ class CoWebsiteManager {
const buttonFullScreenFrame = HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId); const buttonFullScreenFrame = HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId);
buttonFullScreenFrame.addEventListener("click", () => { buttonFullScreenFrame.addEventListener("click", () => {
analyticsClient.fullScreenMultiIframe();
buttonFullScreenFrame.blur(); buttonFullScreenFrame.blur();
this.fullscreen(); this.fullscreen();
}); });
@ -159,6 +162,7 @@ class CoWebsiteManager {
}); });
buttonSwipe.addEventListener("click", () => { buttonSwipe.addEventListener("click", () => {
analyticsClient.switchMultiIframe();
const mainCoWebsite = this.getMainCoWebsite(); const mainCoWebsite = this.getMainCoWebsite();
const highlightedEmbed = get(highlightedEmbedScreen); const highlightedEmbed = get(highlightedEmbedScreen);
if (highlightedEmbed?.type === "cowebsite") { if (highlightedEmbed?.type === "cowebsite") {

View File

@ -1,3 +1 @@
i18n-svelte.ts i18n-*.ts
i18n-types.ts
i18n-util.ts

View File

@ -16,8 +16,6 @@ import trigger from "./trigger";
const de_DE: Translation = { const de_DE: Translation = {
...(en_US as Translation), ...(en_US as Translation),
language: "Deutsch",
country: "Deutschland",
audio, audio,
camera, camera,
chat, chat,

View File

@ -77,7 +77,8 @@ const menu: NonNullable<Translation["menu"]> = {
description: "Link zu diesem Raum teilen!", description: "Link zu diesem Raum teilen!",
copy: "Kopieren", copy: "Kopieren",
share: "Teilen", share: "Teilen",
walk_automatically_to_position: "Automatisch zu meiner Position gehen", walkAutomaticallyToPosition: "Automatisch zu meiner Position gehen",
selectEntryPoint: "Select an entry point",
}, },
globalMessage: { globalMessage: {
text: "Text", text: "Text",

View File

@ -14,8 +14,6 @@ import emoji from "./emoji";
import trigger from "./trigger"; import trigger from "./trigger";
const en_US: BaseTranslation = { const en_US: BaseTranslation = {
language: "English",
country: "United States",
audio, audio,
camera, camera,
chat, chat,

View File

@ -77,7 +77,8 @@ const menu: BaseTranslation = {
description: "Share the link of the room!", description: "Share the link of the room!",
copy: "Copy", copy: "Copy",
share: "Share", share: "Share",
walk_automatically_to_position: "Walk automatically to my position", walkAutomaticallyToPosition: "Walk automatically to my position",
selectEntryPoint: "Select an entry point",
}, },
globalMessage: { globalMessage: {
text: "Text", text: "Text",

View File

@ -1,8 +1,8 @@
import type { AsyncFormattersInitializer } from "typesafe-i18n"; import type { FormattersInitializer } from "typesafe-i18n";
import type { Locales, Formatters } from "./i18n-types"; import type { Locales, Formatters } from "./i18n-types";
// eslint-disable-next-line @typescript-eslint/require-await // eslint-disable-next-line @typescript-eslint/require-await
export const initFormatters: AsyncFormattersInitializer<Locales, Formatters> = async () => { export const initFormatters: FormattersInitializer<Locales, Formatters> = async () => {
const formatters: Formatters = { const formatters: Formatters = {
// add your formatter functions here // add your formatter functions here
}; };

View File

@ -1,70 +1,44 @@
import { detectLocale, navigatorDetector, initLocalStorageDetector } from "typesafe-i18n/detectors"; import { detectLocale, navigatorDetector, initLocalStorageDetector } from "typesafe-i18n/detectors";
import { FALLBACK_LOCALE } from "../Enum/EnvironmentVariable"; import { FALLBACK_LOCALE } from "../Enum/EnvironmentVariable";
import { initI18n, setLocale, locale } from "./i18n-svelte"; import { setLocale } from "./i18n-svelte";
import type { Locales } from "./i18n-types"; import type { Locales } from "./i18n-types";
import { baseLocale, getTranslationForLocale, locales } from "./i18n-util"; import { baseLocale, locales } from "./i18n-util";
import { get } from "svelte/store"; import { loadLocaleAsync } from "./i18n-util.async";
const fallbackLocale = FALLBACK_LOCALE || baseLocale; const fallbackLocale = (FALLBACK_LOCALE || baseLocale) as Locales;
const localStorageProperty = "language"; const localStorageProperty = "language";
export const localeDetector = async () => { export const localeDetector = async () => {
const exist = localStorage.getItem(localStorageProperty); const exist = localStorage.getItem(localStorageProperty);
let detectedLocale: Locales = fallbackLocale as Locales; let detectedLocale: Locales = fallbackLocale;
if (exist) { if (exist) {
const localStorageDetector = initLocalStorageDetector(localStorageProperty); const localStorageDetector = initLocalStorageDetector(localStorageProperty);
detectedLocale = detectLocale(fallbackLocale, locales, localStorageDetector) as Locales; detectedLocale = detectLocale(fallbackLocale, locales, localStorageDetector);
} else { } else {
detectedLocale = detectLocale(fallbackLocale, locales, navigatorDetector) as Locales; detectedLocale = detectLocale(fallbackLocale, locales, navigatorDetector);
} }
await initI18n(detectedLocale); await setCurrentLocale(detectedLocale);
}; };
export const setCurrentLocale = (locale: Locales) => { export const setCurrentLocale = async (locale: Locales) => {
localStorage.setItem(localStorageProperty, locale); localStorage.setItem(localStorageProperty, locale);
setLocale(locale).catch(() => { await loadLocaleAsync(locale);
console.log("Cannot reload the locale!"); setLocale(locale);
});
}; };
export type DisplayableLocale = { id: Locales; language: string; country: string }; export const displayableLocales: { id: Locales; language: string; region: string }[] = locales.map((locale) => {
const [language, region] = locale.split("-");
function getDisplayableLocales() { // backwards compatibility
const localesObject: DisplayableLocale[] = []; if (!Intl.DisplayNames) {
locales.forEach((locale) => { return { id: locale, language, region };
getTranslationForLocale(locale)
.then((translations) => {
localesObject.push({
id: locale,
language: translations.language,
country: translations.country,
});
})
.catch((error) => {
console.log(error);
});
});
return localesObject;
}
export const displayableLocales = getDisplayableLocales();
export const i18nJson = (text: string): string => {
if (text.trim().startsWith("{")) {
try {
const textObject = JSON.parse(text);
if (textObject[get(locale)]) {
return textObject[get(locale)];
} else if (Object.keys(textObject).length > 0) {
// fallback to first value
return textObject[Object.keys(textObject)[0]];
}
} catch (err) {
//
}
} }
return text;
}; return {
id: locale,
language: new Intl.DisplayNames(locale, { type: "language" }).of(language),
region: new Intl.DisplayNames(locale, { type: "region" }).of(region),
};
});

View File

@ -95,7 +95,7 @@
&-buffer { &-buffer {
iframe { iframe {
z-index: 45 !important; z-index: 201 !important;
pointer-events: none !important; pointer-events: none !important;
overflow: hidden; overflow: hidden;
border: 0; border: 0;

View File

@ -8,7 +8,7 @@
"moduleResolution": "node", "moduleResolution": "node",
//"module": "CommonJS", //"module": "CommonJS",
"module": "ESNext", "module": "ESNext",
"target": "ES2017", "target": "ES2020",
"declaration": false, "declaration": false,
"downlevelIteration": true, "downlevelIteration": true,
"jsx": "react", "jsx": "react",

View File

@ -7,6 +7,14 @@
resolved "https://registry.yarnpkg.com/@16bits/nes.css/-/nes.css-2.3.2.tgz#e69db834119b33ae8d3cb044f106a07a17cadd6f" resolved "https://registry.yarnpkg.com/@16bits/nes.css/-/nes.css-2.3.2.tgz#e69db834119b33ae8d3cb044f106a07a17cadd6f"
integrity sha512-nEM5PIth+Bab5JSOa4uUR+PMNUsNTYxA55oVlG3gXI/4LoYtWS767Uv9Pu/KCbHXVvnIjt4ZXt13kZw3083qTw== integrity sha512-nEM5PIth+Bab5JSOa4uUR+PMNUsNTYxA55oVlG3gXI/4LoYtWS767Uv9Pu/KCbHXVvnIjt4ZXt13kZw3083qTw==
"@anatine/zod-openapi@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@anatine/zod-openapi/-/zod-openapi-1.3.0.tgz#b5b38c3d821b79674226aa7b327c88c371860d0d"
integrity sha512-l54DypUdDsIq1Uwjv4ib9IBkTXMKZQLUj7qvdFL51EExC5LdSSqOlTOyaVVZZGYgWPKM7ZjGklhdoknLz4EC+w==
dependencies:
ts-deepmerge "^1.1.0"
validator "^13.7.0"
"@babel/runtime@^7.14.0": "@babel/runtime@^7.14.0":
version "7.14.0" version "7.14.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6"
@ -77,10 +85,10 @@
resolved "https://registry.yarnpkg.com/@geprog/vite-plugin-env-config/-/vite-plugin-env-config-4.0.3.tgz#ca04bd9ad9f55fe568917db79266afe8e766e25e" resolved "https://registry.yarnpkg.com/@geprog/vite-plugin-env-config/-/vite-plugin-env-config-4.0.3.tgz#ca04bd9ad9f55fe568917db79266afe8e766e25e"
integrity sha512-2HDCV+6XXJjSuBAhDWLRr111buMQ3bIZrKo3dymIhEJ4oJCC/3yDqg7HDQIn8Y8KKbsM0AtuHMZW4yz2tPBsYg== integrity sha512-2HDCV+6XXJjSuBAhDWLRr111buMQ3bIZrKo3dymIhEJ4oJCC/3yDqg7HDQIn8Y8KKbsM0AtuHMZW4yz2tPBsYg==
"@home-based-studio/phaser3-utils@^0.4.2": "@home-based-studio/phaser3-utils@^0.4.7":
version "0.4.2" version "0.4.7"
resolved "https://registry.yarnpkg.com/@home-based-studio/phaser3-utils/-/phaser3-utils-0.4.2.tgz#b2c1815a6b51321ea8dab027b5badcf714d99fd6" resolved "https://registry.yarnpkg.com/@home-based-studio/phaser3-utils/-/phaser3-utils-0.4.7.tgz#d0464c81cb27328657d3fd048396f6936e200c48"
integrity sha512-S0VkAq3z0Kf0vEUUyCDes911icvc+nkUq7lGp23zD/5lk7LTGM51NswSAfel7Rm/DLY8IBxvDTBJADTf/De82w== integrity sha512-gYt1mkuad85uzYwHK0+wp+mrsGASV4sRZPaHZHnO8A2ofTAnX36S3PcI+BqKchdJ0I7jvBQcfh0yp1Ug0BHT+A==
dependencies: dependencies:
phaser "3.55.1" phaser "3.55.1"
@ -2060,6 +2068,13 @@ onetime@^5.1.0, onetime@^5.1.2:
dependencies: dependencies:
mimic-fn "^2.1.0" mimic-fn "^2.1.0"
openapi3-ts@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/openapi3-ts/-/openapi3-ts-2.0.2.tgz#a200dd838bf24c9086c8eedcfeb380b7eb31e82a"
integrity sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==
dependencies:
yaml "^1.10.2"
optionator@^0.9.1: optionator@^0.9.1:
version "0.9.1" version "0.9.1"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
@ -2880,6 +2895,11 @@ to-regex-range@^5.0.1:
dependencies: dependencies:
is-number "^7.0.0" is-number "^7.0.0"
ts-deepmerge@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/ts-deepmerge/-/ts-deepmerge-1.1.0.tgz#4236ae102199affe2e77690dcf198a420160eef2"
integrity sha512-VvwaV/6RyYMwT9d8dClmfHIsG2PCdm6WY430QKOIbPRR50Y/1Q2ilp4i2XEZeHFcNqfaYnAQzpyUC6XA0AqqBg==
ts-deferred@^1.0.4: ts-deferred@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/ts-deferred/-/ts-deferred-1.0.4.tgz#58145ebaeef5b8f2a290b8cec3d060839f9489c7" resolved "https://registry.yarnpkg.com/ts-deferred/-/ts-deferred-1.0.4.tgz#58145ebaeef5b8f2a290b8cec3d060839f9489c7"
@ -2996,10 +3016,10 @@ type-fest@^0.21.3:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
typesafe-i18n@^2.59.0: typesafe-i18n@^5.4.0:
version "2.59.0" version "5.4.0"
resolved "https://registry.yarnpkg.com/typesafe-i18n/-/typesafe-i18n-2.59.0.tgz#09a9a32e61711418d927a389fa52e1c06a5fa5c4" resolved "https://registry.yarnpkg.com/typesafe-i18n/-/typesafe-i18n-5.4.0.tgz#cab696160bb144c387d7cbd13f7a728aa8371777"
integrity sha512-Qv3Mrwmb8b73VNzQDPHPECzwymdBRVyDiZ3w2qnp4c2iv/7TGuiJegNHT/l3MooEN7IPbSpc5tbXw2x3MbGtFg== integrity sha512-htewpld3FzZQv3Y1G31w54bofaaKR11MCkDK0FIYuXCpX72y1G6fkXUDslqzZCyVkZWRnIhY8leviNDxLwEzRw==
typescript@*: typescript@*:
version "4.3.2" version "4.3.2"
@ -3071,6 +3091,11 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0" spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0" spdx-expression-parse "^3.0.0"
validator@^13.7.0:
version "13.7.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857"
integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==
vite-plugin-rewrite-all@^0.1.2: vite-plugin-rewrite-all@^0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/vite-plugin-rewrite-all/-/vite-plugin-rewrite-all-0.1.2.tgz#312bbcd76c700ceac5153bfc5ad7e3e3e4bc9606" resolved "https://registry.yarnpkg.com/vite-plugin-rewrite-all/-/vite-plugin-rewrite-all-0.1.2.tgz#312bbcd76c700ceac5153bfc5ad7e3e3e4bc9606"

View File

@ -1,7 +1,7 @@
let menuIframeApi = undefined; let menuIframeApi = undefined;
WA.ui.registerMenuCommand('custom callback menu', () => { WA.ui.registerMenuCommand('custom callback menu', () => {
WA.nav.openTab("https://workadventu.re/"); WA.chat.sendChatMessage('Custom menu clicked', 'Mr Robot');
}) })
WA.ui.registerMenuCommand('custom iframe menu', {iframe: 'customIframeMenu.html'}); WA.ui.registerMenuCommand('custom iframe menu', {iframe: 'customIframeMenu.html'});

View File

@ -40,7 +40,7 @@
{ {
"fontfamily":"Sans Serif", "fontfamily":"Sans Serif",
"pixelsize":13, "pixelsize":13,
"text":"Test:\nClick on tne WOKA name\n\nResult:\nThe tab with all emoji should be displayed\n\nTest:\nChoose on emoji and click on it\n\nResult:\nEmoji will be play at the top of your WOKA", "text":"Test:\nClick on the WOKA name\n\nResult:\nThe tab with all emojis should be displayed\n\nTest:\nChoose on emoji and click on it\n\nResult:\nEmoji will be displayed above your WOKA",
"wrap":true "wrap":true
}, },
"type":"", "type":"",

View File

@ -216,7 +216,7 @@
<input type="radio" name="test-cowebsite-allowAPI"> Success <input type="radio" name="test-cowebsite-allowAPI"> Failure <input type="radio" name="test-cowebsite-allowAPI" checked> Pending <input type="radio" name="test-cowebsite-allowAPI"> Success <input type="radio" name="test-cowebsite-allowAPI"> Failure <input type="radio" name="test-cowebsite-allowAPI" checked> Pending
</td> </td>
<td> <td>
<a href="#" class="testLink" data-testmap="Metadata/cowebsiteAllowApi.json" target="_blank">Test a iframe opened by a script can use Iframe API</a> <a href="#" class="testLink" data-testmap="Metadata/cowebsiteAllowApi.json" target="_blank">Test a iframe opened by a script can use Iframe API <i>(already automated in E2E tests)</i></a>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -224,7 +224,7 @@
<input type="radio" name="test-custom-menu"> Success <input type="radio" name="test-custom-menu"> Failure <input type="radio" name="test-custom-menu" checked> Pending <input type="radio" name="test-custom-menu"> Success <input type="radio" name="test-custom-menu"> Failure <input type="radio" name="test-custom-menu" checked> Pending
</td> </td>
<td> <td>
<a href="#" class="testLink" data-testmap="Metadata/customMenu.json" target="_blank">Testing add a custom menu by scripting API</a> <a href="#" class="testLink" data-testmap="Metadata/customMenu.json" target="_blank">Testing add a custom menu by scripting API <i>(already automated in E2E tests)</i></a>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -1,4 +1,5 @@
import { z } from "zod"; import { z } from "zod";
import { extendApi } from "@anatine/zod-openapi";
/* /*
* WARNING! The original file is in /messages/JsonMessages. * WARNING! The original file is in /messages/JsonMessages.
@ -6,10 +7,17 @@ import { z } from "zod";
*/ */
export const isAdminApiData = z.object({ export const isAdminApiData = z.object({
userUuid: z.string(), // @ts-ignore
email: z.nullable(z.string()), userUuid: extendApi(z.string(), { example: "998ce839-3dea-4698-8b41-ebbdf7688ad9" }),
roomUrl: z.string(), email: extendApi(z.nullable(z.string()), {
mapUrlStart: z.string(), description: "The email of the current user.",
example: "example@workadventu.re",
}),
roomUrl: extendApi(z.string(), { example: "/@/teamSlug/worldSlug/roomSlug" }),
mapUrlStart: extendApi(z.string(), {
description: "The full URL to the JSON map file",
example: "https://myuser.github.io/myrepo/map.json",
}),
messages: z.optional(z.array(z.unknown())), messages: z.optional(z.array(z.unknown())),
}); });

View File

@ -1,40 +1,129 @@
import { z } from "zod"; import { z } from "zod";
import { extendApi } from "@anatine/zod-openapi";
/* /*
* WARNING! The original file is in /messages/JsonMessages. * WARNING! The original file is in /messages/JsonMessages.
* All other files are automatically copied from this file on container startup / build * All other files are automatically copied from this file on container startup / build
*/ */
export const isErrorApiErrorData = z.object({ export const isErrorApiErrorData = extendApi(
// @ts-ignore // @ts-ignore
type: z.literal("error"), z.object({
code: z.string(), type: z.literal("error"),
title: z.string(), code: extendApi(z.string(), {
subtitle: z.string(), description: "The system code of an error, it must be in SCREAMING_SNAKE_CASE.",
details: z.string(), example: "ROOM_NOT_FOUND",
image: z.string(), }),
}); title: extendApi(z.string(), { description: "Big title displayed on the error screen.", example: "ERROR" }),
subtitle: extendApi(z.string(), {
description: "Subtitle displayed to let the user know what is the main subject of the error.",
example: "The room was not found.",
}),
details: extendApi(z.string(), {
description: "Some others details on what the user can do if he don't understand the error.",
example:
"If you would like more information, you can contact the administrator or us at example@workadventu.re.",
}),
image: extendApi(z.string(), {
description: "The URL of the image displayed just under the logo in the error screen.",
example: "https://example.com/error.png",
}),
}),
{
description:
'This is an error that can be returned by the API, its type must be equal to "error".\n If such an error is caught, an error screen will be displayed.',
}
);
export const isErrorApiRetryData = z.object({ export const isErrorApiRetryData = extendApi(
type: z.literal("retry"), z.object({
code: z.string(), type: z.literal("retry"),
title: z.string(), code: extendApi(z.string(), {
subtitle: z.string(), description:
details: z.string(), "The system code of an error, it must be in SCREAMING_SNAKE_CASE. \n It will not be displayed to the user.",
image: z.string(), example: "WORLD_FULL",
buttonTitle: z.optional(z.nullable(z.string())), }),
timeToRetry: z.number(), title: extendApi(z.string(), { description: "Big title displayed on the error screen.", example: "ERROR" }),
canRetryManual: z.boolean(), subtitle: extendApi(z.string(), {
}); description: "Subtitle displayed to let the user know what is the main subject of the error.",
example: "Too successful, your WorkAdventure world is full!",
}),
details: extendApi(z.string(), {
description: "Some others details on what the user can do if he don't understand the error.",
example: "New automatic attempt in 30 seconds",
}),
image: extendApi(z.string(), {
description: "The URL of the image displayed just under the logo in the waiting screen.",
example: "https://example.com/wait.png",
}),
buttonTitle: extendApi(z.optional(z.nullable(z.string())), {
description:
"If this is not defined the button and the parameter canRetryManual is set to true, the button will be not displayed at all.",
example: "Retry",
}),
timeToRetry: extendApi(z.number(), {
description: "This is the time (in millisecond) between the next auto refresh of the page.",
example: 30_000,
}),
canRetryManual: extendApi(z.boolean(), {
description: "This boolean show or hide the button to let the user refresh manually the current page.",
example: true,
}),
}),
{
description:
'This is an error that can be returned by the API, its type must be equal to "retry".\n' +
"If such an error is caught, a waiting screen will be displayed.",
}
);
export const isErrorApiRedirectData = z.object({ export const isErrorApiRedirectData = extendApi(
type: z.literal("redirect"), z.object({
urlToRedirect: z.string(), type: z.literal("redirect"),
}); urlToRedirect: extendApi(z.string(), {
description: "A URL specified to redirect the user onto it directly",
example: "/contact-us",
}),
}),
{
description:
'This is an error that can be returned by the API, its type must be equal to "redirect".\n' +
"If such an error is caught, the user will be automatically redirected to urlToRedirect.",
}
);
export const isErrorApiUnauthorizedData = z.object({ export const isErrorApiUnauthorizedData = extendApi(
type: z.literal("unauthorized"), z.object({
}); type: z.literal("unauthorized"),
code: extendApi(z.string(), {
description: "This is the system code of an error, it must be in SCREAMING_SNAKE_CASE.",
example: "USER_ACCESS_FORBIDDEN",
}),
title: extendApi(z.string(), { description: "Big title displayed on the error screen.", example: "ERROR" }),
subtitle: extendApi(z.string(), {
description: "Subtitle displayed to let the user know what is the main subject of the error.",
example: "You can't access this place.",
}),
details: extendApi(z.string(), {
description: "Some others details on what the user can do if he don't understand the error.",
example:
"If you would like more information, you can contact the administrator or us at example@workadventu.re.",
}),
image: extendApi(z.string(), {
description: "The URL of the image displayed just under the logo in the error screen.",
example: "https://example.com/error.png",
}),
buttonTitle: extendApi(z.optional(z.nullable(z.string())), {
description: "If this is not defined the button to logout will be not displayed.",
example: "Log out",
}),
}),
{
description:
'This is an error that can be returned by the API, its type must be equal to "unauthorized".\n' +
"If such an error is caught, an error screen will be displayed with a button to let him logout and go to login page.",
}
);
export const isErrorApiData = z.discriminatedUnion("type", [ export const isErrorApiData = z.discriminatedUnion("type", [
isErrorApiErrorData, isErrorApiErrorData,

View File

@ -1,4 +1,5 @@
import { z } from "zod"; import { z } from "zod";
import { extendApi } from "@anatine/zod-openapi";
/* /*
* WARNING! The original file is in /messages/JsonMessages. * WARNING! The original file is in /messages/JsonMessages.
@ -6,20 +7,48 @@ import { z } from "zod";
*/ */
export const isMapDetailsData = z.object({ export const isMapDetailsData = z.object({
mapUrl: z.string(), // @ts-ignore
authenticationMandatory: z.optional(z.nullable(z.boolean())), mapUrl: extendApi(z.string(), {
group: z.nullable(z.string()), description: "The full URL to the JSON map file",
example: "https://myuser.github.io/myrepo/map.json",
}),
authenticationMandatory: extendApi(z.optional(z.nullable(z.boolean())), {
description: "Whether the authentication is mandatory or not for this map",
example: true,
}),
group: extendApi(z.nullable(z.string()), {
description: 'The group this room is part of (maps the notion of "world" in WorkAdventure SAAS)',
example: "myorg/myworld",
}),
contactPage: z.optional(z.nullable(z.string())), contactPage: extendApi(z.optional(z.nullable(z.string())), {
iframeAuthentication: z.optional(z.nullable(z.string())), description: "The URL to the contact page",
example: "https://mycompany.com/contact-us",
}),
iframeAuthentication: extendApi(z.optional(z.nullable(z.string())), {
description: "The URL of the authentication Iframe",
example: "https://mycompany.com/authc",
}),
// The date (in ISO 8601 format) at which the room will expire // The date (in ISO 8601 format) at which the room will expire
expireOn: z.optional(z.string()), expireOn: extendApi(z.optional(z.string()), {
description: "The date (in ISO 8601 format) at which the room will expire",
example: "2022-11-05T08:15:30-05:00",
}),
// Whether the "report" feature is enabled or not on this room // Whether the "report" feature is enabled or not on this room
canReport: z.optional(z.boolean()), canReport: extendApi(z.optional(z.boolean()), {
description: 'Whether the "report" feature is enabled or not on this room',
example: true,
}),
// The URL of the logo image on the loading screen // The URL of the logo image on the loading screen
loadingLogo: z.optional(z.nullable(z.string())), loadingLogo: extendApi(z.optional(z.nullable(z.string())), {
description: "The URL of the image to be used on the loading page",
example: "https://example.com/logo.png",
}),
// The URL of the logo image on "LoginScene" // The URL of the logo image on "LoginScene"
loginSceneLogo: z.optional(z.nullable(z.string())), loginSceneLogo: extendApi(z.optional(z.nullable(z.string())), {
description: "The URL of the image to be used on the LoginScene",
example: "https://example.com/logo_login.png",
}),
}); });
export type MapDetailsData = z.infer<typeof isMapDetailsData>; export type MapDetailsData = z.infer<typeof isMapDetailsData>;

View File

@ -18,8 +18,10 @@
"pretty-check": "yarn prettier --check 'JsonMessages/**/*.ts'" "pretty-check": "yarn prettier --check 'JsonMessages/**/*.ts'"
}, },
"dependencies": { "dependencies": {
"@anatine/zod-openapi": "^1.3.0",
"google-protobuf": "^3.13.0", "google-protobuf": "^3.13.0",
"grpc": "^1.24.4", "grpc": "^1.24.4",
"openapi3-ts": "^2.0.2",
"ts-proto": "^1.96.0", "ts-proto": "^1.96.0",
"zod": "^3.14.3" "zod": "^3.14.3"
}, },

View File

@ -2,6 +2,14 @@
# yarn lockfile v1 # yarn lockfile v1
"@anatine/zod-openapi@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@anatine/zod-openapi/-/zod-openapi-1.3.0.tgz#b5b38c3d821b79674226aa7b327c88c371860d0d"
integrity sha512-l54DypUdDsIq1Uwjv4ib9IBkTXMKZQLUj7qvdFL51EExC5LdSSqOlTOyaVVZZGYgWPKM7ZjGklhdoknLz4EC+w==
dependencies:
ts-deepmerge "^1.1.0"
validator "^13.7.0"
"@babel/code-frame@^7.0.0": "@babel/code-frame@^7.0.0":
version "7.10.4" version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
@ -3241,6 +3249,13 @@ onetime@^5.1.0, onetime@^5.1.2:
dependencies: dependencies:
mimic-fn "^2.1.0" mimic-fn "^2.1.0"
openapi3-ts@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/openapi3-ts/-/openapi3-ts-2.0.2.tgz#a200dd838bf24c9086c8eedcfeb380b7eb31e82a"
integrity sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==
dependencies:
yaml "^1.10.2"
optionator@^0.9.1: optionator@^0.9.1:
version "0.9.1" version "0.9.1"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
@ -4284,6 +4299,11 @@ tough-cookie@~2.5.0:
psl "^1.1.28" psl "^1.1.28"
punycode "^2.1.1" punycode "^2.1.1"
ts-deepmerge@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/ts-deepmerge/-/ts-deepmerge-1.1.0.tgz#4236ae102199affe2e77690dcf198a420160eef2"
integrity sha512-VvwaV/6RyYMwT9d8dClmfHIsG2PCdm6WY430QKOIbPRR50Y/1Q2ilp4i2XEZeHFcNqfaYnAQzpyUC6XA0AqqBg==
ts-poet@^4.5.0: ts-poet@^4.5.0:
version "4.6.1" version "4.6.1"
resolved "https://registry.yarnpkg.com/ts-poet/-/ts-poet-4.6.1.tgz#015dc823d726655af9f095c900f84ed7c60e2dd3" resolved "https://registry.yarnpkg.com/ts-poet/-/ts-poet-4.6.1.tgz#015dc823d726655af9f095c900f84ed7c60e2dd3"
@ -4487,6 +4507,11 @@ validate-npm-package-name@^3.0.0:
dependencies: dependencies:
builtins "^1.0.3" builtins "^1.0.3"
validator@^13.7.0:
version "13.7.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857"
integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==
verror@1.10.0: verror@1.10.0:
version "1.10.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
@ -4593,7 +4618,7 @@ yallist@^3.0.0, yallist@^3.0.3:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
yaml@^1.10.0: yaml@^1.10.0, yaml@^1.10.2:
version "1.10.2" version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==

View File

@ -1,9 +1,11 @@
import { BaseHttpController } from "./BaseHttpController"; import { BaseHttpController } from "./BaseHttpController";
import * as fs from "fs"; import * as fs from "fs";
import { ADMIN_URL } from "../Enum/EnvironmentVariable";
import SwaggerGenerator from "../Services/SwaggerGenerator";
export class SwaggerController extends BaseHttpController { export class SwaggerController extends BaseHttpController {
routes() { routes() {
this.app.get("/openapi", (req, res) => { this.app.get("/openapi/pusher", (req, res) => {
// Let's load the module dynamically (it may not exist in prod because part of the -dev packages) // Let's load the module dynamically (it may not exist in prod because part of the -dev packages)
const swaggerJsdoc = require("swagger-jsdoc"); const swaggerJsdoc = require("swagger-jsdoc");
const options = { const options = {
@ -20,6 +22,43 @@ export class SwaggerController extends BaseHttpController {
res.json(swaggerJsdoc(options)); res.json(swaggerJsdoc(options));
}); });
this.app.get("/openapi/admin", (req, res) => {
// Let's load the module dynamically (it may not exist in prod because part of the -dev packages)
const swaggerJsdoc = require("swagger-jsdoc");
const options = {
swaggerDefinition: {
swagger: "2.0",
//openapi: "3.0.0",
info: {
title: "WorkAdventure Pusher",
version: "1.0.0",
description:
"This is a documentation about the endpoints called by the pusher. \n You can find out more about WorkAdventure on [github](https://github.com/thecodingmachine/workadventure).",
contact: {
email: "hello@workadventu.re",
},
},
host: "pusher." + ADMIN_URL.replace("//", ""),
tags: [
{
name: "AdminAPI",
description: "Access to end points of the admin from the pusher",
},
],
securityDefinitions: {
Bearer: {
type: "apiKey",
name: "Authorization",
in: "header",
},
},
...SwaggerGenerator.definitions(),
},
apis: ["./src/Services/*.ts"],
};
res.json(swaggerJsdoc(options));
});
// Create a LiveDirectory instance to virtualize directory with our assets // Create a LiveDirectory instance to virtualize directory with our assets
// @ts-ignore // @ts-ignore
const LiveDirectory = require("live-directory"); const LiveDirectory = require("live-directory");
@ -39,8 +78,16 @@ export class SwaggerController extends BaseHttpController {
if (err) { if (err) {
return response.status(500).send(err.message); return response.status(500).send(err.message);
} }
const result = data.replace(/https:\/\/petstore\.swagger\.io\/v2\/swagger.json/g, "/openapi");
const urls = [
{ url: "/openapi/pusher", name: "Front -> Pusher" },
{ url: "/openapi/admin", name: "Pusher <- Admin" },
];
const result = data.replace(
/url: "https:\/\/petstore\.swagger\.io\/v2\/swagger.json"/g,
`urls: ${JSON.stringify(urls)}, "urls.primaryName": "Pusher <- Admin"`
);
response.send(result); response.send(result);
return; return;

View File

@ -1,4 +1,5 @@
import { z } from "zod"; import { z } from "zod";
import { extendApi } from "@anatine/zod-openapi";
export const isBanBannedAdminMessageInterface = z.object({ export const isBanBannedAdminMessageInterface = z.object({
type: z.enum(["ban", "banned"]), type: z.enum(["ban", "banned"]),
@ -8,7 +9,7 @@ export const isBanBannedAdminMessageInterface = z.object({
export const isUserMessageAdminMessageInterface = z.object({ export const isUserMessageAdminMessageInterface = z.object({
event: z.enum(["user-message"]), event: z.enum(["user-message"]),
message: isBanBannedAdminMessageInterface, message: extendApi(isBanBannedAdminMessageInterface, { $ref: "#/definitions/BanBannedAdminMessageInterface" }),
world: z.string(), world: z.string(),
jwt: z.string(), jwt: z.string(),
}); });

View File

@ -9,6 +9,7 @@ import qs from "qs";
import { AdminInterface } from "./AdminInterface"; import { AdminInterface } from "./AdminInterface";
import { AuthTokenData, jwtTokenManager } from "./JWTTokenManager"; import { AuthTokenData, jwtTokenManager } from "./JWTTokenManager";
import { InvalidTokenError } from "../Controller/InvalidTokenError"; import { InvalidTokenError } from "../Controller/InvalidTokenError";
import { extendApi } from "@anatine/zod-openapi";
export interface AdminBannedData { export interface AdminBannedData {
is_banned: boolean; is_banned: boolean;
@ -16,15 +17,31 @@ export interface AdminBannedData {
} }
export const isFetchMemberDataByUuidResponse = z.object({ export const isFetchMemberDataByUuidResponse = z.object({
email: z.string(), // @ts-ignore
userUuid: z.string(), email: extendApi(z.string(), {
tags: z.array(z.string()), description: "The email of the fetched user, it can be an email, an uuid or undefined.",
visitCardUrl: z.nullable(z.string()), example: "example@workadventu.re",
textures: z.array(isWokaDetail), }),
messages: z.array(z.unknown()), userUuid: extendApi(z.string(), {
description: "The uuid of the fetched user, it can be an email, an uuid or undefined.",
example: "998ce839-3dea-4698-8b41-ebbdf7688ad9",
}),
tags: extendApi(z.array(z.string()), {
description: "List of tags related to the user fetched.",
example: ["editor"],
}),
visitCardUrl: extendApi(z.nullable(z.string()), {
description: "URL of the visitCard of the user fetched.",
example: "https://mycompany.com/contact/me",
}),
textures: extendApi(z.array(isWokaDetail), { $ref: "#/definitions/WokaDetail" }),
messages: extendApi(z.array(z.unknown()), { description: "List of user's messages." }),
anonymous: z.optional(z.boolean()), anonymous: extendApi(z.optional(z.boolean()), {
userRoomToken: z.optional(z.string()), description: "Whether the user if logged as anonymous or not",
example: false,
}),
userRoomToken: extendApi(z.optional(z.string()), { description: "", example: "" }),
}); });
export type FetchMemberDataByUuidResponse = z.infer<typeof isFetchMemberDataByUuidResponse>; export type FetchMemberDataByUuidResponse = z.infer<typeof isFetchMemberDataByUuidResponse>;
@ -69,6 +86,47 @@ class AdminApi implements AdminInterface {
userId, userId,
}; };
/**
* @openapi
* /api/map:
* get:
* tags: ["AdminAPI"]
* description: Returns a map mapping map name to file name of the map
* security:
* - Bearer: []
* produces:
* - "application/json"
* parameters:
* - name: "playUri"
* in: "query"
* description: "The full URL of WorkAdventure"
* required: true
* type: "string"
* example: "http://play.workadventure.localhost/@/teamSlug/worldSLug/roomSlug"
* - name: "userId"
* in: "query"
* description: "The identifier of the current user \n It can be undefined or an uuid or an email"
* type: "string"
* example: "998ce839-3dea-4698-8b41-ebbdf7688ad9"
* responses:
* 200:
* description: The details of the member
* schema:
* $ref: "#/definitions/MapDetailsData"
* 401:
* description: Error while retrieving the data because you are not authorized
* schema:
* $ref: '#/definitions/ErrorApiRedirectData'
* 403:
* description: Error while retrieving the data because you are not authorized
* schema:
* $ref: '#/definitions/ErrorApiUnauthorizedData'
* 404:
* description: Error while retrieving the data
* schema:
* $ref: '#/definitions/ErrorApiErrorData'
*
*/
const res = await Axios.get<unknown, AxiosResponse<unknown>>(ADMIN_API_URL + "/api/map", { const res = await Axios.get<unknown, AxiosResponse<unknown>>(ADMIN_API_URL + "/api/map", {
headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" }, headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" },
params, params,
@ -99,6 +157,58 @@ class AdminApi implements AdminInterface {
characterLayers: string[], characterLayers: string[],
locale?: string locale?: string
): Promise<FetchMemberDataByUuidResponse> { ): Promise<FetchMemberDataByUuidResponse> {
/**
* @openapi
* /api/room/access:
* get:
* tags: ["AdminAPI"]
* description: Returns member's informations if he can access this room
* security:
* - Bearer: []
* produces:
* - "application/json"
* parameters:
* - name: "userIdentifier"
* in: "query"
* description: "The identifier of the current user \n It can be undefined or an uuid or an email"
* type: "string"
* example: "998ce839-3dea-4698-8b41-ebbdf7688ad9"
* - name: "playUri"
* in: "query"
* description: "The full URL of WorkAdventure"
* required: true
* type: "string"
* example: "http://play.workadventure.localhost/@/teamSlug/worldSLug/roomSlug"
* - name: "ipAddress"
* in: "query"
* required: true
* type: "string"
* example: "127.0.0.1"
* - name: "characterLayers"
* in: "query"
* type: "array"
* items:
* type: string
* example: ["male1"]
* responses:
* 200:
* description: The details of the member
* schema:
* $ref: "#/definitions/FetchMemberDataByUuidResponse"
* 401:
* description: Error while retrieving the data because you are not authorized
* schema:
* $ref: '#/definitions/ErrorApiRedirectData'
* 403:
* description: Error while retrieving the data because you are not authorized
* schema:
* $ref: '#/definitions/ErrorApiUnauthorizedData'
* 404:
* description: Error while retrieving the data
* schema:
* $ref: '#/definitions/ErrorApiErrorData'
*
*/
const res = await Axios.get<unknown, AxiosResponse<unknown>>(ADMIN_API_URL + "/api/room/access", { const res = await Axios.get<unknown, AxiosResponse<unknown>>(ADMIN_API_URL + "/api/room/access", {
params: { params: {
userIdentifier, userIdentifier,
@ -130,6 +240,42 @@ class AdminApi implements AdminInterface {
playUri: string | null, playUri: string | null,
locale?: string locale?: string
): Promise<AdminApiData> { ): Promise<AdminApiData> {
/**
* @openapi
* /api/login-url/{organizationMemberToken}:
* get:
* tags: ["AdminAPI"]
* description: Returns a member from the token
* security:
* - Bearer: []
* produces:
* - "application/json"
* parameters:
* - name: "organizationMemberToken"
* in: "path"
* description: "The token of member in the organization"
* type: "string"
* - name: "playUri"
* in: "query"
* description: "The full URL of WorkAdventure"
* required: true
* type: "string"
* example: "http://play.workadventure.localhost/@/teamSlug/worldSLug/roomSlug"
* responses:
* 200:
* description: The details of the member
* schema:
* $ref: "#/definitions/AdminApiData"
* 401:
* description: Error while retrieving the data because you are not authorized
* schema:
* $ref: '#/definitions/ErrorApiRedirectData'
* 404:
* description: Error while retrieving the data
* schema:
* $ref: '#/definitions/ErrorApiErrorData'
*
*/
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
const res = await Axios.get(ADMIN_API_URL + "/api/login-url/" + organizationMemberToken, { const res = await Axios.get(ADMIN_API_URL + "/api/login-url/" + organizationMemberToken, {
params: { playUri }, params: { playUri },
@ -154,6 +300,41 @@ class AdminApi implements AdminInterface {
reportWorldSlug: string, reportWorldSlug: string,
locale?: string locale?: string
) { ) {
/**
* @openapi
* /api/report:
* post:
* tags: ["AdminAPI"]
* description: Report one user with a comment
* security:
* - Bearer: []
* produces:
* - "application/json"
* parameters:
* - name: "reportedUserUuid"
* in: "query"
* description: "The identifier of the reported user \n It can be an uuid or an email"
* type: "string"
* example: "998ce839-3dea-4698-8b41-ebbdf7688ad9"
* - name: "reportedUserComment"
* in: "query"
* description: "The comment of the report"
* required: true
* type: "string"
* - name: "reporterUserUuid"
* in: "query"
* description: "The identifier of the reporter user \n It can be an uuid or an email"
* type: "string"
* example: "998ce839-3dea-4698-8b41-ebbdf7688ad8"
* - name: "reportWorldSlug"
* in: "query"
* description: "The slug of the world where the report is made"
* type: "string"
* example: "/@/teamSlug/worldSlug/roomSlug"
* responses:
* 200:
* description: The report has been successfully saved
*/
return Axios.post( return Axios.post(
`${ADMIN_API_URL}/api/report`, `${ADMIN_API_URL}/api/report`,
{ {
@ -174,6 +355,53 @@ class AdminApi implements AdminInterface {
roomUrl: string, roomUrl: string,
locale?: string locale?: string
): Promise<AdminBannedData> { ): Promise<AdminBannedData> {
/**
* @openapi
* /api/ban:
* get:
* tags: ["AdminAPI"]
* description: Check if user is banned or not
* security:
* - Bearer: []
* produces:
* - "application/json"
* parameters:
* - name: "ipAddress"
* in: "query"
* type: "string"
* required: true
* example: "127.0.0.1"
* - name: "token"
* in: "query"
* description: "The uuid of the user \n It can be an uuid or an email"
* type: "string"
* required: true
* example: "998ce839-3dea-4698-8b41-ebbdf7688ad8"
* - name: "roomUrl"
* in: "query"
* description: "The slug of the world where to check if the user is banned"
* type: "string"
* required: true
* example: "/@/teamSlug/worldSlug/roomSlug"
* responses:
* 200:
* description: The user is banned or not
* content:
* application/json:
* schema:
* type: array
* required:
* - is_banned
* properties:
* is_banned:
* type: boolean
* description: Whether the user is banned or not
* example: true
* 404:
* description: Error while retrieving the data
* schema:
* $ref: '#/definitions/ErrorApiErrorData'
*/
//todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case.
return Axios.get( return Axios.get(
ADMIN_API_URL + ADMIN_API_URL +
@ -191,6 +419,37 @@ class AdminApi implements AdminInterface {
} }
async getUrlRoomsFromSameWorld(roomUrl: string, locale?: string): Promise<string[]> { async getUrlRoomsFromSameWorld(roomUrl: string, locale?: string): Promise<string[]> {
/**
* @openapi
* /api/room/sameWorld:
* get:
* tags: ["AdminAPI"]
* description: Get all URLs of the rooms from the world specified
* security:
* - Bearer: []
* produces:
* - "application/json"
* parameters:
* - name: "roomUrl"
* in: "query"
* description: "The slug of the room"
* type: "string"
* required: true
* example: "/@/teamSlug/worldSlug/roomSlug"
* responses:
* 200:
* description: The list of URL of the rooms from the same world
* schema:
* type: array
* items:
* type: string
* description: URL of a room
* example: "http://example.com/@/teamSlug/worldSlug/room2Slug"
* 404:
* description: Error while retrieving the data
* schema:
* $ref: '#/definitions/ErrorApiErrorData'
*/
return Axios.get(ADMIN_API_URL + "/api/room/sameWorld" + "?roomUrl=" + encodeURIComponent(roomUrl), { return Axios.get(ADMIN_API_URL + "/api/room/sameWorld" + "?roomUrl=" + encodeURIComponent(roomUrl), {
headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" }, headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" },
}).then((data) => { }).then((data) => {
@ -204,10 +463,6 @@ class AdminApi implements AdminInterface {
} }
return `${OPID_PROFILE_SCREEN_PROVIDER}?accessToken=${accessToken}`; return `${OPID_PROFILE_SCREEN_PROVIDER}?accessToken=${accessToken}`;
} }
async logoutOauth(token: string): Promise<void> {
await Axios.get(ADMIN_API_URL + `/oauth/logout?token=${token}`);
}
} }
export const adminApi = new AdminApi(); export const adminApi = new AdminApi();

View File

@ -74,9 +74,4 @@ export interface AdminInterface {
* @return string * @return string
*/ */
getProfileUrl(accessToken: string): string; getProfileUrl(accessToken: string): string;
/**
* @param token
*/
logoutOauth(token: string): Promise<void>;
} }

View File

@ -87,10 +87,6 @@ class LocalAdmin implements AdminInterface {
new Error("No admin backoffice set!"); new Error("No admin backoffice set!");
return ""; return "";
} }
async logoutOauth(token: string): Promise<void> {
return Promise.reject(new Error("No admin backoffice set!"));
}
} }
export const localAdmin = new LocalAdmin(); export const localAdmin = new LocalAdmin();

View File

@ -658,7 +658,7 @@ export class SocketManager implements ZoneEventListener {
public emitErrorScreenMessage(client: compressors.WebSocket, errorApi: ErrorApiData) { public emitErrorScreenMessage(client: compressors.WebSocket, errorApi: ErrorApiData) {
const errorMessage = new ErrorScreenMessage(); const errorMessage = new ErrorScreenMessage();
errorMessage.setType(errorApi.type); errorMessage.setType(errorApi.type);
if (errorApi.type == "retry" || errorApi.type == "error") { if (errorApi.type == "retry" || errorApi.type == "error" || errorApi.type == "unauthorized") {
errorMessage.setCode(new StringValue().setValue(errorApi.code)); errorMessage.setCode(new StringValue().setValue(errorApi.code));
errorMessage.setTitle(new StringValue().setValue(errorApi.title)); errorMessage.setTitle(new StringValue().setValue(errorApi.title));
errorMessage.setSubtitle(new StringValue().setValue(errorApi.subtitle)); errorMessage.setSubtitle(new StringValue().setValue(errorApi.subtitle));

View File

@ -0,0 +1,35 @@
import { generateSchema } from "@anatine/zod-openapi";
import { isAdminApiData } from "../Messages/JsonMessages/AdminApiData";
import {
isErrorApiErrorData,
isErrorApiRedirectData,
isErrorApiRetryData,
isErrorApiUnauthorizedData,
} from "../Messages/JsonMessages/ErrorApiData";
import { isMapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
import { isFetchMemberDataByUuidResponse } from "./AdminApi";
import { isWokaDetail } from "../Messages/JsonMessages/PlayerTextures";
class SwaggerGenerator {
definitions() {
return {
definitions: {
AdminApiData: generateSchema(isAdminApiData),
//BanBannedAdminMessageInterface: generateSchema(isBanBannedAdminMessageInterface),
ErrorApiErrorData: generateSchema(isErrorApiErrorData),
ErrorApiRedirectData: generateSchema(isErrorApiRedirectData),
ErrorApiRetryData: generateSchema(isErrorApiRetryData),
ErrorApiUnauthorizedData: generateSchema(isErrorApiUnauthorizedData),
FetchMemberDataByUuidResponse: generateSchema(isFetchMemberDataByUuidResponse),
//ListenRoomsMessageInterface: generateSchema(isListenRoomsMessageInterface),
MapDetailsData: generateSchema(isMapDetailsData),
//RegisterData: generateSchema(isRegisterData),
//RoomRedirect: generateSchema(isRoomRedirect),
//UserMessageAdminMessageInterface: generateSchema(isUserMessageAdminMessageInterface),
WokaDetail: generateSchema(isWokaDetail),
},
};
}
}
export default new SwaggerGenerator();

View File

@ -0,0 +1,38 @@
import { expect, test } from '@playwright/test';
import { login } from './utils/roles';
test.describe('Iframe API', () => {
test('can be called from an iframe loading a script', async ({
page,
}) => {
await page.goto(
'http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Metadata/cowebsiteAllowApi.json'
);
await login(page);
await expect(page.locator('p.other-text')).toHaveText('The iframe opened by a script works !', {useInnerText: true});
});
test('can add a custom menu by scripting API', async ({
page,
}) => {
await page.goto(
'http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Metadata/customMenu.json'
);
await login(page);
await page.click('.menuIcon img:first-child');
await page.click('button:has-text("custom iframe menu")');
const iframeParagraph = page
.frameLocator('.menu-submenu-container iframe')
.locator('p');
await expect(iframeParagraph).toHaveText('This is an iframe in a custom menu.');
await page.click('button:has-text("custom callback menu")');
await expect(page.locator('p.other-text')).toHaveText('Custom menu clicked', {useInnerText: true});
});
});