2020-03-20 23:38:20 +03:00
|
|
|
/*
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2020-04-28 00:25:04 +03:00
|
|
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
2020-03-20 23:38:20 +03:00
|
|
|
import SettingsStore from "../../settings/SettingsStore";
|
2020-05-04 18:06:34 +03:00
|
|
|
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
|
2020-05-29 16:59:06 +03:00
|
|
|
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/list-ordering/Algorithm";
|
2020-03-20 23:38:20 +03:00
|
|
|
import TagOrderStore from "../TagOrderStore";
|
2020-04-28 00:25:04 +03:00
|
|
|
import { AsyncStore } from "../AsyncStore";
|
2020-05-04 18:06:34 +03:00
|
|
|
import { Room } from "matrix-js-sdk/src/models/room";
|
2020-05-12 01:12:45 +03:00
|
|
|
import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
|
2020-05-21 20:56:04 +03:00
|
|
|
import { getListAlgorithmInstance } from "./algorithms/list-ordering";
|
2020-05-14 22:45:17 +03:00
|
|
|
import { ActionPayload } from "../../dispatcher/payloads";
|
|
|
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
2020-05-26 00:54:02 +03:00
|
|
|
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
2020-05-29 16:59:06 +03:00
|
|
|
import { IFilterCondition } from "./filters/IFilterCondition";
|
|
|
|
import { TagWatcher } from "./TagWatcher";
|
2020-06-06 03:44:05 +03:00
|
|
|
import RoomViewStore from "../RoomViewStore";
|
2020-03-20 23:38:20 +03:00
|
|
|
|
|
|
|
interface IState {
|
|
|
|
tagsEnabled?: boolean;
|
|
|
|
|
|
|
|
preferredSort?: SortAlgorithm;
|
|
|
|
preferredAlgorithm?: ListAlgorithm;
|
|
|
|
}
|
|
|
|
|
2020-04-28 00:25:04 +03:00
|
|
|
/**
|
|
|
|
* The event/channel which is called when the room lists have been changed. Raised
|
|
|
|
* with one argument: the instance of the store.
|
|
|
|
*/
|
|
|
|
export const LISTS_UPDATE_EVENT = "lists_update";
|
|
|
|
|
2020-05-29 16:59:06 +03:00
|
|
|
export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
|
|
|
private _matrixClient: MatrixClient;
|
2020-03-20 23:38:20 +03:00
|
|
|
private initialListsGenerated = false;
|
|
|
|
private enabled = false;
|
2020-04-30 01:19:10 +03:00
|
|
|
private algorithm: Algorithm;
|
2020-05-29 16:59:06 +03:00
|
|
|
private filterConditions: IFilterCondition[] = [];
|
|
|
|
private tagWatcher = new TagWatcher(this);
|
2020-03-20 23:38:20 +03:00
|
|
|
|
|
|
|
private readonly watchedSettings = [
|
|
|
|
'RoomList.orderAlphabetically',
|
|
|
|
'RoomList.orderByImportance',
|
|
|
|
'feature_custom_tags',
|
|
|
|
];
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
super(defaultDispatcher);
|
|
|
|
|
|
|
|
this.checkEnabled();
|
|
|
|
for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null);
|
2020-06-06 03:44:05 +03:00
|
|
|
RoomViewStore.addListener(this.onRVSUpdate);
|
2020-03-20 23:38:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
public get orderedLists(): ITagMap {
|
|
|
|
if (!this.algorithm) return {}; // No tags yet.
|
|
|
|
return this.algorithm.getOrderedRooms();
|
|
|
|
}
|
|
|
|
|
2020-05-29 16:59:06 +03:00
|
|
|
public get matrixClient(): MatrixClient {
|
|
|
|
return this._matrixClient;
|
|
|
|
}
|
|
|
|
|
2020-03-20 23:38:20 +03:00
|
|
|
// TODO: Remove enabled flag when the old RoomListStore goes away
|
|
|
|
private checkEnabled() {
|
|
|
|
this.enabled = SettingsStore.isFeatureEnabled("feature_new_room_list");
|
|
|
|
if (this.enabled) {
|
2020-05-14 22:01:51 +03:00
|
|
|
console.log("⚡ new room list store engaged");
|
2020-03-20 23:38:20 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-28 00:25:04 +03:00
|
|
|
private async readAndCacheSettingsFromStore() {
|
2020-03-20 23:38:20 +03:00
|
|
|
const tagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags");
|
|
|
|
const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance");
|
|
|
|
const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically");
|
2020-04-28 00:25:04 +03:00
|
|
|
await this.updateState({
|
2020-03-20 23:38:20 +03:00
|
|
|
tagsEnabled,
|
|
|
|
preferredSort: orderAlphabetically ? SortAlgorithm.Alphabetic : SortAlgorithm.Recent,
|
|
|
|
preferredAlgorithm: orderByImportance ? ListAlgorithm.Importance : ListAlgorithm.Natural,
|
|
|
|
});
|
|
|
|
this.setAlgorithmClass();
|
|
|
|
}
|
|
|
|
|
2020-06-06 03:44:05 +03:00
|
|
|
private onRVSUpdate = () => {
|
|
|
|
if (!this.enabled) return; // TODO: Remove enabled flag when RoomListStore2 takes over
|
|
|
|
if (!this.matrixClient) return; // We assume there won't be RVS updates without a client
|
|
|
|
|
|
|
|
const activeRoomId = RoomViewStore.getRoomId();
|
|
|
|
if (!activeRoomId && this.algorithm.stickyRoom) {
|
|
|
|
this.algorithm.stickyRoom = null;
|
|
|
|
} else if (activeRoomId) {
|
|
|
|
const activeRoom = this.matrixClient.getRoom(activeRoomId);
|
|
|
|
if (!activeRoom) throw new Error(`${activeRoomId} is current in RVS but missing from client`);
|
|
|
|
if (activeRoom !== this.algorithm.stickyRoom) {
|
|
|
|
console.log(`Changing sticky room to ${activeRoomId}`);
|
|
|
|
this.algorithm.stickyRoom = activeRoom;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-04-28 00:25:04 +03:00
|
|
|
protected async onDispatch(payload: ActionPayload) {
|
2020-03-20 23:38:20 +03:00
|
|
|
if (payload.action === 'MatrixActions.sync') {
|
|
|
|
// Filter out anything that isn't the first PREPARED sync.
|
|
|
|
if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-04-28 00:25:04 +03:00
|
|
|
// TODO: Remove this once the RoomListStore becomes default
|
2020-03-20 23:38:20 +03:00
|
|
|
this.checkEnabled();
|
|
|
|
if (!this.enabled) return;
|
|
|
|
|
2020-05-29 16:59:06 +03:00
|
|
|
this._matrixClient = payload.matrixClient;
|
2020-03-20 23:38:20 +03:00
|
|
|
|
|
|
|
// Update any settings here, as some may have happened before we were logically ready.
|
2020-04-28 00:25:04 +03:00
|
|
|
console.log("Regenerating room lists: Startup");
|
|
|
|
await this.readAndCacheSettingsFromStore();
|
|
|
|
await this.regenerateAllLists();
|
2020-06-06 03:44:05 +03:00
|
|
|
this.onRVSUpdate(); // fake an RVS update to adjust sticky room, if needed
|
2020-03-20 23:38:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2020-04-28 00:25:04 +03:00
|
|
|
this.reset(null, true);
|
2020-05-29 16:59:06 +03:00
|
|
|
this._matrixClient = null;
|
2020-03-20 23:38:20 +03:00
|
|
|
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)) {
|
2020-04-28 00:25:04 +03:00
|
|
|
console.log("Regenerating room lists: Settings changed");
|
|
|
|
await this.readAndCacheSettingsFromStore();
|
2020-03-20 23:38:20 +03:00
|
|
|
|
2020-04-28 00:25:04 +03:00
|
|
|
await this.regenerateAllLists(); // regenerate the lists now
|
2020-03-20 23:38:20 +03:00
|
|
|
}
|
2020-05-04 18:06:34 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.algorithm) {
|
|
|
|
// This shouldn't happen because `initialListsGenerated` implies we have an algorithm.
|
|
|
|
throw new Error("Room list store has no algorithm to process dispatcher update with");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (payload.action === 'MatrixActions.Room.receipt') {
|
2020-03-20 23:38:20 +03:00
|
|
|
// 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).
|
2020-05-26 00:54:02 +03:00
|
|
|
if (readReceiptChangeIsFor(payload.event, this.matrixClient)) {
|
|
|
|
// TODO: Update room now that it's been read
|
|
|
|
console.log(payload);
|
|
|
|
return;
|
2020-03-20 23:38:20 +03:00
|
|
|
}
|
|
|
|
} else if (payload.action === 'MatrixActions.Room.tags') {
|
|
|
|
// TODO: Update room from tags
|
2020-05-04 18:06:34 +03:00
|
|
|
console.log(payload);
|
|
|
|
} else if (payload.action === 'MatrixActions.Room.timeline') {
|
|
|
|
const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
|
|
|
|
|
|
|
|
// Ignore non-live events (backfill)
|
|
|
|
if (!eventPayload.isLiveEvent || !payload.isLiveUnfilteredRoomTimelineEvent) return;
|
|
|
|
|
|
|
|
const roomId = eventPayload.event.getRoomId();
|
|
|
|
const room = this.matrixClient.getRoom(roomId);
|
2020-05-29 16:59:06 +03:00
|
|
|
const tryUpdate = async (updatedRoom: Room) => {
|
|
|
|
console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${updatedRoom.roomId}`);
|
|
|
|
await this.handleRoomUpdate(updatedRoom, RoomUpdateCause.Timeline);
|
|
|
|
};
|
|
|
|
if (!room) {
|
|
|
|
console.warn(`Live timeline event ${eventPayload.event.getId()} received without associated room`);
|
|
|
|
console.warn(`Queuing failed room update for retry as a result.`);
|
|
|
|
setTimeout(async () => {
|
|
|
|
const updatedRoom = this.matrixClient.getRoom(roomId);
|
|
|
|
await tryUpdate(updatedRoom);
|
|
|
|
}, 100); // 100ms should be enough for the room to show up
|
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
await tryUpdate(room);
|
|
|
|
}
|
2020-03-20 23:38:20 +03:00
|
|
|
} else if (payload.action === 'MatrixActions.Event.decrypted') {
|
2020-05-08 20:59:03 +03:00
|
|
|
const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
|
|
|
|
const roomId = eventPayload.event.getRoomId();
|
|
|
|
const room = this.matrixClient.getRoom(roomId);
|
|
|
|
if (!room) {
|
|
|
|
console.warn(`Event ${eventPayload.event.getId()} was decrypted in an unknown room ${roomId}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
console.log(`[RoomListDebug] Decrypted timeline event ${eventPayload.event.getId()} in ${roomId}`);
|
|
|
|
// TODO: Check that e2e rooms are calculated correctly on initial load.
|
|
|
|
// It seems like when viewing the room the timeline is decrypted, rather than at startup. This could
|
|
|
|
// cause inaccuracies with the list ordering. We may have to decrypt the last N messages of every room :(
|
|
|
|
await this.handleRoomUpdate(room, RoomUpdateCause.Timeline);
|
2020-03-20 23:38:20 +03:00
|
|
|
} else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') {
|
|
|
|
// TODO: Update DMs
|
2020-05-04 18:06:34 +03:00
|
|
|
console.log(payload);
|
2020-03-20 23:38:20 +03:00
|
|
|
} else if (payload.action === 'MatrixActions.Room.myMembership') {
|
2020-05-29 16:59:06 +03:00
|
|
|
// TODO: Improve new room check
|
|
|
|
const membershipPayload = (<any>payload); // TODO: Type out the dispatcher types
|
|
|
|
if (!membershipPayload.oldMembership && membershipPayload.membership === "join") {
|
|
|
|
console.log(`[RoomListDebug] Handling new room ${membershipPayload.room.roomId}`);
|
|
|
|
await this.algorithm.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
|
|
|
|
}
|
|
|
|
|
2020-03-20 23:38:20 +03:00
|
|
|
// TODO: Update room from membership change
|
2020-05-04 18:06:34 +03:00
|
|
|
console.log(payload);
|
|
|
|
} else if (payload.action === 'MatrixActions.Room') {
|
2020-05-29 16:59:06 +03:00
|
|
|
// TODO: Improve new room check
|
|
|
|
// const roomPayload = (<any>payload); // TODO: Type out the dispatcher types
|
|
|
|
// console.log(`[RoomListDebug] Handling new room ${roomPayload.room.roomId}`);
|
|
|
|
// await this.algorithm.handleRoomUpdate(roomPayload.room, RoomUpdateCause.NewRoom);
|
2020-03-20 23:38:20 +03:00
|
|
|
} else if (payload.action === 'view_room') {
|
|
|
|
// TODO: Update sticky room
|
2020-05-04 18:06:34 +03:00
|
|
|
console.log(payload);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<any> {
|
|
|
|
const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
|
|
|
|
if (shouldUpdate) {
|
|
|
|
console.log(`[DEBUG] Room "${room.name}" (${room.roomId}) triggered by ${cause} requires list update`);
|
|
|
|
this.emit(LISTS_UPDATE_EVENT, this);
|
2020-03-20 23:38:20 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-28 00:25:04 +03:00
|
|
|
protected async updateState(newState: IState) {
|
2020-03-20 23:38:20 +03:00
|
|
|
if (!this.enabled) return;
|
|
|
|
|
2020-04-28 00:25:04 +03:00
|
|
|
await super.updateState(newState);
|
2020-03-20 23:38:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
private setAlgorithmClass() {
|
2020-05-29 16:59:06 +03:00
|
|
|
if (this.algorithm) {
|
|
|
|
this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
|
|
|
|
}
|
2020-04-30 01:57:06 +03:00
|
|
|
this.algorithm = getListAlgorithmInstance(this.state.preferredAlgorithm);
|
2020-05-29 16:59:06 +03:00
|
|
|
this.algorithm.setFilterConditions(this.filterConditions);
|
|
|
|
this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
|
2020-03-20 23:38:20 +03:00
|
|
|
}
|
|
|
|
|
2020-05-29 16:59:06 +03:00
|
|
|
private onAlgorithmListUpdated = () => {
|
|
|
|
console.log("Underlying algorithm has triggered a list update - refiring");
|
|
|
|
this.emit(LISTS_UPDATE_EVENT, this);
|
|
|
|
};
|
|
|
|
|
2020-03-20 23:38:20 +03:00
|
|
|
private async regenerateAllLists() {
|
2020-04-28 00:25:04 +03:00
|
|
|
console.warn("Regenerating all room lists");
|
2020-05-29 16:59:06 +03:00
|
|
|
|
2020-03-20 23:38:20 +03:00
|
|
|
const tags: ITagSortingMap = {};
|
|
|
|
for (const tagId of OrderedDefaultTagIDs) {
|
|
|
|
tags[tagId] = this.getSortAlgorithmFor(tagId);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.state.tagsEnabled) {
|
2020-05-14 22:01:51 +03:00
|
|
|
// TODO: Find a more reliable way to get tags (this doesn't work)
|
2020-03-20 23:38:20 +03:00
|
|
|
const roomTags = TagOrderStore.getOrderedTags() || [];
|
|
|
|
console.log("rtags", roomTags);
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.algorithm.populateTags(tags);
|
|
|
|
await this.algorithm.setKnownRooms(this.matrixClient.getRooms());
|
|
|
|
|
|
|
|
this.initialListsGenerated = true;
|
|
|
|
|
2020-04-28 00:25:04 +03:00
|
|
|
this.emit(LISTS_UPDATE_EVENT, this);
|
2020-03-20 23:38:20 +03:00
|
|
|
}
|
2020-05-29 16:59:06 +03:00
|
|
|
|
|
|
|
public addFilter(filter: IFilterCondition): void {
|
|
|
|
console.log("Adding filter condition:", filter);
|
|
|
|
this.filterConditions.push(filter);
|
|
|
|
if (this.algorithm) {
|
|
|
|
this.algorithm.addFilterCondition(filter);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public removeFilter(filter: IFilterCondition): void {
|
|
|
|
console.log("Removing filter condition:", filter);
|
|
|
|
const idx = this.filterConditions.indexOf(filter);
|
|
|
|
if (idx >= 0) {
|
|
|
|
this.filterConditions.splice(idx, 1);
|
|
|
|
|
|
|
|
if (this.algorithm) {
|
|
|
|
this.algorithm.removeFilterCondition(filter);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-03-20 23:38:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
export default class RoomListStore {
|
2020-05-29 16:59:06 +03:00
|
|
|
private static internalInstance: RoomListStore2;
|
2020-03-20 23:38:20 +03:00
|
|
|
|
2020-05-29 16:59:06 +03:00
|
|
|
public static get instance(): RoomListStore2 {
|
2020-03-20 23:38:20 +03:00
|
|
|
if (!RoomListStore.internalInstance) {
|
2020-05-29 16:59:06 +03:00
|
|
|
RoomListStore.internalInstance = new RoomListStore2();
|
2020-03-20 23:38:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return RoomListStore.internalInstance;
|
|
|
|
}
|
|
|
|
}
|
2020-05-29 16:59:06 +03:00
|
|
|
|
|
|
|
window.mx_RoomListStore2 = RoomListStore.instance;
|