mirror of
https://github.com/element-hq/element-web
synced 2024-11-25 02:35:48 +03:00
Merge pull request #4319 from matrix-org/dbkr/jitsi_fix_popout
Fix popout support for jitsi widgets
This commit is contained in:
commit
a76b089cf3
8 changed files with 134 additions and 152 deletions
|
@ -24,8 +24,7 @@ import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||||
import RoomViewStore from "./stores/RoomViewStore";
|
import RoomViewStore from "./stores/RoomViewStore";
|
||||||
import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import {Capability, KnownWidgetActions} from "./widgets/WidgetApi";
|
import {Capability} from "./widgets/WidgetApi";
|
||||||
import SdkConfig from "./SdkConfig";
|
|
||||||
|
|
||||||
const WIDGET_API_VERSION = '0.0.2'; // Current API version
|
const WIDGET_API_VERSION = '0.0.2'; // Current API version
|
||||||
const SUPPORTED_WIDGET_API_VERSIONS = [
|
const SUPPORTED_WIDGET_API_VERSIONS = [
|
||||||
|
@ -220,13 +219,6 @@ export default class FromWidgetPostMessageApi {
|
||||||
}
|
}
|
||||||
} else if (action === 'get_openid') {
|
} else if (action === 'get_openid') {
|
||||||
// Handled by caller
|
// Handled by caller
|
||||||
} else if (action === KnownWidgetActions.GetRiotWebConfig) {
|
|
||||||
if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.GetRiotWebConfig)) {
|
|
||||||
this.sendResponse(event, {
|
|
||||||
api: INBOUND_API_NAME,
|
|
||||||
config: SdkConfig.get(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.warn('Widget postMessage event unhandled');
|
console.warn('Widget postMessage event unhandled');
|
||||||
this.sendError(event, {message: 'The postMessage was unhandled'});
|
this.sendError(event, {message: 'The postMessage was unhandled'});
|
||||||
|
|
|
@ -30,8 +30,6 @@ export const DEFAULTS: ConfigOptions = {
|
||||||
jitsi: {
|
jitsi: {
|
||||||
// Default conference domain
|
// Default conference domain
|
||||||
preferredDomain: "jitsi.riot.im",
|
preferredDomain: "jitsi.riot.im",
|
||||||
// Default Jitsi Meet API location
|
|
||||||
externalApiUrl: "https://jitsi.riot.im/libs/external_api.min.js",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018 New Vector Ltd
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -41,12 +42,30 @@ import PersistedElement from "./PersistedElement";
|
||||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||||
const ENABLE_REACT_PERF = false;
|
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) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// The key used for PersistedElement
|
// The key used for PersistedElement
|
||||||
this._persistKey = 'widget_' + this.props.id;
|
this._persistKey = 'widget_' + this.props.app.id;
|
||||||
|
|
||||||
this.state = this._getNewState(props);
|
this.state = this._getNewState(props);
|
||||||
|
|
||||||
|
@ -78,7 +97,7 @@ export default class AppTile extends React.Component {
|
||||||
// This is a function to make the impact of calling SettingsStore slightly less
|
// This is a function to make the impact of calling SettingsStore slightly less
|
||||||
const hasPermissionToLoad = () => {
|
const hasPermissionToLoad = () => {
|
||||||
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", newProps.room.roomId);
|
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", newProps.room.roomId);
|
||||||
return !!currentlyAllowedWidgets[newProps.eventId];
|
return !!currentlyAllowedWidgets[newProps.app.eventId];
|
||||||
};
|
};
|
||||||
|
|
||||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||||
|
@ -86,7 +105,7 @@ export default class AppTile extends React.Component {
|
||||||
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.url),
|
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(),
|
||||||
|
@ -103,7 +122,7 @@ export default class AppTile extends React.Component {
|
||||||
* @return {Boolean} True if capability supported
|
* @return {Boolean} True if capability supported
|
||||||
*/
|
*/
|
||||||
_hasCapability(capability) {
|
_hasCapability(capability) {
|
||||||
return ActiveWidgetStore.widgetHasCapability(this.props.id, capability);
|
return ActiveWidgetStore.widgetHasCapability(this.props.app.id, capability);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -125,7 +144,7 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
const params = qs.parse(u.query);
|
const params = qs.parse(u.query);
|
||||||
// Append widget ID to query parameters
|
// Append widget ID to query parameters
|
||||||
params.widgetId = this.props.id;
|
params.widgetId = this.props.app.id;
|
||||||
// Append current / parent URL, minus the hash because that will change when
|
// Append current / parent URL, minus the hash because that will change when
|
||||||
// we view a different room (ie. may change for persistent widgets)
|
// we view a different room (ie. may change for persistent widgets)
|
||||||
params.parentUrl = window.location.href.split('#', 2)[0];
|
params.parentUrl = window.location.href.split('#', 2)[0];
|
||||||
|
@ -137,11 +156,11 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
isMixedContent() {
|
isMixedContent() {
|
||||||
const parentContentProtocol = window.location.protocol;
|
const parentContentProtocol = window.location.protocol;
|
||||||
const u = url.parse(this.props.url);
|
const u = url.parse(this.props.app.url);
|
||||||
const childContentProtocol = u.protocol;
|
const childContentProtocol = u.protocol;
|
||||||
if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') {
|
if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') {
|
||||||
console.warn("Refusing to load mixed-content app:",
|
console.warn("Refusing to load mixed-content app:",
|
||||||
parentContentProtocol, childContentProtocol, window.location, this.props.url);
|
parentContentProtocol, childContentProtocol, window.location, this.props.app.url);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -164,8 +183,8 @@ export default class AppTile extends React.Component {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
|
|
||||||
// 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.id)) {
|
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
|
||||||
ActiveWidgetStore.destroyPersistentWidget(this.props.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);
|
||||||
}
|
}
|
||||||
|
@ -176,11 +195,11 @@ export default class AppTile extends React.Component {
|
||||||
* Component initialisation is only complete when this function has resolved
|
* Component initialisation is only complete when this function has resolved
|
||||||
*/
|
*/
|
||||||
setScalarToken() {
|
setScalarToken() {
|
||||||
if (!WidgetUtils.isScalarUrl(this.props.url)) {
|
if (!WidgetUtils.isScalarUrl(this.props.app.url)) {
|
||||||
console.warn('Non-scalar widget, not setting scalar token!', url);
|
console.warn('Non-scalar widget, not setting scalar token!', url);
|
||||||
this.setState({
|
this.setState({
|
||||||
error: null,
|
error: null,
|
||||||
widgetUrl: this._addWurlParams(this.props.url),
|
widgetUrl: this._addWurlParams(this.props.app.url),
|
||||||
initialising: false,
|
initialising: false,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -191,7 +210,7 @@ export default class AppTile extends React.Component {
|
||||||
console.warn("No integration manager - not setting scalar token", url);
|
console.warn("No integration manager - not setting scalar token", url);
|
||||||
this.setState({
|
this.setState({
|
||||||
error: null,
|
error: null,
|
||||||
widgetUrl: this._addWurlParams(this.props.url),
|
widgetUrl: this._addWurlParams(this.props.app.url),
|
||||||
initialising: false,
|
initialising: false,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -204,7 +223,7 @@ export default class AppTile extends React.Component {
|
||||||
console.warn('Non-scalar manager, not setting scalar token!', url);
|
console.warn('Non-scalar manager, not setting scalar token!', url);
|
||||||
this.setState({
|
this.setState({
|
||||||
error: null,
|
error: null,
|
||||||
widgetUrl: this._addWurlParams(this.props.url),
|
widgetUrl: this._addWurlParams(this.props.app.url),
|
||||||
initialising: false,
|
initialising: false,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -217,7 +236,7 @@ export default class AppTile extends React.Component {
|
||||||
this._scalarClient.getScalarToken().then((token) => {
|
this._scalarClient.getScalarToken().then((token) => {
|
||||||
// Append scalar_token as a query param if not already present
|
// Append scalar_token as a query param if not already present
|
||||||
this._scalarClient.scalarToken = token;
|
this._scalarClient.scalarToken = token;
|
||||||
const u = url.parse(this._addWurlParams(this.props.url));
|
const u = url.parse(this._addWurlParams(this.props.app.url));
|
||||||
const params = qs.parse(u.query);
|
const params = qs.parse(u.query);
|
||||||
if (!params.scalar_token) {
|
if (!params.scalar_token) {
|
||||||
params.scalar_token = encodeURIComponent(token);
|
params.scalar_token = encodeURIComponent(token);
|
||||||
|
@ -246,7 +265,7 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (nextProps.url !== this.props.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
|
// 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) {
|
||||||
|
@ -280,7 +299,7 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onEditClick() {
|
_onEditClick() {
|
||||||
console.log("Edit widget ID ", this.props.id);
|
console.log("Edit widget ID ", this.props.app.id);
|
||||||
if (this.props.onEditClick) {
|
if (this.props.onEditClick) {
|
||||||
this.props.onEditClick();
|
this.props.onEditClick();
|
||||||
} else {
|
} else {
|
||||||
|
@ -289,13 +308,13 @@ export default class AppTile extends React.Component {
|
||||||
IntegrationManagers.sharedInstance().openAll(
|
IntegrationManagers.sharedInstance().openAll(
|
||||||
this.props.room,
|
this.props.room,
|
||||||
'type_' + this.props.type,
|
'type_' + this.props.type,
|
||||||
this.props.id,
|
this.props.app.id,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(
|
IntegrationManagers.sharedInstance().getPrimaryManager().open(
|
||||||
this.props.room,
|
this.props.room,
|
||||||
'type_' + this.props.type,
|
'type_' + this.props.type,
|
||||||
this.props.id,
|
this.props.app.id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -303,7 +322,7 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
_onSnapshotClick() {
|
_onSnapshotClick() {
|
||||||
console.warn("Requesting widget snapshot");
|
console.warn("Requesting widget snapshot");
|
||||||
ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot()
|
ActiveWidgetStore.getWidgetMessaging(this.props.app.id).getScreenshot()
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error("Failed to get screenshot", err);
|
console.error("Failed to get screenshot", err);
|
||||||
})
|
})
|
||||||
|
@ -351,7 +370,7 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
WidgetUtils.setRoomWidget(
|
WidgetUtils.setRoomWidget(
|
||||||
this.props.room.roomId,
|
this.props.room.roomId,
|
||||||
this.props.id,
|
this.props.app.id,
|
||||||
).catch((e) => {
|
).catch((e) => {
|
||||||
console.error('Failed to delete widget', e);
|
console.error('Failed to delete widget', e);
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
@ -369,7 +388,7 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRevokeClicked() {
|
_onRevokeClicked() {
|
||||||
console.info("Revoke widget permissions - %s", this.props.id);
|
console.info("Revoke widget permissions - %s", this.props.app.id);
|
||||||
this._revokeWidgetPermission();
|
this._revokeWidgetPermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -380,10 +399,10 @@ export default class AppTile extends React.Component {
|
||||||
// Destroy the old widget messaging before starting it back up again. Some widgets
|
// 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
|
// have startup routines that run when they are loaded, so we just need to reinitialize
|
||||||
// the messaging for them.
|
// the messaging for them.
|
||||||
ActiveWidgetStore.delWidgetMessaging(this.props.id);
|
ActiveWidgetStore.delWidgetMessaging(this.props.app.id);
|
||||||
this._setupWidgetMessaging();
|
this._setupWidgetMessaging();
|
||||||
|
|
||||||
ActiveWidgetStore.setRoomId(this.props.id, this.props.room.roomId);
|
ActiveWidgetStore.setRoomId(this.props.app.id, this.props.room.roomId);
|
||||||
this.setState({loading: false});
|
this.setState({loading: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -391,10 +410,10 @@ export default class AppTile extends React.Component {
|
||||||
// FIXME: There's probably no reason to do this here: it should probably be done entirely
|
// FIXME: There's probably no reason to do this here: it should probably be done entirely
|
||||||
// in ActiveWidgetStore.
|
// in ActiveWidgetStore.
|
||||||
const widgetMessaging = new WidgetMessaging(
|
const widgetMessaging = new WidgetMessaging(
|
||||||
this.props.id, this.props.url, this.props.userWidget, this._appFrame.current.contentWindow);
|
this.props.app.id, this._getRenderedUrl(), this.props.userWidget, this._appFrame.current.contentWindow);
|
||||||
ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
|
ActiveWidgetStore.setWidgetMessaging(this.props.app.id, widgetMessaging);
|
||||||
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
||||||
console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);
|
console.log(`Widget ${this.props.app.id} requested capabilities: ` + requestedCapabilities);
|
||||||
requestedCapabilities = requestedCapabilities || [];
|
requestedCapabilities = requestedCapabilities || [];
|
||||||
|
|
||||||
// Allow whitelisted capabilities
|
// Allow whitelisted capabilities
|
||||||
|
@ -406,7 +425,7 @@ export default class AppTile extends React.Component {
|
||||||
}, this.props.whitelistCapabilities);
|
}, this.props.whitelistCapabilities);
|
||||||
|
|
||||||
if (requestedWhitelistCapabilies.length > 0 ) {
|
if (requestedWhitelistCapabilies.length > 0 ) {
|
||||||
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties: ` +
|
console.warn(`Widget ${this.props.app.id} allowing requested, whitelisted properties: ` +
|
||||||
requestedWhitelistCapabilies,
|
requestedWhitelistCapabilies,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -414,7 +433,7 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
// TODO -- Add UI to warn about and optionally allow requested capabilities
|
// TODO -- Add UI to warn about and optionally allow requested capabilities
|
||||||
|
|
||||||
ActiveWidgetStore.setWidgetCapabilities(this.props.id, requestedWhitelistCapabilies);
|
ActiveWidgetStore.setWidgetCapabilities(this.props.app.id, requestedWhitelistCapabilies);
|
||||||
|
|
||||||
if (this.props.onCapabilityRequest) {
|
if (this.props.onCapabilityRequest) {
|
||||||
this.props.onCapabilityRequest(requestedCapabilities);
|
this.props.onCapabilityRequest(requestedCapabilities);
|
||||||
|
@ -422,16 +441,16 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
// We only tell Jitsi widgets that we're ready because they're realistically the only ones
|
// We only tell Jitsi widgets that we're ready because they're realistically the only ones
|
||||||
// using this custom extension to the widget API.
|
// using this custom extension to the widget API.
|
||||||
if (this.props.type === 'jitsi') {
|
if (this.props.app.type === 'jitsi') {
|
||||||
widgetMessaging.flagReadyToContinue();
|
widgetMessaging.flagReadyToContinue();
|
||||||
}
|
}
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, 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.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._hasCapability('m.sticker')) {
|
||||||
|
@ -460,9 +479,9 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
_grantWidgetPermission() {
|
_grantWidgetPermission() {
|
||||||
const roomId = this.props.room.roomId;
|
const roomId = this.props.room.roomId;
|
||||||
console.info("Granting permission for widget to load: " + this.props.eventId);
|
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
||||||
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||||
current[this.props.eventId] = true;
|
current[this.props.app.eventId] = true;
|
||||||
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
|
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
|
||||||
this.setState({hasPermissionToLoad: true});
|
this.setState({hasPermissionToLoad: true});
|
||||||
|
|
||||||
|
@ -476,14 +495,14 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
_revokeWidgetPermission() {
|
_revokeWidgetPermission() {
|
||||||
const roomId = this.props.room.roomId;
|
const roomId = this.props.room.roomId;
|
||||||
console.info("Revoking permission for widget to load: " + this.props.eventId);
|
console.info("Revoking permission for widget to load: " + this.props.app.eventId);
|
||||||
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||||
current[this.props.eventId] = false;
|
current[this.props.app.eventId] = false;
|
||||||
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
|
SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => {
|
||||||
this.setState({hasPermissionToLoad: false});
|
this.setState({hasPermissionToLoad: false});
|
||||||
|
|
||||||
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
||||||
ActiveWidgetStore.destroyPersistentWidget(this.props.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);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
@ -494,8 +513,8 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
formatAppTileName() {
|
formatAppTileName() {
|
||||||
let appTileName = "No name";
|
let appTileName = "No name";
|
||||||
if (this.props.name && this.props.name.trim()) {
|
if (this.props.app.name && this.props.app.name.trim()) {
|
||||||
appTileName = this.props.name.trim();
|
appTileName = this.props.app.name.trim();
|
||||||
}
|
}
|
||||||
return appTileName;
|
return appTileName;
|
||||||
}
|
}
|
||||||
|
@ -519,20 +538,70 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getSafeUrl() {
|
/**
|
||||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl, true);
|
* Replace the widget template variables in a url with their values
|
||||||
|
*
|
||||||
|
* @param {string} u The URL with template variables
|
||||||
|
*
|
||||||
|
* @returns {string} url with temlate variables replaced
|
||||||
|
*/
|
||||||
|
_templatedUrl(u) {
|
||||||
|
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
|
const myUser = MatrixClientPeg.get().getUser(myUserId);
|
||||||
|
const vars = Object.assign({
|
||||||
|
domain: "jitsi.riot.im", // v1 widgets have this hardcoded
|
||||||
|
}, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (this.props.app.type === 'jitsi') {
|
||||||
|
console.log("Replacing Jitsi widget URL with local wrapper");
|
||||||
|
url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true});
|
||||||
|
url = this._addWurlParams(url);
|
||||||
|
} else {
|
||||||
|
url = this._getSafeUrl(this.state.widgetUrl);
|
||||||
|
}
|
||||||
|
return this._templatedUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getPopoutUrl() {
|
||||||
|
return this._templatedUrl(this._getSafeUrl(this.props.app.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
_getSafeUrl(u) {
|
||||||
|
const parsedWidgetUrl = url.parse(u, true);
|
||||||
if (ENABLE_REACT_PERF) {
|
if (ENABLE_REACT_PERF) {
|
||||||
parsedWidgetUrl.search = null;
|
parsedWidgetUrl.search = null;
|
||||||
parsedWidgetUrl.query.react_perf = true;
|
parsedWidgetUrl.query.react_perf = true;
|
||||||
}
|
}
|
||||||
let safeWidgetUrl = '';
|
let safeWidgetUrl = '';
|
||||||
if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol) || (
|
if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol)) {
|
||||||
// Check if the widget URL is a Jitsi widget in Electron
|
|
||||||
parsedWidgetUrl.protocol === 'vector:'
|
|
||||||
&& parsedWidgetUrl.host === 'vector'
|
|
||||||
&& parsedWidgetUrl.pathname === '/webapp/jitsi.html'
|
|
||||||
&& this.props.type === 'jitsi'
|
|
||||||
)) {
|
|
||||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||||
}
|
}
|
||||||
return safeWidgetUrl;
|
return safeWidgetUrl;
|
||||||
|
@ -562,9 +631,9 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
_onPopoutWidgetClick() {
|
_onPopoutWidgetClick() {
|
||||||
// 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._getSafeUrl(), '_blank', 'noopener=yes');
|
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
||||||
Object.assign(document.createElement('a'),
|
Object.assign(document.createElement('a'),
|
||||||
{ target: '_blank', href: this._getSafeUrl(), rel: 'noreferrer noopener'}).click();
|
{ target: '_blank', href: this._getPopoutUrl(), rel: 'noreferrer noopener'}).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onReloadWidgetClick() {
|
_onReloadWidgetClick() {
|
||||||
|
@ -641,7 +710,7 @@ export default class AppTile extends React.Component {
|
||||||
<iframe
|
<iframe
|
||||||
allow={iframeFeatures}
|
allow={iframeFeatures}
|
||||||
ref={this._appFrame}
|
ref={this._appFrame}
|
||||||
src={this._getSafeUrl()}
|
src={this._getRenderedUrl()}
|
||||||
allowFullScreen={true}
|
allowFullScreen={true}
|
||||||
sandbox={sandboxFlags}
|
sandbox={sandboxFlags}
|
||||||
onLoad={this._onLoaded} />
|
onLoad={this._onLoaded} />
|
||||||
|
@ -706,7 +775,7 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
<div className={appTileClass} id={this.props.id}>
|
<div className={appTileClass} id={this.props.app.id}>
|
||||||
{ this.props.showMenubar &&
|
{ this.props.showMenubar &&
|
||||||
<div ref={this._menu_bar} className={menuBarClasses} onClick={this.onClickMenuBar}>
|
<div ref={this._menu_bar} className={menuBarClasses} onClick={this.onClickMenuBar}>
|
||||||
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
||||||
|
@ -753,12 +822,8 @@ export default class AppTile extends React.Component {
|
||||||
AppTile.displayName = 'AppTile';
|
AppTile.displayName = 'AppTile';
|
||||||
|
|
||||||
AppTile.propTypes = {
|
AppTile.propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
app: PropTypes.object.isRequired,
|
||||||
eventId: PropTypes.string, // required for room widgets
|
|
||||||
url: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
room: PropTypes.object.isRequired,
|
room: PropTypes.object.isRequired,
|
||||||
type: PropTypes.string.isRequired,
|
|
||||||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||||
fullWidth: PropTypes.bool,
|
fullWidth: PropTypes.bool,
|
||||||
|
@ -805,7 +870,6 @@ AppTile.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
AppTile.defaultProps = {
|
AppTile.defaultProps = {
|
||||||
url: "",
|
|
||||||
waitForIframeLoad: true,
|
waitForIframeLoad: true,
|
||||||
showMenubar: true,
|
showMenubar: true,
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018 New Vector Ltd
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -75,11 +75,7 @@ export default createReactClass({
|
||||||
const AppTile = sdk.getComponent('elements.AppTile');
|
const AppTile = sdk.getComponent('elements.AppTile');
|
||||||
return <AppTile
|
return <AppTile
|
||||||
key={app.id}
|
key={app.id}
|
||||||
id={app.id}
|
app={app}
|
||||||
eventId={app.eventId}
|
|
||||||
url={app.url}
|
|
||||||
name={app.name}
|
|
||||||
type={app.type}
|
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
room={persistentWidgetInRoom}
|
room={persistentWidgetInRoom}
|
||||||
userId={MatrixClientPeg.get().credentials.userId}
|
userId={MatrixClientPeg.get().credentials.userId}
|
||||||
|
|
|
@ -160,11 +160,7 @@ export default createReactClass({
|
||||||
|
|
||||||
return (<AppTile
|
return (<AppTile
|
||||||
key={app.id}
|
key={app.id}
|
||||||
id={app.id}
|
app={app}
|
||||||
eventId={app.eventId}
|
|
||||||
url={app.url}
|
|
||||||
name={app.name}
|
|
||||||
type={app.type}
|
|
||||||
fullWidth={arr.length<2 ? true : false}
|
fullWidth={arr.length<2 ? true : false}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
userId={this.props.userId}
|
userId={this.props.userId}
|
||||||
|
|
|
@ -240,6 +240,14 @@ export default class Stickerpicker extends React.Component {
|
||||||
// Set default name
|
// Set default name
|
||||||
stickerpickerWidget.content.name = stickerpickerWidget.name || _t("Stickerpack");
|
stickerpickerWidget.content.name = stickerpickerWidget.name || _t("Stickerpack");
|
||||||
|
|
||||||
|
// FIXME: could this use the same code as other apps?
|
||||||
|
const stickerApp = {
|
||||||
|
id: stickerpickerWidget.id,
|
||||||
|
url: stickerpickerWidget.content.url,
|
||||||
|
name: stickerpickerWidget.content.name,
|
||||||
|
type: stickerpickerWidget.content.type,
|
||||||
|
};
|
||||||
|
|
||||||
stickersContent = (
|
stickersContent = (
|
||||||
<div className='mx_Stickers_content_container'>
|
<div className='mx_Stickers_content_container'>
|
||||||
<div
|
<div
|
||||||
|
@ -253,11 +261,8 @@ export default class Stickerpicker extends React.Component {
|
||||||
>
|
>
|
||||||
<PersistedElement persistKey={PERSISTED_ELEMENT_KEY} style={{zIndex: STICKERPICKER_Z_INDEX}}>
|
<PersistedElement persistKey={PERSISTED_ELEMENT_KEY} style={{zIndex: STICKERPICKER_Z_INDEX}}>
|
||||||
<AppTile
|
<AppTile
|
||||||
id={stickerpickerWidget.id}
|
app={stickerApp}
|
||||||
url={stickerpickerWidget.content.url}
|
|
||||||
name={stickerpickerWidget.content.name}
|
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
type={stickerpickerWidget.content.type}
|
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
userId={MatrixClientPeg.get().credentials.userId}
|
userId={MatrixClientPeg.get().credentials.userId}
|
||||||
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
|
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
|
||||||
|
|
|
@ -30,26 +30,6 @@ import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
||||||
import {IntegrationManagers} from "../integrations/IntegrationManagers";
|
import {IntegrationManagers} from "../integrations/IntegrationManagers";
|
||||||
import {Capability} from "../widgets/WidgetApi";
|
import {Capability} from "../widgets/WidgetApi";
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a URI according to a set of template variables. Variables will be
|
|
||||||
* passed through encodeURIComponent.
|
|
||||||
* @param {string} pathTemplate 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 encodeUri(pathTemplate, variables) {
|
|
||||||
for (const key in variables) {
|
|
||||||
if (!variables.hasOwnProperty(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
pathTemplate = pathTemplate.replace(
|
|
||||||
key, encodeURIComponent(variables[key]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return pathTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
* (Does not apply to non-room-based / user widgets)
|
* (Does not apply to non-room-based / user widgets)
|
||||||
|
@ -402,18 +382,6 @@ export default class WidgetUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
static makeAppConfig(appId, app, senderUserId, roomId, eventId) {
|
static makeAppConfig(appId, app, senderUserId, roomId, eventId) {
|
||||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
|
||||||
const user = MatrixClientPeg.get().getUser(myUserId);
|
|
||||||
const params = {
|
|
||||||
'$matrix_user_id': myUserId,
|
|
||||||
'$matrix_room_id': roomId,
|
|
||||||
'$matrix_display_name': user ? user.displayName : myUserId,
|
|
||||||
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
|
|
||||||
|
|
||||||
// TODO: Namespace themes through some standard
|
|
||||||
'$theme': SettingsStore.getValue("theme"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!senderUserId) {
|
if (!senderUserId) {
|
||||||
throw new Error("Widgets must be created by someone - provide a senderUserId");
|
throw new Error("Widgets must be created by someone - provide a senderUserId");
|
||||||
}
|
}
|
||||||
|
@ -423,32 +391,6 @@ export default class WidgetUtils {
|
||||||
app.eventId = eventId;
|
app.eventId = eventId;
|
||||||
app.name = app.name || app.type;
|
app.name = app.name || app.type;
|
||||||
|
|
||||||
if (app.type === 'jitsi') {
|
|
||||||
console.log("Replacing Jitsi widget URL with local wrapper");
|
|
||||||
if (!app.data || !app.data.conferenceId) {
|
|
||||||
// Assumed to be a v1 widget: add a data object for visibility on the wrapper
|
|
||||||
// TODO: Remove this once mobile supports v2 widgets
|
|
||||||
console.log("Replacing v1 Jitsi widget with v2 equivalent");
|
|
||||||
const parsed = new URL(app.url);
|
|
||||||
app.data = {
|
|
||||||
conferenceId: parsed.searchParams.get("confId"),
|
|
||||||
domain: "jitsi.riot.im", // v1 widgets have this hardcoded
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
app.url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (app.data) {
|
|
||||||
Object.keys(app.data).forEach((key) => {
|
|
||||||
params['$' + key] = app.data[key];
|
|
||||||
});
|
|
||||||
|
|
||||||
app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true);
|
|
||||||
}
|
|
||||||
|
|
||||||
app.url = encodeUri(app.url, params);
|
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -462,7 +404,6 @@ export default class WidgetUtils {
|
||||||
// widgets from at all, but it probably makes sense for sanity.
|
// widgets from at all, but it probably makes sense for sanity.
|
||||||
if (appType === 'jitsi') {
|
if (appType === 'jitsi') {
|
||||||
capWhitelist.push(Capability.AlwaysOnScreen);
|
capWhitelist.push(Capability.AlwaysOnScreen);
|
||||||
capWhitelist.push(Capability.GetRiotWebConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return capWhitelist;
|
return capWhitelist;
|
||||||
|
|
|
@ -23,7 +23,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",
|
||||||
GetRiotWebConfig = "im.vector.web.riot_config",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum KnownWidgetActions {
|
export enum KnownWidgetActions {
|
||||||
|
@ -34,7 +33,6 @@ export enum KnownWidgetActions {
|
||||||
UpdateVisibility = "visibility",
|
UpdateVisibility = "visibility",
|
||||||
ReceiveOpenIDCredentials = "openid_credentials",
|
ReceiveOpenIDCredentials = "openid_credentials",
|
||||||
SetAlwaysOnScreen = "set_always_on_screen",
|
SetAlwaysOnScreen = "set_always_on_screen",
|
||||||
GetRiotWebConfig = "im.vector.web.riot_config",
|
|
||||||
ClientReady = "im.vector.ready",
|
ClientReady = "im.vector.ready",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,12 +155,4 @@ export class WidgetApi {
|
||||||
resolve(); // SetAlwaysOnScreen is currently fire-and-forget, but that could change.
|
resolve(); // SetAlwaysOnScreen is currently fire-and-forget, but that could change.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRiotConfig(): Promise<any> {
|
|
||||||
return new Promise<any>(resolve => {
|
|
||||||
this.callAction(KnownWidgetActions.GetRiotWebConfig, {}, response => {
|
|
||||||
resolve(response.response.config);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue