diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d23f4b9..710b85fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## Version develop +### Updates +- New scripting API features : + - Use `WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions): Menu` to add a custom menu or an iframe to the menu. + +## Version 1.4.14 + ### Updates - New scripting API features : - Use `WA.room.loadTileset(url: string) : Promise` to load a tileset from a JSON file. diff --git a/README.md b/README.md index 322f06ba..ba9e70ce 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,9 @@ Note: on some OSes, you will need to add this line to your `/etc/hosts` file: 127.0.0.1 workadventure.localhost ``` +Note: If on the first run you get a page with "network error". Try to ``docker-compose stop`` , then ``docker-compose start``. +Note 2: If you are still getting "network error". Make sure you are authorizing the self-signed certificate by entering https://pusher.workadventure.testing and accepting them. + ### MacOS developers, your environment with Vagrant If you are using MacOS, you can increase Docker performance using Vagrant. If you want more explanations, you can read [this medium article](https://medium.com/better-programming/vagrant-to-increase-docker-performance-with-macos-25b354b0c65c). diff --git a/back/package.json b/back/package.json index 8a1e445e..bb54d624 100644 --- a/back/package.json +++ b/back/package.json @@ -40,7 +40,7 @@ }, "homepage": "https://github.com/thecodingmachine/workadventure#readme", "dependencies": { - "@workadventure/tiled-map-type-guard": "^1.0.0", + "@workadventure/tiled-map-type-guard": "^1.0.2", "axios": "^0.21.1", "busboy": "^0.3.1", "circular-json": "^0.5.9", @@ -54,7 +54,6 @@ "prom-client": "^12.0.0", "query-string": "^6.13.3", "redis": "^3.1.2", - "systeminformation": "^4.31.1", "uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0", "uuidv4": "^6.0.7" }, diff --git a/back/src/Services/VariablesManager.ts b/back/src/Services/VariablesManager.ts index e8aaef25..915c6c05 100644 --- a/back/src/Services/VariablesManager.ts +++ b/back/src/Services/VariablesManager.ts @@ -1,7 +1,12 @@ /** * Handles variables shared between the scripting API and the server. */ -import { ITiledMap, ITiledMapObject, ITiledMapObjectLayer } from "@workadventure/tiled-map-type-guard/dist"; +import { + ITiledMap, + ITiledMapLayer, + ITiledMapObject, + ITiledMapObjectLayer, +} from "@workadventure/tiled-map-type-guard/dist"; import { User } from "_Model/User"; import { variablesRepository } from "./Repository/VariablesRepository"; import { redisClient } from "./RedisClient"; @@ -83,25 +88,33 @@ export class VariablesManager { private static findVariablesInMap(map: ITiledMap): Map { const objects = new Map(); for (const layer of map.layers) { - if (layer.type === "objectgroup") { - for (const object of (layer as ITiledMapObjectLayer).objects) { - if (object.type === "variable") { - if (object.template) { - console.warn( - 'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.' - ); - continue; - } - - // We store a copy of the object (to make it immutable) - objects.set(object.name, this.iTiledObjectToVariable(object)); - } - } - } + this.recursiveFindVariablesInLayer(layer, objects); } return objects; } + private static recursiveFindVariablesInLayer(layer: ITiledMapLayer, objects: Map): void { + if (layer.type === "objectgroup") { + for (const object of layer.objects) { + if (object.type === "variable") { + if (object.template) { + console.warn( + 'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.' + ); + continue; + } + + // We store a copy of the object (to make it immutable) + objects.set(object.name, this.iTiledObjectToVariable(object)); + } + } + } else if (layer.type === "group") { + for (const innerLayer of layer.layers) { + this.recursiveFindVariablesInLayer(innerLayer, objects); + } + } + } + private static iTiledObjectToVariable(object: ITiledMapObject): Variable { const variable: Variable = {}; diff --git a/back/yarn.lock b/back/yarn.lock index 98d675ee..64dcb9ce 100644 --- a/back/yarn.lock +++ b/back/yarn.lock @@ -194,10 +194,10 @@ semver "^7.3.2" tsutils "^3.17.1" -"@workadventure/tiled-map-type-guard@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@workadventure/tiled-map-type-guard/-/tiled-map-type-guard-1.0.0.tgz#02524602ee8b2688429a1f56df1d04da3fc171ba" - integrity sha512-Mc0SE128otQnYlScQWVaQVyu1+CkailU/FTBh09UTrVnBAhyMO+jIn9vT9+Dv244xq+uzgQDpXmiVdjgrYFQ+A== +"@workadventure/tiled-map-type-guard@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@workadventure/tiled-map-type-guard/-/tiled-map-type-guard-1.0.2.tgz#4171550f6cd71be19791faef48360d65d698bcb0" + integrity sha512-RCtygGV5y9cb7QoyGMINBE9arM5pyXjkxvXgA5uXEv4GDbXKorhFim/rHgwbVR+eFnVF3rDgWbRnk3DIaHt+lQ== dependencies: generic-type-guard "^3.4.1" @@ -554,7 +554,7 @@ chokidar@^3.4.0: optionalDependencies: fsevents "~2.1.2" -chownr@^1.1.1: +chownr@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== @@ -1159,7 +1159,7 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" -fs-minipass@^1.2.5: +fs-minipass@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== @@ -1969,7 +1969,7 @@ minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: +minipass@^2.6.0, minipass@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== @@ -1977,7 +1977,7 @@ minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: safe-buffer "^5.1.2" yallist "^3.0.0" -minizlib@^1.2.1: +minizlib@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== @@ -1992,7 +1992,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3: +mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -2290,9 +2290,9 @@ path-key@^3.0.0, path-key@^3.1.0: integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-type@^1.0.0: version "1.1.0" @@ -2578,7 +2578,7 @@ rxjs@^6.6.7: dependencies: tslib "^1.9.0" -safe-buffer@^5.0.1, safe-buffer@^5.1.2: +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -2962,11 +2962,6 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -systeminformation@^4.31.1: - version "4.31.1" - resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.31.1.tgz#2e02c26987494d4b6a4d2d83138724593bc98d50" - integrity sha512-dVCDWNMN8ncMZo5vbMCA5dpAdMgzafK2ucuJy5LFmGtp1cG6farnPg8QNvoOSky9SkFoEX1Aw0XhcOFV6TnLYA== - table@^5.2.3: version "5.4.6" resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" @@ -2978,17 +2973,17 @@ table@^5.2.3: string-width "^3.0.0" tar@^4.4.2: - version "4.4.13" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" - integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== + version "4.4.19" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" + integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== dependencies: - chownr "^1.1.1" - fs-minipass "^1.2.5" - minipass "^2.8.6" - minizlib "^1.2.1" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.3" + chownr "^1.1.4" + fs-minipass "^1.2.7" + minipass "^2.9.0" + minizlib "^1.3.3" + mkdirp "^0.5.5" + safe-buffer "^5.2.1" + yallist "^3.1.1" tdigest@^0.1.1: version "0.1.1" @@ -3282,7 +3277,7 @@ y18n@^3.2.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== -yallist@^3.0.0, yallist@^3.0.3: +yallist@^3.0.0, yallist@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== diff --git a/benchmark/package-lock.json b/benchmark/package-lock.json index 2d25c58a..5d9ef0c6 100644 --- a/benchmark/package-lock.json +++ b/benchmark/package-lock.json @@ -429,9 +429,9 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-type": { "version": "1.1.0", diff --git a/benchmark/yarn.lock b/benchmark/yarn.lock index 8dcffe52..92541451 100644 --- a/benchmark/yarn.lock +++ b/benchmark/yarn.lock @@ -315,8 +315,8 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" path-type@^1.0.0: version "1.1.0" diff --git a/deeployer.libsonnet b/deeployer.libsonnet index 494c72b8..a2e8970a 100644 --- a/deeployer.libsonnet +++ b/deeployer.libsonnet @@ -101,6 +101,7 @@ }, "redis": { "image": "redis:6", + "ports": [6379] } }, "config": { diff --git a/docs/maps/api-deprecated.md b/docs/maps/api-deprecated.md index 930caebe..f2b582a5 100644 --- a/docs/maps/api-deprecated.md +++ b/docs/maps/api-deprecated.md @@ -18,3 +18,4 @@ The list of functions below is **deprecated**. You should not use those but. use - Method `WA.onChatMessage` is deprecated. It has been renamed to `WA.chat.onChatMessage`. - Method `WA.onEnterZone` is deprecated. It has been renamed to `WA.room.onEnterZone`. - Method `WA.onLeaveZone` is deprecated. It has been renamed to `WA.room.onLeaveZone`. +- Method `WA.ui.registerMenuCommand` parameter `callback` is deprecated. Use `WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions)`. \ No newline at end of file diff --git a/docs/maps/api-state.md b/docs/maps/api-state.md index 634b47e1..1cc4f7fb 100644 --- a/docs/maps/api-state.md +++ b/docs/maps/api-state.md @@ -9,6 +9,7 @@ Moreover, `WA.state` functions can be used to persist this state across reloads. ``` WA.state.saveVariable(key : string, data : unknown): void WA.state.loadVariable(key : string) : unknown +WA.state.hasVariable(key : string) : boolean WA.state.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subscription WA.state.[any property]: unknown ``` diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md index e4b9425d..dc701500 100644 --- a/docs/maps/api-ui.md +++ b/docs/maps/api-ui.md @@ -68,25 +68,53 @@ WA.room.onLeaveZone('myZone', () => { ### Add custom menu -```typescript -WA.ui.registerMenuCommand(menuCommand: string, callback: (menuCommand: string) => void): void ``` -Add a custom menu item containing the text `commandDescriptor` in the main menu. A click on the menu will trigger the `callback`. +WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions): Menu +``` +Add a custom menu item containing the text `commandDescriptor` in the navbar of the menu. +`options` attribute accepts an object with three properties : +- `callback : (commandDescriptor: string) => void` : A click on the custom menu will trigger the `callback`. +- `iframe: string` : A click on the custom menu will open the `iframe` inside the menu. +- `allowApi?: boolean` : Allow the iframe of the custom menu to use the Scripting API. + +Important : `options` accepts only `callback` or `iframe` not both. + Custom menu exist only until the map is unloaded, or you leave the iframe zone of the script. -Example: +
+
+ +
+
+ +
+
+Example: ```javascript +const menu = WA.ui.registerMenuCommand('menu test', + { + callback: () => { + WA.chat.sendChatMessage('test'); + } + }) -WA.ui.registerMenuCommand("test", () => { - WA.chat.sendChatMessage("test clicked", "menu cmd") -}) - +// Some time later, if you want to remove the menu: +menu.remove(); ``` -
- -
+Please note that `registerMenuCommand` returns an object of the `Menu` class. + +The `Menu` class contains a single method: `remove(): void`. This will obviously remove the menu when called. + +```javascript +class Menu { + /** + * Remove the menu + */ + remove() {}; +} +``` diff --git a/docs/maps/images/custom-menu-iframe.png b/docs/maps/images/custom-menu-iframe.png new file mode 100644 index 00000000..9df2aa5f Binary files /dev/null and b/docs/maps/images/custom-menu-iframe.png differ diff --git a/docs/maps/images/custom-menu-navbar.png b/docs/maps/images/custom-menu-navbar.png new file mode 100644 index 00000000..c2440956 Binary files /dev/null and b/docs/maps/images/custom-menu-navbar.png differ diff --git a/front/dist/index.tmpl.html b/front/dist/index.tmpl.html index 187e513a..0c89b611 100644 --- a/front/dist/index.tmpl.html +++ b/front/dist/index.tmpl.html @@ -34,7 +34,6 @@ WorkAdventure -
@@ -62,31 +61,6 @@
- -
diff --git a/front/dist/resources/html/gameMenu.html b/front/dist/resources/html/gameMenu.html index 73c62918..e69de29b 100644 --- a/front/dist/resources/html/gameMenu.html +++ b/front/dist/resources/html/gameMenu.html @@ -1,78 +0,0 @@ - - - diff --git a/front/dist/resources/html/gameMenuIcon.html b/front/dist/resources/html/gameMenuIcon.html deleted file mode 100644 index 22fe9867..00000000 --- a/front/dist/resources/html/gameMenuIcon.html +++ /dev/null @@ -1,28 +0,0 @@ - -
-
- -
-
\ No newline at end of file diff --git a/front/dist/resources/html/gameQualityMenu.html b/front/dist/resources/html/gameQualityMenu.html deleted file mode 100644 index babb3f0e..00000000 --- a/front/dist/resources/html/gameQualityMenu.html +++ /dev/null @@ -1,81 +0,0 @@ - - - diff --git a/front/dist/resources/html/gameReport.html b/front/dist/resources/html/gameReport.html deleted file mode 100644 index d35ae556..00000000 --- a/front/dist/resources/html/gameReport.html +++ /dev/null @@ -1,115 +0,0 @@ - - -
-
- -

