diff --git a/contrib/docker/.env.prod.template b/contrib/docker/.env.prod.template index c0c10181..5e9adc87 100644 --- a/contrib/docker/.env.prod.template +++ b/contrib/docker/.env.prod.template @@ -1,20 +1,118 @@ +# Security +# + +SECRET_KEY= +ADMIN_API_TOKEN= + +# +# Networking +# + # The base domain DOMAIN=workadventure.localhost -DEBUG_MODE=false +# Subdomains +# MUST match the DOMAIN variable above +FRONT_HOST=front.workadventure.localhost +PUSHER_HOST=pusher.workadventure.localhost +BACK_HOST=api.workadventure.localhost +MAPS_HOST=maps.workadventure.localhost +ICON_HOST=icon.workadventure.localhost + +# SAAS admin panel +ADMIN_API_URL= + +# +# Basic configuration +# + +# The directory to store data in +DATA_DIR=./wa + +# The URL used by default, in the form: "/_/global/map/url.json" +START_ROOM_URL=/_/global/maps.workadventu.re/Floor0/floor0.json + +# If you want to have a contact page in your menu, +# you MUST set CONTACT_URL to the URL of the page that you want +CONTACT_URL= + +MAX_PER_GROUP=4 +MAX_USERNAME_LENGTH=8 +DISABLE_ANONYMOUS=false + +# The version of the docker image to use +# MUST uncomment "image" keys in the docker-compose file for it to be effective +VERSION=master + +TZ=Europe/Paris + +# +# Jitsi +# + JITSI_URL=meet.jit.si -# If your Jitsi environment has authentication set up, you MUST set JITSI_PRIVATE_MODE to "true" and you MUST pass a SECRET_JITSI_KEY to generate the JWT secret +# If your Jitsi environment has authentication set up, +# you MUST set JITSI_PRIVATE_MODE to "true" +# and you MUST pass a SECRET_JITSI_KEY to generate the JWT secret JITSI_PRIVATE_MODE=false JITSI_ISS= SECRET_JITSI_KEY= +# +# Turn/Stun +# + # URL of the TURN server (needed to "punch a hole" through some networks for P2P connections) TURN_SERVER= TURN_USER= TURN_PASSWORD= +# If your Turn server is configured to use the Turn REST API, you MUST put the shared auth secret here. +# If you are using Coturn, this is the value of the "static-auth-secret" parameter in your coturn config file. +# Keep empty if you are sharing hard coded / clear text credentials. +TURN_STATIC_AUTH_SECRET= +# URL of the STUN server +STUN_SERVER= -# The URL used by default, in the form: "/_/global/map/url.json" -START_ROOM_URL=/_/global/maps.workadventu.re/Floor0/floor0.json +# +# Certificate config +# # The email address used by Let's encrypt to send renewal warnings (compulsory) ACME_EMAIL= + +# +# Additional app configs +# Configuration for apps which are not workadventure itself +# + +# openID +OPID_CLIENT_ID= +OPID_CLIENT_SECRET= +OPID_CLIENT_ISSUER= +OPID_CLIENT_REDIRECT_URL= +OPID_LOGIN_SCREEN_PROVIDER=http://pusher.workadventure.localhost/login-screen +OPID_PROFILE_SCREEN_PROVIDER= + + +# +# Advanced configuration +# Generally does not need to be changed +# + +# Networking +HTTP_PORT=80 +HTTPS_PORT=443 + +# Workadventure settings +DISABLE_NOTIFICATIONS=false +SKIP_RENDER_OPTIMIZATIONS=false +STORE_VARIABLES_FOR_LOCAL_MAPS=true + +# Debugging options +DEBUG_MODE=false +LOG_LEVEL=WARN + +# Internal URLs +API_URL=back:50051 + +RESTART_POLICY=unless-stopped diff --git a/contrib/docker/docker-compose.prod.yaml b/contrib/docker/docker-compose.prod.yaml index 62be6749..80ed192b 100644 --- a/contrib/docker/docker-compose.prod.yaml +++ b/contrib/docker/docker-compose.prod.yaml @@ -1,114 +1,128 @@ -version: "3.3" +version: "3.5" services: reverse-proxy: - image: traefik:v2.3 + image: traefik:v2.6 command: - - --log.level=WARN - #- --api.insecure=true + - --log.level=${LOG_LEVEL} - --providers.docker - - --entryPoints.web.address=:80 + # Entry points + - --entryPoints.web.address=:${HTTP_PORT} - --entrypoints.web.http.redirections.entryPoint.to=websecure - --entrypoints.web.http.redirections.entryPoint.scheme=https - - --entryPoints.websecure.address=:443 + - --entryPoints.websecure.address=:${HTTPS_PORT} + # HTTP challenge - --certificatesresolvers.myresolver.acme.email=${ACME_EMAIL} - --certificatesresolvers.myresolver.acme.storage=/acme.json - # used during the challenge - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web + # Let's Encrypt's staging server + # uncomment during testing to avoid rate limiting + #- --certificatesresolvers.dnsresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory ports: - - "80:80" - - "443:443" - # The Web UI (enabled by --api.insecure=true) - #- "8080:8080" - depends_on: - - pusher - - front + - "${HTTP_PORT}:80" + - "${HTTPS_PORT}:443" volumes: - /var/run/docker.sock:/var/run/docker.sock - - ./acme.json:/acme.json - restart: unless-stopped + - ${DATA_DIR}/letsencrypt/acme.json:/acme.json + restart: ${RESTART_POLICY} front: build: context: ../.. dockerfile: front/Dockerfile - #image: thecodingmachine/workadventure-front:master + #image: thecodingmachine/workadventure-front:${VERSION} environment: - DEBUG_MODE: "$DEBUG_MODE" - JITSI_URL: $JITSI_URL - JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" - PUSHER_URL: //pusher.${DOMAIN} - ICON_URL: //icon.${DOMAIN} - TURN_SERVER: "${TURN_SERVER}" - TURN_USER: "${TURN_USER}" - TURN_PASSWORD: "${TURN_PASSWORD}" - START_ROOM_URL: "${START_ROOM_URL}" + - DEBUG_MODE + - JITSI_URL + - JITSI_PRIVATE_MODE + - PUSHER_URL=//${PUSHER_HOST} + - ICON_URL=//${ICON_HOST} + - TURN_SERVER + - TURN_USER + - TURN_PASSWORD + - TURN_STATIC_AUTH_SECRET + - STUN_SERVER + - START_ROOM_URL + - SKIP_RENDER_OPTIMIZATIONS + - MAX_PER_GROUP + - MAX_USERNAME_LENGTH + - DISABLE_ANONYMOUS + - DISABLE_NOTIFICATIONS labels: - - "traefik.http.routers.front.rule=Host(`play.${DOMAIN}`)" - - "traefik.http.routers.front.entryPoints=web,traefik" + - "traefik.http.routers.front.rule=Host(`${FRONT_HOST}`)" + - "traefik.http.routers.front.entryPoints=web" - "traefik.http.services.front.loadbalancer.server.port=80" - - "traefik.http.routers.front-ssl.rule=Host(`play.${DOMAIN}`)" + - "traefik.http.routers.front-ssl.rule=Host(`${FRONT_HOST}`)" - "traefik.http.routers.front-ssl.entryPoints=websecure" - - "traefik.http.routers.front-ssl.tls=true" - "traefik.http.routers.front-ssl.service=front" + - "traefik.http.routers.front-ssl.tls=true" - "traefik.http.routers.front-ssl.tls.certresolver=myresolver" - restart: unless-stopped + restart: ${RESTART_POLICY} pusher: build: context: ../.. dockerfile: pusher/Dockerfile - #image: thecodingmachine/workadventure-pusher:master + #image: thecodingmachine/workadventure-pusher:${VERSION} command: yarn run runprod environment: - SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" - SECRET_KEY: yourSecretKey - API_URL: back:50051 - JITSI_URL: $JITSI_URL - JITSI_ISS: $JITSI_ISS - FRONT_URL: https://play.${DOMAIN} + - SECRET_JITSI_KEY + - SECRET_KEY + - API_URL + - FRONT_URL=https://${FRONT_HOST} + - JITSI_URL + - JITSI_ISS + - DISABLE_ANONYMOUS labels: - - "traefik.http.routers.pusher.rule=Host(`pusher.${DOMAIN}`)" - - "traefik.http.routers.pusher.entryPoints=web,traefik" + - "traefik.http.routers.pusher.rule=Host(`${PUSHER_HOST}`)" + - "traefik.http.routers.pusher.entryPoints=web" - "traefik.http.services.pusher.loadbalancer.server.port=8080" - - "traefik.http.routers.pusher-ssl.rule=Host(`pusher.${DOMAIN}`)" + - "traefik.http.routers.pusher-ssl.rule=Host(${PUSHER_HOST}`)" - "traefik.http.routers.pusher-ssl.entryPoints=websecure" - - "traefik.http.routers.pusher-ssl.tls=true" - "traefik.http.routers.pusher-ssl.service=pusher" + - "traefik.http.routers.pusher-ssl.tls=true" - "traefik.http.routers.pusher-ssl.tls.certresolver=myresolver" - restart: unless-stopped + restart: ${RESTART_POLICY} back: build: context: ../.. dockerfile: back/Dockerfile - #image: thecodingmachine/workadventure-back:master + #image: thecodingmachine/workadventure-back:${VERSION} command: yarn run runprod environment: - SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" - ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" - ADMIN_API_URL: "$ADMIN_API_URL" - JITSI_URL: $JITSI_URL - JITSI_ISS: $JITSI_ISS + - SECRET_JITSI_KEY + - SECRET_KEY + - ADMIN_API_TOKEN + - ADMIN_API_URL + - TURN_SERVER + - TURN_USER + - TURN_PASSWORD + - TURN_STATIC_AUTH_SECRET + - STUN_SERVER + - JITSI_URL + - JITSI_ISS + - MAX_PER_GROUP + - STORE_VARIABLES_FOR_LOCAL_MAPS labels: - - "traefik.http.routers.back.rule=Host(`api.${DOMAIN}`)" + - "traefik.http.routers.back.rule=Host(`${BACK_HOST}`)" - "traefik.http.routers.back.entryPoints=web" - "traefik.http.services.back.loadbalancer.server.port=8080" - - "traefik.http.routers.back-ssl.rule=Host(`api.${DOMAIN}`)" + - "traefik.http.routers.back-ssl.rule=Host(`${BACK_HOST}`)" - "traefik.http.routers.back-ssl.entryPoints=websecure" - - "traefik.http.routers.back-ssl.tls=true" - "traefik.http.routers.back-ssl.service=back" + - "traefik.http.routers.back-ssl.tls=true" - "traefik.http.routers.back-ssl.tls.certresolver=myresolver" - restart: unless-stopped + restart: ${RESTART_POLICY} icon: image: matthiasluedtke/iconserver:v3.13.0 labels: - - "traefik.http.routers.icon.rule=Host(`icon.${DOMAIN}`)" + - "traefik.http.routers.icon.rule=Host(`${ICON_HOST}`)" - "traefik.http.routers.icon.entryPoints=web,traefik" - "traefik.http.services.icon.loadbalancer.server.port=8080" - - "traefik.http.routers.icon-ssl.rule=Host(`icon.${DOMAIN}`)" + - "traefik.http.routers.icon-ssl.rule=Host(`${ICON_HOST}`)" - "traefik.http.routers.icon-ssl.entryPoints=websecure" - - "traefik.http.routers.icon-ssl.tls=true" - "traefik.http.routers.icon-ssl.service=icon" + - "traefik.http.routers.icon-ssl.tls=true" - "traefik.http.routers.icon-ssl.tls.certresolver=myresolver" diff --git a/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte b/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte index b71a35c0..b01dbf0a 100644 --- a/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte +++ b/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte @@ -2,9 +2,10 @@ import { onMount } from "svelte"; import { ICON_URL } from "../../Enum/EnvironmentVariable"; - import { coWebsitesNotAsleep, mainCoWebsite } from "../../Stores/CoWebsiteStore"; + import { mainCoWebsite } from "../../Stores/CoWebsiteStore"; import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore"; - import type { CoWebsite } from "../../WebRtc/CoWebsiteManager"; + import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite"; + import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite"; import { iframeStates } from "../../WebRtc/CoWebsiteManager"; import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; @@ -14,16 +15,15 @@ let icon: HTMLImageElement; let iconLoaded = false; - let state = coWebsite.state; - - const coWebsiteUrl = coWebsite.iframe.src; - const urlObject = new URL(coWebsiteUrl); + let state = coWebsite.getStateSubscriber(); + let isJitsi: boolean = coWebsite instanceof JitsiCoWebsite; + const mainState = coWebsiteManager.getMainStateSubscriber(); onMount(() => { - icon.src = coWebsite.jitsi + icon.src = isJitsi ? "/resources/logos/meet.svg" - : `${ICON_URL}/icon?url=${urlObject.hostname}&size=64..96..256&fallback_icon_color=14304c`; - icon.alt = coWebsite.altMessage ?? urlObject.hostname; + : `${ICON_URL}/icon?url=${coWebsite.getUrl().hostname}&size=64..96..256&fallback_icon_color=14304c`; + icon.alt = coWebsite.getUrl().hostname; icon.onload = () => { iconLoaded = true; }; @@ -33,21 +33,24 @@ if (vertical) { coWebsiteManager.goToMain(coWebsite); } else if ($mainCoWebsite) { - if ($mainCoWebsite.iframe.id === coWebsite.iframe.id) { - const coWebsites = $coWebsitesNotAsleep; - const newMain = $highlightedEmbedScreen ?? coWebsites.length > 1 ? coWebsites[1] : undefined; - if (newMain && newMain.iframe.id !== $mainCoWebsite.iframe.id) { - coWebsiteManager.goToMain(newMain); - } else if (coWebsiteManager.getMainState() === iframeStates.closed) { + if ($mainCoWebsite.getId() === coWebsite.getId()) { + if (coWebsiteManager.getMainState() === iframeStates.closed) { coWebsiteManager.displayMain(); + } else if ($highlightedEmbedScreen?.type === "cowebsite") { + coWebsiteManager.goToMain($highlightedEmbedScreen.embed); } else { coWebsiteManager.hideMain(); } } else { - highlightedEmbedScreen.toggleHighlight({ - type: "cowebsite", - embed: coWebsite, - }); + if (coWebsiteManager.getMainState() === iframeStates.closed) { + coWebsiteManager.goToMain(coWebsite); + coWebsiteManager.displayMain(); + } else { + highlightedEmbedScreen.toggleHighlight({ + type: "cowebsite", + embed: coWebsite, + }); + } } } @@ -65,11 +68,14 @@ let isHighlight: boolean = false; let isMain: boolean = false; $: { - isMain = $mainCoWebsite !== undefined && $mainCoWebsite.iframe === coWebsite.iframe; + isMain = + $mainState === iframeStates.opened && + $mainCoWebsite !== undefined && + $mainCoWebsite.getId() === coWebsite.getId(); isHighlight = $highlightedEmbedScreen !== null && $highlightedEmbedScreen.type === "cowebsite" && - $highlightedEmbedScreen.embed.iframe === coWebsite.iframe; + $highlightedEmbedScreen.embed.getId() === coWebsite.getId(); } @@ -86,7 +92,7 @@ 0}
- {#each [...$coWebsites.values()] as coWebsite, index (coWebsite.iframe.id)} + {#each [...$coWebsites.values()] as coWebsite, index (coWebsite.getId())} {/each}
diff --git a/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte b/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte index f4a9f939..dbf7ee71 100644 --- a/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte +++ b/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte @@ -9,13 +9,11 @@ function closeCoWebsite() { if ($highlightedEmbedScreen?.type === "cowebsite") { - if ($highlightedEmbedScreen.embed.closable) { - coWebsiteManager.closeCoWebsite($highlightedEmbedScreen.embed).catch(() => { - console.error("Error during co-website highlighted closing"); - }); + if ($highlightedEmbedScreen.embed.isClosable()) { + coWebsiteManager.closeCoWebsite($highlightedEmbedScreen.embed); } else { - coWebsiteManager.unloadCoWebsite($highlightedEmbedScreen.embed).catch(() => { - console.error("Error during co-website highlighted unloading"); + coWebsiteManager.unloadCoWebsite($highlightedEmbedScreen.embed).catch((err) => { + console.error("Cannot unload co-website", err); }); } } @@ -68,9 +66,9 @@ /> {/key} {:else if $highlightedEmbedScreen.type === "cowebsite"} - {#key $highlightedEmbedScreen.embed.iframe.id} + {#key $highlightedEmbedScreen.embed.getId()}
diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index 8e377ae7..3df3cb01 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -121,7 +121,7 @@ export class GameMap { return []; } - public getCollisionsGrid(): number[][] { + public getCollisionGrid(): number[][] { const grid: number[][] = []; for (let y = 0; y < this.map.height; y += 1) { const row: number[] = []; @@ -333,6 +333,9 @@ export class GameMap { private isCollidingAt(x: number, y: number): boolean { for (const layer of this.phaserLayers) { + if (!layer.visible) { + continue; + } if (layer.getTileAt(x, y)?.properties[GameMapProperties.COLLIDES]) { return true; } diff --git a/front/src/Phaser/Game/GameMapPropertiesListener.ts b/front/src/Phaser/Game/GameMapPropertiesListener.ts index 497b6cbc..103cc4bc 100644 --- a/front/src/Phaser/Game/GameMapPropertiesListener.ts +++ b/front/src/Phaser/Game/GameMapPropertiesListener.ts @@ -1,7 +1,6 @@ import type { GameScene } from "./GameScene"; import type { GameMap } from "./GameMap"; import { scriptUtils } from "../../Api/ScriptUtils"; -import type { CoWebsite } from "../../WebRtc/CoWebsiteManager"; import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore"; import { localUserStore } from "../../Connexion/LocalUserStore"; @@ -9,17 +8,19 @@ import { get } from "svelte/store"; import { ON_ACTION_TRIGGER_BUTTON, ON_ICON_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager"; import type { ITiledMapLayer } from "../Map/ITiledMap"; import { GameMapProperties } from "./GameMapProperties"; - -enum OpenCoWebsiteState { - ASLEEP, - OPENED, - MUST_BE_CLOSE, -} +import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite"; +import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite"; +import { jitsiFactory } from "../../WebRtc/JitsiFactory"; +import { JITSI_PRIVATE_MODE, JITSI_URL } from "../../Enum/EnvironmentVariable"; +import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite"; +import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore"; +import { iframeListener } from "../../Api/IframeListener"; +import { Room } from "../../Connexion/Room"; +import LL from "../../i18n/i18n-svelte"; interface OpenCoWebsite { actionId: string; coWebsite?: CoWebsite; - state: OpenCoWebsiteState; } export class GameMapPropertiesListener { @@ -29,6 +30,7 @@ export class GameMapPropertiesListener { constructor(private scene: GameScene, private gameMap: GameMap) {} register() { + // Website on new tab this.gameMap.onPropertyChange(GameMapProperties.OPEN_TAB, (newValue, oldValue, allProps) => { if (newValue === undefined) { layoutManagerActionStore.removeAction("openTab"); @@ -39,7 +41,7 @@ export class GameMapPropertiesListener { if (forceTrigger || openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(GameMapProperties.OPEN_WEBSITE_TRIGGER_MESSAGE); if (message === undefined) { - message = "Press SPACE or touch here to open web site in new tab"; + message = get(LL).trigger.newTab(); } layoutManagerActionStore.addAction({ uuid: "openTab", @@ -54,6 +56,129 @@ export class GameMapPropertiesListener { } }); + // Jitsi room + this.gameMap.onPropertyChange(GameMapProperties.JITSI_ROOM, (newValue, oldValue, allProps) => { + if (newValue === undefined) { + layoutManagerActionStore.removeAction("jitsi"); + coWebsiteManager.getCoWebsites().forEach((coWebsite) => { + if (coWebsite instanceof JitsiCoWebsite) { + coWebsiteManager.closeCoWebsite(coWebsite); + } + }); + } else { + const openJitsiRoomFunction = () => { + const roomName = jitsiFactory.getRoomName(newValue.toString(), this.scene.instance); + const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined; + + if (JITSI_PRIVATE_MODE && !jitsiUrl) { + const adminTag = allProps.get(GameMapProperties.JITSI_ADMIN_ROOM_TAG) as string | undefined; + + this.scene.connection?.emitQueryJitsiJwtMessage(roomName, adminTag); + } else { + let domain = jitsiUrl || JITSI_URL; + if (domain === undefined) { + throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map."); + } + + if (domain.substring(0, 7) !== "http://" && domain.substring(0, 8) !== "https://") { + domain = `${location.protocol}//${domain}`; + } + + const coWebsite = new JitsiCoWebsite(new URL(domain), false, undefined, undefined, false); + + coWebsiteManager.addCoWebsiteToStore(coWebsite, 0); + this.scene.initialiseJitsi(coWebsite, roomName, undefined); + } + layoutManagerActionStore.removeAction("jitsi"); + }; + + const jitsiTriggerValue = allProps.get(GameMapProperties.JITSI_TRIGGER); + const forceTrigger = localUserStore.getForceCowebsiteTrigger(); + if (forceTrigger || jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { + let message = allProps.get(GameMapProperties.JITSI_TRIGGER_MESSAGE); + if (message === undefined) { + message = get(LL).trigger.jitsiRoom(); + } + layoutManagerActionStore.addAction({ + uuid: "jitsi", + type: "message", + message: message, + callback: () => openJitsiRoomFunction(), + userInputManager: this.scene.userInputManager, + }); + } else { + openJitsiRoomFunction(); + } + } + }); + + this.gameMap.onPropertyChange(GameMapProperties.EXIT_SCENE_URL, (newValue, oldValue) => { + if (newValue) { + this.scene + .onMapExit( + Room.getRoomPathFromExitSceneUrl( + newValue as string, + window.location.toString(), + this.scene.MapUrlFile + ) + ) + .catch((e) => console.error(e)); + } else { + setTimeout(() => { + layoutManagerActionStore.removeAction("roomAccessDenied"); + }, 2000); + } + }); + + this.gameMap.onPropertyChange(GameMapProperties.EXIT_URL, (newValue, oldValue) => { + if (newValue) { + this.scene + .onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString())) + .catch((e) => console.error(e)); + } else { + setTimeout(() => { + layoutManagerActionStore.removeAction("roomAccessDenied"); + }, 2000); + } + }); + + this.gameMap.onPropertyChange(GameMapProperties.SILENT, (newValue, oldValue) => { + if (newValue === undefined || newValue === false || newValue === "") { + this.scene.connection?.setSilent(false); + this.scene.CurrentPlayer.noSilent(); + } else { + this.scene.connection?.setSilent(true); + this.scene.CurrentPlayer.isSilent(); + } + }); + + this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO, (newValue, oldValue, allProps) => { + const volume = allProps.get(GameMapProperties.AUDIO_VOLUME) as number | undefined; + const loop = allProps.get(GameMapProperties.AUDIO_LOOP) as boolean | undefined; + newValue === undefined + ? audioManagerFileStore.unloadAudio() + : audioManagerFileStore.playAudio(newValue, this.scene.getMapDirUrl(), volume, loop); + audioManagerVisibilityStore.set(!(newValue === undefined)); + }); + + // TODO: This legacy property should be removed at some point + this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO_LOOP, (newValue, oldValue) => { + newValue === undefined + ? audioManagerFileStore.unloadAudio() + : audioManagerFileStore.playAudio(newValue, this.scene.getMapDirUrl(), undefined, true); + audioManagerVisibilityStore.set(!(newValue === undefined)); + }); + + // TODO: Legacy functionnality replace by layer change + this.gameMap.onPropertyChange(GameMapProperties.ZONE, (newValue, oldValue) => { + if (oldValue) { + iframeListener.sendLeaveEvent(oldValue as string); + } + if (newValue) { + iframeListener.sendEnterEvent(newValue as string); + } + }); + // Open a new co-website by the property. this.gameMap.onEnterLayer((newLayers) => { const handler = () => { @@ -106,49 +231,33 @@ export class GameMapPropertiesListener { return; } - this.coWebsitesOpenByLayer.set(layer, { + const coWebsiteOpen: OpenCoWebsite = { actionId: actionId, - coWebsite: undefined, - state: OpenCoWebsiteState.ASLEEP, - }); + }; + + this.coWebsitesOpenByLayer.set(layer, coWebsiteOpen); const loadCoWebsiteFunction = (coWebsite: CoWebsite) => { - coWebsiteManager - .loadCoWebsite(coWebsite) - .then((coWebsite) => { - const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer); - if (coWebsiteOpen && coWebsiteOpen.state === OpenCoWebsiteState.MUST_BE_CLOSE) { - coWebsiteManager.closeCoWebsite(coWebsite).catch(() => { - console.error("Error during a co-website closing"); - }); - this.coWebsitesOpenByLayer.delete(layer); - this.coWebsitesActionTriggerByLayer.delete(layer); - } else { - this.coWebsitesOpenByLayer.set(layer, { - actionId, - coWebsite, - state: OpenCoWebsiteState.OPENED, - }); - } - }) - .catch(() => { - console.error("Error during loading a co-website: " + coWebsite.url); - }); + coWebsiteManager.loadCoWebsite(coWebsite).catch(() => { + console.error("Error during loading a co-website: " + coWebsite.getUrl()); + }); layoutManagerActionStore.removeAction(actionId); }; const openCoWebsiteFunction = () => { - const coWebsite = coWebsiteManager.addCoWebsite( - openWebsiteProperty ?? "", - this.scene.MapUrlFile, + const coWebsite = new SimpleCoWebsite( + new URL(openWebsiteProperty ?? "", this.scene.MapUrlFile), allowApiProperty, websitePolicyProperty, websiteWidthProperty, - websitePositionProperty, false ); + coWebsiteOpen.coWebsite = coWebsite; + + coWebsiteManager.addCoWebsiteToStore(coWebsite, websitePositionProperty); + loadCoWebsiteFunction(coWebsite); }; @@ -157,7 +266,7 @@ export class GameMapPropertiesListener { websiteTriggerProperty === ON_ACTION_TRIGGER_BUTTON ) { if (!websiteTriggerMessageProperty) { - websiteTriggerMessageProperty = "Press SPACE or touch here to open web site"; + websiteTriggerMessageProperty = get(LL).trigger.cowebsite(); } this.coWebsitesActionTriggerByLayer.set(layer, actionId); @@ -170,21 +279,17 @@ export class GameMapPropertiesListener { userInputManager: this.scene.userInputManager, }); } else if (websiteTriggerProperty === ON_ICON_TRIGGER_BUTTON) { - const coWebsite = coWebsiteManager.addCoWebsite( - openWebsiteProperty, - this.scene.MapUrlFile, + const coWebsite = new SimpleCoWebsite( + new URL(openWebsiteProperty ?? "", this.scene.MapUrlFile), allowApiProperty, websitePolicyProperty, websiteWidthProperty, - websitePositionProperty, false ); - const ObjectByLayer = this.coWebsitesOpenByLayer.get(layer); + coWebsiteOpen.coWebsite = coWebsite; - if (ObjectByLayer) { - ObjectByLayer.coWebsite = coWebsite; - } + coWebsiteManager.addCoWebsiteToStore(coWebsite, websitePositionProperty); } if (!websiteTriggerProperty) { @@ -228,12 +333,10 @@ export class GameMapPropertiesListener { return; } - if (coWebsiteOpen.state === OpenCoWebsiteState.ASLEEP) { - coWebsiteOpen.state = OpenCoWebsiteState.MUST_BE_CLOSE; - } + const coWebsite = coWebsiteOpen.coWebsite; - if (coWebsiteOpen.coWebsite !== undefined) { - coWebsiteManager.closeCoWebsite(coWebsiteOpen.coWebsite).catch((e) => console.error(e)); + if (coWebsite) { + coWebsiteManager.closeCoWebsite(coWebsite); } this.coWebsitesOpenByLayer.delete(layer); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 40baf0c5..87ca28af 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -5,7 +5,7 @@ import { get, Unsubscriber } from "svelte/store"; import { userMessageManager } from "../../Administration/UserMessageManager"; import { connectionManager } from "../../Connexion/ConnectionManager"; -import { CoWebsite, coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; +import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; import { urlManager } from "../../Url/UrlManager"; import { mediaManager } from "../../WebRtc/MediaManager"; import { UserInputManager } from "../UserInput/UserInputManager"; @@ -20,9 +20,8 @@ import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager"; import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; -import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager"; import { iframeListener } from "../../Api/IframeListener"; -import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; +import { DEBUG_MODE, JITSI_URL, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable"; import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils"; import { Room } from "../../Connexion/Room"; import { jitsiFactory } from "../../WebRtc/JitsiFactory"; @@ -76,7 +75,7 @@ import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore"; import { userIsAdminStore } from "../../Stores/GameStore"; import { contactPageStore } from "../../Stores/MenuStore"; import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent"; -import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore"; +import { audioManagerFileStore } from "../../Stores/AudioManagerStore"; import EVENT_TYPE = Phaser.Scenes.Events; import Texture = Phaser.Textures.Texture; @@ -94,6 +93,9 @@ import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandle import { locale } from "../../i18n/i18n-svelte"; import { StringUtils } from "../../Utils/StringUtils"; import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore"; +import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite"; +import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite"; +import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite"; export interface GameSceneInitInterface { initPosition: PointInterface | null; reconnecting: boolean; @@ -564,7 +566,7 @@ export class GameScene extends DirtyScene { this.pathfindingManager = new PathfindingManager( this, - this.gameMap.getCollisionsGrid(), + this.gameMap.getCollisionGrid(), this.gameMap.getTileDimensions() ); @@ -582,7 +584,7 @@ export class GameScene extends DirtyScene { this.pathfindingManager = new PathfindingManager( this, - this.gameMap.getCollisionsGrid(), + this.gameMap.getCollisionGrid(), this.gameMap.getTileDimensions() ); @@ -633,7 +635,6 @@ export class GameScene extends DirtyScene { ); new GameMapPropertiesListener(this, this.gameMap).register(); - this.triggerOnMapLayerPropertyChange(); if (!this.room.isDisconnected()) { this.scene.sleep(); @@ -800,7 +801,19 @@ export class GameScene extends DirtyScene { * Triggered when we receive the JWT token to connect to Jitsi */ this.connection.sendJitsiJwtMessageStream.subscribe((message) => { - this.startJitsi(message.jitsiRoom, message.jwt); + if (!JITSI_URL) { + throw new Error("Missing JITSI_URL environment variable."); + } + + let domain = JITSI_URL; + + if (domain.substring(0, 7) !== "http://" && domain.substring(0, 8) !== "https://") { + domain = `${location.protocol}//${domain}`; + } + + const coWebsite = new JitsiCoWebsite(new URL(domain), false, undefined, undefined, false); + coWebsiteManager.addCoWebsiteToStore(coWebsite, 0); + this.initialiseJitsi(coWebsite, message.jitsiRoom, message.jwt); }); this.messageSubscription = this.connection.worldFullMessageStream.subscribe((message) => { @@ -937,103 +950,6 @@ export class GameScene extends DirtyScene { } } - private triggerOnMapLayerPropertyChange() { - this.gameMap.onPropertyChange(GameMapProperties.EXIT_SCENE_URL, (newValue, oldValue) => { - if (newValue) { - this.onMapExit( - Room.getRoomPathFromExitSceneUrl(newValue as string, window.location.toString(), this.MapUrlFile) - ).catch((e) => console.error(e)); - } else { - setTimeout(() => { - layoutManagerActionStore.removeAction("roomAccessDenied"); - }, 2000); - } - }); - this.gameMap.onPropertyChange(GameMapProperties.EXIT_URL, (newValue, oldValue) => { - if (newValue) { - this.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString())).catch((e) => - console.error(e) - ); - } else { - setTimeout(() => { - layoutManagerActionStore.removeAction("roomAccessDenied"); - }, 2000); - } - }); - - this.gameMap.onPropertyChange(GameMapProperties.JITSI_ROOM, (newValue, oldValue, allProps) => { - if (newValue === undefined) { - layoutManagerActionStore.removeAction("jitsi"); - this.stopJitsi(); - } else { - const openJitsiRoomFunction = () => { - const roomName = jitsiFactory.getRoomName(newValue.toString(), this.instance); - const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined; - if (JITSI_PRIVATE_MODE && !jitsiUrl) { - const adminTag = allProps.get(GameMapProperties.JITSI_ADMIN_ROOM_TAG) as string | undefined; - - this.connection?.emitQueryJitsiJwtMessage(roomName, adminTag); - } else { - this.startJitsi(roomName, undefined); - } - layoutManagerActionStore.removeAction("jitsi"); - }; - - const jitsiTriggerValue = allProps.get(GameMapProperties.JITSI_TRIGGER); - const forceTrigger = localUserStore.getForceCowebsiteTrigger(); - if (forceTrigger || jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { - let message = allProps.get(GameMapProperties.JITSI_TRIGGER_MESSAGE); - if (message === undefined) { - message = "Press SPACE or touch here to enter Jitsi Meet room"; - } - layoutManagerActionStore.addAction({ - uuid: "jitsi", - type: "message", - message: message, - callback: () => openJitsiRoomFunction(), - userInputManager: this.userInputManager, - }); - } else { - openJitsiRoomFunction(); - } - } - }); - this.gameMap.onPropertyChange(GameMapProperties.SILENT, (newValue, oldValue) => { - if (newValue === undefined || newValue === false || newValue === "") { - this.connection?.setSilent(false); - this.CurrentPlayer.noSilent(); - } else { - this.connection?.setSilent(true); - this.CurrentPlayer.isSilent(); - } - }); - this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO, (newValue, oldValue, allProps) => { - const volume = allProps.get(GameMapProperties.AUDIO_VOLUME) as number | undefined; - const loop = allProps.get(GameMapProperties.AUDIO_LOOP) as boolean | undefined; - newValue === undefined - ? audioManagerFileStore.unloadAudio() - : audioManagerFileStore.playAudio(newValue, this.getMapDirUrl(), volume, loop); - audioManagerVisibilityStore.set(!(newValue === undefined)); - }); - // TODO: This legacy property should be removed at some point - this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO_LOOP, (newValue, oldValue) => { - newValue === undefined - ? audioManagerFileStore.unloadAudio() - : audioManagerFileStore.playAudio(newValue, this.getMapDirUrl(), undefined, true); - audioManagerVisibilityStore.set(!(newValue === undefined)); - }); - - // TODO: Legacy functionnality replace by layer change - this.gameMap.onPropertyChange(GameMapProperties.ZONE, (newValue, oldValue) => { - if (oldValue) { - iframeListener.sendLeaveEvent(oldValue as string); - } - if (newValue) { - iframeListener.sendEnterEvent(newValue as string); - } - }); - } - private listenToIframeEvents(): void { this.iframeSubscriptionList = []; this.iframeSubscriptionList.push( @@ -1266,13 +1182,11 @@ ${escapedMessage} throw new Error("Unknown query source"); } - const coWebsite = coWebsiteManager.addCoWebsite( - openCoWebsite.url, - iframeListener.getBaseUrlFromSource(source), + const coWebsite: SimpleCoWebsite = new SimpleCoWebsite( + new URL(openCoWebsite.url, iframeListener.getBaseUrlFromSource(source)), openCoWebsite.allowApi, openCoWebsite.allowPolicy, openCoWebsite.widthPercent, - openCoWebsite.position, openCoWebsite.closable ?? true ); @@ -1281,7 +1195,7 @@ ${escapedMessage} } return { - id: coWebsite.iframe.id, + id: coWebsite.getId(), }; }); @@ -1290,27 +1204,23 @@ ${escapedMessage} return coWebsites.map((coWebsite: CoWebsite) => { return { - id: coWebsite.iframe.id, + id: coWebsite.getId(), }; }); }); - iframeListener.registerAnswerer("closeCoWebsite", async (coWebsiteId) => { + iframeListener.registerAnswerer("closeCoWebsite", (coWebsiteId) => { const coWebsite = coWebsiteManager.getCoWebsiteById(coWebsiteId); if (!coWebsite) { throw new Error("Unknown co-website"); } - return coWebsiteManager.closeCoWebsite(coWebsite).catch((error) => { - throw new Error("Error on closing co-website"); - }); + return coWebsiteManager.closeCoWebsite(coWebsite); }); - iframeListener.registerAnswerer("closeCoWebsites", async () => { - return await coWebsiteManager.closeCoWebsites().catch((error) => { - throw new Error("Error on closing all co-websites"); - }); + iframeListener.registerAnswerer("closeCoWebsites", () => { + return coWebsiteManager.closeCoWebsites(); }); iframeListener.registerAnswerer("getMapData", () => { @@ -1395,7 +1305,7 @@ ${escapedMessage} //Create new colliders with the new GameMap this.createCollisionWithPlayer(); //Create new trigger with the new GameMap - this.triggerOnMapLayerPropertyChange(); + new GameMapPropertiesListener(this, this.gameMap).register(); resolve(newFirstgid); }); }); @@ -1508,14 +1418,15 @@ ${escapedMessage} phaserLayers[i].setCollisionByProperty({ collides: true }, visible); } } + this.pathfindingManager.setCollisionGrid(this.gameMap.getCollisionGrid()); this.markDirty(); } - private getMapDirUrl(): string { - return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/")); + public getMapDirUrl(): string { + return this.MapUrlFile.substring(0, this.MapUrlFile.lastIndexOf("/")); } - private async onMapExit(roomUrl: URL) { + public async onMapExit(roomUrl: URL) { if (this.mapTransitioning) return; this.mapTransitioning = true; @@ -1574,14 +1485,13 @@ ${escapedMessage} public cleanupClosingScene(): void { // stop playing audio, close any open website, stop any open Jitsi - coWebsiteManager.closeCoWebsites().catch((e) => console.error(e)); + coWebsiteManager.closeCoWebsites(); // Stop the script, if any const scripts = this.getScriptUrls(this.mapFile); for (const script of scripts) { iframeListener.unregisterScript(script); } - this.stopJitsi(); audioManagerFileStore.unloadAudio(); // We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map. this.connection?.closeConnection(); @@ -2136,7 +2046,7 @@ ${escapedMessage} mediaManager.hideMyCamera(); } - public startJitsi(roomName: string, jwt?: string): void { + public initialiseJitsi(coWebsite: JitsiCoWebsite, roomName: string, jwt?: string): void { const allProps = this.gameMap.getCurrentProperties(); const jitsiConfig = this.safeParseJSONstring( allProps.get(GameMapProperties.JITSI_CONFIG) as string | undefined, @@ -2148,20 +2058,15 @@ ${escapedMessage} ); const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined; - jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl).catch(() => { - console.error("Cannot start a Jitsi co-website"); + coWebsite.setJitsiLoadPromise(() => { + return jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl); }); - this.disableMediaBehaviors(); - analyticsClient.enteredJitsi(roomName, this.room.id); - } - public stopJitsi(): void { - const coWebsite = coWebsiteManager.searchJitsi(); - if (coWebsite) { - coWebsiteManager.closeCoWebsite(coWebsite).catch((e) => { - console.error("Error during Jitsi co-website closing", e); - }); - } + coWebsiteManager.loadCoWebsite(coWebsite).catch((err) => { + console.error(err); + }); + + analyticsClient.enteredJitsi(roomName, this.room.id); } //todo: put this into an 'orchestrator' scene (EntryScene?) diff --git a/front/src/Stores/CoWebsiteStore.ts b/front/src/Stores/CoWebsiteStore.ts index 4227c405..c4ed2f65 100644 --- a/front/src/Stores/CoWebsiteStore.ts +++ b/front/src/Stores/CoWebsiteStore.ts @@ -1,5 +1,5 @@ -import { derived, get, writable } from "svelte/store"; -import type { CoWebsite } from "../WebRtc/CoWebsiteManager"; +import { derived, writable } from "svelte/store"; +import type { CoWebsite } from "../WebRtc/CoWebsite/CoWesbite"; function createCoWebsiteStore() { const { subscribe, set, update } = writable(Array()); @@ -9,7 +9,7 @@ function createCoWebsiteStore() { return { subscribe, add: (coWebsite: CoWebsite, position?: number) => { - coWebsite.state.subscribe((value) => { + coWebsite.getStateSubscriber().subscribe((value) => { update((currentArray) => currentArray); }); @@ -31,7 +31,7 @@ function createCoWebsiteStore() { }, remove: (coWebsite: CoWebsite) => { update((currentArray) => [ - ...currentArray.filter((currentCoWebsite) => currentCoWebsite.iframe.id !== coWebsite.iframe.id), + ...currentArray.filter((currentCoWebsite) => currentCoWebsite.getId() !== coWebsite.getId()), ]); }, empty: () => { @@ -43,9 +43,9 @@ function createCoWebsiteStore() { export const coWebsites = createCoWebsiteStore(); export const coWebsitesNotAsleep = derived([coWebsites], ([$coWebsites]) => - $coWebsites.filter((coWebsite) => get(coWebsite.state) !== "asleep") + $coWebsites.filter((coWebsite) => coWebsite.getState() !== "asleep") ); export const mainCoWebsite = derived([coWebsites], ([$coWebsites]) => - $coWebsites.find((coWebsite) => get(coWebsite.state) !== "asleep") + $coWebsites.find((coWebsite) => coWebsite.getState() !== "asleep") ); diff --git a/front/src/Stores/EmbedScreensStore.ts b/front/src/Stores/EmbedScreensStore.ts index 0db7c675..724733b3 100644 --- a/front/src/Stores/EmbedScreensStore.ts +++ b/front/src/Stores/EmbedScreensStore.ts @@ -1,5 +1,5 @@ import { derived, get, writable } from "svelte/store"; -import type { CoWebsite } from "../WebRtc/CoWebsiteManager"; +import type { CoWebsite } from "../WebRtc/CoWebsite/CoWesbite"; import { LayoutMode } from "../WebRtc/LayoutManager"; import { coWebsites } from "./CoWebsiteStore"; import { Streamable, streamableCollectionStore } from "./StreamableCollectionStore"; @@ -31,7 +31,7 @@ function createHighlightedEmbedScreenStore() { embedScreen.type !== currentEmbedScreen.type || (embedScreen.type === "cowebsite" && currentEmbedScreen.type === "cowebsite" && - embedScreen.embed.iframe.id !== currentEmbedScreen.embed.iframe.id) || + embedScreen.embed.getId() !== currentEmbedScreen.embed.getId()) || (embedScreen.type === "streamable" && currentEmbedScreen.type === "streamable" && embedScreen.embed.uniqueId !== currentEmbedScreen.embed.uniqueId) diff --git a/front/src/Utils/PathfindingManager.ts b/front/src/Utils/PathfindingManager.ts index 71205070..c5057ed8 100644 --- a/front/src/Utils/PathfindingManager.ts +++ b/front/src/Utils/PathfindingManager.ts @@ -19,6 +19,10 @@ export class PathfindingManager { this.setEasyStarGrid(collisionsGrid); } + public setCollisionGrid(collisionGrid: number[][]): void { + this.setEasyStarGrid(collisionGrid); + } + public async findPath( start: { x: number; y: number }, end: { x: number; y: number }, diff --git a/front/src/WebRtc/CoWebsite/CoWesbite.ts b/front/src/WebRtc/CoWebsite/CoWesbite.ts new file mode 100644 index 00000000..50ce3b9f --- /dev/null +++ b/front/src/WebRtc/CoWebsite/CoWesbite.ts @@ -0,0 +1,17 @@ +import type CancelablePromise from "cancelable-promise"; +import type { Readable, Writable } from "svelte/store"; + +export type CoWebsiteState = "asleep" | "loading" | "ready"; + +export interface CoWebsite { + getId(): string; + getUrl(): URL; + getState(): CoWebsiteState; + getStateSubscriber(): Readable; + getIframe(): HTMLIFrameElement | undefined; + getLoadIframe(): CancelablePromise | undefined; + getWidthPercent(): number | undefined; + isClosable(): boolean; + load(): CancelablePromise; + unload(): Promise; +} diff --git a/front/src/WebRtc/CoWebsite/JitsiCoWebsite.ts b/front/src/WebRtc/CoWebsite/JitsiCoWebsite.ts new file mode 100644 index 00000000..a09b2bcb --- /dev/null +++ b/front/src/WebRtc/CoWebsite/JitsiCoWebsite.ts @@ -0,0 +1,49 @@ +import CancelablePromise from "cancelable-promise"; +import { gameManager } from "../../Phaser/Game/GameManager"; +import { jitsiFactory } from "../JitsiFactory"; +import { SimpleCoWebsite } from "./SimpleCoWebsite"; + +export class JitsiCoWebsite extends SimpleCoWebsite { + private jitsiLoadPromise?: () => CancelablePromise; + + setJitsiLoadPromise(promise: () => CancelablePromise): void { + this.jitsiLoadPromise = promise; + } + + load(): CancelablePromise { + return new CancelablePromise((resolve, reject, cancel) => { + this.state.set("loading"); + + gameManager.getCurrentGameScene().disableMediaBehaviors(); + + if (!this.jitsiLoadPromise) { + return reject("Undefined Jitsi start callback"); + } + + const jitsiLoading = this.jitsiLoadPromise() + .then((iframe) => { + this.iframe = iframe; + this.iframe.classList.add("pixel"); + this.state.set("ready"); + return resolve(iframe); + }) + .catch((err) => { + return reject(err); + }); + + cancel(() => { + jitsiLoading.cancel(); + this.unload().catch((err) => { + console.error("Cannot unload Jitsi co-website while cancel loading", err); + }); + }); + }); + } + + unload(): Promise { + jitsiFactory.destroy(); + gameManager.getCurrentGameScene().enableMediaBehaviors(); + + return super.unload(); + } +} diff --git a/front/src/WebRtc/CoWebsite/SimpleCoWebsite.ts b/front/src/WebRtc/CoWebsite/SimpleCoWebsite.ts new file mode 100644 index 00000000..1ad585b4 --- /dev/null +++ b/front/src/WebRtc/CoWebsite/SimpleCoWebsite.ts @@ -0,0 +1,133 @@ +import CancelablePromise from "cancelable-promise"; +import { get, Readable, writable, Writable } from "svelte/store"; +import { iframeListener } from "../../Api/IframeListener"; +import { coWebsiteManager } from "../CoWebsiteManager"; +import type { CoWebsite, CoWebsiteState } from "./CoWesbite"; + +export class SimpleCoWebsite implements CoWebsite { + protected id: string; + protected url: URL; + protected state: Writable; + protected iframe?: HTMLIFrameElement; + protected loadIframe?: CancelablePromise; + protected allowApi?: boolean; + protected allowPolicy?: string; + protected widthPercent?: number; + protected closable: boolean; + + constructor(url: URL, allowApi?: boolean, allowPolicy?: string, widthPercent?: number, closable?: boolean) { + this.id = coWebsiteManager.generateUniqueId(); + this.url = url; + this.state = writable("asleep" as CoWebsiteState); + this.allowApi = allowApi; + this.allowPolicy = allowPolicy; + this.widthPercent = widthPercent; + this.closable = closable ?? false; + } + + getId(): string { + return this.id; + } + + getUrl(): URL { + return this.url; + } + + getState(): CoWebsiteState { + return get(this.state); + } + + getStateSubscriber(): Readable { + return this.state; + } + + getIframe(): HTMLIFrameElement | undefined { + return this.iframe; + } + + getLoadIframe(): CancelablePromise | undefined { + return this.loadIframe; + } + + getWidthPercent(): number | undefined { + return this.widthPercent; + } + + isClosable(): boolean { + return this.closable; + } + + load(): CancelablePromise { + this.loadIframe = new CancelablePromise((resolve, reject, cancel) => { + this.state.set("loading"); + + const iframe = document.createElement("iframe"); + this.iframe = iframe; + this.iframe.src = this.url.toString(); + this.iframe.id = this.id; + + if (this.allowPolicy) { + this.iframe.allow = this.allowPolicy; + } + + if (this.allowApi) { + iframeListener.registerIframe(this.iframe); + } + + this.iframe.classList.add("pixel"); + + const onloadPromise = new Promise((resolve) => { + if (this.iframe) { + this.iframe.onload = () => { + this.state.set("ready"); + resolve(); + }; + } + }); + + const onTimeoutPromise = new Promise((resolve) => { + setTimeout(() => resolve(), 2000); + }); + + coWebsiteManager.getCoWebsiteBuffer().appendChild(this.iframe); + + const race = CancelablePromise.race([onloadPromise, onTimeoutPromise]) + .then(() => { + return resolve(iframe); + }) + .catch((err) => { + console.error("Error on co-website loading => ", err); + return reject(); + }); + + cancel(() => { + race.cancel(); + this.unload().catch((err) => { + console.error("Cannot unload co-website while cancel loading", err); + }); + }); + }); + + return this.loadIframe; + } + + unload(): Promise { + return new Promise((resolve) => { + if (this.iframe) { + if (this.allowApi) { + iframeListener.unregisterIframe(this.iframe); + } + this.iframe.parentNode?.removeChild(this.iframe); + } + + if (this.loadIframe) { + this.loadIframe.cancel(); + this.loadIframe = undefined; + } + + this.state.set("asleep"); + + resolve(); + }); + } +} diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts index 476526da..a5c57ed6 100644 --- a/front/src/WebRtc/CoWebsiteManager.ts +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -1,14 +1,13 @@ import { HtmlUtils } from "./HtmlUtils"; import { Subject } from "rxjs"; -import { iframeListener } from "../Api/IframeListener"; import { waScaleManager } from "../Phaser/Services/WaScaleManager"; import { coWebsites, coWebsitesNotAsleep, mainCoWebsite } from "../Stores/CoWebsiteStore"; -import { get, Writable, writable } from "svelte/store"; +import { get, Readable, Writable, writable } from "svelte/store"; import { embedScreenLayout, highlightedEmbedScreen } from "../Stores/EmbedScreensStore"; import { isMediaBreakpointDown } from "../Utils/BreakpointsUtils"; -import { jitsiFactory } from "./JitsiFactory"; -import { gameManager } from "../Phaser/Game/GameManager"; import { LayoutMode } from "./LayoutManager"; +import type { CoWebsite } from "./CoWebsite/CoWesbite"; +import type CancelablePromise from "cancelable-promise"; export enum iframeStates { closed = 1, @@ -34,30 +33,12 @@ interface TouchMoveCoordinates { y: number; } -export type CoWebsiteState = "asleep" | "loading" | "ready"; - -export type CoWebsite = { - iframe: HTMLIFrameElement; - url: URL; - state: Writable; - closable: boolean; - allowPolicy: string | undefined; - allowApi: boolean | undefined; - widthPercent?: number | undefined; - jitsi?: boolean; - altMessage?: string; -}; - class CoWebsiteManager { - private openedMain: iframeStates = iframeStates.closed; + private openedMain: Writable = writable(iframeStates.closed); private _onResize: Subject = new Subject(); public onResize = this._onResize.asObservable(); - /** - * Quickly going in and out of an iframe trigger can create conflicts between the iframe states. - * So we use this promise to queue up every cowebsite state transition - */ - private currentOperationPromise: Promise = Promise.resolve(); + private cowebsiteDom: HTMLDivElement; private resizing: boolean = false; private gameOverlayDom: HTMLDivElement; @@ -76,6 +57,10 @@ class CoWebsiteManager { }); public getMainState() { + return get(this.openedMain); + } + + public getMainStateSubscriber(): Readable { return this.openedMain; } @@ -147,13 +132,11 @@ class CoWebsiteManager { throw new Error("Undefined main co-website on closing"); } - if (coWebsite.closable) { - this.closeCoWebsite(coWebsite).catch(() => { - console.error("Error during closing a co-website by a button"); - }); + if (coWebsite.isClosable()) { + this.closeCoWebsite(coWebsite); } else { - this.unloadCoWebsite(coWebsite).catch(() => { - console.error("Error during unloading a co-website by a button"); + this.unloadCoWebsite(coWebsite).catch((err) => { + console.error("Cannot unload co-website on click on close button", err); }); } }); @@ -241,7 +224,10 @@ class CoWebsiteManager { return; } - coWebsite.iframe.style.display = "none"; + const iframe = coWebsite.getIframe(); + if (iframe) { + iframe.style.display = "none"; + } this.resizing = true; document.addEventListener("mousemove", movecallback); }); @@ -257,7 +243,10 @@ class CoWebsiteManager { return; } - coWebsite.iframe.style.display = "flex"; + const iframe = coWebsite.getIframe(); + if (iframe) { + iframe.style.display = "flex"; + } this.resizing = false; }); @@ -270,7 +259,10 @@ class CoWebsiteManager { return; } - coWebsite.iframe.style.display = "none"; + const iframe = coWebsite.getIframe(); + if (iframe) { + iframe.style.display = "none"; + } this.resizing = true; const touchEvent = event.touches[0]; this.previousTouchMoveCoordinates = { x: touchEvent.pageX, y: touchEvent.pageY }; @@ -289,7 +281,10 @@ class CoWebsiteManager { return; } - coWebsite.iframe.style.display = "flex"; + const iframe = coWebsite.getIframe(); + if (iframe) { + iframe.style.display = "flex"; + } this.resizing = false; }); } @@ -313,9 +308,12 @@ class CoWebsiteManager { public displayMain() { const coWebsite = this.getMainCoWebsite(); if (coWebsite) { - coWebsite.iframe.style.display = "block"; + const iframe = coWebsite.getIframe(); + if (iframe) { + iframe.style.display = "block"; + } } - this.loadMain(); + this.loadMain(coWebsite?.getWidthPercent()); this.openMain(); this.fire(); } @@ -323,11 +321,14 @@ class CoWebsiteManager { public hideMain() { const coWebsite = this.getMainCoWebsite(); if (coWebsite) { - coWebsite.iframe.style.display = "none"; + const iframe = coWebsite.getIframe(); + if (iframe) { + iframe.style.display = "none"; + } } this.cowebsiteDom.classList.add("closing"); this.cowebsiteDom.classList.remove("opened"); - this.openedMain = iframeStates.closed; + this.openedMain.set(iframeStates.closed); this.fire(); } @@ -335,7 +336,7 @@ class CoWebsiteManager { this.toggleFullScreenIcon(true); this.cowebsiteDom.classList.add("closing"); this.cowebsiteDom.classList.remove("opened"); - this.openedMain = iframeStates.closed; + this.openedMain.set(iframeStates.closed); this.resetStyleMain(); this.fire(); } @@ -389,14 +390,14 @@ class CoWebsiteManager { } this.cowebsiteDom.classList.add("opened"); - this.openedMain = iframeStates.loading; + this.openedMain.set(iframeStates.loading); } private openMain(): void { this.cowebsiteDom.addEventListener("transitionend", () => { this.resizeAllIframes(); }); - this.openedMain = iframeStates.opened; + this.openedMain.set(iframeStates.opened); } public resetStyleMain() { @@ -409,7 +410,9 @@ class CoWebsiteManager { } public getCoWebsiteById(coWebsiteId: string): CoWebsite | undefined { - return get(coWebsites).find((coWebsite: CoWebsite) => coWebsite.iframe.id === coWebsiteId); + return get(coWebsites).find((coWebsite: CoWebsite) => { + return coWebsite.getId() === coWebsiteId; + }); } private getCoWebsiteByPosition(position: number): CoWebsite | undefined { @@ -429,7 +432,9 @@ class CoWebsiteManager { } private getPositionByCoWebsite(coWebsite: CoWebsite): number { - return get(coWebsites).findIndex((currentCoWebsite) => currentCoWebsite.iframe.id === coWebsite.iframe.id); + return get(coWebsites).findIndex((currentCoWebsite) => { + return currentCoWebsite.getId() === coWebsite.getId(); + }); } private getSlotByCowebsite(coWebsite: CoWebsite): HTMLDivElement | undefined { @@ -443,7 +448,7 @@ class CoWebsiteManager { if (index === 0) { id += "main"; } else { - id += coWebsite.iframe.id; + id += coWebsite.getId(); } const slot = HtmlUtils.getElementById(id); @@ -460,60 +465,72 @@ class CoWebsiteManager { const bounding = coWebsiteSlot.getBoundingClientRect(); - coWebsite.iframe.style.top = bounding.top + "px"; - coWebsite.iframe.style.left = bounding.left + "px"; - coWebsite.iframe.style.width = bounding.right - bounding.left + "px"; - coWebsite.iframe.style.height = bounding.bottom - bounding.top + "px"; + const iframe = coWebsite.getIframe(); + + if (iframe) { + iframe.style.top = bounding.top + "px"; + iframe.style.left = bounding.left + "px"; + iframe.style.width = bounding.right - bounding.left + "px"; + iframe.style.height = bounding.bottom - bounding.top + "px"; + } } public resizeAllIframes() { const mainCoWebsite = this.getCoWebsiteByPosition(0); + const mainIframe = mainCoWebsite?.getIframe(); const highlightEmbed = get(highlightedEmbedScreen); - get(coWebsites).forEach((coWebsite) => { - const notMain = !mainCoWebsite || (mainCoWebsite && mainCoWebsite.iframe.id !== coWebsite.iframe.id); + get(coWebsites).forEach((coWebsite: CoWebsite) => { + const iframe = coWebsite.getIframe(); + if (!iframe) { + return; + } + + const notMain = !mainCoWebsite || (mainCoWebsite && mainIframe && mainIframe.id !== iframe.id); const notHighlighEmbed = !highlightEmbed || (highlightEmbed && (highlightEmbed.type !== "cowebsite" || - (highlightEmbed.type === "cowebsite" && - highlightEmbed.embed.iframe.id !== coWebsite.iframe.id))); + (highlightEmbed.type === "cowebsite" && highlightEmbed.embed.getId() !== coWebsite.getId()))); - if (coWebsite.iframe.classList.contains("main") && notMain) { - coWebsite.iframe.classList.remove("main"); + if (iframe.classList.contains("main") && notMain) { + iframe.classList.remove("main"); } - if (coWebsite.iframe.classList.contains("highlighted") && notHighlighEmbed) { - coWebsite.iframe.classList.remove("highlighted"); - coWebsite.iframe.classList.add("pixel"); - coWebsite.iframe.style.top = "-1px"; - coWebsite.iframe.style.left = "-1px"; + if (iframe.classList.contains("highlighted") && notHighlighEmbed) { + iframe.classList.remove("highlighted"); + iframe.classList.add("pixel"); + iframe.style.top = "-1px"; + iframe.style.left = "-1px"; } if (notMain && notHighlighEmbed) { - coWebsite.iframe.classList.add("pixel"); - coWebsite.iframe.style.top = "-1px"; - coWebsite.iframe.style.left = "-1px"; + iframe.classList.add("pixel"); + iframe.style.top = "-1px"; + iframe.style.left = "-1px"; } this.setIframeOffset(coWebsite); }); - if (mainCoWebsite) { - mainCoWebsite.iframe.classList.add("main"); - mainCoWebsite.iframe.classList.remove("pixel"); + if (mainIframe) { + mainIframe.classList.add("main"); + mainIframe.classList.remove("pixel"); } if (highlightEmbed && highlightEmbed.type === "cowebsite") { - highlightEmbed.embed.iframe.classList.add("highlighted"); - highlightEmbed.embed.iframe.classList.remove("pixel"); + const highlightEmbedIframe = highlightEmbed.embed.getIframe(); + if (highlightEmbedIframe) { + highlightEmbedIframe.classList.add("highlighted"); + highlightEmbedIframe.classList.remove("pixel"); + } } } private removeHighlightCoWebsite(coWebsite: CoWebsite) { const highlighted = get(highlightedEmbedScreen); - if (highlighted && highlighted.type === "cowebsite" && highlighted.embed.iframe.id === coWebsite.iframe.id) { + if (highlighted && highlighted.type === "cowebsite" && highlighted.embed.getId() === coWebsite.getId()) { highlightedEmbedScreen.removeHighlight(); } } @@ -526,7 +543,9 @@ class CoWebsiteManager { this.closeMain(); } - coWebsite.iframe.remove(); + coWebsite.unload().catch((err) => { + console.error("Cannot unload cowebsite on remove from stack"); + }); } public goToMain(coWebsite: CoWebsite) { @@ -538,38 +557,21 @@ class CoWebsiteManager { isMediaBreakpointDown("lg") && get(embedScreenLayout) === LayoutMode.Presentation && mainCoWebsite && - mainCoWebsite.iframe.id !== coWebsite.iframe.id && - get(mainCoWebsite.state) !== "asleep" + mainCoWebsite.getId() !== coWebsite.getId() && + mainCoWebsite.getState() !== "asleep" ) { - highlightedEmbedScreen.toggleHighlight({ - type: "cowebsite", - embed: mainCoWebsite, - }); + highlightedEmbedScreen.removeHighlight(); } this.resizeAllIframes(); } - public searchJitsi(): CoWebsite | undefined { - return get(coWebsites).find((coWebsite: CoWebsite) => coWebsite.jitsi); - } - - private initialiseCowebsite(coWebsite: CoWebsite, position: number | undefined) { - if (coWebsite.allowPolicy) { - coWebsite.iframe.allow = coWebsite.allowPolicy; - } - - if (coWebsite.allowApi) { - iframeListener.registerIframe(coWebsite.iframe); - } - - coWebsite.iframe.classList.add("pixel"); - + public addCoWebsiteToStore(coWebsite: CoWebsite, position: number | undefined) { const coWebsitePosition = position === undefined ? get(coWebsites).length : position; coWebsites.add(coWebsite, coWebsitePosition); } - private generateUniqueId() { + public generateUniqueId() { let id = undefined; do { id = "cowebsite-iframe-" + (Math.random() + 1).toString(36).substring(7); @@ -578,210 +580,89 @@ class CoWebsiteManager { return id; } - public addCoWebsite( - url: string, - base: string, - allowApi?: boolean, - allowPolicy?: string, - widthPercent?: number, - position?: number, - closable?: boolean, - altMessage?: string - ): CoWebsite { - const iframe = document.createElement("iframe"); - const fullUrl = new URL(url, base); - iframe.src = fullUrl.toString(); - iframe.id = this.generateUniqueId(); - - const newCoWebsite: CoWebsite = { - iframe, - url: fullUrl, - state: writable("asleep" as CoWebsiteState), - closable: closable ?? false, - allowPolicy, - allowApi, - widthPercent, - altMessage, - }; - - this.initialiseCowebsite(newCoWebsite, position); - - return newCoWebsite; - } - - public addCoWebsiteFromIframe( - iframe: HTMLIFrameElement, - allowApi?: boolean, - allowPolicy?: string, - widthPercent?: number, - position?: number, - closable?: boolean, - jitsi?: boolean - ): CoWebsite { - if (get(coWebsitesNotAsleep).length < 1) { - this.loadMain(widthPercent); - } - - iframe.id = this.generateUniqueId(); - - const newCoWebsite: CoWebsite = { - iframe, - url: new URL(iframe.src), - state: writable("ready" as CoWebsiteState), - closable: closable ?? false, - allowPolicy, - allowApi, - widthPercent, - jitsi, - }; - - if (position === 0) { - this.openMain(); - setTimeout(() => { - this.fire(); - }, animationTime); - } - - this.initialiseCowebsite(newCoWebsite, position); - - return newCoWebsite; - } - - public loadCoWebsite(coWebsite: CoWebsite): Promise { + public loadCoWebsite(coWebsite: CoWebsite): CancelablePromise { if (get(coWebsitesNotAsleep).length < 1) { coWebsites.remove(coWebsite); coWebsites.add(coWebsite, 0); - this.loadMain(coWebsite.widthPercent); + this.loadMain(coWebsite.getWidthPercent()); } // Check if the main is hide - if (this.getMainCoWebsite() && this.openedMain === iframeStates.closed) { + if (this.getMainCoWebsite() && this.getMainState() === iframeStates.closed) { this.displayMain(); } - coWebsite.state.set("loading"); + const coWebsiteLloading = coWebsite + .load() + .then(() => { + const mainCoWebsite = this.getMainCoWebsite(); + if (mainCoWebsite && mainCoWebsite.getId() === coWebsite.getId()) { + this.openMain(); - const mainCoWebsite = this.getMainCoWebsite(); - - return new Promise((resolve, reject) => { - const onloadPromise = new Promise((resolve) => { - coWebsite.iframe.onload = () => { - coWebsite.state.set("ready"); - resolve(); - }; + setTimeout(() => { + this.fire(); + }, animationTime); + } + this.resizeAllIframes(); + }) + .catch((err) => { + console.error("Error on co-website loading => ", err); + this.removeCoWebsiteFromStack(coWebsite); }); - const onTimeoutPromise = new Promise((resolve) => { - setTimeout(() => resolve(), 2000); - }); - - this.cowebsiteBufferDom.appendChild(coWebsite.iframe); - - if (coWebsite.jitsi) { - const gameScene = gameManager.getCurrentGameScene(); - gameScene.disableMediaBehaviors(); - jitsiFactory.restart(); - } - - this.currentOperationPromise = this.currentOperationPromise - .then(() => Promise.race([onloadPromise, onTimeoutPromise])) - .then(() => { - if (mainCoWebsite && mainCoWebsite.iframe.id === coWebsite.iframe.id) { - this.openMain(); - - setTimeout(() => { - this.fire(); - }, animationTime); - } - - return resolve(coWebsite); - }) - .catch((err) => { - console.error("Error on co-website loading => ", err); - this.removeCoWebsiteFromStack(coWebsite); - return reject(); - }); - }); + return coWebsiteLloading; } public unloadCoWebsite(coWebsite: CoWebsite): Promise { - return new Promise((resolve, reject) => { - this.removeHighlightCoWebsite(coWebsite); + this.removeHighlightCoWebsite(coWebsite); - coWebsite.iframe.parentNode?.removeChild(coWebsite.iframe); - coWebsite.state.set("asleep"); - coWebsites.remove(coWebsite); + return coWebsite + .unload() + .then(() => { + coWebsites.remove(coWebsite); + const mainCoWebsite = this.getMainCoWebsite(); - if (coWebsite.jitsi) { - jitsiFactory.stop(); - const gameScene = gameManager.getCurrentGameScene(); - gameScene.enableMediaBehaviors(); - } + if (mainCoWebsite) { + this.removeHighlightCoWebsite(mainCoWebsite); + this.goToMain(mainCoWebsite); + this.resizeAllIframes(); + } else { + this.closeMain(); + } - const mainCoWebsite = this.getMainCoWebsite(); + coWebsites.add(coWebsite, get(coWebsites).length); + }) + .catch(() => { + console.error(); + }); + } - if (mainCoWebsite) { - this.removeHighlightCoWebsite(mainCoWebsite); - this.goToMain(mainCoWebsite); - this.resizeAllIframes(); - } else { - this.closeMain(); - } + public closeCoWebsite(coWebsite: CoWebsite): void { + if (get(coWebsites).length === 1) { + this.fire(); + } - coWebsites.add(coWebsite, get(coWebsites).length); + this.removeCoWebsiteFromStack(coWebsite); - resolve(); + const mainCoWebsite = this.getMainCoWebsite(); + + if (mainCoWebsite) { + this.removeHighlightCoWebsite(mainCoWebsite); + this.goToMain(mainCoWebsite); + this.resizeAllIframes(); + } else { + this.closeMain(); + } + } + + public closeCoWebsites(): void { + get(coWebsites).forEach((coWebsite: CoWebsite) => { + this.closeCoWebsite(coWebsite); }); } - public closeCoWebsite(coWebsite: CoWebsite): Promise { - this.currentOperationPromise = this.currentOperationPromise.then( - () => - new Promise((resolve) => { - if (coWebsite.jitsi) { - jitsiFactory.destroy(); - const gameScene = gameManager.getCurrentGameScene(); - gameScene.enableMediaBehaviors(); - } - - if (get(coWebsites).length === 1) { - this.fire(); - } - - if (coWebsite.allowApi) { - iframeListener.unregisterIframe(coWebsite.iframe); - } - - this.removeCoWebsiteFromStack(coWebsite); - - const mainCoWebsite = this.getMainCoWebsite(); - - if (mainCoWebsite) { - this.removeHighlightCoWebsite(mainCoWebsite); - this.goToMain(mainCoWebsite); - this.resizeAllIframes(); - } else { - this.closeMain(); - } - resolve(); - }) - ); - return this.currentOperationPromise; - } - - public closeCoWebsites(): Promise { - return (this.currentOperationPromise = this.currentOperationPromise.then(() => { - get(coWebsites).forEach((coWebsite: CoWebsite) => { - this.closeCoWebsite(coWebsite).catch(() => { - console.error("Error during closing a co-website"); - }); - }); - })); - return this.currentOperationPromise; - } - public getGameSize(): { width: number; height: number } { - if (this.openedMain === iframeStates.closed) { + if (this.getMainState() === iframeStates.closed) { return { width: window.innerWidth, height: window.innerHeight, diff --git a/front/src/WebRtc/JitsiFactory.ts b/front/src/WebRtc/JitsiFactory.ts index 8f7ed952..d328a72d 100644 --- a/front/src/WebRtc/JitsiFactory.ts +++ b/front/src/WebRtc/JitsiFactory.ts @@ -1,7 +1,8 @@ import { JITSI_URL } from "../Enum/EnvironmentVariable"; -import { CoWebsite, coWebsiteManager } from "./CoWebsiteManager"; +import { coWebsiteManager } from "./CoWebsiteManager"; import { requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore"; import { get } from "svelte/store"; +import CancelablePromise from "cancelable-promise"; interface jitsiConfigInterface { startWithAudioMuted: boolean; @@ -134,122 +135,96 @@ class JitsiFactory { return slugify(instance.replace("/", "-") + "-" + roomName); } - public async start( + public start( roomName: string, playerName: string, jwt?: string, config?: object, interfaceConfig?: object, jitsiUrl?: string - ) { - const coWebsite = coWebsiteManager.searchJitsi(); + ): CancelablePromise { + return new CancelablePromise((resolve, reject, cancel) => { + // Jitsi meet external API maintains some data in local storage + // which is sent via the appData URL parameter when joining a + // conference. Problem is that this data grows indefinitely. Thus + // after some time the URLs get so huge that loading the iframe + // becomes slow and eventually breaks completely. Thus lets just + // clear jitsi local storage before starting a new conference. + window.localStorage.removeItem("jitsiLocalStorage"); - if (coWebsite) { - await coWebsiteManager.closeCoWebsite(coWebsite); - } - - // Jitsi meet external API maintains some data in local storage - // which is sent via the appData URL parameter when joining a - // conference. Problem is that this data grows indefinitely. Thus - // after some time the URLs get so huge that loading the iframe - // becomes slow and eventually breaks completely. Thus lets just - // clear jitsi local storage before starting a new conference. - window.localStorage.removeItem("jitsiLocalStorage"); - - const domain = jitsiUrl || JITSI_URL; - if (domain === undefined) { - throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map."); - } - await this.loadJitsiScript(domain); - - const options: JitsiOptions = { - roomName: roomName, - jwt: jwt, - width: "100%", - height: "100%", - parentNode: coWebsiteManager.getCoWebsiteBuffer(), - configOverwrite: mergeConfig(config), - interfaceConfigOverwrite: { ...defaultInterfaceConfig, ...interfaceConfig }, - }; - - if (!options.jwt) { - delete options.jwt; - } - - const doResolve = (): void => { - const iframe = coWebsiteManager.getCoWebsiteBuffer().querySelector('[id*="jitsi" i]'); - if (iframe && this.jitsiApi) { - const coWebsite = coWebsiteManager.addCoWebsiteFromIframe( - iframe, - false, - undefined, - undefined, - 0, - false, - true - ); - - this.jitsiApi.addListener("videoConferenceLeft", () => { - this.closeOrUnload(coWebsite); - }); - - this.jitsiApi.addListener("readyToClose", () => { - this.closeOrUnload(coWebsite); - }); + const domain = jitsiUrl || JITSI_URL; + if (domain === undefined) { + throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map."); } - coWebsiteManager.resizeAllIframes(); - }; + const loadScript = this.loadJitsiScript(domain).then(() => { + const options: JitsiOptions = { + roomName: roomName, + jwt: jwt, + width: "100%", + height: "100%", + parentNode: coWebsiteManager.getCoWebsiteBuffer(), + configOverwrite: mergeConfig(config), + interfaceConfigOverwrite: { ...defaultInterfaceConfig, ...interfaceConfig }, + }; - this.jitsiApi = undefined; + if (!options.jwt) { + delete options.jwt; + } - options.onload = () => doResolve(); //we want for the iframe to be loaded before triggering animations. - setTimeout(() => doResolve(), 2000); //failsafe in case the iframe is deleted before loading or too long to load - this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options); - this.jitsiApi.executeCommand("displayName", playerName); + const timemout = setTimeout(() => doResolve(), 2000); //failsafe in case the iframe is deleted before loading or too long to load - this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback); - this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback); + const doResolve = (): void => { + clearTimeout(timemout); + const iframe = coWebsiteManager + .getCoWebsiteBuffer() + .querySelector('[id*="jitsi" i]'); + + if (iframe && this.jitsiApi) { + this.jitsiApi.addListener("videoConferenceLeft", () => { + this.closeOrUnload(iframe); + }); + + this.jitsiApi.addListener("readyToClose", () => { + this.closeOrUnload(iframe); + }); + + return resolve(iframe); + } + }; + + this.jitsiApi = undefined; + + 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("audioMuteStatusChanged", this.audioCallback); + this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback); + }); + + cancel(() => { + loadScript.cancel(); + }); + }); } - private closeOrUnload = function (coWebsite: CoWebsite) { - if (coWebsite.closable) { - coWebsiteManager.closeCoWebsite(coWebsite).catch(() => { - console.error("Error during closing a Jitsi Meet"); - }); + private closeOrUnload = function (iframe: HTMLIFrameElement) { + const coWebsite = coWebsiteManager.getCoWebsites().find((coWebsite) => coWebsite.getIframe() === iframe); + + if (!coWebsite) { + return; + } + + if (coWebsite.isClosable()) { + coWebsiteManager.closeCoWebsite(coWebsite); } else { - coWebsiteManager.unloadCoWebsite(coWebsite).catch(() => { - console.error("Error during unloading a Jitsi Meet"); + coWebsiteManager.unloadCoWebsite(coWebsite).catch((err) => { + console.error("Cannot unload co-website from the Jitsi factory", err); }); } }; - public restart() { - if (!this.jitsiApi) { - return; - } - - this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback); - this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback); - - const coWebsite = coWebsiteManager.searchJitsi(); - console.log("jitsi api ", this.jitsiApi); - console.log("iframe cowebsite", coWebsite?.iframe); - - if (!coWebsite) { - this.destroy(); - return; - } - - this.jitsiApi.addListener("videoConferenceLeft", () => { - this.closeOrUnload(coWebsite); - }); - - this.jitsiApi.addListener("readyToClose", () => { - this.closeOrUnload(coWebsite); - }); - } - public stop() { if (!this.jitsiApi) { return; @@ -284,8 +259,8 @@ class JitsiFactory { } } - private async loadJitsiScript(domain: string): Promise { - return new Promise((resolve, reject) => { + private loadJitsiScript(domain: string): CancelablePromise { + return new CancelablePromise((resolve, reject, cancel) => { if (this.jitsiScriptLoaded) { resolve(); return; @@ -304,6 +279,10 @@ class JitsiFactory { }; document.head.appendChild(jitsiScript); + + cancel(() => { + jitsiScript.remove(); + }); }); } } diff --git a/front/src/i18n/de-DE/index.ts b/front/src/i18n/de-DE/index.ts index ab628a4d..40b63558 100644 --- a/front/src/i18n/de-DE/index.ts +++ b/front/src/i18n/de-DE/index.ts @@ -14,7 +14,7 @@ import warning from "./warning"; import woka from "./woka"; const de_DE: Translation = { - ...en_US, + ...(en_US as Translation), language: "Deutsch", country: "Deutschland", audio, diff --git a/front/src/i18n/en-US/index.ts b/front/src/i18n/en-US/index.ts index 2d3ac74a..b1a0e422 100644 --- a/front/src/i18n/en-US/index.ts +++ b/front/src/i18n/en-US/index.ts @@ -11,6 +11,7 @@ import menu from "./menu"; import report from "./report"; import warning from "./warning"; import emoji from "./emoji"; +import trigger from "./trigger"; const en_US: BaseTranslation = { language: "English", @@ -27,6 +28,7 @@ const en_US: BaseTranslation = { report, warning, emoji, + trigger, }; export default en_US; diff --git a/front/src/i18n/en-US/trigger.ts b/front/src/i18n/en-US/trigger.ts new file mode 100644 index 00000000..40e84bf4 --- /dev/null +++ b/front/src/i18n/en-US/trigger.ts @@ -0,0 +1,9 @@ +import type { BaseTranslation } from "../i18n-types"; + +const trigger: BaseTranslation = { + cowebsite: "Press SPACE or touch here to open web site", + jitsiRoom: "Press SPACE or touch here to enter Jitsi Meet room", + newTab: "Press SPACE or touch here to open web site in new tab", +}; + +export default trigger; diff --git a/front/src/i18n/fr-FR/index.ts b/front/src/i18n/fr-FR/index.ts index b9f91b13..77acbb4a 100644 --- a/front/src/i18n/fr-FR/index.ts +++ b/front/src/i18n/fr-FR/index.ts @@ -12,9 +12,10 @@ import menu from "./menu"; import report from "./report"; import warning from "./warning"; import woka from "./woka"; +import trigger from "./trigger"; const fr_FR: Translation = { - ...en_US, + ...(en_US as Translation), language: "Français", country: "France", audio, @@ -29,6 +30,7 @@ const fr_FR: Translation = { report, warning, emoji, + trigger, }; export default fr_FR; diff --git a/front/src/i18n/fr-FR/menu.ts b/front/src/i18n/fr-FR/menu.ts index bf3ede7a..f8c58990 100644 --- a/front/src/i18n/fr-FR/menu.ts +++ b/front/src/i18n/fr-FR/menu.ts @@ -63,7 +63,7 @@ const menu: NonNullable = { }, fullscreen: "Plein écran", notifications: "Notifications", - cowebsiteTrigger: "Demander toujours avant d'ouvrir des sites web et des salles de réunion Jitsi", + cowebsiteTrigger: "Demander toujours avant d'ouvrir des sites web et des salles de conférence Jitsi", ignoreFollowRequest: "Ignorer les demandes de suivi des autres utilisateurs", }, invite: { diff --git a/front/src/i18n/fr-FR/trigger.ts b/front/src/i18n/fr-FR/trigger.ts new file mode 100644 index 00000000..5940e92a --- /dev/null +++ b/front/src/i18n/fr-FR/trigger.ts @@ -0,0 +1,9 @@ +import type { Translation } from "../i18n-types"; + +const trigger: NonNullable = { + cowebsite: "Appuyez sur ESPACE ou ici pour ouvrir le site Web", + jitsiRoom: "Appuyez sur ESPACE ou ici pour entrer dans la salle conférence Jitsi", + newTab: "Appuyez sur ESPACE ou ici pour ouvrir le site Web dans un nouvel onglet", +}; + +export default trigger; diff --git a/maps/tests/DoorTest/map.json b/maps/tests/DoorTest/map.json new file mode 100644 index 00000000..b954b97c --- /dev/null +++ b/maps/tests/DoorTest/map.json @@ -0,0 +1,197 @@ +{ "compressionlevel":-1, + "height":30, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 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, 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, 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, 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, 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, 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, 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, 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, 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":30, + "id":1, + "name":"floor", + "opacity":1, + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"script.php" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "type":"tilelayer", + "visible":true, + "width":30, + "x":0, + "y":0 + }, + { + "data":[17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 18, 6, 6, 6, 6, 6, 0, 0, 6, 6, 6, 6, 6, 6, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17, 17, 17, 17, 17, 17, 0, 0, 17, 17, 17, 17, 17, 17, 17, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 28, 28, 28, 28, 28, 28, 0, 0, 28, 28, 28, 28, 28, 28, 28, 29, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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":30, + "id":6, + "name":"furnitures", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":30, + "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, 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, 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, 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, 73, 74, 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, 73, 74, 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, 73, 74, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 0, 0], + "height":30, + "id":8, + "name":"closedPath", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":30, + "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, 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, 74, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 0, 0, 0, 0, 0], + "height":30, + "id":2, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":30, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"floorLayer", + "objects":[ + { + "height":101.5, + "id":11, + "name":"", + "rotation":0, + "text": + { + "text":"You should be able to get here only if the path isn't blocked", + "wrap":true + }, + "type":"", + "visible":true, + "width":383, + "x":81.226595249185, + "y":297.048206800186 + }, + { + "height":101.5, + "id":12, + "name":"", + "rotation":0, + "text": + { + "text":"Try to move to the next room by using right-click \/ tap movement. Click \"Toggle Door\" button to block \/ make access.", + "wrap":true + }, + "type":"", + "visible":true, + "width":383, + "x":81, + "y":99.75 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":9, + "nextobjectid":13, + "orientation":"orthogonal", + "properties":[ + { + "name":"openWebsite", + "type":"string", + "value":"script.php" + }, + { + "name":"openWebsiteAllowApi", + "type":"bool", + "value":true + }], + "renderorder":"right-down", + "tiledversion":"1.7.2", + "tileheight":32, + "tilesets":[ + { + "columns":11, + "firstgid":1, + "image":"..\/tileset1.png", + "imageheight":352, + "imagewidth":352, + "margin":0, + "name":"tileset1", + "spacing":0, + "tilecount":121, + "tileheight":32, + "tiles":[ + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":27, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":28, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":72, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":73, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":"1.6", + "width":30 +} \ No newline at end of file diff --git a/maps/tests/DoorTest/script.php b/maps/tests/DoorTest/script.php new file mode 100644 index 00000000..73f81836 --- /dev/null +++ b/maps/tests/DoorTest/script.php @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/maps/tests/index.html b/maps/tests/index.html index 9d8fe146..e8bf4369 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -34,7 +34,7 @@ - Success Failure Pending + Success Failure Pending Test parallax effect @@ -48,6 +48,14 @@ Test animated tiles + + + Success Failure Pending + + + Test Doors + + Success Failure Pending