Wrapper for inline widget HTML

This commit is contained in:
Travis Ralston 2019-06-14 19:49:06 -06:00
parent 1b8fe9e782
commit 7ff2b598af
6 changed files with 195 additions and 1 deletions

View File

@ -78,6 +78,8 @@
"modernizr": "^3.6.0", "modernizr": "^3.6.0",
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.0.tgz", "olm": "https://packages.matrix.org/npm/olm/olm-3.1.0.tgz",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"querystring": "^0.2.0",
"random-string": "^0.2.0",
"react": "^15.6.0", "react": "^15.6.0",
"react-dom": "^15.6.0", "react-dom": "^15.6.0",
"sanitize-html": "^1.19.1", "sanitize-html": "^1.19.1",

View File

@ -0,0 +1,105 @@
/*
Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import randomString from "random-string";
// Dev note: This is largely inspired by Dimension. Used with permission.
// https://github.com/turt2live/matrix-dimension/blob/4f92d560266635e5a3c824606215b84e8c0b19f5/web/app/shared/services/scalar/scalar-widget.api.ts#L1
export default class WidgetApi {
_origin;
_widgetId;
_capabilities;
_inFlightRequests = {}; // { reqId => replyFn(payload) }
constructor(origin, widgetId, capabilities) {
this._origin = new URL(origin).origin;
this._widgetId = widgetId;
this._capabilities = capabilities;
const toWidgetActions = {
"capabilities": this._onCapabilitiesRequest.bind(this),
};
window.addEventListener("message", event => {
if (event.origin !== this._origin) return; // ignore due to invalid origin
if (!event.data) return;
if (event.data.widgetId !== this._widgetId) return;
const payload = event.data;
if (payload.api === "toWidget" && payload.action) {
console.log("[Inline Widget] Got toWidget: " + JSON.stringify(payload));
const handler = toWidgetActions[payload.action];
if (handler) handler(payload);
}
if (payload.api === "fromWidget" && this._inFlightRequests[payload.requestId]) {
console.log("[Inline Widget] Got fromWidget reply: " + JSON.stringify(payload));
const handler = this._inFlightRequests[payload.requestId];
delete this._inFlightRequests[payload.requestId];
handler(payload);
}
});
}
sendText(text) {
this.sendEvent("m.room.message", {msgtype: "m.text", body: text});
}
sendNotice(text) {
this.sendEvent("m.room.message", {msgtype: "m.notice", body: text});
}
sendEvent(eventType, content) {
this._callAction("send_event", {
type: eventType,
content: content,
});
}
_callAction(action, payload) {
if (!window.parent) {
return;
}
const request = {
api: "fromWidget",
widgetId: this._widgetId,
action: action,
requestId: randomString({length: 16}),
data: payload,
};
this._inFlightRequests[request.requestId] = () => {};
console.log("[Inline Widget] Sending fromWidget: ", request);
window.parent.postMessage(request, this._origin);
}
_replyPayload(incPayload, payload) {
if (!window.parent) {
return;
}
let request = JSON.parse(JSON.stringify(incPayload));
request["response"] = payload;
window.parent.postMessage(request, this._origin);
}
_onCapabilitiesRequest(payload) {
this._replyPayload(payload, {capabilities: this._capabilities});
}
}

View File

@ -0,0 +1,5 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<body>
<div id="widgetHtml"></div>
</body>

View File

@ -0,0 +1,71 @@
/*
Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import queryString from "querystring";
import WidgetApi from "./WidgetApi";
let widgetApi;
try {
const qs = queryString.parse(window.location.search.substring(1));
if (!qs["widgetId"]) {
// noinspection ExceptionCaughtLocallyJS
throw new Error("Missing widgetId in query string");
}
if (!qs["parentUrl"]) {
// noinspection ExceptionCaughtLocallyJS
throw new Error("Missing parentUrl in query string");
}
const widgetOpts = JSON.parse(atob(window.location.hash
.substring(1)
.replace(/-/g, '+')
.replace(/_/g, '/')));
// This widget wrapper is always on the same origin as the client itself
widgetApi = new WidgetApi(qs["parentUrl"], qs["widgetId"], widgetOpts["capabilities"]);
document.getElementById("widgetHtml").innerHTML = widgetOpts['html'];
bindButtons();
} catch (e) {
console.error("[Inline Widget Wrapper] Error loading widget from URL: ", e);
document.getElementById("widgetHtml").innerText = "Failed to load widget";
}
function bindButtons() {
const buttons = document.getElementsByTagName("button");
if (!buttons) return;
for (const button of buttons) {
button.addEventListener("click", onClick);
}
}
function onClick(event) {
if (!event.target) return;
const action = event.target.getAttribute("data-mx-action");
if (!action) return; // TODO: Submit form or something?
const value = event.target.getAttribute("data-mx-value");
if (!value) return; // ignore - no value
if (action === "m.send_text") {
widgetApi.sendText(value);
} else if (action === "m.send_notice") {
widgetApi.sendNotice(value);
} else if (action === "m.send_hidden") {
widgetApi.sendEvent("m.room.hidden", {body: value});
} // else ignore
}
// TODO: Binding of forms, etc

View File

@ -14,6 +14,7 @@ module.exports = {
"indexeddb-worker": "./src/vector/indexeddb-worker.js", "indexeddb-worker": "./src/vector/indexeddb-worker.js",
"mobileguide": "./src/vector/mobile_guide/index.js", "mobileguide": "./src/vector/mobile_guide/index.js",
"inline-widget-wrapper": "./src/vector/inline_widget_wrapper/index.js",
// CSS themes // CSS themes
"theme-light": "./node_modules/matrix-react-sdk/res/themes/light/css/light.scss", "theme-light": "./node_modules/matrix-react-sdk/res/themes/light/css/light.scss",
@ -178,7 +179,7 @@ module.exports = {
// bottom of <head> or the bottom of <body>, and I'm a bit scared // bottom of <head> or the bottom of <body>, and I'm a bit scared
// about moving them. // about moving them.
inject: false, inject: false,
excludeChunks: ['mobileguide'], excludeChunks: ['mobileguide', 'inline-widget-wrapper'],
vars: { vars: {
og_image_url: og_image_url, og_image_url: og_image_url,
}, },
@ -188,6 +189,11 @@ module.exports = {
filename: 'mobile_guide/index.html', filename: 'mobile_guide/index.html',
chunks: ['mobileguide'], chunks: ['mobileguide'],
}), }),
new HtmlWebpackPlugin({
template: './src/vector/inline_widget_wrapper/index.html',
filename: 'inline_widget_wrapper/index.html',
chunks: ['inline-widget-wrapper'],
}),
], ],
devtool: 'source-map', devtool: 'source-map',

View File

@ -7176,6 +7176,11 @@ raf@^3.1.0:
dependencies: dependencies:
performance-now "^2.1.0" performance-now "^2.1.0"
random-string@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/random-string/-/random-string-0.2.0.tgz#a46e4375352beda9a0d7b0d19ed6d321ecd1d82d"
integrity sha1-pG5DdTUr7amg17DRntbTIezR2C0=
randomatic@^3.0.0: randomatic@^3.0.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed"