Inline documentation

pull/21833/head
Travis Ralston 2019-02-13 19:49:28 -07:00
parent b08ab6cd12
commit 0c7e0a264b
1 changed files with 54 additions and 23 deletions

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2018 New Vector Ltd Copyright 2018, 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -19,10 +19,22 @@ import DMRoomMap from '../utils/DMRoomMap';
import Unread from '../Unread'; import Unread from '../Unread';
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
const CATEGORY_RED = "red"; /*
const CATEGORY_GREY = "grey"; Room sorting algorithm:
const CATEGORY_BOLD = "bold"; * Always prefer to have red > grey > bold > idle
const CATEGORY_IDLE = "idle"; * The room being viewed should be sticky (not jump down to the idle list)
* When switching to a new room, sort the last sticky room to the top of the idle list.
The approach taken by the store is to generate an initial representation of all the
tagged lists (accepting that it'll take a little bit longer to calculate) and make
small changes to that over time. This results in quick changes to the room list while
also having update operations feel more like popping/pushing to a stack.
*/
const CATEGORY_RED = "red"; // Mentions in the room
const CATEGORY_GREY = "grey"; // Unread notified messages (not mentions)
const CATEGORY_BOLD = "bold"; // Unread messages (not notified, 'Mentions Only' rooms)
const CATEGORY_IDLE = "idle"; // Nothing of interest
const CATEGORY_ORDER = [CATEGORY_RED, CATEGORY_GREY, CATEGORY_BOLD, CATEGORY_IDLE]; const CATEGORY_ORDER = [CATEGORY_RED, CATEGORY_GREY, CATEGORY_BOLD, CATEGORY_IDLE];
const LIST_ORDERS = { const LIST_ORDERS = {
@ -70,6 +82,10 @@ class RoomListStore extends Store {
} }
_setState(newState) { _setState(newState) {
// 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
// categories.
if (newState['lists']) { if (newState['lists']) {
const presentationLists = {}; const presentationLists = {};
for (const key of Object.keys(newState['lists'])) { for (const key of Object.keys(newState['lists'])) {
@ -205,20 +221,24 @@ class RoomListStore extends Store {
// Note: it is important that we set a new stickyRoomId before setting the old room // Note: it is important that we set a new stickyRoomId before setting the old room
// to IDLE. If we don't, the wrong room gets counted as sticky. // to IDLE. If we don't, the wrong room gets counted as sticky.
const currentSticky = this._state.stickyRoomId; const currentStickyId = this._state.stickyRoomId;
this._setState({stickyRoomId: payload.room_id}); this._setState({stickyRoomId: payload.room_id});
if (currentSticky) { if (currentStickyId) {
this._setRoomCategory(this._matrixClient.getRoom(currentSticky), CATEGORY_IDLE); this._setRoomCategory(this._matrixClient.getRoom(currentStickyId), CATEGORY_IDLE);
} }
} }
break; break;
} }
}; }
_roomUpdateTriggered(roomId) { _roomUpdateTriggered(roomId) {
const room = this._matrixClient.getRoom(roomId); const room = this._matrixClient.getRoom(roomId);
if (!room) return; if (!room) return;
// We don't calculate categories for sticky rooms because we have a moderate
// interest in trying to maintain the category that they were last in before
// being artificially flagged as IDLE. Also, this reduces the amount of time
// we spend in _setRoomCategory ever so slightly.
if (this._state.stickyRoomId !== room.roomId) { if (this._state.stickyRoomId !== room.roomId) {
const category = this._calculateCategory(room); const category = this._calculateCategory(room);
this._setRoomCategory(room, category); this._setRoomCategory(room, category);
@ -227,8 +247,8 @@ class RoomListStore extends Store {
_setRoomCategory(room, category) { _setRoomCategory(room, category) {
const listsClone = {}; const listsClone = {};
const targetCatIndex = CATEGORY_ORDER.indexOf(category); const targetCategoryIndex = CATEGORY_ORDER.indexOf(category);
const targetTs = this._tsOfNewestEvent(room); const targetTimestamp = this._tsOfNewestEvent(room);
const myMembership = room.getMyMembership(); const myMembership = room.getMyMembership();
let doInsert = true; let doInsert = true;
@ -247,35 +267,43 @@ class RoomListStore extends Store {
// We need to update all instances of a room to ensure that they are correctly organized // We need to update all instances of a room to ensure that they are correctly organized
// in the list. We do this by shallow-cloning the entire `lists` object using a single // in the list. We do this by shallow-cloning the entire `lists` object using a single
// iterator. Within the loop, we also rebuild the list of rooms per tag (key) so that the // iterator. Within the loop, we also rebuild the list of rooms per tag (key) so that the
// updated room gets slotted into the right spot. // updated room gets slotted into the right spot. This sacrifices code clarity for not
// iterating on potentially large collections multiple times.
let inserted = false; let inserted = false;
for (const key of Object.keys(this._state.lists)) { for (const key of Object.keys(this._state.lists)) {
listsClone[key] = []; listsClone[key] = [];
let pushedEntry = false; let pushedEntry = false;
const hasRoom = !!this._state.lists[key].find((e) => e.room.roomId === room.roomId); const hasRoom = !!this._state.lists[key].find((e) => e.room.roomId === room.roomId);
// We track where the boundary within listsClone[key] is just in case our timestamp
// ordering fails. If we can't stick the room in at the correct place in the category
// grouping based on timestamp, we'll stick it at the top of the group which will be
// the index we track here.
let desiredCategoryBoundaryIndex = 0; let desiredCategoryBoundaryIndex = 0;
let foundBoundary = false; let foundBoundary = false;
for (const entry of this._state.lists[key]) { for (const entry of this._state.lists[key]) {
// if the list is a recent list, and the room appears in this list, and we're not looking at a sticky // if the list is a recent list, and the room appears in this list, and we're not looking at a sticky
// room (sticky rooms have unreliable categories), try to slot the new room in // room (sticky rooms have unreliable categories), try to slot the new room in
if (LIST_ORDERS[key] === 'recent' && hasRoom && entry.room.roomId !== this._state.stickyRoomId) { if (LIST_ORDERS[key] === 'recent' && hasRoom && entry.room.roomId !== this._state.stickyRoomId) {
const inTag = targetTags.length === 0 || targetTags.includes(key); const inTag = targetTags.length === 0 || targetTags.includes(key);
if (!pushedEntry && doInsert && inTag) { if (!pushedEntry && doInsert && inTag) {
const entryTs = this._tsOfNewestEvent(entry.room); const entryTimestamp = this._tsOfNewestEvent(entry.room);
const entryCategory = CATEGORY_ORDER.indexOf(entry.category); const entryCategoryIndex = CATEGORY_ORDER.indexOf(entry.category);
if (entryCategory >= targetCatIndex && !foundBoundary) { // As per above, check if we're meeting that boundary we wanted to locate.
if (entryCategoryIndex >= targetCategoryIndex && !foundBoundary) {
desiredCategoryBoundaryIndex = listsClone[key].length - 1; desiredCategoryBoundaryIndex = listsClone[key].length - 1;
foundBoundary = true; foundBoundary = true;
} }
// If we've hit the top of a boundary (either because there's no rooms in the target or // If we've hit the top of a boundary beyond our target category, insert at the top of
// we've reached the grouping of rooms), insert our room ahead of the others in the category. // the grouping to ensure the room isn't slotted incorrectly. Otherwise, try to insert
// This ensures that our room is on top (more recent) than the others. // based on most recent timestamp.
const changedBoundary = entryCategory >= targetCatIndex; const changedBoundary = entryCategoryIndex > targetCategoryIndex;
const currentCategory = entryCategory === targetCatIndex; const currentCategory = entryCategoryIndex === targetCategoryIndex;
if (changedBoundary || (false && currentCategory && targetTs >= entryTs)) { if (changedBoundary || (false && currentCategory && targetTimestamp >= entryTimestamp)) {
if (changedBoundary && false) { if (changedBoundary && false) {
// If we changed a boundary, then we've gone too far - go to the top of the last // If we changed a boundary, then we've gone too far - go to the top of the last
// section instead. // section instead.
@ -317,8 +345,9 @@ class RoomListStore extends Store {
} }
for (const tag of tags) { for (const tag of tags) {
for (let i = 0; i < listsClone[tag].length; i++) { for (let i = 0; i < listsClone[tag].length; i++) {
// Just find the top of our category grouping and insert it there.
const catIdxAtPosition = CATEGORY_ORDER.indexOf(listsClone[tag][i].category); const catIdxAtPosition = CATEGORY_ORDER.indexOf(listsClone[tag][i].category);
if (catIdxAtPosition >= targetCatIndex) { if (catIdxAtPosition >= targetCategoryIndex) {
listsClone[tag].splice(i, 0, {room: room, category: category}); listsClone[tag].splice(i, 0, {room: room, category: category});
break; break;
} }
@ -379,7 +408,9 @@ class RoomListStore extends Store {
} }
}); });
// We use this cache in the recents comparator because _tsOfNewestEvent can take a while // We use this cache in the recents comparator because _tsOfNewestEvent can take a while. This
// cache only needs to survive the sort operation below and should not be implemented outside
// of this function, otherwise the room lists will almost certainly be out of date and wrong.
const latestEventTsCache = {}; // roomId => timestamp const latestEventTsCache = {}; // roomId => timestamp
Object.keys(lists).forEach((listKey) => { Object.keys(lists).forEach((listKey) => {