diff --git a/CHANGELOG.md b/CHANGELOG.md index ef120a3f..e33a4f97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Version develop +### Updates +- Added multi Co-Website management + ### Bugfix - Moving a discussion over a user will now add this user to the discussion - Being in a silent zone new forces mediaConstraints to false (#1508) diff --git a/docs/maps/api-nav.md b/docs/maps/api-nav.md index f5721063..47ee416e 100644 --- a/docs/maps/api-nav.md +++ b/docs/maps/api-nav.md @@ -49,19 +49,34 @@ WA.nav.goToRoom('../otherMap/map.json'); WA.nav.goToRoom("/_/global/.json#start-layer-2") ``` -### Opening/closing a web page in an iFrame +### Opening/closing web page in Co-Websites ``` -WA.nav.openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void -WA.nav.closeCoWebSite(): void +WA.nav.openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = "", position: number = 0): Promise ``` -Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. `allowApi` allows the webpage to use the "IFrame API" and execute script (it is equivalent to putting the `openWebsiteAllowApi` property in the map). `allowPolicy` grants additional access rights to the iFrame. The `allowPolicy` parameter is turned into an [`allow` feature policy in the iFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allow). +Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. `allowApi` allows the webpage to use the "IFrame API" and execute script (it is equivalent to putting the `openWebsiteAllowApi` property in the map). `allowPolicy` grants additional access rights to the iFrame. The `allowPolicy` parameter is turned into an [`allow` feature policy in the iFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allow), position in whitch slot the web page will be open. +You can have only 5 co-wbesites open simultaneously. Example: ```javascript -WA.nav.openCoWebSite('https://www.wikipedia.org/'); +const coWebsite = await WA.nav.openCoWebSite('https://www.wikipedia.org/'); +const coWebsiteWorkAdventure = await WA.nav.openCoWebSite('https://workadventu.re/', true, "", 1); // ... -WA.nav.closeCoWebSite(); +coWebsite.close(); +``` + +### Get all Co-Websites + +``` +WA.nav.getCoWebSites(): Promise +``` + +Get all opened co-websites with their ids and positions. + +Example: + +```javascript +const coWebsites = await WA.nav.getCowebSites(); ``` diff --git a/docs/maps/opening-a-website.md b/docs/maps/opening-a-website.md index ec6c82d4..64b19f1c 100644 --- a/docs/maps/opening-a-website.md +++ b/docs/maps/opening-a-website.md @@ -5,7 +5,7 @@ ## The openWebsite property -On your map, you can define special zones. When a player will pass over these zones, a website will open (as an iframe +On your map, you can define special zones. When a player will pass over these zones, a website will open (as an iframe on the right side of the screen) In order to create a zone that opens websites: @@ -16,7 +16,7 @@ In order to create a zone that opens websites: * You may also use "`openTab`" property (of type "`string`") to open in a new tab instead. {.alert.alert-warning} -A website can explicitly forbid another website from loading it in an iFrame using +A website can explicitly forbid another website from loading it in an iFrame using the [X-Frame-Options HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options). ## Integrating a Youtube video @@ -64,3 +64,13 @@ For instance, if you want an iFrame to be able to go in fullscreen, you will use
The generated iFrame will have the allow attribute set to: <iframe allow="fullscreen">
+ +### Open a Jitsi with a co-website + +Cowebsites allow you to have several sites open at the same time. + +If you want to open a Jitsi and another page it's easy! + +You have just to [add a Jitsi to the map](meeting-rooms.md) and [add a co-website](opening-a-website.md#the-openwebsite-property) on the same layer. + +It's done! diff --git a/front/dist/index.tmpl.html b/front/dist/index.tmpl.html index 0c89b611..3b43a5ef 100644 --- a/front/dist/index.tmpl.html +++ b/front/dist/index.tmpl.html @@ -37,6 +37,54 @@
+
+
+
+
+ + +
+
+
+ +
+
+
+
+ +
+
+ + +
+
+
+ +
+
+
+ +
+
+ + +
+
+
+ +
+
+
+ +
+
+ + +
+
+
+
+
@@ -48,19 +96,24 @@
+
diff --git a/front/src/Api/Events/CloseCoWebsiteEvent.ts b/front/src/Api/Events/CloseCoWebsiteEvent.ts new file mode 100644 index 00000000..94167d5e --- /dev/null +++ b/front/src/Api/Events/CloseCoWebsiteEvent.ts @@ -0,0 +1,12 @@ +import * as tg from "generic-type-guard"; + +export const isCloseCoWebsite = new tg.IsInterface() + .withProperties({ + id: tg.isOptional(tg.isString) + }) + .get(); + +/** + * A message sent from the iFrame to the game to add a message in the chat. + */ +export type CloseCoWebsiteEvent = tg.GuardedType; diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 861acc22..9e31b46c 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -5,11 +5,10 @@ import type { ClosePopupEvent } from "./ClosePopupEvent"; import type { EnterLeaveEvent } from "./EnterLeaveEvent"; import type { GoToPageEvent } from "./GoToPageEvent"; import type { LoadPageEvent } from "./LoadPageEvent"; -import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent"; +import { isCoWebsite, isOpenCoWebsiteEvent } from "./OpenCoWebsiteEvent"; import type { OpenPopupEvent } from "./OpenPopupEvent"; import type { OpenTabEvent } from "./OpenTabEvent"; import type { UserInputChatEvent } from "./UserInputChatEvent"; -import type { MapDataEvent } from "./MapDataEvent"; import type { LayerEvent } from "./LayerEvent"; import type { SetPropertyEvent } from "./setPropertyEvent"; import type { LoadSoundEvent } from "./LoadSoundEvent"; @@ -27,9 +26,6 @@ import type { LoadTilesetEvent } from "./LoadTilesetEvent"; import { isLoadTilesetEvent } from "./LoadTilesetEvent"; import type { MessageReferenceEvent, - removeActionMessage, - triggerActionMessage, - TriggerActionMessageEvent, } from "./ui/TriggerActionMessageEvent"; import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent"; import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEvent"; @@ -48,8 +44,6 @@ export type IframeEventMap = { closePopup: ClosePopupEvent; openTab: OpenTabEvent; goToPage: GoToPageEvent; - openCoWebSite: OpenCoWebSiteEvent; - closeCoWebSite: null; disablePlayerControls: null; restorePlayerControls: null; displayBubble: null; @@ -118,6 +112,22 @@ export const iframeQueryMapTypeGuards = { query: isLoadTilesetEvent, answer: tg.isNumber, }, + openCoWebsite: { + query: isOpenCoWebsiteEvent, + answer: isCoWebsite + }, + getCoWebsites: { + query: tg.isUndefined, + answer: tg.isArray(isCoWebsite) + }, + closeCoWebsite: { + query: tg.isString, + answer: tg.isUndefined + }, + closeCoWebsites: { + query: tg.isUndefined, + answer: tg.isUndefined + }, triggerActionMessage: { query: isTriggerActionMessageEvent, answer: tg.isUndefined, diff --git a/front/src/Api/Events/OpenCoWebSiteEvent.ts b/front/src/Api/Events/OpenCoWebSiteEvent.ts deleted file mode 100644 index 7b5e6070..00000000 --- a/front/src/Api/Events/OpenCoWebSiteEvent.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as tg from "generic-type-guard"; - -export const isOpenCoWebsite = new tg.IsInterface() - .withProperties({ - url: tg.isString, - allowApi: tg.isBoolean, - allowPolicy: tg.isString, - }) - .get(); - -/** - * A message sent from the iFrame to the game to add a message in the chat. - */ -export type OpenCoWebSiteEvent = tg.GuardedType; diff --git a/front/src/Api/Events/OpenCoWebsiteEvent.ts b/front/src/Api/Events/OpenCoWebsiteEvent.ts new file mode 100644 index 00000000..9c02b7a3 --- /dev/null +++ b/front/src/Api/Events/OpenCoWebsiteEvent.ts @@ -0,0 +1,22 @@ +import * as tg from "generic-type-guard"; + +export const isOpenCoWebsiteEvent = new tg.IsInterface() + .withProperties({ + url: tg.isString, + allowApi: tg.isOptional(tg.isBoolean), + allowPolicy: tg.isOptional(tg.isString), + position: tg.isOptional(tg.isNumber) + }) + .get(); + +export const isCoWebsite = new tg.IsInterface() + .withProperties({ + id: tg.isString, + position: tg.isNumber, + }) + .get(); + +/** + * A message sent from the iFrame to the game to add a message in the chat. + */ +export type OpenCoWebsiteEvent = tg.GuardedType; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 5a9aca85..caa59420 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -1,6 +1,5 @@ import { Subject } from "rxjs"; -import type * as tg from "generic-type-guard"; -import { ChatEvent, isChatEvent } from "./Events/ChatEvent"; +import { isChatEvent } from "./Events/ChatEvent"; import { HtmlUtils } from "../WebRtc/HtmlUtils"; import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent"; import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent"; @@ -8,18 +7,15 @@ import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent"; import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent"; import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent"; import { scriptUtils } from "./ScriptUtils"; -import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent"; -import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent"; +import { isGoToPageEvent } from "./Events/GoToPageEvent"; +import { isCloseCoWebsite, CloseCoWebsiteEvent } from "./Events/CloseCoWebsiteEvent"; import { IframeErrorAnswerEvent, - IframeEvent, - IframeEventMap, IframeQueryMap, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, isIframeQueryWrapper, - TypedMessageEvent, } from "./Events/IframeEvent"; import type { UserInputChatEvent } from "./Events/UserInputChatEvent"; import { isPlaySoundEvent, PlaySoundEvent } from "./Events/PlaySoundEvent"; @@ -33,7 +29,6 @@ import { isMenuRegisterEvent, isUnregisterMenuEvent } from "./Events/ui/MenuRegi 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 = ( @@ -53,10 +48,7 @@ class IframeListener { public readonly openTabStream = this._openTabStream.asObservable(); private readonly _loadPageStream: Subject = new Subject(); - public readonly loadPageStream = this._loadPageStream.asObservable(); - - private readonly _openCoWebSiteStream: Subject = new Subject(); - public readonly openCoWebSiteStream = this._openCoWebSiteStream.asObservable(); + public readonly loadPageStream = this._loadPageStream.asObservable() private readonly _disablePlayerControlStream: Subject = new Subject(); public readonly disablePlayerControlStream = this._disablePlayerControlStream.asObservable(); @@ -138,8 +130,6 @@ class IframeListener { return; } - foundSrc = this.getBaseUrl(foundSrc, message.source); - if (isIframeQueryWrapper(payload)) { const queryId = payload.id; const query = payload.query; @@ -224,15 +214,6 @@ class IframeListener { this._stopSoundStream.next(payload.data); } else if (payload.type === "loadSound" && isLoadSoundEvent(payload.data)) { this._loadSoundStream.next(payload.data); - } else if (payload.type === "openCoWebSite" && isOpenCoWebsite(payload.data)) { - scriptUtils.openCoWebsite( - payload.data.url, - foundSrc, - payload.data.allowApi, - payload.data.allowPolicy - ); - } else if (payload.type === "closeCoWebSite") { - scriptUtils.closeCoWebSite(); } else if (payload.type === "disablePlayerControls") { this._disablePlayerControlStream.next(); } else if (payload.type === "restorePlayerControls") { @@ -252,6 +233,9 @@ class IframeListener { this.iframeCloseCallbacks.get(iframe)?.push(() => { handleMenuUnregisterEvent(dataName); }); + + foundSrc = this.getBaseUrl(foundSrc, message.source); + handleMenuRegistrationEvent( payload.data.name, payload.data.iframe, @@ -354,6 +338,20 @@ class IframeListener { return src; } + public getBaseUrlFromSource(source: MessageEventSource): string { + let foundSrc: string | undefined; + let iframe: HTMLIFrameElement | undefined; + + for (iframe of this.iframes) { + if (iframe.contentWindow === source) { + foundSrc = iframe.src; + break; + } + } + + return this.getBaseUrl(foundSrc ?? "", source); + } + private static getIFrameId(scriptUrl: string): string { return "script" + btoa(scriptUrl); } diff --git a/front/src/Api/ScriptUtils.ts b/front/src/Api/ScriptUtils.ts index ad6dcc0f..10a80c92 100644 --- a/front/src/Api/ScriptUtils.ts +++ b/front/src/Api/ScriptUtils.ts @@ -1,4 +1,4 @@ -import { coWebsiteManager } from "../WebRtc/CoWebsiteManager"; +import { coWebsiteManager, CoWebsite } from "../WebRtc/CoWebsiteManager"; import { playersStore } from "../Stores/PlayersStore"; import { chatMessagesStore } from "../Stores/ChatStore"; import type { ChatEvent } from "./Events/ChatEvent"; @@ -12,14 +12,6 @@ class ScriptUtils { window.location.href = url; } - public openCoWebsite(url: string, base: string, api: boolean, policy: string) { - coWebsiteManager.loadCoWebsite(url, base, api, policy); - } - - public closeCoWebSite() { - coWebsiteManager.closeCoWebsite(); - } - public sendAnonymousChat(chatEvent: ChatEvent) { const userId = playersStore.addFacticePlayer(chatEvent.author); chatMessagesStore.addExternalMessage(userId, chatEvent.message); diff --git a/front/src/Api/iframe/nav.ts b/front/src/Api/iframe/nav.ts index f051a7c0..5acfa2a5 100644 --- a/front/src/Api/iframe/nav.ts +++ b/front/src/Api/iframe/nav.ts @@ -1,8 +1,15 @@ -import type { GoToPageEvent } from "../Events/GoToPageEvent"; -import type { OpenTabEvent } from "../Events/OpenTabEvent"; -import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution"; -import type { OpenCoWebSiteEvent } from "../Events/OpenCoWebSiteEvent"; -import type { LoadPageEvent } from "../Events/LoadPageEvent"; +import { IframeApiContribution, sendToWorkadventure, queryWorkadventure } from "./IframeApiContribution"; + +export class CoWebsite { + constructor(private readonly id: string, public readonly position: number) {} + + close() { + return queryWorkadventure({ + type: "closeCoWebsite", + data: this.id, + }); + } +} export class WorkadventureNavigationCommands extends IframeApiContribution { callbacks = []; @@ -34,21 +41,34 @@ export class WorkadventureNavigationCommands extends IframeApiContribution { + const result = await queryWorkadventure({ + type: "openCoWebsite", data: { url, allowApi, allowPolicy, + position, }, }); + return new CoWebsite(result.id, result.position); } - closeCoWebSite(): void { - sendToWorkadventure({ - type: "closeCoWebSite", - data: null, + async getCoWebSites(): Promise { + const result = await queryWorkadventure({ + type: "getCoWebsites", + data: undefined + }); + return result.map((cowebsiteEvent) => new CoWebsite(cowebsiteEvent.id, cowebsiteEvent.position)); + } + + /** + * @deprecated Use closeCoWebsites instead to close all co-websites + */ + closeCoWebSite() { + return queryWorkadventure({ + type: "closeCoWebsites", + data: undefined, }); } } diff --git a/front/src/Components/Menu/Menu.svelte b/front/src/Components/Menu/Menu.svelte index 6cbef9c1..4eecb370 100644 --- a/front/src/Components/Menu/Menu.svelte +++ b/front/src/Components/Menu/Menu.svelte @@ -124,6 +124,7 @@ top: 10%; position: relative; + z-index: 80; margin: auto; display: grid; diff --git a/front/src/Components/Menu/MenuIcon.svelte b/front/src/Components/Menu/MenuIcon.svelte index 02d6eb53..fa177c88 100644 --- a/front/src/Components/Menu/MenuIcon.svelte +++ b/front/src/Components/Menu/MenuIcon.svelte @@ -29,6 +29,8 @@