Moderate

-

What action do you want to take?

-
-
-

Block:

-

Block any communication from and to this user. This can be reverted.

-
- -
-
-
-

Report:

-

Send a report message to the administrators of this room. They may later ban this user.

-
-
-
Your message:
- -

-
-
- -
-
-
-
- diff --git a/front/dist/resources/html/gameShare.html b/front/dist/resources/html/gameShare.html deleted file mode 100644 index 404c8680..00000000 --- a/front/dist/resources/html/gameShare.html +++ /dev/null @@ -1,96 +0,0 @@ - - - diff --git a/front/dist/resources/logos/tcm_full.png b/front/dist/resources/logos/tcm_full.png new file mode 100644 index 00000000..3ea27990 Binary files /dev/null and b/front/dist/resources/logos/tcm_full.png differ diff --git a/front/dist/resources/logos/tcm_short.png b/front/dist/resources/logos/tcm_short.png new file mode 100644 index 00000000..ed55c836 Binary files /dev/null and b/front/dist/resources/logos/tcm_short.png differ diff --git a/front/dist/resources/logos/logo-WA-min.png b/front/dist/static/images/logo-WA-min.png similarity index 100% rename from front/dist/resources/logos/logo-WA-min.png rename to front/dist/static/images/logo-WA-min.png diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index ed723241..861acc22 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -15,7 +15,6 @@ import type { SetPropertyEvent } from "./setPropertyEvent"; import type { LoadSoundEvent } from "./LoadSoundEvent"; import type { PlaySoundEvent } from "./PlaySoundEvent"; import type { MenuItemClickedEvent } from "./ui/MenuItemClickedEvent"; -import type { MenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent"; import type { HasPlayerMovedEvent } from "./HasPlayerMovedEvent"; import type { SetTilesEvent } from "./SetTilesEvent"; import type { SetVariableEvent } from "./SetVariableEvent"; @@ -33,6 +32,7 @@ import type { TriggerActionMessageEvent, } from "./ui/TriggerActionMessageEvent"; import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent"; +import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent"; export interface TypedMessageEvent extends MessageEvent { data: T; @@ -63,7 +63,8 @@ export type IframeEventMap = { stopSound: null; getState: undefined; loadTileset: LoadTilesetEvent; - registerMenuCommand: MenuItemRegisterEvent; + registerMenu: MenuRegisterEvent; + unregisterMenu: UnregisterMenuEvent; setTiles: SetTilesEvent; modifyEmbeddedWebsite: Partial; // Note: name should be compulsory in fact }; diff --git a/front/src/Api/Events/SetVariableEvent.ts b/front/src/Api/Events/SetVariableEvent.ts index 3b4e9c85..3e2303b3 100644 --- a/front/src/Api/Events/SetVariableEvent.ts +++ b/front/src/Api/Events/SetVariableEvent.ts @@ -1,5 +1,4 @@ import * as tg from "generic-type-guard"; -import { isMenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent"; export const isSetVariableEvent = new tg.IsInterface() .withProperties({ diff --git a/front/src/Api/Events/ui/MenuItemRegisterEvent.ts b/front/src/Api/Events/ui/MenuItemRegisterEvent.ts deleted file mode 100644 index 404bdb13..00000000 --- a/front/src/Api/Events/ui/MenuItemRegisterEvent.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as tg from "generic-type-guard"; -import { Subject } from "rxjs"; - -export const isMenuItemRegisterEvent = new tg.IsInterface() - .withProperties({ - menutItem: tg.isString, - }) - .get(); -/** - * A message sent from the iFrame to the game to add a new menu item. - */ -export type MenuItemRegisterEvent = tg.GuardedType; - -export const isMenuItemRegisterIframeEvent = new tg.IsInterface() - .withProperties({ - type: tg.isSingletonString("registerMenuCommand"), - data: isMenuItemRegisterEvent, - }) - .get(); - -const _registerMenuCommandStream: Subject = new Subject(); -export const registerMenuCommandStream = _registerMenuCommandStream.asObservable(); - -export function handleMenuItemRegistrationEvent(event: MenuItemRegisterEvent) { - _registerMenuCommandStream.next(event.menutItem); -} diff --git a/front/src/Api/Events/ui/MenuRegisterEvent.ts b/front/src/Api/Events/ui/MenuRegisterEvent.ts new file mode 100644 index 00000000..f620745f --- /dev/null +++ b/front/src/Api/Events/ui/MenuRegisterEvent.ts @@ -0,0 +1,31 @@ +import * as tg from "generic-type-guard"; + +/** + * A message sent from a script to the game to remove a custom menu from the menu + */ +export const isUnregisterMenuEvent = new tg.IsInterface() + .withProperties({ + name: tg.isString, + }) + .get(); + +export type UnregisterMenuEvent = tg.GuardedType; + +export const isMenuRegisterOptions = new tg.IsInterface() + .withProperties({ + allowApi: tg.isBoolean, + }) + .get(); + +/** + * A message sent from a script to the game to add a custom menu from the menu + */ +export const isMenuRegisterEvent = new tg.IsInterface() + .withProperties({ + name: tg.isString, + iframe: tg.isUnion(tg.isString, tg.isUndefined), + options: isMenuRegisterOptions, + }) + .get(); + +export type MenuRegisterEvent = tg.GuardedType; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 4dde1b7d..140d2f34 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -29,11 +29,12 @@ import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent" import { isLayerEvent, LayerEvent } from "./Events/LayerEvent"; import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent"; import { isLoadPageEvent } from "./Events/LoadPageEvent"; -import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent"; +import { isMenuRegisterEvent, isUnregisterMenuEvent } from "./Events/ui/MenuRegisterEvent"; import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent"; import type { SetVariableEvent } from "./Events/SetVariableEvent"; import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent"; import { EmbeddedWebsite } from "./iframe/Room/EmbeddedWebsite"; +import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore"; type AnswererCallback = ( query: IframeQueryMap[T]["query"], @@ -93,12 +94,6 @@ class IframeListener { private readonly _setPropertyStream: Subject = new Subject(); public readonly setPropertyStream = this._setPropertyStream.asObservable(); - private readonly _registerMenuCommandStream: Subject = new Subject(); - public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable(); - - private readonly _unregisterMenuCommandStream: Subject = new Subject(); - public readonly unregisterMenuCommandStream = this._unregisterMenuCommandStream.asObservable(); - private readonly _playSoundStream: Subject = new Subject(); public readonly playSoundStream = this._playSoundStream.asObservable(); @@ -260,17 +255,23 @@ class IframeListener { this._removeBubbleStream.next(); } else if (payload.type == "onPlayerMove") { this.sendPlayerMove = true; - } else if (isMenuItemRegisterIframeEvent(payload)) { - const data = payload.data.menutItem; - // @ts-ignore - this.iframeCloseCallbacks.get(iframe).push(() => { - this._unregisterMenuCommandStream.next(data); - }); - handleMenuItemRegistrationEvent(payload.data); } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) { this._setTilesStream.next(payload.data); } else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) { this._modifyEmbeddedWebsiteStream.next(payload.data); + } else if (payload.type == "registerMenu" && isMenuRegisterEvent(payload.data)) { + const dataName = payload.data.name; + this.iframeCloseCallbacks.get(iframe)?.push(() => { + handleMenuUnregisterEvent(dataName); + }); + handleMenuRegistrationEvent( + payload.data.name, + payload.data.iframe, + foundSrc, + payload.data.options + ); + } else if (payload.type == "unregisterMenu" && isUnregisterMenuEvent(payload.data)) { + handleMenuUnregisterEvent(payload.data.name); } } }, @@ -293,57 +294,67 @@ class IframeListener { this.iframes.delete(iframe); } - registerScript(scriptUrl: string): void { - console.log("Loading map related script at ", scriptUrl); + registerScript(scriptUrl: string): Promise { + return new Promise((resolve, reject) => { + console.log("Loading map related script at ", scriptUrl); - if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") { - // Using external iframe mode ( - const iframe = document.createElement("iframe"); - iframe.id = IframeListener.getIFrameId(scriptUrl); - iframe.style.display = "none"; - iframe.src = "/iframe.html?script=" + encodeURIComponent(scriptUrl); + if (!process.env.NODE_ENV || process.env.NODE_ENV === "development") { + // Using external iframe mode ( + const iframe = document.createElement("iframe"); + iframe.id = IframeListener.getIFrameId(scriptUrl); + iframe.style.display = "none"; + iframe.src = "/iframe.html?script=" + encodeURIComponent(scriptUrl); - // We are putting a sandbox on this script because it will run in the same domain as the main website. - iframe.sandbox.add("allow-scripts"); - iframe.sandbox.add("allow-top-navigation-by-user-activation"); + // We are putting a sandbox on this script because it will run in the same domain as the main website. + iframe.sandbox.add("allow-scripts"); + iframe.sandbox.add("allow-top-navigation-by-user-activation"); - document.body.prepend(iframe); + iframe.addEventListener("load", () => { + resolve(); + }); - this.scripts.set(scriptUrl, iframe); - this.registerIframe(iframe); - } else { - // production code - const iframe = document.createElement("iframe"); - iframe.id = IframeListener.getIFrameId(scriptUrl); - iframe.style.display = "none"; + document.body.prepend(iframe); - // We are putting a sandbox on this script because it will run in the same domain as the main website. - iframe.sandbox.add("allow-scripts"); - iframe.sandbox.add("allow-top-navigation-by-user-activation"); + this.scripts.set(scriptUrl, iframe); + this.registerIframe(iframe); + } else { + // production code + const iframe = document.createElement("iframe"); + iframe.id = IframeListener.getIFrameId(scriptUrl); + iframe.style.display = "none"; - //iframe.src = "data:text/html;charset=utf-8," + escape(html); - iframe.srcdoc = - "\n" + - "\n" + - '\n' + - "\n" + - '\n' + - '\n' + - "\n" + - "\n" + - "\n"; + // We are putting a sandbox on this script because it will run in the same domain as the main website. + iframe.sandbox.add("allow-scripts"); + iframe.sandbox.add("allow-top-navigation-by-user-activation"); - document.body.prepend(iframe); + //iframe.src = "data:text/html;charset=utf-8," + escape(html); + iframe.srcdoc = + "\n" + + "\n" + + '\n' + + "\n" + + '\n' + + '\n' + + "\n" + + "\n" + + "\n"; - this.scripts.set(scriptUrl, iframe); - this.registerIframe(iframe); - } + iframe.addEventListener("load", () => { + resolve(); + }); + + document.body.prepend(iframe); + + this.scripts.set(scriptUrl, iframe); + this.registerIframe(iframe); + } + }); } private getBaseUrl(src: string, source: MessageEventSource | null): string { diff --git a/front/src/Api/iframe/Sound/Sound.ts b/front/src/Api/iframe/Sound/Sound.ts index 3bb3251a..132176c2 100644 --- a/front/src/Api/iframe/Sound/Sound.ts +++ b/front/src/Api/iframe/Sound/Sound.ts @@ -1,38 +1,35 @@ -import {sendToWorkadventure} from "../IframeApiContribution"; -import type {LoadSoundEvent} from "../../Events/LoadSoundEvent"; -import type {PlaySoundEvent} from "../../Events/PlaySoundEvent"; -import type {StopSoundEvent} from "../../Events/StopSoundEvent"; +import { sendToWorkadventure } from "../IframeApiContribution"; +import type { LoadSoundEvent } from "../../Events/LoadSoundEvent"; +import type { PlaySoundEvent } from "../../Events/PlaySoundEvent"; +import type { StopSoundEvent } from "../../Events/StopSoundEvent"; import SoundConfig = Phaser.Types.Sound.SoundConfig; export class Sound { constructor(private url: string) { sendToWorkadventure({ - "type": 'loadSound', - "data": { + type: "loadSound", + data: { url: this.url, - } as LoadSoundEvent - + } as LoadSoundEvent, }); } - public play(config: SoundConfig) { + public play(config: SoundConfig | undefined) { sendToWorkadventure({ - "type": 'playSound', - "data": { + type: "playSound", + data: { url: this.url, - config - } as PlaySoundEvent - + config, + } as PlaySoundEvent, }); return this.url; } public stop() { sendToWorkadventure({ - "type": 'stopSound', - "data": { + type: "stopSound", + data: { url: this.url, - } as StopSoundEvent - + } as StopSoundEvent, }); return this.url; } diff --git a/front/src/Api/iframe/Ui/Menu.ts b/front/src/Api/iframe/Ui/Menu.ts new file mode 100644 index 00000000..c0fe772e --- /dev/null +++ b/front/src/Api/iframe/Ui/Menu.ts @@ -0,0 +1,17 @@ +import { sendToWorkadventure } from "../IframeApiContribution"; + +export class Menu { + constructor(private menuName: string) {} + + /** + * remove the menu + */ + public remove() { + sendToWorkadventure({ + type: "unregisterMenu", + data: { + name: this.menuName, + }, + }); + } +} diff --git a/front/src/Api/iframe/state.ts b/front/src/Api/iframe/state.ts index 3b551864..a875f3e0 100644 --- a/front/src/Api/iframe/state.ts +++ b/front/src/Api/iframe/state.ts @@ -62,6 +62,10 @@ export class WorkadventureStateCommands extends IframeApiContribution { let subject = variableSubscribers.get(key); if (subject === undefined) { @@ -85,6 +89,12 @@ const proxyCommand = new Proxy(new WorkadventureStateCommands(), { target.saveVariable(p.toString(), value); return true; }, + has(target: WorkadventureStateCommands, p: PropertyKey): boolean { + if (p in target) { + return true; + } + return target.hasVariable(p.toString()); + }, }) as WorkadventureStateCommands & { [key: string]: unknown }; export default proxyCommand; diff --git a/front/src/Api/iframe/ui.ts b/front/src/Api/iframe/ui.ts index ab5b2007..c4d40d16 100644 --- a/front/src/Api/iframe/ui.ts +++ b/front/src/Api/iframe/ui.ts @@ -6,6 +6,8 @@ import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescrip import { Popup } from "./Ui/Popup"; import { ActionMessage } from "./Ui/ActionMessage"; import { isMessageReferenceEvent } from "../Events/ui/TriggerActionMessageEvent"; +import { Menu } from "./Ui/Menu"; +import type { RequireOnlyOne } from "../types"; let popupId = 0; const popups: Map = new Map(); @@ -14,9 +16,18 @@ const popupCallbacks: Map> = new Map< Map >(); +const menus: Map = new Map(); const menuCallbacks: Map void> = new Map(); const actionMessages = new Map(); +interface MenuDescriptor { + callback?: (commandDescriptor: string) => void; + iframe?: string; + allowApi?: boolean; +} + +export type MenuOptions = RequireOnlyOne; + interface ZonedPopupOptions { zone: string; objectLayerName?: string; @@ -52,6 +63,10 @@ export class WorkAdventureUiCommands extends IframeApiContribution { const callback = menuCallbacks.get(event.menuItem); + const menu = menus.get(event.menuItem); + if (menu === undefined) { + throw new Error('Could not find menu named "' + event.menuItem + '"'); + } if (callback) { callback(event.menuItem); } @@ -104,14 +119,53 @@ export class WorkAdventureUiCommands extends IframeApiContribution void) { - menuCallbacks.set(commandDescriptor, callback); - sendToWorkadventure({ - type: "registerMenuCommand", - data: { - menutItem: commandDescriptor, - }, - }); + registerMenuCommand(commandDescriptor: string, options: MenuOptions | ((commandDescriptor: string) => void)): Menu { + const menu = new Menu(commandDescriptor); + + if (typeof options === "function") { + menuCallbacks.set(commandDescriptor, options); + sendToWorkadventure({ + type: "registerMenu", + data: { + name: commandDescriptor, + options: { + allowApi: false, + }, + }, + }); + } else { + options.allowApi = options.allowApi === undefined ? options.iframe !== undefined : options.allowApi; + + if (options.iframe !== undefined) { + sendToWorkadventure({ + type: "registerMenu", + data: { + name: commandDescriptor, + iframe: options.iframe, + options: { + allowApi: options.allowApi, + }, + }, + }); + } else if (options.callback !== undefined) { + menuCallbacks.set(commandDescriptor, options.callback); + sendToWorkadventure({ + type: "registerMenu", + data: { + name: commandDescriptor, + options: { + allowApi: options.allowApi, + }, + }, + }); + } else { + throw new Error( + "When adding a menu with WA.ui.registerMenuCommand, you must pass either an iframe or a callback" + ); + } + } + menus.set(commandDescriptor, menu); + return menu; } displayBubble(): void { diff --git a/front/src/Api/types.ts b/front/src/Api/types.ts new file mode 100644 index 00000000..7d1a2107 --- /dev/null +++ b/front/src/Api/types.ts @@ -0,0 +1,4 @@ +export type RequireOnlyOne = Pick> & + { + [K in keys]-?: Required> & Partial, undefined>>; + }[keys]; diff --git a/front/src/Components/App.svelte b/front/src/Components/App.svelte index d65f699e..8b033e5f 100644 --- a/front/src/Components/App.svelte +++ b/front/src/Components/App.svelte @@ -1,4 +1,6 @@
@@ -55,22 +62,22 @@ Switch to presentation mode {/if}
-
- {#if $requestedScreenSharingState} +
+ {#if $requestedScreenSharingState && !isSilent} Start screen sharing {:else} Stop screen sharing {/if}
-
- {#if $requestedCameraState} +
+ {#if $requestedCameraState && !isSilent} Turn on webcam {:else} Turn off webcam {/if}
-
- {#if $requestedMicrophoneState} +
+ {#if $requestedMicrophoneState && !isSilent} Turn on microphone {:else} Turn off microphone diff --git a/front/src/Components/ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte b/front/src/Components/ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte deleted file mode 100644 index c1811650..00000000 --- a/front/src/Components/ConsoleGlobalMessageManager/ConsoleGlobalMessageManager.svelte +++ /dev/null @@ -1,152 +0,0 @@ - - - - -
- -
-
-

Global Message

- -
-
- {#if inputSendTextActive} - - {/if} - {#if uploadMusicActive} - - {/if} -
- -
-
- - - - diff --git a/front/src/Components/Menu/AboutRoomSubMenu.svelte b/front/src/Components/Menu/AboutRoomSubMenu.svelte new file mode 100644 index 00000000..3ccc9669 --- /dev/null +++ b/front/src/Components/Menu/AboutRoomSubMenu.svelte @@ -0,0 +1,147 @@ + + +
+ +
+

Share the link of the room !

+ +
+

Information on the map

+
+

{mapName}

+

{mapDescription}

+

expandedMapCopyright = !expandedMapCopyright}>Copyrights of the map

+ +

expandedTilesetCopyright = !expandedTilesetCopyright}>Copyrights of the tilesets

+ +
+
+ + + \ No newline at end of file diff --git a/front/src/Components/ConsoleGlobalMessageManager/UploadAudioGlobalMessage.svelte b/front/src/Components/Menu/AudioGlobalMessage.svelte similarity index 81% rename from front/src/Components/ConsoleGlobalMessageManager/UploadAudioGlobalMessage.svelte rename to front/src/Components/Menu/AudioGlobalMessage.svelte index 91f462c8..1704e732 100644 --- a/front/src/Components/ConsoleGlobalMessageManager/UploadAudioGlobalMessage.svelte +++ b/front/src/Components/Menu/AudioGlobalMessage.svelte @@ -1,20 +1,15 @@ @@ -105,24 +94,17 @@ img { flex: 1 1 auto; - max-height: 80%; margin-bottom: 20px; } - p { - flex: 1 1 auto; - margin-bottom: 5px; - color: whitesmoke; font-size: 1rem; - &.err { color: #ce372b; } } - input { display: none; } diff --git a/front/src/Components/Menu/ContactSubMenu.svelte b/front/src/Components/Menu/ContactSubMenu.svelte new file mode 100644 index 00000000..6cca0609 --- /dev/null +++ b/front/src/Components/Menu/ContactSubMenu.svelte @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/front/src/Components/Menu/CreateMapSubMenu.svelte b/front/src/Components/Menu/CreateMapSubMenu.svelte new file mode 100644 index 00000000..6cce71ac --- /dev/null +++ b/front/src/Components/Menu/CreateMapSubMenu.svelte @@ -0,0 +1,51 @@ + + +
+
+
+

Getting started

+

+ WorkAdventure allows you to create an online space to communicate spontaneously with others. + And it all starts with creating your own space. Choose from a large selection of prefabricated maps by our team. +

+ +
+
+

Create your map

+

You can also create your own custom map by following the step of the documentation.

+ +
+
+
+ + \ No newline at end of file diff --git a/front/src/Components/Menu/CustomSubMenu.svelte b/front/src/Components/Menu/CustomSubMenu.svelte new file mode 100644 index 00000000..f85499c3 --- /dev/null +++ b/front/src/Components/Menu/CustomSubMenu.svelte @@ -0,0 +1,33 @@ + + + + + + \ No newline at end of file diff --git a/front/src/Components/Menu/GlobalMessagesSubMenu.svelte b/front/src/Components/Menu/GlobalMessagesSubMenu.svelte new file mode 100644 index 00000000..8ec66de9 --- /dev/null +++ b/front/src/Components/Menu/GlobalMessagesSubMenu.svelte @@ -0,0 +1,118 @@ + + +
+
+
+ +
+
+ +
+
+
+ {#if inputSendTextActive} + + {/if} + {#if uploadAudioActive} + + {/if} +
+ +
+ + + + + \ No newline at end of file diff --git a/front/src/Components/Menu/Menu.svelte b/front/src/Components/Menu/Menu.svelte new file mode 100644 index 00000000..4086a9ae --- /dev/null +++ b/front/src/Components/Menu/Menu.svelte @@ -0,0 +1,154 @@ + + + + + + + + \ No newline at end of file diff --git a/front/src/Components/Menu/MenuIcon.svelte b/front/src/Components/Menu/MenuIcon.svelte index 241bf45f..2da9e870 100644 --- a/front/src/Components/Menu/MenuIcon.svelte +++ b/front/src/Components/Menu/MenuIcon.svelte @@ -1,33 +1,40 @@ + +
-
- -
+ open menu
diff --git a/front/src/Components/Menu/ProfileSubMenu.svelte b/front/src/Components/Menu/ProfileSubMenu.svelte new file mode 100644 index 00000000..39214b4f --- /dev/null +++ b/front/src/Components/Menu/ProfileSubMenu.svelte @@ -0,0 +1,116 @@ + + +
+ {#if $userIsConnected} +
+ {#if PROFILE_URL != undefined} + + {/if} +
+
+ +
+ {:else} +
+ Sing in +
+ {/if} +
+ + + +
+
+ +
+ +
+ + \ No newline at end of file diff --git a/front/src/Components/Menu/SettingsSubMenu.svelte b/front/src/Components/Menu/SettingsSubMenu.svelte new file mode 100644 index 00000000..4c0c62dd --- /dev/null +++ b/front/src/Components/Menu/SettingsSubMenu.svelte @@ -0,0 +1,140 @@ + + +
+
+

Game quality

+
+ +
+
+
+

Video quality

+
+ +
+
+
+

(Saving these settings will restart the game)

+ +
+
+ + +
+
+ + \ No newline at end of file diff --git a/front/src/Components/ConsoleGlobalMessageManager/InputTextGlobalMessage.svelte b/front/src/Components/Menu/TextGlobalMessage.svelte similarity index 69% rename from front/src/Components/ConsoleGlobalMessageManager/InputTextGlobalMessage.svelte rename to front/src/Components/Menu/TextGlobalMessage.svelte index 7baa8226..1749ba7d 100644 --- a/front/src/Components/ConsoleGlobalMessageManager/InputTextGlobalMessage.svelte +++ b/front/src/Components/Menu/TextGlobalMessage.svelte @@ -1,8 +1,7 @@
-
+
diff --git a/front/src/Components/MyCamera.svelte b/front/src/Components/MyCamera.svelte index ed4154a9..67826859 100644 --- a/front/src/Components/MyCamera.svelte +++ b/front/src/Components/MyCamera.svelte @@ -1,27 +1,11 @@
-
- {#if $localStreamStore.type === "success" && $localStreamStore.stream } - +
+ {#if $localStreamStore.type === "success" && $localStreamStore.stream} + {/if}
+
+ Silent zone +
diff --git a/front/src/Components/ReportMenu/BlockSubMenu.svelte b/front/src/Components/ReportMenu/BlockSubMenu.svelte new file mode 100644 index 00000000..0ec04abc --- /dev/null +++ b/front/src/Components/ReportMenu/BlockSubMenu.svelte @@ -0,0 +1,44 @@ + + +
+

Block

+

Block any communication from and to {userName}. This can be reverted.

+ +
+ + + \ No newline at end of file diff --git a/front/src/Components/ReportMenu/ReportMenu.svelte b/front/src/Components/ReportMenu/ReportMenu.svelte new file mode 100644 index 00000000..7594b1c9 --- /dev/null +++ b/front/src/Components/ReportMenu/ReportMenu.svelte @@ -0,0 +1,141 @@ + + + + +
+
+

Moderate {userName}

+
+ +
+
+
+
+ +
+
+ +
+
+
+ {#if blockActive} + + {:else if reportActive} + + {:else } +

ERROR : There is no action selected.

+ {/if} +
+
+ + \ No newline at end of file diff --git a/front/src/Components/ReportMenu/ReportSubMenu.svelte b/front/src/Components/ReportMenu/ReportSubMenu.svelte new file mode 100644 index 00000000..45167cc0 --- /dev/null +++ b/front/src/Components/ReportMenu/ReportSubMenu.svelte @@ -0,0 +1,55 @@ + + +
+

Report

+

Send a report message to the administrators of this room. They may later ban this user.

+
+
+ + +
+
+ +
+
+
+ + \ No newline at end of file diff --git a/front/src/Components/Video/PresentationLayout.svelte b/front/src/Components/Video/PresentationLayout.svelte index f68dd2f1..65a229f4 100644 --- a/front/src/Components/Video/PresentationLayout.svelte +++ b/front/src/Components/Video/PresentationLayout.svelte @@ -12,7 +12,9 @@
{#if $videoFocusStore } - + {#key $videoFocusStore.uniqueId} + + {/key} {/if}