diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 8909dbb489..2b70d53b59 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -54,6 +54,24 @@ class RoomListStore extends Store { "im.vector.fake.archived": [], }, ready: false, + + // The room cache stores a mapping of roomId to cache record. + // Each cache record is a key/value pair for various bits of + // data used to sort the room list. Currently this stores the + // following bits of informations: + // "timestamp": number, The timestamp of the last relevant + // event in the room. + // "notifications": boolean, Whether or not the user has been + // highlighted on any unread events. + // "unread": boolean, Whether or not the user has any + // unread events. + // + // All of the cached values are lazily loaded on read in the + // recents comparator. When an event is received for a particular + // room, all the cached values are invalidated - forcing the + // next read to set new values. The entries do not expire on + // their own. + roomCache: {}, }; } @@ -85,6 +103,8 @@ class RoomListStore extends Store { !payload.isLiveUnfilteredRoomTimelineEvent || !this._eventTriggersRecentReorder(payload.event) ) break; + + this._clearCachedRoomState(payload.event.getRoomId()); this._generateRoomLists(); } break; @@ -112,6 +132,8 @@ class RoomListStore extends Store { if (liveTimeline !== eventTimeline || !this._eventTriggersRecentReorder(payload.event) ) break; + + this._clearCachedRoomState(payload.event.getRoomId()); this._generateRoomLists(); } break; @@ -217,11 +239,18 @@ class RoomListStore extends Store { } }); + // Note: we check the settings up here instead of in the forEach or + // in the _recentsComparator to avoid hitting the SettingsStore a few + // thousand times. + const pinUnread = SettingsStore.getValue("pinUnreadRooms"); + const pinMentioned = SettingsStore.getValue("pinMentionedRooms"); Object.keys(lists).forEach((listKey) => { let comparator; switch (RoomListStore._listOrders[listKey]) { case "recent": - comparator = this._recentsComparator; + comparator = (roomA, roomB) => { + return this._recentsComparator(roomA, roomB, pinUnread, pinMentioned); + }; break; case "manual": default: @@ -237,6 +266,44 @@ class RoomListStore extends Store { }); } + _updateCachedRoomState(roomId, type, value) { + const roomCache = this._state.roomCache; + if (!roomCache[roomId]) roomCache[roomId] = {}; + + if (value) roomCache[roomId][type] = value; + else delete roomCache[roomId][type]; + + this._setState({roomCache}); + } + + _clearCachedRoomState(roomId) { + const roomCache = this._state.roomCache; + delete roomCache[roomId]; + this._setState({roomCache}); + } + + _getRoomState(room, type) { + const roomId = room.roomId; + const roomCache = this._state.roomCache; + if (roomCache[roomId] && typeof roomCache[roomId][type] !== 'undefined') { + return roomCache[roomId][type]; + } + + if (type === "timestamp") { + const ts = this._tsOfNewestEvent(room); + this._updateCachedRoomState(roomId, "timestamp", ts); + return ts; + } else if (type === "unread") { + const unread = room.getUnreadNotificationCount() > 0; + this._updateCachedRoomState(roomId, "unread", unread); + return unread; + } else if (type === "notifications") { + const notifs = room.getUnreadNotificationCount("highlight") > 0; + this._updateCachedRoomState(roomId, "notifications", notifs); + return notifs; + } else throw new Error("Unrecognized room cache type: " + type); + } + _eventTriggersRecentReorder(ev) { return ev.getTs() && ( Unread.eventTriggersUnreadCount(ev) || @@ -262,34 +329,40 @@ class RoomListStore extends Store { } } - _recentsComparator(roomA, roomB) { - const pinUnread = SettingsStore.getValue("pinUnreadRooms"); - const pinMentioned = SettingsStore.getValue("pinMentionedRooms"); - + _recentsComparator(roomA, roomB, pinUnread, pinMentioned) { // We try and set the ordering to be Mentioned > Unread > Recent - // assuming the user has the right settings, of course + // assuming the user has the right settings, of course. + + const timestampA = this._getRoomState(roomA, "timestamp"); + const timestampB = this._getRoomState(roomB, "timestamp"); + const timestampDiff = timestampB - timestampA; if (pinMentioned) { - const mentionsA = roomA.getUnreadNotificationCount("highlight") > 0; - const mentionsB = roomB.getUnreadNotificationCount("highlight") > 0; - if (mentionsA && !mentionsB) return -1; - if (!mentionsA && mentionsB) return 1; - if (mentionsA && mentionsB) return 0; - // If neither have mentions, fall through to remaining checks + const mentionsA = this._getRoomState(roomA, "notifications"); + const mentionsB = this._getRoomState(roomB, "notifications"); + if (mentionsA && !mentionsB) return -1; + if (!mentionsA && mentionsB) return 1; + + // If they both have notifications, sort by timestamp. + // If neither have notifications (the fourth check not shown + // here), then try and sort by unread messages and finally by + // timestamp. + if (mentionsA && mentionsB) return timestampDiff; } if (pinUnread) { - const unreadA = Unread.doesRoomHaveUnreadMessages(roomA); - const unreadB = Unread.doesRoomHaveUnreadMessages(roomB); + const unreadA = this._getRoomState(roomA, "unread"); + const unreadB = this._getRoomState(roomB, "unread"); if (unreadA && !unreadB) return -1; if (!unreadA && unreadB) return 1; - if (unreadA && unreadB) return 0; - // If neither have unread messages, fall through to remaining checks + + // If they both have unread messages, sort by timestamp + // If nether have unread message (the fourth check not shown + // here), then just sort by timestamp anyways. + if (unreadA && unreadB) return timestampDiff; } - // XXX: We could use a cache here and update it when we see new - // events that trigger a reorder - return this._tsOfNewestEvent(roomB) - this._tsOfNewestEvent(roomA); + return timestampDiff; } _lexicographicalComparator(roomA, roomB) {