Switch widget resizing to re-resizable and add persistence
This commit is contained in:
parent
ae65ed5c2e
commit
cca5ccd79d
6 changed files with 124 additions and 69 deletions
|
@ -15,16 +15,43 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
|
||||||
Size settings
|
|
||||||
*/
|
|
||||||
$AppsDrawerMinHeight: 50px;
|
|
||||||
$AppsDrawerDefaultHeight: 300px;
|
|
||||||
$MiniAppTileHeight: 114px;
|
$MiniAppTileHeight: 114px;
|
||||||
|
|
||||||
.mx_AppsDrawer {
|
.mx_AppsDrawer {
|
||||||
margin: 5px;
|
margin: 5px 5px 5px 18px;
|
||||||
display: block;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.mx_RoomSublist_resizerHandles {
|
||||||
|
flex: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomSublist_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_RoomSublist_resizerHandle {
|
||||||
|
opacity: 0.8;
|
||||||
|
background: $primary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppsDrawer_hidden {
|
.mx_AppsDrawer_hidden {
|
||||||
|
@ -36,13 +63,13 @@ $MiniAppTileHeight: 114px;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: $AppsDrawerMinHeight;
|
height: 100%;
|
||||||
height: $AppsDrawerDefaultHeight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppsDrawer_minimised .mx_AppsContainer {
|
.mx_AppsDrawer_minimised .mx_AppsContainer {
|
||||||
min-height: inherit;
|
// override the re-resizable inline styles
|
||||||
height: inherit;
|
height: inherit !important;
|
||||||
|
min-height: inherit !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AddWidget_button {
|
.mx_AddWidget_button {
|
||||||
|
@ -70,15 +97,14 @@ $MiniAppTileHeight: 114px;
|
||||||
.mx_AppTile {
|
.mx_AppTile {
|
||||||
max-width: 960px;
|
max-width: 960px;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
margin-right: 5px;
|
|
||||||
border: 5px solid $widget-menu-bar-bg-color;
|
border: 5px solid $widget-menu-bar-bg-color;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppTile:last-child {
|
& + .mx_AppTile {
|
||||||
margin-right: 1px;
|
margin-left: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileFullWidth {
|
.mx_AppTileFullWidth {
|
||||||
|
@ -105,7 +131,7 @@ $MiniAppTileHeight: 114px;
|
||||||
.mx_AppTile.mx_AppTile_minimised,
|
.mx_AppTile.mx_AppTile_minimised,
|
||||||
.mx_AppTileFullWidth.mx_AppTile_minimised,
|
.mx_AppTileFullWidth.mx_AppTile_minimised,
|
||||||
.mx_AppTile_mini.mx_AppTile_minimised {
|
.mx_AppTile_mini.mx_AppTile_minimised {
|
||||||
min-height: inherit;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTile .mx_AppTile_persistedWrapper,
|
.mx_AppTile .mx_AppTile_persistedWrapper,
|
||||||
|
@ -117,7 +143,6 @@ $MiniAppTileHeight: 114px;
|
||||||
.mx_AppTile_persistedWrapper div {
|
.mx_AppTile_persistedWrapper div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 300px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileMenuBar {
|
.mx_AppTileMenuBar {
|
||||||
|
@ -402,7 +427,7 @@ form.mx_Custom_Widget_Form div {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppsDrawer_minimised .mx_ResizeHandle {
|
.mx_AppsDrawer_minimised .mx_RoomSublist_resizerHandle {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,10 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AppTile_persistedWrapper div {
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_IncomingCallBox {
|
.mx_IncomingCallBox {
|
||||||
min-width: 250px;
|
min-width: 250px;
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
|
|
|
@ -1547,9 +1547,9 @@ export default createReactClass({
|
||||||
|
|
||||||
// header + footer + status + give us at least 120px of scrollback at all times.
|
// header + footer + status + give us at least 120px of scrollback at all times.
|
||||||
let auxPanelMaxHeight = window.innerHeight -
|
let auxPanelMaxHeight = window.innerHeight -
|
||||||
(83 + // height of RoomHeader
|
(54 + // height of RoomHeader
|
||||||
36 + // height of the status area
|
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
|
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
|
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import {throttle} from "lodash";
|
||||||
import ResizeObserver from 'resize-observer-polyfill';
|
import ResizeObserver from 'resize-observer-polyfill';
|
||||||
|
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
@ -156,7 +156,7 @@ export default class PersistedElement extends React.Component {
|
||||||
child.style.display = visible ? 'block' : 'none';
|
child.style.display = visible ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
updateChildPosition(child, parent) {
|
updateChildPosition = throttle((child, parent) => {
|
||||||
if (!child || !parent) return;
|
if (!child || !parent) return;
|
||||||
|
|
||||||
const parentRect = parent.getBoundingClientRect();
|
const parentRect = parent.getBoundingClientRect();
|
||||||
|
@ -167,9 +167,9 @@ export default class PersistedElement extends React.Component {
|
||||||
width: parentRect.width + 'px',
|
width: parentRect.width + 'px',
|
||||||
height: parentRect.height + 'px',
|
height: parentRect.height + 'px',
|
||||||
});
|
});
|
||||||
}
|
}, 100, {trailing: true, leading: true});
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div ref={this.collectChildContainer}></div>;
|
return <div ref={this.collectChildContainer} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,8 @@ import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import ResizeHandle from '../elements/ResizeHandle';
|
import {Resizable} from "re-resizable";
|
||||||
import {Resizer, FixedDistributor} from '../../../resizer';
|
import {useLocalStorageState} from "../../../hooks/useLocalStorage";
|
||||||
|
|
||||||
// The maximum number of widgets that can be added in a room
|
// The maximum number of widgets that can be added in a room
|
||||||
const MAX_WIDGETS = 2;
|
const MAX_WIDGETS = 2;
|
||||||
|
@ -63,7 +63,6 @@ export default createReactClass({
|
||||||
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
|
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
|
||||||
WidgetEchoStore.on('update', this._updateApps);
|
WidgetEchoStore.on('update', this._updateApps);
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
this._createResizer();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -73,10 +72,6 @@ export default createReactClass({
|
||||||
}
|
}
|
||||||
WidgetEchoStore.removeListener('update', this._updateApps);
|
WidgetEchoStore.removeListener('update', this._updateApps);
|
||||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||||
if (this.resizer) {
|
|
||||||
this.resizer.detach();
|
|
||||||
this.resizer = null;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
|
@ -162,30 +157,6 @@ export default createReactClass({
|
||||||
this._launchManageIntegrations();
|
this._launchManageIntegrations();
|
||||||
},
|
},
|
||||||
|
|
||||||
_createResizer: function() {
|
|
||||||
if (!this.resizeContainer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const classNames = {
|
|
||||||
handle: "mx_ResizeHandle",
|
|
||||||
vertical: "mx_ResizeHandle_vertical",
|
|
||||||
resizing: "mx_AppsDrawer_resizing",
|
|
||||||
};
|
|
||||||
const resizer = new Resizer(
|
|
||||||
this.resizeContainer,
|
|
||||||
FixedDistributor,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
resizer.setClassNames(classNames);
|
|
||||||
resizer.attach();
|
|
||||||
this.resizer = resizer;
|
|
||||||
},
|
|
||||||
|
|
||||||
_setResizeContainerRef: function(div) {
|
|
||||||
this.resizeContainer = div;
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const apps = this.state.apps.map((app, index, arr) => {
|
const apps = this.state.apps.map((app, index, arr) => {
|
||||||
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
|
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
|
||||||
|
@ -193,7 +164,7 @@ export default createReactClass({
|
||||||
return (<AppTile
|
return (<AppTile
|
||||||
key={app.id}
|
key={app.id}
|
||||||
app={app}
|
app={app}
|
||||||
fullWidth={arr.length<2 ? true : false}
|
fullWidth={arr.length < 2}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
userId={this.props.userId}
|
userId={this.props.userId}
|
||||||
show={this.props.showApps}
|
show={this.props.showApps}
|
||||||
|
@ -204,8 +175,8 @@ export default createReactClass({
|
||||||
/>);
|
/>);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (apps.length == 0) {
|
if (apps.length === 0) {
|
||||||
return <div></div>;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let addWidget;
|
let addWidget;
|
||||||
|
@ -223,13 +194,6 @@ export default createReactClass({
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const containerStyle = {
|
|
||||||
maxHeight: Math.max(this.props.maxHeight - 50, 300),
|
|
||||||
};
|
|
||||||
if (!this.props.showApps && this.resizer) {
|
|
||||||
this.resizer.forHandleAt(0).item.clearSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
let spinner;
|
let spinner;
|
||||||
if (
|
if (
|
||||||
apps.length === 0 && WidgetEchoStore.roomHasPendingWidgets(
|
apps.length === 0 && WidgetEchoStore.roomHasPendingWidgets(
|
||||||
|
@ -249,14 +213,39 @@ export default createReactClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes} ref={this._setResizeContainerRef}>
|
<div className={classes}>
|
||||||
<div id='apps' className='mx_AppsContainer' style={containerStyle}>
|
<PersistentVResizer
|
||||||
|
id={"apps-drawer_" + this.props.room.roomId}
|
||||||
|
minHeight={100}
|
||||||
|
maxHeight={this.props.maxHeight - 50}
|
||||||
|
handleWrapperClass="mx_RoomSublist_resizerHandles"
|
||||||
|
handleClass="mx_RoomSublist_resizerHandle"
|
||||||
|
className="mx_AppsContainer"
|
||||||
|
>
|
||||||
{ apps }
|
{ apps }
|
||||||
{ spinner }
|
{ spinner }
|
||||||
</div>
|
</PersistentVResizer>
|
||||||
<ResizeHandle vertical={true} />
|
|
||||||
{ this._canUserModify() && addWidget }
|
{ this._canUserModify() && addWidget }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const PersistentVResizer = ({id, minHeight, maxHeight, className, handleWrapperClass, handleClass, children}) => {
|
||||||
|
const [height, setHeight] = useLocalStorageState("pvr_" + id, 100);
|
||||||
|
|
||||||
|
return <Resizable
|
||||||
|
size={{height: Math.min(height, maxHeight)}}
|
||||||
|
minHeight={minHeight}
|
||||||
|
maxHeight={maxHeight}
|
||||||
|
onResizeStop={(e, dir, ref, d) => {
|
||||||
|
setHeight(height + d.height);
|
||||||
|
}}
|
||||||
|
handleWrapperClass={handleWrapperClass}
|
||||||
|
handleClasses={{bottom: handleClass}}
|
||||||
|
className={className}
|
||||||
|
enable={{bottom: true}}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</Resizable>;
|
||||||
|
};
|
||||||
|
|
37
src/hooks/useLocalStorage.ts
Normal file
37
src/hooks/useLocalStorage.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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 {useEffect, useRef, useState} from "react";
|
||||||
|
|
||||||
|
// Hook behaving like useState but persisting the value to localStorage. Returns same as useState
|
||||||
|
export const useLocalStorageState = (key: string, initialValue: boolean) => {
|
||||||
|
const lsKey = useRef("useLocalStorageState_" + key).current;
|
||||||
|
|
||||||
|
const [value, setValue] = useState(() => {
|
||||||
|
try {
|
||||||
|
const item = window.localStorage.getItem(lsKey);
|
||||||
|
return item ? JSON.parse(item) : initialValue;
|
||||||
|
} catch (error) {
|
||||||
|
return initialValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.localStorage.setItem(lsKey, JSON.stringify(value));
|
||||||
|
}, [lsKey, value]);
|
||||||
|
|
||||||
|
return [value, setValue];
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue