Merge branch 'develop' into travis/room-list/perf/notifications

This commit is contained in:
Travis Ralston 2020-07-09 07:46:01 -06:00
commit 57b2ae9a0d
7 changed files with 127 additions and 55 deletions

View file

@ -21,6 +21,7 @@ import ToastStore from "../stores/ToastStore";
import DeviceListener from "../DeviceListener"; import DeviceListener from "../DeviceListener";
import { RoomListStore2 } from "../stores/room-list/RoomListStore2"; import { RoomListStore2 } from "../stores/room-list/RoomListStore2";
import { PlatformPeg } from "../PlatformPeg"; import { PlatformPeg } from "../PlatformPeg";
import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore";
declare global { declare global {
interface Window { interface Window {
@ -34,6 +35,7 @@ declare global {
mx_ToastStore: ToastStore; mx_ToastStore: ToastStore;
mx_DeviceListener: DeviceListener; mx_DeviceListener: DeviceListener;
mx_RoomListStore2: RoomListStore2; mx_RoomListStore2: RoomListStore2;
mx_RoomListLayoutStore: RoomListLayoutStore;
mxPlatformPeg: PlatformPeg; mxPlatformPeg: PlatformPeg;
} }

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from "react"; import React from "react";
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore"; import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
import AccessibleButton from "../elements/AccessibleButton";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
@ -92,9 +91,6 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
}; };
public render(): React.ReactElement { public render(): React.ReactElement {
// TODO: Decorate crumbs with icons: https://github.com/vector-im/riot-web/issues/14040
// TODO: Scrolling: https://github.com/vector-im/riot-web/issues/14040
// TODO: Tooltips: https://github.com/vector-im/riot-web/issues/14040
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => { const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
const roomTags = RoomListStore.instance.getTagsForRoom(r); const roomTags = RoomListStore.instance.getTagsForRoom(r);
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0]; const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];

View file

@ -32,7 +32,6 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import RoomSublist2 from "./RoomSublist2"; import RoomSublist2 from "./RoomSublist2";
import { ActionPayload } from "../../../dispatcher/payloads"; import { ActionPayload } from "../../../dispatcher/payloads";
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition"; import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
import { ListLayout } from "../../../stores/room-list/ListLayout";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import GroupAvatar from "../avatars/GroupAvatar"; import GroupAvatar from "../avatars/GroupAvatar";
import TemporaryTile from "./TemporaryTile"; import TemporaryTile from "./TemporaryTile";
@ -66,7 +65,6 @@ interface IProps {
interface IState { interface IState {
sublists: ITagMap; sublists: ITagMap;
layouts: Map<TagID, ListLayout>;
} }
const TAG_ORDER: TagID[] = [ const TAG_ORDER: TagID[] = [
@ -151,7 +149,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
this.state = { this.state = {
sublists: {}, sublists: {},
layouts: new Map<TagID, ListLayout>(),
}; };
this.dispatcherRef = defaultDispatcher.register(this.onAction); this.dispatcherRef = defaultDispatcher.register(this.onAction);
@ -224,12 +221,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
const newLists = RoomListStore.instance.orderedLists; const newLists = RoomListStore.instance.orderedLists;
console.log("new lists", newLists); console.log("new lists", newLists);
const layoutMap = new Map<TagID, ListLayout>(); this.setState({sublists: newLists}, () => {
for (const tagId of Object.keys(newLists)) {
layoutMap.set(tagId, new ListLayout(tagId));
}
this.setState({sublists: newLists, layouts: layoutMap}, () => {
this.props.onResize(); this.props.onResize();
}); });
}; };
@ -298,7 +290,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
label={_t(aesthetics.sectionLabel)} label={_t(aesthetics.sectionLabel)}
onAddRoom={onAddRoomFn} onAddRoom={onAddRoomFn}
addRoomLabel={aesthetics.addRoomLabel} addRoomLabel={aesthetics.addRoomLabel}
layout={this.state.layouts.get(orderedTagId)}
isMinimized={this.props.isMinimized} isMinimized={this.props.isMinimized}
onResize={this.props.onResize} onResize={this.props.onResize}
extraBadTilesThatShouldntExist={extraTiles} extraBadTilesThatShouldntExist={extraTiles}

View file

@ -46,6 +46,7 @@ import { Enable, Resizable } from "re-resizable";
import { Direction } from "re-resizable/lib/resizer"; import { Direction } from "re-resizable/lib/resizer";
import { polyfillTouchEvent } from "../../../@types/polyfill"; import { polyfillTouchEvent } from "../../../@types/polyfill";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231 // TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
@ -74,7 +75,6 @@ interface IProps {
label: string; label: string;
onAddRoom?: () => void; onAddRoom?: () => void;
addRoomLabel: string; addRoomLabel: string;
layout?: ListLayout;
isMinimized: boolean; isMinimized: boolean;
tagId: TagID; tagId: TagID;
onResize: () => void; onResize: () => void;
@ -98,10 +98,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
private headerButton = createRef<HTMLDivElement>(); private headerButton = createRef<HTMLDivElement>();
private sublistRef = createRef<HTMLDivElement>(); private sublistRef = createRef<HTMLDivElement>();
private dispatcherRef: string; private dispatcherRef: string;
private layout: ListLayout;
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId);
this.state = { this.state = {
notificationState: RoomNotificationStateStore.instance.getListState(this.props.tagId), notificationState: RoomNotificationStateStore.instance.getListState(this.props.tagId),
contextMenuPosition: null, contextMenuPosition: null,
@ -116,8 +119,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
} }
private get numVisibleTiles(): number { private get numVisibleTiles(): number {
if (!this.props.layout) return 0; const nVisible = Math.floor(this.layout.visibleTiles);
const nVisible = Math.floor(this.props.layout.visibleTiles);
return Math.min(nVisible, this.numTiles); return Math.min(nVisible, this.numTiles);
} }
@ -135,7 +137,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// XXX: we have to do this a tick later because we have incorrect intermediate props during a room change // XXX: we have to do this a tick later because we have incorrect intermediate props during a room change
// where we lose the room we are changing from temporarily and then it comes back in an update right after. // where we lose the room we are changing from temporarily and then it comes back in an update right after.
setImmediate(() => { setImmediate(() => {
const isCollapsed = this.props.layout.isCollapsed; const isCollapsed = this.layout.isCollapsed;
const roomIndex = this.props.rooms.findIndex((r) => r.roomId === payload.room_id); const roomIndex = this.props.rooms.findIndex((r) => r.roomId === payload.room_id);
if (isCollapsed && roomIndex > -1) { if (isCollapsed && roomIndex > -1) {
@ -143,7 +145,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
} }
// extend the visible section to include the room if it is entirely invisible // extend the visible section to include the room if it is entirely invisible
if (roomIndex >= this.numVisibleTiles) { if (roomIndex >= this.numVisibleTiles) {
this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(roomIndex + 1, MAX_PADDING_HEIGHT); this.layout.visibleTiles = this.layout.tilesWithPadding(roomIndex + 1, MAX_PADDING_HEIGHT);
this.forceUpdate(); // because the layout doesn't trigger a re-render this.forceUpdate(); // because the layout doesn't trigger a re-render
} }
}); });
@ -170,10 +172,10 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// resizing started*, meaning it is fairly useless for us. This is why we just use // resizing started*, meaning it is fairly useless for us. This is why we just use
// the client height and run with it. // the client height and run with it.
const heightBefore = this.props.layout.visibleTiles; const heightBefore = this.layout.visibleTiles;
const heightInTiles = this.props.layout.pixelsToTiles(refToElement.clientHeight); const heightInTiles = this.layout.pixelsToTiles(refToElement.clientHeight);
this.props.layout.setVisibleTilesWithin(heightInTiles, this.numTiles); this.layout.setVisibleTilesWithin(heightInTiles, this.numTiles);
if (heightBefore === this.props.layout.visibleTiles) return; // no-op if (heightBefore === this.layout.visibleTiles) return; // no-op
this.forceUpdate(); // because the layout doesn't trigger a re-render this.forceUpdate(); // because the layout doesn't trigger a re-render
}; };
@ -187,13 +189,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
private onShowAllClick = () => { private onShowAllClick = () => {
const numVisibleTiles = this.numVisibleTiles; const numVisibleTiles = this.numVisibleTiles;
this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT); this.layout.visibleTiles = this.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
this.forceUpdate(); // because the layout doesn't trigger a re-render this.forceUpdate(); // because the layout doesn't trigger a re-render
setImmediate(this.focusRoomTile, numVisibleTiles); // focus the tile after the current bottom one setImmediate(this.focusRoomTile, numVisibleTiles); // focus the tile after the current bottom one
}; };
private onShowLessClick = () => { private onShowLessClick = () => {
this.props.layout.visibleTiles = this.props.layout.defaultVisibleTiles; this.layout.visibleTiles = this.layout.defaultVisibleTiles;
this.forceUpdate(); // because the layout doesn't trigger a re-render this.forceUpdate(); // because the layout doesn't trigger a re-render
// focus will flow to the show more button here // focus will flow to the show more button here
}; };
@ -241,7 +243,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
}; };
private onMessagePreviewChanged = () => { private onMessagePreviewChanged = () => {
this.props.layout.showPreviews = !this.props.layout.showPreviews; this.layout.showPreviews = !this.layout.showPreviews;
this.forceUpdate(); // because the layout doesn't trigger a re-render this.forceUpdate(); // because the layout doesn't trigger a re-render
}; };
@ -293,13 +295,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
}; };
private toggleCollapsed = () => { private toggleCollapsed = () => {
this.props.layout.isCollapsed = !this.props.layout.isCollapsed; this.layout.isCollapsed = !this.layout.isCollapsed;
this.forceUpdate(); // because the layout doesn't trigger an update this.forceUpdate(); // because the layout doesn't trigger an update
setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated
}; };
private onHeaderKeyDown = (ev: React.KeyboardEvent) => { private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
const isCollapsed = this.props.layout && this.props.layout.isCollapsed; const isCollapsed = this.layout && this.layout.isCollapsed;
switch (ev.key) { switch (ev.key) {
case Key.ARROW_LEFT: case Key.ARROW_LEFT:
ev.stopPropagation(); ev.stopPropagation();
@ -339,7 +341,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
}; };
private renderVisibleTiles(): React.ReactElement[] { private renderVisibleTiles(): React.ReactElement[] {
if (this.props.layout && this.props.layout.isCollapsed) { if (this.layout && this.layout.isCollapsed) {
// don't waste time on rendering // don't waste time on rendering
return []; return [];
} }
@ -353,7 +355,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
<RoomTile2 <RoomTile2
room={room} room={room}
key={`room-${room.roomId}`} key={`room-${room.roomId}`}
showMessagePreview={this.props.layout.showPreviews} showMessagePreview={this.layout.showPreviews}
isMinimized={this.props.isMinimized} isMinimized={this.props.isMinimized}
tag={this.props.tagId} tag={this.props.tagId}
/> />
@ -404,7 +406,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
<StyledMenuItemCheckbox <StyledMenuItemCheckbox
onClose={this.onCloseMenu} onClose={this.onCloseMenu}
onChange={this.onMessagePreviewChanged} onChange={this.onMessagePreviewChanged}
checked={this.props.layout.showPreviews} checked={this.layout.showPreviews}
> >
{_t("Message preview")} {_t("Message preview")}
</StyledMenuItemCheckbox> </StyledMenuItemCheckbox>
@ -496,7 +498,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const collapseClasses = classNames({ const collapseClasses = classNames({
'mx_RoomSublist2_collapseBtn': true, 'mx_RoomSublist2_collapseBtn': true,
'mx_RoomSublist2_collapseBtn_collapsed': this.props.layout && this.props.layout.isCollapsed, 'mx_RoomSublist2_collapseBtn_collapsed': this.layout && this.layout.isCollapsed,
}); });
const classes = classNames({ const classes = classNames({
@ -524,7 +526,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
tabIndex={tabIndex} tabIndex={tabIndex}
className="mx_RoomSublist2_headerText" className="mx_RoomSublist2_headerText"
role="treeitem" role="treeitem"
aria-expanded={!this.props.layout || !this.props.layout.isCollapsed} aria-expanded={!this.layout.isCollapsed}
aria-level={1} aria-level={1}
onClick={this.onHeaderClick} onClick={this.onHeaderClick}
onContextMenu={this.onContextMenu} onContextMenu={this.onContextMenu}
@ -558,7 +560,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
let content = null; let content = null;
if (visibleTiles.length > 0) { if (visibleTiles.length > 0) {
const layout = this.props.layout; // to shorten calls const layout = this.layout; // to shorten calls
const maxTilesFactored = layout.tilesWithResizerBoxFactor(this.numTiles); const maxTilesFactored = layout.tilesWithResizerBoxFactor(this.numTiles);
const showMoreBtnClasses = classNames({ const showMoreBtnClasses = classNames({
@ -587,7 +589,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
{showMoreText} {showMoreText}
</RovingAccessibleButton> </RovingAccessibleButton>
); );
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.props.layout.defaultVisibleTiles) { } else if (this.numTiles <= visibleTiles.length && this.numTiles > this.layout.defaultVisibleTiles) {
// we have all tiles visible - add a button to show less // we have all tiles visible - add a button to show less
let showLessText = ( let showLessText = (
<span className='mx_RoomSublist2_showNButtonText'> <span className='mx_RoomSublist2_showNButtonText'>

View file

@ -125,6 +125,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
} }
private async appendRoom(room: Room) { private async appendRoom(room: Room) {
let updated = false;
const rooms = (this.state.rooms || []).slice(); // cheap clone const rooms = (this.state.rooms || []).slice(); // cheap clone
// If the room is upgraded, use that room instead. We'll also splice out // If the room is upgraded, use that room instead. We'll also splice out
@ -136,25 +137,36 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
// Take out any room that isn't the most recent room // Take out any room that isn't the most recent room
for (let i = 0; i < history.length - 1; i++) { for (let i = 0; i < history.length - 1; i++) {
const idx = rooms.findIndex(r => r.roomId === history[i].roomId); const idx = rooms.findIndex(r => r.roomId === history[i].roomId);
if (idx !== -1) rooms.splice(idx, 1); if (idx !== -1) {
rooms.splice(idx, 1);
updated = true;
}
} }
} }
// Remove the existing room, if it is present // Remove the existing room, if it is present
const existingIdx = rooms.findIndex(r => r.roomId === room.roomId); const existingIdx = rooms.findIndex(r => r.roomId === room.roomId);
// If we're focusing on the first room no-op
if (existingIdx !== 0) {
if (existingIdx !== -1) { if (existingIdx !== -1) {
rooms.splice(existingIdx, 1); rooms.splice(existingIdx, 1);
} }
// Splice the room to the start of the list // Splice the room to the start of the list
rooms.splice(0, 0, room); rooms.splice(0, 0, room);
updated = true;
}
if (rooms.length > MAX_ROOMS) { if (rooms.length > MAX_ROOMS) {
// This looks weird, but it's saying to start at the MAX_ROOMS point in the // This looks weird, but it's saying to start at the MAX_ROOMS point in the
// list and delete everything after it. // list and delete everything after it.
rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS); rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS);
updated = true;
} }
if (updated) {
// Update the breadcrumbs // Update the breadcrumbs
await this.updateState({rooms}); await this.updateState({rooms});
const roomIds = rooms.map(r => r.roomId); const roomIds = rooms.map(r => r.roomId);
@ -162,5 +174,6 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
await SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds); await SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds);
} }
} }
}
} }

View file

@ -0,0 +1,73 @@
/*
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 { TagID } from "./models";
import { ListLayout } from "./ListLayout";
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { ActionPayload } from "../../dispatcher/payloads";
interface IState {}
export default class RoomListLayoutStore extends AsyncStoreWithClient<IState> {
private static internalInstance: RoomListLayoutStore;
private readonly layoutMap = new Map<TagID, ListLayout>();
constructor() {
super(defaultDispatcher);
}
public static get instance(): RoomListLayoutStore {
if (!RoomListLayoutStore.internalInstance) {
RoomListLayoutStore.internalInstance = new RoomListLayoutStore();
}
return RoomListLayoutStore.internalInstance;
}
public ensureLayoutExists(tagId: TagID) {
if (!this.layoutMap.has(tagId)) {
this.layoutMap.set(tagId, new ListLayout(tagId));
}
}
public getLayoutFor(tagId: TagID): ListLayout {
if (!this.layoutMap.has(tagId)) {
this.layoutMap.set(tagId, new ListLayout(tagId));
}
return this.layoutMap.get(tagId);
}
// Note: this primarily exists for debugging, and isn't really intended to be used by anything.
public async resetLayouts() {
console.warn("Resetting layouts for room list");
for (const layout of this.layoutMap.values()) {
layout.reset();
}
}
protected async onNotReady(): Promise<any> {
// On logout, clear the map.
this.layoutMap.clear();
}
// We don't need this function, but our contract says we do
protected async onAction(payload: ActionPayload): Promise<any> {
return Promise.resolve();
}
}
window.mx_RoomListLayoutStore = RoomListLayoutStore.instance;

View file

@ -32,6 +32,7 @@ import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm";
import { EffectiveMembership, getEffectiveMembership } from "./membership"; import { EffectiveMembership, getEffectiveMembership } from "./membership";
import { ListLayout } from "./ListLayout"; import { ListLayout } from "./ListLayout";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import RoomListLayoutStore from "./RoomListLayoutStore";
interface IState { interface IState {
tagsEnabled?: boolean; tagsEnabled?: boolean;
@ -50,6 +51,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
private algorithm = new Algorithm(); private algorithm = new Algorithm();
private filterConditions: IFilterCondition[] = []; private filterConditions: IFilterCondition[] = [];
private tagWatcher = new TagWatcher(this); private tagWatcher = new TagWatcher(this);
private layoutMap: Map<TagID, ListLayout> = new Map<TagID, ListLayout>();
private readonly watchedSettings = [ private readonly watchedSettings = [
'feature_custom_tags', 'feature_custom_tags',
@ -416,6 +418,8 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
for (const tagId of OrderedDefaultTagIDs) { for (const tagId of OrderedDefaultTagIDs) {
sorts[tagId] = this.calculateTagSorting(tagId); sorts[tagId] = this.calculateTagSorting(tagId);
orders[tagId] = this.calculateListOrder(tagId); orders[tagId] = this.calculateListOrder(tagId);
RoomListLayoutStore.instance.ensureLayoutExists(tagId);
} }
if (this.state.tagsEnabled) { if (this.state.tagsEnabled) {
@ -434,15 +438,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
this.emit(LISTS_UPDATE_EVENT, this); this.emit(LISTS_UPDATE_EVENT, this);
} }
// Note: this primarily exists for debugging, and isn't really intended to be used by anything.
public async resetLayouts() {
console.warn("Resetting layouts for room list");
for (const tagId of Object.keys(this.orderedLists)) {
new ListLayout(tagId).reset();
}
await this.regenerateAllLists();
}
public addFilter(filter: IFilterCondition): void { public addFilter(filter: IFilterCondition): void {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
console.log("Adding filter condition:", filter); console.log("Adding filter condition:", filter);