Adding an API for inter-iframe communication

Adds a first version of an API to communicate between an iFrame opened by WorkAdventure and WorkAdventure itself.
The first API method is a method allowing to add messages in the chat, from the iFrame.

Comes with a test file.
This commit is contained in:
David Négrier 2021-03-04 19:00:00 +01:00
parent bf8e8bf777
commit eb93a04341
8 changed files with 184 additions and 4 deletions

View File

@ -1 +1,2 @@
index.html index.html
/js/

View File

@ -0,0 +1,34 @@
import {Subject} from "rxjs";
interface ChatEvent {
message: string,
author: string
}
/**
* Listens to messages from iframes and turn those messages into easy to use observables.
*/
class IframeListener {
private readonly _chatStream: Subject<ChatEvent> = new Subject();
public readonly chatStream = this._chatStream.asObservable();
init() {
window.addEventListener("message", (event) => {
// Do we trust the sender of this message?
//if (event.origin !== "http://example.com:8080")
// return;
// event.source is window.opener
// event.data is the data sent by the iframe
// FIXME: this is WAAAAAAAY too sloppy as "any" let's us put anything in the message.
if (event.data.type === 'chat') {
this._chatStream.next(event.data.data);
}
}, false);
}
}
export const iframeListener = new IframeListener();

View File

@ -3,6 +3,7 @@ import {mediaManager, ReportCallback, ShowReportCallBack} from "./MediaManager";
import {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
import {connectionManager} from "../Connexion/ConnectionManager"; import {connectionManager} from "../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../Url/UrlManager"; import {GameConnexionTypes} from "../Url/UrlManager";
import {iframeListener} from "../Api/IframeListener";
export type SendMessageCallback = (message:string) => void; export type SendMessageCallback = (message:string) => void;
@ -25,6 +26,11 @@ export class DiscussionManager {
constructor() { constructor() {
this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container'); this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
this.createDiscussPart(''); //todo: why do we always use empty string? this.createDiscussPart(''); //todo: why do we always use empty string?
iframeListener.chatStream.subscribe((chatEvent) => {
this.addMessage(chatEvent.author, chatEvent.message, false);
this.showDiscussion();
});
} }
private createDiscussPart(name: string) { private createDiscussPart(name: string) {

21
front/src/iframe_api.ts Normal file
View File

@ -0,0 +1,21 @@
interface WorkAdventureApi {
sendChatMessage(message: string, author: string): void;
}
declare var WA: WorkAdventureApi;
window.WA = {
/**
* Sends a message in the chat.
* Only the local user will receive this message.
*/
sendChatMessage(message: string, author: string) {
window.parent.postMessage({
'type': 'chat',
'data': {
'message': message,
'author': author
}
}, '*');
}
}

View File

@ -14,6 +14,8 @@ import {coWebsiteManager} from "./WebRtc/CoWebsiteManager";
import {MenuScene} from "./Phaser/Menu/MenuScene"; import {MenuScene} from "./Phaser/Menu/MenuScene";
import {localUserStore} from "./Connexion/LocalUserStore"; import {localUserStore} from "./Connexion/LocalUserStore";
import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene"; import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene";
import {iframeListener} from "./Api/IframeListener";
import {discussionManager} from "./WebRtc/DiscussionManager";
// Load Jitsi if the environment variable is set. // Load Jitsi if the environment variable is set.
if (JITSI_URL) { if (JITSI_URL) {
@ -124,3 +126,5 @@ coWebsiteManager.onStateChange(() => {
const {width, height} = coWebsiteManager.getGameSize(); const {width, height} = coWebsiteManager.getGameSize();
game.scale.resize(width / RESOLUTION, height / RESOLUTION); game.scale.resize(width / RESOLUTION, height / RESOLUTION);
}); });
iframeListener.init();

View File

@ -3,7 +3,10 @@ const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = { module.exports = {
entry: './src/index.ts', entry: {
'main': './src/index.ts',
'iframe_api': './src/iframe_api.ts'
},
devtool: 'inline-source-map', devtool: 'inline-source-map',
devServer: { devServer: {
contentBase: './dist', contentBase: './dist',
@ -29,7 +32,11 @@ module.exports = {
extensions: [ '.tsx', '.ts', '.js' ], extensions: [ '.tsx', '.ts', '.js' ],
}, },
output: { output: {
filename: '[name].[contenthash].js', filename: (pathData) => {
// Add a content hash only for the main bundle.
// We want the iframe_api.js file to keep its name as it will be referenced from outside iframes.
return pathData.chunk.name === 'main' ? 'js/[name].[contenthash].js': '[name].js';
},
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
publicPath: '/' publicPath: '/'
}, },
@ -48,7 +55,8 @@ module.exports = {
removeScriptTypeAttributes: true, removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true, removeStyleLinkTypeAttributes: true,
useShortDoctype: true useShortDoctype: true
} },
chunks: ['main']
} }
), ),
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({

17
maps/tests/iframe.html Normal file
View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<script src="http://play.workadventure.localhost/iframe_api.js"></script>
<script>
</script>
</head>
<body>
<button id="sendchat">Send chat message</button>
<script>
document.getElementById('sendchat').onclick = () => {
WA.sendChatMessage('Hello world!', 'Mr Robot');
}
</script>
</body>
</html>

View File

@ -0,0 +1,89 @@
{ "compressionlevel":-1,
"editorsettings":
{
"export":
{
"target":"."
}
},
"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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 0, 0, 0, 0, 0, 34, 34, 34, 34, 34, 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":5,
"name":"iframe_api",
"opacity":1,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"iframe.html"
}],
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":6,
"nextobjectid":1,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.3.3",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.2,
"width":10
}