Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/peeking-races
This commit is contained in:
commit
37bd59bf90
74 changed files with 3752 additions and 369 deletions
107
src/stores/AsyncStore.ts
Normal file
107
src/stores/AsyncStore.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
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 { EventEmitter } from 'events';
|
||||
import AwaitLock from 'await-lock';
|
||||
import { Dispatcher } from "flux";
|
||||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
|
||||
/**
|
||||
* The event/channel to listen for in an AsyncStore.
|
||||
*/
|
||||
export const UPDATE_EVENT = "update";
|
||||
|
||||
/**
|
||||
* Represents a minimal store which works similar to Flux stores. Instead
|
||||
* of everything needing to happen in a dispatch cycle, everything can
|
||||
* happen async to that cycle.
|
||||
*
|
||||
* The store operates by using Object.assign() to mutate state - it sends the
|
||||
* state objects (current and new) through the function onto a new empty
|
||||
* object. Because of this, it is recommended to break out your state to be as
|
||||
* safe as possible. The state mutations are also locked, preventing concurrent
|
||||
* writes.
|
||||
*
|
||||
* All updates to the store happen on the UPDATE_EVENT event channel with the
|
||||
* one argument being the instance of the store.
|
||||
*
|
||||
* To update the state, use updateState() and preferably await the result to
|
||||
* help prevent lock conflicts.
|
||||
*/
|
||||
export abstract class AsyncStore<T extends Object> extends EventEmitter {
|
||||
private storeState: T = <T>{};
|
||||
private lock = new AwaitLock();
|
||||
private readonly dispatcherRef: string;
|
||||
|
||||
/**
|
||||
* Creates a new AsyncStore using the given dispatcher.
|
||||
* @param {Dispatcher<ActionPayload>} dispatcher The dispatcher to rely upon.
|
||||
*/
|
||||
protected constructor(private dispatcher: Dispatcher<ActionPayload>) {
|
||||
super();
|
||||
|
||||
this.dispatcherRef = dispatcher.register(this.onDispatch.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* The current state of the store. Cannot be mutated.
|
||||
*/
|
||||
protected get state(): T {
|
||||
return Object.freeze(this.storeState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the store's listening functions, such as the listener to the dispatcher.
|
||||
*/
|
||||
protected stop() {
|
||||
if (this.dispatcherRef) this.dispatcher.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the state of the store.
|
||||
* @param {T|*} newState The state to update in the store using Object.assign()
|
||||
*/
|
||||
protected async updateState(newState: T | Object) {
|
||||
await this.lock.acquireAsync();
|
||||
try {
|
||||
this.storeState = Object.assign(<T>{}, this.storeState, newState);
|
||||
this.emit(UPDATE_EVENT, this);
|
||||
} finally {
|
||||
await this.lock.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the store's to the provided state or an empty object.
|
||||
* @param {T|*} newState The new state of the store.
|
||||
* @param {boolean} quiet If true, the function will not raise an UPDATE_EVENT.
|
||||
*/
|
||||
protected async reset(newState: T | Object = null, quiet = false) {
|
||||
await this.lock.acquireAsync();
|
||||
try {
|
||||
this.storeState = <T>(newState || {});
|
||||
if (!quiet) this.emit(UPDATE_EVENT, this);
|
||||
} finally {
|
||||
await this.lock.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the dispatcher broadcasts a dispatch event.
|
||||
* @param {ActionPayload} payload The event being dispatched.
|
||||
*/
|
||||
protected abstract onDispatch(payload: ActionPayload);
|
||||
}
|
|
@ -15,10 +15,10 @@ limitations under the License.
|
|||
*/
|
||||
import dis from '../dispatcher/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();
|
||||
|
|
|
@ -92,11 +92,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("👋 legacy room list store has been disabled");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the sorting algorithm used by the RoomListStore.
|
||||
* @param {string} algorithm The new algorithm to use. Should be one of the ALGO_* constants.
|
||||
|
@ -113,6 +121,8 @@ class RoomListStore extends Store {
|
|||
}
|
||||
|
||||
_init() {
|
||||
if (this.disabled) return;
|
||||
|
||||
// Initialise state
|
||||
const defaultLists = {
|
||||
"m.server_notice": [/* { room: js-sdk room, category: string } */],
|
||||
|
@ -140,6 +150,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
|
||||
|
@ -156,6 +168,8 @@ class RoomListStore extends Store {
|
|||
}
|
||||
|
||||
__onDispatch(payload) {
|
||||
if (this.disabled) return;
|
||||
|
||||
const logicallyReady = this._matrixClient && this._state.ready;
|
||||
switch (payload.action) {
|
||||
case 'setting_updated': {
|
||||
|
@ -182,6 +196,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.
|
||||
|
|
125
src/stores/room-list/README.md
Normal file
125
src/stores/room-list/README.md
Normal file
|
@ -0,0 +1,125 @@
|
|||
# Room list sorting
|
||||
|
||||
It's so complicated it needs its own README.
|
||||
|
||||
## Algorithms involved
|
||||
|
||||
There's two main kinds of algorithms involved in the room list store: list ordering and tag sorting.
|
||||
Throughout the code an intentional decision has been made to call them the List Algorithm and Sorting
|
||||
Algorithm respectively. The list algorithm determines the behaviour of the room list whereas the sorting
|
||||
algorithm determines how rooms get ordered within tags affected by the list algorithm.
|
||||
|
||||
Behaviour of the room list takes the shape of determining what features the room list supports, as well
|
||||
as determining where and when to apply the sorting algorithm in a tag. The importance algorithm, which
|
||||
is described later in this doc, is an example of an algorithm which makes heavy behavioural changes
|
||||
to the room list.
|
||||
|
||||
Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm
|
||||
the power to decide when and how to apply the tag sorting, if at all.
|
||||
|
||||
### Tag sorting algorithm: Alphabetical
|
||||
|
||||
When used, rooms in a given tag will be sorted alphabetically, where the alphabet's order is a problem
|
||||
for the browser. All we do is a simple string comparison and expect the browser to return something
|
||||
useful.
|
||||
|
||||
### Tag sorting algorithm: Manual
|
||||
|
||||
Manual sorting makes use of the `order` property present on all tags for a room, per the
|
||||
[Matrix specification](https://matrix.org/docs/spec/client_server/r0.6.0#room-tagging). Smaller values
|
||||
of `order` cause rooms to appear closer to the top of the list.
|
||||
|
||||
### Tag sorting algorithm: Recent
|
||||
|
||||
Rooms get ordered by the timestamp of the most recent useful message. Usefulness is yet another algorithm
|
||||
in the room list system which determines whether an event type is capable of bubbling up in the room list.
|
||||
Normally events like room messages, stickers, and room security changes will be considered useful enough
|
||||
to cause a shift in time.
|
||||
|
||||
Note that this is reliant on the event timestamps of the most recent message. Because Matrix is eventually
|
||||
consistent this means that from time to time a room might plummet or skyrocket across the tag due to the
|
||||
timestamp contained within the event (generated server-side by the sender's server).
|
||||
|
||||
### List ordering algorithm: Natural
|
||||
|
||||
This is the easiest of the algorithms to understand because it does essentially nothing. It imposes no
|
||||
behavioural changes over the tag sorting algorithm and is by far the simplest way to order a room list.
|
||||
Historically, it's been the only option in Riot and extremely common in most chat applications due to
|
||||
its relative deterministic behaviour.
|
||||
|
||||
### List ordering algorithm: Importance
|
||||
|
||||
On the other end of the spectrum, this is the most complicated algorithm which exists. There's major
|
||||
behavioural changes, and the tag sorting algorithm gets selectively applied depending on circumstances.
|
||||
|
||||
Each tag which is not manually ordered gets split into 4 sections or "categories". Manually ordered tags
|
||||
simply get the manual sorting algorithm applied to them with no further involvement from the importance
|
||||
algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off
|
||||
relative (perceived) importance to the user:
|
||||
|
||||
* **Red**: The room has unread mentions waiting for the user.
|
||||
* **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread
|
||||
messages which cause a push notification or badge count. Typically, this is the default as rooms get
|
||||
set to 'All Messages'.
|
||||
* **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without
|
||||
a badge/notification count (or 'Mentions Only'/'Muted').
|
||||
* **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user
|
||||
last read it.
|
||||
|
||||
Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey
|
||||
above bold, etc.
|
||||
|
||||
Once the algorithm has determined which rooms belong in which categories, the tag sorting algorithm
|
||||
gets applied to each category in a sub-sub-list fashion. This should result in the red rooms (for example)
|
||||
being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but
|
||||
collectively the tag will be sorted into categories with red being at the top.
|
||||
|
||||
<!-- TODO: Implement sticky rooms as described below -->
|
||||
|
||||
The algorithm also has a concept of a 'sticky' room which is the room the user is currently viewing.
|
||||
The sticky room will remain in position on the room list regardless of other factors going on as typically
|
||||
clicking on a room will cause it to change categories into 'idle'. This is done by preserving N rooms
|
||||
above the selected room at all times, where N is the number of rooms above the selected rooms when it was
|
||||
selected.
|
||||
|
||||
For example, if the user has 3 red rooms and selects the middle room, they will always see exactly one
|
||||
room above their selection at all times. If they receive another notification, and the tag ordering is
|
||||
specified as Recent, they'll see the new notification go to the top position, and the one that was previously
|
||||
there fall behind the sticky room.
|
||||
|
||||
The sticky room's category is technically 'idle' while being viewed and is explicitly pulled out of the
|
||||
tag sorting algorithm's input as it must maintain its position in the list. When the user moves to another
|
||||
room, the previous sticky room gets recalculated to determine which category it needs to be in as the user
|
||||
could have been scrolled up while new messages were received.
|
||||
|
||||
Further, the sticky room is not aware of category boundaries and thus the user can see a shift in what
|
||||
kinds of rooms move around their selection. An example would be the user having 4 red rooms, the user
|
||||
selecting the third room (leaving 2 above it), and then having the rooms above it read on another device.
|
||||
This would result in 1 red room and 1 other kind of room above the sticky room as it will try to maintain
|
||||
2 rooms above the sticky room.
|
||||
|
||||
An exception for the sticky room placement is when there's suddenly not enough rooms to maintain the placement
|
||||
exactly. This typically happens if the user selects a room and leaves enough rooms where it cannot maintain
|
||||
the N required rooms above the sticky room. In this case, the sticky room will simply decrease N as needed.
|
||||
The N value will never increase while selection remains unchanged: adding a bunch of rooms after having
|
||||
put the sticky room in a position where it's had to decrease N will not increase N.
|
||||
|
||||
## Responsibilities of the store
|
||||
|
||||
The store is responsible for the ordering, upkeep, and tracking of all rooms. The room list component simply gets
|
||||
an object containing the tags it needs to worry about and the rooms within. The room list component will
|
||||
decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with
|
||||
all kinds of filtering.
|
||||
|
||||
## Class breakdowns
|
||||
|
||||
The `RoomListStore` is the major coordinator of various `Algorithm` implementations, which take care
|
||||
of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` superclass is also
|
||||
responsible for figuring out which tags get which rooms, as Matrix specifies them as a reverse map:
|
||||
tags get defined on rooms and are not defined as a collection of rooms (unlike how they are presented
|
||||
to the user). Various list-specific utilities are also included, though they are expected to move
|
||||
somewhere more general when needed. For example, the `membership` utilities could easily be moved
|
||||
elsewhere as needed.
|
||||
|
||||
The various bits throughout the room list store should also have jsdoc of some kind to help describe
|
||||
what they do and how they work.
|
253
src/stores/room-list/RoomListStore2.ts
Normal file
253
src/stores/room-list/RoomListStore2.ts
Normal file
|
@ -0,0 +1,253 @@
|
|||
/*
|
||||
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 { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
|
||||
import { Algorithm } from "./algorithms/list-ordering/Algorithm";
|
||||
import TagOrderStore from "../TagOrderStore";
|
||||
import { AsyncStore } from "../AsyncStore";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
|
||||
import { getListAlgorithmInstance } from "./algorithms/list-ordering";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
|
||||
interface IState {
|
||||
tagsEnabled?: boolean;
|
||||
|
||||
preferredSort?: SortAlgorithm;
|
||||
preferredAlgorithm?: ListAlgorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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";
|
||||
|
||||
class _RoomListStore extends AsyncStore<ActionPayload> {
|
||||
private matrixClient: MatrixClient;
|
||||
private initialListsGenerated = false;
|
||||
private enabled = false;
|
||||
private algorithm: Algorithm;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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("⚡ new room list store engaged");
|
||||
}
|
||||
}
|
||||
|
||||
private async readAndCacheSettingsFromStore() {
|
||||
const tagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags");
|
||||
const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance");
|
||||
const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically");
|
||||
await this.updateState({
|
||||
tagsEnabled,
|
||||
preferredSort: orderAlphabetically ? SortAlgorithm.Alphabetic : SortAlgorithm.Recent,
|
||||
preferredAlgorithm: orderByImportance ? ListAlgorithm.Importance : ListAlgorithm.Natural,
|
||||
});
|
||||
this.setAlgorithmClass();
|
||||
}
|
||||
|
||||
protected async onDispatch(payload: ActionPayload) {
|
||||
if (payload.action === 'MatrixActions.sync') {
|
||||
// Filter out anything that isn't the first PREPARED sync.
|
||||
if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Remove this once the RoomListStore becomes default
|
||||
this.checkEnabled();
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.matrixClient = payload.matrixClient;
|
||||
|
||||
// Update any settings here, as some may have happened before we were logically ready.
|
||||
console.log("Regenerating room lists: Startup");
|
||||
await this.readAndCacheSettingsFromStore();
|
||||
await 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(null, true);
|
||||
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)) {
|
||||
console.log("Regenerating room lists: Settings changed");
|
||||
await this.readAndCacheSettingsFromStore();
|
||||
|
||||
await this.regenerateAllLists(); // regenerate the lists now
|
||||
}
|
||||
}
|
||||
|
||||
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') {
|
||||
// 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).
|
||||
// noinspection JSObjectNullOrUndefined - this.matrixClient can't be null by this point in the lifecycle
|
||||
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
|
||||
console.log(payload);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (payload.action === 'MatrixActions.Room.tags') {
|
||||
// TODO: Update room from tags
|
||||
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);
|
||||
console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${roomId}`);
|
||||
await this.handleRoomUpdate(room, RoomUpdateCause.Timeline);
|
||||
} else if (payload.action === 'MatrixActions.Event.decrypted') {
|
||||
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);
|
||||
} else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') {
|
||||
// TODO: Update DMs
|
||||
console.log(payload);
|
||||
} else if (payload.action === 'MatrixActions.Room.myMembership') {
|
||||
// TODO: Update room from membership change
|
||||
console.log(payload);
|
||||
} else if (payload.action === 'MatrixActions.Room') {
|
||||
// TODO: Update room from creation/join
|
||||
console.log(payload);
|
||||
} else if (payload.action === 'view_room') {
|
||||
// TODO: Update sticky room
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
protected async updateState(newState: IState) {
|
||||
if (!this.enabled) return;
|
||||
|
||||
await super.updateState(newState);
|
||||
}
|
||||
|
||||
private setAlgorithmClass() {
|
||||
this.algorithm = getListAlgorithmInstance(this.state.preferredAlgorithm);
|
||||
}
|
||||
|
||||
private async regenerateAllLists() {
|
||||
console.warn("Regenerating all room lists");
|
||||
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 (this doesn't work)
|
||||
const roomTags = TagOrderStore.getOrderedTags() || [];
|
||||
console.log("rtags", roomTags);
|
||||
}
|
||||
|
||||
await this.algorithm.populateTags(tags);
|
||||
await this.algorithm.setKnownRooms(this.matrixClient.getRooms());
|
||||
|
||||
this.initialListsGenerated = true;
|
||||
|
||||
this.emit(LISTS_UPDATE_EVENT, this);
|
||||
}
|
||||
}
|
||||
|
||||
export default class RoomListStore {
|
||||
private static internalInstance: _RoomListStore;
|
||||
|
||||
public static get instance(): _RoomListStore {
|
||||
if (!RoomListStore.internalInstance) {
|
||||
RoomListStore.internalInstance = new _RoomListStore();
|
||||
}
|
||||
|
||||
return RoomListStore.internalInstance;
|
||||
}
|
||||
}
|
49
src/stores/room-list/RoomListStoreTempProxy.ts
Normal file
49
src/stores/room-list/RoomListStoreTempProxy.ts
Normal file
|
@ -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 SettingsStore from "../../settings/SettingsStore";
|
||||
import RoomListStore from "./RoomListStore2";
|
||||
import OldRoomListStore from "../RoomListStore";
|
||||
import { UPDATE_EVENT } from "../AsyncStore";
|
||||
import { ITagMap } from "./algorithms/models";
|
||||
|
||||
/**
|
||||
* 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.on(UPDATE_EVENT, handler);
|
||||
} else {
|
||||
return OldRoomListStore.addListener(handler);
|
||||
}
|
||||
}
|
||||
|
||||
public static getRoomLists(): ITagMap {
|
||||
if (RoomListStoreTempProxy.isUsingNewStore()) {
|
||||
return RoomListStore.instance.orderedLists;
|
||||
} else {
|
||||
return OldRoomListStore.getRoomLists();
|
||||
}
|
||||
}
|
||||
}
|
177
src/stores/room-list/algorithms/list-ordering/Algorithm.ts
Normal file
177
src/stores/room-list/algorithms/list-ordering/Algorithm.ts
Normal file
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
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 { DefaultTagID, RoomUpdateCause, TagID } from "../../models";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
import { EffectiveMembership, splitRoomsByMembership } from "../../membership";
|
||||
import { ITagMap, ITagSortingMap } from "../models";
|
||||
import DMRoomMap from "../../../../utils/DMRoomMap";
|
||||
|
||||
// TODO: Add locking support to avoid concurrent writes?
|
||||
// TODO: EventEmitter support? Might not be needed.
|
||||
|
||||
/**
|
||||
* Represents a list ordering algorithm. This class will take care of tag
|
||||
* management (which rooms go in which tags) and ask the implementation to
|
||||
* deal with ordering mechanics.
|
||||
*/
|
||||
export abstract class Algorithm {
|
||||
protected cached: ITagMap = {};
|
||||
protected sortAlgorithms: ITagSortingMap;
|
||||
protected rooms: Room[] = [];
|
||||
protected roomIdsToTags: {
|
||||
[roomId: string]: TagID[];
|
||||
} = {};
|
||||
|
||||
protected constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public async populateTags(tagSortingMap: ITagSortingMap): Promise<any> {
|
||||
if (!tagSortingMap) throw new Error(`Map cannot be null or empty`);
|
||||
this.sortAlgorithms = tagSortingMap;
|
||||
return this.setKnownRooms(this.rooms);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public getOrderedRooms(): ITagMap {
|
||||
return this.cached;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public async setKnownRooms(rooms: Room[]): Promise<any> {
|
||||
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: ITagMap = {};
|
||||
for (const tagId in this.sortAlgorithms) {
|
||||
// noinspection JSUnfilteredForInLoop
|
||||
newTags[tagId] = [];
|
||||
}
|
||||
|
||||
// If we can avoid doing work, do so.
|
||||
if (!rooms.length) {
|
||||
await this.generateFreshTags(newTags); // just in case it wants to do something
|
||||
this.cached = newTags;
|
||||
return;
|
||||
}
|
||||
|
||||
// Split out the easy rooms first (leave and invite)
|
||||
const memberships = splitRoomsByMembership(rooms);
|
||||
for (const room of memberships[EffectiveMembership.Invite]) {
|
||||
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is an Invite`);
|
||||
newTags[DefaultTagID.Invite].push(room);
|
||||
}
|
||||
for (const room of memberships[EffectiveMembership.Leave]) {
|
||||
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Historical`);
|
||||
newTags[DefaultTagID.Archived].push(room);
|
||||
}
|
||||
|
||||
// Now process all the joined rooms. This is a bit more complicated
|
||||
for (const room of memberships[EffectiveMembership.Join]) {
|
||||
let tags = Object.keys(room.tags || {});
|
||||
|
||||
if (tags.length === 0) {
|
||||
// Check to see if it's a DM if it isn't anything else
|
||||
if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
|
||||
tags = [DefaultTagID.DM];
|
||||
}
|
||||
}
|
||||
|
||||
let inTag = false;
|
||||
if (tags.length > 0) {
|
||||
for (const tag of tags) {
|
||||
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged as ${tag}`);
|
||||
if (!isNullOrUndefined(newTags[tag])) {
|
||||
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged with VALID tag ${tag}`);
|
||||
newTags[tag].push(room);
|
||||
inTag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!inTag) {
|
||||
// TODO: Determine if DM and push there instead
|
||||
newTags[DefaultTagID.Untagged].push(room);
|
||||
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Untagged`);
|
||||
}
|
||||
}
|
||||
|
||||
await this.generateFreshTags(newTags);
|
||||
|
||||
this.cached = newTags;
|
||||
this.updateTagsFromCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the roomsToTags map
|
||||
*/
|
||||
protected updateTagsFromCache() {
|
||||
const newMap = {};
|
||||
|
||||
const tags = Object.keys(this.cached);
|
||||
for (const tagId of tags) {
|
||||
const rooms = this.cached[tagId];
|
||||
for (const room of rooms) {
|
||||
if (!newMap[room.roomId]) newMap[room.roomId] = [];
|
||||
newMap[room.roomId].push(tagId);
|
||||
}
|
||||
}
|
||||
|
||||
this.roomIdsToTags = newMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the Algorithm believes a complete regeneration of the existing
|
||||
* lists is needed.
|
||||
* @param {ITagMap} updatedTagMap The tag map which needs populating. Each tag
|
||||
* will already have the rooms which belong to it - they just need ordering. Must
|
||||
* be mutated in place.
|
||||
* @returns {Promise<*>} A promise which resolves when complete.
|
||||
*/
|
||||
protected abstract generateFreshTags(updatedTagMap: ITagMap): Promise<any>;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param {RoomUpdateCause} cause The reason for the update being triggered.
|
||||
* @returns {Promise<boolean>} A promise which resolve to true or false
|
||||
* depending on whether or not getOrderedRooms() should be called after
|
||||
* processing.
|
||||
*/
|
||||
public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean>;
|
||||
}
|
|
@ -0,0 +1,298 @@
|
|||
/*
|
||||
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 { Algorithm } from "./Algorithm";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomUpdateCause, TagID } from "../../models";
|
||||
import { ITagMap, SortAlgorithm } from "../models";
|
||||
import { sortRoomsWithAlgorithm } from "../tag-sorting";
|
||||
import * as Unread from '../../../../Unread';
|
||||
|
||||
/**
|
||||
* The determined category of a room.
|
||||
*/
|
||||
export enum Category {
|
||||
/**
|
||||
* The room has unread mentions within.
|
||||
*/
|
||||
Red = "RED",
|
||||
/**
|
||||
* The room has unread notifications within. Note that these are not unread
|
||||
* mentions - they are simply messages which the user has asked to cause a
|
||||
* badge count update or push notification.
|
||||
*/
|
||||
Grey = "GREY",
|
||||
/**
|
||||
* The room has unread messages within (grey without the badge).
|
||||
*/
|
||||
Bold = "BOLD",
|
||||
/**
|
||||
* The room has no relevant unread messages within.
|
||||
*/
|
||||
Idle = "IDLE",
|
||||
}
|
||||
|
||||
interface ICategorizedRoomMap {
|
||||
// @ts-ignore - TS wants this to be a string, but we know better
|
||||
[category: Category]: Room[];
|
||||
}
|
||||
|
||||
interface ICategoryIndex {
|
||||
// @ts-ignore - TS wants this to be a string, but we know better
|
||||
[category: Category]: number; // integer
|
||||
}
|
||||
|
||||
// Caution: changing this means you'll need to update a bunch of assumptions and
|
||||
// comments! Check the usage of Category carefully to figure out what needs changing
|
||||
// if you're going to change this array's order.
|
||||
const CATEGORY_ORDER = [Category.Red, Category.Grey, Category.Bold, Category.Idle];
|
||||
|
||||
/**
|
||||
* An implementation of the "importance" algorithm for room list sorting. Where
|
||||
* the tag sorting algorithm does not interfere, rooms will be ordered into
|
||||
* categories of varying importance to the user. Alphabetical sorting does not
|
||||
* interfere with this algorithm, however manual ordering does.
|
||||
*
|
||||
* The importance of a room is defined by the kind of notifications, if any, are
|
||||
* present on the room. These are classified internally as Red, Grey, Bold, and
|
||||
* Idle. Red rooms have mentions, grey have unread messages, bold is a less noisy
|
||||
* version of grey, and idle means all activity has been seen by the user.
|
||||
*
|
||||
* The algorithm works by monitoring all room changes, including new messages in
|
||||
* tracked rooms, to determine if it needs a new category or different placement
|
||||
* within the same category. For more information, see the comments contained
|
||||
* within the class.
|
||||
*/
|
||||
export class ImportanceAlgorithm extends Algorithm {
|
||||
|
||||
// HOW THIS WORKS
|
||||
// --------------
|
||||
//
|
||||
// This block of comments assumes you've read the README one level higher.
|
||||
// You should do that if you haven't already.
|
||||
//
|
||||
// Tags are fed into the algorithmic functions from the Algorithm superclass,
|
||||
// which cause subsequent updates to the room list itself. Categories within
|
||||
// those tags are tracked as index numbers within the array (zero = top), with
|
||||
// each sticky room being tracked separately. Internally, the category index
|
||||
// can be found from `this.indices[tag][category]` and the sticky room information
|
||||
// from `this.stickyRoom`.
|
||||
//
|
||||
// The room list store is always provided with the `this.cached` results, which are
|
||||
// updated as needed and not recalculated often. For example, when a room needs to
|
||||
// move within a tag, the array in `this.cached` will be spliced instead of iterated.
|
||||
// The `indices` help track the positions of each category to make splicing easier.
|
||||
|
||||
private indices: {
|
||||
// @ts-ignore - TS wants this to be a string but we know better than it
|
||||
[tag: TagID]: ICategoryIndex;
|
||||
} = {};
|
||||
|
||||
// TODO: Use this (see docs above)
|
||||
private stickyRoom: {
|
||||
roomId: string;
|
||||
tag: TagID;
|
||||
fromTop: number;
|
||||
} = {
|
||||
roomId: null,
|
||||
tag: null,
|
||||
fromTop: 0,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
console.log("Constructed an ImportanceAlgorithm");
|
||||
}
|
||||
|
||||
// noinspection JSMethodCanBeStatic
|
||||
private categorizeRooms(rooms: Room[]): ICategorizedRoomMap {
|
||||
const map: ICategorizedRoomMap = {
|
||||
[Category.Red]: [],
|
||||
[Category.Grey]: [],
|
||||
[Category.Bold]: [],
|
||||
[Category.Idle]: [],
|
||||
};
|
||||
for (const room of rooms) {
|
||||
const category = this.getRoomCategory(room);
|
||||
map[category].push(room);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// noinspection JSMethodCanBeStatic
|
||||
private getRoomCategory(room: Room): Category {
|
||||
// Function implementation borrowed from old RoomListStore
|
||||
|
||||
const mentions = room.getUnreadNotificationCount('highlight') > 0;
|
||||
if (mentions) {
|
||||
return Category.Red;
|
||||
}
|
||||
|
||||
let unread = room.getUnreadNotificationCount() > 0;
|
||||
if (unread) {
|
||||
return Category.Grey;
|
||||
}
|
||||
|
||||
unread = Unread.doesRoomHaveUnreadMessages(room);
|
||||
if (unread) {
|
||||
return Category.Bold;
|
||||
}
|
||||
|
||||
return Category.Idle;
|
||||
}
|
||||
|
||||
protected async generateFreshTags(updatedTagMap: ITagMap): Promise<any> {
|
||||
for (const tagId of Object.keys(updatedTagMap)) {
|
||||
const unorderedRooms = updatedTagMap[tagId];
|
||||
|
||||
const sortBy = this.sortAlgorithms[tagId];
|
||||
if (!sortBy) throw new Error(`${tagId} does not have a sorting algorithm`);
|
||||
|
||||
if (sortBy === SortAlgorithm.Manual) {
|
||||
// Manual tags essentially ignore the importance algorithm, so don't do anything
|
||||
// special about them.
|
||||
updatedTagMap[tagId] = await sortRoomsWithAlgorithm(unorderedRooms, tagId, sortBy);
|
||||
} else {
|
||||
// Every other sorting type affects the categories, not the whole tag.
|
||||
const categorized = this.categorizeRooms(unorderedRooms);
|
||||
for (const category of Object.keys(categorized)) {
|
||||
const roomsToOrder = categorized[category];
|
||||
categorized[category] = await sortRoomsWithAlgorithm(roomsToOrder, tagId, sortBy);
|
||||
}
|
||||
|
||||
const newlyOrganized: Room[] = [];
|
||||
const newIndices: ICategoryIndex = {};
|
||||
|
||||
for (const category of CATEGORY_ORDER) {
|
||||
newIndices[category] = newlyOrganized.length;
|
||||
newlyOrganized.push(...categorized[category]);
|
||||
}
|
||||
|
||||
this.indices[tagId] = newIndices;
|
||||
updatedTagMap[tagId] = newlyOrganized;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
|
||||
const tags = this.roomIdsToTags[room.roomId];
|
||||
if (!tags) {
|
||||
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
|
||||
return false;
|
||||
}
|
||||
const category = this.getRoomCategory(room);
|
||||
let changed = false;
|
||||
for (const tag of tags) {
|
||||
if (this.sortAlgorithms[tag] === SortAlgorithm.Manual) {
|
||||
continue; // Nothing to do here.
|
||||
}
|
||||
|
||||
const taggedRooms = this.cached[tag];
|
||||
const indices = this.indices[tag];
|
||||
let roomIdx = taggedRooms.indexOf(room);
|
||||
if (roomIdx === -1) {
|
||||
console.warn(`Degrading performance to find missing room in "${tag}": ${room.roomId}`);
|
||||
roomIdx = taggedRooms.findIndex(r => r.roomId === room.roomId);
|
||||
}
|
||||
if (roomIdx === -1) {
|
||||
throw new Error(`Room ${room.roomId} has no index in ${tag}`);
|
||||
}
|
||||
|
||||
// Try to avoid doing array operations if we don't have to: only move rooms within
|
||||
// the categories if we're jumping categories
|
||||
const oldCategory = this.getCategoryFromIndices(roomIdx, indices);
|
||||
if (oldCategory !== category) {
|
||||
// Move the room and update the indices
|
||||
this.moveRoomIndexes(1, oldCategory, category, indices);
|
||||
taggedRooms.splice(roomIdx, 1); // splice out the old index (fixed position)
|
||||
taggedRooms.splice(indices[category], 0, room); // splice in the new room (pre-adjusted)
|
||||
// Note: if moveRoomIndexes() is called after the splice then the insert operation
|
||||
// will happen in the wrong place. Because we would have already adjusted the index
|
||||
// for the category, we don't need to determine how the room is moving in the list.
|
||||
// If we instead tried to insert before updating the indices, we'd have to determine
|
||||
// whether the room was moving later (towards IDLE) or earlier (towards RED) from its
|
||||
// current position, as it'll affect the category's start index after we remove the
|
||||
// room from the array.
|
||||
}
|
||||
|
||||
// The room received an update, so take out the slice and sort it. This should be relatively
|
||||
// quick because the room is inserted at the top of the category, and most popular sorting
|
||||
// algorithms will deal with trying to keep the active room at the top/start of the category.
|
||||
// For the few algorithms that will have to move the thing quite far (alphabetic with a Z room
|
||||
// for example), the list should already be sorted well enough that it can rip through the
|
||||
// array and slot the changed room in quickly.
|
||||
const nextCategoryStartIdx = category === CATEGORY_ORDER[CATEGORY_ORDER.length - 1]
|
||||
? Number.MAX_SAFE_INTEGER
|
||||
: indices[CATEGORY_ORDER[CATEGORY_ORDER.indexOf(category) + 1]];
|
||||
const startIdx = indices[category];
|
||||
const numSort = nextCategoryStartIdx - startIdx; // splice() returns up to the max, so MAX_SAFE_INT is fine
|
||||
const unsortedSlice = taggedRooms.splice(startIdx, numSort);
|
||||
const sorted = await sortRoomsWithAlgorithm(unsortedSlice, tag, this.sortAlgorithms[tag]);
|
||||
taggedRooms.splice(startIdx, 0, ...sorted);
|
||||
|
||||
// Finally, flag that we've done something
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private getCategoryFromIndices(index: number, indices: ICategoryIndex): Category {
|
||||
for (let i = 0; i < CATEGORY_ORDER.length; i++) {
|
||||
const category = CATEGORY_ORDER[i];
|
||||
const isLast = i === (CATEGORY_ORDER.length - 1);
|
||||
const startIdx = indices[category];
|
||||
const endIdx = isLast ? Number.MAX_SAFE_INTEGER : indices[CATEGORY_ORDER[i + 1]];
|
||||
if (index >= startIdx && index < endIdx) {
|
||||
return category;
|
||||
}
|
||||
}
|
||||
|
||||
// "Should never happen" disclaimer goes here
|
||||
throw new Error("Programming error: somehow you've ended up with an index that isn't in a category");
|
||||
}
|
||||
|
||||
private moveRoomIndexes(nRooms: number, fromCategory: Category, toCategory: Category, indices: ICategoryIndex) {
|
||||
// We have to update the index of the category *after* the from/toCategory variables
|
||||
// in order to update the indices correctly. Because the room is moving from/to those
|
||||
// categories, the next category's index will change - not the category we're modifying.
|
||||
// We also need to update subsequent categories as they'll all shift by nRooms, so we
|
||||
// loop over the order to achieve that.
|
||||
|
||||
for (let i = CATEGORY_ORDER.indexOf(fromCategory) + 1; i < CATEGORY_ORDER.length; i++) {
|
||||
const nextCategory = CATEGORY_ORDER[i];
|
||||
indices[nextCategory] -= nRooms;
|
||||
}
|
||||
|
||||
for (let i = CATEGORY_ORDER.indexOf(toCategory) + 1; i < CATEGORY_ORDER.length; i++) {
|
||||
const nextCategory = CATEGORY_ORDER[i];
|
||||
indices[nextCategory] += nRooms;
|
||||
}
|
||||
|
||||
// Do a quick check to see if we've completely broken the index
|
||||
for (let i = 1; i <= CATEGORY_ORDER.length; i++) {
|
||||
const lastCat = CATEGORY_ORDER[i - 1];
|
||||
const thisCat = CATEGORY_ORDER[i];
|
||||
|
||||
if (indices[lastCat] > indices[thisCat]) {
|
||||
// "should never happen" disclaimer goes here
|
||||
console.warn(`!! Room list index corruption: ${lastCat} (i:${indices[lastCat]}) is greater than ${thisCat} (i:${indices[thisCat]}) - category indices are likely desynced from reality`);
|
||||
|
||||
// TODO: Regenerate index when this happens
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
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 { Algorithm } from "./Algorithm";
|
||||
import { ITagMap } from "../models";
|
||||
import { sortRoomsWithAlgorithm } from "../tag-sorting";
|
||||
|
||||
/**
|
||||
* Uses the natural tag sorting algorithm order to determine tag ordering. No
|
||||
* additional behavioural changes are present.
|
||||
*/
|
||||
export class NaturalAlgorithm extends Algorithm {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
console.log("Constructed a NaturalAlgorithm");
|
||||
}
|
||||
|
||||
protected async generateFreshTags(updatedTagMap: ITagMap): Promise<any> {
|
||||
for (const tagId of Object.keys(updatedTagMap)) {
|
||||
const unorderedRooms = updatedTagMap[tagId];
|
||||
|
||||
const sortBy = this.sortAlgorithms[tagId];
|
||||
if (!sortBy) throw new Error(`${tagId} does not have a sorting algorithm`);
|
||||
|
||||
updatedTagMap[tagId] = await sortRoomsWithAlgorithm(unorderedRooms, tagId, sortBy);
|
||||
}
|
||||
}
|
||||
|
||||
public async handleRoomUpdate(room, cause): Promise<boolean> {
|
||||
const tags = this.roomIdsToTags[room.roomId];
|
||||
if (!tags) {
|
||||
console.warn(`No tags known for "${room.name}" (${room.roomId})`);
|
||||
return false;
|
||||
}
|
||||
for (const tag of tags) {
|
||||
// TODO: Optimize this loop to avoid useless operations
|
||||
// For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
|
||||
this.cached[tag] = await sortRoomsWithAlgorithm(this.cached[tag], tag, this.sortAlgorithms[tag]);
|
||||
}
|
||||
return true; // assume we changed something
|
||||
}
|
||||
}
|
38
src/stores/room-list/algorithms/list-ordering/index.ts
Normal file
38
src/stores/room-list/algorithms/list-ordering/index.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
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 { Algorithm } from "./Algorithm";
|
||||
import { ImportanceAlgorithm } from "./ImportanceAlgorithm";
|
||||
import { ListAlgorithm } from "../models";
|
||||
import { NaturalAlgorithm } from "./NaturalAlgorithm";
|
||||
|
||||
const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => Algorithm } = {
|
||||
[ListAlgorithm.Natural]: () => new NaturalAlgorithm(),
|
||||
[ListAlgorithm.Importance]: () => new ImportanceAlgorithm(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an instance of the defined algorithm
|
||||
* @param {ListAlgorithm} algorithm The algorithm to get an instance of.
|
||||
* @returns {Algorithm} The algorithm instance.
|
||||
*/
|
||||
export function getListAlgorithmInstance(algorithm: ListAlgorithm): Algorithm {
|
||||
if (!ALGORITHM_FACTORIES[algorithm]) {
|
||||
throw new Error(`${algorithm} is not a known algorithm`);
|
||||
}
|
||||
|
||||
return ALGORITHM_FACTORIES[algorithm]();
|
||||
}
|
42
src/stores/room-list/algorithms/models.ts
Normal file
42
src/stores/room-list/algorithms/models.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
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 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[];
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
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 { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { TagID } from "../../models";
|
||||
import { IAlgorithm } from "./IAlgorithm";
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import * as Unread from "../../../../Unread";
|
||||
|
||||
/**
|
||||
* Sorts rooms according to the browser's determination of alphabetic.
|
||||
*/
|
||||
export class AlphabeticAlgorithm implements IAlgorithm {
|
||||
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
|
||||
return rooms.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
}
|
31
src/stores/room-list/algorithms/tag-sorting/IAlgorithm.ts
Normal file
31
src/stores/room-list/algorithms/tag-sorting/IAlgorithm.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
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 { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { TagID } from "../../models";
|
||||
|
||||
/**
|
||||
* Represents a tag sorting algorithm.
|
||||
*/
|
||||
export interface IAlgorithm {
|
||||
/**
|
||||
* Sorts the given rooms according to the sorting rules of the algorithm.
|
||||
* @param {Room[]} rooms The rooms to sort.
|
||||
* @param {TagID} tagId The tag ID in which the rooms are being sorted.
|
||||
* @returns {Promise<Room[]>} Resolves to the sorted rooms.
|
||||
*/
|
||||
sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]>;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
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 { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { TagID } from "../../models";
|
||||
import { IAlgorithm } from "./IAlgorithm";
|
||||
|
||||
/**
|
||||
* Sorts rooms according to the tag's `order` property on the room.
|
||||
*/
|
||||
export class ManualAlgorithm implements IAlgorithm {
|
||||
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
|
||||
const getOrderProp = (r: Room) => r.tags[tagId].order || 0;
|
||||
return rooms.sort((a, b) => {
|
||||
return getOrderProp(a) - getOrderProp(b);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
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 { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { TagID } from "../../models";
|
||||
import { IAlgorithm } from "./IAlgorithm";
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import * as Unread from "../../../../Unread";
|
||||
|
||||
/**
|
||||
* Sorts rooms according to the last event's timestamp in each room that seems
|
||||
* useful to the user.
|
||||
*/
|
||||
export class RecentAlgorithm implements IAlgorithm {
|
||||
public async sortRooms(rooms: Room[], tagId: TagID): Promise<Room[]> {
|
||||
// We cache the timestamp lookup to avoid iterating forever on the timeline
|
||||
// of events. This cache only survives a single sort though.
|
||||
// We wouldn't need this if `.sort()` didn't constantly try and compare all
|
||||
// of the rooms to each other.
|
||||
|
||||
// TODO: We could probably improve the sorting algorithm here by finding changes.
|
||||
// For example, if we spent a little bit of time to determine which elements have
|
||||
// actually changed (probably needs to be done higher up?) then we could do an
|
||||
// insertion sort or similar on the limited set of changes.
|
||||
|
||||
const tsCache: { [roomId: string]: number } = {};
|
||||
const getLastTs = (r: Room) => {
|
||||
if (tsCache[r.roomId]) {
|
||||
return tsCache[r.roomId];
|
||||
}
|
||||
|
||||
const ts = (() => {
|
||||
// Apparently we can have rooms without timelines, at least under testing
|
||||
// environments. Just return MAX_INT when this happens.
|
||||
if (!r || !r.timeline) {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
for (let i = r.timeline.length - 1; i >= 0; --i) {
|
||||
const ev = r.timeline[i];
|
||||
if (!ev.getTs()) continue; // skip events that don't have timestamps (tests only?)
|
||||
|
||||
// TODO: Don't assume we're using the same client as the peg
|
||||
if (ev.getSender() === MatrixClientPeg.get().getUserId()
|
||||
|| Unread.eventTriggersUnreadCount(ev)) {
|
||||
return ev.getTs();
|
||||
}
|
||||
}
|
||||
|
||||
// we might only have events that don't trigger the unread indicator,
|
||||
// in which case use the oldest event even if normally it wouldn't count.
|
||||
// This is better than just assuming the last event was forever ago.
|
||||
if (r.timeline.length && r.timeline[0].getTs()) {
|
||||
return r.timeline[0].getTs();
|
||||
} else {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
})();
|
||||
|
||||
tsCache[r.roomId] = ts;
|
||||
return ts;
|
||||
};
|
||||
|
||||
return rooms.sort((a, b) => {
|
||||
return getLastTs(a) - getLastTs(b);
|
||||
});
|
||||
}
|
||||
}
|
53
src/stores/room-list/algorithms/tag-sorting/index.ts
Normal file
53
src/stores/room-list/algorithms/tag-sorting/index.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
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 { SortAlgorithm } from "../models";
|
||||
import { ManualAlgorithm } from "./ManualAlgorithm";
|
||||
import { IAlgorithm } from "./IAlgorithm";
|
||||
import { TagID } from "../../models";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RecentAlgorithm } from "./RecentAlgorithm";
|
||||
import { AlphabeticAlgorithm } from "./AlphabeticAlgorithm";
|
||||
|
||||
const ALGORITHM_INSTANCES: { [algorithm in SortAlgorithm]: IAlgorithm } = {
|
||||
[SortAlgorithm.Recent]: new RecentAlgorithm(),
|
||||
[SortAlgorithm.Alphabetic]: new AlphabeticAlgorithm(),
|
||||
[SortAlgorithm.Manual]: new ManualAlgorithm(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an instance of the defined algorithm
|
||||
* @param {SortAlgorithm} algorithm The algorithm to get an instance of.
|
||||
* @returns {IAlgorithm} The algorithm instance.
|
||||
*/
|
||||
export function getSortingAlgorithmInstance(algorithm: SortAlgorithm): IAlgorithm {
|
||||
if (!ALGORITHM_INSTANCES[algorithm]) {
|
||||
throw new Error(`${algorithm} is not a known algorithm`);
|
||||
}
|
||||
|
||||
return ALGORITHM_INSTANCES[algorithm];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts rooms in a given tag according to the algorithm given.
|
||||
* @param {Room[]} rooms The rooms to sort.
|
||||
* @param {TagID} tagId The tag in which the sorting is occurring.
|
||||
* @param {SortAlgorithm} algorithm The algorithm to use for sorting.
|
||||
* @returns {Promise<Room[]>} Resolves to the sorted rooms.
|
||||
*/
|
||||
export function sortRoomsWithAlgorithm(rooms: Room[], tagId: TagID, algorithm: SortAlgorithm): Promise<Room[]> {
|
||||
return getSortingAlgorithmInstance(algorithm).sortRooms(rooms, tagId);
|
||||
}
|
72
src/stores/room-list/membership.ts
Normal file
72
src/stores/room-list/membership.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
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 { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
/**
|
||||
* Approximation of a membership status for a given room.
|
||||
*/
|
||||
export enum EffectiveMembership {
|
||||
/**
|
||||
* The user is effectively joined to the room. For example, actually joined
|
||||
* or knocking on the room (when that becomes possible).
|
||||
*/
|
||||
Join = "JOIN",
|
||||
|
||||
/**
|
||||
* The user is effectively invited to the room. Currently this is a direct map
|
||||
* to the invite membership as no other membership states are effectively
|
||||
* invites.
|
||||
*/
|
||||
Invite = "INVITE",
|
||||
|
||||
/**
|
||||
* The user is effectively no longer in the room. For example, kicked,
|
||||
* banned, or voluntarily left.
|
||||
*/
|
||||
Leave = "LEAVE",
|
||||
}
|
||||
|
||||
export interface MembershipSplit {
|
||||
// @ts-ignore - TS wants this to be a string key, but we know better.
|
||||
[state: EffectiveMembership]: Room[];
|
||||
}
|
||||
|
||||
export function splitRoomsByMembership(rooms: Room[]): MembershipSplit {
|
||||
const split: MembershipSplit = {
|
||||
[EffectiveMembership.Invite]: [],
|
||||
[EffectiveMembership.Join]: [],
|
||||
[EffectiveMembership.Leave]: [],
|
||||
};
|
||||
|
||||
for (const room of rooms) {
|
||||
split[getEffectiveMembership(room.getMyMembership())].push(room);
|
||||
}
|
||||
|
||||
return split;
|
||||
}
|
||||
|
||||
export function getEffectiveMembership(membership: string): EffectiveMembership {
|
||||
if (membership === 'invite') {
|
||||
return EffectiveMembership.Invite;
|
||||
} else if (membership === 'join') {
|
||||
// TODO: Do the same for knock? Update docs as needed in the enum.
|
||||
return EffectiveMembership.Join;
|
||||
} else {
|
||||
// Probably a leave, kick, or ban
|
||||
return EffectiveMembership.Leave;
|
||||
}
|
||||
}
|
42
src/stores/room-list/models.ts
Normal file
42
src/stores/room-list/models.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
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;
|
||||
|
||||
export enum RoomUpdateCause {
|
||||
Timeline = "TIMELINE",
|
||||
RoomRead = "ROOM_READ", // TODO: Use this.
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue