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.
This commit is contained in:
Travis Ralston 2019-02-24 19:44:47 -07:00
parent c12bea06c8
commit 5f760fbf4a

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,127 +285,116 @@ 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] = [];
}
// We track where the boundary within listsClone[key] is just in case our timestamp // Tags sorted by recents are more complicated than manually ordered tags, so hope
// ordering fails. If we can't stick the room in at the correct place in the category // for the best.
// grouping based on timestamp, we'll stick it at the top of the group which will be // TODO: Use the SettingsStore watcher to determine if tags are enabled or not
// the index we track here. if (LIST_ORDERS[key] !== 'recent') {
let desiredCategoryBoundaryIndex = 0; // TODO: Actually insert the room
let foundBoundary = false; } else {
let pushedEntry = false; // 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 foundBoundary = 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) {
if (entry.room.roomId !== this._state.stickyRoomId) { continue;
if (!pushedEntry && doInsert && (targetTags.length === 0 || targetTags.includes(key))) {
// Micro optimization: Support lazily loading the last timestamp in a room
let _entryTimestamp = null;
const entryTimestamp = () => {
if (_entryTimestamp === null) {
_entryTimestamp = this._tsOfNewestEvent(entry.room);
}
return _entryTimestamp;
};
const entryCategoryIndex = CATEGORY_ORDER.indexOf(entry.category);
// As per above, check if we're meeting that boundary we wanted to locate.
if (entryCategoryIndex >= targetCategoryIndex && !foundBoundary) {
desiredCategoryBoundaryIndex = listsClone[key].length - 1;
foundBoundary = true;
} }
// If we've hit the top of a boundary beyond our target category, insert at the top of // if the list is a recent list, and the room appears in this list, and we're
// the grouping to ensure the room isn't slotted incorrectly. Otherwise, try to insert // not looking at a sticky room (sticky rooms have unreliable categories), try
// based on most recent timestamp. // to slot the new room in
const changedBoundary = entryCategoryIndex > targetCategoryIndex; if (entry.room.roomId !== this._state.stickyRoomId) {
const currentCategory = entryCategoryIndex === targetCategoryIndex; if (!pushedEntry && shouldHaveRoom) {
if (changedBoundary || (currentCategory && targetTimestamp() >= entryTimestamp())) { // Micro optimization: Support lazily loading the last timestamp in a room
if (changedBoundary) { let _entryTimestamp = null;
// If we changed a boundary, then we've gone too far - go to the top of the last const entryTimestamp = () => {
// section instead. if (_entryTimestamp === null) {
listsClone[key].splice(desiredCategoryBoundaryIndex, 0, {room, category}); _entryTimestamp = this._tsOfNewestEvent(entry.room);
} else { }
// If we're ordering by timestamp, just insert normally return _entryTimestamp;
listsClone[key].push({room, category}); };
const entryCategoryIndex = CATEGORY_ORDER.indexOf(entry.category);
// As per above, check if we're meeting that boundary we wanted to locate.
if (entryCategoryIndex >= targetCategoryIndex && !foundBoundary) {
desiredCategoryBoundaryIndex = listsClone[key].length - 1;
foundBoundary = true;
}
// If we've hit the top of a boundary beyond our target category, insert at the top of
// the grouping to ensure the room isn't slotted incorrectly. Otherwise, try to insert
// based on most recent timestamp.
const changedBoundary = entryCategoryIndex > targetCategoryIndex;
const currentCategory = entryCategoryIndex === targetCategoryIndex;
if (changedBoundary || (currentCategory && targetTimestamp() >= entryTimestamp())) {
if (changedBoundary) {
// If we changed a boundary, then we've gone too far - go to the top of the last
// section instead.
listsClone[key].splice(desiredCategoryBoundaryIndex, 0, {room, category});
} else {
// If we're ordering by timestamp, just insert normally
listsClone[key].push({room, category});
}
pushedEntry = true;
insertedIntoTags.push(key);
}
} }
pushedEntry = true;
inserted = true;
} }
// Fall through and clone the list.
listsClone[key].push(entry);
} }
// We insert our own record as needed, so don't let the old one through. if (!pushedEntry) {
if (entry.room.roomId === room.roomId) { if (listsClone[key].length === 0) {
continue; listsClone[key].push({room, category});
insertedIntoTags.push(key);
} else {
// In theory, this should never happen
console.warn(`!! Room ${room.roomId} lost: No position available`);
}
} }
} }
// Fall through and clone the list.
listsClone[key].push(entry);
} }
} }
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++) { if (count !== 1) {
// Just find the top of our category grouping and insert it there. console.warn(`!! Room ${room.roomId} inserted ${count} times`);
const catIdxAtPosition = CATEGORY_ORDER.indexOf(listsClone[tag][i].category);
if (catIdxAtPosition >= targetCategoryIndex) {
listsClone[tag].splice(i, 0, {room: room, category: category});
break;
}
}
} }
} }
@ -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);
}; };
} }