Wedge t3chguy's resizer prototype into the sublist
This commit is contained in:
parent
c07b5ebe9a
commit
dbf996439b
9 changed files with 152 additions and 150 deletions
|
@ -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",
|
||||
|
|
|
@ -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";
|
||||
|
|
17
res/css/views/rooms/_RoomSublist2.scss
Normal file
17
res/css/views/rooms/_RoomSublist2.scss
Normal file
|
@ -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";
|
|
@ -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<TagID, number>;
|
||||
layouts: Map<TagID, ListLayout>;
|
||||
}
|
||||
|
||||
const TAG_ORDER: TagID[] = [
|
||||
|
@ -127,20 +127,16 @@ const TAG_AESTHETICS: {
|
|||
};
|
||||
|
||||
export default class RoomList2 extends React.Component<IProps, IState> {
|
||||
private sublistRefs: { [tagId: string]: React.RefObject<RoomSublist2> } = {};
|
||||
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<TagID, number>(),
|
||||
layouts: new Map<TagID, ListLayout>(),
|
||||
};
|
||||
this.loadSublistSizes();
|
||||
}
|
||||
|
@ -165,12 +161,12 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
|||
const newLists = store.orderedLists;
|
||||
console.log("new lists", newLists);
|
||||
|
||||
const heightMap = new Map<TagID, number>();
|
||||
const layoutMap = new Map<TagID, ListLayout>();
|
||||
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<IProps, IState> {
|
|||
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<IProps, IState> {
|
|||
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
||||
|
||||
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
|
||||
components.push(<RoomSublist2
|
||||
key={`sublist-${orderedTagId}`}
|
||||
forRooms={true}
|
||||
rooms={orderedRooms}
|
||||
startAsHidden={aesthetics.defaultHidden}
|
||||
label={_t(aesthetics.sectionLabel)}
|
||||
onAddRoom={onAddRoomFn}
|
||||
addRoomLabel={aesthetics.addRoomLabel}
|
||||
isInvite={aesthetics.isInvite}
|
||||
height={this.state.heights.get(orderedTagId)}
|
||||
/>);
|
||||
components.push(<hr
|
||||
key={`resizer-${orderedTagId}`}
|
||||
data-id={orderedTagId}
|
||||
className="mx_RoomList2_resizer"
|
||||
onMouseDown={this.onResizerMouseDown}
|
||||
onMouseUp={this.onResizerMouseUp}
|
||||
/>);
|
||||
components.push(
|
||||
<RoomSublist2
|
||||
key={`sublist-${orderedTagId}`}
|
||||
forRooms={true}
|
||||
rooms={orderedRooms}
|
||||
startAsHidden={aesthetics.defaultHidden}
|
||||
label={_t(aesthetics.sectionLabel)}
|
||||
onAddRoom={onAddRoomFn}
|
||||
addRoomLabel={aesthetics.addRoomLabel}
|
||||
isInvite={aesthetics.isInvite}
|
||||
layout={this.state.layouts.get(orderedTagId)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return components;
|
||||
|
@ -260,8 +227,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
|||
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")}
|
||||
|
|
|
@ -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<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
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<IProps, IState> {
|
|||
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 = (
|
||||
<IndicatorScrollbar
|
||||
className='mx_RoomSubList_scroll'
|
||||
style={{height: `${this.props.height}px`}}
|
||||
>{tiles}</IndicatorScrollbar>
|
||||
<ResizableBox
|
||||
width={-1}
|
||||
height={tilesPx}
|
||||
axis="y"
|
||||
minConstraints={[-1, minTilesPx]}
|
||||
maxConstraints={[-1, maxTilesPx]}
|
||||
draggableOpts={{grid: [-1, layout.tileHeight]}}
|
||||
resizeHandles={handles}
|
||||
onResize={this.onResize}
|
||||
className="mx_RoomSublist2_resizeBox"
|
||||
>
|
||||
{visibleTiles}
|
||||
</ResizableBox>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
65
src/stores/room-list/ListLayout.ts
Normal file
65
src/stores/room-list/ListLayout.ts
Normal file
|
@ -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 = <ISerializedListLayout>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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<ActionPayload> {
|
||||
public readonly layout = new RoomListLayoutStore();
|
||||
|
||||
private _matrixClient: MatrixClient;
|
||||
private initialListsGenerated = false;
|
||||
private enabled = false;
|
||||
|
|
20
yarn.lock
20
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"
|
||||
|
|
Loading…
Reference in a new issue