Change the room list algo to eagerly delete and carefully insert

Previously it made some complicated assumptions about the contexts it was called in (it generally assumed it just had to shuffle rooms between tags and didn't really handle new rooms very well).

The algorithm now eagerly tries to drop rooms from tags and carefully inserts them. The actual insertion logic is mostly untouched: the only part changed is how it handles failure to insert into tag. It shouldn't be possible for this algorithm to completely skip a room unless the tag is empty, so we special case that.

There are several TODO comments to be addressed here. Namely, it doesn't handle manually ordered tags (favourites, custom, etc) and doesn't check if tags are even enabled. Changes in this area are waiting for https://github.com/matrix-org/matrix-react-sdk/pull/2686 to land to take advantage of monitoring the settings flag for tags.
pull/21833/head
Travis Ralston 2019-02-24 19:44:47 -07:00
parent c12bea06c8
commit 5f760fbf4a
1 changed files with 120 additions and 106 deletions

View File

@ -180,7 +180,7 @@ class RoomListStore extends Store {
break; break;
case 'MatrixActions.Room.myMembership': { case 'MatrixActions.Room.myMembership': {
if (!logicallyReady) break; if (!logicallyReady) break;
this._roomUpdateTriggered(payload.room.roomId); this._roomUpdateTriggered(payload.room.roomId, true);
} }
break; break;
// This could be a new room that we've been invited to, joined or created // This could be a new room that we've been invited to, joined or created
@ -188,7 +188,7 @@ class RoomListStore extends Store {
// a member. // a member.
case 'MatrixActions.Room': { case 'MatrixActions.Room': {
if (!logicallyReady) break; if (!logicallyReady) break;
this._roomUpdateTriggered(payload.room.roomId); this._roomUpdateTriggered(payload.room.roomId, true);
} }
break; break;
// TODO: Re-enable optimistic updates when we support dragging again // TODO: Re-enable optimistic updates when we support dragging again
@ -230,12 +230,12 @@ class RoomListStore extends Store {
} }
} }
_roomUpdateTriggered(roomId) { _roomUpdateTriggered(roomId, ignoreSticky) {
// We don't calculate categories for sticky rooms because we have a moderate // 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 // 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 // being artificially flagged as IDLE. Also, this reduces the amount of time
// we spend in _setRoomCategory ever so slightly. // we spend in _setRoomCategory ever so slightly.
if (this._state.stickyRoomId !== roomId) { if (this._state.stickyRoomId !== roomId || ignoreSticky) {
// Micro optimization: Only look up the room if we're confident we'll need it. // Micro optimization: Only look up the room if we're confident we'll need it.
const room = this._matrixClient.getRoom(roomId); const room = this._matrixClient.getRoom(roomId);
if (!room) return; if (!room) return;
@ -245,6 +245,31 @@ class RoomListStore extends Store {
} }
} }
_getRecommendedTagsForRoom(room) {
const tags = [];
const myMembership = room.getMyMembership();
if (myMembership === 'join' || myMembership === 'invite') {
// Stack the user's tags on top
// TODO: Consider whether tags are enabled at all
tags.push(...(room.tags || []));
const dmRoomMap = DMRoomMap.shared();
if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
tags.push("im.vector.fake.direct");
} else if (myMembership === 'invite') {
tags.push("im.vector.fake.invite");
} else if (tags.length === 0) {
tags.push("im.vector.fake.recent");
}
} else {
tags.push("im.vector.fake.archived");
}
return tags;
}
_setRoomCategory(room, category) { _setRoomCategory(room, category) {
if (!room) return; // This should only happen in tests if (!room) return; // This should only happen in tests
@ -260,43 +285,34 @@ class RoomListStore extends Store {
return _targetTimestamp; return _targetTimestamp;
}; };
const myMembership = room.getMyMembership(); const targetTags = this._getRecommendedTagsForRoom(room);
let doInsert = true; const insertedIntoTags = [];
const targetTags = [];
if (myMembership !== "join" && myMembership !== "invite") {
doInsert = false;
} else {
const dmRoomMap = DMRoomMap.shared();
if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
targetTags.push('im.vector.fake.direct');
} else {
targetTags.push('im.vector.fake.recent');
}
}
// We need to update all instances of a room to ensure that they are correctly organized // We need to make sure all the tags (lists) are updated with the room's new position. We
// in the list. We do this by shallow-cloning the entire `lists` object using a single // generally only get called here when there's a new room to insert or a room has potentially
// iterator. Within the loop, we also rebuild the list of rooms per tag (key) so that the // changed positions within the list.
// updated room gets slotted into the right spot. This sacrifices code clarity for not //
// iterating on potentially large collections multiple times. // We do all our checks by iterating over the rooms in the existing lists, trying to insert
// our room where we can. As a guiding principle, we should be removing the room from all
// tags, and insert the room into targetTags. We should perform the deletion before the addition
// where possible to keep a consistent state. By the end of this, targetTags should be the
// same as insertedIntoTags.
let inserted = false;
for (const key of Object.keys(this._state.lists)) { for (const key of Object.keys(this._state.lists)) {
const hasRoom = this._state.lists[key].some((e) => e.room.roomId === room.roomId); const shouldHaveRoom = targetTags.includes(key);
// Speed optimization: Skip the loop below if we're not going to do anything productive // Speed optimization: Don't do complicated math if we don't have to.
if (!hasRoom || LIST_ORDERS[key] !== 'recent') { if (!shouldHaveRoom) {
listsClone[key] = this._state.lists[key]; listsClone[key] = this._state.lists[key].filter((e) => e.room.roomId !== room.roomId);
if (LIST_ORDERS[key] !== 'recent' && (hasRoom || targetTags.includes(key))) {
// Ensure that we don't try and sort the room into the tag
inserted = true;
doInsert = false;
}
continue;
} else { } else {
listsClone[key] = []; listsClone[key] = [];
}
// Tags sorted by recents are more complicated than manually ordered tags, so hope
// for the best.
// TODO: Use the SettingsStore watcher to determine if tags are enabled or not
if (LIST_ORDERS[key] !== 'recent') {
// TODO: Actually insert the room
} else {
// We track where the boundary within listsClone[key] is just in case our timestamp // 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 // 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 // grouping based on timestamp, we'll stick it at the top of the group which will be
@ -306,10 +322,16 @@ class RoomListStore extends Store {
let pushedEntry = false; let pushedEntry = 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 // We insert our own record as needed, so don't let the old one through.
// room (sticky rooms have unreliable categories), try to slot the new room in if (entry.room.roomId === room.roomId) {
continue;
}
// 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
if (entry.room.roomId !== this._state.stickyRoomId) { if (entry.room.roomId !== this._state.stickyRoomId) {
if (!pushedEntry && doInsert && (targetTags.length === 0 || targetTags.includes(key))) { if (!pushedEntry && shouldHaveRoom) {
// Micro optimization: Support lazily loading the last timestamp in a room // Micro optimization: Support lazily loading the last timestamp in a room
let _entryTimestamp = null; let _entryTimestamp = null;
const entryTimestamp = () => { const entryTimestamp = () => {
@ -342,45 +364,37 @@ class RoomListStore extends Store {
listsClone[key].push({room, category}); listsClone[key].push({room, category});
} }
pushedEntry = true; pushedEntry = true;
inserted = true; insertedIntoTags.push(key);
} }
} }
// We insert our own record as needed, so don't let the old one through.
if (entry.room.roomId === room.roomId) {
continue;
}
} }
// Fall through and clone the list. // Fall through and clone the list.
listsClone[key].push(entry); listsClone[key].push(entry);
} }
if (!pushedEntry) {
if (listsClone[key].length === 0) {
listsClone[key].push({room, category});
insertedIntoTags.push(key);
} else {
// In theory, this should never happen
console.warn(`!! Room ${room.roomId} lost: No position available`);
}
}
}
}
} }
if (!inserted) { // Double check that we inserted the room in the right places
// There's a good chance that we just joined the room, so we need to organize it for (const targetTag of targetTags) {
// We also could have left it... let count = 0;
let tags = []; for (const insertedTag of insertedIntoTags) {
if (doInsert) { if (insertedTag === targetTag) count++;
tags = Object.keys(room.tags);
if (tags.length === 0) {
tags = targetTags;
}
if (tags.length === 0) {
tags = [myMembership === 'join' ? 'im.vector.fake.recent' : 'im.vector.fake.invite'];
}
} else {
tags = ['im.vector.fake.archived'];
}
for (const tag of tags) {
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);
if (catIdxAtPosition >= targetCategoryIndex) {
listsClone[tag].splice(i, 0, {room: room, category: category});
break;
}
} }
if (count !== 1) {
console.warn(`!! Room ${room.roomId} inserted ${count} times`);
} }
} }
@ -574,7 +588,7 @@ class RoomListStore extends Store {
return -1; return -1;
} }
return a === b ? this._lexicographicalComparator(roomA, roomB) : ( a > b ? 1 : -1); return a === b ? this._lexicographicalComparator(roomA, roomB) : (a > b ? 1 : -1);
}; };
} }