diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index 50a9e7ee1f..7abf86cb0e 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -15,6 +15,14 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_RoomList2_resizer { + cursor: ns-resize; +} + +.mx_RoomList.mx_RoomList2 { + overflow-y: auto; +} + .mx_RoomList { /* take up remaining space below TopLeftMenu */ flex: 1; diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index e732e70edf..5e9f6ffb23 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -21,14 +21,13 @@ import { _t, _td } from "../../../languageHandler"; import { Layout } from '../../../resizer/distributors/roomsublist2'; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { ResizeNotifier } from "../../../utils/ResizeNotifier"; -import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore2"; +import RoomListStore, { LISTS_UPDATE_EVENT, RoomListStore2 } from "../../../stores/room-list/RoomListStore2"; import { ITagMap } from "../../../stores/room-list/algorithms/models"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { Dispatcher } from "flux"; import dis from "../../../dispatcher/dispatcher"; import RoomSublist2 from "./RoomSublist2"; import { ActionPayload } from "../../../dispatcher/payloads"; -import { IFilterCondition } from "../../../stores/room-list/filters/IFilterCondition"; import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition"; /******************************************************************* @@ -50,6 +49,7 @@ interface IProps { interface IState { sublists: ITagMap; + heights: Map; } const TAG_ORDER: TagID[] = [ @@ -133,13 +133,16 @@ export default class RoomList2 extends React.Component { private unfilteredLayout: Layout; private filteredLayout: Layout; private searchFilter: NameFilterCondition = new NameFilterCondition(); + private currentTagResize: TagID = null; constructor(props: IProps) { super(props); - this.state = {sublists: {}}; + this.state = { + sublists: {}, + heights: new Map(), + }; this.loadSublistSizes(); - this.prepareLayouts(); } public componentDidUpdate(prevProps: Readonly): void { @@ -158,9 +161,16 @@ export default class RoomList2 extends React.Component { } public componentDidMount(): void { - RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => { - console.log("new lists", store.orderedLists); - this.setState({sublists: store.orderedLists}); + RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store: RoomListStore2) => { + const newLists = store.orderedLists; + console.log("new lists", newLists); + + const heightMap = new Map(); + for (const tagId of Object.keys(newLists)) { + heightMap.set(tagId, store.layout.getPixelHeight(tagId)); + } + + this.setState({sublists: newLists, heights: heightMap}); }); } @@ -177,32 +187,24 @@ export default class RoomList2 extends React.Component { window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.sublistCollapseStates)); } - private prepareLayouts() { - // TODO: Change layout engine for FTUE support - this.unfilteredLayout = new Layout((tagId: string, height: number) => { - const sublist = this.sublistRefs[tagId]; - if (sublist) sublist.current.setHeight(height); + private onResizerMouseDown = (ev: React.MouseEvent) => { + const hr = ev.target as HTMLHRElement; + this.currentTagResize = hr.getAttribute("data-id"); + }; - // TODO: Check overflow (see old impl) + private onResizerMouseUp = (ev: React.MouseEvent) => { + this.currentTagResize = null; + }; - // Don't store a height for collapsed sublists - if (!this.sublistCollapseStates[tagId]) { - this.sublistSizes[tagId] = height; - this.saveSublistSizes(); - } - }, this.sublistSizes, this.sublistCollapseStates, { - allowWhitespace: false, - handleHeight: 1, - }); - - this.filteredLayout = new Layout((tagId: string, height: number) => { - const sublist = this.sublistRefs[tagId]; - if (sublist) sublist.current.setHeight(height); - }, null, null, { - allowWhitespace: false, - handleHeight: 0, - }); - } + private onMouseMove = (ev: React.MouseEvent) => { + ev.preventDefault(); + if (this.currentTagResize) { + const pixelHeight = this.state.heights.get(this.currentTagResize); + RoomListStore.instance.layout.setPixelHeight(this.currentTagResize, pixelHeight + ev.movementY); + this.state.heights.set(this.currentTagResize, RoomListStore.instance.layout.getPixelHeight(this.currentTagResize)); + this.forceUpdate(); + } + }; private renderSublists(): React.ReactElement[] { const components: React.ReactElement[] = []; @@ -235,6 +237,14 @@ export default class RoomList2 extends React.Component { onAddRoom={onAddRoomFn} addRoomLabel={aesthetics.addRoomLabel} isInvite={aesthetics.isInvite} + height={this.state.heights.get(orderedTagId)} + />); + components.push(
); } @@ -250,7 +260,9 @@ export default class RoomList2 extends React.Component { onFocus={this.props.onFocus} onBlur={this.props.onBlur} onKeyDown={onKeyDownHandler} - className="mx_RoomList" + onMouseUp={this.onResizerMouseUp} + onMouseMove={this.onMouseMove} + className="mx_RoomList mx_RoomList2" role="tree" aria-label={_t("Rooms")} // Firefox sometimes makes this element focusable due to diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index e2f489b959..2b5b131393 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -45,9 +45,9 @@ interface IProps { onAddRoom?: () => void; addRoomLabel: string; isInvite: boolean; + height: number; // pixels // TODO: Collapsed state - // TODO: Height // TODO: Group invites // TODO: Calls // TODO: forceExpand? @@ -61,10 +61,6 @@ interface IState { export default class RoomSublist2 extends React.Component { private headerButton = createRef(); - public setHeight(size: number) { - // TODO: Do a thing (maybe - height changes are different in FTUE) - } - private hasTiles(): boolean { return this.numTiles > 0; } @@ -205,9 +201,10 @@ export default class RoomSublist2 extends React.Component { // TODO: Lazy list rendering // TODO: Whatever scrolling magic needs to happen here content = ( - - {tiles} - + {tiles} ) } diff --git a/src/stores/room-list/RoomListLayoutStore.ts b/src/stores/room-list/RoomListLayoutStore.ts new file mode 100644 index 0000000000..cbd570b579 --- /dev/null +++ b/src/stores/room-list/RoomListLayoutStore.ts @@ -0,0 +1,85 @@ +/* +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. +*/ + +// TODO: Simplify the class load when we pick an approach for the list layout + +import { TagID } from "./models"; + +const TILE_HEIGHT_PX = 34; + +export class LayoutUnit { + constructor(public readonly multiplier: number) { + } + + public convert(val: number): number { + return Math.ceil(val * this.multiplier); + } + + public normalizePixels(pixels: number): number { + return this.convert(Math.ceil(pixels / this.multiplier)); + } + + public forNumTiles(n: number): number { + const unitsPerTile = TILE_HEIGHT_PX / this.multiplier; + return unitsPerTile * n; + } +} + +export const SMOOTH_RESIZE = new LayoutUnit(1); +export const CHUNKED_RESIZE = new LayoutUnit(TILE_HEIGHT_PX); + +export class RoomListLayoutStore { + public unit: LayoutUnit = SMOOTH_RESIZE; + public minTilesShown = 1; + + /** + * Minimum list height in pixels. + */ + public get minListHeight(): number { + return this.unit.forNumTiles(this.minTilesShown); + } + + private getStorageKey(tagId: TagID) { + return `mx_rlls_${tagId}_m_${this.unit.multiplier}`; + } + + public setPixelHeight(tagId: TagID, pixels: number): void { + localStorage.setItem(this.getStorageKey(tagId), JSON.stringify({pixels})); + } + + public getPixelHeight(tagId: TagID): number { + const stored = JSON.parse(localStorage.getItem(this.getStorageKey(tagId))); + let storedHeight = 0; + if (stored && stored.pixels) { + storedHeight = stored.pixels; + } + return this.unit.normalizePixels(Math.max(this.minListHeight, storedHeight)); + } + + // TODO: Remove helper functions for design iteration + + public beSmooth() { + this.unit = SMOOTH_RESIZE; + } + + public beChunked() { + this.unit = CHUNKED_RESIZE; + } + + public beDifferent(multiplier: number) { + this.unit = new LayoutUnit(multiplier); + } +} diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index af9970d3cc..84033b5cca 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -29,6 +29,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher"; import { readReceiptChangeIsFor } from "../../utils/read-receipts"; import { IFilterCondition } from "./filters/IFilterCondition"; import { TagWatcher } from "./TagWatcher"; +import { RoomListLayoutStore } from "./RoomListLayoutStore"; interface IState { tagsEnabled?: boolean; @@ -44,6 +45,8 @@ interface IState { export const LISTS_UPDATE_EVENT = "lists_update"; export class RoomListStore2 extends AsyncStore { + public readonly layout = new RoomListLayoutStore(); + private _matrixClient: MatrixClient; private initialListsGenerated = false; private enabled = false;