Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit ee3d2c5

Browse files
authored
Merge pull request #4697 from matrix-org/travis/room-list/scrolling-resize
New room list scrolling and resizing
2 parents 68e59a3 + e93a41c commit ee3d2c5

File tree

9 files changed

+210
-73
lines changed

9 files changed

+210
-73
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
"react-beautiful-dnd": "^4.0.1",
9494
"react-dom": "^16.9.0",
9595
"react-focus-lock": "^2.2.1",
96+
"react-resizable": "^1.10.1",
9697
"resize-observer-polyfill": "^1.5.0",
9798
"sanitize-html": "^1.18.4",
9899
"text-encoding-utf-8": "^1.0.1",

res/css/_components.scss

+1
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@
179179
@import "./views/rooms/_RoomList.scss";
180180
@import "./views/rooms/_RoomPreviewBar.scss";
181181
@import "./views/rooms/_RoomRecoveryReminder.scss";
182+
@import "./views/rooms/_RoomSublist2.scss";
182183
@import "./views/rooms/_RoomTile.scss";
183184
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
184185
@import "./views/rooms/_SearchBar.scss";

res/css/views/rooms/_RoomList.scss

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ See the License for the specific language governing permissions and
1515
limitations under the License.
1616
*/
1717

18+
.mx_RoomList.mx_RoomList2 {
19+
overflow-y: auto;
20+
}
21+
1822
.mx_RoomList {
1923
/* take up remaining space below TopLeftMenu */
2024
flex: 1;
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
Copyright 2020 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
@import "../../../../node_modules/react-resizable/css/styles.css";
18+
19+
.mx_RoomList2 .mx_RoomSubList_labelContainer {
20+
z-index: 12;
21+
}

src/components/views/rooms/RoomList2.tsx

+28-62
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,17 @@ limitations under the License.
1818

1919
import * as React from "react";
2020
import { _t, _td } from "../../../languageHandler";
21-
import { Layout } from '../../../resizer/distributors/roomsublist2';
2221
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
2322
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
24-
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore2";
23+
import RoomListStore, { LISTS_UPDATE_EVENT, RoomListStore2 } from "../../../stores/room-list/RoomListStore2";
2524
import { ITagMap } from "../../../stores/room-list/algorithms/models";
2625
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
2726
import { Dispatcher } from "flux";
2827
import dis from "../../../dispatcher/dispatcher";
2928
import RoomSublist2 from "./RoomSublist2";
3029
import { ActionPayload } from "../../../dispatcher/payloads";
31-
import { IFilterCondition } from "../../../stores/room-list/filters/IFilterCondition";
3230
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
31+
import { ListLayout } from "../../../stores/room-list/ListLayout";
3332

3433
/*******************************************************************
3534
* CAUTION *
@@ -50,6 +49,7 @@ interface IProps {
5049

5150
interface IState {
5251
sublists: ITagMap;
52+
layouts: Map<TagID, ListLayout>;
5353
}
5454

5555
const TAG_ORDER: TagID[] = [
@@ -127,19 +127,15 @@ const TAG_AESTHETICS: {
127127
};
128128

129129
export default class RoomList2 extends React.Component<IProps, IState> {
130-
private sublistRefs: { [tagId: string]: React.RefObject<RoomSublist2> } = {};
131-
private sublistSizes: { [tagId: string]: number } = {};
132-
private sublistCollapseStates: { [tagId: string]: boolean } = {};
133-
private unfilteredLayout: Layout;
134-
private filteredLayout: Layout;
135130
private searchFilter: NameFilterCondition = new NameFilterCondition();
136131

137132
constructor(props: IProps) {
138133
super(props);
139134

140-
this.state = {sublists: {}};
141-
this.loadSublistSizes();
142-
this.prepareLayouts();
135+
this.state = {
136+
sublists: {},
137+
layouts: new Map<TagID, ListLayout>(),
138+
};
143139
}
144140

145141
public componentDidUpdate(prevProps: Readonly<IProps>): void {
@@ -158,49 +154,16 @@ export default class RoomList2 extends React.Component<IProps, IState> {
158154
}
159155

160156
public componentDidMount(): void {
161-
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => {
162-
console.log("new lists", store.orderedLists);
163-
this.setState({sublists: store.orderedLists});
164-
});
165-
}
166-
167-
private loadSublistSizes() {
168-
const sizesJson = window.localStorage.getItem("mx_roomlist_sizes");
169-
if (sizesJson) this.sublistSizes = JSON.parse(sizesJson);
170-
171-
const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed");
172-
if (collapsedJson) this.sublistCollapseStates = JSON.parse(collapsedJson);
173-
}
157+
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store: RoomListStore2) => {
158+
const newLists = store.orderedLists;
159+
console.log("new lists", newLists);
174160

175-
private saveSublistSizes() {
176-
window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.sublistSizes));
177-
window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.sublistCollapseStates));
178-
}
179-
180-
private prepareLayouts() {
181-
// TODO: Change layout engine for FTUE support
182-
this.unfilteredLayout = new Layout((tagId: string, height: number) => {
183-
const sublist = this.sublistRefs[tagId];
184-
if (sublist) sublist.current.setHeight(height);
185-
186-
// TODO: Check overflow (see old impl)
187-
188-
// Don't store a height for collapsed sublists
189-
if (!this.sublistCollapseStates[tagId]) {
190-
this.sublistSizes[tagId] = height;
191-
this.saveSublistSizes();
161+
const layoutMap = new Map<TagID, ListLayout>();
162+
for (const tagId of Object.keys(newLists)) {
163+
layoutMap.set(tagId, new ListLayout(tagId));
192164
}
193-
}, this.sublistSizes, this.sublistCollapseStates, {
194-
allowWhitespace: false,
195-
handleHeight: 1,
196-
});
197165

198-
this.filteredLayout = new Layout((tagId: string, height: number) => {
199-
const sublist = this.sublistRefs[tagId];
200-
if (sublist) sublist.current.setHeight(height);
201-
}, null, null, {
202-
allowWhitespace: false,
203-
handleHeight: 0,
166+
this.setState({sublists: newLists, layouts: layoutMap});
204167
});
205168
}
206169

@@ -226,16 +189,19 @@ export default class RoomList2 extends React.Component<IProps, IState> {
226189
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
227190

228191
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
229-
components.push(<RoomSublist2
230-
key={`sublist-${orderedTagId}`}
231-
forRooms={true}
232-
rooms={orderedRooms}
233-
startAsHidden={aesthetics.defaultHidden}
234-
label={_t(aesthetics.sectionLabel)}
235-
onAddRoom={onAddRoomFn}
236-
addRoomLabel={aesthetics.addRoomLabel}
237-
isInvite={aesthetics.isInvite}
238-
/>);
192+
components.push(
193+
<RoomSublist2
194+
key={`sublist-${orderedTagId}`}
195+
forRooms={true}
196+
rooms={orderedRooms}
197+
startAsHidden={aesthetics.defaultHidden}
198+
label={_t(aesthetics.sectionLabel)}
199+
onAddRoom={onAddRoomFn}
200+
addRoomLabel={aesthetics.addRoomLabel}
201+
isInvite={aesthetics.isInvite}
202+
layout={this.state.layouts.get(orderedTagId)}
203+
/>
204+
);
239205
}
240206

241207
return components;
@@ -250,7 +216,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
250216
onFocus={this.props.onFocus}
251217
onBlur={this.props.onBlur}
252218
onKeyDown={onKeyDownHandler}
253-
className="mx_RoomList"
219+
className="mx_RoomList mx_RoomList2"
254220
role="tree"
255221
aria-label={_t("Rooms")}
256222
// Firefox sometimes makes this element focusable due to

src/components/views/rooms/RoomSublist2.tsx

+65-9
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ import * as React from "react";
2020
import { createRef } from "react";
2121
import { Room } from "matrix-js-sdk/src/models/room";
2222
import classNames from 'classnames';
23-
import IndicatorScrollbar from "../../structures/IndicatorScrollbar";
2423
import * as RoomNotifs from '../../../RoomNotifs';
2524
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
2625
import { _t } from "../../../languageHandler";
2726
import AccessibleButton from "../../views/elements/AccessibleButton";
2827
import AccessibleTooltipButton from "../../views/elements/AccessibleTooltipButton";
2928
import * as FormattingUtils from '../../../utils/FormattingUtils';
3029
import RoomTile2 from "./RoomTile2";
30+
import { ResizableBox, ResizeCallbackData } from "react-resizable";
31+
import { ListLayout } from "../../../stores/room-list/ListLayout";
3132

3233
/*******************************************************************
3334
* CAUTION *
@@ -45,9 +46,9 @@ interface IProps {
4546
onAddRoom?: () => void;
4647
addRoomLabel: string;
4748
isInvite: boolean;
49+
layout: ListLayout;
4850

4951
// TODO: Collapsed state
50-
// TODO: Height
5152
// TODO: Group invites
5253
// TODO: Calls
5354
// TODO: forceExpand?
@@ -61,10 +62,6 @@ interface IState {
6162
export default class RoomSublist2 extends React.Component<IProps, IState> {
6263
private headerButton = createRef();
6364

64-
public setHeight(size: number) {
65-
// TODO: Do a thing (maybe - height changes are different in FTUE)
66-
}
67-
6865
private hasTiles(): boolean {
6966
return this.numTiles > 0;
7067
}
@@ -79,6 +76,18 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
7976
if (this.props.onAddRoom) this.props.onAddRoom();
8077
};
8178

79+
private onResize = (e: React.MouseEvent, data: ResizeCallbackData) => {
80+
const direction = e.movementY < 0 ? -1 : +1;
81+
const tileDiff = this.props.layout.pixelsToTiles(Math.abs(e.movementY)) * direction;
82+
this.props.layout.visibleTiles += tileDiff;
83+
this.forceUpdate(); // because the layout doesn't trigger a re-render
84+
};
85+
86+
private onShowAllClick = () => {
87+
this.props.layout.visibleTiles = this.numTiles;
88+
this.forceUpdate(); // because the layout doesn't trigger a re-render
89+
};
90+
8291
private renderTiles(): React.ReactElement[] {
8392
const tiles: React.ReactElement[] = [];
8493

@@ -204,10 +213,57 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
204213
if (tiles.length > 0) {
205214
// TODO: Lazy list rendering
206215
// TODO: Whatever scrolling magic needs to happen here
216+
const layout = this.props.layout; // to shorten calls
217+
const minTilesPx = layout.tilesToPixels(Math.min(tiles.length, layout.minVisibleTiles));
218+
const maxTilesPx = layout.tilesToPixels(tiles.length);
219+
const tilesPx = layout.tilesToPixels(Math.min(tiles.length, layout.visibleTiles));
220+
let handles = ['s'];
221+
if (layout.visibleTiles >= tiles.length && tiles.length <= layout.minVisibleTiles) {
222+
handles = []; // no handles, we're at a minimum
223+
}
224+
225+
// TODO: This might need adjustment, however for now it is fine as a round.
226+
const nVisible = Math.round(layout.visibleTiles);
227+
const visibleTiles = tiles.slice(0, nVisible);
228+
229+
// If we're hiding rooms, show a 'show more' button to the user. This button
230+
// replaces the last visible tile, so will always show 2+ rooms. We do this
231+
// because if it said "show 1 more room" we had might as well show that room
232+
// instead. We also replace the last item so we don't have to adjust our math
233+
// on pixel heights, etc. It's much easier to pretend the button is a tile.
234+
if (tiles.length > nVisible) {
235+
// we have a cutoff condition - add the button to show all
236+
237+
// we +1 to account for the room we're about to hide with our 'show more' button
238+
// this results in the button always being 1+, and not needing an i18n `count`.
239+
const numMissing = (tiles.length - visibleTiles.length) + 1;
240+
241+
// TODO: CSS TBD
242+
// TODO: Make this an actual tile
243+
// TODO: This is likely to pop out of the list, consider that.
244+
visibleTiles.splice(visibleTiles.length - 1, 1, (
245+
<div
246+
onClick={this.onShowAllClick}
247+
style={{height: '34px', lineHeight: '34px', cursor: 'pointer'}}
248+
key='showall'
249+
>
250+
{_t("Show %(n)s more", {n: numMissing})}
251+
</div>
252+
));
253+
}
207254
content = (
208-
<IndicatorScrollbar className='mx_RoomSubList_scroll'>
209-
{tiles}
210-
</IndicatorScrollbar>
255+
<ResizableBox
256+
width={-1}
257+
height={tilesPx}
258+
axis="y"
259+
minConstraints={[-1, minTilesPx]}
260+
maxConstraints={[-1, maxTilesPx]}
261+
resizeHandles={handles}
262+
onResize={this.onResize}
263+
className="mx_RoomSublist2_resizeBox"
264+
>
265+
{visibleTiles}
266+
</ResizableBox>
211267
)
212268
}
213269

src/i18n/strings/en_EN.json

+1
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,7 @@
11361136
"Jump to first unread room.": "Jump to first unread room.",
11371137
"Jump to first invite.": "Jump to first invite.",
11381138
"Add room": "Add room",
1139+
"Show %(n)s more": "Show %(n)s more",
11391140
"Options": "Options",
11401141
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
11411142
"%(count)s unread messages including mentions.|one": "1 unread mention.",

src/stores/room-list/ListLayout.ts

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
Copyright 2020 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { TagID } from "./models";
18+
19+
const TILE_HEIGHT_PX = 34;
20+
21+
interface ISerializedListLayout {
22+
numTiles: number;
23+
}
24+
25+
export class ListLayout {
26+
private _n = 0;
27+
28+
constructor(public readonly tagId: TagID) {
29+
const serialized = localStorage.getItem(this.key);
30+
if (serialized) {
31+
// We don't use the setters as they cause writes.
32+
const parsed = <ISerializedListLayout>JSON.parse(serialized);
33+
this._n = parsed.numTiles;
34+
}
35+
}
36+
37+
public get tileHeight(): number {
38+
return TILE_HEIGHT_PX;
39+
}
40+
41+
private get key(): string {
42+
return `mx_sublist_layout_${this.tagId}_boxed`;
43+
}
44+
45+
public get visibleTiles(): number {
46+
return Math.max(this._n, this.minVisibleTiles);
47+
}
48+
49+
public set visibleTiles(v: number) {
50+
this._n = v;
51+
localStorage.setItem(this.key, JSON.stringify(this.serialize()));
52+
}
53+
54+
public get minVisibleTiles(): number {
55+
return 3;
56+
}
57+
58+
public tilesToPixels(n: number): number {
59+
return n * this.tileHeight;
60+
}
61+
62+
public pixelsToTiles(px: number): number {
63+
return px / this.tileHeight;
64+
}
65+
66+
private serialize(): ISerializedListLayout {
67+
return {
68+
numTiles: this.visibleTiles,
69+
};
70+
}
71+
}

0 commit comments

Comments
 (0)