Make EC widget theme reactive - Update widget url when the theme changes (#12295)

* update widget url when the theme changes

Signed-off-by: Timo K <toger5@hotmail.de>

* quick "make it EC specific" workaround proposal.

Signed-off-by: Timo K <toger5@hotmail.de>

* use `matches`

Signed-off-by: Timo K <toger5@hotmail.de>

* test coverage

Signed-off-by: Timo K <toger5@hotmail.de>

* more test coverage

Signed-off-by: Timo K <toger5@hotmail.de>

* fix jest

Signed-off-by: Timo K <toger5@hotmail.de>

* add tests for theme changes

Signed-off-by: Timo K <toger5@hotmail.de>

* update snapshots

Signed-off-by: Timo K <toger5@hotmail.de>

* test for theme update with non ec widget

Signed-off-by: Timo K <toger5@hotmail.de>

* add dark custom theme widget url

Signed-off-by: Timo K <toger5@hotmail.de>

* trigger conditions for theme cleanup

Signed-off-by: Timo K <toger5@hotmail.de>

* update tests using testId

Signed-off-by: Timo K <toger5@hotmail.de>

* use typed event emitter for theme watcher

Signed-off-by: Timo K <toger5@hotmail.de>

* simplify condition

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
This commit is contained in:
Timo 2024-03-13 15:52:41 +01:00 committed by GitHub
parent 80c4c3c28c
commit c42562ef39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 172 additions and 23 deletions

View file

@ -57,6 +57,7 @@ import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingSto
import { SdkContextClass } from "../../../contexts/SDKContext";
import { ModuleRunner } from "../../../modules/ModuleRunner";
import { parseUrl } from "../../../utils/UrlUtils";
import ThemeWatcher, { ThemeWatcherEvents } from "../../../settings/watchers/ThemeWatcher";
interface IProps {
app: IWidget | IApp;
@ -115,6 +116,7 @@ interface IState {
menuDisplayed: boolean;
requiresClient: boolean;
hasContextMenuOptions: boolean;
widgetUrl?: string;
}
export default class AppTile extends React.Component<IProps, IState> {
@ -140,7 +142,7 @@ export default class AppTile extends React.Component<IProps, IState> {
private sgWidget: StopGapWidget | null;
private dispatcherRef?: string;
private unmounted = false;
private themeWatcher = new ThemeWatcher();
public constructor(props: IProps, context: ContextType<typeof MatrixClientContext>) {
super(props);
this.context = context; // XXX: workaround for lack of `declare` support on `public context!:` definition
@ -267,6 +269,7 @@ export default class AppTile extends React.Component<IProps, IState> {
!newProps.userWidget,
newProps.onDeleteClick,
),
widgetUrl: this.sgWidget?.embedUrl,
};
}
@ -352,6 +355,8 @@ export default class AppTile extends React.Component<IProps, IState> {
}
private setupSgListeners(): void {
this.themeWatcher.on(ThemeWatcherEvents.ThemeChange, this.onThemeChanged);
this.themeWatcher.start();
this.sgWidget?.on("ready", this.onWidgetReady);
this.sgWidget?.on("error:preparing", this.updateRequiresClient);
// emits when the capabilities have been set up or changed
@ -359,7 +364,9 @@ export default class AppTile extends React.Component<IProps, IState> {
}
private stopSgListeners(): void {
this.themeWatcher.stop();
if (!this.sgWidget) return;
this.themeWatcher.off(ThemeWatcherEvents.ThemeChange, this.onThemeChanged);
this.sgWidget?.off("ready", this.onWidgetReady);
this.sgWidget.off("error:preparing", this.updateRequiresClient);
this.sgWidget.off("capabilitiesNotified", this.updateRequiresClient);
@ -382,6 +389,7 @@ export default class AppTile extends React.Component<IProps, IState> {
private startWidget(): void {
this.sgWidget?.prepare().then(() => {
if (this.unmounted) return;
if (!this.state.initialising) return;
this.setState({ initialising: false });
});
}
@ -456,6 +464,17 @@ export default class AppTile extends React.Component<IProps, IState> {
});
};
private onThemeChanged = (): void => {
// Regenerate widget url when the theme changes
// this updates the url from e.g. `theme=light` to `theme=dark`
// We only do this with EC widgets where the theme prop is in the hash. If the widget puts the
// theme template variable outside the url hash this would cause a (IFrame) page reload on every theme change.
if (WidgetType.CALL.matches(this.props.app.type)) this.setState({ widgetUrl: this.sgWidget?.embedUrl });
// TODO: This is a stop gap solution to responsively update the theme of the widget.
// A new action should be introduced and the widget driver should be called here, so it informs the widget. (or connect to this by itself)
};
private onAction = (payload: ActionPayload): void => {
switch (payload.action) {
case "m.sticker":
@ -548,9 +567,9 @@ export default class AppTile extends React.Component<IProps, IState> {
this.resetWidget(this.props);
this.startMessaging();
if (this.iframe && this.sgWidget) {
if (this.iframe && this.state.widgetUrl) {
// Reload iframe
this.iframe.src = this.sgWidget.embedUrl;
this.iframe.src = this.state.widgetUrl;
}
});
}
@ -619,7 +638,7 @@ export default class AppTile extends React.Component<IProps, IState> {
"mx_AppTileBody--mini": this.props.miniMode,
"mx_AppTileBody--loading": this.state.loading,
// We don't want mx_AppTileBody (rounded corners) for call widgets
"mx_AppTileBody--call": this.props.app.type === WidgetType.CALL.preferred,
"mx_AppTileBody--call": WidgetType.CALL.matches(this.props.app.type),
});
const appTileBodyStyles: CSSProperties = {};
if (this.props.pointerEvents) {
@ -648,7 +667,7 @@ export default class AppTile extends React.Component<IProps, IState> {
<AppPermission
roomId={this.props.room.roomId}
creatorUserId={this.props.creatorUserId}
url={this.sgWidget.embedUrl}
url={this.state.widgetUrl}
isRoomEncrypted={isEncrypted}
onPermissionGranted={this.grantWidgetPermission}
/>
@ -676,7 +695,7 @@ export default class AppTile extends React.Component<IProps, IState> {
title={widgetTitle}
allow={iframeFeatures}
ref={this.iframeRefChange}
src={this.sgWidget.embedUrl}
src={this.state.widgetUrl}
allowFullScreen={true}
sandbox={sandboxFlags}
/>
@ -699,7 +718,12 @@ export default class AppTile extends React.Component<IProps, IState> {
const zIndexAboveOtherPersistentElements = 101;
appTileBody = (
<div className="mx_AppTile_persistedWrapper">
<div
className="mx_AppTile_persistedWrapper"
// We store the widget url to make it possible to test the value of the widgetUrl. since the iframe itself wont be here. (PersistedElement are in a different dom tree)
data-test-widget-url={this.state.widgetUrl}
data-testid="widget-app-tile"
>
<PersistedElement
zIndex={this.props.miniMode ? zIndexAboveOtherPersistentElements : 9}
persistKey={this.persistKey}