mirror of
https://github.com/element-hq/element-web
synced 2024-11-29 12:58:53 +03:00
Merge pull request #2733 from matrix-org/travis/misc-roomlist
Misc room list improvements & invite fix
This commit is contained in:
commit
784f468d94
1 changed files with 119 additions and 77 deletions
|
@ -133,6 +133,8 @@ class RoomListStore extends Store {
|
||||||
const logicallyReady = this._matrixClient && this._state.ready;
|
const logicallyReady = this._matrixClient && this._state.ready;
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'setting_updated': {
|
case 'setting_updated': {
|
||||||
|
if (!logicallyReady) break;
|
||||||
|
|
||||||
if (payload.settingName === 'RoomList.orderByImportance') {
|
if (payload.settingName === 'RoomList.orderByImportance') {
|
||||||
this.updateSortingAlgorithm(payload.newValue === true ? ALGO_IMPORTANCE : ALGO_RECENT);
|
this.updateSortingAlgorithm(payload.newValue === true ? ALGO_IMPORTANCE : ALGO_RECENT);
|
||||||
} else if (payload.settingName === 'feature_custom_tags') {
|
} else if (payload.settingName === 'feature_custom_tags') {
|
||||||
|
@ -147,6 +149,10 @@ class RoomListStore extends Store {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
this._setState({tagsEnabled: SettingsStore.isFeatureEnabled("feature_custom_tags")});
|
this._setState({tagsEnabled: SettingsStore.isFeatureEnabled("feature_custom_tags")});
|
||||||
|
|
||||||
this._matrixClient = payload.matrixClient;
|
this._matrixClient = payload.matrixClient;
|
||||||
|
@ -326,21 +332,119 @@ class RoomListStore extends Store {
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_slotRoomIntoList(room, category, existingEntries, newList, lastTimestampFn) {
|
||||||
|
const targetCategoryIndex = CATEGORY_ORDER.indexOf(category);
|
||||||
|
|
||||||
|
// The slotting algorithm works by trying to position the room in the most relevant
|
||||||
|
// category of the list (red > grey > etc). To accomplish this, we need to consider
|
||||||
|
// a couple cases: the category existing in the list but having other rooms in it and
|
||||||
|
// the case of the category simply not existing and needing to be started. In order to
|
||||||
|
// do this efficiently, we only want to iterate over the list once and solve our sorting
|
||||||
|
// problem as we go.
|
||||||
|
//
|
||||||
|
// Firstly, we'll remove any existing entry that references the room we're trying to
|
||||||
|
// insert. We don't really want to consider the old entry and want to recreate it. We
|
||||||
|
// also exclude the sticky (currently active) room from the categorization logic and
|
||||||
|
// let it pass through wherever it resides in the list: it shouldn't be moving around
|
||||||
|
// the list too much, so we want to keep it where it is.
|
||||||
|
//
|
||||||
|
// The case of the category we want existing is easy to handle: once we hit the category,
|
||||||
|
// find the room that has a most recent event later than our own and insert just before
|
||||||
|
// that (making us the more recent room). If we end up hitting the next category before
|
||||||
|
// we can slot the room in, insert the room at the top of the category as a fallback. We
|
||||||
|
// do this to ensure that the room doesn't go too far down the list given it was previously
|
||||||
|
// considered important (in the case of going down in category) or is now more important
|
||||||
|
// (suddenly becoming red, for instance). The boundary tracking is how we end up achieving
|
||||||
|
// this, as described in the next paragraphs.
|
||||||
|
//
|
||||||
|
// The other case of the category not already existing is a bit more complicated. We track
|
||||||
|
// the boundaries of each category relative to the list we're currently building so that
|
||||||
|
// when we miss the category we can insert the room at the right spot. Most importantly, we
|
||||||
|
// can't assume that the end of the list being built is the right spot because of the last
|
||||||
|
// paragraph's requirement: the room should be put to the top of a category if the category
|
||||||
|
// runs out of places to put it.
|
||||||
|
//
|
||||||
|
// All told, our tracking looks something like this:
|
||||||
|
//
|
||||||
|
// ------ A <- Category boundary (start of red)
|
||||||
|
// RED
|
||||||
|
// RED
|
||||||
|
// RED
|
||||||
|
// ------ B <- In this example, we have a grey room we want to insert.
|
||||||
|
// BOLD
|
||||||
|
// BOLD
|
||||||
|
// ------ C
|
||||||
|
// IDLE
|
||||||
|
// IDLE
|
||||||
|
// ------ D <- End of list
|
||||||
|
//
|
||||||
|
// Given that example, and our desire to insert a GREY room into the list, this iterates
|
||||||
|
// over the room list until it realizes that BOLD comes after GREY and we're no longer
|
||||||
|
// in the RED section. Because there's no rooms there, we simply insert there which is
|
||||||
|
// also a "category boundary". If we change the example to wanting to insert a BOLD room
|
||||||
|
// which can't be ordered by timestamp with the existing couple rooms, we would still make
|
||||||
|
// use of the boundary flag to insert at B before changing the boundary indicator to C.
|
||||||
|
|
||||||
|
let desiredCategoryBoundaryIndex = 0;
|
||||||
|
let foundBoundary = false;
|
||||||
|
let pushedEntry = false;
|
||||||
|
|
||||||
|
for (const entry of existingEntries) {
|
||||||
|
// We insert our own record as needed, so don't let the old one through.
|
||||||
|
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 && !pushedEntry) {
|
||||||
|
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 = newList.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 && lastTimestampFn(room) >= lastTimestampFn(entry.room))) {
|
||||||
|
if (changedBoundary) {
|
||||||
|
// If we changed a boundary, then we've gone too far - go to the top of the last
|
||||||
|
// section instead.
|
||||||
|
newList.splice(desiredCategoryBoundaryIndex, 0, {room, category});
|
||||||
|
} else {
|
||||||
|
// If we're ordering by timestamp, just insert normally
|
||||||
|
newList.push({room, category});
|
||||||
|
}
|
||||||
|
pushedEntry = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall through and clone the list.
|
||||||
|
newList.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pushedEntry;
|
||||||
|
}
|
||||||
|
|
||||||
_setRoomCategory(room, category) {
|
_setRoomCategory(room, category) {
|
||||||
if (!room) return; // This should only happen in tests
|
if (!room) return; // This should only happen in tests
|
||||||
|
|
||||||
const listsClone = {};
|
const listsClone = {};
|
||||||
const targetCategoryIndex = CATEGORY_ORDER.indexOf(category);
|
|
||||||
|
|
||||||
// Micro optimization: Support lazily loading the last timestamp in a room
|
// Micro optimization: Support lazily loading the last timestamp in a room
|
||||||
let _targetTimestamp = null;
|
const timestampCache = {}; // {roomId => ts}
|
||||||
const targetTimestamp = () => {
|
const lastTimestamp = (room) => {
|
||||||
if (_targetTimestamp === null) {
|
if (!timestampCache[room.roomId]) {
|
||||||
_targetTimestamp = this._tsOfNewestEvent(room);
|
timestampCache[room.roomId] = this._tsOfNewestEvent(room);
|
||||||
}
|
}
|
||||||
return _targetTimestamp;
|
return timestampCache[room.roomId];
|
||||||
};
|
};
|
||||||
|
|
||||||
const targetTags = this._getRecommendedTagsForRoom(room);
|
const targetTags = this._getRecommendedTagsForRoom(room);
|
||||||
const insertedIntoTags = [];
|
const insertedIntoTags = [];
|
||||||
|
|
||||||
|
@ -369,74 +473,21 @@ class RoomListStore extends Store {
|
||||||
} else {
|
} else {
|
||||||
listsClone[key] = [];
|
listsClone[key] = [];
|
||||||
|
|
||||||
// We track where the boundary within listsClone[key] is just in case our timestamp
|
const pushedEntry = this._slotRoomIntoList(
|
||||||
// ordering fails. If we can't stick the room in at the correct place in the category
|
room, category, this._state.lists[key], listsClone[key], lastTimestamp);
|
||||||
// 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]) {
|
|
||||||
// We insert our own record as needed, so don't let the old one through.
|
|
||||||
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 (!pushedEntry && shouldHaveRoom) {
|
|
||||||
// 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
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall through and clone the list.
|
|
||||||
listsClone[key].push(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pushedEntry) {
|
if (!pushedEntry) {
|
||||||
if (listsClone[key].length === 0) {
|
// Special case invites: they don't really have timelines and can easily get lost when
|
||||||
|
// the user has multiple pending invites. Pushing them is the least worst option.
|
||||||
|
if (listsClone[key].length === 0 || key === "im.vector.fake.invite") {
|
||||||
listsClone[key].push({room, category});
|
listsClone[key].push({room, category});
|
||||||
insertedIntoTags.push(key);
|
insertedIntoTags.push(key);
|
||||||
} else {
|
} else {
|
||||||
// In theory, this should never happen
|
// In theory, this should never happen
|
||||||
console.warn(`!! Room ${room.roomId} lost: No position available`);
|
console.warn(`!! Room ${room.roomId} lost: No position available`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
insertedIntoTags.push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -480,15 +531,6 @@ class RoomListStore extends Store {
|
||||||
|
|
||||||
const dmRoomMap = DMRoomMap.shared();
|
const dmRoomMap = DMRoomMap.shared();
|
||||||
|
|
||||||
// Speed optimization: Hitting the SettingsStore is expensive, so avoid that at all costs.
|
|
||||||
let _isCustomTagsEnabled = null;
|
|
||||||
const isCustomTagsEnabled = () => {
|
|
||||||
if (_isCustomTagsEnabled === null) {
|
|
||||||
_isCustomTagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags");
|
|
||||||
}
|
|
||||||
return _isCustomTagsEnabled;
|
|
||||||
};
|
|
||||||
|
|
||||||
this._matrixClient.getRooms().forEach((room) => {
|
this._matrixClient.getRooms().forEach((room) => {
|
||||||
const myUserId = this._matrixClient.getUserId();
|
const myUserId = this._matrixClient.getUserId();
|
||||||
const membership = room.getMyMembership();
|
const membership = room.getMyMembership();
|
||||||
|
@ -504,7 +546,7 @@ class RoomListStore extends Store {
|
||||||
tagNames = tagNames.filter((t) => {
|
tagNames = tagNames.filter((t) => {
|
||||||
// Speed optimization: Avoid hitting the SettingsStore at all costs by making it the
|
// Speed optimization: Avoid hitting the SettingsStore at all costs by making it the
|
||||||
// last condition possible.
|
// last condition possible.
|
||||||
return lists[t] !== undefined || (!t.startsWith('m.') && isCustomTagsEnabled());
|
return lists[t] !== undefined || (!t.startsWith('m.') && this._state.tagsEnabled);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (tagNames.length) {
|
if (tagNames.length) {
|
||||||
|
|
Loading…
Reference in a new issue