mirror of
https://github.com/element-hq/element-web
synced 2024-11-28 20:38:55 +03:00
Add widget postmessage API stub.
This commit is contained in:
parent
b571ab02dc
commit
4f5f44ff38
2 changed files with 126 additions and 21 deletions
|
@ -16,26 +16,48 @@ limitations under the License.
|
||||||
|
|
||||||
export default class WidgetMessaging {
|
export default class WidgetMessaging {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.listenerCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register event listeners for the widget instance
|
* Register widget message event listeners
|
||||||
* @param {string} widgetId Unique widget identifier
|
|
||||||
* @return {undefined}
|
* @return {undefined}
|
||||||
*/
|
*/
|
||||||
registerListeners(widgetId) {
|
registerListeners() {
|
||||||
if (widgetId) {
|
if (this.listenerCount === 0) {
|
||||||
console.log("Register widget instance postmessage listeners");
|
window.addEventListener("message", this.onMessage, false);
|
||||||
} else {
|
}
|
||||||
console.error("Register widget event listeners - No widget ID specified!");
|
this.listenerCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
derigisterListeners() {
|
||||||
|
this.listenerCount -= 1;
|
||||||
|
if (this.listenerCount === 0) {
|
||||||
|
window.removeEventListener("message", this.onMessage);
|
||||||
|
}
|
||||||
|
if (this.listenerCount < 0) {
|
||||||
|
// Make an error so we get a stack trace
|
||||||
|
const e = new Error(
|
||||||
|
"WidgetMessaging: mismatched startListening / stopListening detected." +
|
||||||
|
" Negative count",
|
||||||
|
);
|
||||||
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
derigisterListeners(widgetId) {
|
onMessage(event) {
|
||||||
if (widgetId) {
|
console.warn("Checking for widget event", event);
|
||||||
console.log("Register widget instance postmessage listeners");
|
if (!event.origin) { // Handle chrome
|
||||||
} else {
|
event.origin = event.originalEvent.origin;
|
||||||
console.error("Deregister widget event listerns - No widget ID specified!");
|
}
|
||||||
}
|
|
||||||
|
// Event origin is empty string if undefined
|
||||||
|
if (event.origin.length === 0 || !event.data.widgetData) {
|
||||||
|
// TODO / FIXME -- check for valid origin URLs!!
|
||||||
|
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO -- handle widget actions
|
||||||
|
alert(event.data.widgetData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import React from 'react';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import PlatformPeg from '../../../PlatformPeg';
|
import PlatformPeg from '../../../PlatformPeg';
|
||||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
|
import WidgetMessaging from '../../../WidgetMessaging';
|
||||||
import TintableSvgButton from './TintableSvgButton';
|
import TintableSvgButton from './TintableSvgButton';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
|
@ -71,16 +72,46 @@ export default React.createClass({
|
||||||
return {
|
return {
|
||||||
initialising: true, // True while we are mangling the widget URL
|
initialising: true, // True while we are mangling the widget URL
|
||||||
loading: true, // True while the iframe content is loading
|
loading: true, // True while the iframe content is loading
|
||||||
widgetUrl: newProps.url,
|
widgetUrl: this._addWurlParams(newProps.url),
|
||||||
widgetPermissionId: widgetPermissionId,
|
widgetPermissionId: widgetPermissionId,
|
||||||
// Assume that widget has permission to load if we are the user who
|
// Assume that widget has permission to load if we are the user who
|
||||||
// added it to the room, or if explicitly granted by the user
|
// added it to the room, or if explicitly granted by the user
|
||||||
hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId,
|
hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId,
|
||||||
error: null,
|
error: null,
|
||||||
deleting: false,
|
deleting: false,
|
||||||
|
widgetPageTitle: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add widget instance specific parameters to pass in wUrl
|
||||||
|
* Properties passed to widget instance:
|
||||||
|
* - widgetId
|
||||||
|
* - origin / parent URL
|
||||||
|
* @param {string} urlString Url string to modify
|
||||||
|
* @return {string}
|
||||||
|
* Url string with parameters appended.
|
||||||
|
* If url can not be parsed, it is returned unmodified.
|
||||||
|
*/
|
||||||
|
_addWurlParams(urlString) {
|
||||||
|
const u = url.parse(urlString);
|
||||||
|
if (!u) {
|
||||||
|
console.error("_addWurlParams", "Invalid URL", urlString);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = qs.parse(u.query);
|
||||||
|
// Append widget ID to query parameters
|
||||||
|
params.widgetId = this.props.id;
|
||||||
|
// Append current / parent URL
|
||||||
|
params.parentUrl = window.location.href;
|
||||||
|
u.search = undefined;
|
||||||
|
u.query = params;
|
||||||
|
|
||||||
|
console.log("_addWurlParams", "Modified URL", u.format(), params);
|
||||||
|
return u.format();
|
||||||
|
},
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
return this._getNewState(this.props);
|
return this._getNewState(this.props);
|
||||||
},
|
},
|
||||||
|
@ -122,6 +153,8 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
|
this.widgetMessagingClient = new WidgetMessaging();
|
||||||
|
this.widgetMessagingClient.registerListeners();
|
||||||
window.addEventListener('message', this._onMessage, false);
|
window.addEventListener('message', this._onMessage, false);
|
||||||
this.setScalarToken();
|
this.setScalarToken();
|
||||||
},
|
},
|
||||||
|
@ -137,7 +170,7 @@ export default React.createClass({
|
||||||
console.warn('Non-scalar widget, not setting scalar token!', url);
|
console.warn('Non-scalar widget, not setting scalar token!', url);
|
||||||
this.setState({
|
this.setState({
|
||||||
error: null,
|
error: null,
|
||||||
widgetUrl: this.props.url,
|
widgetUrl: this._addWurlParams(this.props.url),
|
||||||
initialising: false,
|
initialising: false,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -150,7 +183,7 @@ export default React.createClass({
|
||||||
this._scalarClient.getScalarToken().done((token) => {
|
this._scalarClient.getScalarToken().done((token) => {
|
||||||
// Append scalar_token as a query param if not already present
|
// Append scalar_token as a query param if not already present
|
||||||
this._scalarClient.scalarToken = token;
|
this._scalarClient.scalarToken = token;
|
||||||
const u = url.parse(this.props.url);
|
const u = url.parse(this._addWurlParams(this.props.url));
|
||||||
const params = qs.parse(u.query);
|
const params = qs.parse(u.query);
|
||||||
if (!params.scalar_token) {
|
if (!params.scalar_token) {
|
||||||
params.scalar_token = encodeURIComponent(token);
|
params.scalar_token = encodeURIComponent(token);
|
||||||
|
@ -256,8 +289,36 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when widget iframe has finished loading
|
||||||
|
*/
|
||||||
_onLoaded() {
|
_onLoaded() {
|
||||||
this.setState({loading: false});
|
this.setState({loading: false});
|
||||||
|
// Get page title and update widget panel
|
||||||
|
// this._updateWidgetTitle();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch remote content title and update app tile
|
||||||
|
*/
|
||||||
|
_updateWidgetTitle() {
|
||||||
|
const safeUrl = this._getSafeUrl();
|
||||||
|
console.warn("widget title safeurl:", safeUrl);
|
||||||
|
if (safeUrl) {
|
||||||
|
let title = null;
|
||||||
|
try {
|
||||||
|
// title = yield this.getUrlTitle(safeUrl);
|
||||||
|
// console.log("Foo");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to get title for:", safeUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn("widget title:", title);
|
||||||
|
this.setState({widgetPageTitle: title});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.warn("widget title: no url");
|
||||||
|
this.setState({widgetPageTitle: null});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Widget labels to render, depending upon user permissions
|
// Widget labels to render, depending upon user permissions
|
||||||
|
@ -290,6 +351,21 @@ export default React.createClass({
|
||||||
return appTileName;
|
return appTileName;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the HTML title for a given URL
|
||||||
|
* @param {string} url URL to process
|
||||||
|
* @return {string} Title of the HTML page, or null
|
||||||
|
*/
|
||||||
|
getUrlTitle(url) {
|
||||||
|
return fetch(url)
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((html) => {
|
||||||
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
||||||
|
const title = doc.querySelectorAll('title')[0];
|
||||||
|
return title.innerText;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onClickMenuBar(ev) {
|
onClickMenuBar(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
|
@ -305,6 +381,15 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_getSafeUrl() {
|
||||||
|
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
||||||
|
let safeWidgetUrl = '';
|
||||||
|
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||||
|
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||||
|
}
|
||||||
|
return safeWidgetUrl;
|
||||||
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let appTileBody;
|
let appTileBody;
|
||||||
|
|
||||||
|
@ -320,11 +405,6 @@ export default React.createClass({
|
||||||
// a link to it.
|
// a link to it.
|
||||||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
||||||
"allow-same-origin allow-scripts allow-presentation";
|
"allow-same-origin allow-scripts allow-presentation";
|
||||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
|
||||||
let safeWidgetUrl = '';
|
|
||||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
|
||||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.show) {
|
if (this.props.show) {
|
||||||
const loadingElement = (
|
const loadingElement = (
|
||||||
|
@ -347,7 +427,7 @@ export default React.createClass({
|
||||||
{ this.state.loading && loadingElement }
|
{ this.state.loading && loadingElement }
|
||||||
<iframe
|
<iframe
|
||||||
ref="appFrame"
|
ref="appFrame"
|
||||||
src={safeWidgetUrl}
|
src={this._getSafeUrl()}
|
||||||
allowFullScreen="true"
|
allowFullScreen="true"
|
||||||
sandbox={sandboxFlags}
|
sandbox={sandboxFlags}
|
||||||
onLoad={this._onLoaded}
|
onLoad={this._onLoaded}
|
||||||
|
@ -383,6 +463,9 @@ export default React.createClass({
|
||||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||||
<b>{ this.formatAppTileName() }</b>
|
<b>{ this.formatAppTileName() }</b>
|
||||||
|
{ this.state.widgetPageTitle && (
|
||||||
|
<span> - { this.state.widgetPageTitle }</span>
|
||||||
|
) }
|
||||||
<span className="mx_AppTileMenuBarWidgets">
|
<span className="mx_AppTileMenuBarWidgets">
|
||||||
{ /* Edit widget */ }
|
{ /* Edit widget */ }
|
||||||
{ showEditButton && <TintableSvgButton
|
{ showEditButton && <TintableSvgButton
|
||||||
|
|
Loading…
Reference in a new issue