Extremely bad support for "temporary widgets"

pull/21833/head
Travis Ralston 2020-09-21 14:36:16 -06:00
parent c1d9d96702
commit 342f1d5b43
7 changed files with 287 additions and 6 deletions

View File

@ -24,8 +24,10 @@ import {MatrixClientPeg} from "./MatrixClientPeg";
import RoomViewStore from "./stores/RoomViewStore";
import {IntegrationManagers} from "./integrations/IntegrationManagers";
import SettingsStore from "./settings/SettingsStore";
import {Capability} from "./widgets/WidgetApi";
import {Capability, KnownWidgetActions} from "./widgets/WidgetApi";
import {objectClone} from "./utils/objects";
import {Action} from "./dispatcher/actions";
import {TempWidgetStore} from "./stores/TempWidgetStore";
const WIDGET_API_VERSION = '0.0.2'; // Current API version
const SUPPORTED_WIDGET_API_VERSIONS = [
@ -218,8 +220,12 @@ export default class FromWidgetPostMessageApi {
if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
ActiveWidgetStore.setWidgetPersistence(widgetId, val);
}
} else if (action === 'get_openid') {
} else if (action === 'get_openid'
|| action === KnownWidgetActions.CloseWidget) {
// Handled by caller
} else if (action === KnownWidgetActions.OpenTempWidget) {
TempWidgetStore.instance.openTempWidget(event.data.data, widgetId);
this.sendResponse(event, {}); // ack
} else {
console.warn('Widget postMessage event unhandled');
this.sendError(event, {message: 'The postMessage was unhandled'});

View File

@ -28,7 +28,7 @@ import AsyncWrapper from './AsyncWrapper';
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
interface IModal<T extends any[]> {
export interface IModal<T extends any[]> {
elem: React.ReactNode;
className?: string;
beforeClosePromise?: Promise<boolean>;

View File

@ -147,6 +147,36 @@ export default class WidgetMessaging {
});
}
sendThemeInfo(themeInfo: any) {
return this.messageToWidget({
api: OUTBOUND_API_NAME,
action: KnownWidgetActions.UpdateThemeInfo,
data: themeInfo,
}).catch((error) => {
console.error("Failed to send theme info: ", error);
});
}
sendWidgetConfig(widgetConfig: any) {
return this.messageToWidget({
api: OUTBOUND_API_NAME,
action: KnownWidgetActions.SendWidgetConfig,
data: widgetConfig,
}).catch((error) => {
console.error("Failed to send widget info: ", error);
});
}
sendTempCloseInfo(info: any) {
return this.messageToWidget({
api: OUTBOUND_API_NAME,
action: KnownWidgetActions.ClosedWidgetResponse,
data: info,
}).catch((error) => {
console.error("Failed to send temp widget close info: ", error);
});
}
start() {
this.fromWidget.addEndpoint(this.widgetId, this.renderedUrl);
this.fromWidget.addListener("get_openid", this._onOpenIdRequest);

View File

@ -0,0 +1,155 @@
/*
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 * as React from 'react';
import BaseDialog from './BaseDialog';
import { _t } from '../../../languageHandler';
import { IDialogProps } from "./IDialogProps";
import WidgetMessaging from "../../../WidgetMessaging";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import Field from "../elements/Field";
import { KnownWidgetActions } from "../../../widgets/WidgetApi";
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
interface IState {
messaging?: WidgetMessaging;
androidMode: boolean;
darkTheme: boolean;
accentColor: string;
}
interface IProps extends IDialogProps {
widgetDefinition: {url: string, data: any};
sourceWidgetId: string;
}
// TODO: Make a better dialog
export default class TempWidgetDialog extends React.PureComponent<IProps, IState> {
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
constructor(props) {
super(props);
this.state = {
androidMode: false,
darkTheme: false,
accentColor: "#03b381",
};
}
public componentDidMount() {
// TODO: Don't violate every principle of widget creation
const messaging = new WidgetMessaging(
"TEMP_ID",
this.props.widgetDefinition.url,
this.props.widgetDefinition.url,
false,
this.appFrame.current.contentWindow,
);
this.setState({messaging});
}
public componentWillUnmount() {
this.state.messaging.fromWidget.removeListener(KnownWidgetActions.CloseWidget, this.onWidgetClose);
this.state.messaging.stop();
}
private onLoad = () => {
this.state.messaging.getCapabilities().then(caps => {
console.log("Requested capabilities: ", caps);
this.sendTheme();
this.state.messaging.sendWidgetConfig(this.props.widgetDefinition.data);
});
this.state.messaging.fromWidget.addListener(KnownWidgetActions.CloseWidget, this.onWidgetClose);
};
private sendTheme() {
if (!this.state.messaging) return;
this.state.messaging.sendThemeInfo({
clientName: this.state.androidMode ? "element-android" : "element-web",
isDark: this.state.darkTheme,
accentColor: this.state.accentColor,
});
}
public static sendExitData(sourceWidgetId: string, success: boolean, data?: any) {
const sourceMessaging = ActiveWidgetStore.getWidgetMessaging(sourceWidgetId);
if (!sourceMessaging) {
console.error("No source widget messaging for temp widget");
return;
}
sourceMessaging.sendTempCloseInfo({success, ...data});
}
private onWidgetClose = (req) => {
this.props.onFinished(true);
TempWidgetDialog.sendExitData(this.props.sourceWidgetId, true, req.data);
}
private onClientToggleChanged = (androidMode) => {
this.setState({androidMode}, () => this.sendTheme());
};
private onDarkThemeChanged = (darkTheme) => {
this.setState({darkTheme}, () => this.sendTheme());
};
private onAccentColorChanged = (ev) => {
this.setState({accentColor: ev.target.value}, () => this.sendTheme());
};
public render() {
// TODO: Don't violate every single security principle
const widgetUrl = this.props.widgetDefinition.url
+ "?widgetId=TEMP_ID&parentUrl=" + encodeURIComponent(window.location.href);
return <BaseDialog
title={_t("Widget Proof of Concept Dashboard")}
className='mx_TempWidgetDialog'
contentId='mx_Dialog_content'
onFinished={this.props.onFinished}
hasCancel={false}
>
<div>
<LabelledToggleSwitch
label={ _t("Look like Android")}
onChange={this.onClientToggleChanged}
value={this.state.androidMode}
/>
<LabelledToggleSwitch
label={ _t("Look like dark theme")}
onChange={this.onDarkThemeChanged}
value={this.state.darkTheme}
/>
<Field
value={this.state.accentColor}
label={_t('Accent Colour')}
onChange={this.onAccentColorChanged}
/>
</div>
<div>
<iframe
ref={this.appFrame}
width={700} height={450}
src={widgetUrl}
onLoad={this.onLoad}
/>
</div>
</BaseDialog>;
}
}

View File

@ -1844,6 +1844,10 @@
"Missing session data": "Missing session data",
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
"Your browser likely removed this data when running low on disk space.": "Your browser likely removed this data when running low on disk space.",
"Widget Proof of Concept Dashboard": "Widget Proof of Concept Dashboard",
"Look like Android": "Look like Android",
"Look like dark theme": "Look like dark theme",
"Accent Colour": "Accent Colour",
"Integration Manager": "Integration Manager",
"Find others by phone or email": "Find others by phone or email",
"Be found by phone or email": "Be found by phone or email",

View File

@ -0,0 +1,54 @@
/*
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 { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import { ActionPayload } from "../dispatcher/payloads";
import Modal, { IModal } from "../Modal";
import TempWidgetDialog from "../components/views/dialogs/TempWidgetDialog";
interface IState {
modal?: IModal<any>;
openedFromId?: string;
}
export class TempWidgetStore extends AsyncStoreWithClient<IState> {
private static internalInstance = new TempWidgetStore();
private constructor() {
super(defaultDispatcher, {});
}
public static get instance(): TempWidgetStore {
return TempWidgetStore.internalInstance;
}
protected async onAction(payload: ActionPayload): Promise<any> {
// nothing
}
public openTempWidget(requestData: any, sourceWidgetId: string) {
Modal.createTrackedDialog('Temp Widget', '', TempWidgetDialog, {
widgetDefinition: {...requestData},
sourceWidgetId: sourceWidgetId,
onFinished: (success) => {
if (!success) {
TempWidgetDialog.sendExitData(sourceWidgetId, false);
}
},
});
}
}

View File

@ -39,6 +39,12 @@ export enum KnownWidgetActions {
SetAlwaysOnScreen = "set_always_on_screen",
ClientReady = "im.vector.ready",
Terminate = "im.vector.terminate",
OpenTempWidget = "io.element.start_temp",
UpdateThemeInfo = "io.element.theme_info",
SendWidgetConfig = "io.element.widget_config",
CloseWidget = "io.element.exit",
ClosedWidgetResponse = "io.element.exit_response",
}
export type WidgetAction = KnownWidgetActions | string;
@ -134,6 +140,19 @@ export class WidgetApi extends EventEmitter {
// Save OpenID credentials
this.setOpenIDCredentials(<ToWidgetRequest>payload);
this.replyToRequest(<ToWidgetRequest>payload, {});
} else if (payload.action === KnownWidgetActions.UpdateThemeInfo
|| payload.action === KnownWidgetActions.SendWidgetConfig
|| payload.action === KnownWidgetActions.ClosedWidgetResponse) {
// Finalization needs to be async, so postpone with a promise
let finalizePromise = Promise.resolve();
const wait = (promise) => {
finalizePromise = finalizePromise.then(() => promise);
};
this.emit(payload.action, payload, wait);
Promise.resolve(finalizePromise).then(() => {
// Acknowledge that we're shut down now
this.replyToRequest(<ToWidgetRequest>payload, {});
});
} else {
console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
}
@ -203,9 +222,8 @@ export class WidgetApi extends EventEmitter {
response: {}, // Not used at this layer - it's used when the client responds
};
if (callback) {
this.inFlightRequests[request.requestId] = callback;
}
if (!callback) callback = () => {}; // noop
this.inFlightRequests[request.requestId] = callback;
console.log(`[WidgetAPI] Sending request: `, request);
window.parent.postMessage(request, "*");
@ -217,4 +235,18 @@ export class WidgetApi extends EventEmitter {
resolve(); // SetAlwaysOnScreen is currently fire-and-forget, but that could change.
});
}
public closeWidget(exitData: any): Promise<any> {
return new Promise<any>(resolve => {
this.callAction(KnownWidgetActions.CloseWidget, exitData, null);
resolve();
});
}
public openTempWidget(url: string, data: any): Promise<any> {
return new Promise<any>(resolve => {
this.callAction(KnownWidgetActions.OpenTempWidget, {url, data}, null);
resolve();
});
}
}