Merge branch 'develop' of github.com:thecodingmachine/workadventure into develop
This commit is contained in:
commit
47e5e360df
@ -40,6 +40,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
||||
"dependencies": {
|
||||
"@anatine/zod-openapi": "^1.3.0",
|
||||
"@workadventure/tiled-map-type-guard": "^1.0.3",
|
||||
"axios": "^0.21.2",
|
||||
"busboy": "^0.3.1",
|
||||
@ -50,6 +51,7 @@
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mkdirp": "^1.0.4",
|
||||
"openapi3-ts": "^2.0.2",
|
||||
"prom-client": "^12.0.0",
|
||||
"query-string": "^6.13.3",
|
||||
"redis": "^3.1.2",
|
||||
|
@ -520,7 +520,16 @@ export class GameRoom {
|
||||
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++;
|
||||
return this.versionNumber;
|
||||
}
|
||||
|
@ -150,6 +150,9 @@ export class User implements Movable {
|
||||
if (this.outlineColor !== undefined) {
|
||||
playerDetails.setOutlinecolor(new UInt32Value().setValue(this.outlineColor));
|
||||
}
|
||||
if (details.getRemoveoutlinecolor()) {
|
||||
playerDetails.setRemoveoutlinecolor(new BoolValue().setValue(true));
|
||||
}
|
||||
if (this.voiceIndicatorShown !== undefined) {
|
||||
playerDetails.setShowvoiceindicator(new BoolValue().setValue(this.voiceIndicatorShown));
|
||||
}
|
||||
|
@ -852,14 +852,14 @@ export class SocketManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const versionNumber = room.incrementVersion();
|
||||
const versionNumber = await room.incrementVersion();
|
||||
room.getUsers().forEach((recipient) => {
|
||||
const worldFullMessage = new RefreshRoomMessage();
|
||||
worldFullMessage.setRoomid(roomId);
|
||||
worldFullMessage.setVersionnumber(versionNumber);
|
||||
const refreshRoomMessage = new RefreshRoomMessage();
|
||||
refreshRoomMessage.setRoomid(roomId);
|
||||
refreshRoomMessage.setVersionnumber(versionNumber);
|
||||
|
||||
const clientMessage = new ServerToClientMessage();
|
||||
clientMessage.setRefreshroommessage(worldFullMessage);
|
||||
clientMessage.setRefreshroommessage(refreshRoomMessage);
|
||||
|
||||
recipient.socket.write(clientMessage);
|
||||
});
|
||||
|
@ -2,6 +2,14 @@
|
||||
# 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":
|
||||
version "7.16.0"
|
||||
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:
|
||||
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:
|
||||
version "0.9.1"
|
||||
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"
|
||||
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:
|
||||
version "1.1.8"
|
||||
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"
|
||||
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:
|
||||
version "3.0.1"
|
||||
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"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
|
||||
yaml@^1.10.0:
|
||||
yaml@^1.10.0, yaml@^1.10.2:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||
|
@ -166,7 +166,8 @@ return [
|
||||
],
|
||||
[
|
||||
'title' => 'Troubleshooting',
|
||||
'url' => '/map-building/troubleshooting',
|
||||
'view' => 'content.map.troubleshooting'
|
||||
'url' => '/map-building/troubleshooting.md',
|
||||
'markdown' => 'maps.troubleshooting',
|
||||
'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/troubleshooting.md',
|
||||
],
|
||||
];
|
||||
|
@ -18,11 +18,18 @@ In order to create a zone that opens websites:
|
||||
|
||||
{.alert.alert-warning}
|
||||
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}
|
||||
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
|
||||
|
||||
|
94
docs/maps/troubleshooting.md
Normal file
94
docs/maps/troubleshooting.md
Normal 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>
|
@ -1,5 +1,3 @@
|
||||
src/Messages/generated
|
||||
src/Messages/JsonMessages
|
||||
src/i18n/i18n-svelte.ts
|
||||
src/i18n/i18n-types.ts
|
||||
src/i18n/i18n-util.ts
|
||||
src/i18n/i18n-*.ts
|
||||
|
@ -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",
|
||||
"adapter": "svelte"
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"@tsconfig/svelte": "^1.0.10",
|
||||
"@types/google-protobuf": "^3.7.3",
|
||||
@ -34,6 +34,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@16bits/nes.css": "^2.3.2",
|
||||
"@anatine/zod-openapi": "^1.3.0",
|
||||
"@fontsource/press-start-2p": "^4.3.0",
|
||||
"@joeattardi/emoji-button": "^4.6.2",
|
||||
"@types/simple-peer": "^9.11.1",
|
||||
@ -47,6 +48,7 @@
|
||||
"easystarjs": "^0.4.4",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"google-protobuf": "^3.13.0",
|
||||
"openapi3-ts": "^2.0.2",
|
||||
"phaser": "3.55.1",
|
||||
"phaser-animated-tiles": "workadventure/phaser-animated-tiles#da68bbededd605925621dd4f03bd27e69284b254",
|
||||
"phaser3-rex-plugins": "^1.1.42",
|
||||
@ -61,7 +63,7 @@
|
||||
"standardized-audio-context": "^25.2.4",
|
||||
"ts-deferred": "^1.0.4",
|
||||
"ts-proto": "^1.96.0",
|
||||
"typesafe-i18n": "^2.59.0",
|
||||
"typesafe-i18n": "^5.4.0",
|
||||
"uuidv4": "^6.2.10",
|
||||
"zod": "^3.14.3"
|
||||
},
|
||||
|
@ -17,7 +17,7 @@ class AnalyticsClient {
|
||||
}
|
||||
}
|
||||
|
||||
identifyUser(uuid: string, email: string | null) {
|
||||
identifyUser(uuid: string, email: string | null): void {
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.identify(uuid, { uuid, email, wa: true });
|
||||
@ -25,7 +25,7 @@ class AnalyticsClient {
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
loggedWithSso() {
|
||||
loggedWithSso(): void {
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("wa-logged-sso");
|
||||
@ -33,7 +33,7 @@ class AnalyticsClient {
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
loggedWithToken() {
|
||||
loggedWithToken(): void {
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("wa-logged-token");
|
||||
@ -41,7 +41,7 @@ class AnalyticsClient {
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
enteredRoom(roomId: string, roomGroup: string | null) {
|
||||
enteredRoom(roomId: string, roomGroup: string | null): void {
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("$pageView", { roomId, roomGroup });
|
||||
@ -50,7 +50,7 @@ class AnalyticsClient {
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
openedMenu() {
|
||||
openedMenu(): void {
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("wa-opened-menu");
|
||||
@ -58,7 +58,7 @@ class AnalyticsClient {
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
launchEmote(emote: string) {
|
||||
launchEmote(emote: string): void {
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("wa-emote-launch", { emote });
|
||||
@ -66,7 +66,7 @@ class AnalyticsClient {
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
enteredJitsi(roomName: string, roomId: string) {
|
||||
enteredJitsi(roomName: string, roomId: string): void {
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("wa-entered-jitsi", { roomName, roomId });
|
||||
@ -74,7 +74,7 @@ class AnalyticsClient {
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
validationName() {
|
||||
validationName(): void {
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("wa-name-validation");
|
||||
@ -82,7 +82,7 @@ class AnalyticsClient {
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
validationWoka(scene: string) {
|
||||
validationWoka(scene: string): void {
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("wa-woka-validation", { scene });
|
||||
@ -90,12 +90,309 @@ class AnalyticsClient {
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
validationVideo() {
|
||||
validationVideo(): void {
|
||||
this.posthogPromise
|
||||
?.then((posthog) => {
|
||||
posthog.capture("wa-video-validation");
|
||||
})
|
||||
.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();
|
||||
|
@ -242,7 +242,7 @@ class IframeListener {
|
||||
} else if (iframeEvent.type === "cameraFollowPlayer") {
|
||||
this._cameraFollowPlayerStream.next(iframeEvent.data);
|
||||
} else if (iframeEvent.type === "chat") {
|
||||
scriptUtils.sendAnonymousChat(iframeEvent.data);
|
||||
scriptUtils.sendAnonymousChat(iframeEvent.data, iframe.contentWindow ?? undefined);
|
||||
} else if (iframeEvent.type === "openPopup") {
|
||||
this._openPopupStream.next(iframeEvent.data);
|
||||
} else if (iframeEvent.type === "closePopup") {
|
||||
@ -404,13 +404,20 @@ class IframeListener {
|
||||
this.scripts.delete(scriptUrl);
|
||||
}
|
||||
|
||||
sendUserInputChat(message: string) {
|
||||
this.postMessage({
|
||||
type: "userInputChat",
|
||||
data: {
|
||||
message: message,
|
||||
} as UserInputChatEvent,
|
||||
});
|
||||
/**
|
||||
* @param message The message to dispatch
|
||||
* @param exceptOrigin Don't dispatch the message to exceptOrigin (to avoid infinite loops)
|
||||
*/
|
||||
sendUserInputChat(message: string, exceptOrigin?: Window) {
|
||||
this.postMessage(
|
||||
{
|
||||
type: "userInputChat",
|
||||
data: {
|
||||
message: message,
|
||||
} as UserInputChatEvent,
|
||||
},
|
||||
exceptOrigin
|
||||
);
|
||||
}
|
||||
|
||||
sendEnterEvent(name: string) {
|
||||
@ -528,8 +535,11 @@ class IframeListener {
|
||||
/**
|
||||
* 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) {
|
||||
if (exceptOrigin === iframe.contentWindow) {
|
||||
continue;
|
||||
}
|
||||
iframe.contentWindow?.postMessage(message, "*");
|
||||
}
|
||||
}
|
||||
|
@ -11,9 +11,9 @@ class ScriptUtils {
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
public sendAnonymousChat(chatEvent: ChatEvent) {
|
||||
public sendAnonymousChat(chatEvent: ChatEvent, origin?: Window) {
|
||||
const userId = playersStore.addFacticePlayer(chatEvent.author);
|
||||
chatMessagesStore.addExternalMessage(userId, chatEvent.message);
|
||||
chatMessagesStore.addExternalMessage(userId, chatEvent.message, origin);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
import { followRoleStore, followStateStore, followUsersStore } from "../Stores/FollowStore";
|
||||
import { gameManager } from "../Phaser/Game/GameManager";
|
||||
import { currentPlayerGroupLockStateStore } from "../Stores/CurrentPlayerGroupStore";
|
||||
import { analyticsClient } from "../Administration/AnalyticsClient";
|
||||
|
||||
const gameScene = gameManager.getCurrentGameScene();
|
||||
|
||||
@ -89,6 +90,7 @@
|
||||
class="btn-follow nes-btn is-dark"
|
||||
class:hide={($peerStore.size === 0 && $followStateStore === "off") || $silentStore}
|
||||
class:disabled={$followStateStore !== "off"}
|
||||
on:click={() => analyticsClient.follow()}
|
||||
on:click={followClick}
|
||||
>
|
||||
<img class="noselect" src={followImg} alt="" />
|
||||
@ -98,6 +100,7 @@
|
||||
class="btn-lock nes-btn is-dark"
|
||||
class:hide={$peerStore.size === 0 || $silentStore}
|
||||
class:disabled={$currentPlayerGroupLockStateStore}
|
||||
on:click={() => analyticsClient.lockDiscussion()}
|
||||
on:click={lockClick}
|
||||
>
|
||||
<img class="noselect" src={lockImg} alt="" />
|
||||
@ -105,6 +108,7 @@
|
||||
|
||||
<div
|
||||
class="btn-monitor nes-btn is-dark"
|
||||
on:click={() => analyticsClient.screenSharing()}
|
||||
on:click={screenSharingClick}
|
||||
class:hide={!$screenSharingAvailableStore || $silentStore}
|
||||
class:enabled={$requestedScreenSharingState}
|
||||
@ -116,7 +120,12 @@
|
||||
{/if}
|
||||
</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}
|
||||
<img class="noselect" src={cinemaImg} alt="Turn on webcam" />
|
||||
{:else}
|
||||
@ -124,7 +133,12 @@
|
||||
{/if}
|
||||
</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}
|
||||
<img class="noselect" src={microphoneImg} alt="Turn on microphone" />
|
||||
{:else}
|
||||
|
@ -12,6 +12,7 @@
|
||||
import { i18nJson } from "../../i18n/locales";
|
||||
|
||||
import uploadFile from "../images/jitsi.png";
|
||||
import { analyticsClient } from "../../Administration/AnalyticsClient";
|
||||
|
||||
export let index: number;
|
||||
export let coWebsite: CoWebsite;
|
||||
@ -103,6 +104,7 @@
|
||||
class:ready={$state === "ready"}
|
||||
class:displayed={isMain || isHighlight}
|
||||
class:vertical
|
||||
on:click={() => analyticsClient.stackOpenCloseMultiIframe()}
|
||||
on:click={onClick}
|
||||
>
|
||||
<div class="cowebsite-thumnail-container">
|
||||
|
@ -19,6 +19,5 @@
|
||||
padding-top: 2%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 200;
|
||||
}
|
||||
</style>
|
||||
|
@ -67,10 +67,11 @@
|
||||
{/key}
|
||||
{:else if $highlightedEmbedScreen.type === "cowebsite"}
|
||||
{#key $highlightedEmbedScreen.embed.getId()}
|
||||
<div
|
||||
id={"cowebsite-slot-" + $highlightedEmbedScreen.embed.getId()}
|
||||
class="highlighted-cowebsite nes-container is-rounded"
|
||||
>
|
||||
<div class="highlighted-cowebsite-container nes-container is-rounded">
|
||||
<div
|
||||
id={"cowebsite-slot-" + $highlightedEmbedScreen.embed.getId()}
|
||||
class="highlighted-cowebsite"
|
||||
/>
|
||||
<div class="actions">
|
||||
<button type="button" class="nes-btn is-error close" on:click={closeCoWebsite}
|
||||
>×</button
|
||||
@ -120,20 +121,29 @@
|
||||
|
||||
.highlighted-cowebsite {
|
||||
height: 100% !important;
|
||||
width: 96%;
|
||||
background-color: rgba(#000000, 0.6);
|
||||
margin: 0 !important;
|
||||
width: 100% !important;
|
||||
position: relative;
|
||||
z-index: 200;
|
||||
|
||||
.actions {
|
||||
z-index: 200;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: end;
|
||||
gap: 2%;
|
||||
&-container {
|
||||
height: 100% !important;
|
||||
width: 96%;
|
||||
background-color: rgba(#000000, 0.6);
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
.actions {
|
||||
z-index: 202;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: end;
|
||||
gap: 2%;
|
||||
|
||||
button {
|
||||
pointer-events: all;
|
||||
button {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,11 +53,14 @@
|
||||
background: #eceeee;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 4%;
|
||||
max-height: 80vh;
|
||||
max-width: 80vw;
|
||||
margin-left: 10%;
|
||||
margin-right: 10%;
|
||||
z-index: 600;
|
||||
overflow: auto;
|
||||
text-align: center;
|
||||
|
@ -68,6 +68,11 @@
|
||||
</aside>
|
||||
|
||||
<section id="main-layout-main">
|
||||
<Lazy
|
||||
when={$showDesktopCapturerSourcePicker}
|
||||
component={() => import("./Video/DesktopCapturerSourcePicker.svelte")}
|
||||
/>
|
||||
|
||||
{#if $menuVisiblilityStore}
|
||||
<Menu />
|
||||
{/if}
|
||||
@ -120,11 +125,6 @@
|
||||
|
||||
<Lazy when={$emoteMenuStore} component={() => import("./EmoteMenu/EmoteMenu.svelte")} />
|
||||
|
||||
<Lazy
|
||||
when={$showDesktopCapturerSourcePicker}
|
||||
component={() => import("./Video/DesktopCapturerSourcePicker.svelte")}
|
||||
/>
|
||||
|
||||
{#if hasEmbedScreen}
|
||||
<EmbedScreensContainer />
|
||||
{/if}
|
||||
|
@ -2,6 +2,7 @@
|
||||
import LL from "../../i18n/i18n-svelte";
|
||||
import { gameManager } from "../../Phaser/Game/GameManager";
|
||||
import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore";
|
||||
import { analyticsClient } from "../../Administration/AnalyticsClient";
|
||||
|
||||
let entryPoint: string = $startLayerNamesStore[0];
|
||||
let walkAutomatically: boolean = false;
|
||||
@ -54,16 +55,26 @@
|
||||
class="link-url nes-input is-dark"
|
||||
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>
|
||||
{:else}
|
||||
<section class="is-mobile">
|
||||
<h3>{$LL.menu.invite.description()}</h3>
|
||||
<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>
|
||||
{/if}
|
||||
<h3>Select an entry point</h3>
|
||||
<h3>{$LL.menu.invite.selectEntryPoint()}</h3>
|
||||
<section class="nes-select is-dark starting-points">
|
||||
<select
|
||||
bind:value={entryPoint}
|
||||
@ -81,11 +92,12 @@
|
||||
type="checkbox"
|
||||
class="nes-checkbox is-dark"
|
||||
bind:checked={walkAutomatically}
|
||||
on:change={(e) => analyticsClient.inviteCopyLinkWalk(e.currentTarget.value)}
|
||||
on:change={() => {
|
||||
updateInputFieldValue();
|
||||
}}
|
||||
/>
|
||||
<span>{$LL.menu.invite.walk_automatically_to_position()}</span>
|
||||
<span>{$LL.menu.invite.walkAutomaticallyToPosition()}</span>
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
|
@ -19,6 +19,7 @@
|
||||
import type { Unsubscriber } from "svelte/store";
|
||||
import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem";
|
||||
import LL from "../../i18n/i18n-svelte";
|
||||
import { analyticsClient } from "../../Administration/AnalyticsClient";
|
||||
|
||||
let activeSubMenu: MenuItem = $subMenusStore[0];
|
||||
let activeComponent: typeof ProfileSubMenu | typeof CustomSubMenu = ProfileSubMenu;
|
||||
@ -48,24 +49,30 @@
|
||||
activeSubMenu = menu;
|
||||
switch (menu.key) {
|
||||
case SubMenusInterface.settings:
|
||||
analyticsClient.menuSetting();
|
||||
activeComponent = SettingsSubMenu;
|
||||
break;
|
||||
case SubMenusInterface.profile:
|
||||
analyticsClient.menuProfile();
|
||||
activeComponent = ProfileSubMenu;
|
||||
break;
|
||||
case SubMenusInterface.worlds:
|
||||
activeComponent = WorldsSubMenu;
|
||||
break;
|
||||
case SubMenusInterface.invite:
|
||||
analyticsClient.menuInvite();
|
||||
activeComponent = GuestSubMenu;
|
||||
break;
|
||||
case SubMenusInterface.aboutRoom:
|
||||
analyticsClient.menuCredit();
|
||||
activeComponent = AboutRoomSubMenu;
|
||||
break;
|
||||
case SubMenusInterface.globalMessages:
|
||||
analyticsClient.globalMessage();
|
||||
activeComponent = (await import("./GlobalMessagesSubMenu.svelte")).default;
|
||||
break;
|
||||
case SubMenusInterface.contact:
|
||||
analyticsClient.menuContact();
|
||||
activeComponent = ContactSubMenu;
|
||||
break;
|
||||
}
|
||||
@ -92,16 +99,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
function translateMenuName(menu: MenuItem) {
|
||||
if (menu.type === "scripting") {
|
||||
return menu.label;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
$: subMenuTranslations = $subMenusStore.map((subMenu) =>
|
||||
subMenu.type === "scripting" ? subMenu.label : $LL.menu.sub[subMenu.key]()
|
||||
);
|
||||
$: activeSubMenuTranslation =
|
||||
activeSubMenu.type === "scripting" ? activeSubMenu.label : $LL.menu.sub[activeSubMenu.key]();
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
@ -110,20 +112,20 @@
|
||||
<div class="menu-nav-sidebar nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}>
|
||||
<h2>{$LL.menu.title()}</h2>
|
||||
<nav>
|
||||
{#each $subMenusStore as submenu}
|
||||
{#each $subMenusStore as submenu, i}
|
||||
<button
|
||||
type="button"
|
||||
class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}"
|
||||
on:click|preventDefault={() => void switchMenu(submenu)}
|
||||
>
|
||||
{translateMenuName(submenu)}
|
||||
{subMenuTranslations[i]}
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
<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}>×</button>
|
||||
<h2>{translateMenuName(activeSubMenu)}</h2>
|
||||
<h2>{activeSubMenuTranslation}</h2>
|
||||
<svelte:component this={activeComponent} {...props} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -10,6 +10,7 @@
|
||||
import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
|
||||
import { showShareLinkMapModalStore } from "../../Stores/ModalStore";
|
||||
import LL from "../../i18n/i18n-svelte";
|
||||
import { analyticsClient } from "../../Administration/AnalyticsClient";
|
||||
|
||||
function showMenu() {
|
||||
menuVisiblilityStore.set(!get(menuVisiblilityStore));
|
||||
@ -43,7 +44,8 @@
|
||||
class="nes-pointer"
|
||||
draggable="false"
|
||||
on:dragstart|preventDefault={noDrag}
|
||||
on:click|preventDefault={showInvite}
|
||||
on:click={() => analyticsClient.openInvite()}
|
||||
on:click={showInvite}
|
||||
/>
|
||||
</span>
|
||||
<span class="nes-btn is-dark">
|
||||
@ -53,7 +55,8 @@
|
||||
class="nes-pointer"
|
||||
draggable="false"
|
||||
on:dragstart|preventDefault={noDrag}
|
||||
on:click|preventDefault={register}
|
||||
on:click={() => analyticsClient.openRegister()}
|
||||
on:click={register}
|
||||
/>
|
||||
</span>
|
||||
{:else}
|
||||
@ -64,11 +67,11 @@
|
||||
class="nes-pointer"
|
||||
draggable="false"
|
||||
on:dragstart|preventDefault={noDrag}
|
||||
on:click|preventDefault={showMenu}
|
||||
on:click={() => analyticsClient.openedMenu()}
|
||||
on:click={showMenu}
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<span class="nes-btn is-dark">
|
||||
<img
|
||||
src={logoTalk}
|
||||
@ -76,7 +79,8 @@
|
||||
class="nes-pointer"
|
||||
draggable="false"
|
||||
on:dragstart|preventDefault={noDrag}
|
||||
on:click|preventDefault={showChat}
|
||||
on:click={() => analyticsClient.openedMenu()}
|
||||
on:click={showChat}
|
||||
/>
|
||||
</span>
|
||||
</main>
|
||||
|
@ -21,6 +21,7 @@
|
||||
import Woka from "../Woka/Woka.svelte";
|
||||
import Companion from "../Companion/Companion.svelte";
|
||||
import LL from "../../i18n/i18n-svelte";
|
||||
import { analyticsClient } from "../../Administration/AnalyticsClient";
|
||||
|
||||
function disableMenuStores() {
|
||||
menuVisiblilityStore.set(false);
|
||||
@ -54,21 +55,41 @@
|
||||
<div class="customize-main">
|
||||
<div class="submenu">
|
||||
<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()} />
|
||||
<span class="btn-hover">{$LL.menu.profile.edit.name()}</span>
|
||||
</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" />
|
||||
<span class="btn-hover">{$LL.menu.profile.edit.woka()}</span>
|
||||
</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" />
|
||||
<span class="btn-hover">{$LL.menu.profile.edit.companion()}</span>
|
||||
</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()} />
|
||||
<span class="btn-hover">{$LL.menu.profile.edit.camera()}</span>
|
||||
</button>
|
||||
@ -83,13 +104,15 @@
|
||||
{/if}
|
||||
</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
|
||||
>
|
||||
</section>
|
||||
{:else}
|
||||
<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>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -10,6 +10,7 @@
|
||||
import { audioManagerVolumeStore } from "../../Stores/AudioManagerStore";
|
||||
|
||||
import infoImg from "../images/info.svg";
|
||||
import { analyticsClient } from "../../Administration/AnalyticsClient";
|
||||
|
||||
let fullscreen: boolean = localUserStore.getFullscreen();
|
||||
let notification: boolean = localUserStore.getNotification() === "granted";
|
||||
@ -28,12 +29,12 @@
|
||||
let previewCameraPrivacySettings = valueCameraPrivacySettings;
|
||||
let previewMicrophonePrivacySettings = valueMicrophonePrivacySettings;
|
||||
|
||||
function saveSetting() {
|
||||
async function saveSetting() {
|
||||
let change = false;
|
||||
|
||||
if (valueLocale !== previewValueLocale) {
|
||||
previewValueLocale = valueLocale;
|
||||
setCurrentLocale(valueLocale as Locales);
|
||||
await setCurrentLocale(valueLocale as Locales);
|
||||
}
|
||||
|
||||
if (valueVideo !== previewValueVideo) {
|
||||
@ -174,7 +175,7 @@
|
||||
<div class="nes-select is-dark">
|
||||
<select class="languages-switcher" bind:value={valueLocale}>
|
||||
{#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}
|
||||
</select>
|
||||
</div>
|
||||
@ -191,11 +192,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
</label>
|
||||
</section>
|
||||
@ -211,6 +222,7 @@
|
||||
type="checkbox"
|
||||
class="nes-checkbox is-dark"
|
||||
bind:checked={fullscreen}
|
||||
on:change={(event) => analyticsClient.settingFullscreen(event.currentTarget.value)}
|
||||
on:change={changeFullscreen}
|
||||
/>
|
||||
<span>{$LL.menu.settings.fullscreen()}</span>
|
||||
@ -220,6 +232,7 @@
|
||||
type="checkbox"
|
||||
class="nes-checkbox is-dark"
|
||||
bind:checked={notification}
|
||||
on:change={(event) => analyticsClient.settingNotification(event.currentTarget.value)}
|
||||
on:change={changeNotification}
|
||||
/>
|
||||
<span>{$LL.menu.settings.notifications()}</span>
|
||||
@ -229,6 +242,7 @@
|
||||
type="checkbox"
|
||||
class="nes-checkbox is-dark"
|
||||
bind:checked={forceCowebsiteTrigger}
|
||||
on:change={(event) => analyticsClient.settingAskWebsite(event.currentTarget.value)}
|
||||
on:change={changeForceCowebsiteTrigger}
|
||||
/>
|
||||
<span>{$LL.menu.settings.cowebsiteTrigger()}</span>
|
||||
@ -238,6 +252,7 @@
|
||||
type="checkbox"
|
||||
class="nes-checkbox is-dark"
|
||||
bind:checked={ignoreFollowRequests}
|
||||
on:change={(event) => analyticsClient.settingRequestFollow(event.currentTarget.value)}
|
||||
on:change={changeIgnoreFollowRequests}
|
||||
/>
|
||||
<span>{$LL.menu.settings.ignoreFollowRequest()}</span>
|
||||
@ -246,6 +261,7 @@
|
||||
type="checkbox"
|
||||
class="nes-checkbox is-dark"
|
||||
bind:checked={decreaseAudioPlayerVolumeWhileTalking}
|
||||
on:change={(event) => analyticsClient.settingDecreaseAudioVolume(event.currentTarget.value)}
|
||||
on:change={changeDecreaseAudioPlayerVolumeWhileTalking}
|
||||
/>
|
||||
<span>{$LL.audio.manager.reduce()}</span>
|
||||
|
@ -2,6 +2,7 @@
|
||||
import { fly } from "svelte/transition";
|
||||
import { errorScreenStore } from "../../Stores/ErrorScreenStore";
|
||||
import { gameManager } from "../../Phaser/Game/GameManager";
|
||||
import { connectionManager } from "../../Connexion/ConnectionManager";
|
||||
import { get } from "svelte/store";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
@ -11,7 +12,8 @@
|
||||
let errorScreen = get(errorScreenStore);
|
||||
|
||||
function click() {
|
||||
window.location.reload();
|
||||
if (errorScreen.type === "unauthorized") void connectionManager.logout();
|
||||
else window.location.reload();
|
||||
}
|
||||
let details = errorScreen.details;
|
||||
let timeVar = errorScreen.timeToRetry ?? 0;
|
||||
@ -37,9 +39,9 @@
|
||||
<p class="details">
|
||||
{detailsStylized}{#if $errorScreenStore.type === "retry"}<div class="loading" />{/if}
|
||||
</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}>
|
||||
<img src={reload} alt="" class="reload" />
|
||||
{#if $errorScreenStore.type === "retry"}<img src={reload} alt="" class="reload" />{/if}
|
||||
{$errorScreenStore.buttonTitle}
|
||||
</button>
|
||||
{/if}
|
||||
|
@ -3,6 +3,7 @@
|
||||
import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene";
|
||||
import LL from "../../i18n/i18n-svelte";
|
||||
import { customizeAvailableStore, selectedCollection } from "../../Stores/SelectCharacterSceneStore";
|
||||
import { analyticsClient } from "../../Administration/AnalyticsClient";
|
||||
|
||||
export let game: Game;
|
||||
|
||||
@ -40,13 +41,15 @@
|
||||
<button
|
||||
type="submit"
|
||||
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}
|
||||
<button
|
||||
type="submit"
|
||||
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}
|
||||
</section>
|
||||
|
@ -364,15 +364,13 @@ class ConnectionManager {
|
||||
|
||||
if (locale) {
|
||||
try {
|
||||
if (locales.indexOf(locale) == -1) {
|
||||
locales.forEach((l) => {
|
||||
if (l.startsWith(locale.split("-")[0])) {
|
||||
setCurrentLocale(l);
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (locales.indexOf(locale) !== -1) {
|
||||
await setCurrentLocale(locale as Locales);
|
||||
} else {
|
||||
setCurrentLocale(locale as Locales);
|
||||
const nonRegionSpecificLocale = locales.find((l) => l.startsWith(locale.split("-")[0]));
|
||||
if (nonRegionSpecificLocale) {
|
||||
await setCurrentLocale(nonRegionSpecificLocale);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Could not set locale", err);
|
||||
|
@ -483,9 +483,15 @@ export class RoomConnection implements RoomConnection {
|
||||
}
|
||||
case "errorScreenMessage": {
|
||||
this._errorScreenMessageStream.next(message.errorScreenMessage);
|
||||
if (message.errorScreenMessage.code !== "retry") this.closed = true;
|
||||
console.error("An error occurred server side: " + message.errorScreenMessage.code);
|
||||
errorScreenStore.setError(message.errorScreenMessage);
|
||||
console.error("An error occurred server side: " + JSON.stringify(message.errorScreenMessage));
|
||||
if (message.errorScreenMessage.code !== "retry") {
|
||||
this.closed = true;
|
||||
}
|
||||
if (message.errorScreenMessage.type === "redirect" && message.errorScreenMessage.urlToRedirect) {
|
||||
window.location.assign(message.errorScreenMessage.urlToRedirect);
|
||||
} else {
|
||||
errorScreenStore.setError(message.errorScreenMessage);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
@ -61,9 +61,8 @@ export class CustomWokaPreviewer extends Phaser.GameObjects.Container {
|
||||
this.frame = this.scene.add.graphics();
|
||||
this.turnIcon = this.scene.add
|
||||
.image(this.background.displayWidth * 0.35, this.background.displayHeight * 0.35, "iconTurn")
|
||||
.setScale(0.25)
|
||||
.setTintFill(0xffffff)
|
||||
.setAlpha(0.5);
|
||||
.setScale(0.2)
|
||||
.setAlpha(0.75);
|
||||
|
||||
this.drawFrame();
|
||||
this.setSize(this.SIZE, this.SIZE);
|
||||
@ -130,11 +129,11 @@ export class CustomWokaPreviewer extends Phaser.GameObjects.Container {
|
||||
this.changeAnimation(direction, moving);
|
||||
|
||||
this.turnIconTween?.stop();
|
||||
this.turnIcon.setScale(0.25);
|
||||
this.turnIcon.setScale(0.2);
|
||||
this.turnIconTween = this.scene.tweens.add({
|
||||
targets: [this.turnIcon],
|
||||
duration: 100,
|
||||
scale: 0.2,
|
||||
scale: 0.15,
|
||||
yoyo: true,
|
||||
ease: Easing.SineEaseIn,
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ export interface IconButtonConfig {
|
||||
hover: IconButtonAppearanceConfig;
|
||||
pressed: IconButtonAppearanceConfig;
|
||||
selected: IconButtonAppearanceConfig;
|
||||
iconScale?: number;
|
||||
}
|
||||
|
||||
export interface IconButtonAppearanceConfig {
|
||||
@ -34,7 +35,7 @@ export class IconButton extends Phaser.GameObjects.Container {
|
||||
this.config = config;
|
||||
|
||||
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.add([this.background, this.icon]);
|
||||
|
@ -4,7 +4,7 @@ import { Character } from "../Entity/Character";
|
||||
import type { GameScene } from "../Game/GameScene";
|
||||
import type { PointInterface } from "../../Connexion/ConnexionModels";
|
||||
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 CancelablePromise from "cancelable-promise";
|
||||
import LL from "../../i18n/i18n-svelte";
|
||||
@ -113,7 +113,7 @@ export class RemotePlayer extends Character implements ActivatableInterface {
|
||||
const actions: ActionsMenuAction[] = [];
|
||||
if (this.visitCardUrl) {
|
||||
actions.push({
|
||||
actionName: LL.woka.menu.businessCard(),
|
||||
actionName: get(LL).woka.menu.businessCard(),
|
||||
protected: true,
|
||||
priority: 1,
|
||||
callback: () => {
|
||||
@ -125,8 +125,8 @@ export class RemotePlayer extends Character implements ActivatableInterface {
|
||||
|
||||
actions.push({
|
||||
actionName: blackListManager.isBlackListed(this.userUuid)
|
||||
? LL.report.block.unblock()
|
||||
: LL.report.block.block(),
|
||||
? get(LL).report.block.unblock()
|
||||
: get(LL).report.block.block(),
|
||||
protected: true,
|
||||
priority: -1,
|
||||
style: "is-error",
|
||||
|
@ -76,6 +76,7 @@ export class ActivatablesManager {
|
||||
const currentPlayerPos = this.currentPlayer.getDirectionalActivationPosition(
|
||||
this.directionalActivationPositionShift
|
||||
);
|
||||
this.activatableObjectsDistances.clear();
|
||||
for (const object of objects) {
|
||||
const distance = MathUtils.distanceBetween(currentPlayerPos, object.getPosition());
|
||||
this.activatableObjectsDistances.set(object, distance);
|
||||
|
@ -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(
|
||||
iframeListener.addActionsMenuKeyToRemotePlayerStream.subscribe((data) => {
|
||||
this.MapPlayersByKey.get(data.id)?.registerActionsMenuAction({
|
||||
|
@ -94,6 +94,7 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
}
|
||||
|
||||
public create(): void {
|
||||
this.tryLoadLastUsedWokaLayers();
|
||||
waScaleManager.zoomModifier = 1;
|
||||
this.createSlotBackgroundTextures();
|
||||
this.initializeCustomWokaPreviewer();
|
||||
@ -149,12 +150,35 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
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 {
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
if (this.textures.getTextureKeys().includes(`floorTexture${i}`)) {
|
||||
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.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.Hat]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconHat")),
|
||||
};
|
||||
}
|
||||
|
||||
private getDefaultIconButtonConfig(iconTextureKey: string): IconButtonConfig {
|
||||
private getDefaultIconButtonConfig(iconTextureKey: string, iconScale?: number): IconButtonConfig {
|
||||
return {
|
||||
iconTextureKey,
|
||||
iconScale,
|
||||
width: 25,
|
||||
height: 25,
|
||||
idle: {
|
||||
@ -327,13 +352,14 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
}
|
||||
|
||||
private handleCustomWokaPreviewerOnResize(): void {
|
||||
const ratio = innerHeight / innerWidth;
|
||||
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 {
|
||||
const ratio = innerHeight / innerWidth;
|
||||
const slotDimension = 50;
|
||||
const slotDimension = WokaBodyPartSlot.SIZE;
|
||||
|
||||
for (const part in this.bodyPartsButtons) {
|
||||
this.bodyPartsButtons[part as CustomWokaBodyPart].setDisplaySize(slotDimension, slotDimension);
|
||||
@ -420,7 +446,7 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
|
||||
private handleRandomizeButtonOnResize(): void {
|
||||
const x =
|
||||
this.customWokaPreviewer.x +
|
||||
this.customWokaPreviewer.x -
|
||||
(this.customWokaPreviewer.displayWidth - this.randomizeButton.displayWidth) * 0.5;
|
||||
const y =
|
||||
this.customWokaPreviewer.y +
|
||||
@ -431,7 +457,7 @@ export class CustomizeScene extends AbstractCharacterScene {
|
||||
|
||||
private handleFinishButtonOnResize(): void {
|
||||
const x =
|
||||
this.customWokaPreviewer.x -
|
||||
this.customWokaPreviewer.x +
|
||||
(this.customWokaPreviewer.displayWidth - this.randomizeButton.displayWidth) * 0.5;
|
||||
const y =
|
||||
this.customWokaPreviewer.y +
|
||||
|
@ -6,7 +6,6 @@ import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene";
|
||||
import { localeDetector } from "../../i18n/locales";
|
||||
import { errorScreenStore } from "../../Stores/ErrorScreenStore";
|
||||
import { isErrorApiData } from "../../Messages/JsonMessages/ErrorApiData";
|
||||
import { connectionManager } from "../../Connexion/ConnectionManager";
|
||||
|
||||
export const EntrySceneName = "EntryScene";
|
||||
|
||||
@ -49,9 +48,7 @@ export class EntryScene extends Scene {
|
||||
.catch((err) => {
|
||||
const errorType = isErrorApiData.safeParse(err?.response?.data);
|
||||
if (errorType.success) {
|
||||
if (errorType.data.type === "unauthorized") {
|
||||
void connectionManager.logout();
|
||||
} else if (errorType.data.type === "redirect") {
|
||||
if (errorType.data.type === "redirect") {
|
||||
window.location.assign(errorType.data.urlToRedirect);
|
||||
} else errorScreenStore.setError(err?.response?.data);
|
||||
} else {
|
||||
|
@ -18,6 +18,7 @@ import { DraggableGrid } from "@home-based-studio/phaser3-utils";
|
||||
import { WokaSlot } from "../Components/SelectWoka/WokaSlot";
|
||||
import { DraggableGridEvent } from "@home-based-studio/phaser3-utils/lib/utils/gui/containers/grids/DraggableGrid";
|
||||
import { wokaList } from "../../Messages/JsonMessages/PlayerTextures";
|
||||
import { myCameraVisibilityStore } from "../../Stores/MyCameraStoreVisibility";
|
||||
|
||||
//todo: put this constants in a dedicated file
|
||||
export const SelectCharacterSceneName = "SelectCharacterScene";
|
||||
@ -133,6 +134,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
|
||||
return;
|
||||
}
|
||||
this.selectedWoka = null;
|
||||
myCameraVisibilityStore.set(false);
|
||||
this.scene.sleep(SelectCharacterSceneName);
|
||||
this.scene.run(CustomizeSceneName);
|
||||
selectCharacterSceneVisibleStore.set(false);
|
||||
|
@ -87,7 +87,10 @@ function createChatMessagesStore() {
|
||||
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) => {
|
||||
const lastMessage = list[list.length - 1];
|
||||
if (
|
||||
@ -106,7 +109,7 @@ function createChatMessagesStore() {
|
||||
});
|
||||
}
|
||||
|
||||
iframeListener.sendUserInputChat(text);
|
||||
iframeListener.sendUserInputChat(text, origin);
|
||||
return list;
|
||||
});
|
||||
chatVisibilityStore.set(true);
|
||||
|
@ -2,7 +2,6 @@ import { get, writable } from "svelte/store";
|
||||
import Timeout = NodeJS.Timeout;
|
||||
import { userIsAdminStore } from "./GameStore";
|
||||
import { CONTACT_URL, IDENTITY_URL, PROFILE_URL } from "../Enum/EnvironmentVariable";
|
||||
import { analyticsClient } from "../Administration/AnalyticsClient";
|
||||
import type { Translation } from "../i18n/i18n-types";
|
||||
import axios from "axios";
|
||||
import { localUserStore } from "../Connexion/LocalUserStore";
|
||||
@ -14,7 +13,6 @@ export const userIsConnected = writable(false);
|
||||
export const profileAvailable = writable(true);
|
||||
|
||||
menuVisiblilityStore.subscribe((value) => {
|
||||
if (value) analyticsClient.openedMenu();
|
||||
if (userIsConnected && value && IDENTITY_URL != null) {
|
||||
axios.get(getMeUrl()).catch((err) => {
|
||||
console.error("menuVisiblilityStore => err => ", err);
|
||||
|
@ -8,6 +8,7 @@ import { isMediaBreakpointDown } from "../Utils/BreakpointsUtils";
|
||||
import { LayoutMode } from "./LayoutManager";
|
||||
import type { CoWebsite } from "./CoWebsite/CoWesbite";
|
||||
import type CancelablePromise from "cancelable-promise";
|
||||
import { analyticsClient } from "../Administration/AnalyticsClient";
|
||||
|
||||
export enum iframeStates {
|
||||
closed = 1,
|
||||
@ -126,6 +127,7 @@ class CoWebsiteManager {
|
||||
|
||||
const buttonCloseCoWebsite = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
|
||||
buttonCloseCoWebsite.addEventListener("click", () => {
|
||||
analyticsClient.closeMultiIframe();
|
||||
const coWebsite = this.getMainCoWebsite();
|
||||
|
||||
if (!coWebsite) {
|
||||
@ -143,6 +145,7 @@ class CoWebsiteManager {
|
||||
|
||||
const buttonFullScreenFrame = HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId);
|
||||
buttonFullScreenFrame.addEventListener("click", () => {
|
||||
analyticsClient.fullScreenMultiIframe();
|
||||
buttonFullScreenFrame.blur();
|
||||
this.fullscreen();
|
||||
});
|
||||
@ -159,6 +162,7 @@ class CoWebsiteManager {
|
||||
});
|
||||
|
||||
buttonSwipe.addEventListener("click", () => {
|
||||
analyticsClient.switchMultiIframe();
|
||||
const mainCoWebsite = this.getMainCoWebsite();
|
||||
const highlightedEmbed = get(highlightedEmbedScreen);
|
||||
if (highlightedEmbed?.type === "cowebsite") {
|
||||
|
4
front/src/i18n/.gitignore
vendored
4
front/src/i18n/.gitignore
vendored
@ -1,3 +1 @@
|
||||
i18n-svelte.ts
|
||||
i18n-types.ts
|
||||
i18n-util.ts
|
||||
i18n-*.ts
|
||||
|
@ -16,8 +16,6 @@ import trigger from "./trigger";
|
||||
|
||||
const de_DE: Translation = {
|
||||
...(en_US as Translation),
|
||||
language: "Deutsch",
|
||||
country: "Deutschland",
|
||||
audio,
|
||||
camera,
|
||||
chat,
|
||||
|
@ -77,7 +77,8 @@ const menu: NonNullable<Translation["menu"]> = {
|
||||
description: "Link zu diesem Raum teilen!",
|
||||
copy: "Kopieren",
|
||||
share: "Teilen",
|
||||
walk_automatically_to_position: "Automatisch zu meiner Position gehen",
|
||||
walkAutomaticallyToPosition: "Automatisch zu meiner Position gehen",
|
||||
selectEntryPoint: "Select an entry point",
|
||||
},
|
||||
globalMessage: {
|
||||
text: "Text",
|
||||
|
@ -14,8 +14,6 @@ import emoji from "./emoji";
|
||||
import trigger from "./trigger";
|
||||
|
||||
const en_US: BaseTranslation = {
|
||||
language: "English",
|
||||
country: "United States",
|
||||
audio,
|
||||
camera,
|
||||
chat,
|
||||
|
@ -77,7 +77,8 @@ const menu: BaseTranslation = {
|
||||
description: "Share the link of the room!",
|
||||
copy: "Copy",
|
||||
share: "Share",
|
||||
walk_automatically_to_position: "Walk automatically to my position",
|
||||
walkAutomaticallyToPosition: "Walk automatically to my position",
|
||||
selectEntryPoint: "Select an entry point",
|
||||
},
|
||||
globalMessage: {
|
||||
text: "Text",
|
||||
|
@ -1,8 +1,8 @@
|
||||
import type { AsyncFormattersInitializer } from "typesafe-i18n";
|
||||
import type { FormattersInitializer } from "typesafe-i18n";
|
||||
import type { Locales, Formatters } from "./i18n-types";
|
||||
|
||||
// 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 = {
|
||||
// add your formatter functions here
|
||||
};
|
||||
|
@ -1,70 +1,44 @@
|
||||
import { detectLocale, navigatorDetector, initLocalStorageDetector } from "typesafe-i18n/detectors";
|
||||
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 { baseLocale, getTranslationForLocale, locales } from "./i18n-util";
|
||||
import { get } from "svelte/store";
|
||||
import { baseLocale, locales } from "./i18n-util";
|
||||
import { loadLocaleAsync } from "./i18n-util.async";
|
||||
|
||||
const fallbackLocale = FALLBACK_LOCALE || baseLocale;
|
||||
const fallbackLocale = (FALLBACK_LOCALE || baseLocale) as Locales;
|
||||
const localStorageProperty = "language";
|
||||
|
||||
export const localeDetector = async () => {
|
||||
const exist = localStorage.getItem(localStorageProperty);
|
||||
let detectedLocale: Locales = fallbackLocale as Locales;
|
||||
let detectedLocale: Locales = fallbackLocale;
|
||||
|
||||
if (exist) {
|
||||
const localStorageDetector = initLocalStorageDetector(localStorageProperty);
|
||||
detectedLocale = detectLocale(fallbackLocale, locales, localStorageDetector) as Locales;
|
||||
detectedLocale = detectLocale(fallbackLocale, locales, localStorageDetector);
|
||||
} 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);
|
||||
setLocale(locale).catch(() => {
|
||||
console.log("Cannot reload the locale!");
|
||||
});
|
||||
await loadLocaleAsync(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() {
|
||||
const localesObject: DisplayableLocale[] = [];
|
||||
locales.forEach((locale) => {
|
||||
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) {
|
||||
//
|
||||
}
|
||||
// backwards compatibility
|
||||
if (!Intl.DisplayNames) {
|
||||
return { id: locale, language, region };
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
return {
|
||||
id: locale,
|
||||
language: new Intl.DisplayNames(locale, { type: "language" }).of(language),
|
||||
region: new Intl.DisplayNames(locale, { type: "region" }).of(region),
|
||||
};
|
||||
});
|
||||
|
@ -95,7 +95,7 @@
|
||||
|
||||
&-buffer {
|
||||
iframe {
|
||||
z-index: 45 !important;
|
||||
z-index: 201 !important;
|
||||
pointer-events: none !important;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
|
@ -8,7 +8,7 @@
|
||||
"moduleResolution": "node",
|
||||
//"module": "CommonJS",
|
||||
"module": "ESNext",
|
||||
"target": "ES2017",
|
||||
"target": "ES2020",
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"jsx": "react",
|
||||
|
@ -7,6 +7,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@16bits/nes.css/-/nes.css-2.3.2.tgz#e69db834119b33ae8d3cb044f106a07a17cadd6f"
|
||||
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":
|
||||
version "7.14.0"
|
||||
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"
|
||||
integrity sha512-2HDCV+6XXJjSuBAhDWLRr111buMQ3bIZrKo3dymIhEJ4oJCC/3yDqg7HDQIn8Y8KKbsM0AtuHMZW4yz2tPBsYg==
|
||||
|
||||
"@home-based-studio/phaser3-utils@^0.4.2":
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@home-based-studio/phaser3-utils/-/phaser3-utils-0.4.2.tgz#b2c1815a6b51321ea8dab027b5badcf714d99fd6"
|
||||
integrity sha512-S0VkAq3z0Kf0vEUUyCDes911icvc+nkUq7lGp23zD/5lk7LTGM51NswSAfel7Rm/DLY8IBxvDTBJADTf/De82w==
|
||||
"@home-based-studio/phaser3-utils@^0.4.7":
|
||||
version "0.4.7"
|
||||
resolved "https://registry.yarnpkg.com/@home-based-studio/phaser3-utils/-/phaser3-utils-0.4.7.tgz#d0464c81cb27328657d3fd048396f6936e200c48"
|
||||
integrity sha512-gYt1mkuad85uzYwHK0+wp+mrsGASV4sRZPaHZHnO8A2ofTAnX36S3PcI+BqKchdJ0I7jvBQcfh0yp1Ug0BHT+A==
|
||||
dependencies:
|
||||
phaser "3.55.1"
|
||||
|
||||
@ -2060,6 +2068,13 @@ onetime@^5.1.0, onetime@^5.1.2:
|
||||
dependencies:
|
||||
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:
|
||||
version "0.9.1"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
|
||||
@ -2880,6 +2895,11 @@ to-regex-range@^5.0.1:
|
||||
dependencies:
|
||||
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:
|
||||
version "1.0.4"
|
||||
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"
|
||||
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
|
||||
|
||||
typesafe-i18n@^2.59.0:
|
||||
version "2.59.0"
|
||||
resolved "https://registry.yarnpkg.com/typesafe-i18n/-/typesafe-i18n-2.59.0.tgz#09a9a32e61711418d927a389fa52e1c06a5fa5c4"
|
||||
integrity sha512-Qv3Mrwmb8b73VNzQDPHPECzwymdBRVyDiZ3w2qnp4c2iv/7TGuiJegNHT/l3MooEN7IPbSpc5tbXw2x3MbGtFg==
|
||||
typesafe-i18n@^5.4.0:
|
||||
version "5.4.0"
|
||||
resolved "https://registry.yarnpkg.com/typesafe-i18n/-/typesafe-i18n-5.4.0.tgz#cab696160bb144c387d7cbd13f7a728aa8371777"
|
||||
integrity sha512-htewpld3FzZQv3Y1G31w54bofaaKR11MCkDK0FIYuXCpX72y1G6fkXUDslqzZCyVkZWRnIhY8leviNDxLwEzRw==
|
||||
|
||||
typescript@*:
|
||||
version "4.3.2"
|
||||
@ -3071,6 +3091,11 @@ validate-npm-package-license@^3.0.1:
|
||||
spdx-correct "^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:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-rewrite-all/-/vite-plugin-rewrite-all-0.1.2.tgz#312bbcd76c700ceac5153bfc5ad7e3e3e4bc9606"
|
||||
|
@ -1,7 +1,7 @@
|
||||
let menuIframeApi = undefined;
|
||||
|
||||
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'});
|
||||
|
@ -13,7 +13,7 @@
|
||||
"width":10,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
},
|
||||
{
|
||||
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
"height":10,
|
||||
@ -25,7 +25,7 @@
|
||||
"width":10,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":3,
|
||||
@ -40,7 +40,7 @@
|
||||
{
|
||||
"fontfamily":"Sans Serif",
|
||||
"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
|
||||
},
|
||||
"type":"",
|
||||
@ -79,4 +79,4 @@
|
||||
"type":"map",
|
||||
"version":"1.6",
|
||||
"width":10
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
</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>
|
||||
</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
|
||||
</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>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { extendApi } from "@anatine/zod-openapi";
|
||||
|
||||
/*
|
||||
* WARNING! The original file is in /messages/JsonMessages.
|
||||
@ -6,10 +7,17 @@ import { z } from "zod";
|
||||
*/
|
||||
|
||||
export const isAdminApiData = z.object({
|
||||
userUuid: z.string(),
|
||||
email: z.nullable(z.string()),
|
||||
roomUrl: z.string(),
|
||||
mapUrlStart: z.string(),
|
||||
// @ts-ignore
|
||||
userUuid: extendApi(z.string(), { example: "998ce839-3dea-4698-8b41-ebbdf7688ad9" }),
|
||||
email: extendApi(z.nullable(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())),
|
||||
});
|
||||
|
||||
|
@ -1,40 +1,129 @@
|
||||
import { z } from "zod";
|
||||
import { extendApi } from "@anatine/zod-openapi";
|
||||
|
||||
/*
|
||||
* WARNING! The original file is in /messages/JsonMessages.
|
||||
* All other files are automatically copied from this file on container startup / build
|
||||
*/
|
||||
|
||||
export const isErrorApiErrorData = z.object({
|
||||
export const isErrorApiErrorData = extendApi(
|
||||
// @ts-ignore
|
||||
type: z.literal("error"),
|
||||
code: z.string(),
|
||||
title: z.string(),
|
||||
subtitle: z.string(),
|
||||
details: z.string(),
|
||||
image: z.string(),
|
||||
});
|
||||
z.object({
|
||||
type: z.literal("error"),
|
||||
code: extendApi(z.string(), {
|
||||
description: "The system code of an error, it must be in SCREAMING_SNAKE_CASE.",
|
||||
example: "ROOM_NOT_FOUND",
|
||||
}),
|
||||
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({
|
||||
type: z.literal("retry"),
|
||||
code: z.string(),
|
||||
title: z.string(),
|
||||
subtitle: z.string(),
|
||||
details: z.string(),
|
||||
image: z.string(),
|
||||
buttonTitle: z.optional(z.nullable(z.string())),
|
||||
timeToRetry: z.number(),
|
||||
canRetryManual: z.boolean(),
|
||||
});
|
||||
export const isErrorApiRetryData = extendApi(
|
||||
z.object({
|
||||
type: z.literal("retry"),
|
||||
code: extendApi(z.string(), {
|
||||
description:
|
||||
"The system code of an error, it must be in SCREAMING_SNAKE_CASE. \n It will not be displayed to the user.",
|
||||
example: "WORLD_FULL",
|
||||
}),
|
||||
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: "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({
|
||||
type: z.literal("redirect"),
|
||||
urlToRedirect: z.string(),
|
||||
});
|
||||
export const isErrorApiRedirectData = extendApi(
|
||||
z.object({
|
||||
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({
|
||||
type: z.literal("unauthorized"),
|
||||
});
|
||||
export const isErrorApiUnauthorizedData = extendApi(
|
||||
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", [
|
||||
isErrorApiErrorData,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { extendApi } from "@anatine/zod-openapi";
|
||||
|
||||
/*
|
||||
* WARNING! The original file is in /messages/JsonMessages.
|
||||
@ -6,20 +7,48 @@ import { z } from "zod";
|
||||
*/
|
||||
|
||||
export const isMapDetailsData = z.object({
|
||||
mapUrl: z.string(),
|
||||
authenticationMandatory: z.optional(z.nullable(z.boolean())),
|
||||
group: z.nullable(z.string()),
|
||||
// @ts-ignore
|
||||
mapUrl: extendApi(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())),
|
||||
iframeAuthentication: z.optional(z.nullable(z.string())),
|
||||
contactPage: extendApi(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
|
||||
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
|
||||
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
|
||||
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"
|
||||
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>;
|
||||
|
@ -18,8 +18,10 @@
|
||||
"pretty-check": "yarn prettier --check 'JsonMessages/**/*.ts'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anatine/zod-openapi": "^1.3.0",
|
||||
"google-protobuf": "^3.13.0",
|
||||
"grpc": "^1.24.4",
|
||||
"openapi3-ts": "^2.0.2",
|
||||
"ts-proto": "^1.96.0",
|
||||
"zod": "^3.14.3"
|
||||
},
|
||||
|
@ -2,6 +2,14 @@
|
||||
# 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":
|
||||
version "7.10.4"
|
||||
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:
|
||||
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:
|
||||
version "0.9.1"
|
||||
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"
|
||||
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:
|
||||
version "4.6.1"
|
||||
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:
|
||||
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:
|
||||
version "1.10.0"
|
||||
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"
|
||||
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
|
||||
|
||||
yaml@^1.10.0:
|
||||
yaml@^1.10.0, yaml@^1.10.2:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { BaseHttpController } from "./BaseHttpController";
|
||||
import * as fs from "fs";
|
||||
import { ADMIN_URL } from "../Enum/EnvironmentVariable";
|
||||
import SwaggerGenerator from "../Services/SwaggerGenerator";
|
||||
|
||||
export class SwaggerController extends BaseHttpController {
|
||||
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)
|
||||
const swaggerJsdoc = require("swagger-jsdoc");
|
||||
const options = {
|
||||
@ -20,6 +22,43 @@ export class SwaggerController extends BaseHttpController {
|
||||
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
|
||||
// @ts-ignore
|
||||
const LiveDirectory = require("live-directory");
|
||||
@ -39,8 +78,16 @@ export class SwaggerController extends BaseHttpController {
|
||||
if (err) {
|
||||
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);
|
||||
|
||||
return;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { extendApi } from "@anatine/zod-openapi";
|
||||
|
||||
export const isBanBannedAdminMessageInterface = z.object({
|
||||
type: z.enum(["ban", "banned"]),
|
||||
@ -8,7 +9,7 @@ export const isBanBannedAdminMessageInterface = z.object({
|
||||
|
||||
export const isUserMessageAdminMessageInterface = z.object({
|
||||
event: z.enum(["user-message"]),
|
||||
message: isBanBannedAdminMessageInterface,
|
||||
message: extendApi(isBanBannedAdminMessageInterface, { $ref: "#/definitions/BanBannedAdminMessageInterface" }),
|
||||
world: z.string(),
|
||||
jwt: z.string(),
|
||||
});
|
||||
|
@ -9,6 +9,7 @@ import qs from "qs";
|
||||
import { AdminInterface } from "./AdminInterface";
|
||||
import { AuthTokenData, jwtTokenManager } from "./JWTTokenManager";
|
||||
import { InvalidTokenError } from "../Controller/InvalidTokenError";
|
||||
import { extendApi } from "@anatine/zod-openapi";
|
||||
|
||||
export interface AdminBannedData {
|
||||
is_banned: boolean;
|
||||
@ -16,15 +17,31 @@ export interface AdminBannedData {
|
||||
}
|
||||
|
||||
export const isFetchMemberDataByUuidResponse = z.object({
|
||||
email: z.string(),
|
||||
userUuid: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
visitCardUrl: z.nullable(z.string()),
|
||||
textures: z.array(isWokaDetail),
|
||||
messages: z.array(z.unknown()),
|
||||
// @ts-ignore
|
||||
email: extendApi(z.string(), {
|
||||
description: "The email of the fetched user, it can be an email, an uuid or undefined.",
|
||||
example: "example@workadventu.re",
|
||||
}),
|
||||
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()),
|
||||
userRoomToken: z.optional(z.string()),
|
||||
anonymous: extendApi(z.optional(z.boolean()), {
|
||||
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>;
|
||||
@ -69,6 +86,47 @@ class AdminApi implements AdminInterface {
|
||||
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", {
|
||||
headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" },
|
||||
params,
|
||||
@ -99,6 +157,58 @@ class AdminApi implements AdminInterface {
|
||||
characterLayers: string[],
|
||||
locale?: string
|
||||
): 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", {
|
||||
params: {
|
||||
userIdentifier,
|
||||
@ -130,6 +240,42 @@ class AdminApi implements AdminInterface {
|
||||
playUri: string | null,
|
||||
locale?: string
|
||||
): 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.
|
||||
const res = await Axios.get(ADMIN_API_URL + "/api/login-url/" + organizationMemberToken, {
|
||||
params: { playUri },
|
||||
@ -154,6 +300,41 @@ class AdminApi implements AdminInterface {
|
||||
reportWorldSlug: 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(
|
||||
`${ADMIN_API_URL}/api/report`,
|
||||
{
|
||||
@ -174,6 +355,53 @@ class AdminApi implements AdminInterface {
|
||||
roomUrl: string,
|
||||
locale?: string
|
||||
): 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.
|
||||
return Axios.get(
|
||||
ADMIN_API_URL +
|
||||
@ -191,6 +419,37 @@ class AdminApi implements AdminInterface {
|
||||
}
|
||||
|
||||
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), {
|
||||
headers: { Authorization: `${ADMIN_API_TOKEN}`, "Accept-Language": locale ?? "en" },
|
||||
}).then((data) => {
|
||||
@ -204,10 +463,6 @@ class AdminApi implements AdminInterface {
|
||||
}
|
||||
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();
|
||||
|
@ -74,9 +74,4 @@ export interface AdminInterface {
|
||||
* @return string
|
||||
*/
|
||||
getProfileUrl(accessToken: string): string;
|
||||
|
||||
/**
|
||||
* @param token
|
||||
*/
|
||||
logoutOauth(token: string): Promise<void>;
|
||||
}
|
||||
|
@ -87,10 +87,6 @@ class LocalAdmin implements AdminInterface {
|
||||
new Error("No admin backoffice set!");
|
||||
return "";
|
||||
}
|
||||
|
||||
async logoutOauth(token: string): Promise<void> {
|
||||
return Promise.reject(new Error("No admin backoffice set!"));
|
||||
}
|
||||
}
|
||||
|
||||
export const localAdmin = new LocalAdmin();
|
||||
|
@ -658,7 +658,7 @@ export class SocketManager implements ZoneEventListener {
|
||||
public emitErrorScreenMessage(client: compressors.WebSocket, errorApi: ErrorApiData) {
|
||||
const errorMessage = new ErrorScreenMessage();
|
||||
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.setTitle(new StringValue().setValue(errorApi.title));
|
||||
errorMessage.setSubtitle(new StringValue().setValue(errorApi.subtitle));
|
||||
|
35
pusher/src/Services/SwaggerGenerator.ts
Normal file
35
pusher/src/Services/SwaggerGenerator.ts
Normal 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();
|
38
tests/tests/iframe_script.spec.ts
Normal file
38
tests/tests/iframe_script.spec.ts
Normal 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});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user