mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 05:04:57 +08:00
Merge pull request #5054 from matrix-org/travis/perf6
Minor improvements to filtering performance
This commit is contained in:
commit
3561de3f8a
@ -44,7 +44,6 @@ interface IProps {
|
||||
}
|
||||
|
||||
interface IState {
|
||||
searchFilter: string;
|
||||
showBreadcrumbs: boolean;
|
||||
showTagPanel: boolean;
|
||||
}
|
||||
@ -69,7 +68,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
searchFilter: "",
|
||||
showBreadcrumbs: BreadcrumbsStore.instance.visible,
|
||||
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);
|
||||
}
|
||||
|
||||
private onSearch = (term: string): void => {
|
||||
this.setState({searchFilter: term});
|
||||
};
|
||||
|
||||
private onExplore = () => {
|
||||
dis.fire(Action.ViewRoomDirectory);
|
||||
};
|
||||
@ -366,7 +360,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
<RoomSearch
|
||||
onQueryUpdate={this.onSearch}
|
||||
isMinimized={this.props.isMinimized}
|
||||
onVerticalArrow={this.onKeyDown}
|
||||
onEnter={this.onEnter}
|
||||
@ -392,7 +385,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||
onKeyDown={this.onKeyDown}
|
||||
resizeNotifier={null}
|
||||
collapsed={false}
|
||||
searchFilter={this.state.searchFilter}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
isMinimized={this.props.isMinimized}
|
||||
|
@ -24,9 +24,10 @@ import { throttle } from 'lodash';
|
||||
import { Key } from "../../Keyboard";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
|
||||
|
||||
interface IProps {
|
||||
onQueryUpdate: (newQuery: string) => void;
|
||||
isMinimized: boolean;
|
||||
onVerticalArrow(ev: React.KeyboardEvent): void;
|
||||
onEnter(ev: React.KeyboardEvent): boolean;
|
||||
@ -40,6 +41,7 @@ interface IState {
|
||||
export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private inputRef: React.RefObject<HTMLInputElement> = createRef();
|
||||
private searchFilter: NameFilterCondition = new NameFilterCondition();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
@ -52,6 +54,21 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||
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() {
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
}
|
||||
@ -78,19 +95,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||
private onChange = () => {
|
||||
if (!this.inputRef.current) return;
|
||||
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>) => {
|
||||
this.setState({focused: true});
|
||||
ev.target.select();
|
||||
|
@ -31,7 +31,6 @@ import dis from "../../../dispatcher/dispatcher";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import RoomSublist from "./RoomSublist";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import GroupAvatar from "../avatars/GroupAvatar";
|
||||
import TemporaryTile from "./TemporaryTile";
|
||||
@ -52,7 +51,6 @@ interface IProps {
|
||||
onResize: () => void;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
collapsed: boolean;
|
||||
searchFilter: string;
|
||||
isMinimized: boolean;
|
||||
}
|
||||
|
||||
@ -150,8 +148,7 @@ function customTagAesthetics(tagId: TagID): ITagAesthetics {
|
||||
};
|
||||
}
|
||||
|
||||
export default class RoomList extends React.Component<IProps, IState> {
|
||||
private searchFilter: NameFilterCondition = new NameFilterCondition();
|
||||
export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
private dispatcherRef;
|
||||
private customTagStoreRef;
|
||||
|
||||
@ -165,21 +162,6 @@ export default class RoomList extends React.Component<IProps, IState> {
|
||||
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 {
|
||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, 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)
|
||||
// See https://github.com/vector-im/riot-web/issues/14456
|
||||
return MatrixClientPeg.get().getGroups().filter(g => {
|
||||
if (g.myMembership !== 'invite') return false;
|
||||
return !this.searchFilter || this.searchFilter.matches(g.name || "");
|
||||
return g.myMembership === 'invite';
|
||||
}).map(g => {
|
||||
const avatar = (
|
||||
<GroupAvatar
|
||||
@ -340,7 +321,6 @@ export default class RoomList extends React.Component<IProps, IState> {
|
||||
isMinimized={this.props.isMinimized}
|
||||
onResize={this.props.onResize}
|
||||
extraBadTilesThatShouldntExist={extraTiles}
|
||||
isFiltered={!!this.searchFilter.search}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -38,7 +38,6 @@ import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
@ -49,6 +48,8 @@ import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNo
|
||||
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
|
||||
import { arrayHasOrderChange } from "../../../utils/arrays";
|
||||
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 RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
|
||||
@ -68,11 +69,10 @@ interface IProps {
|
||||
isMinimized: boolean;
|
||||
tagId: TagID;
|
||||
onResize: () => void;
|
||||
isFiltered: boolean;
|
||||
|
||||
// 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.
|
||||
extraBadTilesThatShouldntExist?: React.ReactElement[];
|
||||
extraBadTilesThatShouldntExist?: TemporaryTile[];
|
||||
|
||||
// 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">;
|
||||
|
||||
interface IState {
|
||||
notificationState: ListNotificationState;
|
||||
contextMenuPosition: PartialDOMRect;
|
||||
isResizing: boolean;
|
||||
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
|
||||
height: number;
|
||||
rooms: Room[];
|
||||
filteredExtraTiles?: TemporaryTile[];
|
||||
}
|
||||
|
||||
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 layout: ListLayout;
|
||||
private heightAtStart: number;
|
||||
private isBeingFiltered: boolean;
|
||||
private notificationState: ListNotificationState;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId);
|
||||
this.heightAtStart = 0;
|
||||
this.isBeingFiltered = !!RoomListStore.instance.getFirstNameFilterCondition();
|
||||
this.notificationState = RoomNotificationStateStore.instance.getListState(this.props.tagId);
|
||||
this.state = {
|
||||
notificationState: RoomNotificationStateStore.instance.getListState(this.props.tagId),
|
||||
contextMenuPosition: null,
|
||||
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.
|
||||
rooms: RoomListStore.instance.orderedLists[this.props.tagId] || [],
|
||||
};
|
||||
// Why Object.assign() and not this.state.height? Because TypeScript says no.
|
||||
this.state = Object.assign(this.state, {height: this.calculateInitialHeight()});
|
||||
this.state.notificationState.setRooms(this.state.rooms);
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||
}
|
||||
@ -146,8 +148,18 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
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 {
|
||||
return RoomSublist.calcNumTiles(this.state.rooms, this.props.extraBadTilesThatShouldntExist);
|
||||
return RoomSublist.calcNumTiles(this.state.rooms, this.extraTiles);
|
||||
}
|
||||
|
||||
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>) {
|
||||
this.state.notificationState.setRooms(this.state.rooms);
|
||||
if (prevProps.isFiltered !== this.props.isFiltered) {
|
||||
if (this.props.isFiltered) {
|
||||
this.setState({isExpanded: true});
|
||||
} else {
|
||||
this.setState({isExpanded: !this.layout.isCollapsed});
|
||||
}
|
||||
}
|
||||
const prevExtraTiles = prevState.filteredExtraTiles || prevProps.extraBadTilesThatShouldntExist;
|
||||
// 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
|
||||
if (RoomSublist.calcNumTiles(prevState.rooms, prevProps.extraBadTilesThatShouldntExist) !== this.numTiles) {
|
||||
if (RoomSublist.calcNumTiles(prevState.rooms, prevExtraTiles) !== this.numTiles) {
|
||||
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
|
||||
// time so we don't have to consider them as part of the visible room optimization.
|
||||
const prevExtraTiles = this.props.extraBadTilesThatShouldntExist || [];
|
||||
const nextExtraTiles = nextProps.extraBadTilesThatShouldntExist || [];
|
||||
const nextExtraTiles = (nextState.filteredExtraTiles || nextProps.extraBadTilesThatShouldntExist) || [];
|
||||
if (prevExtraTiles.length > 0 || nextExtraTiles.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (RoomSublist.calcNumTiles(nextState.rooms, nextProps.extraBadTilesThatShouldntExist) !== this.numTiles) {
|
||||
if (RoomSublist.calcNumTiles(nextState.rooms, nextExtraTiles) !== this.numTiles) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -232,16 +237,41 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.state.notificationState.destroy();
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.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 newRooms = RoomListStore.instance.orderedLists[this.props.tagId] || [];
|
||||
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 {
|
||||
// find the first room with a count of the same colour as the badge count
|
||||
room = this.state.rooms.find((r: Room) => {
|
||||
const notifState = this.state.notificationState.getForRoom(r);
|
||||
return notifState.count > 0 && notifState.color === this.state.notificationState.color;
|
||||
const notifState = this.notificationState.getForRoom(r);
|
||||
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) {
|
||||
tiles.push(...this.props.extraBadTilesThatShouldntExist);
|
||||
if (this.extraTiles) {
|
||||
// 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
|
||||
@ -592,7 +623,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
const badge = (
|
||||
<NotificationBadge
|
||||
forceCount={true}
|
||||
notification={this.state.notificationState}
|
||||
notification={this.notificationState}
|
||||
onClick={this.onBadgeClick}
|
||||
tabIndex={tabIndex}
|
||||
aria-label={ariaLabel}
|
||||
|
@ -148,7 +148,6 @@ export default class SettingsStore {
|
||||
callbackFn(originalSettingName, changedInRoomId, atLevel, newValAtLevel, newValue);
|
||||
};
|
||||
|
||||
console.log(`Starting watcher for ${settingName}@${roomId || '<null room>'} as ID ${watcherId}`);
|
||||
SettingsStore._watchers[watcherId] = localizedCallback;
|
||||
defaultWatchManager.watchSetting(settingName, roomId, localizedCallback);
|
||||
|
||||
@ -167,7 +166,6 @@ export default class SettingsStore {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Ending watcher ID ${watcherReference}`);
|
||||
defaultWatchManager.unwatchSetting(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 {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore";
|
||||
import {isCustomTag} from "./room-list/models";
|
||||
import {objectHasDiff} from "../utils/objects";
|
||||
|
||||
function commonPrefix(a, b) {
|
||||
const len = Math.min(a.length, b.length);
|
||||
@ -107,7 +108,10 @@ class CustomRoomTagStore extends EventEmitter {
|
||||
}
|
||||
|
||||
_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) {
|
||||
@ -134,7 +138,7 @@ class CustomRoomTagStore extends EventEmitter {
|
||||
|
||||
_getUpdatedTags() {
|
||||
if (!SettingsStore.isFeatureEnabled("feature_custom_tags")) {
|
||||
return;
|
||||
return {}; // none
|
||||
}
|
||||
|
||||
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 roomMap = new Map<Room, RoomNotificationState>();
|
||||
private listMap = new Map<TagID, ListNotificationState>();
|
||||
|
||||
private constructor() {
|
||||
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
|
||||
* on the notification state, and destroy the state when it no longer needs it.
|
||||
* @param tagId The tag to create the notification state for.
|
||||
* Gets an instance of the list state class for the given tag.
|
||||
* @param tagId The tag to get the notification state for.
|
||||
* @returns The notification state for the tag.
|
||||
*/
|
||||
public getListState(tagId: TagID): ListNotificationState {
|
||||
// Note: we don't cache these notification states as the consumer is expected to call
|
||||
// .setRooms() on the returned object, which could confuse other consumers.
|
||||
if (this.listMap.has(tagId)) {
|
||||
return this.listMap.get(tagId);
|
||||
}
|
||||
|
||||
// TODO: Update if/when invites move out of the room list.
|
||||
const useTileCount = tagId === DefaultTagID.Invite;
|
||||
const getRoomFn: FetchRoomFn = (room: 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 { MarkedExecution } from "../../utils/MarkedExecution";
|
||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||
import { NameFilterCondition } from "./filters/NameFilterCondition";
|
||||
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
|
||||
|
||||
interface IState {
|
||||
tagsEnabled?: boolean;
|
||||
@ -54,7 +56,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||
private algorithm = new Algorithm();
|
||||
private filterConditions: IFilterCondition[] = [];
|
||||
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 = [
|
||||
'feature_custom_tags',
|
||||
@ -71,6 +78,11 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||
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 {
|
||||
if (!this.algorithm) return {}; // No tags yet.
|
||||
return this.algorithm.getOrderedRooms();
|
||||
@ -588,6 +600,20 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||
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
|
||||
* should never be empty, and will contain DefaultTagID.Untagged if
|
||||
|
@ -465,6 +465,10 @@ export class Algorithm extends EventEmitter {
|
||||
return this.filteredRooms;
|
||||
}
|
||||
|
||||
public getUnfilteredRooms(): ITagMap {
|
||||
return this._cachedStickyRooms || this.cachedRooms;
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns the same as getOrderedRooms(), but without the sticky room
|
||||
* 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 { EventEmitter } from "events";
|
||||
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
|
||||
@ -41,9 +42,13 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
|
||||
|
||||
public set search(val: string) {
|
||||
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 {
|
||||
const lcFilter = this.search.toLowerCase();
|
||||
if (this.search[0] === '#') {
|
||||
|
@ -53,6 +53,9 @@ export function arrayHasDiff(a: any[], b: any[]): boolean {
|
||||
// an element from the other.
|
||||
if (b.some(i => !a.includes(i))) return true;
|
||||
if (a.some(i => !b.includes(i))) return true;
|
||||
|
||||
// if all the keys are common, say so
|
||||
return false;
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* For changes, simple triple equal comparisons are done, not in-depth
|
||||
|
Loading…
Reference in New Issue
Block a user