diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index faaf1cf462..e403057cd3 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -32,34 +32,13 @@ limitations under the License. */ .mx_RoomSubList { - min-height: 31px; - flex: 0 10000 auto; display: flex; flex-direction: column; } -.mx_RoomSubList.resized-sized { - /* - flex-basis to 0 so sublists - are not shrinking/growing relative - to their content (as would be the case with auto), - as this intervenes with sizing an item exactly - when not available space is available - in the flex container - */ - flex: 1 1 0; -} -.mx_RoomSubList_nonEmpty { - min-height: 74px; - - .mx_AutoHideScrollbar_offset { - padding-bottom: 4px; - } -} - -.mx_RoomSubList_hidden { - flex: none !important; +.mx_RoomSubList_nonEmpty .mx_AutoHideScrollbar_offset { + padding-bottom: 4px; } .mx_RoomSubList_labelContainer { diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index 8f78e3bb7a..360966a952 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -17,13 +17,14 @@ limitations under the License. .mx_RoomList { /* take up remaining space below TopLeftMenu */ - flex: 1 1 auto; - /* use flexbox to layout sublists */ - display: flex; - flex-direction: column; + flex: 1; min-height: 0; } +.mx_RoomList .mx_ResizeHandle { + position: relative; +} + .mx_SearchBox { flex: none; } diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index ba0e97366e..a2d08f35c8 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -212,6 +212,7 @@ const LeftPanel = React.createClass({ diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 645782a854..2fd7a9bccf 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -545,6 +545,7 @@ const LoggedInView = React.createClass({
diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index cda1c9967e..852dddd063 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -313,6 +313,12 @@ const RoomSubList = React.createClass({ } }, + setHeight: function(height) { + if (this.refs.subList) { + this.refs.subList.style.height = `${height}px`; + } + }, + render: function() { const len = this.props.list.length + this.props.extraTiles.length; if (len) { @@ -322,13 +328,13 @@ const RoomSubList = React.createClass({ "mx_RoomSubList_nonEmpty": len && !this.state.hidden, }); if (this.state.hidden) { - return
+ return
{this._getHeaderJsx()}
; } else { const tiles = this.makeRoomTiles(); tiles.push(...this.props.extraTiles); - return
+ return
{this._getHeaderJsx()} { tiles } @@ -343,7 +349,7 @@ const RoomSubList = React.createClass({ } return ( -
+
{ this._getHeaderJsx() } { content }
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 01e6d280c6..4f92d0cad6 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -36,7 +36,8 @@ import GroupStore from '../../../stores/GroupStore'; import RoomSubList from '../../structures/RoomSubList'; import ResizeHandle from '../elements/ResizeHandle'; -import {Resizer, RoomSubListDistributor} from '../../../resizer' +import {Resizer} from '../../../resizer' +import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2'; const HIDE_CONFERENCE_CHANS = true; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; @@ -79,6 +80,23 @@ module.exports = React.createClass({ const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed"); this.subListSizes = sizesJson ? JSON.parse(sizesJson) : {}; this.collapsedState = collapsedJson ? JSON.parse(collapsedJson) : {}; + this._layoutSections = []; + + this._layout = new Layout((key, size) => { + const subList = this._subListRefs[key]; + if (subList) { + subList.setHeight(size); + } + // update overflow indicators + this._checkSubListsOverflow(); + // don't store height for collapsed sublists + if(!this.collapsedState[key]) { + this.subListSizes[key] = size; + window.localStorage.setItem("mx_roomlist_sizes", + JSON.stringify(this.subListSizes)); + } + }, this.subListSizes, this.collapsedState); + return { isLoadingLeftRooms: false, totalRoomCount: null, @@ -146,54 +164,38 @@ module.exports = React.createClass({ this._delayedRefreshRoomListLoopCount = 0; }, - _onSubListResize: function(newSize, id) { - if (!id) { - return; - } - if (typeof newSize === "string") { - newSize = Number.MAX_SAFE_INTEGER; - } - if (newSize === null) { - delete this.subListSizes[id]; - } else { - this.subListSizes[id] = newSize; - } - window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.subListSizes)); - // update overflow indicators - this._checkSubListsOverflow(); - }, - componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); const cfg = { - onResized: this._onSubListResize, + layout: this._layout, }; - this.resizer = new Resizer(this.resizeContainer, RoomSubListDistributor, cfg); + this.resizer = new Resizer(this.resizeContainer, Distributor, cfg); this.resizer.setClassNames({ handle: "mx_ResizeHandle", vertical: "mx_ResizeHandle_vertical", reverse: "mx_ResizeHandle_reverse" }); - - // load stored sizes - Object.keys(this.subListSizes).forEach((key) => { - this._restoreSubListSize(key); - }); + this._layout.update( + this._layoutSections, + this.resizeContainer && this.resizeContainer.offsetHeight, + ); this._checkSubListsOverflow(); this.resizer.attach(); + window.addEventListener("resize", this.onWindowResize); this.mounted = true; }, componentDidUpdate: function(prevProps) { this._repositionIncomingCallBox(undefined, false); - if (this.props.searchFilter !== prevProps.searchFilter) { - // restore sizes - Object.keys(this.subListSizes).forEach((key) => { - this._restoreSubListSize(key); - }); - this._checkSubListsOverflow(); - } + // if (this.props.searchFilter !== prevProps.searchFilter) { + // this._checkSubListsOverflow(); + // } + this._layout.update( + this._layoutSections, + this.resizeContainer && this.resizeContainer.clientHeight, + ); + // TODO: call layout.setAvailableHeight, window height was changed when bannerShown prop was changed }, onAction: function(payload) { @@ -222,6 +224,7 @@ module.exports = React.createClass({ componentWillUnmount: function() { this.mounted = false; + window.removeEventListener("resize", this.onWindowResize); dis.unregister(this.dispatcherRef); if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room", this.onRoom); @@ -251,6 +254,17 @@ module.exports = React.createClass({ this._delayedRefreshRoomList.cancelPendingCall(); }, + onWindowResize: function() { + if (this.mounted && this._layout && this.resizeContainer && + Array.isArray(this._layoutSections) + ) { + this._layout.update( + this._layoutSections, + this.resizeContainer.offsetHeight + ); + } + }, + onRoom: function(room) { this.updateVisibleRooms(); }, @@ -551,22 +565,16 @@ module.exports = React.createClass({ this.collapsedState[key] = collapsed; window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.collapsedState)); // load the persisted size configuration of the expanded sub list - if (!collapsed) { - this._restoreSubListSize(key); + if (collapsed) { + this._layout.collapseSection(key); + } else { + this._layout.expandSection(key, this.subListSizes[key]); } // check overflow, as sub lists sizes have changed // important this happens after calling resize above this._checkSubListsOverflow(); }, - _restoreSubListSize(key) { - const size = this.subListSizes[key]; - const handle = this.resizer.forHandleWithId(key); - if (handle) { - handle.resize(size); - } - }, - // check overflow for scroll indicator gradient _checkSubListsOverflow() { Object.values(this._subListRefs).forEach(l => l.checkOverflow()); @@ -581,6 +589,7 @@ module.exports = React.createClass({ }, _mapSubListProps: function(subListsProps) { + this._layoutSections = []; const defaultProps = { collapsed: this.props.collapsed, isFiltered: !!this.props.searchFilter, @@ -599,6 +608,7 @@ module.exports = React.createClass({ return subListsProps.reduce((components, props, i) => { props = Object.assign({}, defaultProps, props); const isLast = i === subListsProps.length - 1; + const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0); const {key, label, onHeaderClick, ... otherProps} = props; const chosenKey = key || label; const onSubListHeaderClick = (collapsed) => { @@ -608,7 +618,10 @@ module.exports = React.createClass({ } }; const startAsHidden = props.startAsHidden || this.collapsedState[chosenKey]; - + this._layoutSections.push({ + id: chosenKey, + count: len, + }); let subList = ( max) return max; + if (height < min) return min; + return height; +} + +export class Layout { + constructor(applyHeight, initialSizes, collapsedState) { + this._applyHeight = applyHeight; + this._sections = []; + this._collapsedState = Object.assign({}, collapsedState); + this._availableHeight = 0; + // heights stored by section section id + this._sectionHeights = Object.assign({}, initialSizes); + // in-progress heights, while dragging. Committed on mouse-up. + this._heights = []; + } + + setAvailableHeight(newSize) { + this._availableHeight = newSize; + // needs more work + this._applyNewSize(); + } + + expandSection(id, height) { + this._collapsedState[id] = false; + this._applyNewSize(); + this.openHandle(id).setHeight(height).finish(); + } + + collapseSection(id) { + this._collapsedState[id] = true; + this._applyNewSize(); + } + + // [{id, count}] + update(sections, availableHeight) { + if (Number.isFinite(availableHeight)) { + this._availableHeight = availableHeight; + } + const totalHeight = this._getAvailableHeight(); + this._sections.forEach((section, i) => { + if (!this._sectionHeights.hasOwnProperty(section.id)) { + this._sectionHeights[section.id] = clamp( + totalHeight / this._sections.length, + this._getMinHeight(i), + this._getMaxHeight(i), + ); + } + }); + this._sections = sections; + this._applyNewSize(); + } + + openHandle(id) { + const index = this._getSectionIndex(id); + //log(`openHandle resolved ${id} to ${index}`); + return new Handle(this, index, this._sectionHeights[id]); + } + + _getAvailableHeight() { + const nonCollapsedSectionCount = this._sections.reduce((count, section) => { + const collapsed = this._collapsedState[section.id]; + return count + (collapsed ? 0 : 1); + }, 0); + return this._availableHeight - ((nonCollapsedSectionCount - 1) * handleHeight); + } + + _applyNewSize() { + const newHeight = this._getAvailableHeight(); + const currHeight = this._sections.reduce((sum, section) => { + return sum + this._sectionHeights[section.id]; + }, 0); + const offset = newHeight - currHeight; + this._heights = this._sections.map((section) => this._sectionHeights[section.id]); + const sections = this._sections.map((_, i) => i); + this._applyOverflow(-offset, sections, true); + this._applyHeights(); + this._commitHeights(); + } + + _getSectionIndex(id) { + return this._sections.findIndex((s) => s.id === id); + } + + _getMaxHeight(i) { + const section = this._sections[i]; + const collapsed = this._collapsedState[section.id]; + + if (collapsed) { + return this._sectionHeight(0); + } else { + return 100000; + // return this._sectionHeight(section.count); + } + } + + _sectionHeight(count) { + return 36 + (count === 0 ? 0 : 4 + (count * 34)); + } + + _getMinHeight(i) { + const section = this._sections[i]; + const collapsed = this._collapsedState[section.id]; + const maxItems = collapsed ? 0 : 1; + // log("_getMinHeight", i, section); + return this._sectionHeight(Math.min(section.count, maxItems)); + } + + _applyOverflow(overflow, sections, blend) { + //log("applyOverflow", overflow, sections); + // take the given overflow amount, and applies it to the given sections. + // calls itself recursively until it has distributed all the overflow + // or run out of unclamped sections. + + const unclampedSections = []; + + let overflowPerSection = blend ? (overflow / sections.length) : overflow; + for (const i of sections) { + const newHeight = clamp( + this._heights[i] - overflowPerSection, + this._getMinHeight(i), + this._getMaxHeight(i), + ); + if (newHeight == this._heights[i] - overflowPerSection) { + unclampedSections.push(i); + } + // when section is growing, overflow increases? + // 100 -= 200 - 300 + // 100 -= -100 + // 200 + overflow -= this._heights[i] - newHeight; + // console.log(`this._heights[${i}] (${this._heights[i]}) - newHeight (${newHeight}) = ${this._heights[i] - newHeight}`); + // console.log(`changing ${this._heights[i]} to ${newHeight}`); + this._heights[i] = newHeight; + // console.log(`for section ${i} overflow is ${overflow}`); + if (!blend) { + overflowPerSection = overflow; + if (Math.abs(overflow) < 1.0) break; + } + } + + if (Math.abs(overflow) > 1.0 && unclampedSections.length > 0) { + // we weren't able to distribute all the overflow so recurse and try again + // log("recursing with", overflow, unclampedSections); + overflow = this._applyOverflow(overflow, unclampedSections, blend); + } + + return overflow; + } + + _rebalanceAbove(anchor, overflowAbove) { + if (Math.abs(overflowAbove) > 1.0) { + // log(`trying to rebalance upstream with ${overflowAbove}`); + const sections = []; + for (let i = anchor - 1; i >= 0; i--) { + sections.push(i); + } + overflowAbove = this._applyOverflow(overflowAbove, sections); + } + return overflowAbove; + } + + _rebalanceBelow(anchor, overflowBelow) { + if (Math.abs(overflowBelow) > 1.0) { + // log(`trying to rebalance downstream with ${overflowBelow}`); + const sections = []; + for (let i = anchor + 1; i < this._sections.length; i++) { + sections.push(i); + } + overflowBelow = this._applyOverflow(overflowBelow, sections); + //log(`rebalanced downstream with ${overflowBelow}`); + } + return overflowBelow; + } + + // @param offset the amount the anchor is moved from what is stored in _sectionHeights, positive if downwards + // if we're clamped, return the offset we should be clamped at. + _relayout(anchor = 0, offset = 0, clamped = false) { + this._heights = this._sections.map((section) => this._sectionHeights[section.id]); + // are these the amounts the items above/below shrank/grew and need to be relayouted? + let overflowAbove; + let overflowBelow; + const maxHeight = this._getMaxHeight(anchor); + const minHeight = this._getMinHeight(anchor); + // new height > max ? + if (this._heights[anchor] + offset > maxHeight) { + // we're pulling downwards and clamped + // overflowAbove = minus how much are we above max height? + overflowAbove = (maxHeight - this._heights[anchor]) - offset; + overflowBelow = offset; + // log(`pulling downwards clamped at max: ${overflowAbove} ${overflowBelow}`); + } else if (this._heights[anchor] + offset < minHeight) { // new height < min? + // we're pulling upwards and clamped + // overflowAbove = ??? (offset is negative here, so - offset will add) + overflowAbove = (minHeight - this._heights[anchor]) - offset; + overflowBelow = offset; + // log(`pulling upwards clamped at min: ${overflowAbove} ${overflowBelow}`); + } else { + overflowAbove = 0; + overflowBelow = offset; + // log(`resizing the anchor: ${overflowAbove} ${overflowBelow}`); + } + this._heights[anchor] = clamp(this._heights[anchor] + offset, minHeight, maxHeight); + + // these are reassigned the amount of overflow that could not be rebalanced + // meaning we dragged the handle too far and it can't follow the cursor anymore + overflowAbove = this._rebalanceAbove(anchor, overflowAbove); + overflowBelow = this._rebalanceBelow(anchor, overflowBelow); + + if (!clamped) { // to avoid risk of infinite recursion + // clamp to avoid overflowing or underflowing the page + if (Math.abs(overflowAbove) > 1.0) { + // log(`clamping with overflowAbove ${overflowAbove}`); + // here we do the layout again with offset - the amount of space we took too much + this._relayout(anchor, offset + overflowAbove, true); + return offset + overflowAbove; + } + + if (Math.abs(overflowBelow) > 1.0) { + // here we do the layout again with offset - the amount of space we took too much + // log(`clamping with overflowBelow ${overflowBelow}`); + this._relayout(anchor, offset - overflowBelow, true); + return offset - overflowBelow; + } + } + + this._applyHeights(); + return undefined; + } + + _applyHeights() { + log("updating layout, heights are now", this._heights); + // apply the heights + for (let i = 0; i < this._sections.length; i++) { + const section = this._sections[i]; + this._applyHeight(section.id, this._heights[i]); + } + } + + _commitHeights() { + this._sections.forEach((section, i) => { + this._sectionHeights[section.id] = this._heights[i]; + }); + } +} + +class Handle { + constructor(layout, anchor, height) { + this._layout = layout; + this._anchor = anchor; + this._initialHeight = height; + } + + setHeight(height) { + this._layout._relayout(this._anchor, height - this._initialHeight); + return this; + } + + finish() { + this._layout._commitHeights(); + return this; + } +} + +export class Distributor extends FixedDistributor { + constructor(item, cfg) { + super(item); + const layout = cfg.layout; + this._handle = layout.openHandle(item.id); + } + + finish() { + this._handle.finish(); + } + + resize(height) { + this._handle.setHeight(height); + } +}