diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index e2603ca8..b964477d 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -570,6 +570,7 @@ export class GameRoom { mapUrl, authenticationMandatory: null, group: null, + showPoweredBy: true, }; } diff --git a/docs/maps/api-controls.md b/docs/maps/api-controls.md index c2b47262..3097b835 100644 --- a/docs/maps/api-controls.md +++ b/docs/maps/api-controls.md @@ -14,7 +14,7 @@ When controls are disabled, the user cannot move anymore using keyboard input. T Example: -```javascript +```ts WA.room.onEnterLayer('myZone').subscribe(() => { WA.controls.disablePlayerControls(); WA.ui.openPopup("popupRectangle", 'This is an imporant message!', [{ @@ -25,5 +25,28 @@ WA.room.onEnterLayer('myZone').subscribe(() => { popup.close(); } }]); -}) +}); +``` + +### Disabling / restoring proximity meeting + +``` +WA.controls.disablePlayerProximityMeeting(): void +WA.controls.restorePlayerProximityMeeting(): void +``` + +These 2 methods can be used to completely disable player proximity meeting and to enable them again. + +When proximity meeting are disabled, the user cannot speak with anyone. + +Example: + +```ts +WA.room.onEnterLayer('myZone').subscribe(() => { + WA.controls.disablePlayerProximityMeeting(); +}); + +WA.room.onLeaveLayer('myZone').subscribe(() => { + WA.controls.restorePlayerProximityMeeting(); +}); ``` diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md index 2fb97e0a..db460ad0 100644 --- a/docs/maps/api-ui.md +++ b/docs/maps/api-ui.md @@ -34,7 +34,7 @@ Please note that `openPopup` returns an object of the `Popup` class. Also, the c The `Popup` class that represents an open popup contains a single method: `close()`. This will obviously close the popup when called. -```javascript +```ts class Popup { /** * Closes the popup @@ -45,7 +45,7 @@ class Popup { Example: -```javascript +```ts let helloWorldPopup; // Open the popup when we enter a given zone @@ -90,8 +90,8 @@ Custom menu exist only until the map is unloaded, or you leave the iframe zone o -Example: -```javascript +Example: +```ts const menu = WA.ui.registerMenuCommand('menu test', { callback: () => { @@ -107,7 +107,7 @@ Please note that `registerMenuCommand` returns an object of the `Menu` class. The `Menu` class contains a single method: `remove(): void`. This will obviously remove the menu when called. -```javascript +```ts class Menu { /** * Remove the menu @@ -120,7 +120,7 @@ class Menu { ### Awaiting User Confirmation (with space bar) -``` +```ts WA.ui.displayActionMessage({ message: string, callback: () => void, @@ -136,7 +136,7 @@ Displays a message at the bottom of the screen (that will disappear when space b Example: -```javascript +```ts const triggerMessage = WA.ui.displayActionMessage({ message: "press 'space' to confirm", callback: () => { @@ -154,7 +154,7 @@ Please note that `displayActionMessage` returns an object of the `ActionMessage` The `ActionMessage` class contains a single method: `remove(): Promise`. This will obviously remove the message when called. -```javascript +```ts class ActionMessage { /** * Hides the message @@ -173,7 +173,7 @@ When clicking on other player's WOKA, the contextual menu (we call it ActionsMen To do that, we need to listen for the `onRemotePlayerClicked` stream and make use of the `remotePlayer` object that is passed by as a payload. -```javascript +```ts WA.ui.onRemotePlayerClicked.subscribe((remotePlayer) => { remotePlayer.addAction('Ask to tell a joke', () => { console.log('I am NOT telling you a joke!'); @@ -182,7 +182,7 @@ WA.ui.onRemotePlayerClicked.subscribe((remotePlayer) => { ``` `remotePlayer.addAction(actionName, callback)` returns an Action object, which can remove itself from ActionsMenu: -```javascript +```ts const action = remotePlayer.addAction('This will disappear!', () => { console.log('You managed to click me!'); }); @@ -193,3 +193,95 @@ setTimeout( 1000, ); ``` + +# Open fixed iframes + +You can use the scripting API to display an iframe (so any HTML element) above the game. The iframe is positionned relative to the browser window (so unlike [embedded websites](website-in-map.md), the position of the iframe does not move when someone walks on the map). + +
+ +
+ +This functonnality creates an iframe positionned on the viewport. + +## Display a UI website + +```ts +WA.ui.website.open(website: CreateUIWebsiteEvent): Promise + +interface CreateUIWebsiteEvent { + url: string, // Website URL + visible?: boolean, // The website is visible or not + allowApi?: boolean, // Allow scripting API on the website + allowPolicy?: string, // The list of feature policies allowed + position: { + vertical: "top"|"middle"|"bottom",, + horizontal: "left","middle","right", + }, + size: { // Size on the UI (available units: px|em|%|cm|in|pc|pt|mm|ex|vw|vh|rem and others values auto|inherit) + height: string, + width: string, + }, + margin?: { // Website margin (available units: px|em|%|cm|in|pc|pt|mm|ex|vw|vh|rem and others values auto|inherit) + top?: string, + bottom?: string, + left?: string, + right?: string, + }, +} + +interface UIWebsite { + readonly id: string, // Unique ID + url: string, // Website URL + visible: boolean, // The website is visible or not + readonly allowApi: boolean, // Allow scripting API on the website + readonly allowPolicy: string, // The list of feature policies allowed + position: { + vertical: string, // Vertical position (top, middle, bottom) + horizontal: string, // Horizontal position (left, middle, right) + }, + size: { // Size on the UI (available units: px|em|%|cm|in|pc|pt|mm|ex|vw|vh|rem and others values auto|inherit) + height: string, + width: string, + }, + margin?: { // Website margin (available units: px|em|%|cm|in|pc|pt|mm|ex|vw|vh|rem and others values auto|inherit) + top?: string, + bottom?: string, + left?: string, + right?: string, + }, + close(): Promise, // Close the current website instance +} +``` + +You can open a website with the `WA.ui.website.open()` method. It returns an `Promise` instance. + +```ts +const myWebsite = await WA.ui.website.open({ + url: "https://wikipedia.org", + position: { + vertical: "middle", + horizontal: "middle", + }, + size: { + height: "50vh", + width: "50vw", + }, +}); + +myWebsite.position.vertical = "top"; +``` + +## Close a UI website +You can close a website with the close function on the `UIWebsite` object + +```ts +myWebsite.close(); +``` + +## Get all UI websites +You can get all websites with the `WA.ui.website.getAll()` method. It returns an `Promise` instance. + +```ts +WA.ui.website.getAll(); +``` diff --git a/docs/maps/images/ui-website.png b/docs/maps/images/ui-website.png new file mode 100644 index 00000000..67c76a14 Binary files /dev/null and b/docs/maps/images/ui-website.png differ diff --git a/front/.gitignore b/front/.gitignore index b7cc8c89..d411e833 100644 --- a/front/.gitignore +++ b/front/.gitignore @@ -5,3 +5,4 @@ dist/ *.sh !templater.sh /public/iframe_api.js +/public/es.js diff --git a/front/package.json b/front/package.json index 8d3b6dc7..dd91ed13 100644 --- a/front/package.json +++ b/front/package.json @@ -21,7 +21,7 @@ "lint-staged": "^12.3.7", "npm-run-all": "^4.1.5", "prettier": "^2.3.1", - "prettier-plugin-svelte": "^2.5.0", + "prettier-plugin-svelte": "^2.7.0", "sass": "^1.49.7", "svelte": "^3.46.3", "svelte-check": "^2.1.0", @@ -96,4 +96,4 @@ "yarn run pretty" ] } -} +} \ No newline at end of file diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts index 36e28948..5b31d871 100644 --- a/front/src/Api/Events/IframeEvent.ts +++ b/front/src/Api/Events/IframeEvent.ts @@ -40,6 +40,7 @@ import { isAddActionsMenuKeyToRemotePlayerEvent } from "./AddActionsMenuKeyToRem import type { ActionsMenuActionClickedEvent } from "./ActionsMenuActionClickedEvent"; import { isRemoveActionsMenuKeyFromRemotePlayerEvent } from "./RemoveActionsMenuKeyFromRemotePlayerEvent"; import { isGetPropertyEvent } from "./GetPropertyEvent"; +import { isCreateUIWebsiteEvent, isModifyUIWebsiteEvent, isUIWebsite } from "./ui/UIWebsite"; export interface TypedMessageEvent extends MessageEvent { data: T; @@ -97,6 +98,14 @@ export const isIframeEventWrapper = z.union([ type: z.literal("restorePlayerControls"), data: z.undefined(), }), + z.object({ + type: z.literal("disablePlayerProximityMeeting"), + data: z.undefined(), + }), + z.object({ + type: z.literal("restorePlayerProximityMeeting"), + data: z.undefined(), + }), z.object({ type: z.literal("displayBubble"), data: z.undefined(), @@ -153,6 +162,10 @@ export const isIframeEventWrapper = z.union([ type: z.literal("modifyEmbeddedWebsite"), data: isEmbeddedWebsiteEvent, }), + z.object({ + type: z.literal("modifyUIWebsite"), + data: isModifyUIWebsiteEvent, + }), ]); export type IframeEvent = z.infer; @@ -266,6 +279,18 @@ export const iframeQueryMapTypeGuards = { query: isMovePlayerToEventConfig, answer: isMovePlayerToEventAnswer, }, + openUIWebsite: { + query: isCreateUIWebsiteEvent, + answer: isUIWebsite, + }, + closeUIWebsite: { + query: z.string(), + answer: z.undefined(), + }, + getUIWebsites: { + query: z.undefined(), + answer: z.array(isUIWebsite), + }, }; type IframeQueryMapTypeGuardsType = typeof iframeQueryMapTypeGuards; diff --git a/front/src/Api/Events/ui/UIWebsite.ts b/front/src/Api/Events/ui/UIWebsite.ts new file mode 100644 index 00000000..37ffb605 --- /dev/null +++ b/front/src/Api/Events/ui/UIWebsite.ts @@ -0,0 +1,75 @@ +import { z } from "zod"; + +const regexUnit = /-*\d+(px|em|%|cm|in|pc|pt|mm|ex|vw|vh|rem)|auto|inherit/; + +// Parse the string to check if is a valid CSS unit (px,%,vw,vh...) +export const isUIWebsiteCSSValue = z.string().regex(regexUnit); + +export type UIWebsiteCSSValue = z.infer; + +export const isUIWebsiteMargin = z.object({ + top: isUIWebsiteCSSValue.optional(), + bottom: isUIWebsiteCSSValue.optional(), + left: isUIWebsiteCSSValue.optional(), + right: isUIWebsiteCSSValue.optional(), +}); + +export type UIWebsiteMargin = z.infer; + +export const isViewportPositionVertical = z.enum(["top", "middle", "bottom"]); + +export type ViewportPositionVertical = z.infer; + +export const isViewportPositionHorizontal = z.enum(["left", "middle", "right"]); + +export type ViewportPositionHorizontal = z.infer; + +export const isUIWebsitePosition = z.object({ + vertical: isViewportPositionVertical, + horizontal: isViewportPositionHorizontal, +}); + +export type UIWebsitePosition = z.infer; + +export const isUIWebsiteSize = z.object({ + height: isUIWebsiteCSSValue, + width: isUIWebsiteCSSValue, +}); + +export type UIWebsiteSize = z.infer; + +export const isCreateUIWebsiteEvent = z.object({ + url: z.string(), + visible: z.optional(z.boolean()), + allowApi: z.optional(z.boolean()), + allowPolicy: z.optional(z.string()), + position: isUIWebsitePosition, + size: isUIWebsiteSize, + margin: isUIWebsiteMargin.optional(), +}); + +export type CreateUIWebsiteEvent = z.infer; + +export const isModifyUIWebsiteEvent = z.object({ + id: z.string(), + url: z.string().optional(), + visible: z.boolean().optional(), + position: isUIWebsitePosition.optional(), + size: isUIWebsiteSize.optional(), + margin: isUIWebsiteMargin.optional(), +}); + +export type ModifyUIWebsiteEvent = z.infer; + +export const isUIWebsite = z.object({ + id: z.string(), + url: z.string(), + visible: z.boolean(), + allowApi: z.boolean(), + allowPolicy: z.string(), + position: isUIWebsitePosition, + size: isUIWebsiteSize, + margin: isUIWebsiteMargin.optional(), +}); + +export type UIWebsite = z.infer; diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index 4156b568..60acdbc3 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -35,6 +35,7 @@ import type { RemotePlayerClickedEvent } from "./Events/RemotePlayerClickedEvent import { AddActionsMenuKeyToRemotePlayerEvent } from "./Events/AddActionsMenuKeyToRemotePlayerEvent"; import type { ActionsMenuActionClickedEvent } from "./Events/ActionsMenuActionClickedEvent"; import { RemoveActionsMenuKeyFromRemotePlayerEvent } from "./Events/RemoveActionsMenuKeyFromRemotePlayerEvent"; +import { ModifyUIWebsiteEvent } from "./Events/ui/UIWebsite"; type AnswererCallback = ( query: IframeQueryMap[T]["query"], @@ -58,6 +59,15 @@ class IframeListener { private readonly _disablePlayerControlStream: Subject = new Subject(); public readonly disablePlayerControlStream = this._disablePlayerControlStream.asObservable(); + private readonly _enablePlayerControlStream: Subject = new Subject(); + public readonly enablePlayerControlStream = this._enablePlayerControlStream.asObservable(); + + private readonly _disablePlayerProximityMeetingStream: Subject = new Subject(); + public readonly disablePlayerProximityMeetingStream = this._disablePlayerProximityMeetingStream.asObservable(); + + private readonly _enablePlayerProximityMeetingStream: Subject = new Subject(); + public readonly enablePlayerProximityMeetingStream = this._enablePlayerProximityMeetingStream.asObservable(); + private readonly _cameraSetStream: Subject = new Subject(); public readonly cameraSetStream = this._cameraSetStream.asObservable(); @@ -73,9 +83,6 @@ class IframeListener { public readonly removeActionsMenuKeyFromRemotePlayerEvent = this._removeActionsMenuKeyFromRemotePlayerEvent.asObservable(); - private readonly _enablePlayerControlStream: Subject = new Subject(); - public readonly enablePlayerControlStream = this._enablePlayerControlStream.asObservable(); - private readonly _closePopupStream: Subject = new Subject(); public readonly closePopupStream = this._closePopupStream.asObservable(); @@ -115,6 +122,9 @@ class IframeListener { private readonly _modifyEmbeddedWebsiteStream: Subject = new Subject(); public readonly modifyEmbeddedWebsiteStream = this._modifyEmbeddedWebsiteStream.asObservable(); + private readonly _modifyUIWebsiteStream: Subject = new Subject(); + public readonly modifyUIWebsiteStream = this._modifyUIWebsiteStream.asObservable(); + private readonly iframes = new Set(); private readonly iframeCloseCallbacks = new Map void)[]>(); private readonly scripts = new Map(); @@ -263,6 +273,10 @@ class IframeListener { this._disablePlayerControlStream.next(); } else if (iframeEvent.type === "restorePlayerControls") { this._enablePlayerControlStream.next(); + } else if (iframeEvent.type === "disablePlayerProximityMeeting") { + this._disablePlayerProximityMeetingStream.next(); + } else if (iframeEvent.type === "restorePlayerProximityMeeting") { + this._enablePlayerProximityMeetingStream.next(); } else if (iframeEvent.type === "displayBubble") { this._displayBubbleStream.next(); } else if (iframeEvent.type === "removeBubble") { @@ -279,6 +293,8 @@ class IframeListener { this._setTilesStream.next(iframeEvent.data); } else if (iframeEvent.type == "modifyEmbeddedWebsite") { this._modifyEmbeddedWebsiteStream.next(iframeEvent.data); + } else if (iframeEvent.type == "modifyUIWebsite") { + this._modifyUIWebsiteStream.next(iframeEvent.data); } else if (iframeEvent.type == "registerMenu") { const dataName = iframeEvent.data.name; this.iframeCloseCallbacks.get(iframe)?.push(() => { diff --git a/front/src/Api/desktop/index.ts b/front/src/Api/desktop/index.ts index 239ec8cf..c841037e 100644 --- a/front/src/Api/desktop/index.ts +++ b/front/src/Api/desktop/index.ts @@ -1,6 +1,6 @@ import { requestedCameraState, requestedMicrophoneState, silentStore } from "../../Stores/MediaStore"; import { get } from "svelte/store"; -import { WorkAdventureDesktopApi } from "@wa-preload-app"; +import { WorkAdventureDesktopApi } from "../../Interfaces/DesktopAppInterfaces"; declare global { interface Window { diff --git a/front/src/Api/iframe/Ui/UIWebsite.ts b/front/src/Api/iframe/Ui/UIWebsite.ts new file mode 100644 index 00000000..d5d8a1ad --- /dev/null +++ b/front/src/Api/iframe/Ui/UIWebsite.ts @@ -0,0 +1,338 @@ +import { + CreateUIWebsiteEvent, + UIWebsiteCSSValue, + UIWebsiteMargin, + UIWebsitePosition, + UIWebsiteSize, + ViewportPositionHorizontal, + ViewportPositionVertical, + UIWebsite as UIWebsiteCore, +} from "../../Events/ui/UIWebsite"; +import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "../IframeApiContribution"; + +class UIWebsitePositionInternal { + private readonly website: UIWebsite; + private _vertical: ViewportPositionVertical; + private _horizontal: ViewportPositionHorizontal; + + constructor(uiWebsite: UIWebsite, position: UIWebsitePosition) { + this.website = uiWebsite; + this._vertical = position.vertical; + this._horizontal = position.horizontal; + } + + public get vertical() { + return this._vertical; + } + + public set vertical(vertical: ViewportPositionVertical) { + this._vertical = vertical; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.website.id, + position: { + vertical: this._vertical, + horizontal: this._horizontal, + }, + }, + }); + } + + public get horizontal() { + return this._horizontal; + } + + public set horizontal(horizontal: ViewportPositionHorizontal) { + this._horizontal = horizontal; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.website.id, + position: { + vertical: this._vertical, + horizontal: this._horizontal, + }, + }, + }); + } +} + +class UIWebsiteSizeInternal { + private readonly website: UIWebsite; + private _height: UIWebsiteCSSValue; + private _width: UIWebsiteCSSValue; + + constructor(uiWebsite: UIWebsite, size: UIWebsiteSize) { + this.website = uiWebsite; + this._height = size.height; + this._width = size.width; + } + + public get height() { + return this._height; + } + + public set height(height: UIWebsiteCSSValue) { + this._height = height; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.website.id, + size: { + height: this._height, + width: this._width, + }, + }, + }); + } + + public get width() { + return this._height; + } + + public set width(width: UIWebsiteCSSValue) { + this._width = width; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.website.id, + size: { + height: this._height, + width: this._width, + }, + }, + }); + } +} + +class UIWebsiteMarginInternal { + private readonly website: UIWebsite; + private _top?: UIWebsiteCSSValue; + private _bottom?: UIWebsiteCSSValue; + private _left?: UIWebsiteCSSValue; + private _right?: UIWebsiteCSSValue; + + constructor(uiWebsite: UIWebsite, margin: UIWebsiteMargin) { + this.website = uiWebsite; + this._top = margin.top; + this._bottom = margin.bottom; + this._left = margin.left; + this._right = margin.right; + } + + public get top() { + return this._top; + } + + public set top(top: UIWebsiteCSSValue | undefined) { + this._top = top; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.website.id, + margin: { + top: this._top, + }, + }, + }); + } + + public get bottom() { + return this._bottom; + } + + public set bottom(bottom: UIWebsiteCSSValue | undefined) { + this._bottom = bottom; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.website.id, + margin: { + bottom: this._bottom, + }, + }, + }); + } + + public get left() { + return this._left; + } + + public set left(left: UIWebsiteCSSValue | undefined) { + this._left = left; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.website.id, + margin: { + left: this._left, + }, + }, + }); + } + + public get right() { + return this._right; + } + + public set right(right: UIWebsiteCSSValue | undefined) { + this._right = right; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.website.id, + margin: { + right: this._right, + }, + }, + }); + } +} + +export class UIWebsite { + public readonly id: string; + private _url: string; + private _visible: boolean; + private readonly _allowPolicy: string; + private readonly _allowApi: boolean; + private _position: UIWebsitePositionInternal; + private _size: UIWebsiteSizeInternal; + private _margin: UIWebsiteMarginInternal; + + constructor(config: UIWebsiteCore) { + this.id = config.id; + this._url = config.url; + this._visible = config.visible ?? true; + this._allowPolicy = config.allowPolicy ?? ""; + this._allowApi = config.allowApi ?? false; + this._position = new UIWebsitePositionInternal(this, config.position); + this._size = new UIWebsiteSizeInternal(this, config.size); + this._margin = new UIWebsiteMarginInternal(this, config.margin ?? {}); + } + + public get url() { + return this._url; + } + + public set url(url: string) { + this._url = url; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.id, + url: this._url, + }, + }); + } + + public get visible() { + return this._visible; + } + + public set visible(visible: boolean) { + this._visible = visible; + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.id, + visible: this._visible, + }, + }); + } + + public get allowPolicy() { + return this._allowPolicy; + } + + public get allowApi() { + return this._allowApi; + } + + public get position() { + return this._position; + } + + public set position(position: UIWebsitePosition) { + this._position = new UIWebsitePositionInternal(this, position); + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.id, + position: { + vertical: this._position.vertical, + horizontal: this._position.horizontal, + }, + }, + }); + } + + public get size() { + return this._size; + } + + public set size(size: UIWebsiteSize) { + this._size = new UIWebsiteSizeInternal(this, size); + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.id, + size: { + height: this._size.height, + width: this._size.width, + }, + }, + }); + } + + public get margin() { + return this._margin; + } + + public set margin(margin: UIWebsiteMargin) { + this._margin = new UIWebsiteMarginInternal(this, margin); + sendToWorkadventure({ + type: "modifyUIWebsite", + data: { + id: this.id, + margin: { + top: this._margin.top, + bottom: this._margin.bottom, + left: this._margin.left, + right: this._margin.right, + }, + }, + }); + } + + close() { + return queryWorkadventure({ + type: "closeUIWebsite", + data: this.id, + }); + } +} + +export class UIWebsiteCommands extends IframeApiContribution { + callbacks = []; + + async open(createUIWebsite: CreateUIWebsiteEvent): Promise { + const result = await queryWorkadventure({ + type: "openUIWebsite", + data: createUIWebsite, + }); + + return new UIWebsite(result); + } + + async getAll(): Promise { + const result = await queryWorkadventure({ + type: "getUIWebsites", + data: undefined, + }); + + return result.map((current) => new UIWebsite(current)); + } +} + +export default new UIWebsiteCommands(); diff --git a/front/src/Api/iframe/controls.ts b/front/src/Api/iframe/controls.ts index 9fb53640..31d37d15 100644 --- a/front/src/Api/iframe/controls.ts +++ b/front/src/Api/iframe/controls.ts @@ -10,6 +10,14 @@ export class WorkadventureControlsCommands extends IframeApiContribution = new Map(); @@ -290,6 +292,10 @@ export class WorkAdventureUiCommands extends IframeApiContribution - {#if logo !== logoImg} + {#if logo !== logoImg && gameManager.currentStartedRoom.showPoweredBy !== false}
Powered by WorkAdventure
diff --git a/front/src/Components/MainLayout.svelte b/front/src/Components/MainLayout.svelte index 2aeed652..f4c02218 100644 --- a/front/src/Components/MainLayout.svelte +++ b/front/src/Components/MainLayout.svelte @@ -39,6 +39,8 @@ import ActionsMenu from "./ActionsMenu/ActionsMenu.svelte"; import Lazy from "./Lazy.svelte"; import { showDesktopCapturerSourcePicker } from "../Stores/ScreenSharingStore"; + import UiWebsiteContainer from "./UI/Website/UIWebsiteContainer.svelte"; + import { uiWebsitesStore } from "../Stores/UIWebsiteStore"; let mainLayout: HTMLDivElement; @@ -128,6 +130,10 @@ {#if hasEmbedScreen} {/if} + + {#if $uiWebsitesStore} + + {/if}
diff --git a/front/src/Components/UI/ErrorScreen.svelte b/front/src/Components/UI/ErrorScreen.svelte index 11ab8ca4..8056bf94 100644 --- a/front/src/Components/UI/ErrorScreen.svelte +++ b/front/src/Components/UI/ErrorScreen.svelte @@ -11,14 +11,17 @@ import reload from "../images/reload.png"; let errorScreen = get(errorScreenStore); + import error from "./images/error.png"; + let errorLogo = errorScreen?.image ?? error; + function click() { - if (errorScreen.type === "unauthorized") void connectionManager.logout(); + if (errorScreen?.type === "unauthorized") void connectionManager.logout(); else window.location.reload(); } - let details = errorScreen.details; - let timeVar = errorScreen.timeToRetry ?? 0; + let details = errorScreen?.details ?? ""; + let timeVar = errorScreen?.timeToRetry ?? 0; - if (errorScreen.type === "retry") { + if (errorScreen?.type === "retry") { let interval = setInterval(() => { if (timeVar <= 1000) click(); timeVar -= 1000; @@ -29,24 +32,29 @@ $: detailsStylized = (details ?? "").replace("{time}", `${timeVar / 1000}`); -
-
- -
- {#if $errorScreenStore.type !== "retry"}

{$errorScreenStore.title}

{/if} -

{$errorScreenStore.subtitle}

- {#if $errorScreenStore.type !== "retry"}

Code : {$errorScreenStore.code}

{/if} -

- {detailsStylized}{#if $errorScreenStore.type === "retry"}

{/if} -

- {#if ($errorScreenStore.type === "retry" && $errorScreenStore.canRetryManual) || $errorScreenStore.type === "unauthorized"} - - {/if} -
-
+{#if $errorScreenStore} +
+
+ +
Error logo
+ {#if $errorScreenStore.type !== "retry"}

{$errorScreenStore.title}

{/if} + {#if $errorScreenStore.subtitle}

{$errorScreenStore.subtitle}

{/if} + {#if $errorScreenStore.type !== "retry"}

Code : {$errorScreenStore.code}

{/if} +

+ {detailsStylized} + {#if $errorScreenStore.type === "retry" || $errorScreenStore.type === "reconnecting"} +

+ {/if} +

+ {#if ($errorScreenStore.type === "retry" && $errorScreenStore.canRetryManual) || $errorScreenStore.type === "unauthorized"} + + {/if} +
+
+{/if} diff --git a/front/src/Components/UI/Website/UIWebsiteLayer.svelte b/front/src/Components/UI/Website/UIWebsiteLayer.svelte new file mode 100644 index 00000000..229e653b --- /dev/null +++ b/front/src/Components/UI/Website/UIWebsiteLayer.svelte @@ -0,0 +1,66 @@ + + +
+ + diff --git a/front/src/Components/UI/images/error.png b/front/src/Components/UI/images/error.png new file mode 100644 index 00000000..ec2ea9ee Binary files /dev/null and b/front/src/Components/UI/images/error.png differ diff --git a/front/src/Components/Video/DesktopCapturerSourcePicker.svelte b/front/src/Components/Video/DesktopCapturerSourcePicker.svelte index 92eb41ae..b5ee960b 100644 --- a/front/src/Components/Video/DesktopCapturerSourcePicker.svelte +++ b/front/src/Components/Video/DesktopCapturerSourcePicker.svelte @@ -5,7 +5,7 @@ showDesktopCapturerSourcePicker, } from "../../Stores/ScreenSharingStore"; import { onDestroy, onMount } from "svelte"; - import type { DesktopCapturerSource } from "@wa-preload-app"; + import type { DesktopCapturerSource } from "../../Interfaces/DesktopAppInterfaces"; let desktopCapturerSources: DesktopCapturerSource[] = []; let interval: ReturnType; diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 9de89d93..4ab1b350 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -296,7 +296,32 @@ class ConnectionManager { }); connection.connectionErrorStream.subscribe((event: CloseEvent) => { - console.log("connectionErrorStream => An error occurred while connecting to socket server. Retrying"); + console.info( + "An error occurred while connecting to socket server. Retrying => Event: ", + event.reason, + event.code, + event + ); + + //However, Chrome will rarely report any close code 1006 reasons to the Javascript side. + //This is likely due to client security rules in the WebSocket spec to prevent abusing WebSocket. + //(such as using it to scan for open ports on a destination server, or for generating lots of connections for a denial-of-service attack). + // more detail here: https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1 + if (event.code === 1006) { + //check cookies + const cookies = document.cookie.split(";"); + for (const cookie of cookies) { + //check id cookie posthog exist + const numberIndexPh = cookie.indexOf("_posthog="); + if (numberIndexPh !== -1) { + //if exist, remove posthog cookie + document.cookie = + cookie.slice(0, numberIndexPh + 9) + + "; domain=.workadventu.re; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/"; + } + } + } + reject( new Error( "An error occurred while connecting to socket server. Retrying. Code: " + diff --git a/front/src/Connexion/Room.ts b/front/src/Connexion/Room.ts index 4e896572..ef036e09 100644 --- a/front/src/Connexion/Room.ts +++ b/front/src/Connexion/Room.ts @@ -25,6 +25,7 @@ export class Room { private _canReport: boolean = false; private _loadingLogo: string | undefined; private _loginSceneLogo: string | undefined; + private _showPoweredBy: boolean | undefined = true; private constructor(private roomUrl: URL) { this.id = roomUrl.pathname; @@ -120,6 +121,7 @@ export class Room { this._canReport = data.canReport ?? false; this._loadingLogo = data.loadingLogo ?? undefined; this._loginSceneLogo = data.loginSceneLogo ?? undefined; + this._showPoweredBy = data.showPoweredBy ?? true; return new MapDetail(data.mapUrl); } else { console.log(data); @@ -213,4 +215,8 @@ export class Room { get loginSceneLogo(): string | undefined { return this._loginSceneLogo; } + + get showPoweredBy(): boolean | undefined { + return this._showPoweredBy; + } } diff --git a/front/src/Interfaces/DesktopAppInterfaces.ts b/front/src/Interfaces/DesktopAppInterfaces.ts new file mode 100644 index 00000000..4069f7de --- /dev/null +++ b/front/src/Interfaces/DesktopAppInterfaces.ts @@ -0,0 +1,21 @@ +// copy of Electron.SourcesOptions to avoid Electron dependency in front +export interface SourcesOptions { + types: string[]; + thumbnailSize?: { height: number; width: number }; +} + +export interface DesktopCapturerSource { + id: string; + name: string; + thumbnailURL: string; +} + +export type WorkAdventureDesktopApi = { + desktop: boolean; + isDevelopment: () => Promise; + getVersion: () => Promise; + notify: (txt: string) => void; + onMuteToggle: (callback: () => void) => void; + onCameraToggle: (callback: () => void) => void; + getDesktopCapturerSources: (options: SourcesOptions) => Promise; +}; diff --git a/front/src/Phaser/Components/Loader.ts b/front/src/Phaser/Components/Loader.ts index 9e8751d7..e84349fb 100644 --- a/front/src/Phaser/Components/Loader.ts +++ b/front/src/Phaser/Components/Loader.ts @@ -54,7 +54,7 @@ export class Loader { .catch((e) => console.warn("Could not load logo: ", logoResource, e)); let poweredByLogoPromise: CancelablePromise | undefined; - if (gameManager.currentStartedRoom.loadingLogo) { + if (gameManager.currentStartedRoom.loadingLogo && gameManager.currentStartedRoom.showPoweredBy !== false) { poweredByLogoPromise = this.superLoad.image( "poweredByLogo", "static/images/Powered_By_WorkAdventure_Small.png" diff --git a/front/src/Phaser/Game/DepthIndexes.ts b/front/src/Phaser/Game/DepthIndexes.ts index d2d38328..04d38146 100644 --- a/front/src/Phaser/Game/DepthIndexes.ts +++ b/front/src/Phaser/Game/DepthIndexes.ts @@ -4,5 +4,6 @@ export const DEPTH_TILE_INDEX = 0; //Note: Player characters use their y coordinate as their depth to simulate a perspective. //See the Character class. export const DEPTH_OVERLAY_INDEX = 10000; +export const DEPTH_BUBBLE_CHAT_SPRITE = 99999; export const DEPTH_INGAME_TEXT_INDEX = 100000; export const DEPTH_UI_INDEX = 1000000; diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 6be4959c..7a349c33 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -76,6 +76,7 @@ import { contactPageStore } from "../../Stores/MenuStore"; import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent"; import { audioManagerFileStore } from "../../Stores/AudioManagerStore"; import { currentPlayerGroupLockStateStore } from "../../Stores/CurrentPlayerGroupStore"; +import { errorScreenStore } from "../../Stores/ErrorScreenStore"; import EVENT_TYPE = Phaser.Scenes.Events; import Texture = Phaser.Textures.Texture; @@ -90,8 +91,8 @@ import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import { MapStore } from "../../Stores/Utils/MapStore"; import { followUsersColorStore } from "../../Stores/FollowStore"; import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler"; -import { locale } from "../../i18n/i18n-svelte"; import { i18nJson } from "../../i18n/locales"; +import LL, { locale } from "../../i18n/i18n-svelte"; import { availabilityStatusStore, localVolumeStore } from "../../Stores/MediaStore"; import { StringUtils } from "../../Utils/StringUtils"; import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore"; @@ -101,7 +102,9 @@ import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite"; import CancelablePromise from "cancelable-promise"; import { Deferred } from "ts-deferred"; import { SuperLoaderPlugin } from "../Services/SuperLoaderPlugin"; -import { PlayerDetailsUpdatedMessage } from "../../Messages/ts-proto-generated/protos/messages"; +import { DEPTH_BUBBLE_CHAT_SPRITE } from "./DepthIndexes"; +import { ErrorScreenMessage, PlayerDetailsUpdatedMessage } from "../../Messages/ts-proto-generated/protos/messages"; +import { uiWebsiteManager } from "./UI/UIWebsiteManager"; export interface GameSceneInitInterface { initPosition: PointInterface | null; reconnecting: boolean; @@ -613,14 +616,30 @@ export class GameScene extends DirtyScene { if (this.isReconnecting) { setTimeout(() => { this.scene.sleep(); - this.scene.launch(ReconnectingSceneName); + errorScreenStore.setError( + ErrorScreenMessage.fromPartial({ + type: "reconnecting", + code: "CONNECTION_LOST", + title: get(LL).warning.connectionLostTitle(), + details: get(LL).warning.connectionLostSubtitle(), + }) + ); + //this.scene.launch(ReconnectingSceneName); }, 0); } else if (this.connection === undefined) { // Let's wait 1 second before printing the "connecting" screen to avoid blinking setTimeout(() => { if (this.connection === undefined) { this.scene.sleep(); - this.scene.launch(ReconnectingSceneName); + errorScreenStore.setError( + ErrorScreenMessage.fromPartial({ + type: "reconnecting", + code: "CONNECTION_LOST", + title: get(LL).warning.connectionLostTitle(), + details: get(LL).warning.connectionLostSubtitle(), + }) + ); + //this.scene.launch(ReconnectingSceneName); } }, 1000); } @@ -904,7 +923,9 @@ export class GameScene extends DirtyScene { // Analyze tags to find if we are admin. If yes, show console. if (this.scene.isSleeping()) { - this.scene.stop(ReconnectingSceneName); + const error = get(errorScreenStore); + if (error && error?.type === "reconnecting") errorScreenStore.delete(); + //this.scene.stop(ReconnectingSceneName); } //init user position and play trigger to check layers properties @@ -1111,6 +1132,24 @@ export class GameScene extends DirtyScene { }) ); + this.iframeSubscriptionList.push( + iframeListener.enablePlayerControlStream.subscribe(() => { + this.userInputManager.restoreControls(); + }) + ); + + this.iframeSubscriptionList.push( + iframeListener.disablePlayerProximityMeetingStream.subscribe(() => { + this.disableMediaBehaviors(); + }) + ); + + this.iframeSubscriptionList.push( + iframeListener.enablePlayerProximityMeetingStream.subscribe(() => { + this.enableMediaBehaviors(); + }) + ); + this.iframeSubscriptionList.push( iframeListener.cameraSetStream.subscribe((cameraSetEvent) => { const duration = cameraSetEvent.smooth ? 1000 : 0; @@ -1198,11 +1237,6 @@ export class GameScene extends DirtyScene { }) ); - this.iframeSubscriptionList.push( - iframeListener.enablePlayerControlStream.subscribe(() => { - this.userInputManager.restoreControls(); - }) - ); this.iframeSubscriptionList.push( iframeListener.loadPageStream.subscribe((url: string) => { this.loadNextGameFromExitUrl(url) @@ -1225,7 +1259,7 @@ export class GameScene extends DirtyScene { this.CurrentPlayer.y, "circleSprite-white" ); - scriptedBubbleSprite.setDisplayOrigin(48, 48); + scriptedBubbleSprite.setDisplayOrigin(48, 48).setDepth(DEPTH_BUBBLE_CHAT_SPRITE); this.add.existing(scriptedBubbleSprite); }) ); @@ -1313,6 +1347,17 @@ export class GameScene extends DirtyScene { data.propertyValue = this.gameMap.getLayerProperty(data.layerName, data.propertyName); return data; }); + iframeListener.registerAnswerer("openUIWebsite", (websiteConfig) => { + return uiWebsiteManager.open(websiteConfig); + }); + + iframeListener.registerAnswerer("getUIWebsites", () => { + return uiWebsiteManager.getAll(); + }); + + iframeListener.registerAnswerer("closeUIWebsite", (websiteId) => { + return uiWebsiteManager.close(websiteId); + }); iframeListener.registerAnswerer("getMapData", () => { return { @@ -1610,6 +1655,9 @@ export class GameScene extends DirtyScene { iframeListener.unregisterAnswerer("getCoWebsites"); iframeListener.unregisterAnswerer("setPlayerOutline"); iframeListener.unregisterAnswerer("setVariable"); + iframeListener.unregisterAnswerer("openUIWebsite"); + iframeListener.unregisterAnswerer("getUIWebsites"); + iframeListener.unregisterAnswerer("closeUIWebsite"); this.sharedVariablesManager?.close(); this.embeddedWebsiteManager?.close(); @@ -2079,7 +2127,7 @@ export class GameScene extends DirtyScene { ? "circleSprite-red" : "circleSprite-white" ); - sprite.setDisplayOrigin(48, 48); + sprite.setDisplayOrigin(48, 48).setDepth(DEPTH_BUBBLE_CHAT_SPRITE); this.add.existing(sprite); this.groups.set(groupPositionMessage.groupId, sprite); if (this.currentPlayerGroupId === groupPositionMessage.groupId) { diff --git a/front/src/Phaser/Game/UI/UIWebsiteManager.ts b/front/src/Phaser/Game/UI/UIWebsiteManager.ts new file mode 100644 index 00000000..ce3bb0d6 --- /dev/null +++ b/front/src/Phaser/Game/UI/UIWebsiteManager.ts @@ -0,0 +1,94 @@ +import { get } from "svelte/store"; +import { CreateUIWebsiteEvent, ModifyUIWebsiteEvent, UIWebsite } from "../../../Api/Events/ui/UIWebsite"; +import { iframeListener } from "../../../Api/IframeListener"; +import { v4 as uuidv4 } from "uuid"; +import { uiWebsitesStore } from "../../../Stores/UIWebsiteStore"; + +class UIWebsiteManager { + constructor() { + iframeListener.modifyUIWebsiteStream.subscribe((websiteEvent: ModifyUIWebsiteEvent) => { + const website = get(uiWebsitesStore).find((currentWebsite) => currentWebsite.id === websiteEvent.id); + if (!website) { + throw new Error(`Could not find ui website with the id "${websiteEvent.id}" in your map`); + } + + if (websiteEvent.url) { + website.url = websiteEvent.url; + } + + if (websiteEvent.visible !== undefined) { + website.visible = websiteEvent.visible; + } + + if (websiteEvent.position) { + if (websiteEvent.position.horizontal) { + website.position.horizontal = websiteEvent.position.horizontal; + } + + if (websiteEvent.position.vertical) { + website.position.vertical = websiteEvent.position.vertical; + } + } + + if (websiteEvent.size) { + if (websiteEvent.size.height) { + website.size.height = websiteEvent.size.height; + } + + if (websiteEvent.size.width) { + website.size.width = websiteEvent.size.width; + } + } + + if (websiteEvent.margin) { + website.margin = {}; + + if (websiteEvent.margin.top !== undefined) { + website.margin.top = websiteEvent.margin.top; + } + + if (websiteEvent.margin.bottom !== undefined) { + website.margin.bottom = websiteEvent.margin.bottom; + } + + if (websiteEvent.margin.left !== undefined) { + website.margin.left = websiteEvent.margin.left; + } + + if (websiteEvent.margin.right !== undefined) { + website.margin.right = websiteEvent.margin.right; + } + } + }); + } + + public open(websiteConfig: CreateUIWebsiteEvent): UIWebsite { + const newWebsite: UIWebsite = { + ...websiteConfig, + id: uuidv4(), + visible: websiteConfig.visible ?? true, + allowPolicy: websiteConfig.allowPolicy ?? "", + allowApi: websiteConfig.allowApi ?? false, + }; + + uiWebsitesStore.add(newWebsite); + + return newWebsite; + } + + public getAll(): UIWebsite[] { + return get(uiWebsitesStore); + } + + public close(websiteId: string) { + const uiWebsite = get(uiWebsitesStore).find((currentWebsite) => currentWebsite.id === websiteId); + + if (!uiWebsite) { + return; + } + + uiWebsitesStore.remove(uiWebsite); + } +} + +export const uiWebsiteManager = new UIWebsiteManager(); diff --git a/front/src/Phaser/Login/CustomizeScene.ts b/front/src/Phaser/Login/CustomizeScene.ts index f07014b9..77e119f7 100644 --- a/front/src/Phaser/Login/CustomizeScene.ts +++ b/front/src/Phaser/Login/CustomizeScene.ts @@ -94,6 +94,7 @@ export class CustomizeScene extends AbstractCharacterScene { } public create(): void { + this.selectedLayers = [0, 0, 0, 0, 0, 0]; this.tryLoadLastUsedWokaLayers(); waScaleManager.zoomModifier = 1; this.createSlotBackgroundTextures(); @@ -154,11 +155,10 @@ export class CustomizeScene extends AbstractCharacterScene { try { const savedWokaLayers = gameManager.getCharacterLayers(); if (savedWokaLayers && savedWokaLayers.length !== 0) { - this.selectedLayers = []; for (let i = 0; i < savedWokaLayers.length; i += 1) { - this.selectedLayers.push( - this.layers[i].findIndex((item) => item.id === gameManager.getCharacterLayers()[i]) - ); + const index = this.layers[i].findIndex((item) => item.id === gameManager.getCharacterLayers()[i]); + // set first item as default if not found + this.selectedLayers[i] = index !== -1 ? index : 0; } } } catch { diff --git a/front/src/Stores/ErrorScreenStore.ts b/front/src/Stores/ErrorScreenStore.ts index 564afb92..3e2827ba 100644 --- a/front/src/Stores/ErrorScreenStore.ts +++ b/front/src/Stores/ErrorScreenStore.ts @@ -5,13 +5,16 @@ import { ErrorScreenMessage } from "../Messages/ts-proto-generated/protos/messag * A store that contains one error of type WAError to be displayed. */ function createErrorScreenStore() { - const { subscribe, set } = writable(undefined); + const { subscribe, set } = writable(undefined); return { subscribe, setError: (e: ErrorScreenMessage): void => { set(e); }, + delete: () => { + set(undefined); + }, }; } diff --git a/front/src/Stores/ScreenSharingStore.ts b/front/src/Stores/ScreenSharingStore.ts index 843e6212..8bf54c10 100644 --- a/front/src/Stores/ScreenSharingStore.ts +++ b/front/src/Stores/ScreenSharingStore.ts @@ -2,7 +2,7 @@ import { derived, Readable, readable, writable } from "svelte/store"; import { peerStore } from "./PeerStore"; import type { LocalStreamStoreValue } from "./MediaStore"; import { myCameraVisibilityStore } from "./MyCameraStoreVisibility"; -import type { DesktopCapturerSource } from "@wa-preload-app"; +import type { DesktopCapturerSource } from "../Interfaces/DesktopAppInterfaces"; declare const navigator: any; // eslint-disable-line @typescript-eslint/no-explicit-any diff --git a/front/src/Stores/UIWebsiteStore.ts b/front/src/Stores/UIWebsiteStore.ts new file mode 100644 index 00000000..37c17156 --- /dev/null +++ b/front/src/Stores/UIWebsiteStore.ts @@ -0,0 +1,20 @@ +import { writable } from "svelte/store"; +import { UIWebsite } from "../Api/Events/ui/UIWebsite"; + +function createUIWebsiteStore() { + const { subscribe, update, set } = writable(Array()); + + set(Array()); + + return { + subscribe, + add: (uiWebsite: UIWebsite) => { + update((currentArray) => [...currentArray, uiWebsite]); + }, + remove: (uiWebsite: UIWebsite) => { + update((currentArray) => currentArray.filter((currentWebsite) => currentWebsite.id !== uiWebsite.id)); + }, + }; +} + +export const uiWebsitesStore = createUIWebsiteStore(); diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts index b378b218..e2b197de 100644 --- a/front/src/WebRtc/CoWebsiteManager.ts +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -238,6 +238,7 @@ class CoWebsiteManager { const iframe = coWebsite.getIframe(); if (iframe) { + this.activateMainLoaderAnimation(); iframe.style.display = "none"; } this.resizing = true; @@ -258,6 +259,7 @@ class CoWebsiteManager { const iframe = coWebsite.getIframe(); if (iframe) { iframe.style.display = "flex"; + this.desactivateMainLoaderAnimation(); } this.resizing = false; }); @@ -273,6 +275,7 @@ class CoWebsiteManager { const iframe = coWebsite.getIframe(); if (iframe) { + this.activateMainLoaderAnimation(); iframe.style.display = "none"; } this.resizing = true; @@ -296,6 +299,7 @@ class CoWebsiteManager { const iframe = coWebsite.getIframe(); if (iframe) { iframe.style.display = "flex"; + this.desactivateMainLoaderAnimation(); } this.resizing = false; }); @@ -309,9 +313,7 @@ class CoWebsiteManager { if (this.cowebsiteDom.classList.contains("closing")) { this.cowebsiteDom.classList.remove("closing"); - if (this.loaderAnimationInterval.interval) { - clearInterval(this.loaderAnimationInterval.interval); - } + this.desactivateMainLoaderAnimation(); this.loaderAnimationInterval.trails = undefined; } }); @@ -353,7 +355,10 @@ class CoWebsiteManager { this.fire(); } - private loadMain(openingWidth?: number): void { + private activateMainLoaderAnimation() { + this.desactivateMainLoaderAnimation(); + + this.cowebsiteLoaderDom.style.display = "block"; this.loaderAnimationInterval.interval = setInterval(() => { if (!this.loaderAnimationInterval.trails) { this.loaderAnimationInterval.trails = [0, 1, 2]; @@ -361,7 +366,6 @@ class CoWebsiteManager { 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; @@ -382,6 +386,17 @@ class CoWebsiteManager { trail === 3 ? 0 : trail + 1 ); }, 200); + } + + private desactivateMainLoaderAnimation() { + if (this.loaderAnimationInterval.interval) { + this.cowebsiteLoaderDom.style.display = "none"; + clearInterval(this.loaderAnimationInterval.interval); + } + } + + private loadMain(openingWidth?: number): void { + this.activateMainLoaderAnimation(); if (!this.verticalMode && openingWidth) { let newWidth = 50; @@ -623,6 +638,8 @@ class CoWebsiteManager { setTimeout(() => { this.fire(); }, animationTime); + + this.desactivateMainLoaderAnimation(); } else if ( !highlightedEmbed && this.getCoWebsites().find((searchCoWebsite) => searchCoWebsite.getId() === coWebsite.getId()) diff --git a/front/src/i18n/de-DE/warning.ts b/front/src/i18n/de-DE/warning.ts index 4d1f4738..49739854 100644 --- a/front/src/i18n/de-DE/warning.ts +++ b/front/src/i18n/de-DE/warning.ts @@ -14,6 +14,8 @@ const warning: NonNullable = { }, importantMessage: "Wichtige Nachricht", connectionLost: "Verbindungen unterbrochen. Wiederverbinden...", + connectionLostTitle: "Verbindungen unterbrochen", + connectionLostSubtitle: "Wiederverbinden", }; export default warning; diff --git a/front/src/i18n/en-US/warning.ts b/front/src/i18n/en-US/warning.ts index 7bf83c87..ee235c4f 100644 --- a/front/src/i18n/en-US/warning.ts +++ b/front/src/i18n/en-US/warning.ts @@ -13,6 +13,8 @@ const warning: BaseTranslation = { }, importantMessage: "Important message", connectionLost: "Connection lost. Reconnecting...", + connectionLostTitle: "Connection lost", + connectionLostSubtitle: "Reconnecting", }; export default warning; diff --git a/front/style/cowebsite/_global.scss b/front/style/cowebsite/_global.scss index 999ed54a..619c61b1 100644 --- a/front/style/cowebsite/_global.scss +++ b/front/style/cowebsite/_global.scss @@ -71,6 +71,7 @@ &-loader { width: 20%; + display: none; #smoke { @for $i from 1 through 3 { diff --git a/front/tsconfig.json b/front/tsconfig.json index e5ac9819..66127d6f 100644 --- a/front/tsconfig.json +++ b/front/tsconfig.json @@ -1,6 +1,5 @@ { -// "include": ["src/**/*"], - + // "include": ["src/**/*"], "extends": "@tsconfig/svelte/tsconfig.json", "compilerOptions": { "outDir": "./dist/", @@ -14,23 +13,17 @@ "jsx": "react", "allowJs": true, "esModuleInterop": true, - "importsNotUsedAsValues": "remove", - - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - "strictNullChecks": true, /* Enable strict null checks. */ - "strictFunctionTypes": true, /* Enable strict checking of function types. */ - "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ - "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - - "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - "paths": { - "@wa-preload-app": ["../desktop/electron/src/preload-app/types.ts"], - } + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* Enable strict null checks. */ + "strictFunctionTypes": true, /* Enable strict checking of function types. */ + "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ }, "exclude": [ "node_modules", @@ -38,4 +31,4 @@ "public/iframe_api.js", "packages/iframe-api-typings" ] -} +} \ No newline at end of file diff --git a/front/yarn.lock b/front/yarn.lock index a5b24ce7..30340e8f 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -2244,10 +2244,10 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier-plugin-svelte@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-2.5.0.tgz#7922534729f7febe59b4c56c3f5360539f0d8ab1" - integrity sha512-+iHY2uGChOngrgKielJUnqo74gIL/EO5oeWm8MftFWjEi213lq9QYTOwm1pv4lI1nA61tdgf80CF2i5zMcu1kw== +prettier-plugin-svelte@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-2.7.0.tgz#ecfa4fe824238a4466a3497df1a96d15cf43cabb" + integrity sha512-fQhhZICprZot2IqEyoiUYLTRdumULGRvw0o4dzl5jt0jfzVWdGqeYW27QTWAeXhoupEZJULmNoH3ueJwUWFLIA== prettier@^2.0.2: version "2.5.1" diff --git a/maps/tests/UIWebsite/index.html b/maps/tests/UIWebsite/index.html new file mode 100644 index 00000000..6ab2a8e8 --- /dev/null +++ b/maps/tests/UIWebsite/index.html @@ -0,0 +1,5 @@ + + + This is test page + + \ No newline at end of file diff --git a/maps/tests/UIWebsite/script.js b/maps/tests/UIWebsite/script.js new file mode 100644 index 00000000..b58780ba --- /dev/null +++ b/maps/tests/UIWebsite/script.js @@ -0,0 +1,48 @@ +WA.onInit().then(() => { + initListeners(); +}); + +function initListeners() { + let first_website = undefined; + let second_website = undefined; + + WA.room.onEnterLayer('first_website').subscribe(async () => { + first_website = await WA.ui.website.open({ + url: "http://maps.workadventure.localhost/tests/UIWebsite/index.html", + position: { + vertical: "middle", + horizontal: "middle", + }, + size: { + height: "50vh", + width: "50vw", + }, + }); + }); + + WA.room.onLeaveLayer('first_website').subscribe(() => { + if (first_website) { + first_website.close(); + } + }); + + WA.room.onEnterLayer('second_website').subscribe(async () => { + second_website = await WA.ui.website.open({ + url: "https://www.wikipedia.org/", + position: { + vertical: "top", + horizontal: "right", + }, + size: { + height: "20vh", + width: "50vw", + }, + }); + }); + + WA.room.onLeaveLayer('second_website').subscribe(() => { + if (second_website) { + second_website.close(); + } + }); +} diff --git a/maps/tests/UIWebsite/uiwebsite.json b/maps/tests/UIWebsite/uiwebsite.json new file mode 100644 index 00000000..74c1dcc3 --- /dev/null +++ b/maps/tests/UIWebsite/uiwebsite.json @@ -0,0 +1,679 @@ +{ "compressionlevel":-1, + "height":10, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":10, + "id":1, + "name":"floor", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":2, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23, + 0, 0, 0, 0, 0, 23, 23, 23, 23, 23], + "height":10, + "id":5, + "name":"first_website", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 12, 12, 12, 12, 12, + 0, 0, 0, 0, 0, 12, 12, 12, 12, 12, + 0, 0, 0, 0, 0, 12, 12, 12, 12, 12, + 0, 0, 0, 0, 0, 12, 12, 12, 12, 12, + 0, 0, 0, 0, 0, 12, 12, 12, 12, 12], + "height":10, + "id":7, + "name":"second_website", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"floorLayer", + "objects":[ + { + "height":116.924156284309, + "id":1, + "name":"Tests", + "rotation":0, + "text": + { + "fontfamily":"Sans Serif", + "pixelsize":8, + "text":"Test 1:\nMove on the white carpet to display a UIWebsite.\n\nTest 2:\nMove on the blue carpet to display an other UIWebsite above the first.", + "wrap":true + }, + "type":"", + "visible":true, + "width":158.381128664136, + "x":1.64026713939023, + "y":201.037039933902 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 82, 0, + 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 19, 27, 0, + 0, 0, 0, 0, 0, 0, 0, 30, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":8, + "name":"objects", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }], + "nextlayerid":9, + "nextobjectid":3, + "orientation":"orthogonal", + "properties":[ + { + "name":"script", + "type":"string", + "value":"script.js" + }], + "renderorder":"right-down", + "tiledversion":"1.8.4", + "tileheight":32, + "tilesets":[ + { + "columns":11, + "firstgid":1, + "image":"..\/tileset1.png", + "imageheight":352, + "imagewidth":352, + "margin":0, + "name":"tileset1", + "spacing":0, + "tilecount":121, + "tileheight":32, + "tiles":[ + { + "id":1, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":2, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":3, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":4, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":5, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":6, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":7, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":8, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":9, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":10, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + + { + "id":12, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":16, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":17, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":18, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":19, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":20, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":21, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":23, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":24, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":25, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + + { + "id":26, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":27, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":28, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":29, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":30, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":31, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":32, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":34, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":35, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":42, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + + { + "id":43, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":45, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":46, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":59, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":60, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":70, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":71, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":80, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":81, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":89, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + + { + "id":91, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":93, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":94, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":95, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":96, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":97, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":100, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":102, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":103, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":104, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + + { + "id":105, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":106, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":107, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":108, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":114, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }, + { + "id":115, + "properties":[ + { + "name":"collides", + "type":"bool", + "value":true + }] + }], + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":"1.8", + "width":10 +} \ No newline at end of file diff --git a/maps/tests/index.html b/maps/tests/index.html index 610337ee..68c03f79 100644 --- a/maps/tests/index.html +++ b/maps/tests/index.html @@ -363,6 +363,14 @@ Testing scripts with modules mode disabled + + + Success Failure Pending + + + Testing UIWebsites + +

CoWebsite

diff --git a/messages/JsonMessages/MapDetailsData.ts b/messages/JsonMessages/MapDetailsData.ts index 5df1bc7f..c191ae72 100644 --- a/messages/JsonMessages/MapDetailsData.ts +++ b/messages/JsonMessages/MapDetailsData.ts @@ -49,6 +49,10 @@ export const isMapDetailsData = z.object({ description: "The URL of the image to be used on the LoginScene", example: "https://example.com/logo_login.png", }), + showPoweredBy: extendApi(z.boolean(), { + description: "The URL of the image to be used on the name scene", + example: "https://example.com/logo_login.png", + }), }); export type MapDetailsData = z.infer; diff --git a/pusher/src/Services/LocalAdmin.ts b/pusher/src/Services/LocalAdmin.ts index 469966b3..d16f4c30 100644 --- a/pusher/src/Services/LocalAdmin.ts +++ b/pusher/src/Services/LocalAdmin.ts @@ -49,6 +49,7 @@ class LocalAdmin implements AdminInterface { iframeAuthentication: null, loadingLogo: null, loginSceneLogo: null, + showPoweredBy: true, }); }