diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index c156f1f07e..8db0d4b836 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -14,15 +14,51 @@ See the License for the specific language governing permissions and limitations under the License. */ +/* a word of explanation about the flex-shrink values employed here: + there are 3 priotized categories of screen real-estate grabbing, + each with a flex-shrink difference of 4 order of magnitude, + so they ideally wouldn't affect each other. + lowest category: .mx_RoomSubList + flex:-shrink: 10000000 + distribute size of items within the same categery by their size + middle category: .mx_RoomSubList.resized-sized + flex:-shrink: 1000 + applied when using the resizer, will have a max-height set to it, + to limit the size + highest category: .mx_RoomSubList.resized-all + flex:-shrink: 1 + small flex-shrink value (1), is only added if you can drag the resizer so far + so in practice you can only assign this category if there is enough space. +*/ + .mx_RoomSubList { min-height: 31px; - flex: 0 1 auto; + flex: 0 100000000 auto; display: flex; flex-direction: column; } .mx_RoomSubList_nonEmpty { - margin-bottom: 4px; + min-height: 76px; + + .mx_AutoHideScrollbar_offset { + padding-bottom: 4px; + } +} + +.mx_RoomSubList_hidden { + flex: none !important; +} + +.mx_RoomSubList.resized-all { + flex: 0 1 auto; +} + +.mx_RoomSubList.resized-sized { + /* resizer set max-height on resized-sized, + so that limits the height and hence + needs a very small flex-shrink */ + flex: 0 10000 auto; } .mx_RoomSubList_labelContainer { @@ -105,39 +141,42 @@ limitations under the License. } .mx_RoomSubList_scroll { - /* let rooms list grab all available space */ + /* let rooms list grab as much space as it needs (auto), + potentially overflowing and showing a scrollbar */ flex: 0 1 auto; padding: 0 8px; } -.mx_RoomSubList_scroll.mx_IndicatorScrollbar_topOverflow::before, -.mx_RoomSubList_scroll.mx_IndicatorScrollbar_bottomOverflow::after { - position: sticky; - left: 0; - right: 0; - height: 40px; - content: ""; - display: block; - z-index: 100; - pointer-events: none; -} +// overflow indicators +.mx_RoomSubList:not(.resized-all) > .mx_RoomSubList_scroll { + &.mx_IndicatorScrollbar_topOverflow::before, + &.mx_IndicatorScrollbar_bottomOverflow::after { + position: sticky; + left: 0; + right: 0; + height: 40px; + content: ""; + display: block; + z-index: 100; + pointer-events: none; + } + &.mx_IndicatorScrollbar_topOverflow > .mx_AutoHideScrollbar_offset { + margin-top: -40px; + } + &.mx_IndicatorScrollbar_bottomOverflow > .mx_AutoHideScrollbar_offset { + margin-bottom: -40px; + } -.mx_RoomSubList_scroll.mx_IndicatorScrollbar_topOverflow > .mx_AutoHideScrollbar_offset { - margin-top: -40px; -} -.mx_RoomSubList_scroll.mx_IndicatorScrollbar_bottomOverflow > .mx_AutoHideScrollbar_offset { - margin-bottom: -40px; -} + &.mx_IndicatorScrollbar_topOverflow::before { + top: 0; + background: linear-gradient($secondary-accent-color, transparent); + } -.mx_RoomSubList_scroll.mx_IndicatorScrollbar_topOverflow::before { - top: 0; - background: linear-gradient($secondary-accent-color, transparent); -} - -.mx_RoomSubList_scroll.mx_IndicatorScrollbar_bottomOverflow::after { - bottom: 0; - background: linear-gradient(transparent, $secondary-accent-color); + &.mx_IndicatorScrollbar_bottomOverflow::after { + bottom: 0; + background: linear-gradient(transparent, $secondary-accent-color); + } } .collapsed { diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index 30a569d41f..8f78e3bb7a 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -24,6 +24,10 @@ limitations under the License. min-height: 0; } +.mx_SearchBox { + flex: none; +} + /* hide resize handles next to collapsed / empty sublists */ .mx_RoomList .mx_RoomSubList:not(.mx_RoomSubList_nonEmpty) + .mx_ResizeHandle { display: none; diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 585fd0f7d4..635c5de44e 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -164,15 +164,13 @@ const LoggedInView = React.createClass({ }; const collapseConfig = { toggleSize: 260 - 50, - onCollapsed: (collapsed, item) => { - if (item.classList.contains("mx_LeftPanel_container")) { - this.setState({collapseLhs: collapsed}); - if (collapsed) { - window.localStorage.setItem("mx_lhs_size", '0'); - } + onCollapsed: (collapsed) => { + this.setState({collapseLhs: collapsed}); + if (collapsed) { + window.localStorage.setItem("mx_lhs_size", '0'); } }, - onResized: (size, item) => { + onResized: (size) => { window.localStorage.setItem("mx_lhs_size", '' + size); }, }; diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 6399fd80d9..91b29d4665 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -318,20 +318,17 @@ const RoomSubList = React.createClass({ if (len) { const subListClasses = classNames({ "mx_RoomSubList": true, + "mx_RoomSubList_hidden": this.state.hidden, "mx_RoomSubList_nonEmpty": len && !this.state.hidden, }); if (this.state.hidden) { - return
+ return
{this._getHeaderJsx()}
; } else { - const heightEstimation = (len * 44) + 31 + (8 + 8); - const style = { - maxHeight: `${heightEstimation}px`, - }; const tiles = this.makeRoomTiles(); tiles.push(...this.props.extraTiles); - return
+ return
{this._getHeaderJsx()} { tiles } diff --git a/src/components/views/elements/ResizeHandle.js b/src/components/views/elements/ResizeHandle.js index 4c09278f84..b5487b1fc1 100644 --- a/src/components/views/elements/ResizeHandle.js +++ b/src/components/views/elements/ResizeHandle.js @@ -14,7 +14,7 @@ const ResizeHandle = (props) => { classNames.push('mx_ResizeHandle_reverse'); } return ( -
+
); }; diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 126ff5b3b9..d4599e5d8a 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -36,7 +36,7 @@ import GroupStore from '../../../stores/GroupStore'; import RoomSubList from '../../structures/RoomSubList'; import ResizeHandle from '../elements/ResizeHandle'; -import {Resizer, FixedDistributor, FlexSizer} from '../../../resizer' +import {Resizer, RoomDistributor, RoomSizer} from '../../../resizer' const HIDE_CONFERENCE_CHANS = true; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; @@ -70,6 +70,10 @@ module.exports = React.createClass({ }, getInitialState: function() { + + const sizesJson = window.localStorage.getItem("mx_roomlist_sizes"); + this.subListSizes = sizesJson ? JSON.parse(sizesJson) : {}; + return { isLoadingLeftRooms: false, totalRoomCount: null, @@ -134,14 +138,34 @@ module.exports = React.createClass({ this._delayedRefreshRoomListLoopCount = 0; }, + _onSubListResize: function(newSize, id) { + if (!id) { + return; + } + if (typeof newSize === "string") { + newSize = Number.MAX_SAFE_INTEGER; + } + this.subListSizes[id] = newSize; + window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.subListSizes)); + }, + componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); - this.resizer = new Resizer(this.resizeContainer, FixedDistributor, null, FlexSizer); + const cfg = { + onResized: this._onSubListResize, + }; + this.resizer = new Resizer(this.resizeContainer, RoomDistributor, cfg, RoomSizer); this.resizer.setClassNames({ handle: "mx_ResizeHandle", vertical: "mx_ResizeHandle_vertical", reverse: "mx_ResizeHandle_reverse" }); + + // load stored sizes + Object.entries(this.subListSizes).forEach(([id, size]) => { + this.resizer.forHandleWithId(id).resize(size); + }); + this.resizer.attach(); this.mounted = true; }, @@ -476,7 +500,7 @@ module.exports = React.createClass({ if (!isLast) { return components.concat( subList, - + ); } else { return components.concat(subList); @@ -484,6 +508,10 @@ module.exports = React.createClass({ }, []); }, + _collectResizeContainer: function(el) { + this.resizeContainer = el; + }, + render: function() { let subLists = [ { @@ -560,7 +588,7 @@ module.exports = React.createClass({ const subListComponents = this._mapSubListProps(subLists); return ( -
this.resizeContainer = d} className="mx_RoomList"> +
{ subListComponents }
); diff --git a/src/resizer/distributors.js b/src/resizer/distributors.js index 29e97e0bce..caf677a18f 100644 --- a/src/resizer/distributors.js +++ b/src/resizer/distributors.js @@ -18,43 +18,46 @@ limitations under the License. distributors translate a moving cursor into CSS/DOM changes by calling the sizer -they have one method, `resize` that receives +they have two methods: + `resize` receives then new item size + `resizeFromContainerOffset` receives resize handle location + within the container bounding box. For internal use. + This method usually ends up calling `resize` once the start offset is subtracted. the offset from the container edge of where the mouse cursor is. */ class FixedDistributor { - constructor(sizer, item, config) { + constructor(sizer, item, id, config) { this.sizer = sizer; this.item = item; + this.id = id; this.beforeOffset = sizer.getItemOffset(this.item); this.onResized = config && config.onResized; } - resize(offset) { - const itemSize = offset - this.beforeOffset; + resize(itemSize) { this.sizer.setItemSize(this.item, itemSize); if (this.onResized) { - this.onResized(itemSize, this.item); + this.onResized(itemSize, this.id, this.item); } return itemSize; } - sizeFromOffset(offset) { - return offset - this.beforeOffset; + resizeFromContainerOffset(offset) { + this.resize(offset - this.beforeOffset); } } class CollapseDistributor extends FixedDistributor { - constructor(sizer, item, config) { - super(sizer, item, config); + constructor(sizer, item, id, config) { + super(sizer, item, id, config); this.toggleSize = config && config.toggleSize; this.onCollapsed = config && config.onCollapsed; this.isCollapsed = false; } - resize(offset) { - const newSize = this.sizeFromOffset(offset); + resize(newSize) { const isCollapsedSize = newSize < this.toggleSize; if (isCollapsedSize && !this.isCollapsed) { this.isCollapsed = true; @@ -68,60 +71,12 @@ class CollapseDistributor extends FixedDistributor { this.isCollapsed = false; } if (!isCollapsedSize) { - super.resize(offset); + super.resize(newSize); } } } -class PercentageDistributor { - constructor(sizer, item, _config, items, container) { - this.container = container; - this.totalSize = sizer.getTotalSize(); - this.sizer = sizer; - - const itemIndex = items.indexOf(item); - this.beforeItems = items.slice(0, itemIndex); - this.afterItems = items.slice(itemIndex); - const percentages = PercentageDistributor._getPercentages(sizer, items); - this.beforePercentages = percentages.slice(0, itemIndex); - this.afterPercentages = percentages.slice(itemIndex); - } - - resize(offset) { - const percent = offset / this.totalSize; - const beforeSum = - this.beforePercentages.reduce((total, p) => total + p, 0); - const beforePercentages = - this.beforePercentages.map(p => (p / beforeSum) * percent); - const afterSum = - this.afterPercentages.reduce((total, p) => total + p, 0); - const afterPercentages = - this.afterPercentages.map(p => (p / afterSum) * (1 - percent)); - - this.beforeItems.forEach((item, index) => { - this.sizer.setItemPercentage(item, beforePercentages[index]); - }); - this.afterItems.forEach((item, index) => { - this.sizer.setItemPercentage(item, afterPercentages[index]); - }); - } - - static _getPercentages(sizer, items) { - const percentages = items.map(i => sizer.getItemPercentage(i)); - const setPercentages = percentages.filter(p => p !== null); - const unsetCount = percentages.length - setPercentages.length; - const setTotal = setPercentages.reduce((total, p) => total + p, 0); - const implicitPercentage = (1 - setTotal) / unsetCount; - return percentages.map(p => p === null ? implicitPercentage : p); - } - - static setPercentage(el, percent) { - el.style.flexGrow = Math.round(percent * 1000); - } -} - module.exports = { FixedDistributor, CollapseDistributor, - PercentageDistributor, }; diff --git a/src/resizer/index.js b/src/resizer/index.js index df7a839b9b..0720fa36ce 100644 --- a/src/resizer/index.js +++ b/src/resizer/index.js @@ -15,8 +15,9 @@ limitations under the License. */ import {Sizer, FlexSizer} from "./sizer"; -import {FixedDistributor, CollapseDistributor, PercentageDistributor} from "./distributors"; +import {FixedDistributor, CollapseDistributor} from "./distributors"; import {Resizer} from "./resizer"; +import {RoomSizer, RoomDistributor} from "./room"; module.exports = { Resizer, @@ -24,5 +25,6 @@ module.exports = { FlexSizer, FixedDistributor, CollapseDistributor, - PercentageDistributor, + RoomSizer, + RoomDistributor, }; diff --git a/src/resizer/resizer.js b/src/resizer/resizer.js index c5112e5139..7ef542a6e1 100644 --- a/src/resizer/resizer.js +++ b/src/resizer/resizer.js @@ -64,8 +64,19 @@ export class Resizer { forHandleAt(handleIndex) { const handles = this._getResizeHandles(); const handle = handles[handleIndex]; - const {distributor} = this._createSizerAndDistributor(handle); - return distributor; + if (handle) { + const {distributor} = this._createSizerAndDistributor(handle); + return distributor; + } + } + + forHandleWithId(id) { + const handles = this._getResizeHandles(); + const handle = handles.find((h) => h.getAttribute("data-id") === id); + if (handle) { + const {distributor} = this._createSizerAndDistributor(handle); + return distributor; + } } _isResizeHandle(el) { @@ -79,6 +90,7 @@ export class Resizer { } // prevent starting a drag operation event.preventDefault(); + // mark as currently resizing if (this.classNames.resizing) { this.container.classList.add(this.classNames.resizing); @@ -88,7 +100,7 @@ export class Resizer { const onMouseMove = (event) => { const offset = sizer.offsetFromEvent(event); - distributor.resize(offset); + distributor.resizeFromContainerOffset(offset); }; const body = document.body; @@ -115,9 +127,10 @@ export class Resizer { // if reverse, resize the item after the handle instead of before, so + 1 const itemIndex = items.indexOf(prevItem) + (reverse ? 1 : 0); const item = items[itemIndex]; + const id = resizeHandle.getAttribute("data-id"); // eslint-disable-next-line new-cap const distributor = new this.distributorCtor( - sizer, item, this.distributorCfg, + sizer, item, id, this.distributorCfg, items, this.container); return {sizer, distributor}; } diff --git a/src/resizer/room.js b/src/resizer/room.js index 3d68d16f9b..70f8be219a 100644 --- a/src/resizer/room.js +++ b/src/resizer/room.js @@ -22,30 +22,33 @@ class RoomSizer extends Sizer { const isString = typeof size === "string"; const cl = item.classList; if (isString) { - item.style.flex = null; - if (size === "show-content") { - cl.add("show-content"); - cl.remove("show-available"); + if (size === "resized-all") { + cl.add("resized-all"); + cl.remove("resized-sized"); item.style.maxHeight = null; } } else { - cl.add("show-available"); - //item.style.flex = `0 1 ${Math.round(size)}px`; + cl.add("resized-sized"); + cl.remove("resized-all"); item.style.maxHeight = `${Math.round(size)}px`; } } } class RoomDistributor extends FixedDistributor { - resize(offset) { - const itemSize = offset - this.sizer.getItemOffset(this.item); - - if (itemSize > this.item.scrollHeight) { - this.sizer.setItemSize(this.item, "show-content"); + resize(itemSize) { + const scrollItem = this.item.querySelector(".mx_RoomSubList_scroll"); + const fixedHeight = this.item.offsetHeight - scrollItem.offsetHeight; + if (itemSize > (fixedHeight + scrollItem.scrollHeight)) { + super.resize("resized-all"); } else { - this.sizer.setItemSize(this.item, itemSize); + super.resize(itemSize); } } + + resizeFromContainerOffset(offset) { + return this.resize(offset - this.sizer.getItemOffset(this.item)); + } } module.exports = {