From 07d399403be2107a077110fac8d55e7045c42efd Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Wed, 23 Feb 2022 00:27:07 +0100 Subject: [PATCH 01/32] Refactor how to use new Jitsi icon Signed-off-by: Gregoire Parant --- .../EmbedScreens/CoWebsiteThumbnailSlot.svelte | 9 ++++++++- .../logos => src/Components/images}/jitsi.png | Bin 2 files changed, 8 insertions(+), 1 deletion(-) rename front/{public/resources/logos => src/Components/images}/jitsi.png (100%) diff --git a/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte b/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte index d8431704..a21d8389 100644 --- a/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte +++ b/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte @@ -9,6 +9,8 @@ import { iframeStates } from "../../WebRtc/CoWebsiteManager"; import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; + import uploadFile from "../images/jitsi.png"; + export let index: number; export let coWebsite: CoWebsite; export let vertical: boolean; @@ -21,7 +23,7 @@ onMount(() => { icon.src = isJitsi - ? "/resources/logos/jitsi.png" + ? uploadFile : `${ICON_URL}/icon?url=${coWebsite.getUrl().hostname}&size=64..96..256&fallback_icon_color=14304c`; icon.alt = coWebsite.getUrl().hostname; icon.onload = () => { @@ -350,9 +352,14 @@ color: white; padding: 4px; border-radius: 4px; + p { margin-bottom: 0; } + + &.hide { + display: none; + } } } diff --git a/front/public/resources/logos/jitsi.png b/front/src/Components/images/jitsi.png similarity index 100% rename from front/public/resources/logos/jitsi.png rename to front/src/Components/images/jitsi.png From 52b502770252c52794a39379389652cad24a3c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 14 Mar 2022 13:55:30 +0100 Subject: [PATCH 02/32] Adding source maps to production build. --- front/vite.config.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/front/vite.config.ts b/front/vite.config.ts index 5b9e259d..ce7ab4cc 100644 --- a/front/vite.config.ts +++ b/front/vite.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "vite"; import { svelte } from "@sveltejs/vite-plugin-svelte"; import { envConfig } from "@geprog/vite-plugin-env-config"; import sveltePreprocess from "svelte-preprocess"; -import pluginRewriteAll from 'vite-plugin-rewrite-all'; +import pluginRewriteAll from "vite-plugin-rewrite-all"; export default defineConfig({ server: { @@ -10,7 +10,10 @@ export default defineConfig({ hmr: { // workaround for development in docker clientPort: 80, - } + }, + }, + build: { + sourcemap: true, }, plugins: [ svelte({ From b6b6c7f15fcc9cf6c86f3e1be72e0115f00682b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 14 Mar 2022 14:28:42 +0100 Subject: [PATCH 03/32] Adding error case when texture is empty --- front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts | 6 ++++++ front/src/Phaser/Game/GameScene.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts b/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts index f3c9d273..ad78f183 100644 --- a/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts +++ b/front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts @@ -96,6 +96,12 @@ export const createLoadingPromise = ( return; }); + // If for some reason, the "img" is empty, let's reject the promise. + if (!playerResourceDescriptor.img) { + console.warn("Tried to load an empty texture for a Woka"); + rej(playerResourceDescriptor); + return; + } loadPlugin.spritesheet(playerResourceDescriptor.id, playerResourceDescriptor.img, frameConfig); const errorCallback = (file: { src: string }) => { if (file.src !== playerResourceDescriptor.img) return; diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 4d4c84f5..02c6101a 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -18,7 +18,7 @@ import { soundManager } from "./SoundManager"; import { SharedVariablesManager } from "./SharedVariablesManager"; import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager"; -import { lazyLoadPlayerCharacterTextures, loadWokaTexture } from "../Entity/PlayerTexturesLoadingManager"; +import { lazyLoadPlayerCharacterTextures } from "../Entity/PlayerTexturesLoadingManager"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; import { iframeListener } from "../../Api/IframeListener"; import { DEBUG_MODE, JITSI_URL, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; From 153bffd521216e8293abfed35422571a6541e72e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 15 Mar 2022 11:21:48 +0100 Subject: [PATCH 04/32] Wait for main character to be loaded to display the GameScene This change makes sure the character of the current player is fully loaded before we display the game scene. Otherwise, you could have a glitch for 0.5-2 seconds between the GameScene being displayed and the actual character being displayed. --- front/package.json | 1 + front/src/Phaser/Entity/Character.ts | 35 ++++++++++++++++++++++++---- front/src/Phaser/Game/GameScene.ts | 6 ++++- front/yarn.lock | 5 ++++ 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/front/package.json b/front/package.json index 4a0ea649..a52193b2 100644 --- a/front/package.json +++ b/front/package.json @@ -57,6 +57,7 @@ "simple-peer": "^9.11.0", "socket.io-client": "^2.3.0", "standardized-audio-context": "^25.2.4", + "ts-deferred": "^1.0.4", "ts-proto": "^1.96.0", "typesafe-i18n": "^2.59.0", "uuidv4": "^6.2.10", diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index 37d92b89..65e4c190 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -18,6 +18,7 @@ import { createColorStore } from "../../Stores/OutlineColorStore"; import type { OutlineableInterface } from "../Game/OutlineableInterface"; import type CancelablePromise from "cancelable-promise"; import { TalkIcon } from "../Components/TalkIcon"; +import { Deferred } from "ts-deferred"; const playerNameY = -25; @@ -50,6 +51,11 @@ export abstract class Character extends Container implements OutlineableInterfac private readonly outlineColorStoreUnsubscribe: Unsubscriber; private texturePromise: CancelablePromise | undefined; + /** + * A deferred promise that resolves when the texture of the character is actually displayed. + */ + private textureLoadedDeferred = new Deferred(); + constructor( scene: GameScene, x: number, @@ -78,6 +84,7 @@ export abstract class Character extends Container implements OutlineableInterfac this.addTextures(textures, frame); this.invisible = false; this.playAnimation(direction, moving); + this.textureLoadedDeferred.resolve(); return this.getSnapshot().then((htmlImageElementSrc) => { this._pictureStore.set(htmlImageElementSrc); }); @@ -92,11 +99,20 @@ export abstract class Character extends Container implements OutlineableInterfac id: "eyes_23", img: "resources/customisation/character_eyes/character_eyes23.png", }, - ]).then((textures) => { - this.addTextures(textures, frame); - this.invisible = false; - this.playAnimation(direction, moving); - }); + ]) + .then((textures) => { + this.addTextures(textures, frame); + this.invisible = false; + this.playAnimation(direction, moving); + this.textureLoadedDeferred.resolve(); + return this.getSnapshot().then((htmlImageElementSrc) => { + this._pictureStore.set(htmlImageElementSrc); + }); + }) + .catch((e) => { + this.textureLoadedDeferred.reject(e); + throw e; + }); }) .finally(() => { this.texturePromise = undefined; @@ -517,4 +533,13 @@ export abstract class Character extends Container implements OutlineableInterfac public characterFarAwayOutline(): void { this.outlineColorStore.characterFarAway(); } + + /** + * Returns a promise that resolves as soon as a texture is displayed for the user. + * The promise will return when the required texture is loaded OR when the fallback texture is loaded (in case + * the required texture could not be loaded). + */ + public getTextureLoadedPromise(): PromiseLike { + return this.textureLoadedDeferred.promise; + } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 02c6101a..4ce35372 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -702,7 +702,11 @@ export class GameScene extends DirtyScene { } }); - Promise.all([this.connectionAnswerPromise as Promise, ...scriptPromises]) + Promise.all([ + this.connectionAnswerPromise as Promise, + ...scriptPromises, + this.CurrentPlayer.getTextureLoadedPromise() as Promise, + ]) .then(() => { this.scene.wake(); }) diff --git a/front/yarn.lock b/front/yarn.lock index c7263531..4c6d813d 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -2900,6 +2900,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +ts-deferred@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/ts-deferred/-/ts-deferred-1.0.4.tgz#58145ebaeef5b8f2a290b8cec3d060839f9489c7" + integrity sha1-WBReuu71uPKikLjOw9Bgg5+Uicc= + ts-node@^10.4.0: version "10.4.0" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" From b959ce7a6dd2db72f868f6b12b525dccc9d6ff88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 15 Mar 2022 11:32:04 +0100 Subject: [PATCH 05/32] Migrating some promises to Deferred objects to simplify the code. --- front/src/Phaser/Game/GameScene.ts | 31 ++++++++++++------------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 4ce35372..fea70920 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -99,6 +99,7 @@ import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite"; import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite"; import { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures"; import CancelablePromise from "cancelable-promise"; +import { Deferred } from "ts-deferred"; export interface GameSceneInitInterface { initPosition: PointInterface | null; reconnecting: boolean; @@ -164,13 +165,9 @@ export class GameScene extends DirtyScene { private playersPositionInterpolator = new PlayersPositionInterpolator(); public connection: RoomConnection | undefined; private simplePeer!: SimplePeer; - private connectionAnswerPromise: Promise; - private connectionAnswerPromiseResolve!: ( - value: RoomJoinedMessageInterface | PromiseLike - ) => void; + private connectionAnswerPromiseDeferred: Deferred; // A promise that will resolve when the "create" method is called (signaling loading is ended) - private createPromise: Promise; - private createPromiseResolve!: (value?: void | PromiseLike) => void; + private createPromiseDeferred: Deferred; private iframeSubscriptionList!: Array; private peerStoreUnsubscribe!: Unsubscriber; private emoteUnsubscribe!: Unsubscriber; @@ -232,12 +229,8 @@ export class GameScene extends DirtyScene { this.MapUrlFile = MapUrlFile; this.roomUrl = room.key; - this.createPromise = new Promise((resolve, reject): void => { - this.createPromiseResolve = resolve; - }); - this.connectionAnswerPromise = new Promise((resolve, reject): void => { - this.connectionAnswerPromiseResolve = resolve; - }); + this.createPromiseDeferred = new Deferred(); + this.connectionAnswerPromiseDeferred = new Deferred(); this.loader = new Loader(this); } @@ -408,11 +401,11 @@ export class GameScene extends DirtyScene { this.load.on("complete", () => { // FIXME: the factory might fail because the resources might not be loaded yet... // We would need to add a loader ended event in addition to the createPromise - this.createPromise + this.createPromiseDeferred.promise .then(async () => { itemFactory.create(this); - const roomJoinedAnswer = await this.connectionAnswerPromise; + const roomJoinedAnswer = await this.connectionAnswerPromiseDeferred.promise; for (const object of objectsOfType) { // TODO: we should pass here a factory to create sprites (maybe?) @@ -609,7 +602,7 @@ export class GameScene extends DirtyScene { } } - this.createPromiseResolve(); + this.createPromiseDeferred.resolve(); // Now, let's load the script, if any const scripts = this.getScriptUrls(this.mapFile); const disableModuleMode = this.getProperty(this.mapFile, GameMapProperties.SCRIPT_DISABLE_MODULE_SUPPORT) as @@ -703,7 +696,7 @@ export class GameScene extends DirtyScene { }); Promise.all([ - this.connectionAnswerPromise as Promise, + this.connectionAnswerPromiseDeferred.promise as Promise, ...scriptPromises, this.CurrentPlayer.getTextureLoadedPromise() as Promise, ]) @@ -872,7 +865,7 @@ export class GameScene extends DirtyScene { ); //this.initUsersPosition(roomJoinedMessage.users); - this.connectionAnswerPromiseResolve(onConnect.room); + this.connectionAnswerPromiseDeferred.resolve(onConnect.room); // Analyze tags to find if we are admin. If yes, show console. if (this.scene.isSleeping()) { @@ -1287,7 +1280,7 @@ ${escapedMessage} iframeListener.registerAnswerer("getState", async () => { // The sharedVariablesManager is not instantiated before the connection is established. So we need to wait // for the connection to send back the answer. - await this.connectionAnswerPromise; + await this.connectionAnswerPromiseDeferred.promise; return { mapUrl: this.MapUrlFile, startLayerName: this.startPositionCalculator.startLayerName, @@ -1310,7 +1303,7 @@ ${escapedMessage} }) ); iframeListener.registerAnswerer("loadTileset", (eventTileset) => { - return this.connectionAnswerPromise.then(() => { + return this.connectionAnswerPromiseDeferred.promise.then(() => { const jsonTilesetDir = eventTileset.url.substr(0, eventTileset.url.lastIndexOf("/")); //Initialise the firstgid to 1 because if there is no tileset in the tilemap, the firstgid will be 1 let newFirstgid = 1; From 79db6c8f3bc72b0d7978a693058127ef1113c332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 15 Mar 2022 17:36:48 +0100 Subject: [PATCH 06/32] Fixing a race condition in Jitsi When setting the name, in rare cases, Jitsi was not initialized yet and setting the name would cause a JS error. We are now waiting for Jitsi to be properly initialized before setting the name. --- front/src/WebRtc/JitsiFactory.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/front/src/WebRtc/JitsiFactory.ts b/front/src/WebRtc/JitsiFactory.ts index d328a72d..3d58d9c2 100644 --- a/front/src/WebRtc/JitsiFactory.ts +++ b/front/src/WebRtc/JitsiFactory.ts @@ -197,7 +197,10 @@ class JitsiFactory { options.onload = () => doResolve(); //we want for the iframe to be loaded before triggering animations. this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options); - this.jitsiApi.executeCommand("displayName", playerName); + + this.jitsiApi.addListener("videoConferenceJoined", () => { + this.jitsiApi?.executeCommand("displayName", playerName); + }); this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback); this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback); From 53b184e82bb0f0bb7aabe16ba2631297a5974a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 15 Mar 2022 15:50:25 +0100 Subject: [PATCH 07/32] Adding support for custom logos The admin can now set custom logos for the login scene and for the loading screen. --- front/src/Components/Login/LoginScene.svelte | 4 +++- front/src/Connexion/Room.ts | 12 +++++++++++ front/src/Phaser/Components/Loader.ts | 21 +++++++++++--------- messages/JsonMessages/MapDetailsData.ts | 4 ++++ pusher/src/Controller/MapController.ts | 8 ++++++++ pusher/src/Services/AdminApi.ts | 12 ++++++++--- 6 files changed, 48 insertions(+), 13 deletions(-) diff --git a/front/src/Components/Login/LoginScene.svelte b/front/src/Components/Login/LoginScene.svelte index 6b26e659..63a76342 100644 --- a/front/src/Components/Login/LoginScene.svelte +++ b/front/src/Components/Login/LoginScene.svelte @@ -13,6 +13,8 @@ let name = gameManager.getPlayerName() || ""; let startValidating = false; + let logo = gameManager.currentStartedRoom.loginSceneLogo ?? logoImg; + function submit() { startValidating = true; @@ -25,7 +27,7 @@
- WorkAdventure logo +

{$LL.login.input.name.placeholder()}

diff --git a/front/src/Connexion/Room.ts b/front/src/Connexion/Room.ts index 778f7e35..bf95e48d 100644 --- a/front/src/Connexion/Room.ts +++ b/front/src/Connexion/Room.ts @@ -31,6 +31,8 @@ export class Room { private _group: string | null = null; private _expireOn: Date | undefined; private _canReport: boolean = false; + private _loadingLogo: string | undefined; + private _loginSceneLogo: string | undefined; private constructor(private roomUrl: URL) { this.id = roomUrl.pathname; @@ -126,6 +128,8 @@ export class Room { this._expireOn = new Date(data.expireOn); } this._canReport = data.canReport ?? false; + this._loadingLogo = data.loadingLogo ?? undefined; + this._loginSceneLogo = data.loginSceneLogo ?? undefined; return new MapDetail(data.mapUrl); } else { throw new Error("Data received by the /map endpoint of the Pusher is not in a valid format."); @@ -233,4 +237,12 @@ export class Room { get canReport(): boolean { return this._canReport; } + + get loadingLogo(): string | undefined { + return this._loadingLogo; + } + + get loginSceneLogo(): string | undefined { + return this._loginSceneLogo; + } } diff --git a/front/src/Phaser/Components/Loader.ts b/front/src/Phaser/Components/Loader.ts index 7eb08e6d..e13272e7 100644 --- a/front/src/Phaser/Components/Loader.ts +++ b/front/src/Phaser/Components/Loader.ts @@ -1,9 +1,8 @@ import ImageFrameConfig = Phaser.Types.Loader.FileTypes.ImageFrameConfig; import { DirtyScene } from "../Game/DirtyScene"; +import { gameManager } from "../Game/GameManager"; -const LogoNameIndex: string = "logoLoading"; const TextName: string = "Loading..."; -const LogoResource: string = "static/images/logo.png"; const LogoFrame: ImageFrameConfig = { frameWidth: 310, frameHeight: 60 }; const loadingBarHeight: number = 16; @@ -15,6 +14,7 @@ export class Loader { private progressAmount: number = 0; private logo: Phaser.GameObjects.Image | undefined; private loadingText: Phaser.GameObjects.Text | null = null; + private logoNameIndex!: string; public constructor(private scene: Phaser.Scene) {} @@ -24,15 +24,18 @@ export class Loader { return; } + const logoResource = gameManager.currentStartedRoom.loadingLogo ?? "static/images/logo.png"; + this.logoNameIndex = "logoLoading" + logoResource; + const loadingBarWidth: number = Math.floor(this.scene.game.renderer.width / 3); const promiseLoadLogoTexture = new Promise((res) => { - if (this.scene.load.textureManager.exists(LogoNameIndex)) { + if (this.scene.load.textureManager.exists(this.logoNameIndex)) { return res( (this.logo = this.scene.add.image( this.scene.game.renderer.width / 2, this.scene.game.renderer.height / 2 - 150, - LogoNameIndex + this.logoNameIndex )) ); } else { @@ -43,8 +46,8 @@ export class Loader { TextName ); } - this.scene.load.spritesheet(LogoNameIndex, LogoResource, LogoFrame); - this.scene.load.once(`filecomplete-spritesheet-${LogoNameIndex}`, () => { + this.scene.load.spritesheet(this.logoNameIndex, logoResource, LogoFrame); + this.scene.load.once(`filecomplete-spritesheet-${this.logoNameIndex}`, () => { if (this.loadingText) { this.loadingText.destroy(); } @@ -52,7 +55,7 @@ export class Loader { (this.logo = this.scene.add.image( this.scene.game.renderer.width / 2, this.scene.game.renderer.height / 2 - 150, - LogoNameIndex + this.logoNameIndex )) ); }); @@ -86,8 +89,8 @@ export class Loader { } public removeLoader(): void { - if (this.scene.load.textureManager.exists(LogoNameIndex)) { - this.scene.load.textureManager.remove(LogoNameIndex); + if (this.scene.load.textureManager.exists(this.logoNameIndex)) { + this.scene.load.textureManager.remove(this.logoNameIndex); } } diff --git a/messages/JsonMessages/MapDetailsData.ts b/messages/JsonMessages/MapDetailsData.ts index 09500b80..2dbf88ea 100644 --- a/messages/JsonMessages/MapDetailsData.ts +++ b/messages/JsonMessages/MapDetailsData.ts @@ -22,6 +22,10 @@ export const isMapDetailsData = new tg.IsInterface() expireOn: tg.isString, // Whether the "report" feature is enabled or not on this room canReport: tg.isBoolean, + // The URL of the logo image on the loading screen + loadingLogo: tg.isNullable(tg.isString), + // The URL of the logo image on "LoginScene" + loginSceneLogo: tg.isNullable(tg.isString), }) .get(); diff --git a/pusher/src/Controller/MapController.ts b/pusher/src/Controller/MapController.ts index c6243713..bbab821d 100644 --- a/pusher/src/Controller/MapController.ts +++ b/pusher/src/Controller/MapController.ts @@ -88,6 +88,14 @@ export class MapController extends BaseHttpController { * type: boolean|undefined * description: Whether the "report" feature is enabled or not on this room * example: true + * loadingLogo: + * type: string + * description: The URL of the image to be used on the loading page + * example: https://example.com/logo.png + * loginSceneLogo: + * type: string + * description: The URL of the image to be used on the LoginScene + * example: https://example.com/logo_login.png * */ this.app.get("/map", (req, res) => { diff --git a/pusher/src/Services/AdminApi.ts b/pusher/src/Services/AdminApi.ts index 9ea84b2d..59df89f9 100644 --- a/pusher/src/Services/AdminApi.ts +++ b/pusher/src/Services/AdminApi.ts @@ -1,7 +1,7 @@ import { ADMIN_API_TOKEN, ADMIN_API_URL, ADMIN_URL, OPID_PROFILE_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable"; import Axios, { AxiosResponse } from "axios"; -import { MapDetailsData } from "../Messages/JsonMessages/MapDetailsData"; -import { RoomRedirect } from "../Messages/JsonMessages/RoomRedirect"; +import { isMapDetailsData, MapDetailsData } from "../Messages/JsonMessages/MapDetailsData"; +import { isRoomRedirect, RoomRedirect } from "../Messages/JsonMessages/RoomRedirect"; import { AdminApiData, isAdminApiData } from "../Messages/JsonMessages/AdminApiData"; import * as tg from "generic-type-guard"; import { isNumber } from "generic-type-guard"; @@ -46,10 +46,16 @@ class AdminApi { userId, }; - const res = await Axios.get(ADMIN_API_URL + "/api/map", { + const res = await Axios.get>(ADMIN_API_URL + "/api/map", { headers: { Authorization: `${ADMIN_API_TOKEN}` }, params, }); + if (!isMapDetailsData(res.data) && !isRoomRedirect(res.data)) { + throw new Error( + "Invalid answer received from the admin for the /api/map endpoint. Received: " + + JSON.stringify(res.data) + ); + } return res.data; } From 37e824c494dc1cf6765ebf7befd779e4d73c7640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 16 Mar 2022 15:27:58 +0100 Subject: [PATCH 08/32] Fixing setInterval not freed for some volume analyzers Also: using a 256 fftSize instead of 2048 to save on CPU cycles. --- front/src/Phaser/Components/SoundMeter.ts | 4 ++-- front/src/Stores/MediaStore.ts | 9 ++++++++- front/src/WebRtc/VideoPeer.ts | 10 +++++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/front/src/Phaser/Components/SoundMeter.ts b/front/src/Phaser/Components/SoundMeter.ts index 6e12912f..86ea610f 100644 --- a/front/src/Phaser/Components/SoundMeter.ts +++ b/front/src/Phaser/Components/SoundMeter.ts @@ -57,8 +57,8 @@ export class SoundMeter { this.context = context; this.analyser = this.context.createAnalyser(); - this.analyser.fftSize = 2048; - const bufferLength = this.analyser.fftSize; + this.analyser.fftSize = 256; + const bufferLength = this.analyser.frequencyBinCount; this.dataArray = new Uint8Array(bufferLength); } diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts index 9494eb7e..b86a97ce 100644 --- a/front/src/Stores/MediaStore.ts +++ b/front/src/Stores/MediaStore.ts @@ -545,8 +545,12 @@ export const obtainedMediaConstraintStore = derived(undefined, (set) => { let timeout: ReturnType; + let soundMeter: SoundMeter; const unsubscribe = localStreamStore.subscribe((localStreamStoreValue) => { clearInterval(timeout); + if (soundMeter) { + soundMeter.stop(); + } if (localStreamStoreValue.type === "error") { set(undefined); return; @@ -557,7 +561,7 @@ export const localVolumeStore = readable(undefined, (set) => set(undefined); return; } - const soundMeter = new SoundMeter(mediaStream); + soundMeter = new SoundMeter(mediaStream); let error = false; timeout = setInterval(() => { @@ -575,6 +579,9 @@ export const localVolumeStore = readable(undefined, (set) => return () => { unsubscribe(); clearInterval(timeout); + if (soundMeter) { + soundMeter.stop(); + } }; }); diff --git a/front/src/WebRtc/VideoPeer.ts b/front/src/WebRtc/VideoPeer.ts index a1ffa14c..50c3e19f 100644 --- a/front/src/WebRtc/VideoPeer.ts +++ b/front/src/WebRtc/VideoPeer.ts @@ -74,12 +74,17 @@ export class VideoPeer extends Peer { this.volumeStore = readable(undefined, (set) => { let timeout: ReturnType; + let soundMeter: SoundMeter; const unsubscribe = this.streamStore.subscribe((mediaStream) => { + clearInterval(timeout); + if (soundMeter) { + soundMeter.stop(); + } if (mediaStream === null || mediaStream.getAudioTracks().length <= 0) { set(undefined); return; } - const soundMeter = new SoundMeter(mediaStream); + soundMeter = new SoundMeter(mediaStream); let error = false; timeout = setInterval(() => { @@ -97,6 +102,9 @@ export class VideoPeer extends Peer { return () => { unsubscribe(); clearInterval(timeout); + if (soundMeter) { + soundMeter.stop(); + } }; }); From d9407a34285cfde6f25970d9936ebb2a088b4aa4 Mon Sep 17 00:00:00 2001 From: Gregoire Parant Date: Tue, 15 Mar 2022 14:56:11 +0100 Subject: [PATCH 09/32] Add * in access location of nginx configuration Signed-off-by: Gregoire Parant --- front/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/nginx.conf b/front/nginx.conf index 9521137c..876ee916 100644 --- a/front/nginx.conf +++ b/front/nginx.conf @@ -45,7 +45,7 @@ server { rewrite ^/jwt /index.html break; } - location ~ ^/[@_]/ { + location ~ ^/[@_*]/ { try_files $uri $uri/ /index.html; } } From 33e78060d4267170a3c4baf35c22db4c4e478481 Mon Sep 17 00:00:00 2001 From: Alexis Faizeau Date: Thu, 17 Mar 2022 17:50:39 +0100 Subject: [PATCH 10/32] Stabilize screensharing (#1982) Co-authored-by: Alexis Faizeau --- front/src/WebRtc/SimplePeer.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index fdd95748..c2d17241 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -125,7 +125,6 @@ export class SimplePeer { if (!peerConnexionDeleted) { throw new Error("Error to delete peer connection"); } - //return this.createPeerConnection(user, localStream); } else { peerConnection.toClose = false; return null; @@ -171,6 +170,7 @@ export class SimplePeer { stream: MediaStream | null ): ScreenSharingPeer | null { const peerConnection = this.PeerScreenSharingConnectionArray.get(user.userId); + if (peerConnection) { if (peerConnection.destroyed) { peerConnection.toClose = true; @@ -182,8 +182,8 @@ export class SimplePeer { this.createPeerConnection(user); } else { peerConnection.toClose = false; + return null; } - return null; } // Enrich the user with last known credentials (if they are not set in the user object, which happens when a user triggers the screen sharing) @@ -201,6 +201,9 @@ export class SimplePeer { this.Connection, stream ); + + peer.toClose = false; + this.PeerScreenSharingConnectionArray.set(user.userId, peer); screenSharingPeerStore.pushNewPeer(peer); @@ -265,10 +268,13 @@ export class SimplePeer { } // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. + peer.toClose = true; peer.destroy(); } catch (err) { console.error("closeConnection", err); } + + screenSharingPeerStore.removePeer(userId); } public closeAllConnections() { @@ -376,11 +382,6 @@ export class SimplePeer { private sendLocalScreenSharingStreamToUser(userId: number, localScreenCapture: MediaStream): void { const uuid = playersStore.getPlayerById(userId)?.userUuid || ""; if (blackListManager.isBlackListed(uuid)) return; - // If a connection already exists with user (because it is already sharing a screen with us... let's use this connection) - if (this.PeerScreenSharingConnectionArray.has(userId)) { - this.pushScreenSharingToRemoteUser(userId, localScreenCapture); - return; - } const screenSharingUser: UserSimplePeerInterface = { userId, From 9c99d760f79869a1c6289a0a6039c525e3709005 Mon Sep 17 00:00:00 2001 From: Alexis Faizeau Date: Fri, 18 Mar 2022 11:44:25 +0100 Subject: [PATCH 11/32] Close socket connection on not authorized woka --- front/src/Connexion/RoomConnection.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 20b69d62..b2ac2e22 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -349,7 +349,9 @@ export class RoomConnection implements RoomConnection { break; } } + if (this.closed) { + this.closeConnection(); break; } From c51fe1cfed7732abf181bc20faffcede8bf4e96e Mon Sep 17 00:00:00 2001 From: Alexis Faizeau Date: Fri, 18 Mar 2022 11:50:48 +0100 Subject: [PATCH 12/32] Translate camera help (#1983) Co-authored-by: Alexis Faizeau --- .../de-DE-chrome.png | Bin 0 -> 42434 bytes .../en-US-firefox.png} | Bin .../fr-FR-chrome.png} | Bin .../HelpCameraSettingsPopup.svelte | 6 ++---- front/src/i18n/de-DE/camera.ts | 4 ++++ front/src/i18n/en-US/camera.ts | 4 ++++ front/src/i18n/fr-FR/camera.ts | 4 ++++ 7 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 front/public/resources/help-setting-camera-permission/de-DE-chrome.png rename front/{src/Components/HelpCameraSettings/images/help-setting-camera-permission-firefox.png => public/resources/help-setting-camera-permission/en-US-firefox.png} (100%) rename front/{src/Components/HelpCameraSettings/images/help-setting-camera-permission-chrome.png => public/resources/help-setting-camera-permission/fr-FR-chrome.png} (100%) diff --git a/front/public/resources/help-setting-camera-permission/de-DE-chrome.png b/front/public/resources/help-setting-camera-permission/de-DE-chrome.png new file mode 100644 index 0000000000000000000000000000000000000000..9903c308135af54dc09227bf6c87e913594d2aed GIT binary patch literal 42434 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%0D|RfE^sU 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 literal 0 HcmV?d00001 diff --git a/front/src/Components/HelpCameraSettings/images/help-setting-camera-permission-firefox.png b/front/public/resources/help-setting-camera-permission/en-US-firefox.png similarity index 100% rename from front/src/Components/HelpCameraSettings/images/help-setting-camera-permission-firefox.png rename to front/public/resources/help-setting-camera-permission/en-US-firefox.png diff --git a/front/src/Components/HelpCameraSettings/images/help-setting-camera-permission-chrome.png b/front/public/resources/help-setting-camera-permission/fr-FR-chrome.png similarity index 100% rename from front/src/Components/HelpCameraSettings/images/help-setting-camera-permission-chrome.png rename to front/public/resources/help-setting-camera-permission/fr-FR-chrome.png diff --git a/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte b/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte index 34d174c6..f28d05ca 100644 --- a/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte +++ b/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte @@ -1,8 +1,6 @@
-
- - - - +
+
+ + - - - - - - - - - - -
-
- + + + + + + + + + + + + +
+
+ +
\ No newline at end of file diff --git a/front/public/resources/logos/fullscreen-exit.svg b/front/public/resources/logos/fullscreen-exit.svg new file mode 100644 index 00000000..1d15cd8b --- /dev/null +++ b/front/public/resources/logos/fullscreen-exit.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/front/public/resources/logos/fullscreen.svg b/front/public/resources/logos/fullscreen.svg new file mode 100644 index 00000000..2c518466 --- /dev/null +++ b/front/public/resources/logos/fullscreen.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file From e20a8caa26944bcddb360db329aaa074dc1ae90e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 22 Mar 2022 11:05:52 +0100 Subject: [PATCH 27/32] Allowing iframeAuthentication URL (stored in OPID_LOGIN_SCREEN_PROVIDER) to be non-absolute. --- front/src/Connexion/ConnectionManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 7578a942..3e314e50 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -55,7 +55,7 @@ class ConnectionManager { loginSceneVisibleIframeStore.set(false); return null; } - const redirectUrl = new URL(`${this._currentRoom.iframeAuthentication}`); + const redirectUrl = new URL(`${this._currentRoom.iframeAuthentication}`, window.location.href); redirectUrl.searchParams.append("state", state); redirectUrl.searchParams.append("nonce", nonce); redirectUrl.searchParams.append("playUri", this._currentRoom.key); From f1549a0ee0eb2faf2775f5f1409e87b555b08bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 22 Mar 2022 12:14:23 +0100 Subject: [PATCH 28/32] Removing the temporary error message when moving to open id screen --- front/src/Connexion/ConnectionManager.ts | 28 ++++++++++++++++-------- front/src/Phaser/Game/GameManager.ts | 10 ++++++++- front/src/Phaser/Login/EmptyScene.ts | 17 ++++++++++++++ front/src/Phaser/Login/LoginScene.ts | 5 ++++- 4 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 front/src/Phaser/Login/EmptyScene.ts diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 3e314e50..09e7257d 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -45,8 +45,10 @@ class ConnectionManager { /** * TODO fix me to be move in game manager + * + * Returns the URL that we need to redirect to to load the OpenID screen, or "null" if no redirection needs to happen. */ - public loadOpenIDScreen() { + public loadOpenIDScreen(): URL | null { const state = localUserStore.generateState(); const nonce = localUserStore.generateNonce(); localUserStore.setAuthToken(null); @@ -59,7 +61,6 @@ class ConnectionManager { redirectUrl.searchParams.append("state", state); redirectUrl.searchParams.append("nonce", nonce); redirectUrl.searchParams.append("playUri", this._currentRoom.key); - window.location.assign(redirectUrl.toString()); return redirectUrl; } @@ -83,8 +84,10 @@ class ConnectionManager { /** * Tries to login to the node server and return the starting map url to be loaded + * + * @return returns a promise to the Room we are going to load OR a pointer to the URL we must redirect to if authentication is needed. */ - public async initGameConnexion(): Promise { + public async initGameConnexion(): Promise { const connexionType = urlManager.getGameConnexionType(); this.connexionType = connexionType; this._currentRoom = null; @@ -101,8 +104,9 @@ class ConnectionManager { if (connexionType === GameConnexionTypes.login) { this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl())); - if (this.loadOpenIDScreen() !== null) { - return Promise.reject(new Error("You will be redirect on login page")); + const redirect = this.loadOpenIDScreen(); + if (redirect !== null) { + return redirect; } urlManager.pushRoomIdToUrl(this._currentRoom); } else if (connexionType === GameConnexionTypes.jwt) { @@ -124,8 +128,11 @@ class ConnectionManager { analyticsClient.loggedWithSso(); } catch (err) { console.error(err); - this.loadOpenIDScreen(); - return Promise.reject(new Error("You will be redirect on login page")); + const redirect = this.loadOpenIDScreen(); + if (redirect === null) { + throw new Error("Unable to redirect on login page."); + } + return redirect; } urlManager.pushRoomIdToUrl(this._currentRoom); } else if (connexionType === GameConnexionTypes.register) { @@ -212,8 +219,11 @@ class ConnectionManager { err.response?.data && err.response.data !== "User cannot to be connected on openid provider") ) { - this.loadOpenIDScreen(); - return Promise.reject(new Error("You will be redirect on login page")); + const redirect = this.loadOpenIDScreen(); + if (redirect === null) { + throw new Error("Unable to redirect on login page."); + } + return redirect; } } } diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 520929eb..0220fca1 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -9,6 +9,7 @@ import { EnableCameraSceneName } from "../Login/EnableCameraScene"; import { LoginSceneName } from "../Login/LoginScene"; import { SelectCharacterSceneName } from "../Login/SelectCharacterScene"; import { GameScene } from "./GameScene"; +import { EmptySceneName } from "../Login/EmptyScene"; /** * This class should be responsible for any scene starting/stopping @@ -32,7 +33,14 @@ export class GameManager { public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise { this.scenePlugin = scenePlugin; - this.startRoom = await connectionManager.initGameConnexion(); + const result = await connectionManager.initGameConnexion(); + if (result instanceof URL) { + window.location.assign(result.toString()); + // window.location.assign is not immediate and Javascript keeps running after. + // so we need to redirect to an empty Phaser scene, waiting for the redirection to take place + return EmptySceneName; + } + this.startRoom = result; this.loadMap(this.startRoom); //If player name was not set show login scene with player name diff --git a/front/src/Phaser/Login/EmptyScene.ts b/front/src/Phaser/Login/EmptyScene.ts new file mode 100644 index 00000000..4511a160 --- /dev/null +++ b/front/src/Phaser/Login/EmptyScene.ts @@ -0,0 +1,17 @@ +import { Scene } from "phaser"; + +export const EmptySceneName = "EmptyScene"; + +export class EmptyScene extends Scene { + constructor() { + super({ + key: EmptySceneName, + }); + } + + preload() {} + + create() {} + + update(time: number, delta: number): void {} +} diff --git a/front/src/Phaser/Login/LoginScene.ts b/front/src/Phaser/Login/LoginScene.ts index 94f04db9..14cee6a1 100644 --- a/front/src/Phaser/Login/LoginScene.ts +++ b/front/src/Phaser/Login/LoginScene.ts @@ -28,7 +28,10 @@ export class LoginScene extends ResizableScene { gameManager.currentStartedRoom && gameManager.currentStartedRoom.authenticationMandatory ) { - connectionManager.loadOpenIDScreen(); + const redirect = connectionManager.loadOpenIDScreen(); + if (redirect !== null) { + window.location.assign(redirect.toString()); + } loginSceneVisibleIframeStore.set(true); } loginSceneVisibleStore.set(true); From 418a8cc6346859a92368cff6963fbffe58b44af7 Mon Sep 17 00:00:00 2001 From: Alexis Faizeau Date: Tue, 22 Mar 2022 17:47:09 +0100 Subject: [PATCH 29/32] Remove login name scene on logout button event --- front/src/Components/Menu/ProfileSubMenu.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/front/src/Components/Menu/ProfileSubMenu.svelte b/front/src/Components/Menu/ProfileSubMenu.svelte index 608b82e0..09237e36 100644 --- a/front/src/Components/Menu/ProfileSubMenu.svelte +++ b/front/src/Components/Menu/ProfileSubMenu.svelte @@ -44,7 +44,6 @@ async function logOut() { disableMenuStores(); - loginSceneVisibleStore.set(true); return connectionManager.logout(); } From 87412c3400321a53b06cda7a3df742ed76a54819 Mon Sep 17 00:00:00 2001 From: Alexis Faizeau Date: Wed, 23 Mar 2022 10:47:46 +0100 Subject: [PATCH 30/32] Add more checking on woka details --- front/src/Connexion/RoomConnection.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index b2ac2e22..ed982536 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -220,6 +220,7 @@ export class RoomConnection implements RoomConnection { this.socket.onmessage = (messageEvent) => { const arrayBuffer: ArrayBuffer = messageEvent.data; + const initCharacterLayers = characterLayers; const serverToClientMessage = ServerToClientMessageTsProto.decode(new Uint8Array(arrayBuffer)); //const message = ServerToClientMessage.deserializeBinary(new Uint8Array(arrayBuffer)); @@ -342,12 +343,12 @@ export class RoomConnection implements RoomConnection { this._userRoomToken = roomJoinedMessage.userRoomToken; // If one of the URLs sent to us does not exist, let's go to the Woka selection screen. - for (const characterLayer of roomJoinedMessage.characterLayer) { - if (!characterLayer.url) { - this.goToSelectYourWokaScene(); - this.closed = true; - break; - } + if ( + roomJoinedMessage.characterLayer.length !== initCharacterLayers.length || + roomJoinedMessage.characterLayer.find((layer) => !layer.url) + ) { + this.goToSelectYourWokaScene(); + this.closed = true; } if (this.closed) { From 14a68ade881f28c6066a0bd8d10674314166a838 Mon Sep 17 00:00:00 2001 From: Alexis Faizeau Date: Wed, 23 Mar 2022 16:59:21 +0100 Subject: [PATCH 31/32] Add z-index on embeds screens and action menu --- front/src/Components/ActionsMenu/ActionsMenu.svelte | 1 + .../Components/EmbedScreens/EmbedScreensContainer.svelte | 2 ++ front/src/Components/MainLayout.svelte | 9 +++++---- front/src/Components/VisitCard/VisitCard.svelte | 1 + 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/front/src/Components/ActionsMenu/ActionsMenu.svelte b/front/src/Components/ActionsMenu/ActionsMenu.svelte index c1eb317a..d6eda249 100644 --- a/front/src/Components/ActionsMenu/ActionsMenu.svelte +++ b/front/src/Components/ActionsMenu/ActionsMenu.svelte @@ -77,6 +77,7 @@ height: max-content !important; max-height: 40vh; margin-top: 200px; + z-index: 425; pointer-events: auto; font-family: "Press Start 2P"; diff --git a/front/src/Components/EmbedScreens/EmbedScreensContainer.svelte b/front/src/Components/EmbedScreens/EmbedScreensContainer.svelte index 058fc71c..d31aac89 100644 --- a/front/src/Components/EmbedScreens/EmbedScreensContainer.svelte +++ b/front/src/Components/EmbedScreens/EmbedScreensContainer.svelte @@ -18,5 +18,7 @@ display: flex; padding-top: 2%; height: 100%; + position: relative; + z-index: 200; } diff --git a/front/src/Components/MainLayout.svelte b/front/src/Components/MainLayout.svelte index f1200640..01e45322 100644 --- a/front/src/Components/MainLayout.svelte +++ b/front/src/Components/MainLayout.svelte @@ -54,6 +54,7 @@ }); +