Merge pull request #4319 from matrix-org/dbkr/jitsi_fix_popout

Fix popout support for jitsi widgets
This commit is contained in:
David Baker 2020-04-01 11:57:25 +01:00 committed by GitHub
commit a76b089cf3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 134 additions and 152 deletions

View file

@ -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'});

View file

@ -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",
}, },
}; };

View file

@ -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,

View file

@ -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}

View file

@ -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}

View file

@ -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}

View file

@ -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;

View file

@ -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);
});
});
}
} }