mirror of https://github.com/vector-im/riot-web
First rough cut of cutting AppTile over to the ClientWidgetApi
parent
14766e24b8
commit
cd93b2c22a
|
@ -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,15 @@ 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 {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:'];
|
|
||||||
const ENABLE_REACT_PERF = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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,6 +48,8 @@ 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.state = this._getNewState(props);
|
this.state = this._getNewState(props);
|
||||||
|
|
||||||
|
@ -123,43 +101,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);
|
||||||
|
@ -175,7 +116,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
|
||||||
|
@ -191,80 +132,26 @@ export default class AppTile extends React.Component {
|
||||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||||
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
|
|
||||||
*/
|
|
||||||
setScalarToken() {
|
|
||||||
if (!WidgetUtils.isScalarUrl(this.props.app.url)) {
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
this._sgWidget = new StopGapWidget(newProps);
|
||||||
|
this._sgWidget.on("ready", this._onWidgetReady);
|
||||||
|
this._startWidget();
|
||||||
|
}
|
||||||
|
|
||||||
const managers = IntegrationManagers.sharedInstance();
|
_startWidget() {
|
||||||
if (!managers.hasManager()) {
|
this._sgWidget.prepare().then(() => {
|
||||||
console.warn("No integration manager - not setting scalar token", url);
|
if (this._appFrame.current) {
|
||||||
this.setState({
|
this._sgWidget.start(this._appFrame.current);
|
||||||
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,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,9 +159,8 @@ export default class AppTile extends React.Component {
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,9 +171,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,7 +203,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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -326,34 +219,23 @@ export default class AppTile extends React.Component {
|
||||||
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
|
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
|
||||||
*/
|
*/
|
||||||
_endWidgetActions() {
|
_endWidgetActions() {
|
||||||
let terminationPromise;
|
// 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
|
||||||
if (this._hasCapability(Capability.ReceiveTerminate)) {
|
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
|
||||||
// Wait for widget to terminate within a timeout
|
if (this._appFrame.current) {
|
||||||
const timeout = 2000;
|
// In practice we could just do `+= ''` to trick the browser
|
||||||
const messaging = ActiveWidgetStore.getWidgetMessaging(this.props.app.id);
|
// into thinking the URL changed, however I can foresee this
|
||||||
terminationPromise = Promise.race([messaging.terminate(), sleep(timeout)]);
|
// being optimized out by a browser. Instead, we'll just point
|
||||||
} else {
|
// the iframe at a page that is reasonably safe to use in the
|
||||||
terminationPromise = Promise.resolve();
|
// event the iframe doesn't wink away.
|
||||||
|
// This is relative to where the Element instance is located.
|
||||||
|
this._appFrame.current.src = 'about:blank';
|
||||||
}
|
}
|
||||||
|
|
||||||
return terminationPromise.finally(() => {
|
// Delete the widget from the persisted store for good measure.
|
||||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
PersistedElement.destroyElement(this._persistKey);
|
||||||
// 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
|
|
||||||
if (this._appFrame.current) {
|
|
||||||
// In practice we could just do `+= ''` to trick the browser
|
|
||||||
// into thinking the URL changed, however I can foresee this
|
|
||||||
// being optimized out by a browser. Instead, we'll just point
|
|
||||||
// the iframe at a page that is reasonably safe to use in the
|
|
||||||
// event the iframe doesn't wink away.
|
|
||||||
// This is relative to where the Element instance is located.
|
|
||||||
this._appFrame.current.src = 'about:blank';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the widget from the persisted store for good measure.
|
this._sgWidget.stop();
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* If user has permission to modify widgets, delete the widget,
|
/* If user has permission to modify widgets, delete the widget,
|
||||||
|
@ -407,69 +289,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});
|
||||||
}
|
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||||
|
this._sgWidget.widgetApi.transport.send("im.vector.ready", {});
|
||||||
_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);
|
|
||||||
|
|
||||||
// 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)) {
|
|
||||||
widgetMessaging.flagReadyToContinue();
|
|
||||||
}
|
|
||||||
}).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(Capability.Sticker)) {
|
||||||
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');
|
||||||
|
@ -487,20 +318,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);
|
||||||
|
@ -510,7 +327,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.
|
||||||
|
@ -529,6 +346,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.
|
||||||
|
@ -566,40 +384,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
|
||||||
|
@ -609,67 +393,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -694,7 +422,7 @@ export default class AppTile extends React.Component {
|
||||||
this._endWidgetActions().then(() => {
|
this._endWidgetActions().then(() => {
|
||||||
if (this._appFrame.current) {
|
if (this._appFrame.current) {
|
||||||
// Reload iframe
|
// Reload iframe
|
||||||
this._appFrame.current.src = this._getRenderedUrl();
|
this._appFrame.current.src = this._sgWidget.embedUrl;
|
||||||
this.setState({});
|
this.setState({});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -702,7 +430,7 @@ 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() {
|
||||||
|
@ -780,7 +508,7 @@ export default class AppTile extends React.Component {
|
||||||
<iframe
|
<iframe
|
||||||
allow={iframeFeatures}
|
allow={iframeFeatures}
|
||||||
ref={this._appFrame}
|
ref={this._appFrame}
|
||||||
src={this._getRenderedUrl()}
|
src={this._sgWidget.embedUrl}
|
||||||
allowFullScreen={true}
|
allowFullScreen={true}
|
||||||
sandbox={sandboxFlags}
|
sandbox={sandboxFlags}
|
||||||
onLoad={this._onLoaded} />
|
onLoad={this._onLoaded} />
|
||||||
|
@ -827,9 +555,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(Capability.Screenshot)
|
||||||
|
&& this.props.show;
|
||||||
|
|
||||||
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
|
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
|
|
|
@ -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: number = 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() {
|
||||||
|
|
|
@ -0,0 +1,171 @@
|
||||||
|
/*
|
||||||
|
* 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, IWidget, IWidgetData, Widget } 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";
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
this.mockWidget = new ElementWidget(appTileProps.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async prepare(): Promise<void> {
|
||||||
|
if (this.scalarToken) return;
|
||||||
|
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 (!this.started) return;
|
||||||
|
WidgetMessagingStore.instance.stopMessaging(this.mockWidget);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 iterableUnion(requested, new Set(this.allowedCapabilities));
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,8 +31,7 @@ import { EnhancedMap } from "../../utils/maps";
|
||||||
export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
||||||
private static internalInstance = new WidgetMessagingStore();
|
private static internalInstance = new WidgetMessagingStore();
|
||||||
|
|
||||||
// <room/user ID, <widget ID, Widget>>
|
private widgetMap = new EnhancedMap<string, ClientWidgetApi>(); // <widget ID, ClientWidgetAPi>
|
||||||
private widgetMap = new EnhancedMap<string, EnhancedMap<string, WidgetSurrogate>>();
|
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
super(defaultDispatcher);
|
super(defaultDispatcher);
|
||||||
|
@ -51,106 +50,16 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
||||||
this.widgetMap.clear();
|
this.widgetMap.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public storeMessaging(widget: Widget, widgetApi: ClientWidgetApi) {
|
||||||
* Finds a widget by ID. Not guaranteed to return an accurate result.
|
this.stopMessaging(widget);
|
||||||
* @param {string} id The widget ID.
|
this.widgetMap.set(widget.id, widgetApi);
|
||||||
* @returns {{widget, room}} The widget and possible room ID, or a falsey value
|
|
||||||
* if not found.
|
|
||||||
* @deprecated Do not use.
|
|
||||||
*/
|
|
||||||
public findWidgetById(id: string): { widget: Widget, room?: Room } {
|
|
||||||
for (const key of this.widgetMap.keys()) {
|
|
||||||
for (const [entityId, surrogate] of this.widgetMap.get(key).entries()) {
|
|
||||||
if (surrogate.definition.id === id) {
|
|
||||||
const room: Room = this.matrixClient?.getRoom(entityId); // will be null for non-rooms
|
|
||||||
return {room, widget: surrogate.definition};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public stopMessaging(widget: Widget) {
|
||||||
* Gets the messaging instance for the widget. Returns a falsey value if none
|
this.widgetMap.remove(widget.id)?.stop();
|
||||||
* is present.
|
|
||||||
* @param {Room} room The room for which the widget lives within.
|
|
||||||
* @param {Widget} widget The widget to get messaging for.
|
|
||||||
* @returns {ClientWidgetApi} The messaging, or a falsey value.
|
|
||||||
*/
|
|
||||||
public messagingForRoomWidget(room: Room, widget: Widget): ClientWidgetApi {
|
|
||||||
return this.widgetMap.get(room.roomId)?.get(widget.id)?.messaging;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public getMessaging(widget: Widget): ClientWidgetApi {
|
||||||
* Gets the messaging instance for the widget. Returns a falsey value if none
|
return this.widgetMap.get(widget.id);
|
||||||
* is present.
|
|
||||||
* @param {Widget} widget The widget to get messaging for.
|
|
||||||
* @returns {ClientWidgetApi} The messaging, or a falsey value.
|
|
||||||
*/
|
|
||||||
public messagingForAccountWidget(widget: Widget): ClientWidgetApi {
|
|
||||||
return this.widgetMap.get(this.matrixClient?.getUserId())?.get(widget.id)?.messaging;
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateMessaging(locationId: string, widget: Widget, iframe: HTMLIFrameElement, driver: WidgetDriver) {
|
|
||||||
const messaging = new ClientWidgetApi(widget, iframe, driver);
|
|
||||||
this.widgetMap.getOrCreate(locationId, new EnhancedMap())
|
|
||||||
.getOrCreate(widget.id, new WidgetSurrogate(widget, messaging));
|
|
||||||
return messaging;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a messaging instance for the widget. If an instance already exists, it
|
|
||||||
* will be returned instead.
|
|
||||||
* @param {Room} room The room in which the widget lives.
|
|
||||||
* @param {Widget} widget The widget to generate/get messaging for.
|
|
||||||
* @param {HTMLIFrameElement} iframe The widget's iframe.
|
|
||||||
* @returns {ClientWidgetApi} The generated/cached messaging.
|
|
||||||
*/
|
|
||||||
public generateMessagingForRoomWidget(room: Room, widget: Widget, iframe: HTMLIFrameElement): ClientWidgetApi {
|
|
||||||
const existing = this.messagingForRoomWidget(room, widget);
|
|
||||||
if (existing) return existing;
|
|
||||||
|
|
||||||
const driver = new SdkWidgetDriver(widget, WidgetKind.Room, room.roomId);
|
|
||||||
return this.generateMessaging(room.roomId, widget, iframe, driver);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a messaging instance for the widget. If an instance already exists, it
|
|
||||||
* will be returned instead.
|
|
||||||
* @param {Widget} widget The widget to generate/get messaging for.
|
|
||||||
* @param {HTMLIFrameElement} iframe The widget's iframe.
|
|
||||||
* @returns {ClientWidgetApi} The generated/cached messaging.
|
|
||||||
*/
|
|
||||||
public generateMessagingForAccountWidget(widget: Widget, iframe: HTMLIFrameElement): ClientWidgetApi {
|
|
||||||
if (!this.matrixClient) {
|
|
||||||
throw new Error("No matrix client to create account widgets with");
|
|
||||||
}
|
|
||||||
|
|
||||||
const existing = this.messagingForAccountWidget(widget);
|
|
||||||
if (existing) return existing;
|
|
||||||
|
|
||||||
const userId = this.matrixClient.getUserId();
|
|
||||||
const driver = new SdkWidgetDriver(widget, WidgetKind.Account, userId);
|
|
||||||
return this.generateMessaging(userId, widget, iframe, driver);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the messaging instance for the widget, unregistering it.
|
|
||||||
* @param {Room} room The room where the widget resides.
|
|
||||||
* @param {Widget} widget The widget
|
|
||||||
*/
|
|
||||||
public stopMessagingForRoomWidget(room: Room, widget: Widget) {
|
|
||||||
const api = this.widgetMap.getOrCreate(room.roomId, new EnhancedMap()).remove(widget.id);
|
|
||||||
if (api) api.messaging.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the messaging instance for the widget, unregistering it.
|
|
||||||
* @param {Widget} widget The widget
|
|
||||||
*/
|
|
||||||
public stopMessagingForAccountWidget(widget: Widget) {
|
|
||||||
if (!this.matrixClient) return;
|
|
||||||
const api = this.widgetMap.getOrCreate(this.matrixClient.getUserId(), new EnhancedMap()).remove(widget.id);
|
|
||||||
if (api) api.messaging.stop();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -424,7 +424,6 @@ export default class WidgetUtils {
|
||||||
if (WidgetType.JITSI.matches(appType)) {
|
if (WidgetType.JITSI.matches(appType)) {
|
||||||
capWhitelist.push(Capability.AlwaysOnScreen);
|
capWhitelist.push(Capability.AlwaysOnScreen);
|
||||||
}
|
}
|
||||||
capWhitelist.push(Capability.ReceiveTerminate);
|
|
||||||
|
|
||||||
return capWhitelist;
|
return capWhitelist;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,6 @@ export enum Capability {
|
||||||
Screenshot = "m.capability.screenshot",
|
Screenshot = "m.capability.screenshot",
|
||||||
Sticker = "m.sticker",
|
Sticker = "m.sticker",
|
||||||
AlwaysOnScreen = "m.always_on_screen",
|
AlwaysOnScreen = "m.always_on_screen",
|
||||||
ReceiveTerminate = "im.vector.receive_terminate",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum KnownWidgetActions {
|
export enum KnownWidgetActions {
|
||||||
|
|
Loading…
Reference in New Issue