Files
ThreadNet-Web/src/components/views/rooms/AppsDrawer.tsx
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

363 lines
13 KiB
TypeScript
Raw Normal View History

2017-05-17 23:21:02 +01:00
/*
2024-09-09 14:57:16 +01:00
Copyright 2018-2024 New Vector Ltd.
2017-06-28 12:26:05 +01:00
Copyright 2017 Vector Creations Ltd
2017-05-17 23:21:02 +01:00
2024-09-09 14:57:16 +01:00
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
2017-05-17 23:21:02 +01:00
*/
import React, { AriaRole } from "react";
2020-09-08 10:19:51 +01:00
import classNames from "classnames";
import { Resizable, Size } from "re-resizable";
import { Room } from "matrix-js-sdk/src/matrix";
import { IWidget } from "matrix-widget-api";
2020-09-08 10:19:51 +01:00
import AppTile from "../elements/AppTile";
2020-05-13 20:41:41 -06:00
import dis from "../../../dispatcher/dispatcher";
2019-12-19 18:19:56 -07:00
import * as ScalarMessaging from "../../../ScalarMessaging";
2018-06-26 11:59:16 +01:00
import WidgetUtils from "../../../utils/WidgetUtils";
2018-07-03 11:16:44 +01:00
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
2020-09-02 11:13:00 +01:00
import ResizeNotifier from "../../../utils/ResizeNotifier";
2020-10-13 12:29:12 +01:00
import ResizeHandle from "../elements/ResizeHandle";
2023-07-07 13:37:26 +01:00
import Resizer, { IConfig } from "../../../resizer/resizer";
2020-10-13 12:29:12 +01:00
import PercentageDistributor from "../../../resizer/distributors/percentage";
2021-06-29 13:11:58 +01:00
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { clamp, percentageOf, percentageWithin } from "../../../utils/numbers";
import UIStore from "../../../stores/UIStore";
2021-09-20 16:20:00 +02:00
import { ActionPayload } from "../../../dispatcher/payloads";
import Spinner from "../elements/Spinner";
import SdkConfig from "../../../SdkConfig";
2021-09-20 16:20:00 +02:00
interface IProps {
userId: string;
room: Room;
resizeNotifier: ResizeNotifier;
showApps?: boolean; // Should apps be rendered
maxHeight: number;
role?: AriaRole;
2021-09-20 16:20:00 +02:00
}
interface IState {
apps: {
[Container.Top]: IWidget[];
[Container.Center]: IWidget[];
[Container.Right]?: IWidget[];
};
2021-09-20 16:20:00 +02:00
resizingVertical: boolean; // true when changing the height of the apps drawer
resizingHorizontal: boolean; // true when changing the distribution of the width between widgets
2021-09-20 16:20:00 +02:00
resizing: boolean;
}
2017-05-30 13:47:17 +01:00
2021-09-20 16:20:00 +02:00
export default class AppsDrawer extends React.Component<IProps, IState> {
2022-01-05 16:14:44 +01:00
private unmounted = false;
private resizeContainer?: HTMLDivElement;
2023-07-07 13:37:26 +01:00
private resizer: Resizer<IConfig>;
private dispatcherRef?: string;
2021-09-20 16:20:00 +02:00
public static defaultProps: Partial<IProps> = {
showApps: true,
2020-08-29 12:14:16 +01:00
};
2017-06-13 15:28:37 +02:00
public constructor(props: IProps) {
2020-08-29 12:14:16 +01:00
super(props);
this.state = {
2021-09-20 16:20:00 +02:00
apps: this.getApps(),
resizingVertical: false,
resizingHorizontal: false,
resizing: false,
2017-06-28 12:54:47 +01:00
};
2020-10-13 12:29:12 +01:00
2021-09-20 16:20:00 +02:00
this.resizer = this.createResizer();
2020-10-19 13:08:11 +01:00
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
2020-08-29 12:14:16 +01:00
}
2017-06-28 12:54:47 +01:00
2021-09-20 16:20:00 +02:00
public componentDidMount(): void {
ScalarMessaging.startListening();
2021-09-20 16:20:00 +02:00
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps);
2017-08-21 15:34:13 +02:00
this.dispatcherRef = dis.register(this.onAction);
2020-08-29 12:14:16 +01:00
}
2017-05-17 23:21:02 +01:00
2021-09-20 16:20:00 +02:00
public componentWillUnmount(): void {
2022-01-05 16:14:44 +01:00
this.unmounted = true;
ScalarMessaging.stopListening();
2021-09-20 16:20:00 +02:00
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps);
dis.unregister(this.dispatcherRef);
2021-09-20 16:20:00 +02:00
if (this.resizeContainer) {
2020-10-13 12:29:12 +01:00
this.resizer.detach();
}
2020-10-19 13:08:11 +01:00
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
2020-08-29 12:14:16 +01:00
}
2021-09-20 16:20:00 +02:00
private onIsResizing = (resizing: boolean): void => {
2021-03-05 17:52:51 +00:00
// This one is the vertical, ie. change height of apps drawer
this.setState({ resizingVertical: resizing });
2020-10-19 13:08:11 +01:00
if (!resizing) {
2021-09-20 16:20:00 +02:00
this.relaxResizer();
2020-10-19 13:08:11 +01:00
}
};
2023-07-07 13:37:26 +01:00
private createResizer(): Resizer<IConfig> {
2021-03-05 17:52:51 +00:00
// This is the horizontal one, changing the distribution of the width between the app tiles
// (ie. a vertical resize handle because, the handle itself is vertical...)
2020-10-13 12:29:12 +01:00
const classNames = {
handle: "mx_ResizeHandle",
2023-05-04 15:19:55 +00:00
vertical: "mx_ResizeHandle--vertical",
reverse: "mx_ResizeHandle_reverse",
2020-10-13 12:29:12 +01:00
};
const collapseConfig = {
onResizeStart: () => {
this.resizeContainer?.classList.add("mx_AppsDrawer--resizing");
2021-03-05 17:52:51 +00:00
this.setState({ resizingHorizontal: true });
2020-10-13 12:29:12 +01:00
},
onResizeStop: () => {
this.resizeContainer?.classList.remove("mx_AppsDrawer--resizing");
WidgetLayoutStore.instance.setResizerDistributions(
this.props.room,
Container.Top,
2021-11-16 15:43:18 +01:00
this.topApps()
.slice(1)
.map((_, i) => this.resizer.forHandleAt(i)!.size),
);
2021-03-05 17:52:51 +00:00
this.setState({ resizingHorizontal: false });
2020-10-13 12:29:12 +01:00
},
};
// pass a truthy container for now, we won't call attach until we update it
2021-09-20 16:20:00 +02:00
const resizer = new Resizer(null, PercentageDistributor, collapseConfig);
2020-10-13 12:29:12 +01:00
resizer.setClassNames(classNames);
return resizer;
}
2021-09-20 16:20:00 +02:00
private collectResizer = (ref: HTMLDivElement): void => {
if (this.resizeContainer) {
2020-10-13 12:29:12 +01:00
this.resizer.detach();
}
if (ref) {
this.resizer.container = ref;
this.resizer.attach();
}
2021-09-20 16:20:00 +02:00
this.resizeContainer = ref;
this.loadResizerPreferences();
2020-10-13 12:29:12 +01:00
};
private getAppsHash = (apps: IWidget[]): string => apps.map((app) => app.id).join("~");
2020-10-13 12:29:12 +01:00
2021-09-20 16:20:00 +02:00
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
// Room has changed, update apps
2021-09-20 16:20:00 +02:00
this.updateApps();
2021-11-16 15:43:18 +01:00
} else if (this.getAppsHash(this.topApps()) !== this.getAppsHash(prevState.apps[Container.Top])) {
2021-09-20 16:20:00 +02:00
this.loadResizerPreferences();
2020-10-15 11:14:48 +01:00
}
}
2021-09-20 16:20:00 +02:00
private relaxResizer = (): void => {
2020-10-19 13:08:11 +01:00
const distributors = this.resizer.getDistributors();
// relax all items if they had any overconstrained flexboxes
distributors.forEach((d) => d.start());
distributors.forEach((d) => d.finish());
};
2021-09-20 16:20:00 +02:00
private loadResizerPreferences = (): void => {
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
2021-11-16 15:43:18 +01:00
if (this.state.apps && this.topApps().length - 1 === distributions.length) {
distributions.forEach((size, i) => {
const distributor = this.resizer.forHandleAt(i);
if (distributor) {
distributor.size = size;
distributor.finish();
}
});
} else if (this.state.apps) {
2020-10-15 11:14:48 +01:00
const distributors = this.resizer.getDistributors();
distributors.forEach((d) => d.item.clearSize());
2020-10-20 16:07:47 +01:00
distributors.forEach((d) => d.start());
2020-10-15 11:14:48 +01:00
distributors.forEach((d) => d.finish());
2020-10-13 12:29:12 +01:00
}
};
2021-09-20 16:20:00 +02:00
private isResizing(): boolean {
2021-03-05 17:52:51 +00:00
return this.state.resizingVertical || this.state.resizingHorizontal;
}
2021-09-20 16:20:00 +02:00
private onAction = (action: ActionPayload): void => {
const hideWidgetKey = this.props.room.roomId + "_hide_widget_drawer";
switch (action.action) {
case "appsDrawer":
2020-04-09 14:47:20 -06:00
// Note: these booleans are awkward because localstorage is fundamentally
// string-based. We also do exact equality on the strings later on.
if (action.show) {
2020-04-09 14:47:20 -06:00
localStorage.setItem(hideWidgetKey, "false");
} else {
// Store hidden state of widget
// Don't show if previously hidden
2020-04-09 14:47:20 -06:00
localStorage.setItem(hideWidgetKey, "true");
}
break;
}
2020-08-29 12:14:16 +01:00
};
2023-02-13 11:39:16 +00:00
private getApps = (): IState["apps"] => ({
[Container.Top]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top),
[Container.Center]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Center),
});
private topApps = (): IWidget[] => this.state.apps[Container.Top];
private centerApps = (): IWidget[] => this.state.apps[Container.Center];
2017-06-13 15:28:37 +02:00
2021-09-20 16:20:00 +02:00
private updateApps = (): void => {
2022-01-05 16:14:44 +01:00
if (this.unmounted) return;
2017-06-13 15:28:37 +02:00
this.setState({
2021-09-20 16:20:00 +02:00
apps: this.getApps(),
2020-10-15 11:14:48 +01:00
});
2020-08-29 12:14:16 +01:00
};
2017-06-13 15:28:37 +02:00
public render(): React.ReactNode {
2020-10-09 08:42:21 +01:00
if (!this.props.showApps) return <div />;
2021-11-16 15:43:18 +01:00
const widgetIsMaxmised: boolean = this.centerApps().length > 0;
const appsToDisplay = widgetIsMaxmised ? this.centerApps() : this.topApps();
const apps = appsToDisplay.map((app, index, arr) => {
return (
<AppTile
key={app.id}
app={app}
fullWidth={arr.length < 2}
room={this.props.room}
userId={this.props.userId}
creatorUserId={app.creatorUserId}
2020-08-28 10:50:27 +01:00
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
2021-03-05 17:52:51 +00:00
pointerEvents={this.isResizing() ? "none" : undefined}
/>
);
});
2017-05-22 12:34:27 +01:00
if (apps.length === 0) {
return <div />;
2018-05-25 03:17:29 +01:00
}
2018-07-03 11:16:44 +01:00
let spinner;
if (
apps.length === 0 &&
WidgetEchoStore.roomHasPendingWidgets(this.props.room.roomId, WidgetUtils.getRoomWidgets(this.props.room))
) {
spinner = <Spinner />;
2018-07-03 11:16:44 +01:00
}
const classes = classNames({
"mx_AppsDrawer": true,
"mx_AppsDrawer--maximised": widgetIsMaxmised,
"mx_AppsDrawer--resizing": this.state.resizing,
"mx_AppsDrawer--2apps": apps.length === 2,
"mx_AppsDrawer--3apps": apps.length === 3,
});
const appContainers = (
2021-11-16 15:43:18 +01:00
<div className="mx_AppsContainer" ref={this.collectResizer}>
{apps.map((app, i) => {
if (i < 1) return app;
return (
<React.Fragment key={app.key}>
<ResizeHandle reverse={i > apps.length / 2} />
2021-11-16 15:43:18 +01:00
{app}
</React.Fragment>
);
})}
</div>
);
let drawer;
if (widgetIsMaxmised) {
drawer = appContainers;
2021-11-16 15:43:18 +01:00
} else {
drawer = (
<PersistentVResizer
room={this.props.room}
minHeight={100}
maxHeight={this.props.maxHeight - 50}
className="mx_AppsDrawer_resizer"
handleWrapperClass="mx_AppsDrawer_resizer_container"
handleClass="mx_AppsDrawer_resizer_container_handle"
2021-11-16 15:43:18 +01:00
resizeNotifier={this.props.resizeNotifier}
>
{appContainers}
2021-11-16 15:43:18 +01:00
</PersistentVResizer>
);
}
2017-05-17 23:21:02 +01:00
return (
<div role={this.props.role} className={classes}>
2021-11-16 15:43:18 +01:00
{drawer}
2020-10-13 12:29:12 +01:00
{spinner}
2017-05-17 23:21:02 +01:00
</div>
);
2020-08-29 12:14:16 +01:00
}
}
2021-09-20 16:20:00 +02:00
interface IPersistentResizerProps {
room: Room;
minHeight: number;
maxHeight: number;
className: string;
handleWrapperClass: string;
handleClass: string;
resizeNotifier: ResizeNotifier;
children: React.ReactNode;
}
const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
2021-01-18 19:27:11 -07:00
room,
2020-09-02 11:13:00 +01:00
minHeight,
maxHeight,
className,
handleWrapperClass,
handleClass,
resizeNotifier,
children,
}) => {
2021-01-18 19:27:11 -07:00
let defaultHeight = WidgetLayoutStore.instance.getContainerHeight(room, Container.Top);
// Arbitrary defaults to avoid NaN problems. 100 px or 3/4 of the visible window.
if (!minHeight) minHeight = 100;
if (!maxHeight) maxHeight = (UIStore.instance.windowHeight / 4) * 3;
2021-01-18 19:27:11 -07:00
// Convert from percentage to height. Note that the default height is 280px.
if (defaultHeight) {
defaultHeight = clamp(defaultHeight, 0, 100);
defaultHeight = percentageWithin(defaultHeight / 100, minHeight, maxHeight);
} else {
defaultHeight = SdkConfig.get().default_widget_container_height ?? 280;
2021-01-18 19:27:11 -07:00
}
return (
<Resizable
// types do not support undefined height/width
// but resizable code checks specifically for undefined on Size prop
size={{ height: Math.min(defaultHeight, maxHeight), width: undefined } as unknown as Size}
minHeight={minHeight}
maxHeight={maxHeight}
2020-08-21 16:38:28 +01:00
onResizeStart={() => {
resizeNotifier.startResizing();
2020-08-21 16:38:28 +01:00
}}
2020-09-02 11:13:00 +01:00
onResize={() => {
resizeNotifier.notifyTimelineHeightChanged();
}}
onResizeStop={(e, dir, ref, d) => {
let newHeight = defaultHeight! + d.height;
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
WidgetLayoutStore.instance.setContainerHeight(room, Container.Top, newHeight);
resizeNotifier.stopResizing();
}}
className={className}
handleWrapperClass={handleWrapperClass}
2021-06-29 13:11:58 +01:00
handleClasses={{ bottom: handleClass }}
enable={{ bottom: true }}
>
{children}
</Resizable>
);
};