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 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} from "./widgets/WidgetApi";
|
import {Capability, KnownWidgetActions} from "./widgets/WidgetApi";
|
||||||
import {objectClone} from "./utils/objects";
|
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 WIDGET_API_VERSION = '0.0.2'; // Current API version
|
||||||
const SUPPORTED_WIDGET_API_VERSIONS = [
|
const SUPPORTED_WIDGET_API_VERSIONS = [
|
||||||
|
@ -218,8 +220,12 @@ export default class FromWidgetPostMessageApi {
|
||||||
if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
|
if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
|
||||||
ActiveWidgetStore.setWidgetPersistence(widgetId, val);
|
ActiveWidgetStore.setWidgetPersistence(widgetId, val);
|
||||||
}
|
}
|
||||||
} else if (action === 'get_openid') {
|
} else if (action === 'get_openid'
|
||||||
|
|| action === KnownWidgetActions.CloseWidget) {
|
||||||
// Handled by caller
|
// Handled by caller
|
||||||
|
} else if (action === KnownWidgetActions.OpenTempWidget) {
|
||||||
|
TempWidgetStore.instance.openTempWidget(event.data.data, widgetId);
|
||||||
|
this.sendResponse(event, {}); // ack
|
||||||
} 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'});
|
||||||
|
|
|
@ -28,7 +28,7 @@ import AsyncWrapper from './AsyncWrapper';
|
||||||
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
||||||
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
|
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
|
||||||
|
|
||||||
interface IModal<T extends any[]> {
|
export interface IModal<T extends any[]> {
|
||||||
elem: React.ReactNode;
|
elem: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
beforeClosePromise?: Promise<boolean>;
|
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() {
|
start() {
|
||||||
this.fromWidget.addEndpoint(this.widgetId, this.renderedUrl);
|
this.fromWidget.addEndpoint(this.widgetId, this.renderedUrl);
|
||||||
this.fromWidget.addListener("get_openid", this._onOpenIdRequest);
|
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",
|
"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.",
|
"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.",
|
"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",
|
"Integration Manager": "Integration Manager",
|
||||||
"Find others by phone or email": "Find others by phone or email",
|
"Find others by phone or email": "Find others by phone or email",
|
||||||
"Be found by phone or email": "Be found 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",
|
SetAlwaysOnScreen = "set_always_on_screen",
|
||||||
ClientReady = "im.vector.ready",
|
ClientReady = "im.vector.ready",
|
||||||
Terminate = "im.vector.terminate",
|
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;
|
export type WidgetAction = KnownWidgetActions | string;
|
||||||
|
@ -134,6 +140,19 @@ export class WidgetApi extends EventEmitter {
|
||||||
// Save OpenID credentials
|
// Save OpenID credentials
|
||||||
this.setOpenIDCredentials(<ToWidgetRequest>payload);
|
this.setOpenIDCredentials(<ToWidgetRequest>payload);
|
||||||
this.replyToRequest(<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 {
|
} else {
|
||||||
console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
|
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
|
response: {}, // Not used at this layer - it's used when the client responds
|
||||||
};
|
};
|
||||||
|
|
||||||
if (callback) {
|
if (!callback) callback = () => {}; // noop
|
||||||
this.inFlightRequests[request.requestId] = 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, "*");
|
||||||
|
@ -217,4 +235,18 @@ export class WidgetApi extends EventEmitter {
|
||||||
resolve(); // SetAlwaysOnScreen is currently fire-and-forget, but that could change.
|
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