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

Commit 5163ad2

Browse files
authored
Use lazy rendering in the AddExistingToSpaceDialog (#7369)
1 parent 53081f5 commit 5163ad2

File tree

3 files changed

+114
-96
lines changed

3 files changed

+114
-96
lines changed

src/components/structures/AutoHideScrollbar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ interface IProps extends Omit<HTMLAttributes<HTMLDivElement>, "onScroll"> {
2727
}
2828

2929
export default class AutoHideScrollbar extends React.Component<IProps> {
30-
private containerRef: React.RefObject<HTMLDivElement> = React.createRef();
30+
public readonly containerRef: React.RefObject<HTMLDivElement> = React.createRef();
3131

3232
public componentDidMount() {
3333
if (this.containerRef.current && this.props.onScroll) {

src/components/views/dialogs/AddExistingToSpaceDialog.tsx

Lines changed: 104 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React, { ReactNode, useContext, useMemo, useState } from "react";
17+
import React, { ReactNode, useContext, useMemo, useRef, useState } from "react";
1818
import classNames from "classnames";
1919
import { Room } from "matrix-js-sdk/src/models/room";
2020
import { sleep } from "matrix-js-sdk/src/utils";
2121
import { EventType } from "matrix-js-sdk/src/@types/event";
2222
import { logger } from "matrix-js-sdk/src/logger";
2323

24-
import { _t } from '../../../languageHandler';
24+
import { _t, _td } from '../../../languageHandler';
2525
import BaseDialog from "./BaseDialog";
2626
import Dropdown from "../elements/Dropdown";
2727
import SearchBox from "../../structures/SearchBox";
@@ -38,9 +38,12 @@ import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/Rece
3838
import ProgressBar from "../elements/ProgressBar";
3939
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
4040
import QueryMatcher from "../../../autocomplete/QueryMatcher";
41-
import TruncatedList from "../elements/TruncatedList";
42-
import EntityTile from "../rooms/EntityTile";
43-
import BaseAvatar from "../avatars/BaseAvatar";
41+
import LazyRenderList from "../elements/LazyRenderList";
42+
43+
// These values match CSS
44+
const ROW_HEIGHT = 32 + 12;
45+
const HEADER_HEIGHT = 15;
46+
const GROUP_MARGIN = 24;
4447

4548
interface IProps {
4649
space: Room;
@@ -64,31 +67,56 @@ export const Entry = ({ room, checked, onChange }) => {
6467
</label>;
6568
};
6669

70+
type OnChangeFn = (checked: boolean, room: Room) => void;
71+
72+
type Renderer = (
73+
rooms: Room[],
74+
selectedToAdd: Set<Room>,
75+
scrollState: IScrollState,
76+
onChange: undefined | OnChangeFn,
77+
) => ReactNode;
78+
6779
interface IAddExistingToSpaceProps {
6880
space: Room;
6981
footerPrompt?: ReactNode;
7082
filterPlaceholder: string;
7183
emptySelectionButton?: ReactNode;
7284
onFinished(added: boolean): void;
73-
roomsRenderer?(
74-
rooms: Room[],
75-
selectedToAdd: Set<Room>,
76-
onChange: undefined | ((checked: boolean, room: Room) => void),
77-
truncateAt: number,
78-
overflowTile: (overflowCount: number, totalCount: number) => JSX.Element,
79-
): ReactNode;
80-
spacesRenderer?(
81-
spaces: Room[],
82-
selectedToAdd: Set<Room>,
83-
onChange?: (checked: boolean, room: Room) => void,
84-
): ReactNode;
85-
dmsRenderer?(
86-
dms: Room[],
87-
selectedToAdd: Set<Room>,
88-
onChange?: (checked: boolean, room: Room) => void,
89-
): ReactNode;
85+
roomsRenderer?: Renderer;
86+
spacesRenderer?: Renderer;
87+
dmsRenderer?: Renderer;
9088
}
9189

90+
interface IScrollState {
91+
scrollTop: number;
92+
height: number;
93+
}
94+
95+
const getScrollState = (
96+
{ scrollTop, height }: IScrollState,
97+
numItems: number,
98+
...prevGroupSizes: number[]
99+
): IScrollState => {
100+
let heightBefore = 0;
101+
prevGroupSizes.forEach(size => {
102+
heightBefore += GROUP_MARGIN + HEADER_HEIGHT + (size * ROW_HEIGHT);
103+
});
104+
105+
const viewportTop = scrollTop;
106+
const viewportBottom = viewportTop + height;
107+
const listTop = heightBefore + HEADER_HEIGHT;
108+
const listBottom = listTop + (numItems * ROW_HEIGHT);
109+
const top = Math.max(viewportTop, listTop);
110+
const bottom = Math.min(viewportBottom, listBottom);
111+
// the viewport height and scrollTop passed to the LazyRenderList
112+
// is capped at the intersection with the real viewport, so lists
113+
// out of view are passed height 0, so they won't render any items.
114+
return {
115+
scrollTop: Math.max(0, scrollTop - listTop),
116+
height: Math.max(0, bottom - top),
117+
};
118+
};
119+
92120
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
93121
space,
94122
footerPrompt,
@@ -102,6 +130,13 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
102130
const cli = useContext(MatrixClientContext);
103131
const visibleRooms = useMemo(() => cli.getVisibleRooms().filter(r => r.getMyMembership() === "join"), [cli]);
104132

133+
const scrollRef = useRef<AutoHideScrollbar>();
134+
const [scrollState, setScrollState] = useState<IScrollState>({
135+
// these are estimates which update as soon as it mounts
136+
scrollTop: 0,
137+
height: 600,
138+
});
139+
105140
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
106141
const [progress, setProgress] = useState<number>(null);
107142
const [error, setError] = useState<Error>(null);
@@ -229,49 +264,56 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
229264
setSelectedToAdd(new Set(selectedToAdd));
230265
} : null;
231266

232-
const [truncateAt, setTruncateAt] = useState(20);
233-
function overflowTile(overflowCount: number, totalCount: number): JSX.Element {
234-
const text = _t("and %(count)s others...", { count: overflowCount });
235-
return (
236-
<EntityTile
237-
className="mx_EntityTile_ellipsis"
238-
avatarJsx={
239-
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
240-
}
241-
name={text}
242-
presenceState="online"
243-
suppressOnHover={true}
244-
onClick={() => setTruncateAt(totalCount)}
245-
/>
246-
);
247-
}
267+
// only count spaces when alone as they're shown on a separate modal all on their own
268+
const numSpaces = (spacesRenderer && !dmsRenderer && !roomsRenderer) ? spaces.length : 0;
248269

249270
let noResults = true;
250-
if ((roomsRenderer && rooms.length > 0) ||
251-
(dmsRenderer && dms.length > 0) ||
252-
(!roomsRenderer && !dmsRenderer && spacesRenderer && spaces.length > 0) // only count spaces when alone
253-
) {
271+
if ((roomsRenderer && rooms.length > 0) || (dmsRenderer && dms.length > 0) || (numSpaces > 0)) {
254272
noResults = false;
255273
}
256274

275+
const onScroll = () => {
276+
const body = scrollRef.current?.containerRef.current;
277+
setScrollState({
278+
scrollTop: body.scrollTop,
279+
height: body.clientHeight,
280+
});
281+
};
282+
283+
const wrappedRef = (body: HTMLDivElement) => {
284+
setScrollState({
285+
scrollTop: body.scrollTop,
286+
height: body.clientHeight,
287+
});
288+
};
289+
290+
const roomsScrollState = getScrollState(scrollState, rooms.length);
291+
const spacesScrollState = getScrollState(scrollState, numSpaces, rooms.length);
292+
const dmsScrollState = getScrollState(scrollState, dms.length, numSpaces, rooms.length);
293+
257294
return <div className="mx_AddExistingToSpace">
258295
<SearchBox
259296
className="mx_textinput_icon mx_textinput_search"
260297
placeholder={filterPlaceholder}
261298
onSearch={setQuery}
262299
autoFocus={true}
263300
/>
264-
<AutoHideScrollbar className="mx_AddExistingToSpace_content">
301+
<AutoHideScrollbar
302+
className="mx_AddExistingToSpace_content"
303+
onScroll={onScroll}
304+
wrappedRef={wrappedRef}
305+
ref={scrollRef}
306+
>
265307
{ rooms.length > 0 && roomsRenderer ? (
266-
roomsRenderer(rooms, selectedToAdd, onChange, truncateAt, overflowTile)
308+
roomsRenderer(rooms, selectedToAdd, roomsScrollState, onChange)
267309
) : undefined }
268310

269311
{ spaces.length > 0 && spacesRenderer ? (
270-
spacesRenderer(spaces, selectedToAdd, onChange)
312+
spacesRenderer(spaces, selectedToAdd, spacesScrollState, onChange)
271313
) : null }
272314

273315
{ dms.length > 0 && dmsRenderer ? (
274-
dmsRenderer(dms, selectedToAdd, onChange)
316+
dmsRenderer(dms, selectedToAdd, dmsScrollState, onChange)
275317
) : null }
276318

277319
{ noResults ? <span className="mx_AddExistingToSpace_noResults">
@@ -285,59 +327,36 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
285327
</div>;
286328
};
287329

288-
export const defaultRoomsRenderer: IAddExistingToSpaceProps["roomsRenderer"] = (
289-
rooms, selectedToAdd, onChange, truncateAt, overflowTile,
330+
const defaultRendererFactory = (title: string): Renderer => (
331+
rooms,
332+
selectedToAdd,
333+
{ scrollTop, height },
334+
onChange,
290335
) => (
291336
<div className="mx_AddExistingToSpace_section">
292-
<h3>{ _t("Rooms") }</h3>
293-
<TruncatedList
294-
truncateAt={truncateAt}
295-
createOverflowElement={overflowTile}
296-
getChildren={(start, end) => rooms.slice(start, end).map(room =>
337+
<h3>{ _t(title) }</h3>
338+
<LazyRenderList
339+
itemHeight={ROW_HEIGHT}
340+
items={rooms}
341+
scrollTop={scrollTop}
342+
height={height}
343+
renderItem={room => (
297344
<Entry
298345
key={room.roomId}
299346
room={room}
300347
checked={selectedToAdd.has(room)}
301348
onChange={onChange ? (checked: boolean) => {
302349
onChange(checked, room);
303350
} : null}
304-
/>,
351+
/>
305352
)}
306-
getChildCount={() => rooms.length}
307353
/>
308354
</div>
309355
);
310356

311-
export const defaultSpacesRenderer: IAddExistingToSpaceProps["spacesRenderer"] = (spaces, selectedToAdd, onChange) => (
312-
<div className="mx_AddExistingToSpace_section">
313-
{ spaces.map(space => {
314-
return <Entry
315-
key={space.roomId}
316-
room={space}
317-
checked={selectedToAdd.has(space)}
318-
onChange={onChange ? (checked) => {
319-
onChange(checked, space);
320-
} : null}
321-
/>;
322-
}) }
323-
</div>
324-
);
325-
326-
export const defaultDmsRenderer: IAddExistingToSpaceProps["dmsRenderer"] = (dms, selectedToAdd, onChange) => (
327-
<div className="mx_AddExistingToSpace_section">
328-
<h3>{ _t("Direct Messages") }</h3>
329-
{ dms.map(room => {
330-
return <Entry
331-
key={room.roomId}
332-
room={room}
333-
checked={selectedToAdd.has(room)}
334-
onChange={onChange ? (checked: boolean) => {
335-
onChange(checked, room);
336-
} : null}
337-
/>;
338-
}) }
339-
</div>
340-
);
357+
export const defaultRoomsRenderer = defaultRendererFactory(_td("Rooms"));
358+
export const defaultSpacesRenderer = defaultRendererFactory(_td("Spaces"));
359+
export const defaultDmsRenderer = defaultRendererFactory(_td("Direct Messages"));
341360

342361
interface ISubspaceSelectorProps {
343362
title: string;

src/components/views/emojipicker/EmojiPicker.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ class EmojiPicker extends React.Component<IProps, IState> {
5656
private readonly memoizedDataByCategory: Record<CategoryKey, IEmoji[]>;
5757
private readonly categories: ICategory[];
5858

59-
private bodyRef = React.createRef<HTMLDivElement>();
59+
private scrollRef = React.createRef<AutoHideScrollbar>();
6060

61-
constructor(props) {
61+
constructor(props: IProps) {
6262
super(props);
6363

6464
this.state = {
@@ -133,7 +133,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
133133
}
134134

135135
private onScroll = () => {
136-
const body = this.bodyRef.current;
136+
const body = this.scrollRef.current?.containerRef.current;
137137
this.setState({
138138
scrollTop: body.scrollTop,
139139
viewportHeight: body.clientHeight,
@@ -142,7 +142,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
142142
};
143143

144144
private updateVisibility = () => {
145-
const body = this.bodyRef.current;
145+
const body = this.scrollRef.current?.containerRef.current;
146146
const rect = body.getBoundingClientRect();
147147
for (const cat of this.categories) {
148148
const elem = body.querySelector(`[data-category-id="${cat.id}"]`);
@@ -169,7 +169,8 @@ class EmojiPicker extends React.Component<IProps, IState> {
169169
};
170170

171171
private scrollToCategory = (category: string) => {
172-
this.bodyRef.current.querySelector(`[data-category-id="${category}"]`).scrollIntoView();
172+
this.scrollRef.current?.containerRef.current
173+
?.querySelector(`[data-category-id="${category}"]`).scrollIntoView();
173174
};
174175

175176
private onChangeFilter = (filter: string) => {
@@ -202,7 +203,8 @@ class EmojiPicker extends React.Component<IProps, IState> {
202203
};
203204

204205
private onEnterFilter = () => {
205-
const btn = this.bodyRef.current.querySelector<HTMLButtonElement>(".mx_EmojiPicker_item");
206+
const btn = this.scrollRef.current?.containerRef.current
207+
?.querySelector<HTMLButtonElement>(".mx_EmojiPicker_item");
206208
if (btn) {
207209
btn.click();
208210
}
@@ -241,10 +243,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
241243
<Search query={this.state.filter} onChange={this.onChangeFilter} onEnter={this.onEnterFilter} />
242244
<AutoHideScrollbar
243245
className="mx_EmojiPicker_body"
244-
wrappedRef={ref => {
245-
// @ts-ignore - AutoHideScrollbar should accept a RefObject or fall back to its own instead
246-
this.bodyRef.current = ref;
247-
}}
246+
ref={this.scrollRef}
248247
onScroll={this.onScroll}
249248
>
250249
{ this.categories.map(category => {

0 commit comments

Comments
 (0)