Add maximise widget functionality (#7098)
Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
This commit is contained in:
parent
2f4f3f2a8c
commit
556cfc7ed8
24 changed files with 418 additions and 233 deletions
|
@ -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>
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue