mirror of https://github.com/vector-im/riot-web
Extremely bad support for "temporary widgets"
parent
c1d9d96702
commit
342f1d5b43
|
@ -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'});
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue