Support and send the config over to capable widgets

For https://github.com/vector-im/riot-web/pull/12845
This commit is contained in:
Travis Ralston 2020-03-24 09:55:54 -06:00
parent 26bda5933b
commit bdcb65de77
5 changed files with 64 additions and 6 deletions

View file

@ -24,6 +24,8 @@ 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 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 = [
@ -213,11 +215,18 @@ export default class FromWidgetPostMessageApi {
const data = event.data.data; const data = event.data.data;
const val = data.value; const val = data.value;
if (ActiveWidgetStore.widgetHasCapability(widgetId, 'm.always_on_screen')) { if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
ActiveWidgetStore.setWidgetPersistence(widgetId, val); ActiveWidgetStore.setWidgetPersistence(widgetId, val);
} }
} 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

@ -27,6 +27,7 @@ import {MatrixClientPeg} from "./MatrixClientPeg";
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog"; import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog";
import WidgetUtils from "./utils/WidgetUtils"; import WidgetUtils from "./utils/WidgetUtils";
import {KnownWidgetActions} from "./widgets/WidgetApi";
if (!global.mxFromWidgetMessaging) { if (!global.mxFromWidgetMessaging) {
global.mxFromWidgetMessaging = new FromWidgetPostMessageApi(); global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
@ -75,6 +76,16 @@ export default class WidgetMessaging {
}); });
} }
/**
* Tells the widget that the client is ready to handle further widget requests.
*/
flagReadyToContinue() {
return this.messageToWidget({
api: OUTBOUND_API_NAME,
action: KnownWidgetActions.ClientReady,
});
}
/** /**
* Request a screenshot from a widget * Request a screenshot from a widget
* @return {Promise} To be resolved with screenshot data when it has been generated * @return {Promise} To be resolved with screenshot data when it has been generated

View file

@ -419,6 +419,12 @@ export default class AppTile extends React.Component {
if (this.props.onCapabilityRequest) { if (this.props.onCapabilityRequest) {
this.props.onCapabilityRequest(requestedCapabilities); this.props.onCapabilityRequest(requestedCapabilities);
} }
// We only tell Jitsi widgets that we're ready because they're realistically the only ones
// using this custom extension to the widget API.
if (this.props.type === 'jitsi') {
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.type}`, this.props.id, err);
}); });

View file

@ -28,6 +28,7 @@ const WIDGET_WAIT_TIME = 20000;
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import ActiveWidgetStore from "../stores/ActiveWidgetStore";
import {IntegrationManagers} from "../integrations/IntegrationManagers"; import {IntegrationManagers} from "../integrations/IntegrationManagers";
import {Capability} from "../widgets/WidgetApi";
/** /**
* Encodes a URI according to a set of template variables. Variables will be * Encodes a URI according to a set of template variables. Variables will be
@ -454,12 +455,15 @@ export default class WidgetUtils {
static getCapWhitelistForAppTypeInRoomId(appType, roomId) { static getCapWhitelistForAppTypeInRoomId(appType, roomId) {
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId); const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId);
const capWhitelist = enableScreenshots ? ["m.capability.screenshot"] : []; const capWhitelist = enableScreenshots ? [Capability.Screenshot] : [];
// Obviously anyone that can add a widget can claim it's a jitsi widget, // Obviously anyone that can add a widget can claim it's a jitsi widget,
// so this doesn't really offer much over the set of domains we load // so this doesn't really offer much over the set of domains we load
// widgets from at all, but it probably makes sense for sanity. // widgets from at all, but it probably makes sense for sanity.
if (appType == 'jitsi') capWhitelist.push("m.always_on_screen"); if (appType === 'jitsi') {
capWhitelist.push(Capability.AlwaysOnScreen);
capWhitelist.push(Capability.GetRiotWebConfig);
}
return capWhitelist; return capWhitelist;
} }

View file

@ -23,6 +23,7 @@ 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 {
@ -33,7 +34,10 @@ 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",
} }
export type WidgetAction = KnownWidgetActions | string; export type WidgetAction = KnownWidgetActions | string;
export enum WidgetApiType { export enum WidgetApiType {
@ -63,10 +67,15 @@ export interface FromWidgetRequest extends WidgetRequest {
*/ */
export class WidgetApi { export class WidgetApi {
private origin: string; private origin: string;
private inFlightRequests: {[requestId: string]: (reply: FromWidgetRequest) => void} = {}; private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {};
private readyPromise: Promise<any>; private readyPromise: Promise<any>;
private readyPromiseResolve: () => void; private readyPromiseResolve: () => void;
/**
* Set this to true if your widget is expecting a ready message from the client. False otherwise (default).
*/
public expectingExplicitReady = false;
constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) { constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) {
this.origin = new URL(currentUrl).origin; this.origin = new URL(currentUrl).origin;
@ -83,7 +92,14 @@ export class WidgetApi {
if (payload.action === KnownWidgetActions.GetCapabilities) { if (payload.action === KnownWidgetActions.GetCapabilities) {
this.onCapabilitiesRequest(<ToWidgetRequest>payload); this.onCapabilitiesRequest(<ToWidgetRequest>payload);
if (!this.expectingExplicitReady) {
this.readyPromiseResolve();
}
} else if (payload.action === KnownWidgetActions.ClientReady) {
this.readyPromiseResolve(); this.readyPromiseResolve();
// Automatically acknowledge so we can move on
this.replyToRequest(<ToWidgetRequest>payload, {});
} else { } else {
console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`); console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
} }
@ -126,7 +142,10 @@ export class WidgetApi {
data: payload, data: payload,
response: {}, // Not used at this layer - it's used when the client responds response: {}, // Not used at this layer - it's used when the client responds
}; };
this.inFlightRequests[request.requestId] = callback;
if (callback) {
this.inFlightRequests[request.requestId] = callback;
}
console.log(`[WidgetAPI] Sending request: `, request); console.log(`[WidgetAPI] Sending request: `, request);
window.parent.postMessage(request, "*"); window.parent.postMessage(request, "*");
@ -134,7 +153,16 @@ export class WidgetApi {
public setAlwaysOnScreen(onScreen: boolean): Promise<any> { public setAlwaysOnScreen(onScreen: boolean): Promise<any> {
return new Promise<any>(resolve => { return new Promise<any>(resolve => {
this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, resolve); this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, null);
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);
});
}); });
} }
} }