mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 17:56:01 +03:00
Merge pull request #5138 from matrix-org/t3chguy/dpsah/6785
Allow persistent resizing of the widget app drawer
This commit is contained in:
commit
ddba5c6223
16 changed files with 292 additions and 67 deletions
|
@ -15,18 +15,39 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
$MiniAppTileHeight: 114px;
|
||||||
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;
|
|
||||||
|
|
||||||
.mx_AppsDrawer {
|
.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 {
|
.mx_AppsDrawer_hidden {
|
||||||
|
@ -36,15 +57,23 @@ $AppsDrawerBodyHeight: 273px;
|
||||||
.mx_AppsContainer {
|
.mx_AppsContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
justify-content: center;
|
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 {
|
.mx_AddWidget_button {
|
||||||
order: 2;
|
order: 2;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 5px auto 5px auto;
|
margin: -3px auto 5px 0;
|
||||||
color: $accent-color;
|
color: $accent-color;
|
||||||
font-size: $font-12px;
|
font-size: $font-12px;
|
||||||
}
|
}
|
||||||
|
@ -65,40 +94,52 @@ $AppsDrawerBodyHeight: 273px;
|
||||||
.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;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.mx_AppTile:last-child {
|
& + .mx_AppTile {
|
||||||
margin-right: 1px;
|
margin-left: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileFullWidth {
|
.mx_AppTileFullWidth {
|
||||||
max-width: 960px;
|
max-width: 960px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 5px solid $widget-menu-bar-bg-color;
|
border: 5px solid $widget-menu-bar-bg-color;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTile_mini {
|
.mx_AppTile_mini {
|
||||||
max-width: 960px;
|
max-width: 960px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: $MiniAppTileHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTile_persistedWrapper {
|
.mx_AppTile.mx_AppTile_minimised,
|
||||||
height: $AppsDrawerBodyHeight;
|
.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 {
|
.mx_AppTile_mini .mx_AppTile_persistedWrapper {
|
||||||
height: 114px;
|
flex: 1;
|
||||||
min-width: 300px;
|
}
|
||||||
|
|
||||||
|
.mx_AppTile_persistedWrapper div {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileMenuBar {
|
.mx_AppTileMenuBar {
|
||||||
|
@ -110,6 +151,7 @@ $AppsDrawerBodyHeight: 273px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileMenuBar_expanded {
|
.mx_AppTileMenuBar_expanded {
|
||||||
|
@ -172,7 +214,7 @@ $AppsDrawerBodyHeight: 273px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileBody {
|
.mx_AppTileBody {
|
||||||
height: $AppsDrawerBodyHeight;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
@ -183,6 +225,13 @@ $AppsDrawerBodyHeight: 273px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AppTile .mx_AppTileBody,
|
||||||
|
.mx_AppTileFullWidth .mx_AppTileBody,
|
||||||
|
.mx_AppTile_mini .mx_AppTileBody_mini {
|
||||||
|
height: inherit;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_AppTileBody_mini iframe {
|
.mx_AppTileBody_mini iframe {
|
||||||
border: none;
|
border: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -191,7 +240,7 @@ $AppsDrawerBodyHeight: 273px;
|
||||||
|
|
||||||
.mx_AppTileBody iframe {
|
.mx_AppTileBody iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: $AppsDrawerBodyHeight;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -331,7 +380,7 @@ form.mx_Custom_Widget_Form div {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: $AppsDrawerBodyHeight;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppLoading .mx_Spinner {
|
.mx_AppLoading .mx_Spinner {
|
||||||
|
@ -358,3 +407,16 @@ form.mx_Custom_Widget_Form div {
|
||||||
.mx_AppLoading iframe {
|
.mx_AppLoading iframe {
|
||||||
display: none;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -1322,7 +1322,7 @@ export default class GroupView extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
<GroupHeaderButtons />
|
<GroupHeaderButtons />
|
||||||
</div>
|
</div>
|
||||||
<MainSplit panel={rightPanel}>
|
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||||
<AutoHideScrollbar className="mx_GroupView_body">
|
<AutoHideScrollbar className="mx_GroupView_body">
|
||||||
{ this._getMembershipSection() }
|
{ this._getMembershipSection() }
|
||||||
{ this._getGroupSection() }
|
{ this._getGroupSection() }
|
||||||
|
|
|
@ -257,6 +257,12 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
window.localStorage.setItem("mx_lhs_size", '' + size);
|
window.localStorage.setItem("mx_lhs_size", '' + size);
|
||||||
this.props.resizeNotifier.notifyLeftHandleResized();
|
this.props.resizeNotifier.notifyLeftHandleResized();
|
||||||
},
|
},
|
||||||
|
onResizeStart: () => {
|
||||||
|
this.props.resizeNotifier.startResizing();
|
||||||
|
},
|
||||||
|
onResizeStop: () => {
|
||||||
|
this.props.resizeNotifier.stopResizing();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const resizer = new Resizer(
|
const resizer = new Resizer(
|
||||||
this._resizeContainer.current,
|
this._resizeContainer.current,
|
||||||
|
@ -650,12 +656,13 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PageTypes.UserView:
|
case PageTypes.UserView:
|
||||||
pageElement = <UserView userId={this.props.currentUserId} />;
|
pageElement = <UserView userId={this.props.currentUserId} resizeNotifier={this.props.resizeNotifier} />;
|
||||||
break;
|
break;
|
||||||
case PageTypes.GroupView:
|
case PageTypes.GroupView:
|
||||||
pageElement = <GroupView
|
pageElement = <GroupView
|
||||||
groupId={this.props.currentGroupId}
|
groupId={this.props.currentGroupId}
|
||||||
isNew={this.props.currentGroupIsNew}
|
isNew={this.props.currentGroupIsNew}
|
||||||
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
/>;
|
/>;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,18 @@ import React from 'react';
|
||||||
import { Resizable } from 're-resizable';
|
import { Resizable } from 're-resizable';
|
||||||
|
|
||||||
export default class MainSplit extends React.Component {
|
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);
|
window.localStorage.setItem("mx_rhs_size", this._loadSidePanelSize().width + delta.width);
|
||||||
}
|
};
|
||||||
|
|
||||||
_loadSidePanelSize() {
|
_loadSidePanelSize() {
|
||||||
let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10);
|
let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10);
|
||||||
|
@ -58,7 +67,9 @@ export default class MainSplit extends React.Component {
|
||||||
bottomLeft: false,
|
bottomLeft: false,
|
||||||
topLeft: false,
|
topLeft: false,
|
||||||
}}
|
}}
|
||||||
onResizeStop={this._onResized}
|
onResizeStart={this._onResizeStart}
|
||||||
|
onResize={this._onResize}
|
||||||
|
onResizeStop={this._onResizeStop}
|
||||||
className="mx_RightPanel_ResizeWrapper"
|
className="mx_RightPanel_ResizeWrapper"
|
||||||
handleClasses={{left: "mx_RightPanel_ResizeHandle"}}
|
handleClasses={{left: "mx_RightPanel_ResizeHandle"}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1545,9 +1545,9 @@ export default class RoomView extends React.Component {
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -1884,15 +1884,19 @@ export default class RoomView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const auxPanel = (
|
const auxPanel = (
|
||||||
<AuxPanel room={this.state.room}
|
<AuxPanel
|
||||||
fullHeight={false}
|
room={this.state.room}
|
||||||
userId={this.context.credentials.userId}
|
fullHeight={false}
|
||||||
conferenceHandler={this.props.ConferenceHandler}
|
userId={this.context.credentials.userId}
|
||||||
draggingFile={this.state.draggingFile}
|
conferenceHandler={this.props.ConferenceHandler}
|
||||||
displayConfCallNotification={this.state.displayConfCallNotification}
|
draggingFile={this.state.draggingFile}
|
||||||
maxHeight={this.state.auxPanelMaxHeight}
|
displayConfCallNotification={this.state.displayConfCallNotification}
|
||||||
showApps={this.state.showApps}
|
maxHeight={this.state.auxPanelMaxHeight}
|
||||||
hideAppsDrawer={false} >
|
showApps={this.state.showApps}
|
||||||
|
hideAppsDrawer={false}
|
||||||
|
onResize={this.onResize}
|
||||||
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
|
>
|
||||||
{ aux }
|
{ aux }
|
||||||
</AuxPanel>
|
</AuxPanel>
|
||||||
);
|
);
|
||||||
|
@ -2090,10 +2094,7 @@ export default class RoomView extends React.Component {
|
||||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||||
e2eStatus={this.state.e2eStatus}
|
e2eStatus={this.state.e2eStatus}
|
||||||
/>
|
/>
|
||||||
<MainSplit
|
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||||
panel={rightPanel}
|
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
|
||||||
>
|
|
||||||
<div className={fadableSectionClasses}>
|
<div className={fadableSectionClasses}>
|
||||||
{auxPanel}
|
{auxPanel}
|
||||||
<div className={timelineClasses}>
|
<div className={timelineClasses}>
|
||||||
|
|
|
@ -163,7 +163,7 @@ export default class ScrollPanel extends React.Component {
|
||||||
this._pendingFillRequests = {b: null, f: null};
|
this._pendingFillRequests = {b: null, f: null};
|
||||||
|
|
||||||
if (this.props.resizeNotifier) {
|
if (this.props.resizeNotifier) {
|
||||||
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
|
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.resetScrollState();
|
this.resetScrollState();
|
||||||
|
@ -193,11 +193,12 @@ export default class ScrollPanel extends React.Component {
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
|
|
||||||
if (this.props.resizeNotifier) {
|
if (this.props.resizeNotifier) {
|
||||||
this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
|
this.props.resizeNotifier.removeListener("middlePanelResizedNoisy", this.onResize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onScroll = ev => {
|
onScroll = ev => {
|
||||||
|
if (this.props.resizeNotifier.isResizing) return; // skip scroll events caused by resizing
|
||||||
debuglog("onScroll", this._getScrollNode().scrollTop);
|
debuglog("onScroll", this._getScrollNode().scrollTop);
|
||||||
this._scrollTimeout.restart();
|
this._scrollTimeout.restart();
|
||||||
this._saveScrollState();
|
this._saveScrollState();
|
||||||
|
@ -207,6 +208,7 @@ export default class ScrollPanel extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
onResize = () => {
|
onResize = () => {
|
||||||
|
debuglog("onResize");
|
||||||
this.checkScroll();
|
this.checkScroll();
|
||||||
// update preventShrinkingState if present
|
// update preventShrinkingState if present
|
||||||
if (this.preventShrinkingState) {
|
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.
|
// when scrolled all the way down. E.g. Chrome 72 on debian.
|
||||||
// so check difference <= 1;
|
// so check difference <= 1;
|
||||||
return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) <= 1;
|
return Math.abs(sn.scrollHeight - (sn.scrollTop + sn.clientHeight)) <= 1;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// returns the vertical height in the given direction that can be removed from
|
// returns the vertical height in the given direction that can be removed from
|
||||||
|
|
|
@ -80,7 +80,9 @@ export default class UserView extends React.Component {
|
||||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||||
const MainSplit = sdk.getComponent('structures.MainSplit');
|
const MainSplit = sdk.getComponent('structures.MainSplit');
|
||||||
const panel = <RightPanel user={this.state.member} />;
|
const panel = <RightPanel user={this.state.member} />;
|
||||||
return (<MainSplit panel={panel}><HomePage /></MainSplit>);
|
return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}>
|
||||||
|
<HomePage />
|
||||||
|
</MainSplit>);
|
||||||
} else {
|
} else {
|
||||||
return (<div />);
|
return (<div />);
|
||||||
}
|
}
|
||||||
|
|
|
@ -804,14 +804,16 @@ export default class AppTile extends React.Component {
|
||||||
const showMinimiseButton = this.props.showMinimise && this.props.show;
|
const showMinimiseButton = this.props.showMinimise && this.props.show;
|
||||||
const showMaximiseButton = this.props.showMinimise && !this.props.show;
|
const showMaximiseButton = this.props.showMinimise && !this.props.show;
|
||||||
|
|
||||||
let appTileClass;
|
let appTileClasses;
|
||||||
if (this.props.miniMode) {
|
if (this.props.miniMode) {
|
||||||
appTileClass = 'mx_AppTile_mini';
|
appTileClasses = {mx_AppTile_mini: true};
|
||||||
} else if (this.props.fullWidth) {
|
} else if (this.props.fullWidth) {
|
||||||
appTileClass = 'mx_AppTileFullWidth';
|
appTileClasses = {mx_AppTileFullWidth: true};
|
||||||
} else {
|
} else {
|
||||||
appTileClass = 'mx_AppTile';
|
appTileClasses = {mx_AppTile: true};
|
||||||
}
|
}
|
||||||
|
appTileClasses.mx_AppTile_minimised = !this.props.show;
|
||||||
|
appTileClasses = classNames(appTileClasses);
|
||||||
|
|
||||||
const menuBarClasses = classNames({
|
const menuBarClasses = classNames({
|
||||||
mx_AppTileMenuBar: true,
|
mx_AppTileMenuBar: true,
|
||||||
|
@ -843,7 +845,7 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
<div className={appTileClass} id={this.props.app.id}>
|
<div className={appTileClasses} id={this.props.app.id}>
|
||||||
{ this.props.showMenubar &&
|
{ this.props.showMenubar &&
|
||||||
<div ref={this._menu_bar} className={menuBarClasses} onClick={this.onClickMenuBar}>
|
<div ref={this._menu_bar} className={menuBarClasses} onClick={this.onClickMenuBar}>
|
||||||
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
||||||
|
|
|
@ -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} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {useState} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import AppTile from '../elements/AppTile';
|
import AppTile from '../elements/AppTile';
|
||||||
|
@ -29,6 +29,10 @@ import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
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 {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
|
// The maximum number of widgets that can be added in a room
|
||||||
const MAX_WIDGETS = 2;
|
const MAX_WIDGETS = 2;
|
||||||
|
@ -37,6 +41,7 @@ export default class AppsDrawer extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
userId: PropTypes.string.isRequired,
|
userId: PropTypes.string.isRequired,
|
||||||
room: PropTypes.object.isRequired,
|
room: PropTypes.object.isRequired,
|
||||||
|
resizeNotifier: PropTypes.instanceOf(ResizeNotifier).isRequired,
|
||||||
showApps: PropTypes.bool, // Should apps be rendered
|
showApps: PropTypes.bool, // Should apps be rendered
|
||||||
hide: PropTypes.bool, // If rendered, should apps drawer be visible
|
hide: PropTypes.bool, // If rendered, should apps drawer be visible
|
||||||
};
|
};
|
||||||
|
@ -161,7 +166,7 @@ export default class AppsDrawer extends React.Component {
|
||||||
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}
|
||||||
|
@ -172,8 +177,8 @@ export default class AppsDrawer extends React.Component {
|
||||||
/>);
|
/>);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (apps.length == 0) {
|
if (apps.length === 0) {
|
||||||
return <div></div>;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let addWidget;
|
let addWidget;
|
||||||
|
@ -202,14 +207,68 @@ export default class AppsDrawer extends React.Component {
|
||||||
spinner = <Loader />;
|
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 (
|
return (
|
||||||
<div className={'mx_AppsDrawer' + (this.props.hide ? ' mx_AppsDrawer_hidden' : '')}>
|
<div className={classes}>
|
||||||
<div id='apps' className='mx_AppsContainer'>
|
<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 }
|
{ apps }
|
||||||
{ spinner }
|
{ spinner }
|
||||||
</div>
|
</PersistentVResizer>
|
||||||
{ this._canUserModify() && addWidget }
|
{ this._canUserModify() && addWidget }
|
||||||
</div>
|
</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>;
|
||||||
|
};
|
||||||
|
|
|
@ -204,6 +204,7 @@ export default class AuxPanel extends React.Component {
|
||||||
maxHeight={this.props.maxHeight}
|
maxHeight={this.props.maxHeight}
|
||||||
showApps={this.props.showApps}
|
showApps={this.props.showApps}
|
||||||
hide={this.props.hideAppsDrawer}
|
hide={this.props.hideAppsDrawer}
|
||||||
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
let stateViews = null;
|
let stateViews = null;
|
||||||
|
|
44
src/hooks/useLocalStorageState.ts
Normal file
44
src/hooks/useLocalStorageState.ts
Normal 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];
|
||||||
|
};
|
|
@ -105,6 +105,9 @@ export default class Resizer {
|
||||||
if (this.classNames.resizing) {
|
if (this.classNames.resizing) {
|
||||||
this.container.classList.add(this.classNames.resizing);
|
this.container.classList.add(this.classNames.resizing);
|
||||||
}
|
}
|
||||||
|
if (this.config.onResizeStart) {
|
||||||
|
this.config.onResizeStart();
|
||||||
|
}
|
||||||
|
|
||||||
const {sizer, distributor} = this._createSizerAndDistributor(resizeHandle);
|
const {sizer, distributor} = this._createSizerAndDistributor(resizeHandle);
|
||||||
distributor.start();
|
distributor.start();
|
||||||
|
@ -119,6 +122,9 @@ export default class Resizer {
|
||||||
if (this.classNames.resizing) {
|
if (this.classNames.resizing) {
|
||||||
this.container.classList.remove(this.classNames.resizing);
|
this.container.classList.remove(this.classNames.resizing);
|
||||||
}
|
}
|
||||||
|
if (this.config.onResizeStop) {
|
||||||
|
this.config.onResizeStop();
|
||||||
|
}
|
||||||
distributor.finish();
|
distributor.finish();
|
||||||
body.removeEventListener("mouseup", finishResize, false);
|
body.removeEventListener("mouseup", finishResize, false);
|
||||||
document.removeEventListener("mouseleave", finishResize, false);
|
document.removeEventListener("mouseleave", finishResize, false);
|
||||||
|
|
|
@ -56,6 +56,18 @@ export default class Sizer {
|
||||||
return this.vertical ? this.container.offsetTop : this.container.offsetLeft;
|
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) {
|
setItemSize(item, size) {
|
||||||
if (this.vertical) {
|
if (this.vertical) {
|
||||||
item.style.height = `${Math.round(size)}px`;
|
item.style.height = `${Math.round(size)}px`;
|
||||||
|
@ -80,9 +92,9 @@ export default class Sizer {
|
||||||
offsetFromEvent(event) {
|
offsetFromEvent(event) {
|
||||||
const pos = this.vertical ? event.pageY : event.pageX;
|
const pos = this.vertical ? event.pageY : event.pageX;
|
||||||
if (this.reverse) {
|
if (this.reverse) {
|
||||||
return (this._getOffset() + this.getTotalSize()) - pos;
|
return (this._getPageOffset() + this.getTotalSize()) - pos;
|
||||||
} else {
|
} else {
|
||||||
return pos - this._getOffset();
|
return pos - this._getPageOffset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
// with default options, will call fn once at first call, and then every x ms
|
||||||
// if there was another call in that timespan
|
// if there was another call in that timespan
|
||||||
this._throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200);
|
this._throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200);
|
||||||
|
this._isResizing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isResizing() {
|
||||||
|
return this._isResizing;
|
||||||
|
}
|
||||||
|
|
||||||
|
startResizing() {
|
||||||
|
this._isResizing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopResizing() {
|
||||||
|
this._isResizing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_noisyMiddlePanel() {
|
_noisyMiddlePanel() {
|
||||||
|
|
Loading…
Reference in a new issue