diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 527f50c5bc..633c33feea 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -181,7 +181,6 @@ limitations under the License. } .mx_RoomSublist2_resizeBox { - margin-bottom: 4px; // for the resize handle position: relative; // Create another flexbox column for the tiles @@ -189,55 +188,22 @@ limitations under the License. flex-direction: column; overflow: hidden; - .mx_RoomSublist2_placeholder { - height: 44px; // Height of a room tile plus margins + .mx_RoomSublist2_tiles { + flex: 1 0 0; + overflow: hidden; + // need this to be flex otherwise the overflow hidden from above + // sometimes vertically centers the clipped list ... no idea why it would do this + // as the box model should be top aligned. Happens in both FF and Chromium + display: flex; + flex-direction: column; } - .mx_RoomSublist2_showNButton { - cursor: pointer; - font-size: $font-13px; - line-height: $font-18px; - color: $roomtile2-preview-color; + .mx_RoomSublist2_resizerHandles_showNButton { + flex: 0 0 32px; + } - // Update the render() function for RoomSublist2 if these change - // Update the ListLayout class for minVisibleTiles if these change. - // - // At 24px high, 8px padding on the top and 4px padding on the bottom this equates to 0.73 of - // a tile due to how the padding calculations work. - height: 24px; - padding-top: 8px; - padding-bottom: 4px; - - // We force this to the bottom so it will overlap rooms as needed. - // We account for the space it takes up (24px) in the code through padding. - position: absolute; - bottom: 0; - left: 0; - right: 0; - - // We create a flexbox to cheat at alignment - display: flex; - align-items: center; - - .mx_RoomSublist2_showNButtonChevron { - position: relative; - width: 16px; - height: 16px; - margin-left: 12px; - margin-right: 18px; - mask-position: center; - mask-size: contain; - mask-repeat: no-repeat; - background: $roomtile2-preview-color; - } - - .mx_RoomSublist2_showMoreButtonChevron { - mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); - } - - .mx_RoomSublist2_showLessButtonChevron { - mask-image: url('$(res)/img/feather-customised/chevron-up.svg'); - } + .mx_RoomSublist2_resizerHandles { + flex: 0 0 4px; } // Class name comes from the ResizableBox component @@ -269,6 +235,42 @@ limitations under the License. } } + .mx_RoomSublist2_showNButton { + cursor: pointer; + font-size: $font-13px; + line-height: $font-18px; + color: $roomtile2-preview-color; + + // Update the render() function for RoomSublist2 if these change + // Update the ListLayout class for minVisibleTiles if these change. + height: 24px; + padding-bottom: 4px; + + // We create a flexbox to cheat at alignment + display: flex; + align-items: center; + + .mx_RoomSublist2_showNButtonChevron { + position: relative; + width: 16px; + height: 16px; + margin-left: 12px; + margin-right: 18px; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $roomtile2-preview-color; + } + + .mx_RoomSublist2_showMoreButtonChevron { + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + } + + .mx_RoomSublist2_showLessButtonChevron { + mask-image: url('$(res)/img/feather-customised/chevron-up.svg'); + } + } + &.mx_RoomSublist2_hasMenuOpen, &:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:focus-within, &:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:hover { @@ -314,13 +316,13 @@ limitations under the License. .mx_RoomSublist2_resizeBox { align-items: center; + } - .mx_RoomSublist2_showNButton { - flex-direction: column; + .mx_RoomSublist2_showNButton { + flex-direction: column; - .mx_RoomSublist2_showNButtonChevron { - margin-right: 12px; // to center - } + .mx_RoomSublist2_showNButtonChevron { + margin-right: 12px; // to center } } diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 6d55c41c44..841a7a2d2b 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -59,7 +59,7 @@ import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore"; * warning disappears. * *******************************************************************/ -const SHOW_N_BUTTON_HEIGHT = 32; // As defined by CSS +const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS export const HEADER_HEIGHT = 32; // As defined by CSS @@ -87,6 +87,12 @@ interface IProps { // TODO: Account for https://github.com/vector-im/riot-web/issues/14179 } +// TODO: Use re-resizer's NumberSize when it is exposed as the type +interface ResizeDelta { + width: number; + height: number; +} + type PartialDOMRect = Pick; interface IState { @@ -94,6 +100,7 @@ interface IState { contextMenuPosition: PartialDOMRect; isResizing: boolean; isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered + height: number; } export default class RoomSublist2 extends React.Component { @@ -101,28 +108,54 @@ export default class RoomSublist2 extends React.Component { private sublistRef = createRef(); private dispatcherRef: string; private layout: ListLayout; + private heightAtStart: number; constructor(props: IProps) { super(props); this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId); - + this.heightAtStart = 0; + const height = this.calculateInitialHeight(); this.state = { notificationState: RoomNotificationStateStore.instance.getListState(this.props.tagId), contextMenuPosition: null, isResizing: false, - isExpanded: this.props.isFiltered ? this.props.isFiltered : !this.layout.isCollapsed + isExpanded: this.props.isFiltered ? this.props.isFiltered : !this.layout.isCollapsed, + height, }; this.state.notificationState.setRooms(this.props.rooms); this.dispatcherRef = defaultDispatcher.register(this.onAction); } + private calculateInitialHeight() { + const requestedVisibleTiles = Math.max(Math.floor(this.layout.visibleTiles), this.layout.minVisibleTiles); + const tileCount = Math.min(this.numTiles, requestedVisibleTiles); + const height = this.layout.tilesToPixelsWithPadding(tileCount, this.padding); + return height; + } + + private get padding() { + let padding = RESIZE_HANDLE_HEIGHT; + // this is used for calculating the max height of the whole container, + // and takes into account whether there should be room reserved for the show less button + // when fully expanded. Note that the show more button might still be shown when not fully expanded, + // but in this case it will take the space of a tile and we don't need to reserve space for it. + if (this.numTiles > this.layout.defaultVisibleTiles) { + padding += SHOW_N_BUTTON_HEIGHT; + } + return padding; + } + private get numTiles(): number { - return (this.props.rooms || []).length + (this.props.extraBadTilesThatShouldntExist || []).length; + return RoomSublist2.calcNumTiles(this.props); + } + + private static calcNumTiles(props) { + return (props.rooms || []).length + (props.extraBadTilesThatShouldntExist || []).length; } private get numVisibleTiles(): number { - const nVisible = Math.floor(this.layout.visibleTiles); + const nVisible = Math.ceil(this.layout.visibleTiles); return Math.min(nVisible, this.numTiles); } @@ -135,6 +168,11 @@ export default class RoomSublist2 extends React.Component { this.setState({isExpanded: !this.layout.isCollapsed}); } } + // as the rooms can come in one by one we need to reevaluate + // the amount of available rooms to cap the amount of requested visible rooms by the layout + if (RoomSublist2.calcNumTiles(prevProps) !== this.numTiles) { + this.setState({height: this.calculateInitialHeight()}); + } } public componentWillUnmount() { @@ -166,47 +204,50 @@ export default class RoomSublist2 extends React.Component { if (this.props.onAddRoom) this.props.onAddRoom(); }; + private applyHeightChange(newHeight: number) { + const heightInTiles = Math.ceil(this.layout.pixelsToTiles(newHeight - this.padding)); + this.layout.visibleTiles = Math.min(this.numTiles, heightInTiles); + } + private onResize = ( e: MouseEvent | TouchEvent, travelDirection: Direction, refToElement: HTMLDivElement, - delta: { width: number, height: number }, // TODO: Use re-resizer's NumberSize when it is exposed as the type + delta: ResizeDelta, ) => { - // Do some sanity checks, but in reality we shouldn't need these. - if (travelDirection !== "bottom") return; - if (delta.height === 0) return; // something went wrong, so just ignore it. - - // NOTE: the movement in the MouseEvent (not present on a TouchEvent) is inaccurate - // for our purposes. The delta provided by the library is also a change *from when - // resizing started*, meaning it is fairly useless for us. This is why we just use - // the client height and run with it. - - const heightBefore = this.layout.visibleTiles; - const heightInTiles = this.layout.pixelsToTiles(refToElement.clientHeight); - this.layout.setVisibleTilesWithin(heightInTiles, this.numTiles); - if (heightBefore === this.layout.visibleTiles) return; // no-op - this.forceUpdate(); // because the layout doesn't trigger a re-render + const newHeight = this.heightAtStart + delta.height; + this.applyHeightChange(newHeight); + this.setState({height: newHeight}); }; private onResizeStart = () => { + this.heightAtStart = this.state.height; this.setState({isResizing: true}); }; - private onResizeStop = () => { - this.setState({isResizing: false}); + private onResizeStop = ( + e: MouseEvent | TouchEvent, + travelDirection: Direction, + refToElement: HTMLDivElement, + delta: ResizeDelta, + ) => { + const newHeight = this.heightAtStart + delta.height; + this.applyHeightChange(newHeight); + this.setState({isResizing: false, height: newHeight}); }; private onShowAllClick = () => { - const numVisibleTiles = this.numVisibleTiles; - this.layout.visibleTiles = this.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT); - this.forceUpdate(); // because the layout doesn't trigger a re-render - setImmediate(this.focusRoomTile, numVisibleTiles); // focus the tile after the current bottom one + const newHeight = this.layout.tilesToPixelsWithPadding(this.numTiles, this.padding); + this.applyHeightChange(newHeight); + this.setState({height: newHeight}, () => { + this.focusRoomTile(this.numTiles - 1); + }); }; private onShowLessClick = () => { - this.layout.visibleTiles = this.layout.defaultVisibleTiles; - this.forceUpdate(); // because the layout doesn't trigger a re-render - // focus will flow to the show more button here + const newHeight = this.layout.tilesToPixelsWithPadding(this.layout.defaultVisibleTiles, this.padding); + this.applyHeightChange(newHeight); + this.setState({height: newHeight}); }; private focusRoomTile = (index: number) => { @@ -559,7 +600,6 @@ export default class RoomSublist2 extends React.Component { // TODO: Error boundary: https://github.com/vector-im/riot-web/issues/14185 const visibleTiles = this.renderVisibleTiles(); - const classes = classNames({ 'mx_RoomSublist2': true, 'mx_RoomSublist2_hasMenuOpen': !!this.state.contextMenuPosition, @@ -570,6 +610,11 @@ export default class RoomSublist2 extends React.Component { if (visibleTiles.length > 0) { const layout = this.layout; // to shorten calls + const minTiles = Math.min(layout.minVisibleTiles, this.numTiles); + const showMoreAtMinHeight = minTiles < this.numTiles; + const minHeightPadding = RESIZE_HANDLE_HEIGHT + (showMoreAtMinHeight ? SHOW_N_BUTTON_HEIGHT : 0); + const minTilesPx = layout.tilesToPixelsWithPadding(minTiles, minHeightPadding); + const maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, this.padding); const showMoreBtnClasses = classNames({ 'mx_RoomSublist2_showNButton': true, }); @@ -578,9 +623,11 @@ export default class RoomSublist2 extends React.Component { // floats above the resize handle, if we have one present. If the user has all // tiles visible, it becomes 'show less'. let showNButton = null; - if (this.numTiles > visibleTiles.length) { - // we have a cutoff condition - add the button to show all - const numMissing = this.numTiles - visibleTiles.length; + + if (maxTilesPx > this.state.height) { + const nonPaddedHeight = this.state.height - RESIZE_HANDLE_HEIGHT - SHOW_N_BUTTON_HEIGHT; + const amountFullyShown = Math.floor(nonPaddedHeight / this.layout.tileHeight); + const numMissing = this.numTiles - amountFullyShown; let showMoreText = ( {_t("Show %(count)s more", {count: numMissing})} @@ -595,7 +642,7 @@ export default class RoomSublist2 extends React.Component { {showMoreText} ); - } else if (this.numTiles <= visibleTiles.length && this.numTiles > this.layout.defaultVisibleTiles) { + } else if (this.numTiles > this.layout.defaultVisibleTiles) { // we have all tiles visible - add a button to show less let showLessText = ( @@ -639,44 +686,31 @@ export default class RoomSublist2 extends React.Component { // goes backwards and can become wildly incorrect (visibleTiles says 18 when there's // only mathematically 7 possible). - // The padding is variable though, so figure out what we need padding for. - let padding = 0; - if (showNButton) padding += SHOW_N_BUTTON_HEIGHT; - padding += RESIZE_HANDLE_HEIGHT; // always append the handle height + const handleWrapperClasses = classNames({ + 'mx_RoomSublist2_resizerHandles': true, + 'mx_RoomSublist2_resizerHandles_showNButton': !!showNButton, + }); - const relativeTiles = layout.tilesWithPadding(this.numTiles, padding); - const minTilesPx = layout.calculateTilesToPixelsMin(relativeTiles, layout.minVisibleTiles, padding); - const maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, padding); - const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles); - const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding); - - // Now that we know our padding constraints, let's find out if we need to chop off the - // last rendered visible tile so it doesn't collide with the 'show more' button - let visibleUnpaddedTiles = Math.round(layout.visibleTiles - layout.pixelsToTiles(padding)); - if (visibleUnpaddedTiles === visibleTiles.length - 1) { - const placeholder =
; - visibleTiles.splice(visibleUnpaddedTiles, 1, placeholder); - } - - const dimensions = { - height: tilesPx, - }; content = ( - - {visibleTiles} - {showNButton} - + + +
+ {visibleTiles} +
+ {showNButton} +
+
); } diff --git a/src/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts index 5169c5e4e5..f1900487bc 100644 --- a/src/stores/room-list/ListLayout.ts +++ b/src/stores/room-list/ListLayout.ts @@ -20,7 +20,8 @@ const TILE_HEIGHT_PX = 44; // this comes from the CSS where the show more button is // mathematically this percent of a tile when floating. -const RESIZER_BOX_FACTOR = 0.78; +//const RESIZER_BOX_FACTOR = 0.78; +const RESIZER_BOX_FACTOR = 0; interface ISerializedListLayout { numTiles: number; @@ -109,6 +110,10 @@ export class ListLayout { return this.tilesToPixels(Math.min(maxTiles, n)) + padding; } + public tilesWithResizerBoxFactor(n: number): number { + return n + RESIZER_BOX_FACTOR; + } + public tilesWithPadding(n: number, paddingPx: number): number { return this.pixelsToTiles(this.tilesToPixelsWithPadding(n, paddingPx)); } diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index acbb72f1d8..0d1ddfc4ca 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -25,7 +25,7 @@ import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm import { ActionPayload } from "../../dispatcher/payloads"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { readReceiptChangeIsFor } from "../../utils/read-receipts"; -import { IFilterCondition } from "./filters/IFilterCondition"; +import { FILTER_CHANGED, IFilterCondition } from "./filters/IFilterCondition"; import { TagWatcher } from "./TagWatcher"; import RoomViewStore from "../RoomViewStore"; import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm"; @@ -71,6 +71,7 @@ export class RoomListStore2 extends AsyncStore { for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null); RoomViewStore.addListener(() => this.handleRVSUpdate({})); this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated); + this.algorithm.on(FILTER_CHANGED, this.onAlgorithmFilterUpdated); } public get orderedLists(): ITagMap { @@ -512,6 +513,11 @@ export class RoomListStore2 extends AsyncStore { this.updateFn.mark(); }; + private onAlgorithmFilterUpdated = () => { + // The filter can happen off-cycle, so trigger an update if we need to. + this.updateFn.triggerIfWillMark(); + }; + /** * Regenerates the room whole room list, discarding any previous results. * diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index d985abd392..17e8283c74 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -153,11 +153,11 @@ export class Algorithm extends EventEmitter { // Populate the cache of the new filter this.allowedByFilter.set(filterCondition, this.rooms.filter(r => filterCondition.isVisible(r))); this.recalculateFilteredRooms(); - filterCondition.on(FILTER_CHANGED, this.recalculateFilteredRooms.bind(this)); + filterCondition.on(FILTER_CHANGED, this.handleFilterChange.bind(this)); } public removeFilterCondition(filterCondition: IFilterCondition): void { - filterCondition.off(FILTER_CHANGED, this.recalculateFilteredRooms.bind(this)); + filterCondition.off(FILTER_CHANGED, this.handleFilterChange.bind(this)); if (this.allowedByFilter.has(filterCondition)) { this.allowedByFilter.delete(filterCondition); @@ -169,6 +169,13 @@ export class Algorithm extends EventEmitter { } } + private async handleFilterChange() { + await this.recalculateFilteredRooms(); + + // re-emit the update so the list store can fire an off-cycle update if needed + this.emit(FILTER_CHANGED); + } + private async updateStickyRoom(val: Room) { try { return await this.doUpdateStickyRoom(val);