diff --git a/package.json b/package.json index 11906452e7..1029efaccd 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "react-beautiful-dnd": "^4.0.1", "react-dom": "^16.9.0", "react-focus-lock": "^2.2.1", + "react-resizable": "^1.10.1", "resize-observer-polyfill": "^1.5.0", "sanitize-html": "^1.18.4", "text-encoding-utf-8": "^1.0.1", diff --git a/res/css/_components.scss b/res/css/_components.scss index 5a7630a51f..b047519d99 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -179,6 +179,7 @@ @import "./views/rooms/_RoomList.scss"; @import "./views/rooms/_RoomPreviewBar.scss"; @import "./views/rooms/_RoomRecoveryReminder.scss"; +@import "./views/rooms/_RoomSublist2.scss"; @import "./views/rooms/_RoomTile.scss"; @import "./views/rooms/_RoomUpgradeWarningBar.scss"; @import "./views/rooms/_SearchBar.scss"; diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss new file mode 100644 index 0000000000..bb760e7e6e --- /dev/null +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -0,0 +1,17 @@ +/* +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 "../../../../node_modules/react-resizable/css/styles.css"; diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 5e9f6ffb23..b6fe37589c 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -18,7 +18,6 @@ limitations under the License. import * as React from "react"; 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, RoomListStore2 } from "../../../stores/room-list/RoomListStore2"; @@ -29,6 +28,7 @@ import dis from "../../../dispatcher/dispatcher"; import RoomSublist2 from "./RoomSublist2"; import { ActionPayload } from "../../../dispatcher/payloads"; import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition"; +import { ListLayout } from "../../../stores/room-list/ListLayout"; /******************************************************************* * CAUTION * @@ -49,7 +49,7 @@ interface IProps { interface IState { sublists: ITagMap; - heights: Map; + layouts: Map; } const TAG_ORDER: TagID[] = [ @@ -127,20 +127,16 @@ const TAG_AESTHETICS: { }; export default class RoomList2 extends React.Component { - private sublistRefs: { [tagId: string]: React.RefObject } = {}; private sublistSizes: { [tagId: string]: number } = {}; private sublistCollapseStates: { [tagId: string]: boolean } = {}; - private unfilteredLayout: Layout; - private filteredLayout: Layout; private searchFilter: NameFilterCondition = new NameFilterCondition(); - private currentTagResize: TagID = null; constructor(props: IProps) { super(props); this.state = { sublists: {}, - heights: new Map(), + layouts: new Map(), }; this.loadSublistSizes(); } @@ -165,12 +161,12 @@ export default class RoomList2 extends React.Component { const newLists = store.orderedLists; console.log("new lists", newLists); - const heightMap = new Map(); + const layoutMap = new Map(); for (const tagId of Object.keys(newLists)) { - heightMap.set(tagId, store.layout.getPixelHeight(tagId)); + layoutMap.set(tagId, new ListLayout(tagId)); } - this.setState({sublists: newLists, heights: heightMap}); + this.setState({sublists: newLists, layouts: layoutMap}); }); } @@ -182,30 +178,6 @@ export default class RoomList2 extends React.Component { if (collapsedJson) this.sublistCollapseStates = JSON.parse(collapsedJson); } - private saveSublistSizes() { - window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.sublistSizes)); - window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.sublistCollapseStates)); - } - - private onResizerMouseDown = (ev: React.MouseEvent) => { - const hr = ev.target as HTMLHRElement; - this.currentTagResize = hr.getAttribute("data-id"); - }; - - private onResizerMouseUp = (ev: React.MouseEvent) => { - this.currentTagResize = null; - }; - - 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[] = []; @@ -228,24 +200,19 @@ export default class RoomList2 extends React.Component { if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`); const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null; - components.push(); - components.push(
); + components.push( + + ); } return components; @@ -260,8 +227,6 @@ export default class RoomList2 extends React.Component { onFocus={this.props.onFocus} onBlur={this.props.onBlur} onKeyDown={onKeyDownHandler} - onMouseUp={this.onResizerMouseUp} - onMouseMove={this.onMouseMove} className="mx_RoomList mx_RoomList2" role="tree" aria-label={_t("Rooms")} diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 2b5b131393..6705448764 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -20,7 +20,6 @@ import * as React from "react"; import { createRef } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import classNames from 'classnames'; -import IndicatorScrollbar from "../../structures/IndicatorScrollbar"; import * as RoomNotifs from '../../../RoomNotifs'; import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import { _t } from "../../../languageHandler"; @@ -28,6 +27,8 @@ import AccessibleButton from "../../views/elements/AccessibleButton"; import AccessibleTooltipButton from "../../views/elements/AccessibleTooltipButton"; import * as FormattingUtils from '../../../utils/FormattingUtils'; import RoomTile2 from "./RoomTile2"; +import { ResizableBox, ResizeCallbackData } from "react-resizable"; +import { ListLayout } from "../../../stores/room-list/ListLayout"; /******************************************************************* * CAUTION * @@ -45,7 +46,7 @@ interface IProps { onAddRoom?: () => void; addRoomLabel: string; isInvite: boolean; - height: number; // pixels + layout: ListLayout; // TODO: Collapsed state // TODO: Group invites @@ -183,6 +184,12 @@ export default class RoomSublist2 extends React.Component { ); } + private onResize = (e: React.MouseEvent, data: ResizeCallbackData) => { + const tileDiff = e.movementY < 0 ? -1 : +1; + this.props.layout.visibleTiles += tileDiff; + this.forceUpdate(); // because the layout doesn't trigger a re-render + }; + public render(): React.ReactElement { // TODO: Proper rendering // TODO: Error boundary @@ -200,11 +207,29 @@ export default class RoomSublist2 extends React.Component { if (tiles.length > 0) { // TODO: Lazy list rendering // TODO: Whatever scrolling magic needs to happen here + const layout = this.props.layout; // to shorten calls + const minTilesPx = layout.tilesToPixels(Math.min(tiles.length, layout.minVisibleTiles)); + const maxTilesPx = layout.tilesToPixels(tiles.length); + const tilesPx = layout.tilesToPixels(Math.min(tiles.length, layout.visibleTiles)); + let handles = ['s']; + if (layout.visibleTiles >= tiles.length && tiles.length <= layout.minVisibleTiles) { + handles = []; // no handles, we're at a minimum + } + const visibleTiles = tiles.slice(0, layout.visibleTiles); content = ( - {tiles} + + {visibleTiles} + ) } diff --git a/src/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts new file mode 100644 index 0000000000..af6abe3297 --- /dev/null +++ b/src/stores/room-list/ListLayout.ts @@ -0,0 +1,65 @@ +/* +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. +*/ + +const TILE_HEIGHT_PX = 34; + +interface ISerializedListLayout { + numTiles: number; +} + +export class ListLayout { + private _n = 0; + + constructor(public readonly tagId) { + const serialized = localStorage.getItem(this.key); + if (serialized) { + // We don't use the setters as they cause writes. + const parsed = JSON.parse(serialized); + this._n = parsed.numTiles; + } + } + + public get tileHeight(): number { + return TILE_HEIGHT_PX; + } + + private get key(): string { + return `mx_sublist_layout_${this.tagId}_boxed`; + } + + public get visibleTiles(): number { + return Math.max(this._n, this.minVisibleTiles); + } + + public set visibleTiles(v: number) { + this._n = v; + localStorage.setItem(this.key, JSON.stringify(this.serialize())); + } + + public get minVisibleTiles(): number { + return 3; + } + + public tilesToPixels(n: number): number { + return n * this.tileHeight; + } + + private serialize(): ISerializedListLayout { + return { + numTiles: this.visibleTiles, + }; + } +} diff --git a/src/stores/room-list/RoomListLayoutStore.ts b/src/stores/room-list/RoomListLayoutStore.ts deleted file mode 100644 index cbd570b579..0000000000 --- a/src/stores/room-list/RoomListLayoutStore.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* -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 84033b5cca..af9970d3cc 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -29,7 +29,6 @@ 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; @@ -45,8 +44,6 @@ 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; diff --git a/yarn.lock b/yarn.lock index 56cf596fcf..ded8aa13f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2458,7 +2458,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@^2.1.2: +classnames@^2.1.2, classnames@^2.2.5: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== @@ -6858,7 +6858,7 @@ prop-types-exact@^1.2.0: object.assign "^4.1.0" reflect.ownkeys "^0.2.0" -prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@15.x, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -7062,6 +7062,14 @@ react-dom@^16.9.0: prop-types "^15.6.2" scheduler "^0.19.1" +react-draggable@^4.0.3: + version "4.4.2" + resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.2.tgz#f3cefecee25f467f865144cda0d066e5f05f94a0" + integrity sha512-zLQs4R4bnBCGnCVTZiD8hPsHtkiJxgMpGDlRESM+EHQo8ysXhKJ2GKdJ8UxxLJdRVceX1j19jy+hQS2wHislPQ== + dependencies: + classnames "^2.2.5" + prop-types "^15.6.0" + react-focus-lock@^2.2.1: version "2.3.1" resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.3.1.tgz#9d5d85899773609c7eefa4fc54fff6a0f5f2fc47" @@ -7106,6 +7114,14 @@ react-redux@^5.0.6: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" +react-resizable@^1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.10.1.tgz#f0c2cf1d83b3470b87676ce6d6b02bbe3f4d8cd4" + integrity sha512-Jd/bKOKx6+19NwC4/aMLRu/J9/krfxlDnElP41Oc+oLiUWs/zwV1S9yBfBZRnqAwQb6vQ/HRSk3bsSWGSgVbpw== + dependencies: + prop-types "15.x" + react-draggable "^4.0.3" + react-test-renderer@^16.0.0-0, react-test-renderer@^16.9.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1"