From 4f068c72beaec72b4361e82e7181e0172de3dfb0 Mon Sep 17 00:00:00 2001 From: Alexis Faizeau Date: Wed, 5 Jan 2022 10:30:27 +0100 Subject: [PATCH] Co-website management move to Svelte --- front/src/Api/Events/OpenCoWebsiteEvent.ts | 3 +- front/src/Api/iframe/nav.ts | 17 +- .../AudioManager/AudioManager.svelte | 2 +- .../EmbedScreens/CamerasContainer.svelte | 32 + .../CoWebsiteThumbnailSlot.svelte | 100 ++ .../EmbedScreens/CoWebsitesContainer.svelte | 34 + .../EmbedScreens/EmbedScreensContainer.svelte | 22 + .../EmbedScreens/Layouts/MozaicLayout.svelte | 61 ++ .../Layouts/PresentationLayout.svelte | 169 ++++ .../src/Components/EmoteMenu/EmoteMenu.svelte | 2 +- .../Components/FollowMenu/FollowMenu.svelte | 2 +- .../HelpCameraSettingsPopup.svelte | 2 +- front/src/Components/MainLayout.svelte | 25 +- front/src/Components/Menu/MenuIcon.svelte | 3 + .../Components/Modal/LimitRoomModal.svelte | 1 + .../Components/Modal/ShareLinkMapModal.svelte | 1 + .../Components/ReportMenu/ReportMenu.svelte | 2 +- .../TypeMessage/BanMessageContainer.svelte | 6 + .../TypeMessage/TextMessageContainer.svelte | 3 +- front/src/Components/UI/AudioPlaying.svelte | 1 + front/src/Components/Video/MediaBox.svelte | 43 +- .../src/Components/VisitCard/VisitCard.svelte | 2 +- .../WarningContainer/WarningContainer.svelte | 2 +- .../Phaser/Game/GameMapPropertiesListener.ts | 87 +- front/src/Phaser/Game/GameScene.ts | 4 +- front/src/Stores/CoWebsiteStore.ts | 66 ++ front/src/Stores/EmbedScreensStore.ts | 51 ++ front/src/WebRtc/CoWebsiteManager.ts | 854 ++++++++++-------- front/src/WebRtc/JitsiFactory.ts | 104 +-- front/style/cowebsite.scss | 223 +---- front/style/cowebsite/_global.scss | 119 +++ front/style/cowebsite/_short-screens.scss | 84 ++ front/style/cowebsite/_wide-screens.scss | 41 + 33 files changed, 1409 insertions(+), 759 deletions(-) create mode 100644 front/src/Components/EmbedScreens/CamerasContainer.svelte create mode 100644 front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte create mode 100644 front/src/Components/EmbedScreens/CoWebsitesContainer.svelte create mode 100644 front/src/Components/EmbedScreens/EmbedScreensContainer.svelte create mode 100644 front/src/Components/EmbedScreens/Layouts/MozaicLayout.svelte create mode 100644 front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte create mode 100644 front/src/Stores/CoWebsiteStore.ts create mode 100644 front/src/Stores/EmbedScreensStore.ts create mode 100644 front/style/cowebsite/_global.scss create mode 100644 front/style/cowebsite/_short-screens.scss create mode 100644 front/style/cowebsite/_wide-screens.scss diff --git a/front/src/Api/Events/OpenCoWebsiteEvent.ts b/front/src/Api/Events/OpenCoWebsiteEvent.ts index 514fd110..51a17763 100644 --- a/front/src/Api/Events/OpenCoWebsiteEvent.ts +++ b/front/src/Api/Events/OpenCoWebsiteEvent.ts @@ -6,13 +6,14 @@ export const isOpenCoWebsiteEvent = new tg.IsInterface() allowApi: tg.isOptional(tg.isBoolean), allowPolicy: tg.isOptional(tg.isString), position: tg.isOptional(tg.isNumber), + closable: tg.isOptional(tg.isBoolean), + lazy: tg.isOptional(tg.isBoolean), }) .get(); export const isCoWebsite = new tg.IsInterface() .withProperties({ id: tg.isString, - position: tg.isNumber, }) .get(); diff --git a/front/src/Api/iframe/nav.ts b/front/src/Api/iframe/nav.ts index 206961bf..d5362b4b 100644 --- a/front/src/Api/iframe/nav.ts +++ b/front/src/Api/iframe/nav.ts @@ -1,7 +1,7 @@ import { IframeApiContribution, sendToWorkadventure, queryWorkadventure } from "./IframeApiContribution"; export class CoWebsite { - constructor(private readonly id: string, public readonly position: number) {} + constructor(private readonly id: string) {} close() { return queryWorkadventure({ @@ -41,7 +41,14 @@ export class WorkadventureNavigationCommands extends IframeApiContribution { + async openCoWebSite( + url: string, + allowApi?: boolean, + allowPolicy?: string, + position?: number, + closable?: boolean, + lazy?: boolean + ): Promise { const result = await queryWorkadventure({ type: "openCoWebsite", data: { @@ -49,9 +56,11 @@ export class WorkadventureNavigationCommands extends IframeApiContribution { @@ -59,7 +68,7 @@ export class WorkadventureNavigationCommands extends IframeApiContribution new CoWebsite(cowebsiteEvent.id, cowebsiteEvent.position)); + return result.map((cowebsiteEvent) => new CoWebsite(cowebsiteEvent.id)); } /** diff --git a/front/src/Components/AudioManager/AudioManager.svelte b/front/src/Components/AudioManager/AudioManager.svelte index 87b949c7..eec51572 100644 --- a/front/src/Components/AudioManager/AudioManager.svelte +++ b/front/src/Components/AudioManager/AudioManager.svelte @@ -166,7 +166,7 @@ margin-right: auto; left: 0; right: 0; - z-index: 200; + z-index: 550; background-color: rgb(0, 0, 0, 0.5); display: grid; diff --git a/front/src/Components/EmbedScreens/CamerasContainer.svelte b/front/src/Components/EmbedScreens/CamerasContainer.svelte new file mode 100644 index 00000000..208ae529 --- /dev/null +++ b/front/src/Components/EmbedScreens/CamerasContainer.svelte @@ -0,0 +1,32 @@ + + + + + diff --git a/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte b/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte new file mode 100644 index 00000000..a3895a52 --- /dev/null +++ b/front/src/Components/EmbedScreens/CoWebsiteThumbnailSlot.svelte @@ -0,0 +1,100 @@ + + +
+ +
+ + diff --git a/front/src/Components/EmbedScreens/CoWebsitesContainer.svelte b/front/src/Components/EmbedScreens/CoWebsitesContainer.svelte new file mode 100644 index 00000000..95000daf --- /dev/null +++ b/front/src/Components/EmbedScreens/CoWebsitesContainer.svelte @@ -0,0 +1,34 @@ + + +{#if $coWebsiteThumbails.length > 0} +
+ {#each [...$coWebsiteThumbails.values()] as coWebsite, index (coWebsite.iframe.id)} + + {/each} +
+{/if} + + diff --git a/front/src/Components/EmbedScreens/EmbedScreensContainer.svelte b/front/src/Components/EmbedScreens/EmbedScreensContainer.svelte new file mode 100644 index 00000000..79c59a58 --- /dev/null +++ b/front/src/Components/EmbedScreens/EmbedScreensContainer.svelte @@ -0,0 +1,22 @@ + + +
+ {#if $embedScreenLayout === LayoutMode.Presentation} + + {:else} + + {/if} +
+ + diff --git a/front/src/Components/EmbedScreens/Layouts/MozaicLayout.svelte b/front/src/Components/EmbedScreens/Layouts/MozaicLayout.svelte new file mode 100644 index 00000000..25ff16c8 --- /dev/null +++ b/front/src/Components/EmbedScreens/Layouts/MozaicLayout.svelte @@ -0,0 +1,61 @@ + + +
+
+ {#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)} + + {/each} +
+
+ + diff --git a/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte b/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte new file mode 100644 index 00000000..8cecfe12 --- /dev/null +++ b/front/src/Components/EmbedScreens/Layouts/PresentationLayout.svelte @@ -0,0 +1,169 @@ + + +
+ {#if displayFullMedias} +
+ +
+ {:else} +
+
+ {#if $highlightedEmbedScreen} + {#if $highlightedEmbedScreen.type === "streamable"} + {#key $highlightedEmbedScreen.embed.uniqueId} + + {/key} + {:else if $highlightedEmbedScreen.type === "cowebsite"} + {#key $highlightedEmbedScreen.embed.iframe.id} +
+
+ + + +
+
+ {/key} + {/if} + {/if} +
+ + {#if displayCoWebsiteContainer} + + {/if} +
+ + {#if $peerStore.size > 0} + + {/if} + {/if} +
+ + diff --git a/front/src/Components/EmoteMenu/EmoteMenu.svelte b/front/src/Components/EmoteMenu/EmoteMenu.svelte index 24f08812..ac068484 100644 --- a/front/src/Components/EmoteMenu/EmoteMenu.svelte +++ b/front/src/Components/EmoteMenu/EmoteMenu.svelte @@ -87,7 +87,7 @@ justify-content: center; align-items: center; position: absolute; - z-index: 101; + z-index: 300; } .emote-menu { diff --git a/front/src/Components/FollowMenu/FollowMenu.svelte b/front/src/Components/FollowMenu/FollowMenu.svelte index bc054443..0a0f5b68 100644 --- a/front/src/Components/FollowMenu/FollowMenu.svelte +++ b/front/src/Components/FollowMenu/FollowMenu.svelte @@ -121,7 +121,7 @@ right: 0; margin-left: auto; margin-right: auto; - z-index: 150; + z-index: 400; } div.interact-menu { diff --git a/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte b/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte index 2401399a..d4bb4ae0 100644 --- a/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte +++ b/front/src/Components/HelpCameraSettings/HelpCameraSettingsPopup.svelte @@ -60,7 +60,7 @@ margin-top: 4%; max-height: 80vh; max-width: 80vw; - z-index: 250; + z-index: 600; overflow: auto; text-align: center; diff --git a/front/src/Components/MainLayout.svelte b/front/src/Components/MainLayout.svelte index b6bfecda..6175f540 100644 --- a/front/src/Components/MainLayout.svelte +++ b/front/src/Components/MainLayout.svelte @@ -1,7 +1,7 @@ -
+
{#if streamable instanceof VideoPeer} - + {:else if streamable instanceof ScreenSharingPeer} - + {:else} - + {/if}
@@ -57,6 +64,32 @@ } } + &.mozaic-full-width { + width: 95%; + max-width: 95%; + margin-left: 3%; + margin-right: 3%; + margin-top: auto; + margin-bottom: auto; + + &:hover { + margin-top: auto; + margin-bottom: auto; + } + } + + &.mozaic-quarter { + width: 95%; + max-width: 95%; + margin-top: auto; + margin-bottom: auto; + + &:hover { + margin-top: auto; + margin-bottom: auto; + } + } + &.clickable { cursor: url("../../../style/images/cursor_pointer.png"), pointer; } diff --git a/front/src/Components/VisitCard/VisitCard.svelte b/front/src/Components/VisitCard/VisitCard.svelte index 94f19f93..57911638 100644 --- a/front/src/Components/VisitCard/VisitCard.svelte +++ b/front/src/Components/VisitCard/VisitCard.svelte @@ -57,7 +57,7 @@ height: 120px; margin: auto; animation: spin 2s linear infinite; - z-index: 102; + z-index: 350; } @keyframes spin { diff --git a/front/src/Components/WarningContainer/WarningContainer.svelte b/front/src/Components/WarningContainer/WarningContainer.svelte index c85d3bd7..b5def355 100644 --- a/front/src/Components/WarningContainer/WarningContainer.svelte +++ b/front/src/Components/WarningContainer/WarningContainer.svelte @@ -42,7 +42,7 @@ font-family: Lato; min-width: 300px; opacity: 0.9; - z-index: 270; + z-index: 700; h2 { padding: 5px; } diff --git a/front/src/Phaser/Game/GameMapPropertiesListener.ts b/front/src/Phaser/Game/GameMapPropertiesListener.ts index f6c28862..fec982d1 100644 --- a/front/src/Phaser/Game/GameMapPropertiesListener.ts +++ b/front/src/Phaser/Game/GameMapPropertiesListener.ts @@ -9,21 +9,21 @@ import { get } from "svelte/store"; import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager"; import type { ITiledMapLayer } from "../Map/ITiledMap"; import { GameMapProperties } from "./GameMapProperties"; +import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore"; enum OpenCoWebsiteState { - LOADING, + ASLEEP, OPENED, MUST_BE_CLOSE, } interface OpenCoWebsite { - coWebsite: CoWebsite | undefined; + coWebsite: CoWebsite; state: OpenCoWebsiteState; } export class GameMapPropertiesListener { private coWebsitesOpenByLayer = new Map(); - private coWebsitesActionTriggerByLayer = new Map(); constructor(private scene: GameScene, private gameMap: GameMap) {} @@ -64,10 +64,8 @@ export class GameMapPropertiesListener { let openWebsiteProperty: string | undefined; let allowApiProperty: boolean | undefined; let websitePolicyProperty: string | undefined; - let websiteWidthProperty: number | undefined; let websitePositionProperty: number | undefined; let websiteTriggerProperty: string | undefined; - let websiteTriggerMessageProperty: string | undefined; layer.properties.forEach((property) => { switch (property.name) { @@ -80,18 +78,12 @@ export class GameMapPropertiesListener { case GameMapProperties.OPEN_WEBSITE_POLICY: websitePolicyProperty = property.value as string | undefined; break; - case GameMapProperties.OPEN_WEBSITE_WIDTH: - websiteWidthProperty = property.value as number | undefined; - break; case GameMapProperties.OPEN_WEBSITE_POSITION: websitePositionProperty = property.value as number | undefined; break; case GameMapProperties.OPEN_WEBSITE_TRIGGER: websiteTriggerProperty = property.value as string | undefined; break; - case GameMapProperties.OPEN_WEBSITE_TRIGGER_MESSAGE: - websiteTriggerMessageProperty = property.value as string | undefined; - break; } }); @@ -105,27 +97,30 @@ export class GameMapPropertiesListener { return; } + const coWebsite = coWebsiteManager.addCoWebsite( + openWebsiteProperty, + this.scene.MapUrlFile, + allowApiProperty, + websitePolicyProperty, + websitePositionProperty, + false + ); + this.coWebsitesOpenByLayer.set(layer, { - coWebsite: undefined, - state: OpenCoWebsiteState.LOADING, + coWebsite: coWebsite, + state: OpenCoWebsiteState.ASLEEP, }); const openWebsiteFunction = () => { coWebsiteManager - .loadCoWebsite( - openWebsiteProperty as string, - this.scene.MapUrlFile, - allowApiProperty, - websitePolicyProperty, - websiteWidthProperty, - websitePositionProperty - ) + .loadCoWebsite(coWebsite) .then((coWebsite) => { const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer); if (coWebsiteOpen && coWebsiteOpen.state === OpenCoWebsiteState.MUST_BE_CLOSE) { - coWebsiteManager.closeCoWebsite(coWebsite).catch((e) => console.error(e)); + 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, { coWebsite, @@ -133,27 +128,17 @@ export class GameMapPropertiesListener { }); } }) - .catch((e) => console.error(e)); + .catch(() => { + console.error("Error during loading a co-website: " + coWebsite.url); + }); layoutManagerActionStore.removeAction(actionUuid); }; - const forceTrigger = localUserStore.getForceCowebsiteTrigger(); - if (forceTrigger || websiteTriggerProperty === ON_ACTION_TRIGGER_BUTTON) { - if (!websiteTriggerMessageProperty) { - websiteTriggerMessageProperty = "Press SPACE or touch here to open web site"; - } - - this.coWebsitesActionTriggerByLayer.set(layer, actionUuid); - - layoutManagerActionStore.addAction({ - uuid: actionUuid, - type: "message", - message: websiteTriggerMessageProperty, - callback: () => openWebsiteFunction(), - userInputManager: this.scene.userInputManager, - }); - } else { + if ( + !localUserStore.getForceCowebsiteTrigger() && + websiteTriggerProperty !== ON_ACTION_TRIGGER_BUTTON + ) { openWebsiteFunction(); } }); @@ -194,7 +179,7 @@ export class GameMapPropertiesListener { return; } - if (coWebsiteOpen.state === OpenCoWebsiteState.LOADING) { + if (coWebsiteOpen.state === OpenCoWebsiteState.ASLEEP) { coWebsiteOpen.state = OpenCoWebsiteState.MUST_BE_CLOSE; } @@ -203,26 +188,6 @@ export class GameMapPropertiesListener { } this.coWebsitesOpenByLayer.delete(layer); - - if (!websiteTriggerProperty) { - return; - } - - const actionStore = get(layoutManagerActionStore); - const actionTriggerUuid = this.coWebsitesActionTriggerByLayer.get(layer); - - if (!actionTriggerUuid) { - return; - } - - const action = - actionStore && actionStore.length > 0 - ? actionStore.find((action) => action.uuid === actionTriggerUuid) - : undefined; - - if (action) { - layoutManagerActionStore.removeAction(actionTriggerUuid); - } }); }; diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 9b93ce5b..5b10f5c3 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -2145,8 +2145,8 @@ ${escapedMessage} public stopJitsi(): void { const coWebsite = coWebsiteManager.searchJitsi(); if (coWebsite) { - coWebsiteManager.closeCoWebsite(coWebsite).catch(() => { - console.error("Error during Jitsi co-website closing"); + coWebsiteManager.closeCoWebsite(coWebsite).catch((e) => { + console.error("Error during Jitsi co-website closing", e); }); } } diff --git a/front/src/Stores/CoWebsiteStore.ts b/front/src/Stores/CoWebsiteStore.ts new file mode 100644 index 00000000..57779e58 --- /dev/null +++ b/front/src/Stores/CoWebsiteStore.ts @@ -0,0 +1,66 @@ +import { derived, get, writable } from "svelte/store"; +import type { CoWebsite } from "../WebRtc/CoWebsiteManager"; +import { highlightedEmbedScreen } from "./EmbedScreensStore"; + +function createCoWebsiteStore() { + const { subscribe, set, update } = writable(Array()); + + set(Array()); + + return { + subscribe, + add: (coWebsite: CoWebsite, position?: number) => { + coWebsite.state.subscribe((value) => { + update((currentArray) => currentArray); + }); + + if (position || position === 0) { + update((currentArray) => { + if (position === 0) { + return [coWebsite, ...currentArray]; + } else if (currentArray.length > position) { + const test = [...currentArray.splice(position, 0, coWebsite)]; + return [...currentArray.splice(position, 0, coWebsite)]; + } + + return [...currentArray, coWebsite]; + }); + return; + } + + update((currentArray) => [...currentArray, coWebsite]); + }, + remove: (coWebsite: CoWebsite) => { + update((currentArray) => [ + ...currentArray.filter((currentCoWebsite) => currentCoWebsite.iframe.id !== coWebsite.iframe.id), + ]); + }, + empty: () => { + set(Array()); + }, + }; +} + +export const coWebsites = createCoWebsiteStore(); + +export const coWebsitesNotAsleep = derived([coWebsites], ([$coWebsites]) => + $coWebsites.filter((coWebsite) => get(coWebsite.state) !== "asleep") +); + +export const mainCoWebsite = derived([coWebsites], ([$coWebsites]) => + $coWebsites.find((coWebsite) => get(coWebsite.state) !== "asleep") +); + +export const coWebsiteThumbails = derived( + [coWebsites, highlightedEmbedScreen, mainCoWebsite], + ([$coWebsites, highlightedEmbedScreen, $mainCoWebsite]) => + $coWebsites.filter((coWebsite, index) => { + return ( + (!$mainCoWebsite || $mainCoWebsite.iframe.id !== coWebsite.iframe.id) && + (!highlightedEmbedScreen || + highlightedEmbedScreen.type !== "cowebsite" || + (highlightedEmbedScreen.type === "cowebsite" && + highlightedEmbedScreen.embed.iframe.id !== coWebsite.iframe.id)) + ); + }) +); diff --git a/front/src/Stores/EmbedScreensStore.ts b/front/src/Stores/EmbedScreensStore.ts new file mode 100644 index 00000000..0db7c675 --- /dev/null +++ b/front/src/Stores/EmbedScreensStore.ts @@ -0,0 +1,51 @@ +import { derived, get, writable } from "svelte/store"; +import type { CoWebsite } from "../WebRtc/CoWebsiteManager"; +import { LayoutMode } from "../WebRtc/LayoutManager"; +import { coWebsites } from "./CoWebsiteStore"; +import { Streamable, streamableCollectionStore } from "./StreamableCollectionStore"; + +export type EmbedScreen = + | { + type: "streamable"; + embed: Streamable; + } + | { + type: "cowebsite"; + embed: CoWebsite; + }; + +function createHighlightedEmbedScreenStore() { + const { subscribe, set, update } = writable(null); + + return { + subscribe, + highlight: (embedScreen: EmbedScreen) => { + set(embedScreen); + }, + removeHighlight: () => { + set(null); + }, + toggleHighlight: (embedScreen: EmbedScreen) => { + update((currentEmbedScreen) => + !currentEmbedScreen || + embedScreen.type !== currentEmbedScreen.type || + (embedScreen.type === "cowebsite" && + currentEmbedScreen.type === "cowebsite" && + embedScreen.embed.iframe.id !== currentEmbedScreen.embed.iframe.id) || + (embedScreen.type === "streamable" && + currentEmbedScreen.type === "streamable" && + embedScreen.embed.uniqueId !== currentEmbedScreen.embed.uniqueId) + ? embedScreen + : null + ); + }, + }; +} + +export const highlightedEmbedScreen = createHighlightedEmbedScreenStore(); +export const embedScreenLayout = writable(LayoutMode.Presentation); + +export const hasEmbedScreen = derived( + [streamableCollectionStore], + ($values) => get(streamableCollectionStore).size + get(coWebsites).length > 0 +); diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts index 92629dd6..ae61f35d 100644 --- a/front/src/WebRtc/CoWebsiteManager.ts +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -2,7 +2,13 @@ import { HtmlUtils } from "./HtmlUtils"; import { Subject } from "rxjs"; import { iframeListener } from "../Api/IframeListener"; import { waScaleManager } from "../Phaser/Services/WaScaleManager"; -import { ICON_URL } from "../Enum/EnvironmentVariable"; +import { coWebsites, coWebsitesNotAsleep, mainCoWebsite } from "../Stores/CoWebsiteStore"; +import { get, 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"; enum iframeStates { closed = 1, @@ -11,16 +17,15 @@ enum iframeStates { } const cowebsiteDomId = "cowebsite"; // the id of the whole container. -const cowebsiteContainerDomId = "cowebsite-container"; // the id of the whole container. -const cowebsiteMainDomId = "cowebsite-slot-0"; // the id of the parent div of the iframe. +const gameOverlayDomId = "game-overlay"; const cowebsiteBufferDomId = "cowebsite-buffer"; // the id of the container who contains cowebsite iframes. -const cowebsiteAsideDomId = "cowebsite-aside"; // the id of the parent div of the iframe. const cowebsiteAsideHolderDomId = "cowebsite-aside-holder"; -const cowebsiteSubIconsDomId = "cowebsite-sub-icons"; +const cowebsiteLoaderDomId = "cowebsite-loader"; export const cowebsiteCloseButtonId = "cowebsite-close"; const cowebsiteFullScreenButtonId = "cowebsite-fullscreen"; const cowebsiteOpenFullScreenImageId = "cowebsite-fullscreen-open"; const cowebsiteCloseFullScreenImageId = "cowebsite-fullscreen-close"; +const cowebsiteSlotBaseDomId = "cowebsite-slot-"; const animationTime = 500; //time used by the css transitions, in ms. interface TouchMoveCoordinates { @@ -28,15 +33,16 @@ interface TouchMoveCoordinates { y: number; } +export type CoWebsiteState = "asleep" | "loading" | "ready"; + export type CoWebsite = { iframe: HTMLIFrameElement; - icon: HTMLDivElement; - position: number; -}; - -type CoWebsiteSlot = { - container: HTMLElement; - position: number; + url: URL; + state: Writable; + closable: boolean; + allowPolicy: string | undefined; + allowApi: boolean | undefined; + jitsi?: boolean; }; class CoWebsiteManager { @@ -50,18 +56,17 @@ class CoWebsiteManager { */ private currentOperationPromise: Promise = Promise.resolve(); private cowebsiteDom: HTMLDivElement; - private cowebsiteContainerDom: HTMLDivElement; private resizing: boolean = false; - private cowebsiteMainDom: HTMLDivElement; + private gameOverlayDom: HTMLDivElement; private cowebsiteBufferDom: HTMLDivElement; - private cowebsiteAsideDom: HTMLDivElement; private cowebsiteAsideHolderDom: HTMLDivElement; - private cowebsiteSubIconsDom: HTMLDivElement; + private cowebsiteLoaderDom: HTMLDivElement; private previousTouchMoveCoordinates: TouchMoveCoordinates | null = null; //only use on touchscreens to track touch movement - private coWebsites: CoWebsite[] = []; - - private slots: CoWebsiteSlot[]; + private loaderAnimationInterval: { + interval: NodeJS.Timeout | undefined; + trails: number[] | undefined; + }; private resizeObserver = new ResizeObserver((entries) => { this.resizeAllIframes(); @@ -97,59 +102,39 @@ class CoWebsiteManager { constructor() { this.cowebsiteDom = HtmlUtils.getElementByIdOrFail(cowebsiteDomId); - this.cowebsiteContainerDom = HtmlUtils.getElementByIdOrFail(cowebsiteContainerDomId); - this.cowebsiteMainDom = HtmlUtils.getElementByIdOrFail(cowebsiteMainDomId); + this.gameOverlayDom = HtmlUtils.getElementByIdOrFail(gameOverlayDomId); this.cowebsiteBufferDom = HtmlUtils.getElementByIdOrFail(cowebsiteBufferDomId); - this.cowebsiteAsideDom = HtmlUtils.getElementByIdOrFail(cowebsiteAsideDomId); this.cowebsiteAsideHolderDom = HtmlUtils.getElementByIdOrFail(cowebsiteAsideHolderDomId); - this.cowebsiteSubIconsDom = HtmlUtils.getElementByIdOrFail(cowebsiteSubIconsDomId); - this.initResizeListeners(); + this.cowebsiteLoaderDom = HtmlUtils.getElementByIdOrFail(cowebsiteLoaderDomId); + + this.loaderAnimationInterval = { + interval: undefined, + trails: undefined, + }; + + this.holderListeners(); + this.transitionListeners(); this.resizeObserver.observe(this.cowebsiteDom); - this.resizeObserver.observe(this.cowebsiteContainerDom); - - this.slots = [ - { - container: this.cowebsiteMainDom, - position: 0, - }, - { - container: HtmlUtils.getElementByIdOrFail("cowebsite-slot-1"), - position: 1, - }, - { - container: HtmlUtils.getElementByIdOrFail("cowebsite-slot-2"), - position: 2, - }, - { - container: HtmlUtils.getElementByIdOrFail("cowebsite-slot-3"), - position: 3, - }, - { - container: HtmlUtils.getElementByIdOrFail("cowebsite-slot-4"), - position: 4, - }, - ]; - - this.slots.forEach((slot) => { - this.resizeObserver.observe(slot.container); - }); - - this.initActionsListeners(); + this.resizeObserver.observe(this.gameOverlayDom); const buttonCloseCoWebsites = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId); buttonCloseCoWebsites.addEventListener("click", () => { - if (this.isSmallScreen() && this.coWebsites.length > 1) { - const coWebsite = this.getCoWebsiteByPosition(0); + const coWebsite = this.getMainCoWebsite(); - if (coWebsite) { - this.removeCoWebsiteFromStack(coWebsite); - return; - } + if (!coWebsite) { + throw new Error("Undefined main co-website on closing"); } - buttonCloseCoWebsites.blur(); - this.closeCoWebsites().catch((e) => console.error(e)); + if (coWebsite.closable) { + this.closeCoWebsite(coWebsite).catch(() => { + console.error("Error during closing a co-website by a button"); + }); + } else { + this.unloadCoWebsite(coWebsite).catch(() => { + console.error("Error during unloading a co-website by a button"); + }); + } }); const buttonFullScreenFrame = HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId); @@ -159,20 +144,17 @@ class CoWebsiteManager { }); } + public getCoWebsiteBuffer(): HTMLDivElement { + return this.cowebsiteBufferDom; + } + public getDevicePixelRatio(): number { //on chrome engines, movementX and movementY return global screens coordinates while other browser return pixels //so on chrome-based browser we need to adjust using 'devicePixelRatio' return window.navigator.userAgent.includes("Firefox") ? 1 : window.devicePixelRatio; } - private isSmallScreen(): boolean { - return ( - window.matchMedia("(max-aspect-ratio: 1/1)").matches || - window.matchMedia("(max-width:960px) and (max-height:768px)").matches - ); - } - - private initResizeListeners() { + private holderListeners() { const movecallback = (event: MouseEvent | TouchEvent) => { let x, y; if (event.type === "mousemove") { @@ -187,13 +169,48 @@ class CoWebsiteManager { y = last.y - previous.y; } - this.verticalMode ? (this.height += y) : (this.width -= x); + if (this.verticalMode) { + const tempValue = this.height + y; + let maxHeight = 60 * window.innerHeight; + if (maxHeight !== 0) { + maxHeight = Math.round(maxHeight / 100); + } + + if (tempValue < this.cowebsiteAsideHolderDom.offsetHeight) { + this.height = this.cowebsiteAsideHolderDom.offsetHeight; + } else if (tempValue > maxHeight) { + this.height = maxHeight; + } else { + this.height = tempValue; + } + } else { + const tempValue = this.width - x; + let maxWidth = 75 * window.innerWidth; + if (maxWidth !== 0) { + maxWidth = Math.round(maxWidth / 100); + } + + if (tempValue < this.cowebsiteAsideHolderDom.offsetWidth) { + this.width = this.cowebsiteAsideHolderDom.offsetWidth; + } else if (tempValue > maxWidth) { + this.width = maxWidth; + } else { + this.width = tempValue; + } + } this.fire(); }; this.cowebsiteAsideHolderDom.addEventListener("mousedown", (event) => { if (this.isFullScreen) return; - this.cowebsiteMainDom.style.display = "none"; + const coWebsite = this.getMainCoWebsite(); + + if (!coWebsite) { + this.closeMain(); + return; + } + + coWebsite.iframe.style.display = "none"; this.resizing = true; document.addEventListener("mousemove", movecallback); }); @@ -201,14 +218,28 @@ class CoWebsiteManager { document.addEventListener("mouseup", (event) => { if (!this.resizing || this.isFullScreen) return; document.removeEventListener("mousemove", movecallback); - this.cowebsiteMainDom.style.display = "block"; + const coWebsite = this.getMainCoWebsite(); + + if (!coWebsite) { + this.resizing = false; + this.closeMain(); + return; + } + + coWebsite.iframe.style.display = "flex"; this.resizing = false; - this.cowebsiteMainDom.style.display = "flex"; }); this.cowebsiteAsideHolderDom.addEventListener("touchstart", (event) => { if (this.isFullScreen) return; - this.cowebsiteMainDom.style.display = "none"; + const coWebsite = this.getMainCoWebsite(); + + if (!coWebsite) { + this.closeMain(); + return; + } + + coWebsite.iframe.style.display = "none"; this.resizing = true; const touchEvent = event.touches[0]; this.previousTouchMoveCoordinates = { x: touchEvent.pageX, y: touchEvent.pageY }; @@ -219,30 +250,81 @@ class CoWebsiteManager { if (!this.resizing || this.isFullScreen) return; this.previousTouchMoveCoordinates = null; document.removeEventListener("touchmove", movecallback); - this.cowebsiteMainDom.style.display = "block"; + const coWebsite = this.getMainCoWebsite(); + + if (!coWebsite) { + this.closeMain(); + this.resizing = false; + return; + } + + coWebsite.iframe.style.display = "flex"; this.resizing = false; - this.cowebsiteMainDom.style.display = "flex"; + }); + } + + private transitionListeners() { + this.cowebsiteDom.addEventListener("transitionend", (event) => { + if (this.cowebsiteDom.classList.contains("loading")) { + this.fire(); + } + + if (this.cowebsiteDom.classList.contains("closing")) { + this.cowebsiteDom.classList.remove("closing"); + if (this.loaderAnimationInterval.interval) { + clearInterval(this.loaderAnimationInterval.interval); + } + this.loaderAnimationInterval.trails = undefined; + } }); } private closeMain(): void { - this.cowebsiteDom.classList.remove("loaded"); //edit the css class to trigger the transition - this.cowebsiteDom.classList.add("hidden"); + this.toggleFullScreenIcon(true); + this.cowebsiteDom.classList.add("closing"); + this.cowebsiteDom.classList.remove("opened"); this.openedMain = iframeStates.closed; this.resetStyleMain(); - this.cowebsiteDom.style.display = "none"; + this.fire(); } + private loadMain(): void { - this.cowebsiteDom.style.display = "flex"; - this.cowebsiteDom.classList.remove("hidden"); //edit the css class to trigger the transition - this.cowebsiteDom.classList.add("loading"); + this.loaderAnimationInterval.interval = setInterval(() => { + if (!this.loaderAnimationInterval.trails) { + this.loaderAnimationInterval.trails = [0, 1, 2]; + } + + for (let trail = 1; trail < this.loaderAnimationInterval.trails.length + 1; trail++) { + for (let state = 0; state < 4; state++) { + // const newState = this.loaderAnimationInterval.frames + trail -1; + const stateDom = this.cowebsiteLoaderDom.querySelector( + `#trail-${trail}-state-${state}` + ) as SVGPolygonElement; + + if (!stateDom) { + continue; + } + + stateDom.style.visibility = + this.loaderAnimationInterval.trails[trail - 1] !== 0 && + this.loaderAnimationInterval.trails[trail - 1] >= state + ? "visible" + : "hidden"; + } + } + + this.loaderAnimationInterval.trails = this.loaderAnimationInterval.trails.map((trail) => + trail === 3 ? 0 : trail + 1 + ); + }, 200); + this.cowebsiteDom.classList.add("opened"); this.openedMain = iframeStates.loading; } + private openMain(): void { this.cowebsiteDom.addEventListener("transitionend", () => { this.resizeAllIframes(); }); - this.cowebsiteDom.classList.remove("loading", "hidden"); //edit the css class to trigger the transition this.openedMain = iframeStates.opened; this.resetStyleMain(); } @@ -252,335 +334,321 @@ class CoWebsiteManager { this.cowebsiteDom.style.height = ""; } - private initActionsListeners() { - this.slots.forEach((slot: CoWebsiteSlot) => { - const expandButton = slot.container.querySelector(".expand"); - const highlightButton = slot.container.querySelector(".hightlight"); - const closeButton = slot.container.querySelector(".close"); - - if (expandButton) { - expandButton.addEventListener("click", (event) => { - event.preventDefault(); - const coWebsite = this.getCoWebsiteByPosition(slot.position); - - if (!coWebsite) { - return; - } - - this.moveRightPreviousCoWebsite(coWebsite, 0); - }); - } - - if (highlightButton) { - highlightButton.addEventListener("click", (event) => { - event.preventDefault(); - const coWebsite = this.getCoWebsiteByPosition(slot.position); - - if (!coWebsite) { - return; - } - - this.moveRightPreviousCoWebsite(coWebsite, 1); - }); - } - - if (closeButton) { - closeButton.addEventListener("click", (event) => { - event.preventDefault(); - const coWebsite = this.getCoWebsiteByPosition(slot.position); - - if (!coWebsite) { - return; - } - - this.removeCoWebsiteFromStack(coWebsite); - }); - } - }); - } - public getCoWebsites(): CoWebsite[] { - return this.coWebsites; + return get(coWebsites); } public getCoWebsiteById(coWebsiteId: string): CoWebsite | undefined { - return this.coWebsites.find((coWebsite: CoWebsite) => coWebsite.iframe.id === coWebsiteId); - } - - private getSlotByPosition(position: number): CoWebsiteSlot | undefined { - return this.slots.find((slot: CoWebsiteSlot) => slot.position === position); + return get(coWebsites).find((coWebsite: CoWebsite) => coWebsite.iframe.id === coWebsiteId); } private getCoWebsiteByPosition(position: number): CoWebsite | undefined { - return this.coWebsites.find((coWebsite: CoWebsite) => coWebsite.position === position); - } - - private setIframeOffset(coWebsite: CoWebsite, slot: CoWebsiteSlot) { - const bounding = slot.container.getBoundingClientRect(); - - if (coWebsite.iframe.classList.contains("thumbnail")) { - coWebsite.iframe.style.width = (bounding.right - bounding.left) * 2 + "px"; - coWebsite.iframe.style.height = (bounding.bottom - bounding.top) * 2 + "px"; - coWebsite.iframe.style.top = bounding.top - Math.floor(bounding.height * 0.5) + "px"; - coWebsite.iframe.style.left = bounding.left - Math.floor(bounding.width * 0.5) + "px"; - } else { - 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"; - } - } - - private resizeAllIframes() { - this.coWebsites.forEach((coWebsite: CoWebsite) => { - const slot = this.getSlotByPosition(coWebsite.position); - - if (slot) { - this.setIframeOffset(coWebsite, slot); + let i = 0; + return get(coWebsites).find((coWebsite: CoWebsite) => { + if (i === position) { + return coWebsite; } + + i++; + return false; }); } - private moveCoWebsite(coWebsite: CoWebsite, newPosition: number) { - const oldSlot = this.getSlotByPosition(coWebsite.position); - const newSlot = this.getSlotByPosition(newPosition); + private getMainCoWebsite(): CoWebsite | undefined { + return get(mainCoWebsite); + } - if (!newSlot) { + private getPositionByCoWebsite(coWebsite: CoWebsite): number { + return get(coWebsites).findIndex((currentCoWebsite) => currentCoWebsite.iframe.id === coWebsite.iframe.id); + } + + private getSlotByCowebsite(coWebsite: CoWebsite): HTMLDivElement | undefined { + const index = this.getPositionByCoWebsite(coWebsite); + if (index === -1) { + return undefined; + } + + let id = cowebsiteSlotBaseDomId; + + if (index === 0) { + id += "main"; + } else { + id += coWebsite.iframe.id; + } + + const slot = HtmlUtils.getElementById(id); + + return slot; + } + + private setIframeOffset(coWebsite: CoWebsite) { + const coWebsiteSlot = this.getSlotByCowebsite(coWebsite); + + if (!coWebsiteSlot) { return; } - coWebsite.iframe.scrolling = newPosition === 0 || newPosition === 1 ? "yes" : "no"; + const bounding = coWebsiteSlot.getBoundingClientRect(); - if (newPosition === 0) { - coWebsite.iframe.classList.add("main"); - coWebsite.icon.style.display = "none"; - } else { - coWebsite.iframe.classList.remove("main"); - coWebsite.icon.style.display = "flex"; + 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"; + } + + public resizeAllIframes() { + const mainCoWebsite = this.getCoWebsiteByPosition(0); + const highlightEmbed = get(highlightedEmbedScreen); + + get(coWebsites).forEach((coWebsite) => { + const notMain = !mainCoWebsite || (mainCoWebsite && mainCoWebsite.iframe.id !== coWebsite.iframe.id); + const notHighlighEmbed = + !highlightEmbed || + (highlightEmbed && + (highlightEmbed.type !== "cowebsite" || + (highlightEmbed.type === "cowebsite" && + highlightEmbed.embed.iframe.id !== coWebsite.iframe.id))); + + if (coWebsite.iframe.classList.contains("main") && notMain) { + coWebsite.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 (notMain && notHighlighEmbed) { + coWebsite.iframe.classList.add("pixel"); + coWebsite.iframe.style.top = "-1px"; + coWebsite.iframe.style.left = "-1px"; + } + + this.setIframeOffset(coWebsite); + }); + + if (mainCoWebsite) { + mainCoWebsite.iframe.classList.add("main"); + mainCoWebsite.iframe.classList.remove("pixel"); } - if (newPosition === 1) { - coWebsite.iframe.classList.add("sub-main"); - } else { - coWebsite.iframe.classList.remove("sub-main"); + if (highlightEmbed && highlightEmbed.type === "cowebsite") { + highlightEmbed.embed.iframe.classList.add("highlighted"); + highlightEmbed.embed.iframe.classList.remove("pixel"); + } + } + + private removeHighlightCoWebsite(coWebsite: CoWebsite) { + const highlighted = get(highlightedEmbedScreen); + + if (highlighted && highlighted.type === "cowebsite" && highlighted.embed.iframe.id === coWebsite.iframe.id) { + highlightedEmbedScreen.removeHighlight(); + } + } + + private removeCoWebsiteFromStack(coWebsite: CoWebsite) { + this.removeHighlightCoWebsite(coWebsite); + coWebsites.remove(coWebsite); + + if (get(coWebsites).length < 1) { + this.closeMain(); } - if (newPosition >= 2) { - coWebsite.iframe.classList.add("thumbnail"); - } else { - coWebsite.iframe.classList.remove("thumbnail"); + coWebsite.iframe.remove(); + } + + public goToMain(coWebsite: CoWebsite) { + const mainCoWebsite = this.getMainCoWebsite(); + coWebsites.remove(coWebsite); + coWebsites.add(coWebsite, 0); + + if ( + isMediaBreakpointDown("lg") && + get(embedScreenLayout) === LayoutMode.Presentation && + mainCoWebsite && + mainCoWebsite.iframe.id !== coWebsite.iframe.id && + get(mainCoWebsite.state) !== "asleep" + ) { + highlightedEmbedScreen.toggleHighlight({ + type: "cowebsite", + embed: mainCoWebsite, + }); } - coWebsite.position = newPosition; - - if (oldSlot && !this.getCoWebsiteByPosition(oldSlot.position)) { - oldSlot.container.style.display = "none"; - } - - this.displayCowebsiteContainer(); - - newSlot.container.style.display = "block"; - - coWebsite.iframe.classList.remove("pixel"); - this.resizeAllIframes(); } - private displayCowebsiteContainer() { - if (this.coWebsites.find((cowebsite) => cowebsite.position > 0)) { - this.cowebsiteContainerDom.style.display = "block"; - } else { - this.cowebsiteContainerDom.style.display = "none"; - } - } - - private moveLeftPreviousCoWebsite(coWebsite: CoWebsite, newPosition: number) { - const nextCoWebsite = this.getCoWebsiteByPosition(coWebsite.position + 1); - - this.moveCoWebsite(coWebsite, newPosition); - - if (nextCoWebsite) { - this.moveLeftPreviousCoWebsite(nextCoWebsite, nextCoWebsite.position - 1); - } - } - - private moveRightPreviousCoWebsite(coWebsite: CoWebsite, newPosition: number) { - if (newPosition >= 5) { - return; - } - - const currentCoWebsite = this.getCoWebsiteByPosition(newPosition); - - this.moveCoWebsite(coWebsite, newPosition); - - if (newPosition === 4 || !currentCoWebsite || currentCoWebsite.iframe.id === coWebsite.iframe.id) { - return; - } - - if (!currentCoWebsite) { - return; - } - - this.moveRightPreviousCoWebsite(currentCoWebsite, currentCoWebsite.position + 1); - } - - private removeCoWebsiteFromStack(coWebsite: CoWebsite) { - this.coWebsites = this.coWebsites.filter( - (coWebsiteToRemove: CoWebsite) => coWebsiteToRemove.iframe.id !== coWebsite.iframe.id - ); - - if (this.coWebsites.length < 1) { - this.closeMain(); - } - - if (coWebsite.position > 0) { - const slot = this.getSlotByPosition(coWebsite.position); - if (slot) { - slot.container.style.display = "none"; - } - } - - const previousCoWebsite = this.coWebsites.find( - (coWebsiteToCheck: CoWebsite) => coWebsite.position + 1 === coWebsiteToCheck.position - ); - - if (previousCoWebsite) { - this.moveLeftPreviousCoWebsite(previousCoWebsite, coWebsite.position); - } - - this.displayCowebsiteContainer(); - - coWebsite.icon.remove(); - coWebsite.iframe.remove(); - } - public searchJitsi(): CoWebsite | undefined { - return this.coWebsites.find((coWebsite: CoWebsite) => coWebsite.iframe.id.toLowerCase().includes("jitsi")); + return get(coWebsites).find((coWebsite: CoWebsite) => coWebsite.jitsi); } - private generateCoWebsiteIcon(iframe: HTMLIFrameElement): HTMLDivElement { - const icon = document.createElement("div"); - icon.id = "cowebsite-icon-" + iframe.id; - icon.style.display = "none"; + private initialiseCowebsite(coWebsite: CoWebsite, position: number | undefined) { + if (coWebsite.allowPolicy) { + coWebsite.iframe.allow = coWebsite.allowPolicy; + } - const iconImage = document.createElement("img"); - iconImage.src = `${ICON_URL}/icon?url=${iframe.src}&size=16..30..256`; - const url = new URL(iframe.src); - iconImage.alt = url.hostname; + if (coWebsite.allowApi) { + iframeListener.registerIframe(coWebsite.iframe); + } - icon.appendChild(iconImage); + coWebsite.iframe.classList.add("pixel"); - return icon; + const coWebsitePosition = position === undefined ? get(coWebsites).length : position; + coWebsites.add(coWebsite, coWebsitePosition); } - public loadCoWebsite( + private generateUniqueId() { + let id = undefined; + do { + id = "cowebsite-iframe-" + (Math.random() + 1).toString(36).substring(7); + } while (this.getCoWebsiteById(id)); + + return id; + } + + public addCoWebsite( url: string, base: string, allowApi?: boolean, allowPolicy?: string, - widthPercent?: number, - position?: number - ): Promise { - return this.addCoWebsite( - (iframeBuffer) => { - const iframe = document.createElement("iframe"); - iframe.src = new URL(url, base).toString(); + position?: number, + closable?: boolean + ): CoWebsite { + const iframe = document.createElement("iframe"); + const fullUrl = new URL(url, base); + iframe.src = fullUrl.toString(); + iframe.id = this.generateUniqueId(); - if (allowPolicy) { - iframe.allow = allowPolicy; - } + const newCoWebsite: CoWebsite = { + iframe, + url: fullUrl, + state: writable("asleep" as CoWebsiteState), + closable: closable ?? false, + allowPolicy, + allowApi, + }; - if (allowApi) { - iframeListener.registerIframe(iframe); - } + this.initialiseCowebsite(newCoWebsite, position); - iframeBuffer.appendChild(iframe); - - return iframe; - }, - widthPercent, - position - ); + return newCoWebsite; } - public async addCoWebsite( - callback: (iframeBuffer: HTMLDivElement) => PromiseLike | HTMLIFrameElement, - widthPercent?: number, - position?: number - ): Promise { + public addCoWebsiteFromIframe( + iframe: HTMLIFrameElement, + allowApi?: boolean, + allowPolicy?: string, + position?: number, + closable?: boolean, + jitsi?: boolean + ): CoWebsite { + if (get(coWebsitesNotAsleep).length < 1) { + this.loadMain(); + } + + iframe.id = this.generateUniqueId(); + + const newCoWebsite: CoWebsite = { + iframe, + url: new URL(iframe.src), + state: writable("ready" as CoWebsiteState), + closable: closable ?? false, + allowPolicy, + allowApi, + jitsi, + }; + + if (position === 0) { + this.openMain(); + setTimeout(() => { + this.fire(); + }, animationTime); + } + + this.initialiseCowebsite(newCoWebsite, position); + + return newCoWebsite; + } + + public loadCoWebsite(coWebsite: CoWebsite): Promise { + if (get(coWebsitesNotAsleep).length < 1) { + coWebsites.remove(coWebsite); + coWebsites.add(coWebsite, 0); + this.loadMain(); + } + + coWebsite.state.set("loading"); + + const mainCoWebsite = this.getMainCoWebsite(); + return new Promise((resolve, reject) => { - if (this.coWebsites.length < 1) { - this.loadMain(); - } else if (this.coWebsites.length === 5) { - throw new Error("Too many websites"); + const onloadPromise = new Promise((resolve) => { + coWebsite.iframe.onload = () => { + coWebsite.state.set("ready"); + resolve(); + }; + }); + + const onTimeoutPromise = new Promise((resolve) => { + setTimeout(() => resolve(), 2000); + }); + + this.cowebsiteBufferDom.appendChild(coWebsite.iframe); + + if (coWebsite.jitsi) { + const gameScene = gameManager.getCurrentGameScene(); + gameScene.disableMediaBehaviors(); } - Promise.resolve(callback(this.cowebsiteBufferDom)) - .then((iframe) => { - iframe?.classList.add("pixel"); + this.currentOperationPromise = this.currentOperationPromise + .then(() => Promise.race([onloadPromise, onTimeoutPromise])) + .then(() => { + if (mainCoWebsite && mainCoWebsite.iframe.id === coWebsite.iframe.id) { + this.openMain(); - if (!iframe.id) { - do { - iframe.id = "cowebsite-iframe-" + (Math.random() + 1).toString(36).substring(7); - } while (this.getCoWebsiteById(iframe.id)); + setTimeout(() => { + this.fire(); + }, animationTime); } - const onloadPromise = new Promise((resolve) => { - iframe.onload = () => resolve(); - }); - - const icon = this.generateCoWebsiteIcon(iframe); - - const coWebsite = { - iframe, - icon, - position: position ?? this.coWebsites.length, - }; - - // Iframe management on mobile - icon.addEventListener("click", () => { - if (this.isSmallScreen()) { - this.moveRightPreviousCoWebsite(coWebsite, 0); - } - }); - - this.coWebsites.push(coWebsite); - this.cowebsiteSubIconsDom.appendChild(icon); - - const onTimeoutPromise = new Promise((resolve) => { - setTimeout(() => resolve(), 2000); - }); - - this.currentOperationPromise = this.currentOperationPromise - .then(() => Promise.race([onloadPromise, onTimeoutPromise])) - .then(() => { - if (coWebsite.position === 0) { - this.openMain(); - if (widthPercent) { - this.widthPercent = widthPercent; - } - - setTimeout(() => { - this.fire(); - position !== undefined - ? this.moveRightPreviousCoWebsite(coWebsite, coWebsite.position) - : this.moveCoWebsite(coWebsite, coWebsite.position); - }, animationTime); - } else { - position !== undefined - ? this.moveRightPreviousCoWebsite(coWebsite, coWebsite.position) - : this.moveCoWebsite(coWebsite, coWebsite.position); - } - - return resolve(coWebsite); - }) - .catch((err) => { - console.error("Error loadCoWebsite => ", err); - this.removeCoWebsiteFromStack(coWebsite); - return reject(); - }); + return resolve(coWebsite); }) - .catch((e) => console.error("Error loadCoWebsite => ", e)); + .catch((err) => { + console.error("Error on co-website loading => ", err); + this.removeCoWebsiteFromStack(coWebsite); + return reject(); + }); + }); + } + + public unloadCoWebsite(coWebsite: CoWebsite): Promise { + return new Promise((resolve, reject) => { + this.removeHighlightCoWebsite(coWebsite); + + coWebsite.iframe.parentNode?.removeChild(coWebsite.iframe); + coWebsite.state.set("asleep"); + coWebsites.remove(coWebsite); + + if (coWebsite.jitsi) { + jitsiFactory.stop(); + const gameScene = gameManager.getCurrentGameScene(); + gameScene.enableMediaBehaviors(); + } + + const mainCoWebsite = this.getMainCoWebsite(); + + if (mainCoWebsite) { + this.removeHighlightCoWebsite(mainCoWebsite); + this.goToMain(mainCoWebsite); + this.resizeAllIframes(); + } else { + this.closeMain(); + } + + coWebsites.add(coWebsite, get(coWebsites).length); + + resolve(); }); } @@ -588,13 +656,17 @@ class CoWebsiteManager { this.currentOperationPromise = this.currentOperationPromise.then( () => new Promise((resolve) => { - if (this.coWebsites.length === 1) { - if (this.openedMain === iframeStates.closed) resolve(); //this method may be called twice, in case of iframe error for example - this.closeMain(); + if (coWebsite.jitsi) { + jitsiFactory.stop(); + const gameScene = gameManager.getCurrentGameScene(); + gameScene.enableMediaBehaviors(); + } + + if (get(coWebsites).length === 1) { this.fire(); } - if (coWebsite) { + if (coWebsite.allowApi) { iframeListener.unregisterIframe(coWebsite.iframe); } @@ -605,27 +677,19 @@ class CoWebsiteManager { return this.currentOperationPromise; } - public async closeJitsi() { - const jitsi = this.searchJitsi(); - if (jitsi) { - return this.closeCoWebsite(jitsi); - } - } - - public async closeCoWebsites(): Promise { - await this.currentOperationPromise; - - const promises: Promise[] = []; - this.coWebsites.forEach((coWebsite: CoWebsite) => { - promises.push(this.closeCoWebsite(coWebsite)); - }); - await Promise.all(promises); - // TODO: this.currentOperationPromise does not point any more on the last promise - return; + 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.opened) { + if (this.openedMain === iframeStates.closed) { return { width: window.innerWidth, height: window.innerHeight, @@ -651,19 +715,27 @@ class CoWebsiteManager { } private fullscreen(): void { - const openFullscreenImage = HtmlUtils.getElementByIdOrFail(cowebsiteOpenFullScreenImageId); - const closeFullScreenImage = HtmlUtils.getElementByIdOrFail(cowebsiteCloseFullScreenImageId); - if (this.isFullScreen) { + this.toggleFullScreenIcon(true); this.resetStyleMain(); this.fire(); //we don't trigger a resize of the phaser game since it won't be visible anyway. + } else { + this.toggleFullScreenIcon(false); + this.verticalMode ? (this.height = window.innerHeight) : (this.width = window.innerWidth); + //we don't trigger a resize of the phaser game since it won't be visible anyway. + } + } + + private toggleFullScreenIcon(visible: boolean) { + const openFullscreenImage = HtmlUtils.getElementByIdOrFail(cowebsiteOpenFullScreenImageId); + const closeFullScreenImage = HtmlUtils.getElementByIdOrFail(cowebsiteCloseFullScreenImageId); + + if (visible) { this.cowebsiteAsideHolderDom.style.visibility = "visible"; openFullscreenImage.style.display = "inline"; closeFullScreenImage.style.display = "none"; } else { - this.verticalMode ? (this.height = window.innerHeight) : (this.width = window.innerWidth); - //we don't trigger a resize of the phaser game since it won't be visible anyway. this.cowebsiteAsideHolderDom.style.visibility = "hidden"; openFullscreenImage.style.display = "none"; closeFullScreenImage.style.display = "inline"; diff --git a/front/src/WebRtc/JitsiFactory.ts b/front/src/WebRtc/JitsiFactory.ts index c067a255..8f9524a2 100644 --- a/front/src/WebRtc/JitsiFactory.ts +++ b/front/src/WebRtc/JitsiFactory.ts @@ -132,64 +132,64 @@ class JitsiFactory { return slugify(instance.replace("/", "-") + "-" + roomName); } - public start( + public async start( roomName: string, playerName: string, jwt?: string, config?: object, interfaceConfig?: object, - jitsiUrl?: string, - jitsiWidth?: number - ): Promise { - return coWebsiteManager.addCoWebsite( - async (cowebsiteDiv) => { - // 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"); + jitsiUrl?: string + ) { + const coWebsite = coWebsiteManager.searchJitsi(); - 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); + if (coWebsite) { + await coWebsiteManager.closeCoWebsite(coWebsite); + } - const options: JitsiOptions = { - roomName: roomName, - jwt: jwt, - width: "100%", - height: "100%", - parentNode: cowebsiteDiv, - configOverwrite: mergeConfig(config), - interfaceConfigOverwrite: { ...defaultInterfaceConfig, ...interfaceConfig }, - }; - if (!options.jwt) { - delete options.jwt; - } + // 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"); - return new Promise((resolve, reject) => { - const doResolve = (): void => { - const iframe = cowebsiteDiv.querySelector('[id*="jitsi" i]'); - if (iframe === null) { - throw new Error("Could not find Jitsi Iframe"); - } - resolve(iframe); - }; - 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 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); - this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback); - this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback); - }); - }, - jitsiWidth, - 0 - ); + 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) { + coWebsiteManager.addCoWebsiteFromIframe(iframe, false, undefined, 0, false, true); + } + + coWebsiteManager.resizeAllIframes(); + }; + + 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); + + this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback); + this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback); } public stop() { @@ -197,12 +197,6 @@ class JitsiFactory { return; } - const jitsiCoWebsite = coWebsiteManager.searchJitsi(); - - if (jitsiCoWebsite) { - coWebsiteManager.closeJitsi().catch((e) => console.error(e)); - } - this.jitsiApi.removeListener("audioMuteStatusChanged", this.audioCallback); this.jitsiApi.removeListener("videoMuteStatusChanged", this.videoCallback); this.jitsiApi?.dispose(); diff --git a/front/style/cowebsite.scss b/front/style/cowebsite.scss index a6af21ee..529bcae1 100644 --- a/front/style/cowebsite.scss +++ b/front/style/cowebsite.scss @@ -1,220 +1,3 @@ -/* A potentially shared website could appear in an iframe in the cowebsite space. */ - -#cowebsite { - position: fixed; - z-index: 200; - transition: transform 0.5s; - background-color: whitesmoke; - display: none; - - &.loading { - background-color: gray; - } - - main { - iframe { - width: 100%; - height: 100%; - max-width: 100vw; - max-height: 100vh; - } - } - - aside { - background: gray; - align-items: center; - display: flex; - flex-direction: column; - justify-content: space-between; - - #cowebsite-aside-holder { - background: gray; - height: 20px; - flex: 1; - display: flex; - justify-content: center; - align-items: center; - width: 100%; - - img { - width: 80%; - pointer-events: none; - } - } - - #cowebsite-aside-buttons { - display: flex; - flex-direction: column; - margin-bottom: auto; - flex: 1; - justify-content: start; - } - - .top-right-btn{ - transform: scale(0.5); - cursor: url('./images/cursor_pointer.png'), pointer; - } - - #cowebsite-sub-icons { - display: flex; - margin-top: auto; - visibility: hidden; - justify-content: end; - flex: 1; - } - } - - &-container { - position: absolute; - display: none; - height: 100%; - width: 100%; - - &-main { - padding: 2% 5%; - height: 50%; - } - - &-sub { - position: absolute !important; - display: inline-flex; - justify-content: center; - align-items: center; - bottom: 23%; - height: 20% !important; - width: 100%; - } - } - - &-slot-0 { - z-index: 70 !important; - background-color: whitesmoke; - } - - @for $i from 1 through 4 { - &-slot-#{$i} { - transition: transform 0.5s; - position: relative; - height: 100%; - display: none; - background-color: #333333; - - @if $i == 1 { - width: 100%; - } @else { - width: 33%; - margin: 5px; - } - - .overlay { - width: 100%; - height: 100%; - z-index: 50; - position: absolute; - display: flex; - flex-direction: column; - - .actions-move { - display: none; - flex-direction: row; - justify-content: center; - align-items: center; - position: absolute; - height: 100%; - width: 100%; - gap: 10%; - } - - &:hover { - background-color: rgba($color: #333333, $alpha: 0.6); - - .actions-move { - display: flex; - } - } - } - - .actions { - pointer-events: all !important; - margin: 3% 2%; - display: flex; - flex-direction: row; - justify-content: end; - position: relative; - z-index: 50; - - button { - width: 32px; - height: 32px; - margin: 8px; - display: flex; - justify-content: center; - align-items: center; - } - } - } - } - - &-buffer { - iframe { - z-index: 45 !important; - pointer-events: none !important; - overflow: hidden; - border: 0; - position: absolute; - } - - .main { - pointer-events: all !important; - z-index: 205 !important; - } - - .sub-main { - pointer-events: all !important; - } - - .thumbnail { - transform: scale(0.5, 0.5); - } - } - - .pixel { - visibility: hidden; - height: 1px; - width: 1px; - } -} - -@media (min-aspect-ratio: 1/1) { - #cowebsite { - right: 0; - top: 0; - width: 50%; - height: 100vh; - display: none; - - &.loading { - transform: translateX(90%); - } - &.hidden { - transform: translateX(100%); - } - - main { - width: 100%; - } - - - aside { - width: 30px; - - img { - transform: rotate(90deg); - } - } - - &-aside-holder { - cursor: ew-resize; - } - } -} +@import "cowebsite/global"; +@import "cowebsite/short-screens"; +@import "cowebsite/wide-screens"; diff --git a/front/style/cowebsite/_global.scss b/front/style/cowebsite/_global.scss new file mode 100644 index 00000000..b9d8e2ee --- /dev/null +++ b/front/style/cowebsite/_global.scss @@ -0,0 +1,119 @@ +#cowebsite { + position: fixed; + z-index: 820; + transition: transform 0.5s; + background-color: rgba(10, 9, 9, 0.8); + display: none; + + main { + iframe { + width: 100%; + height: 100%; + max-width: 100vw; + max-height: 100vh; + } + } + + aside { + background: gray; + align-items: center; + display: flex; + flex-direction: column; + justify-content: space-between; + + #cowebsite-aside-holder { + background: gray; + height: 20px; + flex: 1; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + + img { + width: 80%; + pointer-events: none; + } + } + + #cowebsite-aside-buttons { + display: flex; + flex-direction: column; + margin-bottom: auto; + flex: 1; + justify-content: start; + } + + .top-right-btn { + transform: scale(0.5); + cursor: url("./images/cursor_pointer.png"), pointer; + } + + #cowebsite-other-actions { + display: flex; + margin-top: auto; + visibility: hidden; + justify-content: end; + flex: 1; + } + } + + &-loader { + width: 20%; + + #smoke { + @for $i from 1 through 3 { + #trail-#{$i} { + @for $y from 1 through 3 { + #trail-#{$i}-state-#{$y} { + visibility: hidden; + } + } + } + } + } + } + + &-slot-main { + z-index: 70 !important; + background-color: rgba(10, 9, 9, 0); + display: flex; + justify-content: center; + align-items: center; + } + + &-buffer { + iframe { + z-index: 45 !important; + pointer-events: none !important; + overflow: hidden; + border: 0; + position: absolute; + + &.pixel { + height: 1px !important; + width: 1px !important; + } + } + + .main { + pointer-events: all !important; + z-index: 821 !important; + } + + .highlighted { + pointer-events: all !important; + padding: 4px; + } + + .thumbnail { + transform: scale(0.5, 0.5); + } + } + + .pixel { + visibility: hidden; + height: 1px; + width: 1px; + } +} diff --git a/front/style/cowebsite/_short-screens.scss b/front/style/cowebsite/_short-screens.scss new file mode 100644 index 00000000..89a5d123 --- /dev/null +++ b/front/style/cowebsite/_short-screens.scss @@ -0,0 +1,84 @@ +@include media-breakpoint-up(md) { + #main-container { + display: flex; + flex-direction: column-reverse; + } + + #cowebsite { + left: 0; + top: 0; + width: 100%; + height: 50%; + display: flex; + flex-direction: column-reverse; + + visibility: collapse; + transform: translateY(-100%); + + &.loading { + visibility: visible; + transform: translateY(0%); + } + + &.opened { + visibility: visible; + transform: translateY(0%); + } + + &.closing { + visibility: visible; + } + + &-loader { + height: 20%; + } + + main { + height: 100%; + } + + aside { + height: 50px; + flex-direction: row-reverse; + align-items: center; + display: flex; + justify-content: space-between; + + #cowebsite-aside-holder { + height: 100%; + cursor: ns-resize; + + img { + height: 100%; + } + } + + #cowebsite-aside-buttons { + flex-direction: row-reverse; + margin-left: auto; + margin-bottom: 0; + justify-content: end; + } + + #cowebsite-fullscreen { + padding-top: 0; + } + + #cowebsite-other-actions { + display: inline-flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-top: 0; + height: 100%; + visibility: visible; + } + + .top-right-btn { + img { + width: 15px; + } + } + } + } +} diff --git a/front/style/cowebsite/_wide-screens.scss b/front/style/cowebsite/_wide-screens.scss new file mode 100644 index 00000000..432a4dec --- /dev/null +++ b/front/style/cowebsite/_wide-screens.scss @@ -0,0 +1,41 @@ +@include media-breakpoint-down(lg) { + #cowebsite { + right: 0; + top: 0; + width: 50%; + height: 100vh; + display: flex; + visibility: collapse; + transform: translateX(100%); + + &.loading { + visibility: visible; + transform: translateX(0%); + } + + &.opened { + visibility: visible; + transform: translateX(0%); + } + + &.closing { + visibility: visible; + } + + main { + width: 100%; + } + + aside { + width: 30px; + + img { + transform: rotate(90deg); + } + } + + &-aside-holder { + cursor: ew-resize; + } + } +}