This commit is contained in:
Ludwig Behm 2022-02-12 00:16:44 +01:00
commit 9e30039551
57 changed files with 1819 additions and 815 deletions

View file

@ -5,6 +5,7 @@ export const isOpenCoWebsiteEvent = new tg.IsInterface()
url: tg.isString,
allowApi: tg.isOptional(tg.isBoolean),
allowPolicy: tg.isOptional(tg.isString),
widthPercent: tg.isOptional(tg.isNumber),
position: tg.isOptional(tg.isNumber),
closable: tg.isOptional(tg.isBoolean),
lazy: tg.isOptional(tg.isBoolean),

View file

@ -45,6 +45,7 @@ export class WorkadventureNavigationCommands extends IframeApiContribution<Worka
url: string,
allowApi?: boolean,
allowPolicy?: string,
widthPercent?: number,
position?: number,
closable?: boolean,
lazy?: boolean
@ -55,6 +56,7 @@ export class WorkadventureNavigationCommands extends IframeApiContribution<Worka
url,
allowApi,
allowPolicy,
widthPercent,
position,
closable,
lazy,

View file

@ -2,9 +2,11 @@
import { onMount } from "svelte";
import { ICON_URL } from "../../Enum/EnvironmentVariable";
import { coWebsitesNotAsleep, mainCoWebsite } from "../../Stores/CoWebsiteStore";
import { mainCoWebsite } from "../../Stores/CoWebsiteStore";
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
import type { CoWebsite } from "../../WebRtc/CoWebsiteManager";
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
import { iframeStates } from "../../WebRtc/CoWebsiteManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
export let index: number;
@ -13,16 +15,15 @@
let icon: HTMLImageElement;
let iconLoaded = false;
let state = coWebsite.state;
const coWebsiteUrl = coWebsite.iframe.src;
const urlObject = new URL(coWebsiteUrl);
let state = coWebsite.getStateSubscriber();
let isJitsi: boolean = coWebsite instanceof JitsiCoWebsite;
const mainState = coWebsiteManager.getMainStateSubscriber();
onMount(() => {
icon.src = coWebsite.jitsi
icon.src = isJitsi
? "/resources/logos/meet.svg"
: `${ICON_URL}/icon?url=${urlObject.hostname}&size=64..96..256&fallback_icon_color=14304c`;
icon.alt = coWebsite.altMessage ?? urlObject.hostname;
: `${ICON_URL}/icon?url=${coWebsite.getUrl().hostname}&size=64..96..256&fallback_icon_color=14304c`;
icon.alt = coWebsite.getUrl().hostname;
icon.onload = () => {
iconLoaded = true;
};
@ -32,17 +33,24 @@
if (vertical) {
coWebsiteManager.goToMain(coWebsite);
} else if ($mainCoWebsite) {
if ($mainCoWebsite.iframe.id === coWebsite.iframe.id) {
const coWebsites = $coWebsitesNotAsleep;
const newMain = $highlightedEmbedScreen ?? coWebsites.length > 1 ? coWebsites[1] : undefined;
if (newMain) {
coWebsiteManager.goToMain(newMain);
if ($mainCoWebsite.getId() === coWebsite.getId()) {
if (coWebsiteManager.getMainState() === iframeStates.closed) {
coWebsiteManager.displayMain();
} else if ($highlightedEmbedScreen?.type === "cowebsite") {
coWebsiteManager.goToMain($highlightedEmbedScreen.embed);
} else {
coWebsiteManager.hideMain();
}
} else {
highlightedEmbedScreen.toggleHighlight({
type: "cowebsite",
embed: coWebsite,
});
if (coWebsiteManager.getMainState() === iframeStates.closed) {
coWebsiteManager.goToMain(coWebsite);
coWebsiteManager.displayMain();
} else {
highlightedEmbedScreen.toggleHighlight({
type: "cowebsite",
embed: coWebsite,
});
}
}
}
@ -60,11 +68,14 @@
let isHighlight: boolean = false;
let isMain: boolean = false;
$: {
isMain = $mainCoWebsite !== undefined && $mainCoWebsite.iframe === coWebsite.iframe;
isMain =
$mainState === iframeStates.opened &&
$mainCoWebsite !== undefined &&
$mainCoWebsite.getId() === coWebsite.getId();
isHighlight =
$highlightedEmbedScreen !== null &&
$highlightedEmbedScreen.type === "cowebsite" &&
$highlightedEmbedScreen.embed.iframe === coWebsite.iframe;
$highlightedEmbedScreen.embed.getId() === coWebsite.getId();
}
</script>
@ -81,7 +92,7 @@
<img
class="cowebsite-icon noselect nes-pointer"
class:hide={!iconLoaded}
class:jitsi={coWebsite.jitsi}
class:jitsi={isJitsi}
bind:this={icon}
on:dragstart|preventDefault={noDrag}
alt=""
@ -208,7 +219,8 @@
}
&:not(.vertical) {
animation: bounce 0.35s ease 6 alternate;
transition: all 300ms;
transform: translateY(0px);
}
&.vertical {
@ -229,7 +241,7 @@
&.displayed {
&:not(.vertical) {
animation: activeThumbnail 300ms ease-in 0s forwards;
transform: translateY(-15px);
}
}
@ -258,16 +270,6 @@
}
}
@keyframes activeThumbnail {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-15px);
}
}
@keyframes bounce {
from {
transform: translateY(0);

View file

@ -7,7 +7,7 @@
{#if $coWebsites.length > 0}
<div id="cowebsite-thumbnail-container" class:vertical>
{#each [...$coWebsites.values()] as coWebsite, index (coWebsite.iframe.id)}
{#each [...$coWebsites.values()] as coWebsite, index (coWebsite.getId())}
<CoWebsiteThumbnail {index} {coWebsite} {vertical} />
{/each}
</div>

View file

@ -9,13 +9,11 @@
function closeCoWebsite() {
if ($highlightedEmbedScreen?.type === "cowebsite") {
if ($highlightedEmbedScreen.embed.closable) {
coWebsiteManager.closeCoWebsite($highlightedEmbedScreen.embed).catch(() => {
console.error("Error during co-website highlighted closing");
});
if ($highlightedEmbedScreen.embed.isClosable()) {
coWebsiteManager.closeCoWebsite($highlightedEmbedScreen.embed);
} else {
coWebsiteManager.unloadCoWebsite($highlightedEmbedScreen.embed).catch(() => {
console.error("Error during co-website highlighted unloading");
coWebsiteManager.unloadCoWebsite($highlightedEmbedScreen.embed).catch((err) => {
console.error("Cannot unload co-website", err);
});
}
}
@ -68,9 +66,9 @@
/>
{/key}
{:else if $highlightedEmbedScreen.type === "cowebsite"}
{#key $highlightedEmbedScreen.embed.iframe.id}
{#key $highlightedEmbedScreen.embed.getId()}
<div
id={"cowebsite-slot-" + $highlightedEmbedScreen.embed.iframe.id}
id={"cowebsite-slot-" + $highlightedEmbedScreen.embed.getId()}
class="highlighted-cowebsite nes-container is-rounded"
>
<div class="actions">

View file

@ -53,7 +53,7 @@
<section class="terms-and-conditions">
<a style="display: none;" href="traduction">Need for traduction</a>
<p>
{$LL.login.terms()}
{@html $LL.login.terms()}
</p>
</section>
{/if}

View file

@ -1,5 +1,12 @@
<script lang="ts">
import LL from "../../i18n/i18n-svelte";
import { gameManager } from "../../Phaser/Game/GameManager";
import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore";
let entryPoint: string = $startLayerNamesStore[0];
let walkAutomatically: boolean = false;
const currentPlayer = gameManager.getCurrentGameScene().CurrentPlayer;
const playerPos = { x: Math.floor(currentPlayer.x), y: Math.floor(currentPlayer.y) };
function copyLink() {
const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement;
@ -8,8 +15,23 @@
document.execCommand("copy");
}
function getLink() {
return `${location.origin}${location.pathname}#${entryPoint}${
walkAutomatically ? `&moveTo=${playerPos.x},${playerPos.y}` : ""
}`;
}
function updateInputFieldValue() {
const input = document.getElementById("input-share-link");
if (input) {
(input as HTMLInputElement).value = getLink();
}
}
let canShare = navigator.share !== undefined;
async function shareLink() {
const shareData = { url: location.toString() };
const shareData = { url: getLink() };
try {
await navigator.share(shareData);
@ -22,16 +44,43 @@
<div class="guest-main">
<section class="container-overflow">
<section class="share-url not-mobile">
<h3>{$LL.menu.invite.description()}</h3>
<input type="text" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={copyLink}>{$LL.menu.invite.copy()}</button>
</section>
<section class="is-mobile">
<h3>{$LL.menu.invite.description()}</h3>
<input type="hidden" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={shareLink}>{$LL.menu.invite.share()}</button>
{#if !canShare}
<section class="share-url not-mobile">
<h3>{$LL.menu.invite.description()}</h3>
<input type="text" readonly id="input-share-link" class="link-url" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={copyLink}>{$LL.menu.invite.copy()}</button>
</section>
{:else}
<section class="is-mobile">
<h3>{$LL.menu.invite.description()}</h3>
<input type="hidden" readonly id="input-share-link" value={location.toString()} />
<button type="button" class="nes-btn is-primary" on:click={shareLink}>{$LL.menu.invite.share()}</button>
</section>
{/if}
<h3>Select an entry point</h3>
<section class="nes-select is-dark starting-points">
<select
bind:value={entryPoint}
on:blur={() => {
updateInputFieldValue();
}}
>
{#each $startLayerNamesStore as entryPointName}
<option value={entryPointName}>{entryPointName}</option>
{/each}
</select>
</section>
<label>
<input
type="checkbox"
class="nes-checkbox is-dark"
bind:checked={walkAutomatically}
on:change={() => {
updateInputFieldValue();
}}
/>
<span>{$LL.menu.invite.walk_automatically_to_position()}</span>
</label>
</section>
</div>
@ -39,14 +88,27 @@
@import "../../../style/breakpoints.scss";
div.guest-main {
width: 50%;
margin-left: auto;
margin-right: auto;
height: calc(100% - 56px);
text-align: center;
input.link-url {
width: calc(100% - 200px);
}
.starting-points {
width: 80%;
}
section {
margin-bottom: 50px;
}
section.nes-select select:focus {
outline: none;
}
section.container-overflow {
height: 100%;
margin: 0;
@ -55,25 +117,23 @@
}
section.is-mobile {
display: none;
display: block;
text-align: center;
margin-bottom: 20px;
}
}
@include media-breakpoint-up(md) {
div.guest-main {
section.share-url.not-mobile {
display: none;
}
section.is-mobile {
display: block;
text-align: center;
margin-bottom: 20px;
}
section.container-overflow {
height: calc(100% - 120px);
}
}
}
@include media-breakpoint-up(lg) {
div.guest-main {
width: 100%;
}
}
</style>

View file

@ -69,6 +69,7 @@
} else {
const customMenu = customMenuIframe.get(menu.label);
if (customMenu !== undefined) {
activeSubMenu = menu;
props = { url: customMenu.url, allowApi: customMenu.allowApi };
activeComponent = CustomSubMenu;
} else {

View file

@ -9,4 +9,7 @@ export interface UserInputHandlerInterface {
handlePointerUpEvent: (pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => void;
handlePointerDownEvent: (pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => void;
handleSpaceKeyUpEvent: (event: Event) => Event;
addSpaceEventListener: (callback: Function) => void;
removeSpaceEventListner: (callback: Function) => void;
}

View file

@ -159,6 +159,27 @@ export abstract class Character extends Container implements OutlineableInterfac
return { x: this.x, y: this.y };
}
/**
* Returns position based on where player is currently facing
* @param shift How far from player should the point of interest be.
*/
public getDirectionalActivationPosition(shift: number): { x: number; y: number } {
switch (this.lastDirection) {
case PlayerAnimationDirections.Down: {
return { x: this.x, y: this.y + shift };
}
case PlayerAnimationDirections.Left: {
return { x: this.x - shift, y: this.y };
}
case PlayerAnimationDirections.Right: {
return { x: this.x + shift, y: this.y };
}
case PlayerAnimationDirections.Up: {
return { x: this.x, y: this.y - shift };
}
}
}
public getObjectToOutline(): Phaser.GameObjects.GameObject {
return this.playerNameText;
}
@ -455,16 +476,16 @@ export abstract class Character extends Container implements OutlineableInterfac
this.outlineColorStore.removeApiColor();
}
public pointerOverOutline(): void {
this.outlineColorStore.pointerOver();
public pointerOverOutline(color: number): void {
this.outlineColorStore.pointerOver(color);
}
public pointerOutOutline(): void {
this.outlineColorStore.pointerOut();
}
public characterCloseByOutline(): void {
this.outlineColorStore.characterCloseBy();
public characterCloseByOutline(color: number): void {
this.outlineColorStore.characterCloseBy(color);
}
public characterFarAwayOutline(): void {

View file

@ -11,6 +11,11 @@ export class ActivatablesManager {
private currentPlayer: Player;
private canSelectByDistance: boolean = true;
private readonly outlineColor = 0xffff00;
private readonly directionalActivationPositionShift = 50;
constructor(currentPlayer: Player) {
this.currentPlayer = currentPlayer;
}
@ -27,7 +32,7 @@ export class ActivatablesManager {
}
this.selectedActivatableObjectByPointer = object;
if (isOutlineable(this.selectedActivatableObjectByPointer)) {
this.selectedActivatableObjectByPointer?.pointerOverOutline();
this.selectedActivatableObjectByPointer?.pointerOverOutline(this.outlineColor);
}
}
@ -37,7 +42,7 @@ export class ActivatablesManager {
}
this.selectedActivatableObjectByPointer = undefined;
if (isOutlineable(this.selectedActivatableObjectByDistance)) {
this.selectedActivatableObjectByDistance?.characterCloseByOutline();
this.selectedActivatableObjectByDistance?.characterCloseByOutline(this.outlineColor);
}
}
@ -46,6 +51,9 @@ export class ActivatablesManager {
}
public deduceSelectedActivatableObjectByDistance(): void {
if (!this.canSelectByDistance) {
return;
}
const newNearestObject = this.findNearestActivatableObject();
if (this.selectedActivatableObjectByDistance === newNearestObject) {
return;
@ -60,10 +68,42 @@ export class ActivatablesManager {
}
this.selectedActivatableObjectByDistance = newNearestObject;
if (isOutlineable(this.selectedActivatableObjectByDistance)) {
this.selectedActivatableObjectByDistance?.characterCloseByOutline();
this.selectedActivatableObjectByDistance?.characterCloseByOutline(this.outlineColor);
}
}
public updateActivatableObjectsDistances(objects: ActivatableInterface[]): void {
const currentPlayerPos = this.currentPlayer.getDirectionalActivationPosition(
this.directionalActivationPositionShift
);
for (const object of objects) {
const distance = MathUtils.distanceBetween(currentPlayerPos, object.getPosition());
this.activatableObjectsDistances.set(object, distance);
}
}
public updateDistanceForSingleActivatableObject(object: ActivatableInterface): void {
this.activatableObjectsDistances.set(
object,
MathUtils.distanceBetween(
this.currentPlayer.getDirectionalActivationPosition(this.directionalActivationPositionShift),
object.getPosition()
)
);
}
public disableSelectingByDistance(): void {
this.canSelectByDistance = false;
if (isOutlineable(this.selectedActivatableObjectByDistance)) {
this.selectedActivatableObjectByDistance?.characterFarAwayOutline();
}
this.selectedActivatableObjectByDistance = undefined;
}
public enableSelectingByDistance(): void {
this.canSelectByDistance = true;
}
private findNearestActivatableObject(): ActivatableInterface | undefined {
let shortestDistance: number = Infinity;
let closestObject: ActivatableInterface | undefined = undefined;
@ -76,18 +116,8 @@ export class ActivatablesManager {
}
return closestObject;
}
public updateActivatableObjectsDistances(objects: ActivatableInterface[]): void {
const currentPlayerPos = this.currentPlayer.getPosition();
for (const object of objects) {
const distance = MathUtils.distanceBetween(currentPlayerPos, object.getPosition());
this.activatableObjectsDistances.set(object, distance);
}
}
public updateDistanceForSingleActivatableObject(object: ActivatableInterface): void {
this.activatableObjectsDistances.set(
object,
MathUtils.distanceBetween(this.currentPlayer.getPosition(), object.getPosition())
);
public isSelectingByDistanceEnabled(): boolean {
return this.canSelectByDistance;
}
}

View file

@ -26,15 +26,6 @@ export class Game extends Phaser.Game {
}
}
});
/*window.addEventListener('resize', (event) => {
// Let's trigger the onResize method of any active scene that is a ResizableScene
for (const scene of this.scene.getScenes(true)) {
if (scene instanceof ResizableScene) {
scene.onResize(event);
}
}
});*/
}
public step(time: number, delta: number) {

View file

@ -85,6 +85,7 @@ export class GameMap {
phaserMap
.createLayer(layer.name, terrains, (layer.x || 0) * 32, (layer.y || 0) * 32)
.setDepth(depth)
.setScrollFactor(layer.parallaxx ?? 1, layer.parallaxy ?? 1)
.setAlpha(layer.opacity)
.setVisible(layer.visible)
.setSize(layer.width, layer.height)
@ -120,7 +121,7 @@ export class GameMap {
return [];
}
public getCollisionsGrid(): number[][] {
public getCollisionGrid(): number[][] {
const grid: number[][] = [];
for (let y = 0; y < this.map.height; y += 1) {
const row: number[] = [];
@ -322,12 +323,19 @@ export class GameMap {
throw new Error("No possible position found");
}
public getObjectWithName(name: string): ITiledMapObject | undefined {
return this.tiledObjects.find((object) => object.name === name);
}
private getLayersByKey(key: number): Array<ITiledMapLayer> {
return this.flatLayers.filter((flatLayer) => flatLayer.type === "tilelayer" && flatLayer.data[key] !== 0);
}
private isCollidingAt(x: number, y: number): boolean {
for (const layer of this.phaserLayers) {
if (!layer.visible) {
continue;
}
if (layer.getTileAt(x, y)?.properties[GameMapProperties.COLLIDES]) {
return true;
}

View file

@ -20,6 +20,7 @@ export enum GameMapProperties {
OPEN_WEBSITE = "openWebsite",
OPEN_WEBSITE_ALLOW_API = "openWebsiteAllowApi",
OPEN_WEBSITE_POLICY = "openWebsitePolicy",
OPEN_WEBSITE_WIDTH = "openWebsiteWidth",
OPEN_WEBSITE_POSITION = "openWebsitePosition",
OPEN_WEBSITE_TRIGGER = "openWebsiteTrigger",
OPEN_WEBSITE_TRIGGER_MESSAGE = "openWebsiteTriggerMessage",

View file

@ -1,33 +1,36 @@
import type { GameScene } from "./GameScene";
import type { GameMap } from "./GameMap";
import { scriptUtils } from "../../Api/ScriptUtils";
import type { CoWebsite } from "../../WebRtc/CoWebsiteManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { layoutManagerActionStore } from "../../Stores/LayoutManagerStore";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { get } from "svelte/store";
import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import { ON_ACTION_TRIGGER_BUTTON, ON_ICON_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import type { ITiledMapLayer } from "../Map/ITiledMap";
import { GameMapProperties } from "./GameMapProperties";
import { highlightedEmbedScreen } from "../../Stores/EmbedScreensStore";
enum OpenCoWebsiteState {
ASLEEP,
OPENED,
MUST_BE_CLOSE,
}
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite";
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
import { JITSI_PRIVATE_MODE, JITSI_URL } from "../../Enum/EnvironmentVariable";
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore";
import { iframeListener } from "../../Api/IframeListener";
import { Room } from "../../Connexion/Room";
import LL from "../../i18n/i18n-svelte";
interface OpenCoWebsite {
coWebsite: CoWebsite;
state: OpenCoWebsiteState;
actionId: string;
coWebsite?: CoWebsite;
}
export class GameMapPropertiesListener {
private coWebsitesOpenByLayer = new Map<ITiledMapLayer, OpenCoWebsite>();
private coWebsitesActionTriggerByLayer = new Map<ITiledMapLayer, string>();
constructor(private scene: GameScene, private gameMap: GameMap) {}
register() {
// Website on new tab
this.gameMap.onPropertyChange(GameMapProperties.OPEN_TAB, (newValue, oldValue, allProps) => {
if (newValue === undefined) {
layoutManagerActionStore.removeAction("openTab");
@ -38,7 +41,7 @@ export class GameMapPropertiesListener {
if (forceTrigger || openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(GameMapProperties.OPEN_WEBSITE_TRIGGER_MESSAGE);
if (message === undefined) {
message = "Press SPACE or touch here to open web site in new tab";
message = get(LL).trigger.newTab();
}
layoutManagerActionStore.addAction({
uuid: "openTab",
@ -53,6 +56,129 @@ export class GameMapPropertiesListener {
}
});
// Jitsi room
this.gameMap.onPropertyChange(GameMapProperties.JITSI_ROOM, (newValue, oldValue, allProps) => {
if (newValue === undefined) {
layoutManagerActionStore.removeAction("jitsi");
coWebsiteManager.getCoWebsites().forEach((coWebsite) => {
if (coWebsite instanceof JitsiCoWebsite) {
coWebsiteManager.closeCoWebsite(coWebsite);
}
});
} else {
const openJitsiRoomFunction = () => {
const roomName = jitsiFactory.getRoomName(newValue.toString(), this.scene.instance);
const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined;
if (JITSI_PRIVATE_MODE && !jitsiUrl) {
const adminTag = allProps.get(GameMapProperties.JITSI_ADMIN_ROOM_TAG) as string | undefined;
this.scene.connection?.emitQueryJitsiJwtMessage(roomName, adminTag);
} else {
let domain = jitsiUrl || JITSI_URL;
if (domain === undefined) {
throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map.");
}
if (domain.substring(0, 7) !== "http://" && domain.substring(0, 8) !== "https://") {
domain = `${location.protocol}//${domain}`;
}
const coWebsite = new JitsiCoWebsite(new URL(domain), false, undefined, undefined, false);
coWebsiteManager.addCoWebsiteToStore(coWebsite, 0);
this.scene.initialiseJitsi(coWebsite, roomName, undefined);
}
layoutManagerActionStore.removeAction("jitsi");
};
const jitsiTriggerValue = allProps.get(GameMapProperties.JITSI_TRIGGER);
const forceTrigger = localUserStore.getForceCowebsiteTrigger();
if (forceTrigger || jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(GameMapProperties.JITSI_TRIGGER_MESSAGE);
if (message === undefined) {
message = get(LL).trigger.jitsiRoom();
}
layoutManagerActionStore.addAction({
uuid: "jitsi",
type: "message",
message: message,
callback: () => openJitsiRoomFunction(),
userInputManager: this.scene.userInputManager,
});
} else {
openJitsiRoomFunction();
}
}
});
this.gameMap.onPropertyChange(GameMapProperties.EXIT_SCENE_URL, (newValue, oldValue) => {
if (newValue) {
this.scene
.onMapExit(
Room.getRoomPathFromExitSceneUrl(
newValue as string,
window.location.toString(),
this.scene.MapUrlFile
)
)
.catch((e) => console.error(e));
} else {
setTimeout(() => {
layoutManagerActionStore.removeAction("roomAccessDenied");
}, 2000);
}
});
this.gameMap.onPropertyChange(GameMapProperties.EXIT_URL, (newValue, oldValue) => {
if (newValue) {
this.scene
.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString()))
.catch((e) => console.error(e));
} else {
setTimeout(() => {
layoutManagerActionStore.removeAction("roomAccessDenied");
}, 2000);
}
});
this.gameMap.onPropertyChange(GameMapProperties.SILENT, (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === "") {
this.scene.connection?.setSilent(false);
this.scene.CurrentPlayer.noSilent();
} else {
this.scene.connection?.setSilent(true);
this.scene.CurrentPlayer.isSilent();
}
});
this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO, (newValue, oldValue, allProps) => {
const volume = allProps.get(GameMapProperties.AUDIO_VOLUME) as number | undefined;
const loop = allProps.get(GameMapProperties.AUDIO_LOOP) as boolean | undefined;
newValue === undefined
? audioManagerFileStore.unloadAudio()
: audioManagerFileStore.playAudio(newValue, this.scene.getMapDirUrl(), volume, loop);
audioManagerVisibilityStore.set(!(newValue === undefined));
});
// TODO: This legacy property should be removed at some point
this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO_LOOP, (newValue, oldValue) => {
newValue === undefined
? audioManagerFileStore.unloadAudio()
: audioManagerFileStore.playAudio(newValue, this.scene.getMapDirUrl(), undefined, true);
audioManagerVisibilityStore.set(!(newValue === undefined));
});
// TODO: Legacy functionnality replace by layer change
this.gameMap.onPropertyChange(GameMapProperties.ZONE, (newValue, oldValue) => {
if (oldValue) {
iframeListener.sendLeaveEvent(oldValue as string);
}
if (newValue) {
iframeListener.sendEnterEvent(newValue as string);
}
});
// Open a new co-website by the property.
this.gameMap.onEnterLayer((newLayers) => {
const handler = () => {
@ -64,6 +190,7 @@ 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;
@ -79,6 +206,9 @@ 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;
@ -95,55 +225,75 @@ export class GameMapPropertiesListener {
return;
}
const actionUuid = "openWebsite-" + (Math.random() + 1).toString(36).substring(7);
const actionId = "openWebsite-" + (Math.random() + 1).toString(36).substring(7);
if (this.coWebsitesOpenByLayer.has(layer)) {
return;
}
const coWebsite = coWebsiteManager.addCoWebsite(
openWebsiteProperty,
this.scene.MapUrlFile,
allowApiProperty,
websitePolicyProperty,
websitePositionProperty,
false
);
const coWebsiteOpen: OpenCoWebsite = {
actionId: actionId,
};
this.coWebsitesOpenByLayer.set(layer, {
coWebsite: coWebsite,
state: OpenCoWebsiteState.ASLEEP,
});
this.coWebsitesOpenByLayer.set(layer, coWebsiteOpen);
const openWebsiteFunction = () => {
coWebsiteManager
.loadCoWebsite(coWebsite)
.then((coWebsite) => {
const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer);
if (coWebsiteOpen && coWebsiteOpen.state === OpenCoWebsiteState.MUST_BE_CLOSE) {
coWebsiteManager.closeCoWebsite(coWebsite).catch(() => {
console.error("Error during a co-website closing");
});
this.coWebsitesOpenByLayer.delete(layer);
} else {
this.coWebsitesOpenByLayer.set(layer, {
coWebsite,
state: OpenCoWebsiteState.OPENED,
});
}
})
.catch(() => {
console.error("Error during loading a co-website: " + coWebsite.url);
});
const loadCoWebsiteFunction = (coWebsite: CoWebsite) => {
coWebsiteManager.loadCoWebsite(coWebsite).catch(() => {
console.error("Error during loading a co-website: " + coWebsite.getUrl());
});
layoutManagerActionStore.removeAction(actionUuid);
layoutManagerActionStore.removeAction(actionId);
};
const openCoWebsiteFunction = () => {
const coWebsite = new SimpleCoWebsite(
new URL(openWebsiteProperty ?? "", this.scene.MapUrlFile),
allowApiProperty,
websitePolicyProperty,
websiteWidthProperty,
false
);
coWebsiteOpen.coWebsite = coWebsite;
coWebsiteManager.addCoWebsiteToStore(coWebsite, websitePositionProperty);
loadCoWebsiteFunction(coWebsite);
};
if (
!localUserStore.getForceCowebsiteTrigger() &&
websiteTriggerProperty !== ON_ACTION_TRIGGER_BUTTON
localUserStore.getForceCowebsiteTrigger() ||
websiteTriggerProperty === ON_ACTION_TRIGGER_BUTTON
) {
openWebsiteFunction();
if (!websiteTriggerMessageProperty) {
websiteTriggerMessageProperty = get(LL).trigger.cowebsite();
}
this.coWebsitesActionTriggerByLayer.set(layer, actionId);
layoutManagerActionStore.addAction({
uuid: actionId,
type: "message",
message: websiteTriggerMessageProperty,
callback: () => openCoWebsiteFunction(),
userInputManager: this.scene.userInputManager,
});
} else if (websiteTriggerProperty === ON_ICON_TRIGGER_BUTTON) {
const coWebsite = new SimpleCoWebsite(
new URL(openWebsiteProperty ?? "", this.scene.MapUrlFile),
allowApiProperty,
websitePolicyProperty,
websiteWidthProperty,
false
);
coWebsiteOpen.coWebsite = coWebsite;
coWebsiteManager.addCoWebsiteToStore(coWebsite, websitePositionProperty);
}
if (!websiteTriggerProperty) {
openCoWebsiteFunction();
}
});
};
@ -183,15 +333,35 @@ export class GameMapPropertiesListener {
return;
}
if (coWebsiteOpen.state === OpenCoWebsiteState.ASLEEP) {
coWebsiteOpen.state = OpenCoWebsiteState.MUST_BE_CLOSE;
}
const coWebsite = coWebsiteOpen.coWebsite;
if (coWebsiteOpen.coWebsite !== undefined) {
coWebsiteManager.closeCoWebsite(coWebsiteOpen.coWebsite).catch((e) => console.error(e));
if (coWebsite) {
coWebsiteManager.closeCoWebsite(coWebsite);
}
this.coWebsitesOpenByLayer.delete(layer);
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);
}
this.coWebsitesActionTriggerByLayer.delete(layer);
});
};

View file

@ -5,7 +5,7 @@ import { get, Unsubscriber } from "svelte/store";
import { userMessageManager } from "../../Administration/UserMessageManager";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { CoWebsite, coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager";
import { urlManager } from "../../Url/UrlManager";
import { mediaManager } from "../../WebRtc/MediaManager";
import { UserInputManager } from "../UserInput/UserInputManager";
@ -20,9 +20,8 @@ import { EmbeddedWebsiteManager } from "./EmbeddedWebsiteManager";
import { lazyLoadPlayerCharacterTextures, loadCustomTexture } from "../Entity/PlayerTexturesLoadingManager";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import { ON_ACTION_TRIGGER_BUTTON } from "../../WebRtc/LayoutManager";
import { iframeListener } from "../../Api/IframeListener";
import { DEBUG_MODE, JITSI_PRIVATE_MODE, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
import { DEBUG_MODE, JITSI_URL, MAX_PER_GROUP, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils";
import { Room } from "../../Connexion/Room";
import { jitsiFactory } from "../../WebRtc/JitsiFactory";
@ -76,7 +75,7 @@ import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
import { userIsAdminStore } from "../../Stores/GameStore";
import { contactPageStore } from "../../Stores/MenuStore";
import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent";
import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore";
import { audioManagerFileStore } from "../../Stores/AudioManagerStore";
import EVENT_TYPE = Phaser.Scenes.Events;
import Texture = Phaser.Textures.Texture;
@ -92,6 +91,11 @@ import { MapStore } from "../../Stores/Utils/MapStore";
import { followUsersColorStore } from "../../Stores/FollowStore";
import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler";
import { locale } from "../../i18n/i18n-svelte";
import { StringUtils } from "../../Utils/StringUtils";
import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore";
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
import { SimpleCoWebsite } from "../../WebRtc/CoWebsite/SimpleCoWebsite";
import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
reconnecting: boolean;
@ -542,6 +546,8 @@ export class GameScene extends DirtyScene {
urlManager.getStartLayerNameFromUrl()
);
startLayerNamesStore.set(this.startPositionCalculator.getStartPositionNames());
//add entities
this.Objects = new Array<Phaser.Physics.Arcade.Sprite>();
@ -561,7 +567,7 @@ export class GameScene extends DirtyScene {
this.pathfindingManager = new PathfindingManager(
this,
this.gameMap.getCollisionsGrid(),
this.gameMap.getCollisionGrid(),
this.gameMap.getTileDimensions()
);
@ -569,6 +575,8 @@ export class GameScene extends DirtyScene {
this.createCurrentPlayer();
this.removeAllRemotePlayers(); //cleanup the list of remote players in case the scene was rebooted
this.tryMovePlayerWithMoveToParameter();
this.cameraManager = new CameraManager(
this,
{ x: this.Map.widthInPixels, y: this.Map.heightInPixels },
@ -577,7 +585,7 @@ export class GameScene extends DirtyScene {
this.pathfindingManager = new PathfindingManager(
this,
this.gameMap.getCollisionsGrid(),
this.gameMap.getCollisionGrid(),
this.gameMap.getTileDimensions()
);
@ -628,7 +636,6 @@ export class GameScene extends DirtyScene {
);
new GameMapPropertiesListener(this, this.gameMap).register();
this.triggerOnMapLayerPropertyChange();
if (!this.room.isDisconnected()) {
this.scene.sleep();
@ -798,7 +805,19 @@ export class GameScene extends DirtyScene {
* Triggered when we receive the JWT token to connect to Jitsi
*/
this.connection.sendJitsiJwtMessageStream.subscribe((message) => {
this.startJitsi(message.jitsiRoom, message.jwt);
if (!JITSI_URL) {
throw new Error("Missing JITSI_URL environment variable.");
}
let domain = JITSI_URL;
if (domain.substring(0, 7) !== "http://" && domain.substring(0, 8) !== "https://") {
domain = `${location.protocol}//${domain}`;
}
const coWebsite = new JitsiCoWebsite(new URL(domain), false, undefined, undefined, false);
coWebsiteManager.addCoWebsiteToStore(coWebsite, 0);
this.initialiseJitsi(coWebsite, message.jitsiRoom, message.jwt);
});
this.messageSubscription = this.connection.worldFullMessageStream.subscribe((message) => {
@ -935,103 +954,6 @@ export class GameScene extends DirtyScene {
}
}
private triggerOnMapLayerPropertyChange() {
this.gameMap.onPropertyChange(GameMapProperties.EXIT_SCENE_URL, (newValue, oldValue) => {
if (newValue) {
this.onMapExit(
Room.getRoomPathFromExitSceneUrl(newValue as string, window.location.toString(), this.MapUrlFile)
).catch((e) => console.error(e));
} else {
setTimeout(() => {
layoutManagerActionStore.removeAction("roomAccessDenied");
}, 2000);
}
});
this.gameMap.onPropertyChange(GameMapProperties.EXIT_URL, (newValue, oldValue) => {
if (newValue) {
this.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString())).catch((e) =>
console.error(e)
);
} else {
setTimeout(() => {
layoutManagerActionStore.removeAction("roomAccessDenied");
}, 2000);
}
});
this.gameMap.onPropertyChange(GameMapProperties.JITSI_ROOM, (newValue, oldValue, allProps) => {
if (newValue === undefined) {
layoutManagerActionStore.removeAction("jitsi");
this.stopJitsi();
} else {
const openJitsiRoomFunction = () => {
const roomName = jitsiFactory.getRoomName(newValue.toString(), this.instance);
const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined;
if (JITSI_PRIVATE_MODE && !jitsiUrl) {
const adminTag = allProps.get(GameMapProperties.JITSI_ADMIN_ROOM_TAG) as string | undefined;
this.connection?.emitQueryJitsiJwtMessage(roomName, adminTag);
} else {
this.startJitsi(roomName, undefined);
}
layoutManagerActionStore.removeAction("jitsi");
};
const jitsiTriggerValue = allProps.get(GameMapProperties.JITSI_TRIGGER);
const forceTrigger = localUserStore.getForceCowebsiteTrigger();
if (forceTrigger || jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
let message = allProps.get(GameMapProperties.JITSI_TRIGGER_MESSAGE);
if (message === undefined) {
message = "Press SPACE or touch here to enter Jitsi Meet room";
}
layoutManagerActionStore.addAction({
uuid: "jitsi",
type: "message",
message: message,
callback: () => openJitsiRoomFunction(),
userInputManager: this.userInputManager,
});
} else {
openJitsiRoomFunction();
}
}
});
this.gameMap.onPropertyChange(GameMapProperties.SILENT, (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === "") {
this.connection?.setSilent(false);
this.CurrentPlayer.noSilent();
} else {
this.connection?.setSilent(true);
this.CurrentPlayer.isSilent();
}
});
this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO, (newValue, oldValue, allProps) => {
const volume = allProps.get(GameMapProperties.AUDIO_VOLUME) as number | undefined;
const loop = allProps.get(GameMapProperties.AUDIO_LOOP) as boolean | undefined;
newValue === undefined
? audioManagerFileStore.unloadAudio()
: audioManagerFileStore.playAudio(newValue, this.getMapDirUrl(), volume, loop);
audioManagerVisibilityStore.set(!(newValue === undefined));
});
// TODO: This legacy property should be removed at some point
this.gameMap.onPropertyChange(GameMapProperties.PLAY_AUDIO_LOOP, (newValue, oldValue) => {
newValue === undefined
? audioManagerFileStore.unloadAudio()
: audioManagerFileStore.playAudio(newValue, this.getMapDirUrl(), undefined, true);
audioManagerVisibilityStore.set(!(newValue === undefined));
});
// TODO: Legacy functionnality replace by layer change
this.gameMap.onPropertyChange(GameMapProperties.ZONE, (newValue, oldValue) => {
if (oldValue) {
iframeListener.sendLeaveEvent(oldValue as string);
}
if (newValue) {
iframeListener.sendEnterEvent(newValue as string);
}
});
}
private listenToIframeEvents(): void {
this.iframeSubscriptionList = [];
this.iframeSubscriptionList.push(
@ -1264,12 +1186,11 @@ ${escapedMessage}
throw new Error("Unknown query source");
}
const coWebsite = coWebsiteManager.addCoWebsite(
openCoWebsite.url,
iframeListener.getBaseUrlFromSource(source),
const coWebsite: SimpleCoWebsite = new SimpleCoWebsite(
new URL(openCoWebsite.url, iframeListener.getBaseUrlFromSource(source)),
openCoWebsite.allowApi,
openCoWebsite.allowPolicy,
openCoWebsite.position,
openCoWebsite.widthPercent,
openCoWebsite.closable ?? true
);
@ -1278,7 +1199,7 @@ ${escapedMessage}
}
return {
id: coWebsite.iframe.id,
id: coWebsite.getId(),
};
});
@ -1287,27 +1208,23 @@ ${escapedMessage}
return coWebsites.map((coWebsite: CoWebsite) => {
return {
id: coWebsite.iframe.id,
id: coWebsite.getId(),
};
});
});
iframeListener.registerAnswerer("closeCoWebsite", async (coWebsiteId) => {
iframeListener.registerAnswerer("closeCoWebsite", (coWebsiteId) => {
const coWebsite = coWebsiteManager.getCoWebsiteById(coWebsiteId);
if (!coWebsite) {
throw new Error("Unknown co-website");
}
return coWebsiteManager.closeCoWebsite(coWebsite).catch((error) => {
throw new Error("Error on closing co-website");
});
return coWebsiteManager.closeCoWebsite(coWebsite);
});
iframeListener.registerAnswerer("closeCoWebsites", async () => {
return await coWebsiteManager.closeCoWebsites().catch((error) => {
throw new Error("Error on closing all co-websites");
});
iframeListener.registerAnswerer("closeCoWebsites", () => {
return coWebsiteManager.closeCoWebsites();
});
iframeListener.registerAnswerer("getMapData", () => {
@ -1392,7 +1309,7 @@ ${escapedMessage}
//Create new colliders with the new GameMap
this.createCollisionWithPlayer();
//Create new trigger with the new GameMap
this.triggerOnMapLayerPropertyChange();
new GameMapPropertiesListener(this, this.gameMap).register();
resolve(newFirstgid);
});
});
@ -1463,9 +1380,9 @@ ${escapedMessage}
});
iframeListener.registerAnswerer("movePlayerTo", async (message) => {
const index = this.getGameMap().getTileIndexAt(message.x, message.y);
const startTile = this.getGameMap().getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y);
const path = await this.getPathfindingManager().findPath(startTile, index, true, true);
const startTileIndex = this.getGameMap().getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y);
const destinationTileIndex = this.getGameMap().getTileIndexAt(message.x, message.y);
const path = await this.getPathfindingManager().findPath(startTileIndex, destinationTileIndex, true, true);
path.shift();
if (path.length === 0) {
throw new Error("no path available");
@ -1505,14 +1422,15 @@ ${escapedMessage}
phaserLayers[i].setCollisionByProperty({ collides: true }, visible);
}
}
this.pathfindingManager.setCollisionGrid(this.gameMap.getCollisionGrid());
this.markDirty();
}
private getMapDirUrl(): string {
return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/"));
public getMapDirUrl(): string {
return this.MapUrlFile.substring(0, this.MapUrlFile.lastIndexOf("/"));
}
private async onMapExit(roomUrl: URL) {
public async onMapExit(roomUrl: URL) {
if (this.mapTransitioning) return;
this.mapTransitioning = true;
@ -1571,14 +1489,13 @@ ${escapedMessage}
public cleanupClosingScene(): void {
// stop playing audio, close any open website, stop any open Jitsi
coWebsiteManager.closeCoWebsites().catch((e) => console.error(e));
coWebsiteManager.closeCoWebsites();
// Stop the script, if any
const scripts = this.getScriptUrls(this.mapFile);
for (const script of scripts) {
iframeListener.unregisterScript(script);
}
this.stopJitsi();
audioManagerFileStore.unloadAudio();
// We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map.
this.connection?.closeConnection();
@ -1629,6 +1546,36 @@ ${escapedMessage}
this.MapPlayersByKey.clear();
}
private tryMovePlayerWithMoveToParameter(): void {
const moveToParam = urlManager.getHashParameter("moveTo");
if (moveToParam) {
try {
let endPos;
const posFromParam = StringUtils.parsePointFromParam(moveToParam);
if (posFromParam) {
endPos = this.gameMap.getTileIndexAt(posFromParam.x, posFromParam.y);
} else {
const destinationObject = this.gameMap.getObjectWithName(moveToParam);
if (destinationObject) {
endPos = this.gameMap.getTileIndexAt(destinationObject.x, destinationObject.y);
} else {
endPos = this.gameMap.getRandomPositionFromLayer(moveToParam);
}
}
this.pathfindingManager
.findPath(this.gameMap.getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y), endPos)
.then((path) => {
if (path && path.length > 0) {
this.CurrentPlayer.setPathToFollow(path).catch((reason) => console.warn(reason));
}
})
.catch((reason) => console.warn(reason));
} catch (err) {
console.warn(`Cannot proceed with moveTo command:\n\t-> ${err}`);
}
}
}
private getExitUrl(layer: ITiledMapLayer): string | undefined {
return this.getProperty(layer, GameMapProperties.EXIT_URL) as string | undefined;
}
@ -1745,26 +1692,16 @@ ${escapedMessage}
emoteMenuStore.openEmoteMenu();
}
});
this.CurrentPlayer.on(Phaser.Input.Events.POINTER_OVER, (pointer: Phaser.Input.Pointer) => {
this.CurrentPlayer.pointerOverOutline(0x00ffff);
});
this.CurrentPlayer.on(Phaser.Input.Events.POINTER_OUT, (pointer: Phaser.Input.Pointer) => {
this.CurrentPlayer.pointerOutOutline();
});
this.CurrentPlayer.on(requestEmoteEventName, (emoteKey: string) => {
this.connection?.emitEmoteEvent(emoteKey);
analyticsClient.launchEmote(emoteKey);
});
const moveToParam = urlManager.getHashParameter("moveTo");
if (moveToParam) {
try {
const endPos = this.gameMap.getRandomPositionFromLayer(moveToParam);
this.pathfindingManager
.findPath(this.gameMap.getTileIndexAt(this.CurrentPlayer.x, this.CurrentPlayer.y), endPos)
.then((path) => {
if (path && path.length > 0) {
this.CurrentPlayer.setPathToFollow(path).catch((reason) => console.warn(reason));
}
})
.catch((reason) => console.warn(reason));
} catch (err) {
console.warn(`Cannot proceed with moveTo command:\n\t-> ${err}`);
}
}
} catch (err) {
if (err instanceof TextureError) {
gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());
@ -2117,7 +2054,7 @@ ${escapedMessage}
mediaManager.hideMyCamera();
}
public startJitsi(roomName: string, jwt?: string): void {
public initialiseJitsi(coWebsite: JitsiCoWebsite, roomName: string, jwt?: string): void {
const allProps = this.gameMap.getCurrentProperties();
const jitsiConfig = this.safeParseJSONstring(
allProps.get(GameMapProperties.JITSI_CONFIG) as string | undefined,
@ -2129,20 +2066,15 @@ ${escapedMessage}
);
const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined;
jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl).catch(() => {
console.error("Cannot start a Jitsi co-website");
coWebsite.setJitsiLoadPromise(() => {
return jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl);
});
this.disableMediaBehaviors();
analyticsClient.enteredJitsi(roomName, this.room.id);
}
public stopJitsi(): void {
const coWebsite = coWebsiteManager.searchJitsi();
if (coWebsite) {
coWebsiteManager.closeCoWebsite(coWebsite).catch((e) => {
console.error("Error during Jitsi co-website closing", e);
});
}
coWebsiteManager.loadCoWebsite(coWebsite).catch((err) => {
console.error(err);
});
analyticsClient.enteredJitsi(roomName, this.room.id);
}
//todo: put this into an 'orchestrator' scene (EntryScene?)

View file

@ -3,8 +3,8 @@ export interface OutlineableInterface {
removeFollowOutlineColor(): void;
setApiOutlineColor(color: number): void;
removeApiOutlineColor(): void;
pointerOverOutline(): void;
pointerOverOutline(color: number): void;
pointerOutOutline(): void;
characterCloseByOutline(): void;
characterCloseByOutline(color: number): void;
characterFarAwayOutline(): void;
}

View file

@ -16,32 +16,6 @@ export class StartPositionCalculator {
) {
this.initStartXAndStartY();
}
private initStartXAndStartY() {
// If there is an init position passed
if (this.initPosition !== null) {
this.startPosition = this.initPosition;
} else {
// Now, let's find the start layer
if (this.startLayerName) {
this.initPositionFromLayerName(this.startLayerName, this.startLayerName);
}
if (this.startPosition === undefined) {
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
this.initPositionFromLayerName(defaultStartLayerName, this.startLayerName);
}
}
// Still no start position? Something is wrong with the map, we need a "start" layer.
if (this.startPosition === undefined) {
console.warn(
'This map is missing a layer named "start" that contains the available default start positions.'
);
// Let's start in the middle of the map
this.startPosition = {
x: this.mapFile.width * 16,
y: this.mapFile.height * 16,
};
}
}
/**
*
@ -76,6 +50,47 @@ export class StartPositionCalculator {
}
}
public getStartPositionNames(): string[] {
const names: string[] = [];
for (const layer of this.gameMap.flatLayers) {
if (layer.name === "start") {
names.push(layer.name);
continue;
}
if (this.isStartLayer(layer)) {
names.push(layer.name);
}
}
return names;
}
private initStartXAndStartY() {
// If there is an init position passed
if (this.initPosition !== null) {
this.startPosition = this.initPosition;
} else {
// Now, let's find the start layer
if (this.startLayerName) {
this.initPositionFromLayerName(this.startLayerName, this.startLayerName);
}
if (this.startPosition === undefined) {
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
this.initPositionFromLayerName(defaultStartLayerName, this.startLayerName);
}
}
// Still no start position? Something is wrong with the map, we need a "start" layer.
if (this.startPosition === undefined) {
console.warn(
'This map is missing a layer named "start" that contains the available default start positions.'
);
// Let's start in the middle of the map
this.startPosition = {
x: this.mapFile.width * 16,
y: this.mapFile.height * 16,
};
}
}
private isStartLayer(layer: ITiledMapLayer): boolean {
return this.getProperty(layer, GameMapProperties.START_LAYER) == true;
}

View file

@ -78,6 +78,8 @@ export interface ITiledMapTileLayer {
width: number;
x: number;
y: number;
parallaxx?: number;
parallaxy?: number;
/**
* Draw order (topdown (default), index)

View file

@ -53,10 +53,20 @@ export class GameSceneUserInputHandler implements UserInputHandlerInterface {
public handlePointerDownEvent(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]): void {}
public handleSpaceKeyUpEvent(event: Event): Event {
const activatable = this.gameScene.getActivatablesManager().getSelectedActivatableObject();
if (activatable && activatable.isActivatable()) {
const activatableManager = this.gameScene.getActivatablesManager();
const activatable = activatableManager.getSelectedActivatableObject();
if (activatable && activatable.isActivatable() && activatableManager.isSelectingByDistanceEnabled()) {
activatable.activate();
}
return event;
}
public addSpaceEventListener(callback: Function): void {
this.gameScene.input.keyboard.addListener("keyup-SPACE", callback);
this.gameScene.getActivatablesManager().disableSelectingByDistance();
}
public removeSpaceEventListner(callback: Function): void {
this.gameScene.input.keyboard.removeListener("keyup-SPACE", callback);
this.gameScene.getActivatablesManager().enableSelectingByDistance();
}
}

View file

@ -223,10 +223,10 @@ export class UserInputManager {
}
addSpaceEventListner(callback: Function) {
this.scene.input.keyboard.addListener("keyup-SPACE", callback);
this.userInputHandler.addSpaceEventListener(callback);
}
removeSpaceEventListner(callback: Function) {
this.scene.input.keyboard.removeListener("keyup-SPACE", callback);
this.userInputHandler.removeSpaceEventListner(callback);
}
destroy(): void {
@ -255,6 +255,11 @@ export class UserInputManager {
(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => {
this.joystick?.hide();
this.userInputHandler.handlePointerUpEvent(pointer, gameObjects);
// Disable focus on iframe (need by Firefox)
if (pointer.downElement.nodeName === "CANVAS" && document.activeElement instanceof HTMLIFrameElement) {
document.activeElement.blur();
}
}
);

View file

@ -1,5 +1,5 @@
import { derived, get, writable } from "svelte/store";
import type { CoWebsite } from "../WebRtc/CoWebsiteManager";
import { derived, writable } from "svelte/store";
import type { CoWebsite } from "../WebRtc/CoWebsite/CoWesbite";
function createCoWebsiteStore() {
const { subscribe, set, update } = writable(Array<CoWebsite>());
@ -9,7 +9,7 @@ function createCoWebsiteStore() {
return {
subscribe,
add: (coWebsite: CoWebsite, position?: number) => {
coWebsite.state.subscribe((value) => {
coWebsite.getStateSubscriber().subscribe((value) => {
update((currentArray) => currentArray);
});
@ -31,7 +31,7 @@ function createCoWebsiteStore() {
},
remove: (coWebsite: CoWebsite) => {
update((currentArray) => [
...currentArray.filter((currentCoWebsite) => currentCoWebsite.iframe.id !== coWebsite.iframe.id),
...currentArray.filter((currentCoWebsite) => currentCoWebsite.getId() !== coWebsite.getId()),
]);
},
empty: () => {
@ -43,9 +43,9 @@ function createCoWebsiteStore() {
export const coWebsites = createCoWebsiteStore();
export const coWebsitesNotAsleep = derived([coWebsites], ([$coWebsites]) =>
$coWebsites.filter((coWebsite) => get(coWebsite.state) !== "asleep")
$coWebsites.filter((coWebsite) => coWebsite.getState() !== "asleep")
);
export const mainCoWebsite = derived([coWebsites], ([$coWebsites]) =>
$coWebsites.find((coWebsite) => get(coWebsite.state) !== "asleep")
$coWebsites.find((coWebsite) => coWebsite.getState() !== "asleep")
);

View file

@ -1,5 +1,5 @@
import { derived, get, writable } from "svelte/store";
import type { CoWebsite } from "../WebRtc/CoWebsiteManager";
import type { CoWebsite } from "../WebRtc/CoWebsite/CoWesbite";
import { LayoutMode } from "../WebRtc/LayoutManager";
import { coWebsites } from "./CoWebsiteStore";
import { Streamable, streamableCollectionStore } from "./StreamableCollectionStore";
@ -31,7 +31,7 @@ function createHighlightedEmbedScreenStore() {
embedScreen.type !== currentEmbedScreen.type ||
(embedScreen.type === "cowebsite" &&
currentEmbedScreen.type === "cowebsite" &&
embedScreen.embed.iframe.id !== currentEmbedScreen.embed.iframe.id) ||
embedScreen.embed.getId() !== currentEmbedScreen.embed.getId()) ||
(embedScreen.type === "streamable" &&
currentEmbedScreen.type === "streamable" &&
embedScreen.embed.uniqueId !== currentEmbedScreen.embed.uniqueId)

View file

@ -1,4 +1,5 @@
import { derived, writable } from "svelte/store";
import type { ActivatablesManager } from "../Phaser/Game/ActivatablesManager";
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
export interface LayoutManagerAction {

View file

@ -5,38 +5,33 @@ export function createColorStore() {
let followColor: number | undefined = undefined;
let apiColor: number | undefined = undefined;
let pointedByPointer: boolean = false;
let pointedByCharacter: boolean = false;
let pointedByPointer: number | undefined = undefined;
let pointedByCharacter: number | undefined = undefined;
const updateColor = () => {
if (pointedByPointer || pointedByCharacter) {
set(0xffff00);
} else {
set(followColor ?? apiColor);
}
set(pointedByPointer ?? pointedByCharacter ?? followColor ?? apiColor);
};
return {
subscribe,
pointerOver() {
pointedByPointer = true;
pointerOver(color: number) {
pointedByPointer = color;
updateColor();
},
pointerOut() {
pointedByPointer = false;
pointedByPointer = undefined;
updateColor();
},
characterCloseBy() {
pointedByCharacter = true;
characterCloseBy(color: number) {
pointedByCharacter = color;
updateColor();
},
characterFarAway() {
pointedByCharacter = false;
pointedByCharacter = undefined;
updateColor();
},

View file

@ -0,0 +1,6 @@
import { Readable, writable } from "svelte/store";
/**
* A store that contains the map starting layers names
*/
export const startLayerNamesStore = writable<string[]>([]);

View file

@ -19,6 +19,10 @@ export class PathfindingManager {
this.setEasyStarGrid(collisionsGrid);
}
public setCollisionGrid(collisionGrid: number[][]): void {
this.setEasyStarGrid(collisionGrid);
}
public async findPath(
start: { x: number; y: number },
end: { x: number; y: number },

View file

@ -0,0 +1,12 @@
export class StringUtils {
public static parsePointFromParam(param: string, separator: string = ","): { x: number; y: number } | undefined {
const values = param.split(separator).map((val) => parseInt(val));
if (values.length !== 2) {
return;
}
if (isNaN(values[0]) || isNaN(values[1])) {
return;
}
return { x: values[0], y: values[1] };
}
}

View file

@ -0,0 +1,17 @@
import type CancelablePromise from "cancelable-promise";
import type { Readable, Writable } from "svelte/store";
export type CoWebsiteState = "asleep" | "loading" | "ready";
export interface CoWebsite {
getId(): string;
getUrl(): URL;
getState(): CoWebsiteState;
getStateSubscriber(): Readable<CoWebsiteState>;
getIframe(): HTMLIFrameElement | undefined;
getLoadIframe(): CancelablePromise<HTMLIFrameElement> | undefined;
getWidthPercent(): number | undefined;
isClosable(): boolean;
load(): CancelablePromise<HTMLIFrameElement>;
unload(): Promise<void>;
}

View file

@ -0,0 +1,49 @@
import CancelablePromise from "cancelable-promise";
import { gameManager } from "../../Phaser/Game/GameManager";
import { jitsiFactory } from "../JitsiFactory";
import { SimpleCoWebsite } from "./SimpleCoWebsite";
export class JitsiCoWebsite extends SimpleCoWebsite {
private jitsiLoadPromise?: () => CancelablePromise<HTMLIFrameElement>;
setJitsiLoadPromise(promise: () => CancelablePromise<HTMLIFrameElement>): void {
this.jitsiLoadPromise = promise;
}
load(): CancelablePromise<HTMLIFrameElement> {
return new CancelablePromise((resolve, reject, cancel) => {
this.state.set("loading");
gameManager.getCurrentGameScene().disableMediaBehaviors();
if (!this.jitsiLoadPromise) {
return reject("Undefined Jitsi start callback");
}
const jitsiLoading = this.jitsiLoadPromise()
.then((iframe) => {
this.iframe = iframe;
this.iframe.classList.add("pixel");
this.state.set("ready");
return resolve(iframe);
})
.catch((err) => {
return reject(err);
});
cancel(() => {
jitsiLoading.cancel();
this.unload().catch((err) => {
console.error("Cannot unload Jitsi co-website while cancel loading", err);
});
});
});
}
unload(): Promise<void> {
jitsiFactory.destroy();
gameManager.getCurrentGameScene().enableMediaBehaviors();
return super.unload();
}
}

View file

@ -0,0 +1,133 @@
import CancelablePromise from "cancelable-promise";
import { get, Readable, writable, Writable } from "svelte/store";
import { iframeListener } from "../../Api/IframeListener";
import { coWebsiteManager } from "../CoWebsiteManager";
import type { CoWebsite, CoWebsiteState } from "./CoWesbite";
export class SimpleCoWebsite implements CoWebsite {
protected id: string;
protected url: URL;
protected state: Writable<CoWebsiteState>;
protected iframe?: HTMLIFrameElement;
protected loadIframe?: CancelablePromise<HTMLIFrameElement>;
protected allowApi?: boolean;
protected allowPolicy?: string;
protected widthPercent?: number;
protected closable: boolean;
constructor(url: URL, allowApi?: boolean, allowPolicy?: string, widthPercent?: number, closable?: boolean) {
this.id = coWebsiteManager.generateUniqueId();
this.url = url;
this.state = writable("asleep" as CoWebsiteState);
this.allowApi = allowApi;
this.allowPolicy = allowPolicy;
this.widthPercent = widthPercent;
this.closable = closable ?? false;
}
getId(): string {
return this.id;
}
getUrl(): URL {
return this.url;
}
getState(): CoWebsiteState {
return get(this.state);
}
getStateSubscriber(): Readable<CoWebsiteState> {
return this.state;
}
getIframe(): HTMLIFrameElement | undefined {
return this.iframe;
}
getLoadIframe(): CancelablePromise<HTMLIFrameElement> | undefined {
return this.loadIframe;
}
getWidthPercent(): number | undefined {
return this.widthPercent;
}
isClosable(): boolean {
return this.closable;
}
load(): CancelablePromise<HTMLIFrameElement> {
this.loadIframe = new CancelablePromise((resolve, reject, cancel) => {
this.state.set("loading");
const iframe = document.createElement("iframe");
this.iframe = iframe;
this.iframe.src = this.url.toString();
this.iframe.id = this.id;
if (this.allowPolicy) {
this.iframe.allow = this.allowPolicy;
}
if (this.allowApi) {
iframeListener.registerIframe(this.iframe);
}
this.iframe.classList.add("pixel");
const onloadPromise = new Promise<void>((resolve) => {
if (this.iframe) {
this.iframe.onload = () => {
this.state.set("ready");
resolve();
};
}
});
const onTimeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => resolve(), 2000);
});
coWebsiteManager.getCoWebsiteBuffer().appendChild(this.iframe);
const race = CancelablePromise.race([onloadPromise, onTimeoutPromise])
.then(() => {
return resolve(iframe);
})
.catch((err) => {
console.error("Error on co-website loading => ", err);
return reject();
});
cancel(() => {
race.cancel();
this.unload().catch((err) => {
console.error("Cannot unload co-website while cancel loading", err);
});
});
});
return this.loadIframe;
}
unload(): Promise<void> {
return new Promise((resolve) => {
if (this.iframe) {
if (this.allowApi) {
iframeListener.unregisterIframe(this.iframe);
}
this.iframe.parentNode?.removeChild(this.iframe);
}
if (this.loadIframe) {
this.loadIframe.cancel();
this.loadIframe = undefined;
}
this.state.set("asleep");
resolve();
});
}
}

View file

@ -1,16 +1,15 @@
import { HtmlUtils } from "./HtmlUtils";
import { Subject } from "rxjs";
import { iframeListener } from "../Api/IframeListener";
import { waScaleManager } from "../Phaser/Services/WaScaleManager";
import { coWebsites, coWebsitesNotAsleep, mainCoWebsite } from "../Stores/CoWebsiteStore";
import { get, Writable, writable } from "svelte/store";
import { get, Readable, Writable, writable } from "svelte/store";
import { embedScreenLayout, highlightedEmbedScreen } from "../Stores/EmbedScreensStore";
import { isMediaBreakpointDown } from "../Utils/BreakpointsUtils";
import { jitsiFactory } from "./JitsiFactory";
import { gameManager } from "../Phaser/Game/GameManager";
import { LayoutMode } from "./LayoutManager";
import type { CoWebsite } from "./CoWebsite/CoWesbite";
import type CancelablePromise from "cancelable-promise";
enum iframeStates {
export enum iframeStates {
closed = 1,
loading, // loading an iframe can be slow, so we show some placeholder until it is ready
opened,
@ -21,7 +20,7 @@ const gameOverlayDomId = "game-overlay";
const cowebsiteBufferDomId = "cowebsite-buffer"; // the id of the container who contains cowebsite iframes.
const cowebsiteAsideHolderDomId = "cowebsite-aside-holder";
const cowebsiteLoaderDomId = "cowebsite-loader";
export const cowebsiteCloseButtonId = "cowebsite-close";
const cowebsiteCloseButtonId = "cowebsite-close";
const cowebsiteFullScreenButtonId = "cowebsite-fullscreen";
const cowebsiteOpenFullScreenImageId = "cowebsite-fullscreen-open";
const cowebsiteCloseFullScreenImageId = "cowebsite-fullscreen-close";
@ -34,29 +33,12 @@ interface TouchMoveCoordinates {
y: number;
}
export type CoWebsiteState = "asleep" | "loading" | "ready";
export type CoWebsite = {
iframe: HTMLIFrameElement;
url: URL;
state: Writable<CoWebsiteState>;
closable: boolean;
allowPolicy: string | undefined;
allowApi: boolean | undefined;
jitsi?: boolean;
altMessage?: string;
};
class CoWebsiteManager {
private openedMain: iframeStates = iframeStates.closed;
private openedMain: Writable<iframeStates> = writable(iframeStates.closed);
private _onResize: Subject<void> = new Subject();
public onResize = this._onResize.asObservable();
/**
* Quickly going in and out of an iframe trigger can create conflicts between the iframe states.
* So we use this promise to queue up every cowebsite state transition
*/
private currentOperationPromise: Promise<void> = Promise.resolve();
private cowebsiteDom: HTMLDivElement;
private resizing: boolean = false;
private gameOverlayDom: HTMLDivElement;
@ -74,6 +56,14 @@ class CoWebsiteManager {
this.resizeAllIframes();
});
public getMainState() {
return get(this.openedMain);
}
public getMainStateSubscriber(): Readable<iframeStates> {
return this.openedMain;
}
get width(): number {
return this.cowebsiteDom.clientWidth;
}
@ -82,8 +72,13 @@ class CoWebsiteManager {
this.cowebsiteDom.style.width = width + "px";
}
set widthPercent(width: number) {
this.cowebsiteDom.style.width = width + "%";
get maxWidth(): number {
let maxWidth = 75 * window.innerWidth;
if (maxWidth !== 0) {
maxWidth = Math.round(maxWidth / 100);
}
return maxWidth;
}
get height(): number {
@ -94,6 +89,15 @@ class CoWebsiteManager {
this.cowebsiteDom.style.height = height + "px";
}
get maxHeight(): number {
let maxHeight = 60 * window.innerHeight;
if (maxHeight !== 0) {
maxHeight = Math.round(maxHeight / 100);
}
return maxHeight;
}
get verticalMode(): boolean {
return window.innerWidth < window.innerHeight;
}
@ -128,13 +132,11 @@ class CoWebsiteManager {
throw new Error("Undefined main co-website on closing");
}
if (coWebsite.closable) {
this.closeCoWebsite(coWebsite).catch(() => {
console.error("Error during closing a co-website by a button");
});
if (coWebsite.isClosable()) {
this.closeCoWebsite(coWebsite);
} else {
this.unloadCoWebsite(coWebsite).catch(() => {
console.error("Error during unloading a co-website by a button");
this.unloadCoWebsite(coWebsite).catch((err) => {
console.error("Cannot unload co-website on click on close button", err);
});
}
});
@ -191,29 +193,21 @@ class CoWebsiteManager {
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 if (tempValue > this.maxHeight) {
this.height = this.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 if (tempValue > this.maxWidth) {
this.width = this.maxWidth;
} else {
this.width = tempValue;
}
@ -230,7 +224,10 @@ class CoWebsiteManager {
return;
}
coWebsite.iframe.style.display = "none";
const iframe = coWebsite.getIframe();
if (iframe) {
iframe.style.display = "none";
}
this.resizing = true;
document.addEventListener("mousemove", movecallback);
});
@ -246,7 +243,10 @@ class CoWebsiteManager {
return;
}
coWebsite.iframe.style.display = "flex";
const iframe = coWebsite.getIframe();
if (iframe) {
iframe.style.display = "flex";
}
this.resizing = false;
});
@ -259,7 +259,10 @@ class CoWebsiteManager {
return;
}
coWebsite.iframe.style.display = "none";
const iframe = coWebsite.getIframe();
if (iframe) {
iframe.style.display = "none";
}
this.resizing = true;
const touchEvent = event.touches[0];
this.previousTouchMoveCoordinates = { x: touchEvent.pageX, y: touchEvent.pageY };
@ -278,7 +281,10 @@ class CoWebsiteManager {
return;
}
coWebsite.iframe.style.display = "flex";
const iframe = coWebsite.getIframe();
if (iframe) {
iframe.style.display = "flex";
}
this.resizing = false;
});
}
@ -299,16 +305,43 @@ class CoWebsiteManager {
});
}
public displayMain() {
const coWebsite = this.getMainCoWebsite();
if (coWebsite) {
const iframe = coWebsite.getIframe();
if (iframe) {
iframe.style.display = "block";
}
}
this.loadMain(coWebsite?.getWidthPercent());
this.openMain();
this.fire();
}
public hideMain() {
const coWebsite = this.getMainCoWebsite();
if (coWebsite) {
const iframe = coWebsite.getIframe();
if (iframe) {
iframe.style.display = "none";
}
}
this.cowebsiteDom.classList.add("closing");
this.cowebsiteDom.classList.remove("opened");
this.openedMain.set(iframeStates.closed);
this.fire();
}
private closeMain(): void {
this.toggleFullScreenIcon(true);
this.cowebsiteDom.classList.add("closing");
this.cowebsiteDom.classList.remove("opened");
this.openedMain = iframeStates.closed;
this.openedMain.set(iframeStates.closed);
this.resetStyleMain();
this.fire();
}
private loadMain(): void {
private loadMain(openingWidth?: number): void {
this.loaderAnimationInterval.interval = setInterval(() => {
if (!this.loaderAnimationInterval.trails) {
this.loaderAnimationInterval.trails = [0, 1, 2];
@ -337,16 +370,34 @@ class CoWebsiteManager {
trail === 3 ? 0 : trail + 1
);
}, 200);
if (!this.verticalMode && openingWidth) {
let newWidth = 50;
if (openingWidth > 100) {
newWidth = 100;
} else if (openingWidth > 1) {
newWidth = openingWidth;
}
newWidth = Math.round((newWidth * this.maxWidth) / 100);
if (newWidth < this.cowebsiteAsideHolderDom.offsetWidth) {
newWidth = this.cowebsiteAsideHolderDom.offsetWidth;
}
this.width = newWidth;
}
this.cowebsiteDom.classList.add("opened");
this.openedMain = iframeStates.loading;
this.openedMain.set(iframeStates.loading);
}
private openMain(): void {
this.cowebsiteDom.addEventListener("transitionend", () => {
this.resizeAllIframes();
});
this.openedMain = iframeStates.opened;
this.resetStyleMain();
this.openedMain.set(iframeStates.opened);
}
public resetStyleMain() {
@ -359,7 +410,9 @@ class CoWebsiteManager {
}
public getCoWebsiteById(coWebsiteId: string): CoWebsite | undefined {
return get(coWebsites).find((coWebsite: CoWebsite) => coWebsite.iframe.id === coWebsiteId);
return get(coWebsites).find((coWebsite: CoWebsite) => {
return coWebsite.getId() === coWebsiteId;
});
}
private getCoWebsiteByPosition(position: number): CoWebsite | undefined {
@ -379,7 +432,9 @@ class CoWebsiteManager {
}
private getPositionByCoWebsite(coWebsite: CoWebsite): number {
return get(coWebsites).findIndex((currentCoWebsite) => currentCoWebsite.iframe.id === coWebsite.iframe.id);
return get(coWebsites).findIndex((currentCoWebsite) => {
return currentCoWebsite.getId() === coWebsite.getId();
});
}
private getSlotByCowebsite(coWebsite: CoWebsite): HTMLDivElement | undefined {
@ -393,7 +448,7 @@ class CoWebsiteManager {
if (index === 0) {
id += "main";
} else {
id += coWebsite.iframe.id;
id += coWebsite.getId();
}
const slot = HtmlUtils.getElementById<HTMLDivElement>(id);
@ -410,60 +465,72 @@ class CoWebsiteManager {
const bounding = coWebsiteSlot.getBoundingClientRect();
coWebsite.iframe.style.top = bounding.top + "px";
coWebsite.iframe.style.left = bounding.left + "px";
coWebsite.iframe.style.width = bounding.right - bounding.left + "px";
coWebsite.iframe.style.height = bounding.bottom - bounding.top + "px";
const iframe = coWebsite.getIframe();
if (iframe) {
iframe.style.top = bounding.top + "px";
iframe.style.left = bounding.left + "px";
iframe.style.width = bounding.right - bounding.left + "px";
iframe.style.height = bounding.bottom - bounding.top + "px";
}
}
public resizeAllIframes() {
const mainCoWebsite = this.getCoWebsiteByPosition(0);
const mainIframe = mainCoWebsite?.getIframe();
const highlightEmbed = get(highlightedEmbedScreen);
get(coWebsites).forEach((coWebsite) => {
const notMain = !mainCoWebsite || (mainCoWebsite && mainCoWebsite.iframe.id !== coWebsite.iframe.id);
get(coWebsites).forEach((coWebsite: CoWebsite) => {
const iframe = coWebsite.getIframe();
if (!iframe) {
return;
}
const notMain = !mainCoWebsite || (mainCoWebsite && mainIframe && mainIframe.id !== iframe.id);
const notHighlighEmbed =
!highlightEmbed ||
(highlightEmbed &&
(highlightEmbed.type !== "cowebsite" ||
(highlightEmbed.type === "cowebsite" &&
highlightEmbed.embed.iframe.id !== coWebsite.iframe.id)));
(highlightEmbed.type === "cowebsite" && highlightEmbed.embed.getId() !== coWebsite.getId())));
if (coWebsite.iframe.classList.contains("main") && notMain) {
coWebsite.iframe.classList.remove("main");
if (iframe.classList.contains("main") && notMain) {
iframe.classList.remove("main");
}
if (coWebsite.iframe.classList.contains("highlighted") && notHighlighEmbed) {
coWebsite.iframe.classList.remove("highlighted");
coWebsite.iframe.classList.add("pixel");
coWebsite.iframe.style.top = "-1px";
coWebsite.iframe.style.left = "-1px";
if (iframe.classList.contains("highlighted") && notHighlighEmbed) {
iframe.classList.remove("highlighted");
iframe.classList.add("pixel");
iframe.style.top = "-1px";
iframe.style.left = "-1px";
}
if (notMain && notHighlighEmbed) {
coWebsite.iframe.classList.add("pixel");
coWebsite.iframe.style.top = "-1px";
coWebsite.iframe.style.left = "-1px";
iframe.classList.add("pixel");
iframe.style.top = "-1px";
iframe.style.left = "-1px";
}
this.setIframeOffset(coWebsite);
});
if (mainCoWebsite) {
mainCoWebsite.iframe.classList.add("main");
mainCoWebsite.iframe.classList.remove("pixel");
if (mainIframe) {
mainIframe.classList.add("main");
mainIframe.classList.remove("pixel");
}
if (highlightEmbed && highlightEmbed.type === "cowebsite") {
highlightEmbed.embed.iframe.classList.add("highlighted");
highlightEmbed.embed.iframe.classList.remove("pixel");
const highlightEmbedIframe = highlightEmbed.embed.getIframe();
if (highlightEmbedIframe) {
highlightEmbedIframe.classList.add("highlighted");
highlightEmbedIframe.classList.remove("pixel");
}
}
}
private removeHighlightCoWebsite(coWebsite: CoWebsite) {
const highlighted = get(highlightedEmbedScreen);
if (highlighted && highlighted.type === "cowebsite" && highlighted.embed.iframe.id === coWebsite.iframe.id) {
if (highlighted && highlighted.type === "cowebsite" && highlighted.embed.getId() === coWebsite.getId()) {
highlightedEmbedScreen.removeHighlight();
}
}
@ -476,7 +543,9 @@ class CoWebsiteManager {
this.closeMain();
}
coWebsite.iframe.remove();
coWebsite.unload().catch((err) => {
console.error("Cannot unload cowebsite on remove from stack");
});
}
public goToMain(coWebsite: CoWebsite) {
@ -488,38 +557,21 @@ class CoWebsiteManager {
isMediaBreakpointDown("lg") &&
get(embedScreenLayout) === LayoutMode.Presentation &&
mainCoWebsite &&
mainCoWebsite.iframe.id !== coWebsite.iframe.id &&
get(mainCoWebsite.state) !== "asleep"
mainCoWebsite.getId() !== coWebsite.getId() &&
mainCoWebsite.getState() !== "asleep"
) {
highlightedEmbedScreen.toggleHighlight({
type: "cowebsite",
embed: mainCoWebsite,
});
highlightedEmbedScreen.removeHighlight();
}
this.resizeAllIframes();
}
public searchJitsi(): CoWebsite | undefined {
return get(coWebsites).find((coWebsite: CoWebsite) => coWebsite.jitsi);
}
private initialiseCowebsite(coWebsite: CoWebsite, position: number | undefined) {
if (coWebsite.allowPolicy) {
coWebsite.iframe.allow = coWebsite.allowPolicy;
}
if (coWebsite.allowApi) {
iframeListener.registerIframe(coWebsite.iframe);
}
coWebsite.iframe.classList.add("pixel");
public addCoWebsiteToStore(coWebsite: CoWebsite, position: number | undefined) {
const coWebsitePosition = position === undefined ? get(coWebsites).length : position;
coWebsites.add(coWebsite, coWebsitePosition);
}
private generateUniqueId() {
public generateUniqueId() {
let id = undefined;
do {
id = "cowebsite-iframe-" + (Math.random() + 1).toString(36).substring(7);
@ -528,201 +580,89 @@ class CoWebsiteManager {
return id;
}
public addCoWebsite(
url: string,
base: string,
allowApi?: boolean,
allowPolicy?: string,
position?: number,
closable?: boolean,
altMessage?: string
): CoWebsite {
const iframe = document.createElement("iframe");
const fullUrl = new URL(url, base);
iframe.src = fullUrl.toString();
iframe.id = this.generateUniqueId();
const newCoWebsite: CoWebsite = {
iframe,
url: fullUrl,
state: writable("asleep" as CoWebsiteState),
closable: closable ?? false,
allowPolicy,
allowApi,
altMessage,
};
this.initialiseCowebsite(newCoWebsite, position);
return newCoWebsite;
}
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<CoWebsite> {
public loadCoWebsite(coWebsite: CoWebsite): CancelablePromise<void> {
if (get(coWebsitesNotAsleep).length < 1) {
coWebsites.remove(coWebsite);
coWebsites.add(coWebsite, 0);
this.loadMain();
this.loadMain(coWebsite.getWidthPercent());
}
coWebsite.state.set("loading");
// Check if the main is hide
if (this.getMainCoWebsite() && this.getMainState() === iframeStates.closed) {
this.displayMain();
}
const mainCoWebsite = this.getMainCoWebsite();
const coWebsiteLloading = coWebsite
.load()
.then(() => {
const mainCoWebsite = this.getMainCoWebsite();
if (mainCoWebsite && mainCoWebsite.getId() === coWebsite.getId()) {
this.openMain();
return new Promise((resolve, reject) => {
const onloadPromise = new Promise<void>((resolve) => {
coWebsite.iframe.onload = () => {
coWebsite.state.set("ready");
resolve();
};
setTimeout(() => {
this.fire();
}, animationTime);
}
this.resizeAllIframes();
})
.catch((err) => {
console.error("Error on co-website loading => ", err);
this.removeCoWebsiteFromStack(coWebsite);
});
const onTimeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => resolve(), 2000);
});
this.cowebsiteBufferDom.appendChild(coWebsite.iframe);
if (coWebsite.jitsi) {
const gameScene = gameManager.getCurrentGameScene();
gameScene.disableMediaBehaviors();
jitsiFactory.restart();
}
this.currentOperationPromise = this.currentOperationPromise
.then(() => Promise.race([onloadPromise, onTimeoutPromise]))
.then(() => {
if (mainCoWebsite && mainCoWebsite.iframe.id === coWebsite.iframe.id) {
this.openMain();
setTimeout(() => {
this.fire();
}, animationTime);
}
return resolve(coWebsite);
})
.catch((err) => {
console.error("Error on co-website loading => ", err);
this.removeCoWebsiteFromStack(coWebsite);
return reject();
});
});
return coWebsiteLloading;
}
public unloadCoWebsite(coWebsite: CoWebsite): Promise<void> {
return new Promise((resolve, reject) => {
this.removeHighlightCoWebsite(coWebsite);
this.removeHighlightCoWebsite(coWebsite);
coWebsite.iframe.parentNode?.removeChild(coWebsite.iframe);
coWebsite.state.set("asleep");
coWebsites.remove(coWebsite);
return coWebsite
.unload()
.then(() => {
coWebsites.remove(coWebsite);
const mainCoWebsite = this.getMainCoWebsite();
if (coWebsite.jitsi) {
jitsiFactory.stop();
const gameScene = gameManager.getCurrentGameScene();
gameScene.enableMediaBehaviors();
}
if (mainCoWebsite) {
this.removeHighlightCoWebsite(mainCoWebsite);
this.goToMain(mainCoWebsite);
this.resizeAllIframes();
} else {
this.closeMain();
}
const mainCoWebsite = this.getMainCoWebsite();
coWebsites.add(coWebsite, get(coWebsites).length);
})
.catch(() => {
console.error();
});
}
if (mainCoWebsite) {
this.removeHighlightCoWebsite(mainCoWebsite);
this.goToMain(mainCoWebsite);
this.resizeAllIframes();
} else {
this.closeMain();
}
public closeCoWebsite(coWebsite: CoWebsite): void {
if (get(coWebsites).length === 1) {
this.fire();
}
coWebsites.add(coWebsite, get(coWebsites).length);
this.removeCoWebsiteFromStack(coWebsite);
resolve();
const mainCoWebsite = this.getMainCoWebsite();
if (mainCoWebsite) {
this.removeHighlightCoWebsite(mainCoWebsite);
this.goToMain(mainCoWebsite);
this.resizeAllIframes();
} else {
this.closeMain();
}
}
public closeCoWebsites(): void {
get(coWebsites).forEach((coWebsite: CoWebsite) => {
this.closeCoWebsite(coWebsite);
});
}
public closeCoWebsite(coWebsite: CoWebsite): Promise<void> {
this.currentOperationPromise = this.currentOperationPromise.then(
() =>
new Promise((resolve) => {
if (coWebsite.jitsi) {
jitsiFactory.destroy();
const gameScene = gameManager.getCurrentGameScene();
gameScene.enableMediaBehaviors();
}
if (get(coWebsites).length === 1) {
this.fire();
}
if (coWebsite.allowApi) {
iframeListener.unregisterIframe(coWebsite.iframe);
}
this.removeCoWebsiteFromStack(coWebsite);
const mainCoWebsite = this.getMainCoWebsite();
if (mainCoWebsite) {
this.removeHighlightCoWebsite(mainCoWebsite);
this.goToMain(mainCoWebsite);
this.resizeAllIframes();
} else {
this.closeMain();
}
resolve();
})
);
return this.currentOperationPromise;
}
public closeCoWebsites(): Promise<void> {
return (this.currentOperationPromise = this.currentOperationPromise.then(() => {
get(coWebsites).forEach((coWebsite: CoWebsite) => {
this.closeCoWebsite(coWebsite).catch(() => {
console.error("Error during closing a co-website");
});
});
}));
return this.currentOperationPromise;
}
public getGameSize(): { width: number; height: number } {
if (this.openedMain === iframeStates.closed) {
if (this.getMainState() === iframeStates.closed) {
return {
width: window.innerWidth,
height: window.innerHeight,

View file

@ -1,7 +1,8 @@
import { JITSI_URL } from "../Enum/EnvironmentVariable";
import { CoWebsite, coWebsiteManager } from "./CoWebsiteManager";
import { coWebsiteManager } from "./CoWebsiteManager";
import { requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore";
import { get } from "svelte/store";
import CancelablePromise from "cancelable-promise";
interface jitsiConfigInterface {
startWithAudioMuted: boolean;
@ -134,114 +135,96 @@ class JitsiFactory {
return slugify((instance !== 'global') ? instance.replace("/", "-") + "-" + roomName : roomName);
}
public async start(
public start(
roomName: string,
playerName: string,
jwt?: string,
config?: object,
interfaceConfig?: object,
jitsiUrl?: string
) {
const coWebsite = coWebsiteManager.searchJitsi();
): CancelablePromise<HTMLIFrameElement> {
return new CancelablePromise((resolve, reject, cancel) => {
// Jitsi meet external API maintains some data in local storage
// which is sent via the appData URL parameter when joining a
// conference. Problem is that this data grows indefinitely. Thus
// after some time the URLs get so huge that loading the iframe
// becomes slow and eventually breaks completely. Thus lets just
// clear jitsi local storage before starting a new conference.
window.localStorage.removeItem("jitsiLocalStorage");
if (coWebsite) {
await coWebsiteManager.closeCoWebsite(coWebsite);
}
// Jitsi meet external API maintains some data in local storage
// which is sent via the appData URL parameter when joining a
// conference. Problem is that this data grows indefinitely. Thus
// after some time the URLs get so huge that loading the iframe
// becomes slow and eventually breaks completely. Thus lets just
// clear jitsi local storage before starting a new conference.
window.localStorage.removeItem("jitsiLocalStorage");
const domain = jitsiUrl || JITSI_URL;
if (domain === undefined) {
throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map.");
}
await this.loadJitsiScript(domain);
const options: JitsiOptions = {
roomName: roomName,
jwt: jwt,
width: "100%",
height: "100%",
parentNode: coWebsiteManager.getCoWebsiteBuffer(),
configOverwrite: mergeConfig(config),
interfaceConfigOverwrite: { ...defaultInterfaceConfig, ...interfaceConfig },
};
if (!options.jwt) {
delete options.jwt;
}
const doResolve = (): void => {
const iframe = coWebsiteManager.getCoWebsiteBuffer().querySelector<HTMLIFrameElement>('[id*="jitsi" i]');
if (iframe && this.jitsiApi) {
const coWebsite = coWebsiteManager.addCoWebsiteFromIframe(iframe, false, undefined, 0, false, true);
this.jitsiApi.addListener("videoConferenceLeft", () => {
this.closeOrUnload(coWebsite);
});
this.jitsiApi.addListener("readyToClose", () => {
this.closeOrUnload(coWebsite);
});
const domain = jitsiUrl || JITSI_URL;
if (domain === undefined) {
throw new Error("Missing JITSI_URL environment variable or jitsiUrl parameter in the map.");
}
coWebsiteManager.resizeAllIframes();
};
const loadScript = this.loadJitsiScript(domain).then(() => {
const options: JitsiOptions = {
roomName: roomName,
jwt: jwt,
width: "100%",
height: "100%",
parentNode: coWebsiteManager.getCoWebsiteBuffer(),
configOverwrite: mergeConfig(config),
interfaceConfigOverwrite: { ...defaultInterfaceConfig, ...interfaceConfig },
};
this.jitsiApi = undefined;
if (!options.jwt) {
delete options.jwt;
}
options.onload = () => doResolve(); //we want for the iframe to be loaded before triggering animations.
setTimeout(() => doResolve(), 2000); //failsafe in case the iframe is deleted before loading or too long to load
this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options);
this.jitsiApi.executeCommand("displayName", playerName);
const timemout = setTimeout(() => doResolve(), 2000); //failsafe in case the iframe is deleted before loading or too long to load
this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback);
const doResolve = (): void => {
clearTimeout(timemout);
const iframe = coWebsiteManager
.getCoWebsiteBuffer()
.querySelector<HTMLIFrameElement>('[id*="jitsi" i]');
if (iframe && this.jitsiApi) {
this.jitsiApi.addListener("videoConferenceLeft", () => {
this.closeOrUnload(iframe);
});
this.jitsiApi.addListener("readyToClose", () => {
this.closeOrUnload(iframe);
});
return resolve(iframe);
}
};
this.jitsiApi = undefined;
options.onload = () => doResolve(); //we want for the iframe to be loaded before triggering animations.
this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options);
this.jitsiApi.executeCommand("displayName", playerName);
this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback);
});
cancel(() => {
loadScript.cancel();
});
});
}
private closeOrUnload = function (coWebsite: CoWebsite) {
if (coWebsite.closable) {
coWebsiteManager.closeCoWebsite(coWebsite).catch(() => {
console.error("Error during closing a Jitsi Meet");
});
private closeOrUnload = function (iframe: HTMLIFrameElement) {
const coWebsite = coWebsiteManager.getCoWebsites().find((coWebsite) => coWebsite.getIframe() === iframe);
if (!coWebsite) {
return;
}
if (coWebsite.isClosable()) {
coWebsiteManager.closeCoWebsite(coWebsite);
} else {
coWebsiteManager.unloadCoWebsite(coWebsite).catch(() => {
console.error("Error during unloading a Jitsi Meet");
coWebsiteManager.unloadCoWebsite(coWebsite).catch((err) => {
console.error("Cannot unload co-website from the Jitsi factory", err);
});
}
};
public restart() {
if (!this.jitsiApi) {
return;
}
this.jitsiApi.addListener("audioMuteStatusChanged", this.audioCallback);
this.jitsiApi.addListener("videoMuteStatusChanged", this.videoCallback);
const coWebsite = coWebsiteManager.searchJitsi();
console.log("jitsi api ", this.jitsiApi);
console.log("iframe cowebsite", coWebsite?.iframe);
if (!coWebsite) {
this.destroy();
return;
}
this.jitsiApi.addListener("videoConferenceLeft", () => {
this.closeOrUnload(coWebsite);
});
this.jitsiApi.addListener("readyToClose", () => {
this.closeOrUnload(coWebsite);
});
}
public stop() {
if (!this.jitsiApi) {
return;
@ -276,8 +259,8 @@ class JitsiFactory {
}
}
private async loadJitsiScript(domain: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
private loadJitsiScript(domain: string): CancelablePromise<void> {
return new CancelablePromise<void>((resolve, reject, cancel) => {
if (this.jitsiScriptLoaded) {
resolve();
return;
@ -296,6 +279,10 @@ class JitsiFactory {
};
document.head.appendChild(jitsiScript);
cancel(() => {
jitsiScript.remove();
});
});
}
}

View file

@ -13,5 +13,6 @@ export enum DivImportance {
}
export const ON_ACTION_TRIGGER_BUTTON = "onaction";
export const ON_ICON_TRIGGER_BUTTON = "onicon";
export type Box = { xStart: number; yStart: number; xEnd: number; yEnd: number };

View file

@ -14,7 +14,7 @@ import warning from "./warning";
import woka from "./woka";
const de_DE: Translation = {
...en_US,
...(en_US as Translation),
language: "Deutsch",
country: "Deutschland",
audio,

View file

@ -70,6 +70,7 @@ const menu: NonNullable<Translation["menu"]> = {
description: "Link zu diesem Raum teilen!",
copy: "Kopieren",
share: "Teilen",
walk_automatically_to_position: "Walk automatically to my position",
},
globalMessage: {
text: "Text",

View file

@ -11,6 +11,7 @@ import menu from "./menu";
import report from "./report";
import warning from "./warning";
import emoji from "./emoji";
import trigger from "./trigger";
const en_US: BaseTranslation = {
language: "English",
@ -27,6 +28,7 @@ const en_US: BaseTranslation = {
report,
warning,
emoji,
trigger,
};
export default en_US;

View file

@ -70,6 +70,7 @@ const menu: BaseTranslation = {
description: "Share the link of the room!",
copy: "Copy",
share: "Share",
walk_automatically_to_position: "Walk automatically to my position",
},
globalMessage: {
text: "Text",

View file

@ -0,0 +1,9 @@
import type { BaseTranslation } from "../i18n-types";
const trigger: BaseTranslation = {
cowebsite: "Press SPACE or touch here to open web site",
jitsiRoom: "Press SPACE or touch here to enter Jitsi Meet room",
newTab: "Press SPACE or touch here to open web site in new tab",
};
export default trigger;

View file

@ -12,9 +12,10 @@ import menu from "./menu";
import report from "./report";
import warning from "./warning";
import woka from "./woka";
import trigger from "./trigger";
const fr_FR: Translation = {
...en_US,
...(en_US as Translation),
language: "Français",
country: "France",
audio,
@ -29,6 +30,7 @@ const fr_FR: Translation = {
report,
warning,
emoji,
trigger,
};
export default fr_FR;

View file

@ -63,13 +63,14 @@ const menu: NonNullable<Translation["menu"]> = {
},
fullscreen: "Plein écran",
notifications: "Notifications",
cowebsiteTrigger: "Demander toujours avant d'ouvrir des sites web et des salles de réunion Jitsi",
cowebsiteTrigger: "Demander toujours avant d'ouvrir des sites web et des salles de conférence Jitsi",
ignoreFollowRequest: "Ignorer les demandes de suivi des autres utilisateurs",
},
invite: {
description: "Partager le lien de la salle!",
copy: "Copier",
share: "Partager",
walk_automatically_to_position: "Marcher automatiquement jusqu'à ma position",
},
globalMessage: {
text: "Texte",

View file

@ -0,0 +1,9 @@
import type { Translation } from "../i18n-types";
const trigger: NonNullable<Translation["trigger"]> = {
cowebsite: "Appuyez sur ESPACE ou ici pour ouvrir le site Web",
jitsiRoom: "Appuyez sur ESPACE ou ici pour entrer dans la salle conférence Jitsi",
newTab: "Appuyez sur ESPACE ou ici pour ouvrir le site Web dans un nouvel onglet",
};
export default trigger;