Merge pull request #5171 from matrix-org/travis/widget-api
Switch to using the Widget API SDK for widget messaging
This commit is contained in:
commit
cca9b91aca
21 changed files with 560 additions and 1272 deletions
|
@ -79,6 +79,7 @@
|
||||||
"linkifyjs": "^2.1.9",
|
"linkifyjs": "^2.1.9",
|
||||||
"lodash": "^4.17.19",
|
"lodash": "^4.17.19",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||||
|
"matrix-widget-api": "^0.1.0-beta.2",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"pako": "^1.0.11",
|
"pako": "^1.0.11",
|
||||||
"parse5": "^5.1.1",
|
"parse5": "^5.1.1",
|
||||||
|
|
|
@ -75,7 +75,8 @@ import {base32} from "rfc4648";
|
||||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||||
import WidgetStore from "./stores/WidgetStore";
|
import WidgetStore from "./stores/WidgetStore";
|
||||||
import ActiveWidgetStore from "./stores/ActiveWidgetStore";
|
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
|
||||||
|
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
||||||
|
|
||||||
// until we ts-ify the js-sdk voip code
|
// until we ts-ify the js-sdk voip code
|
||||||
type Call = any;
|
type Call = any;
|
||||||
|
@ -503,10 +504,10 @@ export default class CallHandler {
|
||||||
|
|
||||||
const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
|
const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
|
||||||
jitsiWidgets.forEach(w => {
|
jitsiWidgets.forEach(w => {
|
||||||
const messaging = ActiveWidgetStore.getWidgetMessaging(w.id);
|
const messaging = WidgetMessagingStore.instance.getMessagingForId(w.id);
|
||||||
if (!messaging) return; // more "should never happen" words
|
if (!messaging) return; // more "should never happen" words
|
||||||
|
|
||||||
messaging.hangup();
|
messaging.transport.send(ElementWidgetActions.HangupCall, {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,275 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 Travis Ralston
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
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 URL from 'url';
|
|
||||||
import dis from './dispatcher/dispatcher';
|
|
||||||
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
|
|
||||||
import ActiveWidgetStore from './stores/ActiveWidgetStore';
|
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
|
||||||
import RoomViewStore from "./stores/RoomViewStore";
|
|
||||||
import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
|
||||||
import {Capability} from "./widgets/WidgetApi";
|
|
||||||
import {objectClone} from "./utils/objects";
|
|
||||||
|
|
||||||
const WIDGET_API_VERSION = '0.0.2'; // Current API version
|
|
||||||
const SUPPORTED_WIDGET_API_VERSIONS = [
|
|
||||||
'0.0.1',
|
|
||||||
'0.0.2',
|
|
||||||
];
|
|
||||||
const INBOUND_API_NAME = 'fromWidget';
|
|
||||||
|
|
||||||
// Listen for and handle incoming requests using the 'fromWidget' postMessage
|
|
||||||
// API and initiate responses
|
|
||||||
export default class FromWidgetPostMessageApi {
|
|
||||||
constructor() {
|
|
||||||
this.widgetMessagingEndpoints = [];
|
|
||||||
this.widgetListeners = {}; // {action: func[]}
|
|
||||||
|
|
||||||
this.start = this.start.bind(this);
|
|
||||||
this.stop = this.stop.bind(this);
|
|
||||||
this.onPostMessage = this.onPostMessage.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
window.addEventListener('message', this.onPostMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
window.removeEventListener('message', this.onPostMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a listener for a given action
|
|
||||||
* @param {string} action The action to listen for.
|
|
||||||
* @param {Function} callbackFn A callback function to be called when the action is
|
|
||||||
* encountered. Called with two parameters: the interesting request information and
|
|
||||||
* the raw event received from the postMessage API. The raw event is meant to be used
|
|
||||||
* for sendResponse and similar functions.
|
|
||||||
*/
|
|
||||||
addListener(action, callbackFn) {
|
|
||||||
if (!this.widgetListeners[action]) this.widgetListeners[action] = [];
|
|
||||||
this.widgetListeners[action].push(callbackFn);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a listener for a given action.
|
|
||||||
* @param {string} action The action that was subscribed to.
|
|
||||||
* @param {Function} callbackFn The original callback function that was used to subscribe
|
|
||||||
* to updates.
|
|
||||||
*/
|
|
||||||
removeListener(action, callbackFn) {
|
|
||||||
if (!this.widgetListeners[action]) return;
|
|
||||||
|
|
||||||
const idx = this.widgetListeners[action].indexOf(callbackFn);
|
|
||||||
if (idx !== -1) this.widgetListeners[action].splice(idx, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a widget endpoint for trusted postMessage communication
|
|
||||||
* @param {string} widgetId Unique widget identifier
|
|
||||||
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
|
||||||
*/
|
|
||||||
addEndpoint(widgetId, endpointUrl) {
|
|
||||||
const u = URL.parse(endpointUrl);
|
|
||||||
if (!u || !u.protocol || !u.host) {
|
|
||||||
console.warn('Add FromWidgetPostMessageApi endpoint - Invalid origin:', endpointUrl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const origin = u.protocol + '//' + u.host;
|
|
||||||
const endpoint = new WidgetMessagingEndpoint(widgetId, origin);
|
|
||||||
if (this.widgetMessagingEndpoints.some(function(ep) {
|
|
||||||
return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
|
|
||||||
})) {
|
|
||||||
// Message endpoint already registered
|
|
||||||
console.warn('Add FromWidgetPostMessageApi - Endpoint already registered');
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
console.log(`Adding fromWidget messaging endpoint for ${widgetId}`, endpoint);
|
|
||||||
this.widgetMessagingEndpoints.push(endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* De-register a widget endpoint from trusted communication sources
|
|
||||||
* @param {string} widgetId Unique widget identifier
|
|
||||||
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
|
||||||
* @return {boolean} True if endpoint was successfully removed
|
|
||||||
*/
|
|
||||||
removeEndpoint(widgetId, endpointUrl) {
|
|
||||||
const u = URL.parse(endpointUrl);
|
|
||||||
if (!u || !u.protocol || !u.host) {
|
|
||||||
console.warn('Remove widget messaging endpoint - Invalid origin');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const origin = u.protocol + '//' + u.host;
|
|
||||||
if (this.widgetMessagingEndpoints && this.widgetMessagingEndpoints.length > 0) {
|
|
||||||
const length = this.widgetMessagingEndpoints.length;
|
|
||||||
this.widgetMessagingEndpoints = this.widgetMessagingEndpoints
|
|
||||||
.filter((endpoint) => endpoint.widgetId !== widgetId || endpoint.endpointUrl !== origin);
|
|
||||||
return (length > this.widgetMessagingEndpoints.length);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle widget postMessage events
|
|
||||||
* Messages are only handled where a valid, registered messaging endpoints
|
|
||||||
* @param {Event} event Event to handle
|
|
||||||
* @return {undefined}
|
|
||||||
*/
|
|
||||||
onPostMessage(event) {
|
|
||||||
if (!event.origin) { // Handle chrome
|
|
||||||
event.origin = event.originalEvent.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event origin is empty string if undefined
|
|
||||||
if (
|
|
||||||
event.origin.length === 0 ||
|
|
||||||
!this.trustedEndpoint(event.origin) ||
|
|
||||||
event.data.api !== INBOUND_API_NAME ||
|
|
||||||
!event.data.widgetId
|
|
||||||
) {
|
|
||||||
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call any listeners we have registered
|
|
||||||
if (this.widgetListeners[event.data.action]) {
|
|
||||||
for (const fn of this.widgetListeners[event.data.action]) {
|
|
||||||
fn(event.data, event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Although the requestId is required, we don't use it. We'll be nice and process the message
|
|
||||||
// if the property is missing, but with a warning for widget developers.
|
|
||||||
if (!event.data.requestId) {
|
|
||||||
console.warn("fromWidget action '" + event.data.action + "' does not have a requestId");
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = event.data.action;
|
|
||||||
const widgetId = event.data.widgetId;
|
|
||||||
if (action === 'content_loaded') {
|
|
||||||
console.log('Widget reported content loaded for', widgetId);
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'widget_content_loaded',
|
|
||||||
widgetId: widgetId,
|
|
||||||
});
|
|
||||||
this.sendResponse(event, {success: true});
|
|
||||||
} else if (action === 'supported_api_versions') {
|
|
||||||
this.sendResponse(event, {
|
|
||||||
api: INBOUND_API_NAME,
|
|
||||||
supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
|
|
||||||
});
|
|
||||||
} else if (action === 'api_version') {
|
|
||||||
this.sendResponse(event, {
|
|
||||||
api: INBOUND_API_NAME,
|
|
||||||
version: WIDGET_API_VERSION,
|
|
||||||
});
|
|
||||||
} else if (action === 'm.sticker') {
|
|
||||||
// console.warn('Got sticker message from widget', widgetId);
|
|
||||||
// NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually
|
|
||||||
const data = event.data.data || event.data.widgetData;
|
|
||||||
dis.dispatch({action: 'm.sticker', data: data, widgetId: event.data.widgetId});
|
|
||||||
} else if (action === 'integration_manager_open') {
|
|
||||||
// Close the stickerpicker
|
|
||||||
dis.dispatch({action: 'stickerpicker_close'});
|
|
||||||
// Open the integration manager
|
|
||||||
// NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually
|
|
||||||
const data = event.data.data || event.data.widgetData;
|
|
||||||
const integType = (data && data.integType) ? data.integType : null;
|
|
||||||
const integId = (data && data.integId) ? data.integId : null;
|
|
||||||
|
|
||||||
// TODO: Open the right integration manager for the widget
|
|
||||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
|
||||||
IntegrationManagers.sharedInstance().openAll(
|
|
||||||
MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
|
|
||||||
`type_${integType}`,
|
|
||||||
integId,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(
|
|
||||||
MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
|
|
||||||
`type_${integType}`,
|
|
||||||
integId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (action === 'set_always_on_screen') {
|
|
||||||
// This is a new message: there is no reason to support the deprecated widgetData here
|
|
||||||
const data = event.data.data;
|
|
||||||
const val = data.value;
|
|
||||||
|
|
||||||
if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
|
|
||||||
ActiveWidgetStore.setWidgetPersistence(widgetId, val);
|
|
||||||
}
|
|
||||||
} else if (action === 'get_openid') {
|
|
||||||
// Handled by caller
|
|
||||||
} else {
|
|
||||||
console.warn('Widget postMessage event unhandled');
|
|
||||||
this.sendError(event, {message: 'The postMessage was unhandled'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if message origin is registered as trusted
|
|
||||||
* @param {string} origin PostMessage origin to check
|
|
||||||
* @return {boolean} True if trusted
|
|
||||||
*/
|
|
||||||
trustedEndpoint(origin) {
|
|
||||||
if (!origin) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.widgetMessagingEndpoints.some((endpoint) => {
|
|
||||||
// TODO / FIXME -- Should this also check the widgetId?
|
|
||||||
return endpoint.endpointUrl === origin;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a postmessage response to a postMessage request
|
|
||||||
* @param {Event} event The original postMessage request event
|
|
||||||
* @param {Object} res Response data
|
|
||||||
*/
|
|
||||||
sendResponse(event, res) {
|
|
||||||
const data = objectClone(event.data);
|
|
||||||
data.response = res;
|
|
||||||
event.source.postMessage(data, event.origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send an error response to a postMessage request
|
|
||||||
* @param {Event} event The original postMessage request event
|
|
||||||
* @param {string} msg Error message
|
|
||||||
* @param {Error} nestedError Nested error event (optional)
|
|
||||||
*/
|
|
||||||
sendError(event, msg, nestedError) {
|
|
||||||
console.error('Action:' + event.data.action + ' failed with message: ' + msg);
|
|
||||||
const data = objectClone(event.data);
|
|
||||||
data.response = {
|
|
||||||
error: {
|
|
||||||
message: msg,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (nestedError) {
|
|
||||||
data.response.error._error = nestedError;
|
|
||||||
}
|
|
||||||
event.source.postMessage(data, event.origin);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// const OUTBOUND_API_NAME = 'toWidget';
|
|
||||||
|
|
||||||
// Initiate requests using the "toWidget" postMessage API and handle responses
|
|
||||||
// NOTE: ToWidgetPostMessageApi only handles message events with a data payload with a
|
|
||||||
// response field
|
|
||||||
export default class ToWidgetPostMessageApi {
|
|
||||||
constructor(timeoutMs) {
|
|
||||||
this._timeoutMs = timeoutMs || 5000; // default to 5s timer
|
|
||||||
this._counter = 0;
|
|
||||||
this._requestMap = {
|
|
||||||
// $ID: {resolve, reject}
|
|
||||||
};
|
|
||||||
this.start = this.start.bind(this);
|
|
||||||
this.stop = this.stop.bind(this);
|
|
||||||
this.onPostMessage = this.onPostMessage.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
window.addEventListener('message', this.onPostMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
window.removeEventListener('message', this.onPostMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
onPostMessage(ev) {
|
|
||||||
// THIS IS ALL UNSAFE EXECUTION.
|
|
||||||
// We do not verify who the sender of `ev` is!
|
|
||||||
const payload = ev.data;
|
|
||||||
// NOTE: Workaround for running in a mobile WebView where a
|
|
||||||
// postMessage immediately triggers this callback even though it is
|
|
||||||
// not the response.
|
|
||||||
if (payload.response === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const promise = this._requestMap[payload.requestId];
|
|
||||||
if (!promise) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
delete this._requestMap[payload.requestId];
|
|
||||||
promise.resolve(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initiate outbound requests (toWidget)
|
|
||||||
exec(action, targetWindow, targetOrigin) {
|
|
||||||
targetWindow = targetWindow || window.parent; // default to parent window
|
|
||||||
targetOrigin = targetOrigin || "*";
|
|
||||||
this._counter += 1;
|
|
||||||
action.requestId = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this._requestMap[action.requestId] = {resolve, reject};
|
|
||||||
targetWindow.postMessage(action, targetOrigin);
|
|
||||||
|
|
||||||
if (this._timeoutMs > 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!this._requestMap[action.requestId]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.error("postMessage request timed out. Sent object: " + JSON.stringify(action),
|
|
||||||
this._requestMap);
|
|
||||||
this._requestMap[action.requestId].reject(new Error("Timed out"));
|
|
||||||
delete this._requestMap[action.requestId];
|
|
||||||
}, this._timeoutMs);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,223 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 New Vector Ltd
|
|
||||||
Copyright 2019 Travis Ralston
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* See - https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing for
|
|
||||||
* spec. details / documentation.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import FromWidgetPostMessageApi from './FromWidgetPostMessageApi';
|
|
||||||
import ToWidgetPostMessageApi from './ToWidgetPostMessageApi';
|
|
||||||
import Modal from "./Modal";
|
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
|
||||||
import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog";
|
|
||||||
import WidgetUtils from "./utils/WidgetUtils";
|
|
||||||
import {KnownWidgetActions} from "./widgets/WidgetApi";
|
|
||||||
|
|
||||||
if (!global.mxFromWidgetMessaging) {
|
|
||||||
global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
|
|
||||||
global.mxFromWidgetMessaging.start();
|
|
||||||
}
|
|
||||||
if (!global.mxToWidgetMessaging) {
|
|
||||||
global.mxToWidgetMessaging = new ToWidgetPostMessageApi();
|
|
||||||
global.mxToWidgetMessaging.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
const OUTBOUND_API_NAME = 'toWidget';
|
|
||||||
|
|
||||||
export default class WidgetMessaging {
|
|
||||||
/**
|
|
||||||
* @param {string} widgetId The widget's ID
|
|
||||||
* @param {string} wurl The raw URL of the widget as in the event (the 'wURL')
|
|
||||||
* @param {string} renderedUrl The url used in the widget's iframe (either similar to the wURL
|
|
||||||
* or a different URL of the clients choosing if it is using its own impl).
|
|
||||||
* @param {bool} isUserWidget If true, the widget is a user widget, otherwise it's a room widget
|
|
||||||
* @param {object} target Where widget messages should be sent (eg. the iframe object)
|
|
||||||
*/
|
|
||||||
constructor(widgetId, wurl, renderedUrl, isUserWidget, target) {
|
|
||||||
this.widgetId = widgetId;
|
|
||||||
this.wurl = wurl;
|
|
||||||
this.renderedUrl = renderedUrl;
|
|
||||||
this.isUserWidget = isUserWidget;
|
|
||||||
this.target = target;
|
|
||||||
this.fromWidget = global.mxFromWidgetMessaging;
|
|
||||||
this.toWidget = global.mxToWidgetMessaging;
|
|
||||||
this._onOpenIdRequest = this._onOpenIdRequest.bind(this);
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
messageToWidget(action) {
|
|
||||||
action.widgetId = this.widgetId; // Required to be sent for all outbound requests
|
|
||||||
|
|
||||||
return this.toWidget.exec(action, this.target).then((data) => {
|
|
||||||
// Check for errors and reject if found
|
|
||||||
if (data.response === undefined) { // null is valid
|
|
||||||
throw new Error("Missing 'response' field");
|
|
||||||
}
|
|
||||||
if (data.response && data.response.error) {
|
|
||||||
const err = data.response.error;
|
|
||||||
const msg = String(err.message ? err.message : "An error was returned");
|
|
||||||
if (err._error) {
|
|
||||||
console.error(err._error);
|
|
||||||
}
|
|
||||||
// Potential XSS attack if 'msg' is not appropriately sanitized,
|
|
||||||
// as it is untrusted input by our parent window (which we assume is Element).
|
|
||||||
// We can't aggressively sanitize [A-z0-9] since it might be a translation.
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
// Return the response field for the request
|
|
||||||
return data.response;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells the widget that the client is ready to handle further widget requests.
|
|
||||||
* @returns {Promise<*>} Resolves after the widget has acknowledged the ready message.
|
|
||||||
*/
|
|
||||||
flagReadyToContinue() {
|
|
||||||
return this.messageToWidget({
|
|
||||||
api: OUTBOUND_API_NAME,
|
|
||||||
action: KnownWidgetActions.ClientReady,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells the widget that it should terminate now.
|
|
||||||
* @returns {Promise<*>} Resolves when widget has acknowledged the message.
|
|
||||||
*/
|
|
||||||
terminate() {
|
|
||||||
return this.messageToWidget({
|
|
||||||
api: OUTBOUND_API_NAME,
|
|
||||||
action: KnownWidgetActions.Terminate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells the widget to hang up on its call.
|
|
||||||
* @returns {Promise<*>} Resolves when the widget has acknowledged the message.
|
|
||||||
*/
|
|
||||||
hangup() {
|
|
||||||
return this.messageToWidget({
|
|
||||||
api: OUTBOUND_API_NAME,
|
|
||||||
action: KnownWidgetActions.Hangup,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request a screenshot from a widget
|
|
||||||
* @return {Promise} To be resolved with screenshot data when it has been generated
|
|
||||||
*/
|
|
||||||
getScreenshot() {
|
|
||||||
console.log('Requesting screenshot for', this.widgetId);
|
|
||||||
return this.messageToWidget({
|
|
||||||
api: OUTBOUND_API_NAME,
|
|
||||||
action: "screenshot",
|
|
||||||
})
|
|
||||||
.catch((error) => new Error("Failed to get screenshot: " + error.message))
|
|
||||||
.then((response) => response.screenshot);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request capabilities required by the widget
|
|
||||||
* @return {Promise} To be resolved with an array of requested widget capabilities
|
|
||||||
*/
|
|
||||||
getCapabilities() {
|
|
||||||
console.log('Requesting capabilities for', this.widgetId);
|
|
||||||
return this.messageToWidget({
|
|
||||||
api: OUTBOUND_API_NAME,
|
|
||||||
action: "capabilities",
|
|
||||||
}).then((response) => {
|
|
||||||
console.log('Got capabilities for', this.widgetId, response.capabilities);
|
|
||||||
return response.capabilities;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sendVisibility(visible) {
|
|
||||||
return this.messageToWidget({
|
|
||||||
api: OUTBOUND_API_NAME,
|
|
||||||
action: "visibility",
|
|
||||||
visible,
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Failed to send visibility: ", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
this.fromWidget.addEndpoint(this.widgetId, this.renderedUrl);
|
|
||||||
this.fromWidget.addListener("get_openid", this._onOpenIdRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
this.fromWidget.removeEndpoint(this.widgetId, this.renderedUrl);
|
|
||||||
this.fromWidget.removeListener("get_openid", this._onOpenIdRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _onOpenIdRequest(ev, rawEv) {
|
|
||||||
if (ev.widgetId !== this.widgetId) return; // not interesting
|
|
||||||
|
|
||||||
const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.wurl, this.isUserWidget);
|
|
||||||
|
|
||||||
const settings = SettingsStore.getValue("widgetOpenIDPermissions");
|
|
||||||
if (settings.deny && settings.deny.includes(widgetSecurityKey)) {
|
|
||||||
this.fromWidget.sendResponse(rawEv, {state: "blocked"});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (settings.allow && settings.allow.includes(widgetSecurityKey)) {
|
|
||||||
const responseBody = {state: "allowed"};
|
|
||||||
const credentials = await MatrixClientPeg.get().getOpenIdToken();
|
|
||||||
Object.assign(responseBody, credentials);
|
|
||||||
this.fromWidget.sendResponse(rawEv, responseBody);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm that we received the request
|
|
||||||
this.fromWidget.sendResponse(rawEv, {state: "request"});
|
|
||||||
|
|
||||||
// Actually ask for permission to send the user's data
|
|
||||||
Modal.createTrackedDialog("OpenID widget permissions", '',
|
|
||||||
WidgetOpenIDPermissionsDialog, {
|
|
||||||
widgetUrl: this.wurl,
|
|
||||||
widgetId: this.widgetId,
|
|
||||||
isUserWidget: this.isUserWidget,
|
|
||||||
|
|
||||||
onFinished: async (confirm) => {
|
|
||||||
const responseBody = {
|
|
||||||
// Legacy (early draft) fields
|
|
||||||
success: confirm,
|
|
||||||
|
|
||||||
// New style MSC1960 fields
|
|
||||||
state: confirm ? "allowed" : "blocked",
|
|
||||||
original_request_id: ev.requestId, // eslint-disable-line camelcase
|
|
||||||
};
|
|
||||||
if (confirm) {
|
|
||||||
const credentials = await MatrixClientPeg.get().getOpenIdToken();
|
|
||||||
Object.assign(responseBody, credentials);
|
|
||||||
}
|
|
||||||
this.messageToWidget({
|
|
||||||
api: OUTBOUND_API_NAME,
|
|
||||||
action: "openid_credentials",
|
|
||||||
data: responseBody,
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error("Failed to send OpenID credentials: ", error);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents mapping of widget instance to URLs for trusted postMessage communication.
|
|
||||||
*/
|
|
||||||
export default class WidgetMessageEndpoint {
|
|
||||||
/**
|
|
||||||
* Mapping of widget instance to URL for trusted postMessage communication.
|
|
||||||
* @param {string} widgetId Unique widget identifier
|
|
||||||
* @param {string} endpointUrl Widget wurl origin.
|
|
||||||
*/
|
|
||||||
constructor(widgetId, endpointUrl) {
|
|
||||||
if (!widgetId) {
|
|
||||||
throw new Error("No widgetId specified in widgetMessageEndpoint constructor");
|
|
||||||
}
|
|
||||||
if (!endpointUrl) {
|
|
||||||
throw new Error("No endpoint specified in widgetMessageEndpoint constructor");
|
|
||||||
}
|
|
||||||
this.widgetId = widgetId;
|
|
||||||
this.endpointUrl = endpointUrl;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,11 +18,9 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import qs from 'qs';
|
|
||||||
import React, {createRef} from 'react';
|
import React, {createRef} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import WidgetMessaging from '../../../WidgetMessaging';
|
|
||||||
import AccessibleButton from './AccessibleButton';
|
import AccessibleButton from './AccessibleButton';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -34,37 +32,16 @@ import WidgetUtils from '../../../utils/WidgetUtils';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
||||||
import PersistedElement from "./PersistedElement";
|
import PersistedElement from "./PersistedElement";
|
||||||
import {WidgetType} from "../../../widgets/WidgetType";
|
import {WidgetType} from "../../../widgets/WidgetType";
|
||||||
import {Capability} from "../../../widgets/WidgetApi";
|
|
||||||
import {sleep} from "../../../utils/promise";
|
|
||||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||||
import WidgetStore from "../../../stores/WidgetStore";
|
import WidgetStore from "../../../stores/WidgetStore";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
|
import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
|
||||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions";
|
||||||
const ENABLE_REACT_PERF = false;
|
import {MatrixCapabilities} from "matrix-widget-api";
|
||||||
|
|
||||||
/**
|
|
||||||
* Does template substitution on a URL (or any string). Variables will be
|
|
||||||
* passed through encodeURIComponent.
|
|
||||||
* @param {string} uriTemplate The path with template variables e.g. '/foo/$bar'.
|
|
||||||
* @param {Object} variables The key/value pairs to replace the template
|
|
||||||
* variables with. E.g. { '$bar': 'baz' }.
|
|
||||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
|
||||||
*/
|
|
||||||
function uriFromTemplate(uriTemplate, variables) {
|
|
||||||
let out = uriTemplate;
|
|
||||||
for (const [key, val] of Object.entries(variables)) {
|
|
||||||
out = out.replace(
|
|
||||||
'$' + key, encodeURIComponent(val),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class AppTile extends React.Component {
|
export default class AppTile extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -72,11 +49,13 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
// The key used for PersistedElement
|
// The key used for PersistedElement
|
||||||
this._persistKey = 'widget_' + this.props.app.id;
|
this._persistKey = 'widget_' + this.props.app.id;
|
||||||
|
this._sgWidget = new StopGapWidget(this.props);
|
||||||
|
this._sgWidget.on("ready", this._onWidgetReady);
|
||||||
|
this.iframe = null; // ref to the iframe (callback style)
|
||||||
|
|
||||||
this.state = this._getNewState(props);
|
this.state = this._getNewState(props);
|
||||||
|
|
||||||
this._onAction = this._onAction.bind(this);
|
this._onAction = this._onAction.bind(this);
|
||||||
this._onLoaded = this._onLoaded.bind(this);
|
|
||||||
this._onEditClick = this._onEditClick.bind(this);
|
this._onEditClick = this._onEditClick.bind(this);
|
||||||
this._onDeleteClick = this._onDeleteClick.bind(this);
|
this._onDeleteClick = this._onDeleteClick.bind(this);
|
||||||
this._onRevokeClicked = this._onRevokeClicked.bind(this);
|
this._onRevokeClicked = this._onRevokeClicked.bind(this);
|
||||||
|
@ -89,7 +68,6 @@ export default class AppTile extends React.Component {
|
||||||
this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
|
this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
|
||||||
|
|
||||||
this._contextMenuButton = createRef();
|
this._contextMenuButton = createRef();
|
||||||
this._appFrame = createRef();
|
|
||||||
this._menu_bar = createRef();
|
this._menu_bar = createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,12 +86,10 @@ export default class AppTile extends React.Component {
|
||||||
return !!currentlyAllowedWidgets[newProps.app.eventId];
|
return !!currentlyAllowedWidgets[newProps.app.eventId];
|
||||||
};
|
};
|
||||||
|
|
||||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
|
||||||
return {
|
return {
|
||||||
initialising: true, // True while we are mangling the widget URL
|
initialising: true, // True while we are mangling the widget URL
|
||||||
// True while the iframe content is loading
|
// True while the iframe content is loading
|
||||||
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
|
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
|
||||||
widgetUrl: this._addWurlParams(newProps.app.url),
|
|
||||||
// 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: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(),
|
hasPermissionToLoad: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(),
|
||||||
|
@ -124,43 +100,6 @@ export default class AppTile extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Does the widget support a given capability
|
|
||||||
* @param {string} capability Capability to check for
|
|
||||||
* @return {Boolean} True if capability supported
|
|
||||||
*/
|
|
||||||
_hasCapability(capability) {
|
|
||||||
return ActiveWidgetStore.widgetHasCapability(this.props.app.id, capability);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
try {
|
|
||||||
const parsed = new URL(urlString);
|
|
||||||
|
|
||||||
// TODO: Replace these with proper widget params
|
|
||||||
// See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
|
|
||||||
parsed.searchParams.set('widgetId', this.props.app.id);
|
|
||||||
parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
|
|
||||||
|
|
||||||
// Replace the encoded dollar signs back to dollar signs. They have no special meaning
|
|
||||||
// in HTTP, but URL parsers encode them anyways.
|
|
||||||
return parsed.toString().replace(/%24/g, '$');
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to add widget URL params:", e);
|
|
||||||
return urlString;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isMixedContent() {
|
isMixedContent() {
|
||||||
const parentContentProtocol = window.location.protocol;
|
const parentContentProtocol = window.location.protocol;
|
||||||
const u = url.parse(this.props.app.url);
|
const u = url.parse(this.props.app.url);
|
||||||
|
@ -176,7 +115,7 @@ export default class AppTile extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// Only fetch IM token on mount if we're showing and have permission to load
|
// Only fetch IM token on mount if we're showing and have permission to load
|
||||||
if (this.props.show && this.state.hasPermissionToLoad) {
|
if (this.props.show && this.state.hasPermissionToLoad) {
|
||||||
this.setScalarToken();
|
this._startWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Widget action listeners
|
// Widget action listeners
|
||||||
|
@ -190,93 +129,44 @@ export default class AppTile extends React.Component {
|
||||||
// if it's not remaining on screen, get rid of the PersistedElement container
|
// if it's not remaining on screen, get rid of the PersistedElement container
|
||||||
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
|
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
|
||||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
PersistedElement.destroyElement(this._persistKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._sgWidget) {
|
||||||
|
this._sgWidget.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Generify the name of this function. It's not just scalar tokens.
|
_resetWidget(newProps) {
|
||||||
/**
|
if (this._sgWidget) {
|
||||||
* Adds a scalar token to the widget URL, if required
|
this._sgWidget.stop();
|
||||||
* Component initialisation is only complete when this function has resolved
|
}
|
||||||
*/
|
this._sgWidget = new StopGapWidget(newProps);
|
||||||
setScalarToken() {
|
this._sgWidget.on("ready", this._onWidgetReady);
|
||||||
if (!WidgetUtils.isScalarUrl(this.props.app.url)) {
|
this._startWidget();
|
||||||
console.warn('Widget does not match integration manager, refusing to set auth token', url);
|
|
||||||
this.setState({
|
|
||||||
error: null,
|
|
||||||
widgetUrl: this._addWurlParams(this.props.app.url),
|
|
||||||
initialising: false,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const managers = IntegrationManagers.sharedInstance();
|
_startWidget() {
|
||||||
if (!managers.hasManager()) {
|
this._sgWidget.prepare().then(() => {
|
||||||
console.warn("No integration manager - not setting scalar token", url);
|
this.setState({initialising: false});
|
||||||
this.setState({
|
|
||||||
error: null,
|
|
||||||
widgetUrl: this._addWurlParams(this.props.app.url),
|
|
||||||
initialising: false,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Pick the right manager for the widget
|
|
||||||
|
|
||||||
const defaultManager = managers.getPrimaryManager();
|
|
||||||
if (!WidgetUtils.isScalarUrl(defaultManager.apiUrl)) {
|
|
||||||
console.warn('Unknown integration manager, refusing to set auth token', url);
|
|
||||||
this.setState({
|
|
||||||
error: null,
|
|
||||||
widgetUrl: this._addWurlParams(this.props.app.url),
|
|
||||||
initialising: false,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the token before loading the iframe as we need it to mangle the URL
|
|
||||||
if (!this._scalarClient) {
|
|
||||||
this._scalarClient = defaultManager.getScalarClient();
|
|
||||||
}
|
|
||||||
this._scalarClient.getScalarToken().then((token) => {
|
|
||||||
// Append scalar_token as a query param if not already present
|
|
||||||
this._scalarClient.scalarToken = token;
|
|
||||||
const u = url.parse(this._addWurlParams(this.props.app.url));
|
|
||||||
const params = qs.parse(u.query);
|
|
||||||
if (!params.scalar_token) {
|
|
||||||
params.scalar_token = encodeURIComponent(token);
|
|
||||||
// u.search must be set to undefined, so that u.format() uses query parameters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
|
|
||||||
u.search = undefined;
|
|
||||||
u.query = params;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
error: null,
|
|
||||||
widgetUrl: u.format(),
|
|
||||||
initialising: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch page title from remote content if not already set
|
|
||||||
if (!this.state.widgetPageTitle && params.url) {
|
|
||||||
this._fetchWidgetTitle(params.url);
|
|
||||||
}
|
|
||||||
}, (err) => {
|
|
||||||
console.error("Failed to get scalar_token", err);
|
|
||||||
this.setState({
|
|
||||||
error: err.message,
|
|
||||||
initialising: false,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_iframeRefChange = (ref) => {
|
||||||
|
this.iframe = ref;
|
||||||
|
if (ref) {
|
||||||
|
this._sgWidget.start(ref);
|
||||||
|
} else {
|
||||||
|
this._resetWidget(this.props);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
||||||
if (nextProps.app.url !== this.props.app.url) {
|
if (nextProps.app.url !== this.props.app.url) {
|
||||||
this._getNewState(nextProps);
|
this._getNewState(nextProps);
|
||||||
// Fetch IM token for new URL if we're showing and have permission to load
|
|
||||||
if (this.props.show && this.state.hasPermissionToLoad) {
|
if (this.props.show && this.state.hasPermissionToLoad) {
|
||||||
this.setScalarToken();
|
this._resetWidget(nextProps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,9 +177,9 @@ export default class AppTile extends React.Component {
|
||||||
loading: true,
|
loading: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Fetch IM token now that we're showing if we already have permission to load
|
// Start the widget now that we're showing if we already have permission to load
|
||||||
if (this.state.hasPermissionToLoad) {
|
if (this.state.hasPermissionToLoad) {
|
||||||
this.setScalarToken();
|
this._startWidget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,7 +209,14 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSnapshotClick() {
|
_onSnapshotClick() {
|
||||||
WidgetUtils.snapshotWidget(this.props.app);
|
this._sgWidget.widgetApi.takeScreenshot().then(data => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'picture_snapshot',
|
||||||
|
file: data.screenshot,
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Failed to take screenshot: ", err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -327,35 +224,24 @@ export default class AppTile extends React.Component {
|
||||||
* @private
|
* @private
|
||||||
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
|
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
|
||||||
*/
|
*/
|
||||||
_endWidgetActions() {
|
async _endWidgetActions() { // widget migration dev note: async to maintain signature
|
||||||
let terminationPromise;
|
|
||||||
|
|
||||||
if (this._hasCapability(Capability.ReceiveTerminate)) {
|
|
||||||
// Wait for widget to terminate within a timeout
|
|
||||||
const timeout = 2000;
|
|
||||||
const messaging = ActiveWidgetStore.getWidgetMessaging(this.props.app.id);
|
|
||||||
terminationPromise = Promise.race([messaging.terminate(), sleep(timeout)]);
|
|
||||||
} else {
|
|
||||||
terminationPromise = Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
return terminationPromise.finally(() => {
|
|
||||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
||||||
// its hold on the webcam. Without this, the widget holds a media
|
// its hold on the webcam. Without this, the widget holds a media
|
||||||
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
|
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
|
||||||
if (this._appFrame.current) {
|
if (this.iframe) {
|
||||||
// In practice we could just do `+= ''` to trick the browser
|
// In practice we could just do `+= ''` to trick the browser
|
||||||
// into thinking the URL changed, however I can foresee this
|
// into thinking the URL changed, however I can foresee this
|
||||||
// being optimized out by a browser. Instead, we'll just point
|
// being optimized out by a browser. Instead, we'll just point
|
||||||
// the iframe at a page that is reasonably safe to use in the
|
// the iframe at a page that is reasonably safe to use in the
|
||||||
// event the iframe doesn't wink away.
|
// event the iframe doesn't wink away.
|
||||||
// This is relative to where the Element instance is located.
|
// This is relative to where the Element instance is located.
|
||||||
this._appFrame.current.src = 'about:blank';
|
this.iframe.src = 'about:blank';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the widget from the persisted store for good measure.
|
// Delete the widget from the persisted store for good measure.
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
PersistedElement.destroyElement(this._persistKey);
|
||||||
});
|
|
||||||
|
this._sgWidget.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If user has permission to modify widgets, delete the widget,
|
/* If user has permission to modify widgets, delete the widget,
|
||||||
|
@ -409,73 +295,18 @@ export default class AppTile extends React.Component {
|
||||||
this._revokeWidgetPermission();
|
this._revokeWidgetPermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
_onWidgetReady = () => {
|
||||||
* Called when widget iframe has finished loading
|
|
||||||
*/
|
|
||||||
_onLoaded() {
|
|
||||||
// Destroy the old widget messaging before starting it back up again. Some widgets
|
|
||||||
// have startup routines that run when they are loaded, so we just need to reinitialize
|
|
||||||
// the messaging for them.
|
|
||||||
ActiveWidgetStore.delWidgetMessaging(this.props.app.id);
|
|
||||||
this._setupWidgetMessaging();
|
|
||||||
|
|
||||||
ActiveWidgetStore.setRoomId(this.props.app.id, this.props.room.roomId);
|
|
||||||
this.setState({loading: false});
|
this.setState({loading: false});
|
||||||
}
|
|
||||||
|
|
||||||
_setupWidgetMessaging() {
|
|
||||||
// FIXME: There's probably no reason to do this here: it should probably be done entirely
|
|
||||||
// in ActiveWidgetStore.
|
|
||||||
const widgetMessaging = new WidgetMessaging(
|
|
||||||
this.props.app.id,
|
|
||||||
this.props.app.url,
|
|
||||||
this._getRenderedUrl(),
|
|
||||||
this.props.userWidget,
|
|
||||||
this._appFrame.current.contentWindow,
|
|
||||||
);
|
|
||||||
ActiveWidgetStore.setWidgetMessaging(this.props.app.id, widgetMessaging);
|
|
||||||
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
|
||||||
console.log(`Widget ${this.props.app.id} requested capabilities: ` + requestedCapabilities);
|
|
||||||
requestedCapabilities = requestedCapabilities || [];
|
|
||||||
|
|
||||||
// Allow whitelisted capabilities
|
|
||||||
let requestedWhitelistCapabilies = [];
|
|
||||||
|
|
||||||
if (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) {
|
|
||||||
requestedWhitelistCapabilies = requestedCapabilities.filter(function(e) {
|
|
||||||
return this.indexOf(e)>=0;
|
|
||||||
}, this.props.whitelistCapabilities);
|
|
||||||
|
|
||||||
if (requestedWhitelistCapabilies.length > 0 ) {
|
|
||||||
console.log(`Widget ${this.props.app.id} allowing requested, whitelisted properties: ` +
|
|
||||||
requestedWhitelistCapabilies,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO -- Add UI to warn about and optionally allow requested capabilities
|
|
||||||
|
|
||||||
ActiveWidgetStore.setWidgetCapabilities(this.props.app.id, requestedWhitelistCapabilies);
|
|
||||||
|
|
||||||
if (this.props.onCapabilityRequest) {
|
|
||||||
this.props.onCapabilityRequest(requestedCapabilities);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only tell Jitsi widgets that we're ready because they're realistically the only ones
|
|
||||||
// using this custom extension to the widget API.
|
|
||||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||||
widgetMessaging.flagReadyToContinue();
|
this._sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(`Failed to get capabilities for widget type ${this.props.app.type}`, this.props.app.id, err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
_onAction(payload) {
|
_onAction(payload) {
|
||||||
if (payload.widgetId === this.props.app.id) {
|
if (payload.widgetId === this.props.app.id) {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'm.sticker':
|
case 'm.sticker':
|
||||||
if (this._hasCapability('m.sticker')) {
|
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
||||||
dis.dispatch({action: 'post_sticker_message', data: payload.data});
|
dis.dispatch({action: 'post_sticker_message', data: payload.data});
|
||||||
} else {
|
} else {
|
||||||
console.warn('Ignoring sticker message. Invalid capability');
|
console.warn('Ignoring sticker message. Invalid capability');
|
||||||
|
@ -493,20 +324,6 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set remote content title on AppTile
|
|
||||||
* @param {string} url Url to check for title
|
|
||||||
*/
|
|
||||||
_fetchWidgetTitle(url) {
|
|
||||||
this._scalarClient.getScalarPageTitle(url).then((widgetPageTitle) => {
|
|
||||||
if (widgetPageTitle) {
|
|
||||||
this.setState({widgetPageTitle: widgetPageTitle});
|
|
||||||
}
|
|
||||||
}, (err) =>{
|
|
||||||
console.error("Failed to get page title", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_grantWidgetPermission() {
|
_grantWidgetPermission() {
|
||||||
const roomId = this.props.room.roomId;
|
const roomId = this.props.room.roomId;
|
||||||
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
||||||
|
@ -516,7 +333,7 @@ export default class AppTile extends React.Component {
|
||||||
this.setState({hasPermissionToLoad: true});
|
this.setState({hasPermissionToLoad: true});
|
||||||
|
|
||||||
// Fetch a token for the integration manager, now that we're allowed to
|
// Fetch a token for the integration manager, now that we're allowed to
|
||||||
this.setScalarToken();
|
this._startWidget();
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
// We don't really need to do anything about this - the user will just hit the button again.
|
// We don't really need to do anything about this - the user will just hit the button again.
|
||||||
|
@ -535,6 +352,7 @@ export default class AppTile extends React.Component {
|
||||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
PersistedElement.destroyElement(this._persistKey);
|
||||||
|
this._sgWidget.stop();
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
// We don't really need to do anything about this - the user will just hit the button again.
|
// We don't really need to do anything about this - the user will just hit the button again.
|
||||||
|
@ -572,40 +390,6 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace the widget template variables in a url with their values
|
|
||||||
*
|
|
||||||
* @param {string} u The URL with template variables
|
|
||||||
* @param {string} widgetType The widget's type
|
|
||||||
*
|
|
||||||
* @returns {string} url with temlate variables replaced
|
|
||||||
*/
|
|
||||||
_templatedUrl(u, widgetType: string) {
|
|
||||||
const targetData = {};
|
|
||||||
if (WidgetType.JITSI.matches(widgetType)) {
|
|
||||||
targetData['domain'] = 'jitsi.riot.im'; // v1 jitsi widgets have this hardcoded
|
|
||||||
}
|
|
||||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
|
||||||
const myUser = MatrixClientPeg.get().getUser(myUserId);
|
|
||||||
const vars = Object.assign(targetData, this.props.app.data, {
|
|
||||||
'matrix_user_id': myUserId,
|
|
||||||
'matrix_room_id': this.props.room.roomId,
|
|
||||||
'matrix_display_name': myUser ? myUser.displayName : myUserId,
|
|
||||||
'matrix_avatar_url': myUser ? MatrixClientPeg.get().mxcUrlToHttp(myUser.avatarUrl) : '',
|
|
||||||
|
|
||||||
// TODO: Namespace themes through some standard
|
|
||||||
'theme': SettingsStore.getValue("theme"),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (vars.conferenceId === undefined) {
|
|
||||||
// we'll need to parse the conference ID out of the URL for v1 Jitsi widgets
|
|
||||||
const parsedUrl = new URL(this.props.app.url);
|
|
||||||
vars.conferenceId = parsedUrl.searchParams.get("confId");
|
|
||||||
}
|
|
||||||
|
|
||||||
return uriFromTemplate(u, vars);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether we're using a local version of the widget rather than loading the
|
* Whether we're using a local version of the widget rather than loading the
|
||||||
* actual widget URL
|
* actual widget URL
|
||||||
|
@ -615,67 +399,11 @@ export default class AppTile extends React.Component {
|
||||||
return WidgetType.JITSI.matches(this.props.app.type);
|
return WidgetType.JITSI.matches(this.props.app.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the URL used in the iframe
|
|
||||||
* In cases where we supply our own UI for a widget, this is an internal
|
|
||||||
* URL different to the one used if the widget is popped out to a separate
|
|
||||||
* tab / browser
|
|
||||||
*
|
|
||||||
* @returns {string} url
|
|
||||||
*/
|
|
||||||
_getRenderedUrl() {
|
|
||||||
let url;
|
|
||||||
|
|
||||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
|
||||||
console.log("Replacing Jitsi widget URL with local wrapper");
|
|
||||||
url = WidgetUtils.getLocalJitsiWrapperUrl({
|
|
||||||
forLocalRender: true,
|
|
||||||
auth: this.props.app.data ? this.props.app.data.auth : null,
|
|
||||||
});
|
|
||||||
url = this._addWurlParams(url);
|
|
||||||
} else {
|
|
||||||
url = this._getSafeUrl(this.state.widgetUrl);
|
|
||||||
}
|
|
||||||
return this._templatedUrl(url, this.props.app.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
_getPopoutUrl() {
|
|
||||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
|
||||||
return this._templatedUrl(
|
|
||||||
WidgetUtils.getLocalJitsiWrapperUrl({
|
|
||||||
forLocalRender: false,
|
|
||||||
auth: this.props.app.data ? this.props.app.data.auth : null,
|
|
||||||
}),
|
|
||||||
this.props.app.type,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// use app.url, not state.widgetUrl, because we want the one without
|
|
||||||
// the wURL params for the popped-out version.
|
|
||||||
return this._templatedUrl(this._getSafeUrl(this.props.app.url), this.props.app.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_getSafeUrl(u) {
|
|
||||||
const parsedWidgetUrl = url.parse(u, true);
|
|
||||||
if (ENABLE_REACT_PERF) {
|
|
||||||
parsedWidgetUrl.search = null;
|
|
||||||
parsedWidgetUrl.query.react_perf = true;
|
|
||||||
}
|
|
||||||
let safeWidgetUrl = '';
|
|
||||||
if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol)) {
|
|
||||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace all the dollar signs back to dollar signs as they don't affect HTTP at all.
|
|
||||||
// We also need the dollar signs in-tact for variable substitution.
|
|
||||||
return safeWidgetUrl.replace(/%24/g, '$');
|
|
||||||
}
|
|
||||||
|
|
||||||
_getTileTitle() {
|
_getTileTitle() {
|
||||||
const name = this.formatAppTileName();
|
const name = this.formatAppTileName();
|
||||||
const titleSpacer = <span> - </span>;
|
const titleSpacer = <span> - </span>;
|
||||||
let title = '';
|
let title = '';
|
||||||
if (this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName()) {
|
if (this.state.widgetPageTitle && this.state.widgetPageTitle !== this.formatAppTileName()) {
|
||||||
title = this.state.widgetPageTitle;
|
title = this.state.widgetPageTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -698,9 +426,9 @@ export default class AppTile extends React.Component {
|
||||||
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
|
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
|
||||||
if (WidgetType.JITSI.matches(this.props.app.type) && this.props.show) {
|
if (WidgetType.JITSI.matches(this.props.app.type) && this.props.show) {
|
||||||
this._endWidgetActions().then(() => {
|
this._endWidgetActions().then(() => {
|
||||||
if (this._appFrame.current) {
|
if (this.iframe) {
|
||||||
// Reload iframe
|
// Reload iframe
|
||||||
this._appFrame.current.src = this._getRenderedUrl();
|
this.iframe.src = this._sgWidget.embedUrl;
|
||||||
this.setState({});
|
this.setState({});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -708,13 +436,13 @@ export default class AppTile extends React.Component {
|
||||||
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
||||||
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
||||||
Object.assign(document.createElement('a'),
|
Object.assign(document.createElement('a'),
|
||||||
{ target: '_blank', href: this._getPopoutUrl(), rel: 'noreferrer noopener'}).click();
|
{ target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener'}).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onReloadWidgetClick() {
|
_onReloadWidgetClick() {
|
||||||
// Reload iframe in this way to avoid cross-origin restrictions
|
// Reload iframe in this way to avoid cross-origin restrictions
|
||||||
// eslint-disable-next-line no-self-assign
|
// eslint-disable-next-line no-self-assign
|
||||||
this._appFrame.current.src = this._appFrame.current.src;
|
this.iframe.src = this.iframe.src;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onContextMenuClick = () => {
|
_onContextMenuClick = () => {
|
||||||
|
@ -760,7 +488,7 @@ export default class AppTile extends React.Component {
|
||||||
<AppPermission
|
<AppPermission
|
||||||
roomId={this.props.room.roomId}
|
roomId={this.props.room.roomId}
|
||||||
creatorUserId={this.props.creatorUserId}
|
creatorUserId={this.props.creatorUserId}
|
||||||
url={this.state.widgetUrl}
|
url={this._sgWidget.embedUrl}
|
||||||
isRoomEncrypted={isEncrypted}
|
isRoomEncrypted={isEncrypted}
|
||||||
onPermissionGranted={this._grantWidgetPermission}
|
onPermissionGranted={this._grantWidgetPermission}
|
||||||
/>
|
/>
|
||||||
|
@ -785,11 +513,11 @@ export default class AppTile extends React.Component {
|
||||||
{ this.state.loading && loadingElement }
|
{ this.state.loading && loadingElement }
|
||||||
<iframe
|
<iframe
|
||||||
allow={iframeFeatures}
|
allow={iframeFeatures}
|
||||||
ref={this._appFrame}
|
ref={this._iframeRefChange}
|
||||||
src={this._getRenderedUrl()}
|
src={this._sgWidget.embedUrl}
|
||||||
allowFullScreen={true}
|
allowFullScreen={true}
|
||||||
sandbox={sandboxFlags}
|
sandbox={sandboxFlags}
|
||||||
onLoad={this._onLoaded} />
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
// if the widget would be allowed to remain on screen, we must put it in
|
// if the widget would be allowed to remain on screen, we must put it in
|
||||||
|
@ -833,9 +561,10 @@ export default class AppTile extends React.Component {
|
||||||
const elementRect = this._contextMenuButton.current.getBoundingClientRect();
|
const elementRect = this._contextMenuButton.current.getBoundingClientRect();
|
||||||
|
|
||||||
const canUserModify = this._canUserModify();
|
const canUserModify = this._canUserModify();
|
||||||
const showEditButton = Boolean(this._scalarClient && canUserModify);
|
const showEditButton = Boolean(this._sgWidget.isManagedByManager && canUserModify);
|
||||||
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
|
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
|
||||||
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
|
const showPictureSnapshotButton = this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.Screenshots)
|
||||||
|
&& this.props.show;
|
||||||
|
|
||||||
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
|
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
|
@ -943,9 +672,6 @@ AppTile.propTypes = {
|
||||||
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
||||||
// basic widget capabilities, e.g. injecting sticker message events.
|
// basic widget capabilities, e.g. injecting sticker message events.
|
||||||
whitelistCapabilities: PropTypes.array,
|
whitelistCapabilities: PropTypes.array,
|
||||||
// Optional function to be called on widget capability request
|
|
||||||
// Called with an array of the requested capabilities
|
|
||||||
onCapabilityRequest: PropTypes.func,
|
|
||||||
// Is this an instance of a user widget
|
// Is this an instance of a user widget
|
||||||
userWidget: PropTypes.bool,
|
userWidget: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,16 +29,17 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
import WidgetStore from "../../../stores/WidgetStore";
|
import WidgetStore from "../../../stores/WidgetStore";
|
||||||
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
|
|
||||||
import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
|
import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
|
||||||
import IconizedContextMenu, {
|
import IconizedContextMenu, {
|
||||||
IconizedContextMenuOption,
|
IconizedContextMenuOption,
|
||||||
IconizedContextMenuOptionList,
|
IconizedContextMenuOptionList,
|
||||||
} from "../context_menus/IconizedContextMenu";
|
} from "../context_menus/IconizedContextMenu";
|
||||||
import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload";
|
import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload";
|
||||||
import {Capability} from "../../../widgets/WidgetApi";
|
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
|
||||||
|
import { MatrixCapabilities } from "matrix-widget-api";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -77,9 +78,17 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||||
let contextMenu;
|
let contextMenu;
|
||||||
if (menuDisplayed) {
|
if (menuDisplayed) {
|
||||||
let snapshotButton;
|
let snapshotButton;
|
||||||
if (ActiveWidgetStore.widgetHasCapability(app.id, Capability.Screenshot)) {
|
const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id);
|
||||||
|
if (widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) {
|
||||||
const onSnapshotClick = () => {
|
const onSnapshotClick = () => {
|
||||||
WidgetUtils.snapshotWidget(app);
|
widgetMessaging.takeScreenshot().then(data => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'picture_snapshot',
|
||||||
|
file: data.screenshot,
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Failed to take screenshot: ", err);
|
||||||
|
});
|
||||||
closeMenu();
|
closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@ import * as sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
|
||||||
import PersistedElement from "../elements/PersistedElement";
|
import PersistedElement from "../elements/PersistedElement";
|
||||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
@ -30,6 +29,7 @@ import {ContextMenu} from "../../structures/ContextMenu";
|
||||||
import {WidgetType} from "../../../widgets/WidgetType";
|
import {WidgetType} from "../../../widgets/WidgetType";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
|
import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore";
|
||||||
|
|
||||||
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
|
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
|
||||||
// We sit in a context menu, so this should be given to the context menu.
|
// We sit in a context menu, so this should be given to the context menu.
|
||||||
|
@ -212,9 +212,11 @@ export default class Stickerpicker extends React.Component {
|
||||||
|
|
||||||
_sendVisibilityToWidget(visible) {
|
_sendVisibilityToWidget(visible) {
|
||||||
if (!this.state.stickerpickerWidget) return;
|
if (!this.state.stickerpickerWidget) return;
|
||||||
const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(this.state.stickerpickerWidget.id);
|
const messaging = WidgetMessagingStore.instance.getMessagingForId(this.state.stickerpickerWidget.id);
|
||||||
if (widgetMessaging && visible !== this._prevSentVisibility) {
|
if (messaging && visible !== this._prevSentVisibility) {
|
||||||
widgetMessaging.sendVisibility(visible);
|
messaging.updateVisibility(visible).catch(err => {
|
||||||
|
console.error("Error updating widget visibility: ", err);
|
||||||
|
});
|
||||||
this._prevSentVisibility = visible;
|
this._prevSentVisibility = visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||||
|
import {WidgetMessagingStore} from "./widgets/WidgetMessagingStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores information about the widgets active in the app right now:
|
* Stores information about the widgets active in the app right now:
|
||||||
|
@ -29,15 +30,6 @@ class ActiveWidgetStore extends EventEmitter {
|
||||||
super();
|
super();
|
||||||
this._persistentWidgetId = null;
|
this._persistentWidgetId = null;
|
||||||
|
|
||||||
// A list of negotiated capabilities for each widget, by ID
|
|
||||||
// {
|
|
||||||
// widgetId: [caps...],
|
|
||||||
// }
|
|
||||||
this._capsByWidgetId = {};
|
|
||||||
|
|
||||||
// A WidgetMessaging instance for each widget ID
|
|
||||||
this._widgetMessagingByWidgetId = {};
|
|
||||||
|
|
||||||
// What room ID each widget is associated with (if it's a room widget)
|
// What room ID each widget is associated with (if it's a room widget)
|
||||||
this._roomIdByWidgetId = {};
|
this._roomIdByWidgetId = {};
|
||||||
|
|
||||||
|
@ -54,8 +46,6 @@ class ActiveWidgetStore extends EventEmitter {
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
|
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
|
||||||
}
|
}
|
||||||
this._capsByWidgetId = {};
|
|
||||||
this._widgetMessagingByWidgetId = {};
|
|
||||||
this._roomIdByWidgetId = {};
|
this._roomIdByWidgetId = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,9 +66,9 @@ class ActiveWidgetStore extends EventEmitter {
|
||||||
if (id !== this._persistentWidgetId) return;
|
if (id !== this._persistentWidgetId) return;
|
||||||
const toDeleteId = this._persistentWidgetId;
|
const toDeleteId = this._persistentWidgetId;
|
||||||
|
|
||||||
|
WidgetMessagingStore.instance.stopMessagingById(id);
|
||||||
|
|
||||||
this.setWidgetPersistence(toDeleteId, false);
|
this.setWidgetPersistence(toDeleteId, false);
|
||||||
this.delWidgetMessaging(toDeleteId);
|
|
||||||
this.delWidgetCapabilities(toDeleteId);
|
|
||||||
this.delRoomId(toDeleteId);
|
this.delRoomId(toDeleteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,43 +89,6 @@ class ActiveWidgetStore extends EventEmitter {
|
||||||
return this._persistentWidgetId;
|
return this._persistentWidgetId;
|
||||||
}
|
}
|
||||||
|
|
||||||
setWidgetCapabilities(widgetId, caps) {
|
|
||||||
this._capsByWidgetId[widgetId] = caps;
|
|
||||||
this.emit('update');
|
|
||||||
}
|
|
||||||
|
|
||||||
widgetHasCapability(widgetId, cap) {
|
|
||||||
return this._capsByWidgetId[widgetId] && this._capsByWidgetId[widgetId].includes(cap);
|
|
||||||
}
|
|
||||||
|
|
||||||
delWidgetCapabilities(widgetId) {
|
|
||||||
delete this._capsByWidgetId[widgetId];
|
|
||||||
this.emit('update');
|
|
||||||
}
|
|
||||||
|
|
||||||
setWidgetMessaging(widgetId, wm) {
|
|
||||||
// Stop any existing widget messaging first
|
|
||||||
this.delWidgetMessaging(widgetId);
|
|
||||||
this._widgetMessagingByWidgetId[widgetId] = wm;
|
|
||||||
this.emit('update');
|
|
||||||
}
|
|
||||||
|
|
||||||
getWidgetMessaging(widgetId) {
|
|
||||||
return this._widgetMessagingByWidgetId[widgetId];
|
|
||||||
}
|
|
||||||
|
|
||||||
delWidgetMessaging(widgetId) {
|
|
||||||
if (this._widgetMessagingByWidgetId[widgetId]) {
|
|
||||||
try {
|
|
||||||
this._widgetMessagingByWidgetId[widgetId].stop();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to stop listening for widgetMessaging events', e.message);
|
|
||||||
}
|
|
||||||
delete this._widgetMessagingByWidgetId[widgetId];
|
|
||||||
this.emit('update');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getRoomId(widgetId) {
|
getRoomId(widgetId) {
|
||||||
return this._roomIdByWidgetId[widgetId];
|
return this._roomIdByWidgetId[widgetId];
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,12 +66,14 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
|
||||||
/**
|
/**
|
||||||
* Gets the user's avatar as an HTTP URL of the given size. If the user's
|
* Gets the user's avatar as an HTTP URL of the given size. If the user's
|
||||||
* avatar is not present, this returns null.
|
* avatar is not present, this returns null.
|
||||||
* @param size The size of the avatar
|
* @param size The size of the avatar. If zero, a full res copy of the avatar
|
||||||
|
* will be returned as an HTTP URL.
|
||||||
* @returns The HTTP URL of the user's avatar
|
* @returns The HTTP URL of the user's avatar
|
||||||
*/
|
*/
|
||||||
public getHttpAvatarUrl(size: number): string {
|
public getHttpAvatarUrl(size = 0): string {
|
||||||
if (!this.avatarMxc) return null;
|
if (!this.avatarMxc) return null;
|
||||||
return this.matrixClient.mxcUrlToHttp(this.avatarMxc, size, size);
|
const adjustedSize = size > 1 ? size : undefined; // don't let negatives or zero through
|
||||||
|
return this.matrixClient.mxcUrlToHttp(this.avatarMxc, adjustedSize, adjustedSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onNotReady() {
|
protected async onNotReady() {
|
||||||
|
|
21
src/stores/widgets/ElementWidgetActions.ts
Normal file
21
src/stores/widgets/ElementWidgetActions.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum ElementWidgetActions {
|
||||||
|
ClientReady = "im.vector.ready",
|
||||||
|
HangupCall = "im.vector.hangup",
|
||||||
|
OpenIntegrationManager = "integration_manager_open",
|
||||||
|
}
|
266
src/stores/widgets/StopGapWidget.ts
Normal file
266
src/stores/widgets/StopGapWidget.ts
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import {
|
||||||
|
ClientWidgetApi,
|
||||||
|
IStickerActionRequest,
|
||||||
|
IStickyActionRequest,
|
||||||
|
IWidget,
|
||||||
|
IWidgetApiRequest,
|
||||||
|
IWidgetApiRequestEmptyData,
|
||||||
|
IWidgetData,
|
||||||
|
MatrixCapabilities,
|
||||||
|
Widget,
|
||||||
|
WidgetApiFromWidgetAction,
|
||||||
|
} from "matrix-widget-api";
|
||||||
|
import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { WidgetMessagingStore } from "./WidgetMessagingStore";
|
||||||
|
import RoomViewStore from "../RoomViewStore";
|
||||||
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
|
import { OwnProfileStore } from "../OwnProfileStore";
|
||||||
|
import WidgetUtils from '../../utils/WidgetUtils';
|
||||||
|
import { IntegrationManagers } from "../../integrations/IntegrationManagers";
|
||||||
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
import { WidgetType } from "../../widgets/WidgetType";
|
||||||
|
import ActiveWidgetStore from "../ActiveWidgetStore";
|
||||||
|
import { objectShallowClone } from "../../utils/objects";
|
||||||
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
|
import { ElementWidgetActions } from "./ElementWidgetActions";
|
||||||
|
|
||||||
|
// TODO: Destroy all of this code
|
||||||
|
|
||||||
|
interface IAppTileProps {
|
||||||
|
// Note: these are only the props we care about
|
||||||
|
|
||||||
|
app: IWidget;
|
||||||
|
room: Room;
|
||||||
|
userId: string;
|
||||||
|
creatorUserId: string;
|
||||||
|
waitForIframeLoad: boolean;
|
||||||
|
whitelistCapabilities: string[];
|
||||||
|
userWidget: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Don't use this because it's wrong
|
||||||
|
class ElementWidget extends Widget {
|
||||||
|
constructor(w) {
|
||||||
|
super(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get templateUrl(): string {
|
||||||
|
if (WidgetType.JITSI.matches(this.type)) {
|
||||||
|
return WidgetUtils.getLocalJitsiWrapperUrl({
|
||||||
|
forLocalRender: true,
|
||||||
|
auth: this.rawData?.auth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return super.templateUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get rawData(): IWidgetData {
|
||||||
|
let conferenceId = super.rawData['conferenceId'];
|
||||||
|
if (conferenceId === undefined) {
|
||||||
|
// we'll need to parse the conference ID out of the URL for v1 Jitsi widgets
|
||||||
|
const parsedUrl = new URL(this.templateUrl);
|
||||||
|
conferenceId = parsedUrl.searchParams.get("confId");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...super.rawData,
|
||||||
|
theme: SettingsStore.getValue("theme"),
|
||||||
|
conferenceId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StopGapWidget extends EventEmitter {
|
||||||
|
private messaging: ClientWidgetApi;
|
||||||
|
private mockWidget: Widget;
|
||||||
|
private scalarToken: string;
|
||||||
|
|
||||||
|
constructor(private appTileProps: IAppTileProps) {
|
||||||
|
super();
|
||||||
|
let app = appTileProps.app;
|
||||||
|
|
||||||
|
// Backwards compatibility: not all old widgets have a creatorUserId
|
||||||
|
if (!app.creatorUserId) {
|
||||||
|
app = objectShallowClone(app); // clone to prevent accidental mutation
|
||||||
|
app.creatorUserId = MatrixClientPeg.get().getUserId();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mockWidget = new ElementWidget(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get widgetApi(): ClientWidgetApi {
|
||||||
|
return this.messaging;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to use in the iframe
|
||||||
|
*/
|
||||||
|
public get embedUrl(): string {
|
||||||
|
const templated = this.mockWidget.getCompleteUrl({
|
||||||
|
currentRoomId: RoomViewStore.getRoomId(),
|
||||||
|
currentUserId: MatrixClientPeg.get().getUserId(),
|
||||||
|
userDisplayName: OwnProfileStore.instance.displayName,
|
||||||
|
userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add in some legacy support sprinkles
|
||||||
|
// TODO: Replace these with proper widget params
|
||||||
|
// See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
|
||||||
|
const parsed = new URL(templated);
|
||||||
|
parsed.searchParams.set('widgetId', this.mockWidget.id);
|
||||||
|
parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
|
||||||
|
|
||||||
|
// Give the widget a scalar token if we're supposed to (more legacy)
|
||||||
|
// TODO: Stop doing this
|
||||||
|
if (this.scalarToken) {
|
||||||
|
parsed.searchParams.set('scalar_token', this.scalarToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the encoded dollar signs back to dollar signs. They have no special meaning
|
||||||
|
// in HTTP, but URL parsers encode them anyways.
|
||||||
|
return parsed.toString().replace(/%24/g, '$');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to use in the popout
|
||||||
|
*/
|
||||||
|
public get popoutUrl(): string {
|
||||||
|
if (WidgetType.JITSI.matches(this.mockWidget.type)) {
|
||||||
|
return WidgetUtils.getLocalJitsiWrapperUrl({
|
||||||
|
forLocalRender: false,
|
||||||
|
auth: this.mockWidget.rawData?.auth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.embedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isManagedByManager(): boolean {
|
||||||
|
return !!this.scalarToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get started(): boolean {
|
||||||
|
return !!this.messaging;
|
||||||
|
}
|
||||||
|
|
||||||
|
public start(iframe: HTMLIFrameElement) {
|
||||||
|
if (this.started) return;
|
||||||
|
const driver = new StopGapWidgetDriver( this.appTileProps.whitelistCapabilities || []);
|
||||||
|
this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
|
||||||
|
this.messaging.addEventListener("ready", () => this.emit("ready"));
|
||||||
|
WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);
|
||||||
|
|
||||||
|
if (!this.appTileProps.userWidget && this.appTileProps.room) {
|
||||||
|
ActiveWidgetStore.setRoomId(this.mockWidget.id, this.appTileProps.room.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WidgetType.JITSI.matches(this.mockWidget.type)) {
|
||||||
|
this.messaging.addEventListener("action:set_always_on_screen",
|
||||||
|
(ev: CustomEvent<IStickyActionRequest>) => {
|
||||||
|
if (this.messaging.hasCapability(MatrixCapabilities.AlwaysOnScreen)) {
|
||||||
|
ActiveWidgetStore.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value);
|
||||||
|
ev.preventDefault();
|
||||||
|
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (WidgetType.STICKERPICKER.matches(this.mockWidget.type)) {
|
||||||
|
this.messaging.addEventListener(`action:${ElementWidgetActions.OpenIntegrationManager}`,
|
||||||
|
(ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
|
// Acknowledge first
|
||||||
|
ev.preventDefault();
|
||||||
|
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{});
|
||||||
|
|
||||||
|
// First close the stickerpicker
|
||||||
|
defaultDispatcher.dispatch({action: "stickerpicker_close"});
|
||||||
|
|
||||||
|
// Now open the integration manager
|
||||||
|
// TODO: Spec this interaction.
|
||||||
|
const data = ev.detail.data;
|
||||||
|
const integType = data?.integType
|
||||||
|
const integId = <string>data?.integId;
|
||||||
|
|
||||||
|
// TODO: Open the right integration manager for the widget
|
||||||
|
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||||
|
IntegrationManagers.sharedInstance().openAll(
|
||||||
|
MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
|
||||||
|
`type_${integType}`,
|
||||||
|
integId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
IntegrationManagers.sharedInstance().getPrimaryManager().open(
|
||||||
|
MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
|
||||||
|
`type_${integType}`,
|
||||||
|
integId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Replace this event listener with appropriate driver functionality once the API
|
||||||
|
// establishes a sane way to send events back and forth.
|
||||||
|
this.messaging.addEventListener(`action:${WidgetApiFromWidgetAction.SendSticker}`,
|
||||||
|
(ev: CustomEvent<IStickerActionRequest>) => {
|
||||||
|
// Acknowledge first
|
||||||
|
ev.preventDefault();
|
||||||
|
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{});
|
||||||
|
|
||||||
|
// Send the sticker
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: 'm.sticker',
|
||||||
|
data: ev.detail.data,
|
||||||
|
widgetId: this.mockWidget.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async prepare(): Promise<void> {
|
||||||
|
if (this.scalarToken) return;
|
||||||
|
const existingMessaging = WidgetMessagingStore.instance.getMessaging(this.mockWidget);
|
||||||
|
if (existingMessaging) this.messaging = existingMessaging;
|
||||||
|
try {
|
||||||
|
if (WidgetUtils.isScalarUrl(this.mockWidget.templateUrl)) {
|
||||||
|
const managers = IntegrationManagers.sharedInstance();
|
||||||
|
if (managers.hasManager()) {
|
||||||
|
// TODO: Pick the right manager for the widget
|
||||||
|
const defaultManager = managers.getPrimaryManager();
|
||||||
|
if (WidgetUtils.isScalarUrl(defaultManager.apiUrl)) {
|
||||||
|
const scalar = defaultManager.getScalarClient();
|
||||||
|
this.scalarToken = await scalar.getScalarToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// All errors are non-fatal
|
||||||
|
console.error("Error preparing widget communications: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop() {
|
||||||
|
if (ActiveWidgetStore.getPersistentWidgetId() === this.mockWidget.id) {
|
||||||
|
console.log("Skipping destroy - persistent widget");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.started) return;
|
||||||
|
WidgetMessagingStore.instance.stopMessaging(this.mockWidget);
|
||||||
|
ActiveWidgetStore.delRoomId(this.mockWidget.id);
|
||||||
|
}
|
||||||
|
}
|
30
src/stores/widgets/StopGapWidgetDriver.ts
Normal file
30
src/stores/widgets/StopGapWidgetDriver.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 { Capability, WidgetDriver } from "matrix-widget-api";
|
||||||
|
import { iterableUnion } from "../../utils/iterables";
|
||||||
|
|
||||||
|
// TODO: Purge this from the universe
|
||||||
|
|
||||||
|
export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
|
constructor(private allowedCapabilities: Capability[]) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async validateCapabilities(requested: Set<Capability>): Promise<Set<Capability>> {
|
||||||
|
return new Set(iterableUnion(requested, this.allowedCapabilities));
|
||||||
|
}
|
||||||
|
}
|
82
src/stores/widgets/WidgetMessagingStore.ts
Normal file
82
src/stores/widgets/WidgetMessagingStore.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 { ClientWidgetApi, Widget } from "matrix-widget-api";
|
||||||
|
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||||
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
import { EnhancedMap } from "../../utils/maps";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temporary holding store for widget messaging instances. This is eventually
|
||||||
|
* going to be merged with a more complete WidgetStore, but for now it's
|
||||||
|
* easiest to split this into a single place.
|
||||||
|
*/
|
||||||
|
export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
||||||
|
private static internalInstance = new WidgetMessagingStore();
|
||||||
|
|
||||||
|
// TODO: Fix uniqueness problem (widget IDs are not unique across the whole app)
|
||||||
|
private widgetMap = new EnhancedMap<string, ClientWidgetApi>(); // <widget ID, ClientWidgetAPi>
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
super(defaultDispatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static get instance(): WidgetMessagingStore {
|
||||||
|
return WidgetMessagingStore.internalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onAction(payload: ActionPayload): Promise<any> {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onReady(): Promise<any> {
|
||||||
|
// just in case
|
||||||
|
this.widgetMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public storeMessaging(widget: Widget, widgetApi: ClientWidgetApi) {
|
||||||
|
this.stopMessaging(widget);
|
||||||
|
this.widgetMap.set(widget.id, widgetApi);
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopMessaging(widget: Widget) {
|
||||||
|
this.widgetMap.remove(widget.id)?.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMessaging(widget: Widget): ClientWidgetApi {
|
||||||
|
return this.widgetMap.get(widget.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the widget messaging instance for a given widget ID.
|
||||||
|
* @param {string} widgetId The widget ID.
|
||||||
|
* @deprecated Widget IDs are not globally unique.
|
||||||
|
*/
|
||||||
|
public stopMessagingById(widgetId: string) {
|
||||||
|
this.widgetMap.remove(widgetId)?.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the widget messaging class for a given widget ID.
|
||||||
|
* @param {string} widgetId The widget ID.
|
||||||
|
* @returns {ClientWidgetApi} The widget API, or a falsey value if not found.
|
||||||
|
* @deprecated Widget IDs are not globally unique.
|
||||||
|
*/
|
||||||
|
public getMessagingForId(widgetId: string): ClientWidgetApi {
|
||||||
|
return this.widgetMap.get(widgetId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,11 +28,11 @@ const WIDGET_WAIT_TIME = 20000;
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
||||||
import {IntegrationManagers} from "../integrations/IntegrationManagers";
|
import {IntegrationManagers} from "../integrations/IntegrationManagers";
|
||||||
import {Capability} from "../widgets/WidgetApi";
|
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
import {WidgetType} from "../widgets/WidgetType";
|
import {WidgetType} from "../widgets/WidgetType";
|
||||||
import {objectClone} from "./objects";
|
import {objectClone} from "./objects";
|
||||||
import {_t} from "../languageHandler";
|
import {_t} from "../languageHandler";
|
||||||
|
import {MatrixCapabilities} from "matrix-widget-api";
|
||||||
|
|
||||||
export default class WidgetUtils {
|
export default class WidgetUtils {
|
||||||
/* Returns true if user is able to send state events to modify widgets in this room
|
/* Returns true if user is able to send state events to modify widgets in this room
|
||||||
|
@ -416,15 +416,14 @@ export default class WidgetUtils {
|
||||||
static getCapWhitelistForAppTypeInRoomId(appType, roomId) {
|
static getCapWhitelistForAppTypeInRoomId(appType, roomId) {
|
||||||
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId);
|
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId);
|
||||||
|
|
||||||
const capWhitelist = enableScreenshots ? [Capability.Screenshot] : [];
|
const capWhitelist = enableScreenshots ? [MatrixCapabilities.Screenshots] : [];
|
||||||
|
|
||||||
// Obviously anyone that can add a widget can claim it's a jitsi widget,
|
// Obviously anyone that can add a widget can claim it's a jitsi widget,
|
||||||
// so this doesn't really offer much over the set of domains we load
|
// so this doesn't really offer much over the set of domains we load
|
||||||
// widgets from at all, but it probably makes sense for sanity.
|
// widgets from at all, but it probably makes sense for sanity.
|
||||||
if (WidgetType.JITSI.matches(appType)) {
|
if (WidgetType.JITSI.matches(appType)) {
|
||||||
capWhitelist.push(Capability.AlwaysOnScreen);
|
capWhitelist.push(MatrixCapabilities.AlwaysOnScreen);
|
||||||
}
|
}
|
||||||
capWhitelist.push(Capability.ReceiveTerminate);
|
|
||||||
|
|
||||||
return capWhitelist;
|
return capWhitelist;
|
||||||
}
|
}
|
||||||
|
@ -495,16 +494,4 @@ export default class WidgetUtils {
|
||||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(room, 'type_' + app.type, app.id);
|
IntegrationManagers.sharedInstance().getPrimaryManager().open(room, 'type_' + app.type, app.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static snapshotWidget(app) {
|
|
||||||
console.log("Requesting widget snapshot");
|
|
||||||
ActiveWidgetStore.getWidgetMessaging(app.id).getScreenshot().catch((err) => {
|
|
||||||
console.error("Failed to get screenshot", err);
|
|
||||||
}).then((screenshot) => {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'picture_snapshot',
|
|
||||||
file: screenshot,
|
|
||||||
}, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
21
src/utils/iterables.ts
Normal file
21
src/utils/iterables.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 { arrayUnion } from "./arrays";
|
||||||
|
|
||||||
|
export function iterableUnion<T>(a: Iterable<T>, b: Iterable<T>): Iterable<T> {
|
||||||
|
return arrayUnion(Array.from(a), Array.from(b));
|
||||||
|
}
|
|
@ -44,3 +44,26 @@ export function mapKeyChanges<K, V>(a: Map<K, V>, b: Map<K, V>): K[] {
|
||||||
const diff = mapDiff(a, b);
|
const diff = mapDiff(a, b);
|
||||||
return arrayMerge(diff.removed, diff.added, diff.changed);
|
return arrayMerge(diff.removed, diff.added, diff.changed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Map<K, V> with added utility.
|
||||||
|
*/
|
||||||
|
export class EnhancedMap<K, V> extends Map<K, V> {
|
||||||
|
public constructor(entries?: Iterable<[K, V]>) {
|
||||||
|
super(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getOrCreate(key: K, def: V): V {
|
||||||
|
if (this.has(key)) {
|
||||||
|
return this.get(key);
|
||||||
|
}
|
||||||
|
this.set(key, def);
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
public remove(key: K): V {
|
||||||
|
const v = this.get(key);
|
||||||
|
this.delete(key);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,223 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
|
||||||
import { EventEmitter } from "events";
|
|
||||||
import { objectClone } from "../utils/objects";
|
|
||||||
|
|
||||||
export enum Capability {
|
|
||||||
Screenshot = "m.capability.screenshot",
|
|
||||||
Sticker = "m.sticker",
|
|
||||||
AlwaysOnScreen = "m.always_on_screen",
|
|
||||||
ReceiveTerminate = "im.vector.receive_terminate",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum KnownWidgetActions {
|
|
||||||
GetSupportedApiVersions = "supported_api_versions",
|
|
||||||
TakeScreenshot = "screenshot",
|
|
||||||
GetCapabilities = "capabilities",
|
|
||||||
SendEvent = "send_event",
|
|
||||||
UpdateVisibility = "visibility",
|
|
||||||
GetOpenIDCredentials = "get_openid",
|
|
||||||
ReceiveOpenIDCredentials = "openid_credentials",
|
|
||||||
SetAlwaysOnScreen = "set_always_on_screen",
|
|
||||||
ClientReady = "im.vector.ready",
|
|
||||||
Terminate = "im.vector.terminate",
|
|
||||||
Hangup = "im.vector.hangup",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WidgetAction = KnownWidgetActions | string;
|
|
||||||
|
|
||||||
export enum WidgetApiType {
|
|
||||||
ToWidget = "toWidget",
|
|
||||||
FromWidget = "fromWidget",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WidgetRequest {
|
|
||||||
api: WidgetApiType;
|
|
||||||
widgetId: string;
|
|
||||||
requestId: string;
|
|
||||||
data: any;
|
|
||||||
action: WidgetAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToWidgetRequest extends WidgetRequest {
|
|
||||||
api: WidgetApiType.ToWidget;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FromWidgetRequest extends WidgetRequest {
|
|
||||||
api: WidgetApiType.FromWidget;
|
|
||||||
response: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OpenIDCredentials {
|
|
||||||
accessToken: string;
|
|
||||||
tokenType: string;
|
|
||||||
matrixServerName: string;
|
|
||||||
expiresIn: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles Element <--> Widget interactions for embedded/standalone widgets.
|
|
||||||
*
|
|
||||||
* Emitted events:
|
|
||||||
* - terminate(wait): client requested the widget to terminate.
|
|
||||||
* Call the argument 'wait(promise)' to postpone the finalization until
|
|
||||||
* the given promise resolves.
|
|
||||||
*/
|
|
||||||
export class WidgetApi extends EventEmitter {
|
|
||||||
private readonly origin: string;
|
|
||||||
private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {};
|
|
||||||
private readonly readyPromise: Promise<any>;
|
|
||||||
private readyPromiseResolve: () => void;
|
|
||||||
private openIDCredentialsCallback: () => void;
|
|
||||||
public openIDCredentials: OpenIDCredentials;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set this to true if your widget is expecting a ready message from the client. False otherwise (default).
|
|
||||||
*/
|
|
||||||
public expectingExplicitReady = false;
|
|
||||||
|
|
||||||
constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.origin = new URL(currentUrl).origin;
|
|
||||||
|
|
||||||
this.readyPromise = new Promise<any>(resolve => this.readyPromiseResolve = resolve);
|
|
||||||
|
|
||||||
window.addEventListener("message", event => {
|
|
||||||
if (event.origin !== this.origin) return; // ignore: invalid origin
|
|
||||||
if (!event.data) return; // invalid schema
|
|
||||||
if (event.data.widgetId !== this.widgetId) return; // not for us
|
|
||||||
|
|
||||||
const payload = <WidgetRequest>event.data;
|
|
||||||
if (payload.api === WidgetApiType.ToWidget && payload.action) {
|
|
||||||
console.log(`[WidgetAPI] Got request: ${JSON.stringify(payload)}`);
|
|
||||||
|
|
||||||
if (payload.action === KnownWidgetActions.GetCapabilities) {
|
|
||||||
this.onCapabilitiesRequest(<ToWidgetRequest>payload);
|
|
||||||
if (!this.expectingExplicitReady) {
|
|
||||||
this.readyPromiseResolve();
|
|
||||||
}
|
|
||||||
} else if (payload.action === KnownWidgetActions.ClientReady) {
|
|
||||||
this.readyPromiseResolve();
|
|
||||||
|
|
||||||
// Automatically acknowledge so we can move on
|
|
||||||
this.replyToRequest(<ToWidgetRequest>payload, {});
|
|
||||||
} else if (payload.action === KnownWidgetActions.Terminate
|
|
||||||
|| payload.action === KnownWidgetActions.Hangup) {
|
|
||||||
// Finalization needs to be async, so postpone with a promise
|
|
||||||
let finalizePromise = Promise.resolve();
|
|
||||||
const wait = (promise) => {
|
|
||||||
finalizePromise = finalizePromise.then(() => promise);
|
|
||||||
};
|
|
||||||
const emitName = payload.action === KnownWidgetActions.Terminate ? 'terminate' : 'hangup';
|
|
||||||
this.emit(emitName, wait);
|
|
||||||
Promise.resolve(finalizePromise).then(() => {
|
|
||||||
// Acknowledge that we're shut down now
|
|
||||||
this.replyToRequest(<ToWidgetRequest>payload, {});
|
|
||||||
});
|
|
||||||
} else if (payload.action === KnownWidgetActions.ReceiveOpenIDCredentials) {
|
|
||||||
// Save OpenID credentials
|
|
||||||
this.setOpenIDCredentials(<ToWidgetRequest>payload);
|
|
||||||
this.replyToRequest(<ToWidgetRequest>payload, {});
|
|
||||||
} else {
|
|
||||||
console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
|
|
||||||
}
|
|
||||||
} else if (payload.api === WidgetApiType.FromWidget && this.inFlightRequests[payload.requestId]) {
|
|
||||||
console.log(`[WidgetAPI] Got reply: ${JSON.stringify(payload)}`);
|
|
||||||
const handler = this.inFlightRequests[payload.requestId];
|
|
||||||
delete this.inFlightRequests[payload.requestId];
|
|
||||||
handler(<FromWidgetRequest>payload);
|
|
||||||
} else {
|
|
||||||
console.warn(`[WidgetAPI] Unhandled payload: ${JSON.stringify(payload)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public setOpenIDCredentials(value: WidgetRequest) {
|
|
||||||
const data = value.data;
|
|
||||||
if (data.state === 'allowed') {
|
|
||||||
this.openIDCredentials = {
|
|
||||||
accessToken: data.access_token,
|
|
||||||
tokenType: data.token_type,
|
|
||||||
matrixServerName: data.matrix_server_name,
|
|
||||||
expiresIn: data.expires_in,
|
|
||||||
}
|
|
||||||
} else if (data.state === 'blocked') {
|
|
||||||
this.openIDCredentials = null;
|
|
||||||
}
|
|
||||||
if (['allowed', 'blocked'].includes(data.state) && this.openIDCredentialsCallback) {
|
|
||||||
this.openIDCredentialsCallback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public requestOpenIDCredentials(credentialsResponseCallback: () => void) {
|
|
||||||
this.openIDCredentialsCallback = credentialsResponseCallback;
|
|
||||||
this.callAction(
|
|
||||||
KnownWidgetActions.GetOpenIDCredentials,
|
|
||||||
{},
|
|
||||||
this.setOpenIDCredentials,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public waitReady(): Promise<any> {
|
|
||||||
return this.readyPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private replyToRequest(payload: ToWidgetRequest, reply: any) {
|
|
||||||
if (!window.parent) return;
|
|
||||||
|
|
||||||
const request: ToWidgetRequest & {response?: any} = objectClone(payload);
|
|
||||||
request.response = reply;
|
|
||||||
|
|
||||||
window.parent.postMessage(request, this.origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onCapabilitiesRequest(payload: ToWidgetRequest) {
|
|
||||||
return this.replyToRequest(payload, {capabilities: this.requestedCapabilities});
|
|
||||||
}
|
|
||||||
|
|
||||||
public callAction(action: WidgetAction, payload: any, callback: (reply: FromWidgetRequest) => void) {
|
|
||||||
if (!window.parent) return;
|
|
||||||
|
|
||||||
const request: FromWidgetRequest = {
|
|
||||||
api: WidgetApiType.FromWidget,
|
|
||||||
widgetId: this.widgetId,
|
|
||||||
action: action,
|
|
||||||
requestId: randomString(160),
|
|
||||||
data: payload,
|
|
||||||
response: {}, // Not used at this layer - it's used when the client responds
|
|
||||||
};
|
|
||||||
|
|
||||||
if (callback) {
|
|
||||||
this.inFlightRequests[request.requestId] = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[WidgetAPI] Sending request: `, request);
|
|
||||||
window.parent.postMessage(request, "*");
|
|
||||||
}
|
|
||||||
|
|
||||||
public setAlwaysOnScreen(onScreen: boolean): Promise<any> {
|
|
||||||
return new Promise<any>(resolve => {
|
|
||||||
this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, null);
|
|
||||||
resolve(); // SetAlwaysOnScreen is currently fire-and-forget, but that could change.
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// TODO: Move to matrix-widget-api
|
||||||
export class WidgetType {
|
export class WidgetType {
|
||||||
public static readonly JITSI = new WidgetType("m.jitsi", "jitsi");
|
public static readonly JITSI = new WidgetType("m.jitsi", "jitsi");
|
||||||
public static readonly STICKERPICKER = new WidgetType("m.stickerpicker", "m.stickerpicker");
|
public static readonly STICKERPICKER = new WidgetType("m.stickerpicker", "m.stickerpicker");
|
||||||
|
|
|
@ -5953,6 +5953,11 @@ matrix-react-test-utils@^0.2.2:
|
||||||
resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
|
resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
|
||||||
integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
|
integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
|
||||||
|
|
||||||
|
matrix-widget-api@^0.1.0-beta.2:
|
||||||
|
version "0.1.0-beta.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.2.tgz#367da1ccd26b711f73fc5b6e02edf55ac2ea2692"
|
||||||
|
integrity sha512-q5g5RZN+RRjM4HmcJ+LYoQAYrB1wzyERmoQ+LvKbTV/+9Ov36Kp0QEP8CleSXEd5WLp6bkRlt60axDaY6pWGmg==
|
||||||
|
|
||||||
mdast-util-compact@^1.0.0:
|
mdast-util-compact@^1.0.0:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/mdast-util-compact/-/mdast-util-compact-1.0.4.tgz#d531bb7667b5123abf20859be086c4d06c894593"
|
resolved "https://registry.yarnpkg.com/mdast-util-compact/-/mdast-util-compact-1.0.4.tgz#d531bb7667b5123abf20859be086c4d06c894593"
|
||||||
|
|
Loading…
Reference in a new issue