diff --git a/package.json b/package.json
index dda4a5a897..22ff071ba7 100644
--- a/package.json
+++ b/package.json
@@ -117,6 +117,7 @@
"@babel/register": "^7.7.4",
"@peculiar/webcrypto": "^1.0.22",
"@types/classnames": "^2.2.10",
+ "@types/flux": "^3.1.9",
"@types/modernizr": "^3.5.3",
"@types/qrcode": "^1.3.4",
"@types/react": "16.9",
diff --git a/src/actions/RoomListActions.js b/src/actions/RoomListActions.js
index 10a3848dda..072b1d9a86 100644
--- a/src/actions/RoomListActions.js
+++ b/src/actions/RoomListActions.js
@@ -15,11 +15,12 @@ limitations under the License.
*/
import { asyncAction } from './actionCreators';
-import RoomListStore, {TAG_DM} from '../stores/RoomListStore';
import Modal from '../Modal';
import * as Rooms from '../Rooms';
import { _t } from '../languageHandler';
import * as sdk from '../index';
+import {RoomListStoreTempProxy} from "../stores/room-list/RoomListStoreTempProxy";
+import {DefaultTagID} from "../stores/room-list/models";
const RoomListActions = {};
@@ -44,7 +45,7 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex,
// Is the tag ordered manually?
if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
- const lists = RoomListStore.getRoomLists();
+ const lists = RoomListStoreTempProxy.getRoomLists();
const newList = [...lists[newTag]];
newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order);
@@ -73,11 +74,11 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex,
const roomId = room.roomId;
// Evil hack to get DMs behaving
- if ((oldTag === undefined && newTag === TAG_DM) ||
- (oldTag === TAG_DM && newTag === undefined)
+ if ((oldTag === undefined && newTag === DefaultTagID.DM) ||
+ (oldTag === DefaultTagID.DM && newTag === undefined)
) {
return Rooms.guessAndSetDMRoom(
- room, newTag === TAG_DM,
+ room, newTag === DefaultTagID.DM,
).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to set direct chat tag " + err);
@@ -91,10 +92,10 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex,
const hasChangedSubLists = oldTag !== newTag;
// More evilness: We will still be dealing with moving to favourites/low prio,
- // but we avoid ever doing a request with TAG_DM.
+ // but we avoid ever doing a request with DefaultTagID.DM.
//
// if we moved lists, remove the old tag
- if (oldTag && oldTag !== TAG_DM &&
+ if (oldTag && oldTag !== DefaultTagID.DM &&
hasChangedSubLists
) {
const promiseToDelete = matrixClient.deleteRoomTag(
@@ -112,7 +113,7 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex,
}
// if we moved lists or the ordering changed, add the new tag
- if (newTag && newTag !== TAG_DM &&
+ if (newTag && newTag !== DefaultTagID.DM &&
(hasChangedSubLists || metaData)
) {
// metaData is the body of the PUT to set the tag, so it must
diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js
index a9cd12199b..1993e2f419 100644
--- a/src/components/structures/LeftPanel.js
+++ b/src/components/structures/LeftPanel.js
@@ -26,6 +26,7 @@ import * as VectorConferenceHandler from '../../VectorConferenceHandler';
import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler";
import Analytics from "../../Analytics";
+import RoomList2 from "../views/rooms/RoomList2";
const LeftPanel = createReactClass({
@@ -273,6 +274,29 @@ const LeftPanel = createReactClass({
breadcrumbs = ();
}
+ let roomList = null;
+ if (SettingsStore.isFeatureEnabled("feature_new_room_list")) {
+ roomList = ;
+ } else {
+ roomList = ;
+ }
+
return (
{ tagPanelContainer }
@@ -284,15 +308,7 @@ const LeftPanel = createReactClass({
{ exploreButton }
{ searchBox }
-
+ {roomList}
);
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index 9de2aac8e9..0950e52bba 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -31,7 +31,6 @@ import dis from '../../dispatcher';
import sessionStore from '../../stores/SessionStore';
import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore";
-import RoomListStore from "../../stores/RoomListStore";
import TagOrderActions from '../../actions/TagOrderActions';
import RoomListActions from '../../actions/RoomListActions';
@@ -42,6 +41,8 @@ import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
import HomePage from "./HomePage";
import ResizeNotifier from "../../utils/ResizeNotifier";
import PlatformPeg from "../../PlatformPeg";
+import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy";
+import { DefaultTagID } from "../../stores/room-list/models";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
// NB. this is just for server notices rather than pinned messages in general.
@@ -297,18 +298,18 @@ class LoggedInView extends React.PureComponent {
};
onRoomStateEvents = (ev, state) => {
- const roomLists = RoomListStore.getRoomLists();
- if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) {
+ const roomLists = RoomListStoreTempProxy.getRoomLists();
+ if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) {
this._updateServerNoticeEvents();
}
};
_updateServerNoticeEvents = async () => {
- const roomLists = RoomListStore.getRoomLists();
- if (!roomLists['m.server_notice']) return [];
+ const roomLists = RoomListStoreTempProxy.getRoomLists();
+ if (!roomLists[DefaultTagID.ServerNotice]) return [];
const pinnedEvents = [];
- for (const room of roomLists['m.server_notice']) {
+ for (const room of roomLists[DefaultTagID.ServerNotice]) {
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js
index 7cbbf8ba64..e719c45f49 100644
--- a/src/components/views/dialogs/InviteDialog.js
+++ b/src/components/views/dialogs/InviteDialog.js
@@ -34,8 +34,9 @@ import {humanizeTime} from "../../../utils/humanize";
import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
import {inviteMultipleToRoom} from "../../../RoomInvite";
import SettingsStore from '../../../settings/SettingsStore';
-import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore";
import {Key} from "../../../Keyboard";
+import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
+import {DefaultTagID} from "../../../stores/room-list/models";
export const KIND_DM = "dm";
export const KIND_INVITE = "invite";
@@ -343,10 +344,10 @@ export default class InviteDialog extends React.PureComponent {
_buildRecents(excludedTargetIds: Set): {userId: string, user: RoomMember, lastActive: number} {
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
- // Also pull in all the rooms tagged as TAG_DM so we don't miss anything. Sometimes the
+ // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
// room list doesn't tag the room for the DMRoomMap, but does for the room list.
- const taggedRooms = RoomListStore.getRoomLists();
- const dmTaggedRooms = taggedRooms[TAG_DM];
+ const taggedRooms = RoomListStoreTempProxy.getRoomLists();
+ const dmTaggedRooms = taggedRooms[DefaultTagID.DM];
const myUserId = MatrixClientPeg.get().getUserId();
for (const dmRoom of dmTaggedRooms) {
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index 289a89a206..dc9c9238cd 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -29,7 +29,6 @@ import rate_limited_func from "../../../ratelimitedfunc";
import * as Rooms from '../../../Rooms';
import DMRoomMap from '../../../utils/DMRoomMap';
import TagOrderStore from '../../../stores/TagOrderStore';
-import RoomListStore, {TAG_DM} from '../../../stores/RoomListStore';
import CustomRoomTagStore from '../../../stores/CustomRoomTagStore';
import GroupStore from '../../../stores/GroupStore';
import RoomSubList from '../../structures/RoomSubList';
@@ -41,6 +40,8 @@ import * as Receipt from "../../../utils/Receipt";
import {Resizer} from '../../../resizer';
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex";
+import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
+import {DefaultTagID} from "../../../stores/room-list/models";
import * as Unread from "../../../Unread";
import RoomViewStore from "../../../stores/RoomViewStore";
@@ -161,7 +162,7 @@ export default createReactClass({
this.updateVisibleRooms();
});
- this._roomListStoreToken = RoomListStore.addListener(() => {
+ this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => {
this._delayedRefreshRoomList();
});
@@ -521,7 +522,7 @@ export default createReactClass({
},
getTagNameForRoomId: function(roomId) {
- const lists = RoomListStore.getRoomLists();
+ const lists = RoomListStoreTempProxy.getRoomLists();
for (const tagName of Object.keys(lists)) {
for (const room of lists[tagName]) {
// Should be impossible, but guard anyways.
@@ -541,7 +542,7 @@ export default createReactClass({
},
getRoomLists: function() {
- const lists = RoomListStore.getRoomLists();
+ const lists = RoomListStoreTempProxy.getRoomLists();
const filteredLists = {};
@@ -773,10 +774,10 @@ export default createReactClass({
incomingCall: incomingCallIfTaggedAs('m.favourite'),
},
{
- list: this.state.lists[TAG_DM],
+ list: this.state.lists[DefaultTagID.DM],
label: _t('Direct Messages'),
- tagName: TAG_DM,
- incomingCall: incomingCallIfTaggedAs(TAG_DM),
+ tagName: DefaultTagID.DM,
+ incomingCall: incomingCallIfTaggedAs(DefaultTagID.DM),
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'});},
addRoomLabel: _t("Start chat"),
},
diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx
new file mode 100644
index 0000000000..1790fa8cf6
--- /dev/null
+++ b/src/components/views/rooms/RoomList2.tsx
@@ -0,0 +1,130 @@
+/*
+Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2017, 2018 Vector Creations Ltd
+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 * as React from "react";
+import { _t } from "../../../languageHandler";
+import { Layout } from '../../../resizer/distributors/roomsublist2';
+import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
+import { ResizeNotifier } from "../../../utils/ResizeNotifier";
+import RoomListStore from "../../../stores/room-list/RoomListStore2";
+
+interface IProps {
+ onKeyDown: (ev: React.KeyboardEvent) => void;
+ onFocus: (ev: React.FocusEvent) => void;
+ onBlur: (ev: React.FocusEvent) => void;
+ resizeNotifier: ResizeNotifier;
+ collapsed: boolean;
+ searchFilter: string;
+}
+
+interface IState {
+}
+
+// TODO: Actually write stub
+export class RoomSublist2 extends React.Component {
+ public setHeight(size: number) {
+ }
+}
+
+export default class RoomList2 extends React.Component {
+ private sublistRefs: { [tagId: string]: React.RefObject } = {};
+ private sublistSizes: { [tagId: string]: number } = {};
+ private sublistCollapseStates: { [tagId: string]: boolean } = {};
+ private unfilteredLayout: Layout;
+ private filteredLayout: Layout;
+
+ constructor(props: IProps) {
+ super(props);
+
+ this.loadSublistSizes();
+ this.prepareLayouts();
+ }
+
+ public componentDidMount(): void {
+ RoomListStore.instance.addListener(() => {
+ console.log(RoomListStore.instance.orderedLists);
+ });
+ }
+
+ private loadSublistSizes() {
+ const sizesJson = window.localStorage.getItem("mx_roomlist_sizes");
+ if (sizesJson) this.sublistSizes = JSON.parse(sizesJson);
+
+ const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed");
+ if (collapsedJson) this.sublistCollapseStates = JSON.parse(collapsedJson);
+ }
+
+ private saveSublistSizes() {
+ window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.sublistSizes));
+ window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.sublistCollapseStates));
+ }
+
+ private prepareLayouts() {
+ this.unfilteredLayout = new Layout((tagId: string, height: number) => {
+ const sublist = this.sublistRefs[tagId];
+ if (sublist) sublist.current.setHeight(height);
+
+ // TODO: Check overflow
+
+ // Don't store a height for collapsed sublists
+ if (!this.sublistCollapseStates[tagId]) {
+ this.sublistSizes[tagId] = height;
+ this.saveSublistSizes();
+ }
+ }, this.sublistSizes, this.sublistCollapseStates, {
+ allowWhitespace: false,
+ handleHeight: 1,
+ });
+
+ this.filteredLayout = new Layout((tagId: string, height: number) => {
+ const sublist = this.sublistRefs[tagId];
+ if (sublist) sublist.current.setHeight(height);
+ }, null, null, {
+ allowWhitespace: false,
+ handleHeight: 0,
+ });
+ }
+
+ private collectSublistRef(tagId: string, ref: React.RefObject) {
+ if (!ref) {
+ delete this.sublistRefs[tagId];
+ } else {
+ this.sublistRefs[tagId] = ref;
+ }
+ }
+
+ public render() {
+ return (
+
+ {({onKeyDownHandler}) => (
+ {_t("TODO")}
+ )}
+
+ );
+ }
+}
diff --git a/src/dispatcher-types.ts b/src/dispatcher-types.ts
new file mode 100644
index 0000000000..16fac0c849
--- /dev/null
+++ b/src/dispatcher-types.ts
@@ -0,0 +1,28 @@
+/*
+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 * as flux from "flux";
+import dis from "./dispatcher";
+
+// TODO: Merge this with the dispatcher and centralize types
+
+export interface ActionPayload {
+ [property: string]: any; // effectively "extends Object"
+ action: string;
+}
+
+// For ease of reference in TypeScript classes
+export const defaultDispatcher: flux.Dispatcher = dis;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index f16a0d7755..fd474f378c 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -406,6 +406,7 @@
"Render simple counters in room header": "Render simple counters in room header",
"Multiple integration managers": "Multiple integration managers",
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
+ "Use the improved room list component (refresh to apply changes)": "Use the improved room list component (refresh to apply changes)",
"Support adding custom themes": "Support adding custom themes",
"Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session",
"Show info about bridges in room settings": "Show info about bridges in room settings",
@@ -1116,6 +1117,7 @@
"Low priority": "Low priority",
"Historical": "Historical",
"System Alerts": "System Alerts",
+ "TODO": "TODO",
"This room": "This room",
"Joining room …": "Joining room …",
"Loading …": "Loading …",
diff --git a/src/settings/Settings.js b/src/settings/Settings.js
index 5c6d843349..554cf6b968 100644
--- a/src/settings/Settings.js
+++ b/src/settings/Settings.js
@@ -131,6 +131,12 @@ export const SETTINGS = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
+ "feature_new_room_list": {
+ isFeature: true,
+ displayName: _td("Use the improved room list component (refresh to apply changes)"),
+ supportedLevels: LEVELS_FEATURE,
+ default: false,
+ },
"feature_custom_themes": {
isFeature: true,
displayName: _td("Support adding custom themes"),
diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js
index 909282c085..bf8e970535 100644
--- a/src/stores/CustomRoomTagStore.js
+++ b/src/stores/CustomRoomTagStore.js
@@ -15,10 +15,10 @@ limitations under the License.
*/
import dis from '../dispatcher';
import * as RoomNotifs from '../RoomNotifs';
-import RoomListStore from './RoomListStore';
import EventEmitter from 'events';
import { throttle } from "lodash";
import SettingsStore from "../settings/SettingsStore";
+import {RoomListStoreTempProxy} from "./room-list/RoomListStoreTempProxy";
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
@@ -60,7 +60,7 @@ class CustomRoomTagStore extends EventEmitter {
trailing: true,
},
);
- this._roomListStoreToken = RoomListStore.addListener(() => {
+ this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => {
this._setState({tags: this._getUpdatedTags()});
});
dis.register(payload => this._onDispatch(payload));
@@ -85,7 +85,7 @@ class CustomRoomTagStore extends EventEmitter {
}
getSortedTags() {
- const roomLists = RoomListStore.getRoomLists();
+ const roomLists = RoomListStoreTempProxy.getRoomLists();
const tagNames = Object.keys(this._state.tags).sort();
const prefixes = tagNames.map((name, i) => {
@@ -140,7 +140,7 @@ class CustomRoomTagStore extends EventEmitter {
return;
}
- const newTagNames = Object.keys(RoomListStore.getRoomLists())
+ const newTagNames = Object.keys(RoomListStoreTempProxy.getRoomLists())
.filter((tagName) => {
return !tagName.match(STANDARD_TAGS_REGEX);
}).sort();
diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js
index e217f7ea38..ccccbcc313 100644
--- a/src/stores/RoomListStore.js
+++ b/src/stores/RoomListStore.js
@@ -112,11 +112,19 @@ class RoomListStore extends Store {
constructor() {
super(dis);
+ this._checkDisabled();
this._init();
this._getManualComparator = this._getManualComparator.bind(this);
this._recentsComparator = this._recentsComparator.bind(this);
}
+ _checkDisabled() {
+ this.disabled = SettingsStore.isFeatureEnabled("feature_new_room_list");
+ if (this.disabled) {
+ console.warn("DISABLING LEGACY ROOM LIST STORE");
+ }
+ }
+
/**
* Changes the sorting algorithm used by the RoomListStore.
* @param {string} algorithm The new algorithm to use. Should be one of the ALGO_* constants.
@@ -133,6 +141,8 @@ class RoomListStore extends Store {
}
_init() {
+ if (this.disabled) return;
+
// Initialise state
const defaultLists = {
"m.server_notice": [/* { room: js-sdk room, category: string } */],
@@ -160,6 +170,8 @@ class RoomListStore extends Store {
}
_setState(newState) {
+ if (this.disabled) return;
+
// If we're changing the lists, transparently change the presentation lists (which
// is given to requesting components). This dramatically simplifies our code elsewhere
// while also ensuring we don't need to update all the calling components to support
@@ -176,6 +188,8 @@ class RoomListStore extends Store {
}
__onDispatch(payload) {
+ if (this.disabled) return;
+
const logicallyReady = this._matrixClient && this._state.ready;
switch (payload.action) {
case 'setting_updated': {
@@ -202,6 +216,9 @@ class RoomListStore extends Store {
break;
}
+ this._checkDisabled();
+ if (this.disabled) return;
+
// Always ensure that we set any state needed for settings here. It is possible that
// setting updates trigger on startup before we are ready to sync, so we want to make
// sure that the right state is in place before we actually react to those changes.
diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts
new file mode 100644
index 0000000000..70d6d4a598
--- /dev/null
+++ b/src/stores/room-list/RoomListStore2.ts
@@ -0,0 +1,213 @@
+/*
+Copyright 2018, 2019 New Vector Ltd
+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 {Store} from 'flux/utils';
+import {Room} from "matrix-js-sdk/src/models/room";
+import {MatrixClient} from "matrix-js-sdk/src/client";
+import { ActionPayload, defaultDispatcher } from "../../dispatcher-types";
+import SettingsStore from "../../settings/SettingsStore";
+import { OrderedDefaultTagIDs, DefaultTagID, TagID } from "./models";
+import { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/IAlgorithm";
+import TagOrderStore from "../TagOrderStore";
+import { getAlgorithmInstance } from "./algorithms";
+
+interface IState {
+ tagsEnabled?: boolean;
+
+ preferredSort?: SortAlgorithm;
+ preferredAlgorithm?: ListAlgorithm;
+}
+
+class _RoomListStore extends Store {
+ private state: IState = {};
+ private matrixClient: MatrixClient;
+ private initialListsGenerated = false;
+ private enabled = false;
+ private algorithm: IAlgorithm;
+
+ private readonly watchedSettings = [
+ 'RoomList.orderAlphabetically',
+ 'RoomList.orderByImportance',
+ 'feature_custom_tags',
+ ];
+
+ constructor() {
+ super(defaultDispatcher);
+
+ this.checkEnabled();
+ this.reset();
+ for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null);
+ }
+
+ public get orderedLists(): ITagMap {
+ if (!this.algorithm) return {}; // No tags yet.
+ return this.algorithm.getOrderedRooms();
+ }
+
+ // TODO: Remove enabled flag when the old RoomListStore goes away
+ private checkEnabled() {
+ this.enabled = SettingsStore.isFeatureEnabled("feature_new_room_list");
+ if (this.enabled) {
+ console.log("ENABLING NEW ROOM LIST STORE");
+ }
+ }
+
+ private reset(): void {
+ // We don't call setState() because it'll cause changes to emitted which could
+ // crash the app during logout/signin/etc.
+ this.state = {};
+ }
+
+ private readAndCacheSettingsFromStore() {
+ const tagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags");
+ const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance");
+ const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically");
+ this.setState({
+ tagsEnabled,
+ preferredSort: orderAlphabetically ? SortAlgorithm.Alphabetic : SortAlgorithm.Recent,
+ preferredAlgorithm: orderByImportance ? ListAlgorithm.Importance : ListAlgorithm.Natural,
+ });
+ this.setAlgorithmClass();
+ }
+
+ protected __onDispatch(payload: ActionPayload): void {
+ if (payload.action === 'MatrixActions.sync') {
+ // Filter out anything that isn't the first PREPARED sync.
+ if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) {
+ return;
+ }
+
+ this.checkEnabled();
+ if (!this.enabled) return;
+
+ this.matrixClient = payload.matrixClient;
+
+ // Update any settings here, as some may have happened before we were logically ready.
+ this.readAndCacheSettingsFromStore();
+
+ // noinspection JSIgnoredPromiseFromCall
+ this.regenerateAllLists();
+ }
+
+ // TODO: Remove this once the RoomListStore becomes default
+ if (!this.enabled) return;
+
+ if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') {
+ // Reset state without causing updates as the client will have been destroyed
+ // and downstream code will throw NPE errors.
+ this.reset();
+ this.matrixClient = null;
+ this.initialListsGenerated = false; // we'll want to regenerate them
+ }
+
+ // Everything below here requires a MatrixClient or some sort of logical readiness.
+ const logicallyReady = this.matrixClient && this.initialListsGenerated;
+ if (!logicallyReady) return;
+
+ if (payload.action === 'setting_updated') {
+ if (this.watchedSettings.includes(payload.settingName)) {
+ this.readAndCacheSettingsFromStore();
+
+ // noinspection JSIgnoredPromiseFromCall
+ this.regenerateAllLists(); // regenerate the lists now
+ }
+ } else if (payload.action === 'MatrixActions.Room.receipt') {
+ // First see if the receipt event is for our own user. If it was, trigger
+ // a room update (we probably read the room on a different device).
+ const myUserId = this.matrixClient.getUserId();
+ for (const eventId of Object.keys(payload.event.getContent())) {
+ const receiptUsers = Object.keys(payload.event.getContent()[eventId]['m.read'] || {});
+ if (receiptUsers.includes(myUserId)) {
+ // TODO: Update room now that it's been read
+ return;
+ }
+ }
+ } else if (payload.action === 'MatrixActions.Room.tags') {
+ // TODO: Update room from tags
+ } else if (payload.action === 'MatrixActions.room.timeline') {
+ // TODO: Update room from new events
+ } else if (payload.action === 'MatrixActions.Event.decrypted') {
+ // TODO: Update room from decrypted event
+ } else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') {
+ // TODO: Update DMs
+ } else if (payload.action === 'MatrixActions.Room.myMembership') {
+ // TODO: Update room from membership change
+ } else if (payload.action === 'MatrixActions.room') {
+ // TODO: Update room from creation/join
+ } else if (payload.action === 'view_room') {
+ // TODO: Update sticky room
+ }
+ }
+
+ private getSortAlgorithmFor(tagId: TagID): SortAlgorithm {
+ switch (tagId) {
+ case DefaultTagID.Invite:
+ case DefaultTagID.Untagged:
+ case DefaultTagID.Archived:
+ case DefaultTagID.LowPriority:
+ case DefaultTagID.DM:
+ return this.state.preferredSort;
+ case DefaultTagID.Favourite:
+ default:
+ return SortAlgorithm.Manual;
+ }
+ }
+
+ private setState(newState: IState) {
+ if (!this.enabled) return;
+
+ this.state = Object.assign(this.state, newState);
+ this.__emitChange();
+ }
+
+ private setAlgorithmClass() {
+ this.algorithm = getAlgorithmInstance(this.state.preferredAlgorithm);
+ }
+
+ private async regenerateAllLists() {
+ console.log("REGEN");
+ const tags: ITagSortingMap = {};
+ for (const tagId of OrderedDefaultTagIDs) {
+ tags[tagId] = this.getSortAlgorithmFor(tagId);
+ }
+
+ if (this.state.tagsEnabled) {
+ // TODO: Find a more reliable way to get tags
+ const roomTags = TagOrderStore.getOrderedTags() || [];
+ console.log("rtags", roomTags);
+ }
+
+ await this.algorithm.populateTags(tags);
+ await this.algorithm.setKnownRooms(this.matrixClient.getRooms());
+
+ this.initialListsGenerated = true;
+
+ // TODO: How do we asynchronously update the store's state? or do we just give in and make it all sync?
+ }
+}
+
+export default class RoomListStore {
+ private static internalInstance: _RoomListStore;
+
+ public static get instance(): _RoomListStore {
+ if (!RoomListStore.internalInstance) {
+ RoomListStore.internalInstance = new _RoomListStore();
+ }
+
+ return RoomListStore.internalInstance;
+ }
+}
diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts
new file mode 100644
index 0000000000..7b12602541
--- /dev/null
+++ b/src/stores/room-list/RoomListStoreTempProxy.ts
@@ -0,0 +1,49 @@
+/*
+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 { Room } from "matrix-js-sdk/src/models/room";
+import SettingsStore from "../../settings/SettingsStore";
+import RoomListStore from "./RoomListStore2";
+import OldRoomListStore from "../RoomListStore";
+
+/**
+ * Temporary RoomListStore proxy. Should be replaced with RoomListStore2 when
+ * it is available to everyone.
+ *
+ * TODO: Remove this when RoomListStore gets fully replaced.
+ */
+export class RoomListStoreTempProxy {
+ public static isUsingNewStore(): boolean {
+ return SettingsStore.isFeatureEnabled("feature_new_room_list");
+ }
+
+ public static addListener(handler: () => void) {
+ if (RoomListStoreTempProxy.isUsingNewStore()) {
+ return RoomListStore.instance.addListener(handler);
+ } else {
+ return OldRoomListStore.addListener(handler);
+ }
+ }
+
+ public static getRoomLists(): {[tagId in TagID]: Room[]} {
+ if (RoomListStoreTempProxy.isUsingNewStore()) {
+ return RoomListStore.instance.orderedLists;
+ } else {
+ return OldRoomListStore.getRoomLists();
+ }
+ }
+}
diff --git a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts
new file mode 100644
index 0000000000..4fe5125a15
--- /dev/null
+++ b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts
@@ -0,0 +1,100 @@
+/*
+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 { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm } from "./IAlgorithm";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
+import { DefaultTagID } from "../models";
+
+/**
+ * A demonstration/temporary algorithm to verify the API surface works.
+ * TODO: Remove this before shipping
+ */
+export class ChaoticAlgorithm implements IAlgorithm {
+
+ private cached: ITagMap = {};
+ private sortAlgorithms: ITagSortingMap;
+ private rooms: Room[] = [];
+
+ constructor(private representativeAlgorithm: ListAlgorithm) {
+ }
+
+ getOrderedRooms(): ITagMap {
+ return this.cached;
+ }
+
+ async populateTags(tagSortingMap: ITagSortingMap): Promise {
+ if (!tagSortingMap) throw new Error(`Map cannot be null or empty`);
+ this.sortAlgorithms = tagSortingMap;
+ this.setKnownRooms(this.rooms); // regenerate the room lists
+ }
+
+ handleRoomUpdate(room): Promise {
+ return undefined;
+ }
+
+ setKnownRooms(rooms: Room[]): Promise {
+ if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`);
+ if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`);
+
+ this.rooms = rooms;
+
+ const newTags = {};
+ for (const tagId in this.sortAlgorithms) {
+ // noinspection JSUnfilteredForInLoop
+ newTags[tagId] = [];
+ }
+
+ // If we can avoid doing work, do so.
+ if (!rooms.length) {
+ this.cached = newTags;
+ return;
+ }
+
+ // TODO: Remove logging
+ console.log('setting known rooms - regen in progress');
+ console.log({alg: this.representativeAlgorithm});
+
+ // Step through each room and determine which tags it should be in.
+ // We don't care about ordering or sorting here - we're simply organizing things.
+ for (const room of rooms) {
+ const tags = room.tags;
+ let inTag = false;
+ for (const tagId in tags) {
+ // noinspection JSUnfilteredForInLoop
+ if (isNullOrUndefined(newTags[tagId])) {
+ // skip the tag if we don't know about it
+ continue;
+ }
+
+ inTag = true;
+
+ // noinspection JSUnfilteredForInLoop
+ newTags[tagId].push(room);
+ }
+
+ // If the room wasn't pushed to a tag, push it to the untagged tag.
+ if (!inTag) {
+ newTags[DefaultTagID.Untagged].push(room);
+ }
+ }
+
+ // TODO: Do sorting
+
+ // Finally, assign the tags to our cache
+ this.cached = newTags;
+ }
+}
diff --git a/src/stores/room-list/algorithms/IAlgorithm.ts b/src/stores/room-list/algorithms/IAlgorithm.ts
new file mode 100644
index 0000000000..fbe2f7a27d
--- /dev/null
+++ b/src/stores/room-list/algorithms/IAlgorithm.ts
@@ -0,0 +1,95 @@
+/*
+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 { Room } from "matrix-js-sdk/src/models/room";
+
+
+export enum SortAlgorithm {
+ Manual = "MANUAL",
+ Alphabetic = "ALPHABETIC",
+ Recent = "RECENT",
+}
+
+export enum ListAlgorithm {
+ // Orders Red > Grey > Bold > Idle
+ Importance = "IMPORTANCE",
+
+ // Orders however the SortAlgorithm decides
+ Natural = "NATURAL",
+}
+
+export enum Category {
+ Red = "RED",
+ Grey = "GREY",
+ Bold = "BOLD",
+ Idle = "IDLE",
+}
+
+export interface ITagSortingMap {
+ // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better.
+ [tagId: TagID]: SortAlgorithm;
+}
+
+export interface ITagMap {
+ // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better.
+ [tagId: TagID]: Room[];
+}
+
+// TODO: Convert IAlgorithm to an abstract class?
+// TODO: Add locking support to avoid concurrent writes
+// TODO: EventEmitter support
+
+/**
+ * Represents an algorithm for the RoomListStore to use
+ */
+export interface IAlgorithm {
+ /**
+ * Asks the Algorithm to regenerate all lists, using the tags given
+ * as reference for which lists to generate and which way to generate
+ * them.
+ * @param {ITagSortingMap} tagSortingMap The tags to generate.
+ * @returns {Promise<*>} A promise which resolves when complete.
+ */
+ populateTags(tagSortingMap: ITagSortingMap): Promise;
+
+ /**
+ * Gets an ordered set of rooms for the all known tags.
+ * @returns {ITagMap} The cached list of rooms, ordered,
+ * for each tag. May be empty, but never null/undefined.
+ */
+ getOrderedRooms(): ITagMap;
+
+ /**
+ * Seeds the Algorithm with a set of rooms. The algorithm will discard all
+ * previously known information and instead use these rooms instead.
+ * @param {Room[]} rooms The rooms to force the algorithm to use.
+ * @returns {Promise<*>} A promise which resolves when complete.
+ */
+ setKnownRooms(rooms: Room[]): Promise;
+
+ /**
+ * Asks the Algorithm to update its knowledge of a room. For example, when
+ * a user tags a room, joins/creates a room, or leaves a room the Algorithm
+ * should be told that the room's info might have changed. The Algorithm
+ * may no-op this request if no changes are required.
+ * @param {Room} room The room which might have affected sorting.
+ * @returns {Promise} A promise which resolve to true or false
+ * depending on whether or not getOrderedRooms() should be called after
+ * processing.
+ */
+ handleRoomUpdate(room: Room): Promise;
+}
diff --git a/src/stores/room-list/algorithms/index.ts b/src/stores/room-list/algorithms/index.ts
new file mode 100644
index 0000000000..cb67d42187
--- /dev/null
+++ b/src/stores/room-list/algorithms/index.ts
@@ -0,0 +1,36 @@
+/*
+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 { IAlgorithm, ListAlgorithm } from "./IAlgorithm";
+import { ChaoticAlgorithm } from "./ChaoticAlgorithm";
+
+const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => IAlgorithm } = {
+ [ListAlgorithm.Natural]: () => new ChaoticAlgorithm(ListAlgorithm.Natural),
+ [ListAlgorithm.Importance]: () => new ChaoticAlgorithm(ListAlgorithm.Importance),
+};
+
+/**
+ * Gets an instance of the defined algorithm
+ * @param {ListAlgorithm} algorithm The algorithm to get an instance of.
+ * @returns {IAlgorithm} The algorithm instance.
+ */
+export function getAlgorithmInstance(algorithm: ListAlgorithm): IAlgorithm {
+ if (!ALGORITHM_FACTORIES[algorithm]) {
+ throw new Error(`${algorithm} is not a known algorithm`);
+ }
+
+ return ALGORITHM_FACTORIES[algorithm]();
+}
diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts
new file mode 100644
index 0000000000..d1c915e035
--- /dev/null
+++ b/src/stores/room-list/models.ts
@@ -0,0 +1,36 @@
+/*
+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.
+*/
+
+export enum DefaultTagID {
+ Invite = "im.vector.fake.invite",
+ Untagged = "im.vector.fake.recent", // legacy: used to just be 'recent rooms' but now it's all untagged rooms
+ Archived = "im.vector.fake.archived",
+ LowPriority = "m.lowpriority",
+ Favourite = "m.favourite",
+ DM = "im.vector.fake.direct",
+ ServerNotice = "m.server_notice",
+}
+export const OrderedDefaultTagIDs = [
+ DefaultTagID.Invite,
+ DefaultTagID.Favourite,
+ DefaultTagID.DM,
+ DefaultTagID.Untagged,
+ DefaultTagID.LowPriority,
+ DefaultTagID.ServerNotice,
+ DefaultTagID.Archived,
+];
+
+export type TagID = string | DefaultTagID;
diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js
index 8dc4647920..7bcd2a8ae3 100644
--- a/test/components/views/rooms/RoomList-test.js
+++ b/test/components/views/rooms/RoomList-test.js
@@ -14,7 +14,7 @@ import DMRoomMap from '../../../../src/utils/DMRoomMap.js';
import GroupStore from '../../../../src/stores/GroupStore.js';
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
-import {TAG_DM} from "../../../../src/stores/RoomListStore";
+import {DefaultTagID} from "../../../../src/stores/room-list/models";
function generateRoomId() {
return '!' + Math.random().toString().slice(2, 10) + ':domain';
@@ -153,7 +153,7 @@ describe('RoomList', () => {
// Set up the room that will be moved such that it has the correct state for a room in
// the section for oldTag
if (['m.favourite', 'm.lowpriority'].includes(oldTag)) movingRoom.tags = {[oldTag]: {}};
- if (oldTag === TAG_DM) {
+ if (oldTag === DefaultTagID.DM) {
// Mock inverse m.direct
DMRoomMap.shared().roomToUser = {
[movingRoom.roomId]: '@someotheruser:domain',
@@ -180,7 +180,7 @@ describe('RoomList', () => {
// TODO: Re-enable dragging tests when we support dragging again.
describe.skip('does correct optimistic update when dragging from', () => {
it('rooms to people', () => {
- expectCorrectMove(undefined, TAG_DM);
+ expectCorrectMove(undefined, DefaultTagID.DM);
});
it('rooms to favourites', () => {
@@ -195,15 +195,15 @@ describe('RoomList', () => {
// Whe running the app live, it updates when some other event occurs (likely the
// m.direct arriving) that these tests do not fire.
xit('people to rooms', () => {
- expectCorrectMove(TAG_DM, undefined);
+ expectCorrectMove(DefaultTagID.DM, undefined);
});
it('people to favourites', () => {
- expectCorrectMove(TAG_DM, 'm.favourite');
+ expectCorrectMove(DefaultTagID.DM, 'm.favourite');
});
it('people to lowpriority', () => {
- expectCorrectMove(TAG_DM, 'm.lowpriority');
+ expectCorrectMove(DefaultTagID.DM, 'm.lowpriority');
});
it('low priority to rooms', () => {
@@ -211,7 +211,7 @@ describe('RoomList', () => {
});
it('low priority to people', () => {
- expectCorrectMove('m.lowpriority', TAG_DM);
+ expectCorrectMove('m.lowpriority', DefaultTagID.DM);
});
it('low priority to low priority', () => {
@@ -223,7 +223,7 @@ describe('RoomList', () => {
});
it('favourites to people', () => {
- expectCorrectMove('m.favourite', TAG_DM);
+ expectCorrectMove('m.favourite', DefaultTagID.DM);
});
it('favourites to low priority', () => {
diff --git a/tsconfig.json b/tsconfig.json
index b87f640734..8a01ca335e 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -14,7 +14,8 @@
"jsx": "react",
"types": [
"node",
- "react"
+ "react",
+ "flux"
]
},
"include": [
diff --git a/yarn.lock b/yarn.lock
index b0d3816dc4..6375c745fc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1218,6 +1218,19 @@
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
+"@types/fbemitter@*":
+ version "2.0.32"
+ resolved "https://registry.yarnpkg.com/@types/fbemitter/-/fbemitter-2.0.32.tgz#8ed204da0f54e9c8eaec31b1eec91e25132d082c"
+ integrity sha1-jtIE2g9U6cjq7DGx7skeJRMtCCw=
+
+"@types/flux@^3.1.9":
+ version "3.1.9"
+ resolved "https://registry.yarnpkg.com/@types/flux/-/flux-3.1.9.tgz#ddfc9641ee2e2e6cb6cd730c6a48ef82e2076711"
+ integrity sha512-bSbDf4tTuN9wn3LTGPnH9wnSSLtR5rV7UPWFpM00NJ1pSTBwCzeZG07XsZ9lBkxwngrqjDtM97PLt5IuIdCQUA==
+ dependencies:
+ "@types/fbemitter" "*"
+ "@types/react" "*"
+
"@types/glob@^7.1.1":
version "7.1.1"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"