diff --git a/docs/maps/api-ui.md b/docs/maps/api-ui.md
index 286f2ac7..89d46932 100644
--- a/docs/maps/api-ui.md
+++ b/docs/maps/api-ui.md
@@ -86,4 +86,51 @@ WA.ui.registerMenuCommand("test", () => {

-
\ No newline at end of file
+
+
+
+
+### Awaiting User Confirmation (with space bar)
+
+```
+WA.ui.displayActionMessage({
+ message: string,
+ callback: () => void,
+ type?: "message"|"warning",
+}): ActionMessage
+```
+
+Displays a message at the bottom of the screen (that will disappear when space bar is pressed).
+
+
+

+
+
+Example:
+
+```javascript
+const triggerMessage = WA.ui.displayActionMessage({
+ message: "press 'space' to confirm",
+ callback: () => {
+ WA.chat.sendChatMessage("confirmed", "trigger message logic")
+ }
+});
+
+setTimeout(() => {
+ // later
+ triggerMessage.remove();
+}, 1000)
+```
+
+Please note that `displayActionMessage` returns an object of the `ActionMessage` class.
+
+The `ActionMessage` class contains a single method: `remove(): Promise`. This will obviously remove the message when called.
+
+```javascript
+class ActionMessage {
+ /**
+ * Hides the message
+ */
+ remove() {};
+}
+```
diff --git a/front/.eslintrc.json b/front/.eslintrc.json
index 037fddae..45b44456 100644
--- a/front/.eslintrc.json
+++ b/front/.eslintrc.json
@@ -26,7 +26,6 @@
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "error",
-
// TODO: remove those ignored rules and write a stronger code!
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-unsafe-call": "off",
diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts
index 0590939b..ca1d9cc3 100644
--- a/front/src/Api/Events/IframeEvent.ts
+++ b/front/src/Api/Events/IframeEvent.ts
@@ -9,6 +9,7 @@ import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent";
import type { OpenPopupEvent } from "./OpenPopupEvent";
import type { OpenTabEvent } from "./OpenTabEvent";
import type { UserInputChatEvent } from "./UserInputChatEvent";
+import type { MapDataEvent } from "./MapDataEvent";
import type { LayerEvent } from "./LayerEvent";
import type { SetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent";
@@ -23,6 +24,13 @@ import { isMapDataEvent } from "./MapDataEvent";
import { isSetVariableEvent } from "./SetVariableEvent";
import type { LoadTilesetEvent } from "./LoadTilesetEvent";
import { isLoadTilesetEvent } from "./LoadTilesetEvent";
+import type {
+ MessageReferenceEvent,
+ removeActionMessage,
+ triggerActionMessage,
+ TriggerActionMessageEvent,
+} from "./ui/TriggerActionMessageEvent";
+import { isMessageReferenceEvent, isTriggerActionMessageEvent } from "./ui/TriggerActionMessageEvent";
export interface TypedMessageEvent extends MessageEvent {
data: T;
@@ -73,6 +81,7 @@ export interface IframeResponseEventMap {
hasPlayerMoved: HasPlayerMovedEvent;
menuItemClicked: MenuItemClickedEvent;
setVariable: SetVariableEvent;
+ messageTriggered: MessageReferenceEvent;
}
export interface IframeResponseEvent {
type: T;
@@ -105,6 +114,14 @@ export const iframeQueryMapTypeGuards = {
query: isLoadTilesetEvent,
answer: tg.isNumber,
},
+ triggerActionMessage: {
+ query: isTriggerActionMessageEvent,
+ answer: tg.isUndefined,
+ },
+ removeActionMessage: {
+ query: isMessageReferenceEvent,
+ answer: tg.isUndefined,
+ },
};
type GuardedType = T extends (x: unknown) => x is infer T ? T : never;
diff --git a/front/src/Api/Events/ui/TriggerActionMessageEvent.ts b/front/src/Api/Events/ui/TriggerActionMessageEvent.ts
new file mode 100644
index 00000000..48f1cae6
--- /dev/null
+++ b/front/src/Api/Events/ui/TriggerActionMessageEvent.ts
@@ -0,0 +1,26 @@
+import * as tg from "generic-type-guard";
+
+export const triggerActionMessage = "triggerActionMessage";
+export const removeActionMessage = "removeActionMessage";
+
+export const isActionMessageType = tg.isSingletonStringUnion("message", "warning");
+
+export type ActionMessageType = tg.GuardedType;
+
+export const isTriggerActionMessageEvent = new tg.IsInterface()
+ .withProperties({
+ message: tg.isString,
+ uuid: tg.isString,
+ type: isActionMessageType,
+ })
+ .get();
+
+export type TriggerActionMessageEvent = tg.GuardedType;
+
+export const isMessageReferenceEvent = new tg.IsInterface()
+ .withProperties({
+ uuid: tg.isString,
+ })
+ .get();
+
+export type MessageReferenceEvent = tg.GuardedType;
diff --git a/front/src/Api/Events/ui/TriggerMessageEventHandler.ts b/front/src/Api/Events/ui/TriggerMessageEventHandler.ts
new file mode 100644
index 00000000..fb64c742
--- /dev/null
+++ b/front/src/Api/Events/ui/TriggerMessageEventHandler.ts
@@ -0,0 +1,24 @@
+import {
+ isMessageReferenceEvent,
+ isTriggerActionMessageEvent,
+ removeActionMessage,
+ triggerActionMessage,
+} from './TriggerActionMessageEvent';
+
+import * as tg from 'generic-type-guard';
+
+const isTriggerMessageEventObject = new tg.IsInterface()
+ .withProperties({
+ type: tg.isSingletonString(triggerActionMessage),
+ data: isTriggerActionMessageEvent,
+ })
+ .get();
+
+const isTriggerMessageRemoveEventObject = new tg.IsInterface()
+ .withProperties({
+ type: tg.isSingletonString(removeActionMessage),
+ data: isMessageReferenceEvent,
+ })
+ .get();
+
+export const isTriggerMessageHandlerEvent = tg.isUnion(isTriggerMessageEventObject, isTriggerMessageRemoveEventObject);
diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts
index d9286ef0..2ed65f15 100644
--- a/front/src/Api/IframeListener.ts
+++ b/front/src/Api/IframeListener.ts
@@ -1,4 +1,5 @@
import { Subject } from "rxjs";
+import type * as tg from "generic-type-guard";
import { ChatEvent, isChatEvent } from "./Events/ChatEvent";
import { HtmlUtils } from "../WebRtc/HtmlUtils";
import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent";
@@ -121,7 +122,7 @@ class IframeListener {
init() {
window.addEventListener(
"message",
- (message: TypedMessageEvent>) => {
+ (message: MessageEvent) => {
// Do we trust the sender of this message?
// Let's only accept messages from the iframe that are allowed.
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
@@ -416,6 +417,15 @@ class IframeListener {
});
}
+ sendActionMessageTriggered(uuid: string): void {
+ this.postMessage({
+ type: "messageTriggered",
+ data: {
+ uuid,
+ },
+ });
+ }
+
/**
* Sends the message... to all allowed iframes.
*/
diff --git a/front/src/Api/iframe/IframeApiContribution.ts b/front/src/Api/iframe/IframeApiContribution.ts
index e4ba089e..96548d5e 100644
--- a/front/src/Api/iframe/IframeApiContribution.ts
+++ b/front/src/Api/iframe/IframeApiContribution.ts
@@ -1,51 +1,66 @@
import type * as tg from "generic-type-guard";
import type {
IframeEvent,
- IframeEventMap, IframeQuery,
+ IframeEventMap,
+ IframeQuery,
IframeQueryMap,
- IframeResponseEventMap
-} from '../Events/IframeEvent';
-import type {IframeQueryWrapper} from "../Events/IframeEvent";
+ IframeResponseEventMap,
+} from "../Events/IframeEvent";
+import type { IframeQueryWrapper } from "../Events/IframeEvent";
export function sendToWorkadventure(content: IframeEvent) {
- window.parent.postMessage(content, "*")
+ window.parent.postMessage(content, "*");
}
let queryNumber = 0;
-export const answerPromises = new Map)) => void,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- reject: (reason?: any) => void
-}>();
+export const answerPromises = new Map<
+ number,
+ {
+ resolve: (
+ value:
+ | IframeQueryMap[keyof IframeQueryMap]["answer"]
+ | PromiseLike
+ ) => void;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ reject: (reason?: any) => void;
+ }
+>();
-export function queryWorkadventure(content: IframeQuery): Promise {
- return new Promise((resolve, reject) => {
- window.parent.postMessage({
- id: queryNumber,
- query: content
- } as IframeQueryWrapper, "*");
+export function queryWorkadventure(
+ content: IframeQuery
+): Promise {
+ return new Promise((resolve, reject) => {
+ window.parent.postMessage(
+ {
+ id: queryNumber,
+ query: content,
+ } as IframeQueryWrapper,
+ "*"
+ );
answerPromises.set(queryNumber, {
resolve,
- reject
+ reject,
});
queryNumber++;
});
}
-type GuardedType> = Guard extends tg.TypeGuard ? T : never
+type GuardedType> = Guard extends tg.TypeGuard ? T : never;
-export interface IframeCallback> {
-
- typeChecker: Guard,
- callback: (payloadData: T) => void
+export interface IframeCallback<
+ Key extends keyof IframeResponseEventMap,
+ T = IframeResponseEventMap[Key],
+ Guard = tg.TypeGuard
+> {
+ typeChecker: Guard;
+ callback: (payloadData: T) => void;
}
export interface IframeCallbackContribution extends IframeCallback {
-
- type: Key
+ type: Key;
}
/**
@@ -54,9 +69,10 @@ export interface IframeCallbackContribution>,
-}> {
-
- abstract callbacks: T["callbacks"]
+export abstract class IframeApiContribution<
+ T extends {
+ callbacks: Array>;
+ }
+> {
+ abstract callbacks: T["callbacks"];
}
diff --git a/front/src/Api/iframe/Ui/ActionMessage.ts b/front/src/Api/iframe/Ui/ActionMessage.ts
new file mode 100644
index 00000000..912603b9
--- /dev/null
+++ b/front/src/Api/iframe/Ui/ActionMessage.ts
@@ -0,0 +1,56 @@
+import {
+ ActionMessageType,
+ MessageReferenceEvent,
+ removeActionMessage,
+ triggerActionMessage,
+ TriggerActionMessageEvent,
+} from "../../Events/ui/TriggerActionMessageEvent";
+import { queryWorkadventure } from "../IframeApiContribution";
+import type { ActionMessageOptions } from "../ui";
+function uuidv4() {
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
+ const r = (Math.random() * 16) | 0,
+ v = c === "x" ? r : (r & 0x3) | 0x8;
+ return v.toString(16);
+ });
+}
+
+export class ActionMessage {
+ public readonly uuid: string;
+ private readonly type: ActionMessageType;
+ private readonly message: string;
+ private readonly callback: () => void;
+
+ constructor(actionMessageOptions: ActionMessageOptions, private onRemove: () => void) {
+ this.uuid = uuidv4();
+ this.message = actionMessageOptions.message;
+ this.type = actionMessageOptions.type ?? "message";
+ this.callback = actionMessageOptions.callback;
+ this.create();
+ }
+
+ private async create() {
+ await queryWorkadventure({
+ type: triggerActionMessage,
+ data: {
+ message: this.message,
+ type: this.type,
+ uuid: this.uuid,
+ } as TriggerActionMessageEvent,
+ });
+ }
+
+ async remove() {
+ await queryWorkadventure({
+ type: removeActionMessage,
+ data: {
+ uuid: this.uuid,
+ } as MessageReferenceEvent,
+ });
+ this.onRemove();
+ }
+
+ triggerCallback() {
+ this.callback();
+ }
+}
diff --git a/front/src/Api/iframe/ui.ts b/front/src/Api/iframe/ui.ts
index 61c7076e..ab5b2007 100644
--- a/front/src/Api/iframe/ui.ts
+++ b/front/src/Api/iframe/ui.ts
@@ -1,10 +1,11 @@
import { isButtonClickedEvent } from "../Events/ButtonClickedEvent";
import { isMenuItemClickedEvent } from "../Events/ui/MenuItemClickedEvent";
-import type { MenuItemRegisterEvent } from "../Events/ui/MenuItemRegisterEvent";
import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks";
import type { ButtonClickedCallback, ButtonDescriptor } from "./Ui/ButtonDescriptor";
import { Popup } from "./Ui/Popup";
+import { ActionMessage } from "./Ui/ActionMessage";
+import { isMessageReferenceEvent } from "../Events/ui/TriggerActionMessageEvent";
let popupId = 0;
const popups: Map = new Map();
@@ -14,6 +15,7 @@ const popupCallbacks: Map> = new Map<
>();
const menuCallbacks: Map void> = new Map();
+const actionMessages = new Map();
interface ZonedPopupOptions {
zone: string;
@@ -23,6 +25,12 @@ interface ZonedPopupOptions {
popupOptions: Array;
}
+export interface ActionMessageOptions {
+ message: string;
+ type?: "message" | "warning";
+ callback: () => void;
+}
+
export class WorkAdventureUiCommands extends IframeApiContribution {
callbacks = [
apiCallback({
@@ -49,6 +57,16 @@ export class WorkAdventureUiCommands extends IframeApiContribution {
+ const actionMessage = actionMessages.get(event.uuid);
+ if (actionMessage) {
+ actionMessage.triggerCallback();
+ }
+ },
+ }),
];
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
@@ -103,6 +121,14 @@ export class WorkAdventureUiCommands extends IframeApiContribution {
+ actionMessages.delete(actionMessage.uuid);
+ });
+ actionMessages.set(actionMessage.uuid, actionMessage);
+ return actionMessage;
+ }
}
export default new WorkAdventureUiCommands();
diff --git a/front/src/Components/LayoutManager/LayoutManager.svelte b/front/src/Components/LayoutManager/LayoutManager.svelte
index ef90a4e3..5bc6e097 100644
--- a/front/src/Components/LayoutManager/LayoutManager.svelte
+++ b/front/src/Components/LayoutManager/LayoutManager.svelte
@@ -1,26 +1,5 @@