Add maximise widget functionality (#7098)

Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
This commit is contained in:
Timo 2021-11-16 15:43:18 +01:00 committed by GitHub
parent 2f4f3f2a8c
commit 556cfc7ed8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 418 additions and 233 deletions

View file

@ -95,6 +95,7 @@ import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
import { dispatchShowThreadEvent } from '../../dispatcher/dispatch-actions/threads';
import { fetchInitialEvent } from "../../utils/EventUtils";
import { ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
import AppsDrawer from '../views/rooms/AppsDrawer';
const DEBUG = false;
let debuglog = function(msg: string) {};
@ -119,6 +120,13 @@ interface IRoomProps extends MatrixClientProps {
onRegistered?(credentials: IMatrixClientCreds): void;
}
// This defines the content of the mainSplit.
// If the mainSplit does not contain the Timeline, the chat is shown in the right panel.
enum MainSplitContentType {
Timeline,
MaximisedWidget,
// Video
}
export interface IRoomState {
room?: Room;
roomId?: string;
@ -188,6 +196,7 @@ export interface IRoomState {
rejecting?: boolean;
rejectError?: Error;
hasPinnedWidgets?: boolean;
mainSplitContentType?: MainSplitContentType;
dragCounter: number;
// whether or not a spaces context switch brought us here,
// if it did we don't want the room to be marked as read as soon as it is loaded.
@ -254,6 +263,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
showAvatarChanges: true,
showDisplaynameChanges: true,
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
mainSplitContentType: MainSplitContentType.Timeline,
dragCounter: 0,
timelineRenderingType: TimelineRenderingType.Room,
liveTimeline: undefined,
@ -306,18 +316,35 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
private onWidgetStoreUpdate = () => {
if (this.state.room) {
this.checkWidgets(this.state.room);
}
if (!this.state.room) return;
this.checkWidgets(this.state.room);
};
private onWidgetEchoStoreUpdate = () => {
if (!this.state.room) return;
this.checkWidgets(this.state.room);
};
private onWidgetLayoutChange = () => {
if (!this.state.room) return;
this.checkWidgets(this.state.room);
};
private checkWidgets = (room) => {
this.setState({
hasPinnedWidgets: WidgetLayoutStore.instance.getContainerWidgets(room, Container.Top).length > 0,
showApps: this.shouldShowApps(room),
hasPinnedWidgets: WidgetLayoutStore.instance.hasPinnedWidgets(this.state.room),
mainSplitContentType: this.getMainSplitContentType(),
showApps: this.shouldShowApps(this.state.room),
});
};
private getMainSplitContentType = () => {
// TODO-video check if video should be displayed in main panel
return (WidgetLayoutStore.instance.hasMaximisedWidget(this.state.room))
? MainSplitContentType.MaximisedWidget
: MainSplitContentType.Timeline;
};
private onReadReceiptsChange = () => {
this.setState({
showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId),
@ -504,18 +531,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
}
private onWidgetEchoStoreUpdate = () => {
if (!this.state.room) return;
this.setState({
hasPinnedWidgets: WidgetLayoutStore.instance.getContainerWidgets(this.state.room, Container.Top).length > 0,
showApps: this.shouldShowApps(this.state.room),
});
};
private onWidgetLayoutChange = () => {
this.onWidgetEchoStoreUpdate(); // we cheat here by calling the thing that matters
};
private setupRoom(room: Room, roomId: string, joining: boolean, shouldPeek: boolean) {
// if this is an unknown room then we're in one of three states:
// - This is a room we can peek into (search engine) (we can /peek)
@ -972,7 +987,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (this.unmounted) return;
// Attach a widget store listener only when we get a room
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
this.onWidgetLayoutChange(); // provoke an update
this.calculatePeekRules(room);
this.updatePreviewUrlVisibility(room);
@ -2094,6 +2108,38 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const showChatEffects = SettingsStore.getValue('showChatEffects');
// Decide what to show in the main split
let mainSplitBody = <React.Fragment>
{ auxPanel }
<div className={timelineClasses}>
{ fileDropTarget }
{ topUnreadMessagesBar }
{ jumpToBottom }
{ messagePanel }
{ searchResultsPanel }
</div>
{ statusBarArea }
{ previewBar }
{ messageComposer }
</React.Fragment>;
switch (this.state.mainSplitContentType) {
case MainSplitContentType.Timeline:
// keep the timeline in as the mainSplitBody
break;
case MainSplitContentType.MaximisedWidget:
if (!SettingsStore.getValue("feature_maximised_widgets")) break;
mainSplitBody = <AppsDrawer
room={this.state.room}
userId={this.context.credentials.userId}
resizeNotifier={this.props.resizeNotifier}
showApps={true}
/>;
break;
// TODO-video MainSplitContentType.Video:
// break;
}
return (
<RoomContext.Provider value={this.state}>
<main className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
@ -2115,17 +2161,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
/>
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
<div className="mx_RoomView_body">
{ auxPanel }
<div className={timelineClasses}>
{ fileDropTarget }
{ topUnreadMessagesBar }
{ jumpToBottom }
{ messagePanel }
{ searchResultsPanel }
</div>
{ statusBarArea }
{ previewBar }
{ messageComposer }
{ mainSplitBody }
</div>
</MainSplit>
</ErrorBoundary>

View file

@ -40,7 +40,7 @@ import WidgetAvatar from "../avatars/WidgetAvatar";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Room } from "matrix-js-sdk/src/models/room";
import { IApp } from "../../../stores/WidgetStore";
import { WidgetLayoutStore, Container } from "../../../stores/widgets/WidgetLayoutStore";
interface IProps {
app: IApp;
// If room is not specified then it is an account level widget
@ -400,6 +400,14 @@ export default class AppTile extends React.Component<IProps, IState> {
{ target: '_blank', href: this.sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click();
};
private onMaxMinWidgetClick = (): void => {
const targetContainer =
WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, Container.Center)
? Container.Right
: Container.Center;
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, targetContainer);
};
private onContextMenuClick = (): void => {
this.setState({ menuDisplayed: true });
};
@ -522,6 +530,23 @@ export default class AppTile extends React.Component<IProps, IState> {
/>
);
}
let maxMinButton;
if (SettingsStore.getValue("feature_maximised_widgets")) {
const widgetIsMaximised = WidgetLayoutStore.instance.
isInContainer(this.props.room, this.props.app, Container.Center);
maxMinButton = <AccessibleButton
className={
"mx_AppTileMenuBar_iconButton"
+ (widgetIsMaximised
? " mx_AppTileMenuBar_iconButton_minWidget"
: " mx_AppTileMenuBar_iconButton_maxWidget")
}
title={
widgetIsMaximised ? _t('Close'): _t('Maximise widget')
}
onClick={this.onMaxMinWidgetClick}
/>;
}
return <React.Fragment>
<div className={appTileClasses} id={this.props.app.id}>
@ -531,6 +556,7 @@ export default class AppTile extends React.Component<IProps, IState> {
{ this.props.showTitle && this.getTileTitle() }
</span>
<span className="mx_AppTileMenuBarWidgets">
{ maxMinButton }
{ (this.props.showPopout && !this.state.requiresClient) && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
title={_t('Popout widget')}

View file

@ -138,14 +138,28 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
mx_RoomSummaryCard_Button_pinned: isPinned,
});
const isMaximised = WidgetLayoutStore.instance.isInContainer(room, app, Container.Center);
const toggleMaximised = isMaximised
? () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Right); }
: () => { WidgetLayoutStore.instance.moveToContainer(room, app, Container.Center); };
const maximiseTitle = isMaximised ? _t("Close") : _t("Maximise widget");
let openTitle = "";
if (isPinned) {
openTitle = _t("Unpin this widget to view it in this panel");
} else if (isMaximised) {
openTitle =_t("Close this widget to view it in this panel");
}
return <div className={classes} ref={handle}>
<AccessibleTooltipButton
className="mx_RoomSummaryCard_icon_app"
onClick={onOpenWidgetClick}
// only show a tooltip if the widget is pinned
title={isPinned ? _t("Unpin a widget to view it in this panel") : ""}
forceHide={!isPinned}
disabled={isPinned}
title={openTitle}
forceHide={!(isPinned || isMaximised)}
disabled={isPinned || isMaximised}
yOffset={-48}
>
<WidgetAvatar app={app} />
@ -154,7 +168,10 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
</AccessibleTooltipButton>
<ContextMenuTooltipButton
className="mx_RoomSummaryCard_app_options"
className={classNames({
"mx_RoomSummaryCard_app_options": true,
"mx_RoomSummaryCard_maximised_widget": SettingsStore.getValue("feature_maximised_widgets"),
})}
isExpanded={menuDisplayed}
onClick={openMenu}
title={_t("Options")}
@ -168,6 +185,13 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
disabled={cannotPin}
yOffset={-24}
/>
{ SettingsStore.getValue("feature_maximised_widgets") &&
<AccessibleTooltipButton
className={isMaximised ? "mx_RoomSummaryCard_app_minimise" : "mx_RoomSummaryCard_app_maximise"}
onClick={toggleMaximised}
title={maximiseTitle}
yOffset={-24}
/> }
{ contextMenu }
</div>;

View file

@ -47,7 +47,8 @@ interface IProps {
}
interface IState {
apps: IApp[];
// @ts-ignore - TS wants a string key, but we know better
apps: {[id: Container]: IApp[]};
resizingVertical: boolean; // true when changing the height of the apps drawer
resizingHorizontal: boolean; // true when chagning the distribution of the width between widgets
resizing: boolean;
@ -118,7 +119,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
this.resizeContainer.classList.remove("mx_AppsDrawer_resizing");
WidgetLayoutStore.instance.setResizerDistributions(
this.props.room, Container.Top,
this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
this.topApps().slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
);
this.setState({ resizingHorizontal: false });
},
@ -148,7 +149,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
// Room has changed, update apps
this.updateApps();
} else if (this.getAppsHash(this.state.apps) !== this.getAppsHash(prevState.apps)) {
} else if (this.getAppsHash(this.topApps()) !== this.getAppsHash(prevState.apps[Container.Top])) {
this.loadResizerPreferences();
}
}
@ -163,7 +164,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
private loadResizerPreferences = (): void => {
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
if (this.state.apps && (this.state.apps.length - 1) === distributions.length) {
if (this.state.apps && (this.topApps().length - 1) === distributions.length) {
distributions.forEach((size, i) => {
const distributor = this.resizer.forHandleAt(i);
if (distributor) {
@ -200,8 +201,16 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
break;
}
};
private getApps = (): IApp[] => WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
// @ts-ignore - TS wants a string key, but we know better
private getApps = (): { [id: Container]: IApp[] } => {
// @ts-ignore
const appsDict: { [id: Container]: IApp[] } = {};
appsDict[Container.Top] = WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top);
appsDict[Container.Center] = WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Center);
return appsDict;
};
private topApps = (): IApp[] => this.state.apps[Container.Top];
private centerApps = (): IApp[] => this.state.apps[Container.Center];
private updateApps = (): void => {
this.setState({
@ -211,8 +220,9 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
public render(): JSX.Element {
if (!this.props.showApps) return <div />;
const apps = this.state.apps.map((app, index, arr) => {
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}
@ -242,33 +252,42 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
const classes = classNames({
mx_AppsDrawer: true,
mx_AppsDrawer_maximise: widgetIsMaxmised,
mx_AppsDrawer_fullWidth: apps.length < 2,
mx_AppsDrawer_resizing: this.state.resizing,
mx_AppsDrawer_2apps: apps.length === 2,
mx_AppsDrawer_3apps: apps.length === 3,
});
const appConatiners =
<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} />
{ app }
</React.Fragment>;
}) }
</div>;
let drawer;
if (widgetIsMaxmised) {
drawer = appConatiners;
} else {
drawer = <PersistentVResizer
room={this.props.room}
minHeight={100}
maxHeight={(this.props.maxHeight || !widgetIsMaxmised) ? this.props.maxHeight - 50 : undefined}
handleClass="mx_AppsContainer_resizerHandle"
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
className="mx_AppsContainer_resizer"
resizeNotifier={this.props.resizeNotifier}>
{ appConatiners }
</PersistentVResizer>;
}
return (
<div className={classes}>
<PersistentVResizer
room={this.props.room}
minHeight={100}
maxHeight={this.props.maxHeight ? this.props.maxHeight - 50 : undefined}
handleClass="mx_AppsContainer_resizerHandle"
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
className="mx_AppsContainer_resizer"
resizeNotifier={this.props.resizeNotifier}
>
<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} />
{ app }
</React.Fragment>;
}) }
</div>
</PersistentVResizer>
{ drawer }
{ spinner }
</div>
);