Merge pull request #5138 from matrix-org/t3chguy/dpsah/6785

Allow persistent resizing of the widget app drawer
pull/21833/head
Michael Telatynski 2020-09-08 09:47:11 +01:00 committed by GitHub
commit ddba5c6223
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 292 additions and 67 deletions

View File

@ -15,18 +15,39 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/*
the tile title bar is 5 (top border) + 12 (title, buttons) + 5 (bottom padding) px = 22px
the body is assumed to be 300px (assumed by at least the sticker pickerm, perhaps elsewhere),
so the body height would be 300px - 22px (room for title bar) = 278px
BUT! the sticker picker also assumes it's a little less high than that because the iframe
for the sticker picker doesn't have any padding or margin on it's bottom.
so subtracking another 5px, which brings us at 273px.
*/
$AppsDrawerBodyHeight: 273px;
$MiniAppTileHeight: 114px;
.mx_AppsDrawer {
margin: 5px;
margin: 5px 5px 5px 18px;
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
.mx_AppsContainer_resizerHandle {
cursor: ns-resize;
border-radius: 3px;
// Override styles from library
width: unset !important;
height: 4px !important;
// This is positioned directly below frame
position: absolute;
bottom: -8px !important; // override from library
// Together, these make the bar 64px wide
// These are also overridden from the library
left: calc(50% - 32px) !important;
right: calc(50% - 32px) !important;
}
&:hover {
.mx_AppsContainer_resizerHandle {
opacity: 0.8;
background: $primary-fg-color;
}
}
}
.mx_AppsDrawer_hidden {
@ -36,15 +57,23 @@ $AppsDrawerBodyHeight: 273px;
.mx_AppsContainer {
display: flex;
flex-direction: row;
align-items: center;
align-items: stretch;
justify-content: center;
height: 100%;
margin-bottom: 8px;
}
.mx_AppsDrawer_minimised .mx_AppsContainer {
// override the re-resizable inline styles
height: inherit !important;
min-height: inherit !important;
}
.mx_AddWidget_button {
order: 2;
cursor: pointer;
padding: 0;
margin: 5px auto 5px auto;
margin: -3px auto 5px 0;
color: $accent-color;
font-size: $font-12px;
}
@ -65,40 +94,52 @@ $AppsDrawerBodyHeight: 273px;
.mx_AppTile {
max-width: 960px;
width: 50%;
margin-right: 5px;
border: 5px solid $widget-menu-bar-bg-color;
border-radius: 4px;
}
display: flex;
flex-direction: column;
.mx_AppTile:last-child {
margin-right: 1px;
& + .mx_AppTile {
margin-left: 5px;
}
}
.mx_AppTileFullWidth {
max-width: 960px;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
border: 5px solid $widget-menu-bar-bg-color;
border-radius: 8px;
display: flex;
flex-direction: column;
}
.mx_AppTile_mini {
max-width: 960px;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
height: $MiniAppTileHeight;
}
.mx_AppTile_persistedWrapper {
height: $AppsDrawerBodyHeight;
.mx_AppTile.mx_AppTile_minimised,
.mx_AppTileFullWidth.mx_AppTile_minimised,
.mx_AppTile_mini.mx_AppTile_minimised {
height: 14px;
}
.mx_AppTile .mx_AppTile_persistedWrapper,
.mx_AppTileFullWidth .mx_AppTile_persistedWrapper,
.mx_AppTile_mini .mx_AppTile_persistedWrapper {
height: 114px;
min-width: 300px;
flex: 1;
}
.mx_AppTile_persistedWrapper div {
width: 100%;
height: 100%;
}
.mx_AppTileMenuBar {
@ -110,6 +151,7 @@ $AppsDrawerBodyHeight: 273px;
align-items: center;
justify-content: space-between;
cursor: pointer;
width: 100%;
}
.mx_AppTileMenuBar_expanded {
@ -172,7 +214,7 @@ $AppsDrawerBodyHeight: 273px;
}
.mx_AppTileBody {
height: $AppsDrawerBodyHeight;
height: 100%;
width: 100%;
overflow: hidden;
}
@ -183,6 +225,13 @@ $AppsDrawerBodyHeight: 273px;
overflow: hidden;
}
.mx_AppTile .mx_AppTileBody,
.mx_AppTileFullWidth .mx_AppTileBody,
.mx_AppTile_mini .mx_AppTileBody_mini {
height: inherit;
flex: 1;
}
.mx_AppTileBody_mini iframe {
border: none;
width: 100%;
@ -191,7 +240,7 @@ $AppsDrawerBodyHeight: 273px;
.mx_AppTileBody iframe {
width: 100%;
height: $AppsDrawerBodyHeight;
height: 100%;
overflow: hidden;
border: none;
padding: 0;
@ -331,7 +380,7 @@ form.mx_Custom_Widget_Form div {
align-items: center;
font-weight: bold;
position: relative;
height: $AppsDrawerBodyHeight;
height: 100%;
}
.mx_AppLoading .mx_Spinner {
@ -358,3 +407,16 @@ form.mx_Custom_Widget_Form div {
.mx_AppLoading iframe {
display: none;
}
.mx_AppsDrawer_minimised .mx_AppsContainer_resizerHandle {
display: none;
}
/* Avoid apptile iframes capturing mouse event focus when resizing */
.mx_AppsDrawer_resizing iframe {
pointer-events: none;
}
.mx_AppsDrawer_resizing .mx_AppTile_persistedWrapper {
z-index: 1;
}

View File

@ -36,6 +36,10 @@ limitations under the License.
}
}
.mx_AppTile_persistedWrapper div {
min-width: 300px;
}
.mx_IncomingCallBox {
min-width: 250px;
background-color: $primary-bg-color;

View File

@ -1322,7 +1322,7 @@ export default class GroupView extends React.Component {
</div>
<GroupHeaderButtons />
</div>
<MainSplit panel={rightPanel}>
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
<AutoHideScrollbar className="mx_GroupView_body">
{ this._getMembershipSection() }
{ this._getGroupSection() }

View File

@ -257,6 +257,12 @@ class LoggedInView extends React.Component<IProps, IState> {
window.localStorage.setItem("mx_lhs_size", '' + size);
this.props.resizeNotifier.notifyLeftHandleResized();
},
onResizeStart: () => {
this.props.resizeNotifier.startResizing();
},
onResizeStop: () => {
this.props.resizeNotifier.stopResizing();
},
};
const resizer = new Resizer(
this._resizeContainer.current,
@ -650,12 +656,13 @@ class LoggedInView extends React.Component<IProps, IState> {
break;
case PageTypes.UserView:
pageElement = <UserView userId={this.props.currentUserId} />;
pageElement = <UserView userId={this.props.currentUserId} resizeNotifier={this.props.resizeNotifier} />;
break;
case PageTypes.GroupView:
pageElement = <GroupView
groupId={this.props.currentGroupId}
isNew={this.props.currentGroupIsNew}
resizeNotifier={this.props.resizeNotifier}
/>;
break;
}

View File

@ -19,9 +19,18 @@ import React from 'react';
import { Resizable } from 're-resizable';
export default class MainSplit extends React.Component {
_onResized = (event, direction, refToElement, delta) => {
_onResizeStart = () => {
this.props.resizeNotifier.startResizing();
};
_onResize = () => {
this.props.resizeNotifier.notifyRightHandleResized();
};
_onResizeStop = (event, direction, refToElement, delta) => {
this.props.resizeNotifier.stopResizing();
window.localStorage.setItem("mx_rhs_size", this._loadSidePanelSize().width + delta.width);
}
};
_loadSidePanelSize() {
let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10);
@ -58,7 +67,9 @@ export default class MainSplit extends React.Component {
bottomLeft: false,
topLeft: false,
}}
onResizeStop={this._onResized}
onResizeStart={this._onResizeStart}
onResize={this._onResize}
onResizeStop={this._onResizeStop}
className="mx_RightPanel_ResizeWrapper"
handleClasses={{left: "mx_RightPanel_ResizeHandle"}}
>

View File

@ -1545,9 +1545,9 @@ export default class RoomView extends React.Component {
// header + footer + status + give us at least 120px of scrollback at all times.
let auxPanelMaxHeight = window.innerHeight -
(83 + // height of RoomHeader
(54 + // height of RoomHeader
36 + // height of the status area
72 + // minimum height of the message compmoser
51 + // minimum height of the message compmoser
120); // amount of desired scrollback
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
@ -1884,15 +1884,19 @@ export default class RoomView extends React.Component {
}
const auxPanel = (
<AuxPanel room={this.state.room}
fullHeight={false}
userId={this.context.credentials.userId}
conferenceHandler={this.props.ConferenceHandler}
draggingFile={this.state.draggingFile}
displayConfCallNotification={this.state.displayConfCallNotification}
maxHeight={this.state.auxPanelMaxHeight}
showApps={this.state.showApps}
hideAppsDrawer={false} >
<AuxPanel
room={this.state.room}
fullHeight={false}
userId={this.context.credentials.userId}
conferenceHandler={this.props.ConferenceHandler}
draggingFile={this.state.draggingFile}
displayConfCallNotification={this.state.displayConfCallNotification}
maxHeight={this.state.auxPanelMaxHeight}
showApps={this.state.showApps}
hideAppsDrawer={false}
onResize={this.onResize}
resizeNotifier={this.props.resizeNotifier}
>
{ aux }
</AuxPanel>
);
@ -2090,10 +2094,7 @@ export default class RoomView extends React.Component {
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus}
/>
<MainSplit
panel={rightPanel}
resizeNotifier={this.props.resizeNotifier}
>
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
<div className={fadableSectionClasses}>
{auxPanel}
<div className={timelineClasses}>

View File

@ -163,7 +163,7 @@ export default class ScrollPanel extends React.Component {
this._pendingFillRequests = {b: null, f: null};
if (this.props.resizeNotifier) {
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
}
this.resetScrollState();
@ -193,11 +193,12 @@ export default class ScrollPanel extends React.Component {
this.unmounted = true;
if (this.props.resizeNotifier) {
this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
this.props.resizeNotifier.removeListener("middlePanelResizedNoisy", this.onResize);
}
}
onScroll = ev => {
if (this.props.resizeNotifier.isResizing) return; // skip scroll events caused by resizing
debuglog("onScroll", this._getScrollNode().scrollTop);
this._scrollTimeout.restart();
this._saveScrollState();
@ -207,6 +208,7 @@ export default class ScrollPanel extends React.Component {
};
onResize = () => {
debuglog("onResize");
this.checkScroll();
// update preventShrinkingState if present
if (this.preventShrinkingState) {
@ -236,7 +238,6 @@ export default class ScrollPanel extends React.Component {
// when scrolled all the way down. E.g. Chrome 72 on debian.
// so check difference <= 1;
return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) <= 1;
};
// returns the vertical height in the given direction that can be removed from

View File

@ -80,7 +80,9 @@ export default class UserView extends React.Component {
const RightPanel = sdk.getComponent('structures.RightPanel');
const MainSplit = sdk.getComponent('structures.MainSplit');
const panel = <RightPanel user={this.state.member} />;
return (<MainSplit panel={panel}><HomePage /></MainSplit>);
return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}>
<HomePage />
</MainSplit>);
} else {
return (<div />);
}

View File

@ -804,14 +804,16 @@ export default class AppTile extends React.Component {
const showMinimiseButton = this.props.showMinimise && this.props.show;
const showMaximiseButton = this.props.showMinimise && !this.props.show;
let appTileClass;
let appTileClasses;
if (this.props.miniMode) {
appTileClass = 'mx_AppTile_mini';
appTileClasses = {mx_AppTile_mini: true};
} else if (this.props.fullWidth) {
appTileClass = 'mx_AppTileFullWidth';
appTileClasses = {mx_AppTileFullWidth: true};
} else {
appTileClass = 'mx_AppTile';
appTileClasses = {mx_AppTile: true};
}
appTileClasses.mx_AppTile_minimised = !this.props.show;
appTileClasses = classNames(appTileClasses);
const menuBarClasses = classNames({
mx_AppTileMenuBar: true,
@ -843,7 +845,7 @@ export default class AppTile extends React.Component {
}
return <React.Fragment>
<div className={appTileClass} id={this.props.app.id}>
<div className={appTileClasses} id={this.props.app.id}>
{ this.props.showMenubar &&
<div ref={this._menu_bar} className={menuBarClasses} onClick={this.onClickMenuBar}>
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>

View File

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import {throttle} from "lodash";
import ResizeObserver from 'resize-observer-polyfill';
import dis from '../../../dispatcher/dispatcher';
@ -156,7 +156,7 @@ export default class PersistedElement extends React.Component {
child.style.display = visible ? 'block' : 'none';
}
updateChildPosition(child, parent) {
updateChildPosition = throttle((child, parent) => {
if (!child || !parent) return;
const parentRect = parent.getBoundingClientRect();
@ -167,9 +167,9 @@ export default class PersistedElement extends React.Component {
width: parentRect.width + 'px',
height: parentRect.height + 'px',
});
}
}, 100, {trailing: true, leading: true});
render() {
return <div ref={this.collectChildContainer}></div>;
return <div ref={this.collectChildContainer} />;
}
}

View File

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {useState} from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import AppTile from '../elements/AppTile';
@ -29,6 +29,10 @@ import WidgetEchoStore from "../../../stores/WidgetEchoStore";
import AccessibleButton from '../elements/AccessibleButton';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import classNames from 'classnames';
import {Resizable} from "re-resizable";
import {useLocalStorageState} from "../../../hooks/useLocalStorageState";
import ResizeNotifier from "../../../utils/ResizeNotifier";
// The maximum number of widgets that can be added in a room
const MAX_WIDGETS = 2;
@ -37,6 +41,7 @@ export default class AppsDrawer extends React.Component {
static propTypes = {
userId: PropTypes.string.isRequired,
room: PropTypes.object.isRequired,
resizeNotifier: PropTypes.instanceOf(ResizeNotifier).isRequired,
showApps: PropTypes.bool, // Should apps be rendered
hide: PropTypes.bool, // If rendered, should apps drawer be visible
};
@ -161,7 +166,7 @@ export default class AppsDrawer extends React.Component {
return (<AppTile
key={app.id}
app={app}
fullWidth={arr.length<2 ? true : false}
fullWidth={arr.length < 2}
room={this.props.room}
userId={this.props.userId}
show={this.props.showApps}
@ -172,8 +177,8 @@ export default class AppsDrawer extends React.Component {
/>);
});
if (apps.length == 0) {
return <div></div>;
if (apps.length === 0) {
return <div />;
}
let addWidget;
@ -202,14 +207,68 @@ export default class AppsDrawer extends React.Component {
spinner = <Loader />;
}
const classes = classNames({
"mx_AppsDrawer": true,
"mx_AppsDrawer_hidden": this.props.hide,
"mx_AppsDrawer_fullWidth": apps.length < 2,
"mx_AppsDrawer_minimised": !this.props.showApps,
});
return (
<div className={'mx_AppsDrawer' + (this.props.hide ? ' mx_AppsDrawer_hidden' : '')}>
<div id='apps' className='mx_AppsContainer'>
<div className={classes}>
<PersistentVResizer
id={"apps-drawer_" + this.props.room.roomId}
minHeight={100}
maxHeight={this.props.maxHeight ? this.props.maxHeight - 50 : undefined}
handleClass="mx_AppsContainer_resizerHandle"
className="mx_AppsContainer"
resizeNotifier={this.props.resizeNotifier}
>
{ apps }
{ spinner }
</div>
</PersistentVResizer>
{ this._canUserModify() && addWidget }
</div>
);
}
}
const PersistentVResizer = ({
id,
minHeight,
maxHeight,
className,
handleWrapperClass,
handleClass,
resizeNotifier,
children,
}) => {
const [height, setHeight] = useLocalStorageState("pvr_" + id, 100);
const [resizing, setResizing] = useState(false);
return <Resizable
size={{height: Math.min(height, maxHeight)}}
minHeight={minHeight}
maxHeight={maxHeight}
onResizeStart={() => {
if (!resizing) setResizing(true);
resizeNotifier.startResizing();
}}
onResize={() => {
resizeNotifier.notifyTimelineHeightChanged();
}}
onResizeStop={(e, dir, ref, d) => {
setHeight(height + d.height);
if (resizing) setResizing(false);
resizeNotifier.stopResizing();
}}
handleWrapperClass={handleWrapperClass}
handleClasses={{bottom: handleClass}}
className={classNames(className, {
mx_AppsDrawer_resizing: resizing,
})}
enable={{bottom: true}}
>
{ children }
</Resizable>;
};

View File

@ -204,6 +204,7 @@ export default class AuxPanel extends React.Component {
maxHeight={this.props.maxHeight}
showApps={this.props.showApps}
hide={this.props.hideAppsDrawer}
resizeNotifier={this.props.resizeNotifier}
/>;
let stateViews = null;

View File

@ -0,0 +1,44 @@
/*
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 {Dispatch, SetStateAction, useCallback, useEffect, useState} from "react";
const getValue = <T>(key: string, initialValue: T): T => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
};
// Hook behaving like useState but persisting the value to localStorage. Returns same as useState
export const useLocalStorageState = <T>(key: string, initialValue: T) => {
const lsKey = "mx_" + key;
const [value, setValue] = useState<T>(getValue(lsKey, initialValue));
useEffect(() => {
setValue(getValue(lsKey, initialValue));
}, [lsKey, initialValue]);
const _setValue: Dispatch<SetStateAction<T>> = useCallback((v: T) => {
window.localStorage.setItem(lsKey, JSON.stringify(v));
setValue(v);
}, [lsKey]);
return [value, _setValue];
};

View File

@ -105,6 +105,9 @@ export default class Resizer {
if (this.classNames.resizing) {
this.container.classList.add(this.classNames.resizing);
}
if (this.config.onResizeStart) {
this.config.onResizeStart();
}
const {sizer, distributor} = this._createSizerAndDistributor(resizeHandle);
distributor.start();
@ -119,6 +122,9 @@ export default class Resizer {
if (this.classNames.resizing) {
this.container.classList.remove(this.classNames.resizing);
}
if (this.config.onResizeStop) {
this.config.onResizeStop();
}
distributor.finish();
body.removeEventListener("mouseup", finishResize, false);
document.removeEventListener("mouseleave", finishResize, false);

View File

@ -56,6 +56,18 @@ export default class Sizer {
return this.vertical ? this.container.offsetTop : this.container.offsetLeft;
}
/** @return {number} container offset to document */
_getPageOffset() {
let element = this.container;
let offset = 0;
while (element) {
const pos = this.vertical ? element.offsetTop : element.offsetLeft;
offset = offset + pos;
element = element.offsetParent;
}
return offset;
}
setItemSize(item, size) {
if (this.vertical) {
item.style.height = `${Math.round(size)}px`;
@ -80,9 +92,9 @@ export default class Sizer {
offsetFromEvent(event) {
const pos = this.vertical ? event.pageY : event.pageX;
if (this.reverse) {
return (this._getOffset() + this.getTotalSize()) - pos;
return (this._getPageOffset() + this.getTotalSize()) - pos;
} else {
return pos - this._getOffset();
return pos - this._getPageOffset();
}
}
}

View File

@ -31,6 +31,19 @@ export default class ResizeNotifier extends EventEmitter {
// with default options, will call fn once at first call, and then every x ms
// if there was another call in that timespan
this._throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200);
this._isResizing = false;
}
get isResizing() {
return this._isResizing;
}
startResizing() {
this._isResizing = true;
}
stopResizing() {
this._isResizing = false;
}
_noisyMiddlePanel() {