Merge pull request #5054 from matrix-org/travis/perf6
Minor improvements to filtering performance
This commit is contained in:
commit
3561de3f8a
12 changed files with 150 additions and 81 deletions
|
@ -44,7 +44,6 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
searchFilter: string;
|
|
||||||
showBreadcrumbs: boolean;
|
showBreadcrumbs: boolean;
|
||||||
showTagPanel: boolean;
|
showTagPanel: boolean;
|
||||||
}
|
}
|
||||||
|
@ -69,7 +68,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
searchFilter: "",
|
|
||||||
showBreadcrumbs: BreadcrumbsStore.instance.visible,
|
showBreadcrumbs: BreadcrumbsStore.instance.visible,
|
||||||
showTagPanel: SettingsStore.getValue('TagPanel.enableTagPanel'),
|
showTagPanel: SettingsStore.getValue('TagPanel.enableTagPanel'),
|
||||||
};
|
};
|
||||||
|
@ -97,10 +95,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
|
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSearch = (term: string): void => {
|
|
||||||
this.setState({searchFilter: term});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onExplore = () => {
|
private onExplore = () => {
|
||||||
dis.fire(Action.ViewRoomDirectory);
|
dis.fire(Action.ViewRoomDirectory);
|
||||||
};
|
};
|
||||||
|
@ -366,7 +360,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
>
|
>
|
||||||
<RoomSearch
|
<RoomSearch
|
||||||
onQueryUpdate={this.onSearch}
|
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
onVerticalArrow={this.onKeyDown}
|
onVerticalArrow={this.onKeyDown}
|
||||||
onEnter={this.onEnter}
|
onEnter={this.onEnter}
|
||||||
|
@ -392,7 +385,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
resizeNotifier={null}
|
resizeNotifier={null}
|
||||||
collapsed={false}
|
collapsed={false}
|
||||||
searchFilter={this.state.searchFilter}
|
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
|
|
|
@ -24,9 +24,10 @@ import { throttle } from 'lodash';
|
||||||
import { Key } from "../../Keyboard";
|
import { Key } from "../../Keyboard";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
|
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||||
|
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onQueryUpdate: (newQuery: string) => void;
|
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
onVerticalArrow(ev: React.KeyboardEvent): void;
|
onVerticalArrow(ev: React.KeyboardEvent): void;
|
||||||
onEnter(ev: React.KeyboardEvent): boolean;
|
onEnter(ev: React.KeyboardEvent): boolean;
|
||||||
|
@ -40,6 +41,7 @@ interface IState {
|
||||||
export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private inputRef: React.RefObject<HTMLInputElement> = createRef();
|
private inputRef: React.RefObject<HTMLInputElement> = createRef();
|
||||||
|
private searchFilter: NameFilterCondition = new NameFilterCondition();
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -52,6 +54,21 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
|
||||||
|
if (prevState.query !== this.state.query) {
|
||||||
|
const hadSearch = !!this.searchFilter.search.trim();
|
||||||
|
const haveSearch = !!this.state.query.trim();
|
||||||
|
this.searchFilter.search = this.state.query;
|
||||||
|
if (!hadSearch && haveSearch) {
|
||||||
|
// started a new filter - add the condition
|
||||||
|
RoomListStore.instance.addFilter(this.searchFilter);
|
||||||
|
} else if (hadSearch && !haveSearch) {
|
||||||
|
// cleared a filter - remove the condition
|
||||||
|
RoomListStore.instance.removeFilter(this.searchFilter);
|
||||||
|
} // else the filter hasn't changed enough for us to care here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
defaultDispatcher.unregister(this.dispatcherRef);
|
defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
@ -78,19 +95,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
private onChange = () => {
|
private onChange = () => {
|
||||||
if (!this.inputRef.current) return;
|
if (!this.inputRef.current) return;
|
||||||
this.setState({query: this.inputRef.current.value});
|
this.setState({query: this.inputRef.current.value});
|
||||||
this.onSearchUpdated();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// it wants this at the top of the file, but we know better
|
|
||||||
// tslint:disable-next-line
|
|
||||||
private onSearchUpdated = throttle(
|
|
||||||
() => {
|
|
||||||
// We can't use the state variable because it can lag behind the input.
|
|
||||||
// The lag is most obvious when deleting/clearing text with the keyboard.
|
|
||||||
this.props.onQueryUpdate(this.inputRef.current.value);
|
|
||||||
}, 200, {trailing: true, leading: true},
|
|
||||||
);
|
|
||||||
|
|
||||||
private onFocus = (ev: React.FocusEvent<HTMLInputElement>) => {
|
private onFocus = (ev: React.FocusEvent<HTMLInputElement>) => {
|
||||||
this.setState({focused: true});
|
this.setState({focused: true});
|
||||||
ev.target.select();
|
ev.target.select();
|
||||||
|
|
|
@ -31,7 +31,6 @@ import dis from "../../../dispatcher/dispatcher";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import RoomSublist from "./RoomSublist";
|
import RoomSublist from "./RoomSublist";
|
||||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
|
|
||||||
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";
|
||||||
|
@ -52,7 +51,6 @@ interface IProps {
|
||||||
onResize: () => void;
|
onResize: () => void;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
searchFilter: string;
|
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,8 +148,7 @@ function customTagAesthetics(tagId: TagID): ITagAesthetics {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RoomList extends React.Component<IProps, IState> {
|
export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
private searchFilter: NameFilterCondition = new NameFilterCondition();
|
|
||||||
private dispatcherRef;
|
private dispatcherRef;
|
||||||
private customTagStoreRef;
|
private customTagStoreRef;
|
||||||
|
|
||||||
|
@ -165,21 +162,6 @@ export default class RoomList extends React.Component<IProps, IState> {
|
||||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: Readonly<IProps>): void {
|
|
||||||
if (prevProps.searchFilter !== this.props.searchFilter) {
|
|
||||||
const hadSearch = !!this.searchFilter.search.trim();
|
|
||||||
const haveSearch = !!this.props.searchFilter.trim();
|
|
||||||
this.searchFilter.search = this.props.searchFilter;
|
|
||||||
if (!hadSearch && haveSearch) {
|
|
||||||
// started a new filter - add the condition
|
|
||||||
RoomListStore.instance.addFilter(this.searchFilter);
|
|
||||||
} else if (hadSearch && !haveSearch) {
|
|
||||||
// cleared a filter - remove the condition
|
|
||||||
RoomListStore.instance.removeFilter(this.searchFilter);
|
|
||||||
} // else the filter hasn't changed enough for us to care here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
|
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
|
||||||
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
|
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
|
||||||
|
@ -266,12 +248,11 @@ export default class RoomList extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderCommunityInvites(): React.ReactElement[] {
|
private renderCommunityInvites(): TemporaryTile[] {
|
||||||
// TODO: Put community invites in a more sensible place (not in the room list)
|
// TODO: Put community invites in a more sensible place (not in the room list)
|
||||||
// See https://github.com/vector-im/riot-web/issues/14456
|
// See https://github.com/vector-im/riot-web/issues/14456
|
||||||
return MatrixClientPeg.get().getGroups().filter(g => {
|
return MatrixClientPeg.get().getGroups().filter(g => {
|
||||||
if (g.myMembership !== 'invite') return false;
|
return g.myMembership === 'invite';
|
||||||
return !this.searchFilter || this.searchFilter.matches(g.name || "");
|
|
||||||
}).map(g => {
|
}).map(g => {
|
||||||
const avatar = (
|
const avatar = (
|
||||||
<GroupAvatar
|
<GroupAvatar
|
||||||
|
@ -340,7 +321,6 @@ export default class RoomList extends React.Component<IProps, IState> {
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
onResize={this.props.onResize}
|
onResize={this.props.onResize}
|
||||||
extraBadTilesThatShouldntExist={extraTiles}
|
extraBadTilesThatShouldntExist={extraTiles}
|
||||||
isFiltered={!!this.searchFilter.search}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,6 @@ import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import NotificationBadge from "./NotificationBadge";
|
import NotificationBadge from "./NotificationBadge";
|
||||||
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
|
@ -49,6 +48,8 @@ import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNo
|
||||||
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
|
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
|
||||||
import { arrayHasOrderChange } from "../../../utils/arrays";
|
import { arrayHasOrderChange } from "../../../utils/arrays";
|
||||||
import { objectExcluding, objectHasValueChange } from "../../../utils/objects";
|
import { objectExcluding, objectHasValueChange } from "../../../utils/objects";
|
||||||
|
import TemporaryTile from "./TemporaryTile";
|
||||||
|
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
||||||
|
|
||||||
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
|
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
|
||||||
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
|
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
|
||||||
|
@ -68,11 +69,10 @@ interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
tagId: TagID;
|
tagId: TagID;
|
||||||
onResize: () => void;
|
onResize: () => void;
|
||||||
isFiltered: boolean;
|
|
||||||
|
|
||||||
// TODO: Don't use this. It's for community invites, and community invites shouldn't be here.
|
// TODO: Don't use this. It's for community invites, and community invites shouldn't be here.
|
||||||
// You should feel bad if you use this.
|
// You should feel bad if you use this.
|
||||||
extraBadTilesThatShouldntExist?: React.ReactElement[];
|
extraBadTilesThatShouldntExist?: TemporaryTile[];
|
||||||
|
|
||||||
// TODO: Account for https://github.com/vector-im/riot-web/issues/14179
|
// TODO: Account for https://github.com/vector-im/riot-web/issues/14179
|
||||||
}
|
}
|
||||||
|
@ -86,12 +86,12 @@ interface ResizeDelta {
|
||||||
type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">;
|
type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">;
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
notificationState: ListNotificationState;
|
|
||||||
contextMenuPosition: PartialDOMRect;
|
contextMenuPosition: PartialDOMRect;
|
||||||
isResizing: boolean;
|
isResizing: boolean;
|
||||||
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
|
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
|
||||||
height: number;
|
height: number;
|
||||||
rooms: Room[];
|
rooms: Room[];
|
||||||
|
filteredExtraTiles?: TemporaryTile[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RoomSublist extends React.Component<IProps, IState> {
|
export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
|
@ -100,23 +100,25 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private layout: ListLayout;
|
private layout: ListLayout;
|
||||||
private heightAtStart: number;
|
private heightAtStart: number;
|
||||||
|
private isBeingFiltered: boolean;
|
||||||
|
private notificationState: ListNotificationState;
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId);
|
this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId);
|
||||||
this.heightAtStart = 0;
|
this.heightAtStart = 0;
|
||||||
|
this.isBeingFiltered = !!RoomListStore.instance.getFirstNameFilterCondition();
|
||||||
|
this.notificationState = RoomNotificationStateStore.instance.getListState(this.props.tagId);
|
||||||
this.state = {
|
this.state = {
|
||||||
notificationState: RoomNotificationStateStore.instance.getListState(this.props.tagId),
|
|
||||||
contextMenuPosition: null,
|
contextMenuPosition: null,
|
||||||
isResizing: false,
|
isResizing: false,
|
||||||
isExpanded: this.props.isFiltered ? this.props.isFiltered : !this.layout.isCollapsed,
|
isExpanded: this.isBeingFiltered ? this.isBeingFiltered : !this.layout.isCollapsed,
|
||||||
height: 0, // to be fixed in a moment, we need `rooms` to calculate this.
|
height: 0, // to be fixed in a moment, we need `rooms` to calculate this.
|
||||||
rooms: RoomListStore.instance.orderedLists[this.props.tagId] || [],
|
rooms: RoomListStore.instance.orderedLists[this.props.tagId] || [],
|
||||||
};
|
};
|
||||||
// Why Object.assign() and not this.state.height? Because TypeScript says no.
|
// Why Object.assign() and not this.state.height? Because TypeScript says no.
|
||||||
this.state = Object.assign(this.state, {height: this.calculateInitialHeight()});
|
this.state = Object.assign(this.state, {height: this.calculateInitialHeight()});
|
||||||
this.state.notificationState.setRooms(this.state.rooms);
|
|
||||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||||
}
|
}
|
||||||
|
@ -146,8 +148,18 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
return padding;
|
return padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get extraTiles(): TemporaryTile[] | null {
|
||||||
|
if (this.state.filteredExtraTiles) {
|
||||||
|
return this.state.filteredExtraTiles;
|
||||||
|
}
|
||||||
|
if (this.props.extraBadTilesThatShouldntExist) {
|
||||||
|
return this.props.extraBadTilesThatShouldntExist;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private get numTiles(): number {
|
private get numTiles(): number {
|
||||||
return RoomSublist.calcNumTiles(this.state.rooms, this.props.extraBadTilesThatShouldntExist);
|
return RoomSublist.calcNumTiles(this.state.rooms, this.extraTiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static calcNumTiles(rooms: Room[], extraTiles: any[]) {
|
private static calcNumTiles(rooms: Room[], extraTiles: any[]) {
|
||||||
|
@ -160,17 +172,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) {
|
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) {
|
||||||
this.state.notificationState.setRooms(this.state.rooms);
|
const prevExtraTiles = prevState.filteredExtraTiles || prevProps.extraBadTilesThatShouldntExist;
|
||||||
if (prevProps.isFiltered !== this.props.isFiltered) {
|
|
||||||
if (this.props.isFiltered) {
|
|
||||||
this.setState({isExpanded: true});
|
|
||||||
} else {
|
|
||||||
this.setState({isExpanded: !this.layout.isCollapsed});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// as the rooms can come in one by one we need to reevaluate
|
// as the rooms can come in one by one we need to reevaluate
|
||||||
// the amount of available rooms to cap the amount of requested visible rooms by the layout
|
// the amount of available rooms to cap the amount of requested visible rooms by the layout
|
||||||
if (RoomSublist.calcNumTiles(prevState.rooms, prevProps.extraBadTilesThatShouldntExist) !== this.numTiles) {
|
if (RoomSublist.calcNumTiles(prevState.rooms, prevExtraTiles) !== this.numTiles) {
|
||||||
this.setState({height: this.calculateInitialHeight()});
|
this.setState({height: this.calculateInitialHeight()});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -191,14 +196,14 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
// If we're supposed to handle extra tiles, take the performance hit and re-render all the
|
// If we're supposed to handle extra tiles, take the performance hit and re-render all the
|
||||||
// time so we don't have to consider them as part of the visible room optimization.
|
// time so we don't have to consider them as part of the visible room optimization.
|
||||||
const prevExtraTiles = this.props.extraBadTilesThatShouldntExist || [];
|
const prevExtraTiles = this.props.extraBadTilesThatShouldntExist || [];
|
||||||
const nextExtraTiles = nextProps.extraBadTilesThatShouldntExist || [];
|
const nextExtraTiles = (nextState.filteredExtraTiles || nextProps.extraBadTilesThatShouldntExist) || [];
|
||||||
if (prevExtraTiles.length > 0 || nextExtraTiles.length > 0) {
|
if (prevExtraTiles.length > 0 || nextExtraTiles.length > 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're about to update the height of the list, we don't really care about which rooms
|
// If we're about to update the height of the list, we don't really care about which rooms
|
||||||
// are visible or not for no-op purposes, so ensure that the height calculation runs through.
|
// are visible or not for no-op purposes, so ensure that the height calculation runs through.
|
||||||
if (RoomSublist.calcNumTiles(nextState.rooms, nextProps.extraBadTilesThatShouldntExist) !== this.numTiles) {
|
if (RoomSublist.calcNumTiles(nextState.rooms, nextExtraTiles) !== this.numTiles) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,16 +237,41 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
this.state.notificationState.destroy();
|
|
||||||
defaultDispatcher.unregister(this.dispatcherRef);
|
defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onListsUpdated = () => {
|
private onListsUpdated = () => {
|
||||||
|
const stateUpdates: IState & any = {}; // &any is to avoid a cast on the initializer
|
||||||
|
|
||||||
|
if (this.props.extraBadTilesThatShouldntExist) {
|
||||||
|
const nameCondition = RoomListStore.instance.getFirstNameFilterCondition();
|
||||||
|
if (nameCondition) {
|
||||||
|
stateUpdates.filteredExtraTiles = this.props.extraBadTilesThatShouldntExist
|
||||||
|
.filter(t => nameCondition.matches(t.props.displayName || ""));
|
||||||
|
} else if (this.state.filteredExtraTiles) {
|
||||||
|
stateUpdates.filteredExtraTiles = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const currentRooms = this.state.rooms;
|
const currentRooms = this.state.rooms;
|
||||||
const newRooms = RoomListStore.instance.orderedLists[this.props.tagId] || [];
|
const newRooms = RoomListStore.instance.orderedLists[this.props.tagId] || [];
|
||||||
if (arrayHasOrderChange(currentRooms, newRooms)) {
|
if (arrayHasOrderChange(currentRooms, newRooms)) {
|
||||||
this.setState({rooms: newRooms});
|
stateUpdates.rooms = newRooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isStillBeingFiltered = !!RoomListStore.instance.getFirstNameFilterCondition();
|
||||||
|
if (isStillBeingFiltered !== this.isBeingFiltered) {
|
||||||
|
this.isBeingFiltered = isStillBeingFiltered;
|
||||||
|
if (isStillBeingFiltered) {
|
||||||
|
stateUpdates.isExpanded = true;
|
||||||
|
} else {
|
||||||
|
stateUpdates.isExpanded = !this.layout.isCollapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(stateUpdates).length > 0) {
|
||||||
|
this.setState(stateUpdates);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -376,8 +406,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
} else {
|
} else {
|
||||||
// find the first room with a count of the same colour as the badge count
|
// find the first room with a count of the same colour as the badge count
|
||||||
room = this.state.rooms.find((r: Room) => {
|
room = this.state.rooms.find((r: Room) => {
|
||||||
const notifState = this.state.notificationState.getForRoom(r);
|
const notifState = this.notificationState.getForRoom(r);
|
||||||
return notifState.count > 0 && notifState.color === this.state.notificationState.color;
|
return notifState.count > 0 && notifState.color === this.notificationState.color;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -484,8 +514,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.extraBadTilesThatShouldntExist) {
|
if (this.extraTiles) {
|
||||||
tiles.push(...this.props.extraBadTilesThatShouldntExist);
|
// HACK: We break typing here, but this 'extra tiles' property shouldn't exist.
|
||||||
|
(tiles as any[]).push(...this.extraTiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We only have to do this because of the extra tiles. We do it conditionally
|
// We only have to do this because of the extra tiles. We do it conditionally
|
||||||
|
@ -592,7 +623,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
const badge = (
|
const badge = (
|
||||||
<NotificationBadge
|
<NotificationBadge
|
||||||
forceCount={true}
|
forceCount={true}
|
||||||
notification={this.state.notificationState}
|
notification={this.notificationState}
|
||||||
onClick={this.onBadgeClick}
|
onClick={this.onBadgeClick}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
|
|
|
@ -148,7 +148,6 @@ export default class SettingsStore {
|
||||||
callbackFn(originalSettingName, changedInRoomId, atLevel, newValAtLevel, newValue);
|
callbackFn(originalSettingName, changedInRoomId, atLevel, newValAtLevel, newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`Starting watcher for ${settingName}@${roomId || '<null room>'} as ID ${watcherId}`);
|
|
||||||
SettingsStore._watchers[watcherId] = localizedCallback;
|
SettingsStore._watchers[watcherId] = localizedCallback;
|
||||||
defaultWatchManager.watchSetting(settingName, roomId, localizedCallback);
|
defaultWatchManager.watchSetting(settingName, roomId, localizedCallback);
|
||||||
|
|
||||||
|
@ -167,7 +166,6 @@ export default class SettingsStore {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Ending watcher ID ${watcherReference}`);
|
|
||||||
defaultWatchManager.unwatchSetting(SettingsStore._watchers[watcherReference]);
|
defaultWatchManager.unwatchSetting(SettingsStore._watchers[watcherReference]);
|
||||||
delete SettingsStore._watchers[watcherReference];
|
delete SettingsStore._watchers[watcherReference];
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import SettingsStore from "../settings/SettingsStore";
|
||||||
import RoomListStore, {LISTS_UPDATE_EVENT} from "./room-list/RoomListStore";
|
import RoomListStore, {LISTS_UPDATE_EVENT} from "./room-list/RoomListStore";
|
||||||
import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore";
|
import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore";
|
||||||
import {isCustomTag} from "./room-list/models";
|
import {isCustomTag} from "./room-list/models";
|
||||||
|
import {objectHasDiff} from "../utils/objects";
|
||||||
|
|
||||||
function commonPrefix(a, b) {
|
function commonPrefix(a, b) {
|
||||||
const len = Math.min(a.length, b.length);
|
const len = Math.min(a.length, b.length);
|
||||||
|
@ -107,7 +108,10 @@ class CustomRoomTagStore extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onListsUpdated = () => {
|
_onListsUpdated = () => {
|
||||||
this._setState({tags: this._getUpdatedTags()});
|
const newTags = this._getUpdatedTags();
|
||||||
|
if (!this._state.tags || objectHasDiff(this._state.tags, newTags)) {
|
||||||
|
this._setState({tags: newTags});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDispatch(payload) {
|
_onDispatch(payload) {
|
||||||
|
@ -134,7 +138,7 @@ class CustomRoomTagStore extends EventEmitter {
|
||||||
|
|
||||||
_getUpdatedTags() {
|
_getUpdatedTags() {
|
||||||
if (!SettingsStore.isFeatureEnabled("feature_custom_tags")) {
|
if (!SettingsStore.isFeatureEnabled("feature_custom_tags")) {
|
||||||
return;
|
return {}; // none
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTagNames = Object.keys(RoomListStore.instance.orderedLists).filter(t => isCustomTag(t)).sort();
|
const newTagNames = Object.keys(RoomListStore.instance.orderedLists).filter(t => isCustomTag(t)).sort();
|
||||||
|
|
|
@ -29,6 +29,7 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
||||||
private static internalInstance = new RoomNotificationStateStore();
|
private static internalInstance = new RoomNotificationStateStore();
|
||||||
|
|
||||||
private roomMap = new Map<Room, RoomNotificationState>();
|
private roomMap = new Map<Room, RoomNotificationState>();
|
||||||
|
private listMap = new Map<TagID, ListNotificationState>();
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
super(defaultDispatcher, {});
|
super(defaultDispatcher, {});
|
||||||
|
@ -52,21 +53,23 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new list notification state. The consumer is expected to set the rooms
|
* Gets an instance of the list state class for the given tag.
|
||||||
* on the notification state, and destroy the state when it no longer needs it.
|
* @param tagId The tag to get the notification state for.
|
||||||
* @param tagId The tag to create the notification state for.
|
|
||||||
* @returns The notification state for the tag.
|
* @returns The notification state for the tag.
|
||||||
*/
|
*/
|
||||||
public getListState(tagId: TagID): ListNotificationState {
|
public getListState(tagId: TagID): ListNotificationState {
|
||||||
// Note: we don't cache these notification states as the consumer is expected to call
|
if (this.listMap.has(tagId)) {
|
||||||
// .setRooms() on the returned object, which could confuse other consumers.
|
return this.listMap.get(tagId);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Update if/when invites move out of the room list.
|
// TODO: Update if/when invites move out of the room list.
|
||||||
const useTileCount = tagId === DefaultTagID.Invite;
|
const useTileCount = tagId === DefaultTagID.Invite;
|
||||||
const getRoomFn: FetchRoomFn = (room: Room) => {
|
const getRoomFn: FetchRoomFn = (room: Room) => {
|
||||||
return this.getRoomState(room);
|
return this.getRoomState(room);
|
||||||
};
|
};
|
||||||
return new ListNotificationState(useTileCount, tagId, getRoomFn);
|
const state = new ListNotificationState(useTileCount, tagId, getRoomFn);
|
||||||
|
this.listMap.set(tagId, state);
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -32,6 +32,8 @@ import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||||
import RoomListLayoutStore from "./RoomListLayoutStore";
|
import RoomListLayoutStore from "./RoomListLayoutStore";
|
||||||
import { MarkedExecution } from "../../utils/MarkedExecution";
|
import { MarkedExecution } from "../../utils/MarkedExecution";
|
||||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||||
|
import { NameFilterCondition } from "./filters/NameFilterCondition";
|
||||||
|
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
tagsEnabled?: boolean;
|
tagsEnabled?: boolean;
|
||||||
|
@ -54,7 +56,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
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 updateFn = new MarkedExecution(() => this.emit(LISTS_UPDATE_EVENT));
|
private updateFn = new MarkedExecution(() => {
|
||||||
|
for (const tagId of Object.keys(this.unfilteredLists)) {
|
||||||
|
RoomNotificationStateStore.instance.getListState(tagId).setRooms(this.unfilteredLists[tagId]);
|
||||||
|
}
|
||||||
|
this.emit(LISTS_UPDATE_EVENT);
|
||||||
|
});
|
||||||
|
|
||||||
private readonly watchedSettings = [
|
private readonly watchedSettings = [
|
||||||
'feature_custom_tags',
|
'feature_custom_tags',
|
||||||
|
@ -71,6 +78,11 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
this.algorithm.on(FILTER_CHANGED, this.onAlgorithmFilterUpdated);
|
this.algorithm.on(FILTER_CHANGED, this.onAlgorithmFilterUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get unfilteredLists(): ITagMap {
|
||||||
|
if (!this.algorithm) return {}; // No tags yet.
|
||||||
|
return this.algorithm.getUnfilteredRooms();
|
||||||
|
}
|
||||||
|
|
||||||
public get orderedLists(): ITagMap {
|
public get orderedLists(): ITagMap {
|
||||||
if (!this.algorithm) return {}; // No tags yet.
|
if (!this.algorithm) return {}; // No tags yet.
|
||||||
return this.algorithm.getOrderedRooms();
|
return this.algorithm.getOrderedRooms();
|
||||||
|
@ -588,6 +600,20 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
this.updateFn.trigger();
|
this.updateFn.trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the first (and ideally only) name filter condition. If one isn't present,
|
||||||
|
* this returns null.
|
||||||
|
* @returns The first name filter condition, or null if none.
|
||||||
|
*/
|
||||||
|
public getFirstNameFilterCondition(): NameFilterCondition | null {
|
||||||
|
for (const filter of this.filterConditions) {
|
||||||
|
if (filter instanceof NameFilterCondition) {
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the tags for a room identified by the store. The returned set
|
* Gets the tags for a room identified by the store. The returned set
|
||||||
* should never be empty, and will contain DefaultTagID.Untagged if
|
* should never be empty, and will contain DefaultTagID.Untagged if
|
||||||
|
|
|
@ -465,6 +465,10 @@ export class Algorithm extends EventEmitter {
|
||||||
return this.filteredRooms;
|
return this.filteredRooms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getUnfilteredRooms(): ITagMap {
|
||||||
|
return this._cachedStickyRooms || this.cachedRooms;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This returns the same as getOrderedRooms(), but without the sticky room
|
* This returns the same as getOrderedRooms(), but without the sticky room
|
||||||
* map as it causes issues for sticky room handling (see sticky room handling
|
* map as it causes issues for sticky room handling (see sticky room handling
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition";
|
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { removeHiddenChars } from "matrix-js-sdk/src/utils";
|
import { removeHiddenChars } from "matrix-js-sdk/src/utils";
|
||||||
|
import { throttle } from "lodash";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A filter condition for the room list which reveals rooms of a particular
|
* A filter condition for the room list which reveals rooms of a particular
|
||||||
|
@ -41,9 +42,13 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
|
||||||
|
|
||||||
public set search(val: string) {
|
public set search(val: string) {
|
||||||
this._search = val;
|
this._search = val;
|
||||||
this.emit(FILTER_CHANGED);
|
this.callUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private callUpdate = throttle(() => {
|
||||||
|
this.emit(FILTER_CHANGED);
|
||||||
|
}, 200, {trailing: true, leading: true});
|
||||||
|
|
||||||
public isVisible(room: Room): boolean {
|
public isVisible(room: Room): boolean {
|
||||||
const lcFilter = this.search.toLowerCase();
|
const lcFilter = this.search.toLowerCase();
|
||||||
if (this.search[0] === '#') {
|
if (this.search[0] === '#') {
|
||||||
|
|
|
@ -53,6 +53,9 @@ export function arrayHasDiff(a: any[], b: any[]): boolean {
|
||||||
// an element from the other.
|
// an element from the other.
|
||||||
if (b.some(i => !a.includes(i))) return true;
|
if (b.some(i => !a.includes(i))) return true;
|
||||||
if (a.some(i => !b.includes(i))) return true;
|
if (a.some(i => !b.includes(i))) return true;
|
||||||
|
|
||||||
|
// if all the keys are common, say so
|
||||||
|
return false;
|
||||||
} else {
|
} else {
|
||||||
return true; // different lengths means they are naturally diverged
|
return true; // different lengths means they are naturally diverged
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,6 +72,23 @@ export function objectHasValueChange(a: any, b: any): boolean {
|
||||||
return arrayHasDiff(aValues, bValues);
|
return arrayHasDiff(aValues, bValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if any keys were added, removed, or changed between two objects.
|
||||||
|
* For changes, simple triple equal comparisons are done, not in-depth
|
||||||
|
* tree checking.
|
||||||
|
* @param a The first object. Must be defined.
|
||||||
|
* @param b The second object. Must be defined.
|
||||||
|
* @returns True if there's a difference between the objects, false otherwise
|
||||||
|
*/
|
||||||
|
export function objectHasDiff(a: any, b: any): boolean {
|
||||||
|
const aKeys = Object.keys(a);
|
||||||
|
const bKeys = Object.keys(b);
|
||||||
|
if (arrayHasDiff(aKeys, bKeys)) return true;
|
||||||
|
|
||||||
|
const possibleChanges = arrayUnion(aKeys, bKeys);
|
||||||
|
return possibleChanges.some(k => a[k] !== b[k]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines the keys added, changed, and removed between two objects.
|
* Determines the keys added, changed, and removed between two objects.
|
||||||
* For changes, simple triple equal comparisons are done, not in-depth
|
* For changes, simple triple equal comparisons are done, not in-depth
|
||||||
|
|
Loading…
Reference in a new issue