From 7b33d14277c3a3ca3db9bf1415c1a7372645b8e1 Mon Sep 17 00:00:00 2001
From: Nicolas Werner <nicolas.werner@hotmail.de>
Date: Sat, 16 Jul 2022 03:07:00 +0200
Subject: [PATCH] Make notification count calculation more efficient

---
 resources/qml/CommunitiesList.qml | 60 +++++++++-------------
 src/Utils.cpp                     | 18 -------
 src/Utils.h                       |  5 --
 src/timeline/CommunitiesModel.cpp | 84 ++++++++++++++++++++++++-------
 src/timeline/RoomlistModel.cpp    | 17 ++++---
 src/timeline/RoomlistModel.h      |  8 +--
 6 files changed, 103 insertions(+), 89 deletions(-)

diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml
index ec9ef9405..ca63bffd6 100644
--- a/resources/qml/CommunitiesList.qml
+++ b/resources/qml/CommunitiesList.qml
@@ -71,30 +71,20 @@ Page {
             property color unimportantText: Nheko.colors.buttonText
             property color bubbleBackground: Nheko.colors.highlight
             property color bubbleText: Nheko.colors.highlightedText
-            required property string avatarUrl
-            required property string displayName
-            required property string tooltip
-            required property bool collapsed
-            required property bool collapsible
-            required property bool hidden
-            required property int depth
-            required property string id
-            required property int unreadMessages
-            required property bool hasLoudNotification
-            required property bool muted
+            required property var model
 
             height: avatarSize + 2 * Nheko.paddingMedium
             width: ListView.view.width
             state: "normal"
             ToolTip.visible: hovered && collapsed
-            ToolTip.text: communityItem.tooltip
+            ToolTip.text: model.tooltip
             ToolTip.delay: Nheko.tooltipDelay
-            onClicked: Communities.setCurrentTagId(communityItem.id)
-            onPressAndHold: communityContextMenu.show(communityItem.id, communityItem.hidden, communityItem.muted)
+            onClicked: Communities.setCurrentTagId(model.id)
+            onPressAndHold: communityContextMenu.show(model.id, model.hidden, model.muted)
             states: [
                 State {
                     name: "highlight"
-                    when: (communityItem.hovered || communityItem.hidden) && !(Communities.currentTagId === communityItem.id)
+                    when: (communityItem.hovered || model.hidden) && !(Communities.currentTagId === model.id)
 
                     PropertyChanges {
                         target: communityItem
@@ -108,7 +98,7 @@ Page {
                 },
                 State {
                     name: "selected"
-                    when: Communities.currentTagId == communityItem.id
+                    when: Communities.currentTagId == model.id
 
                     PropertyChanges {
                         target: communityItem
@@ -127,7 +117,7 @@ Page {
 
                 TapHandler {
                     acceptedButtons: Qt.RightButton
-                    onSingleTapped: communityContextMenu.show(communityItem.id, communityItem.hidden, communityItem.muted)
+                    onSingleTapped: communityContextMenu.show(model.id, model.hidden, model.muted)
                     gesturePolicy: TapHandler.ReleaseWithinBounds
                     acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
                 }
@@ -139,27 +129,27 @@ Page {
                 spacing: Nheko.paddingMedium
                 anchors.fill: parent
                 anchors.margins: Nheko.paddingMedium
-                anchors.leftMargin: Nheko.paddingMedium + (communitySidebar.collapsed ? 0 : (fontMetrics.lineSpacing * communityItem.depth))
+                anchors.leftMargin: Nheko.paddingMedium + (communitySidebar.collapsed ? 0 : (fontMetrics.lineSpacing * model.depth))
 
                 ImageButton {
-                    visible: !communitySidebar.collapsed && communityItem.collapsible
+                    visible: !communitySidebar.collapsed && model.collapsible
                     Layout.preferredHeight: fontMetrics.lineSpacing
                     Layout.preferredWidth: fontMetrics.lineSpacing
                     Layout.alignment: Qt.AlignVCenter
                     height: fontMetrics.lineSpacing
                     width: fontMetrics.lineSpacing
-                    image: communityItem.collapsed ? ":/icons/icons/ui/collapsed.svg" : ":/icons/icons/ui/expanded.svg"
+                    image: model.collapsed ? ":/icons/icons/ui/collapsed.svg" : ":/icons/icons/ui/expanded.svg"
                     ToolTip.visible: hovered
                     ToolTip.delay: Nheko.tooltipDelay
-                    ToolTip.text: communityItem.collapsed ? qsTr("Expand") : qsTr("Collapse")
+                    ToolTip.text: model.collapsed ? qsTr("Expand") : qsTr("Collapse")
                     hoverEnabled: true
 
-                    onClicked: communityItem.collapsed = !communityItem.collapsed
+                    onClicked: model.collapsed = !model.collapsed
                 }
 
                 Item {
                     Layout.preferredWidth: fontMetrics.lineSpacing
-                    visible: !communitySidebar.collapsed && !communityItem.collapsible && Communities.containsSubspaces
+                    visible: !communitySidebar.collapsed && !model.collapsible && Communities.containsSubspaces
                 }
 
                 Avatar {
@@ -170,22 +160,22 @@ Page {
                     height: avatarSize
                     width: avatarSize
                     url: {
-                        if (communityItem.avatarUrl.startsWith("mxc://"))
-                            return communityItem.avatarUrl.replace("mxc://", "image://MxcImage/");
+                        if (model.avatarUrl.startsWith("mxc://"))
+                            return model.avatarUrl.replace("mxc://", "image://MxcImage/");
                         else
-                            return "image://colorimage/" + communityItem.avatarUrl + "?" + communityItem.unimportantText;
+                            return "image://colorimage/" + model.avatarUrl + "?" + communityItem.unimportantText;
                     }
-                    roomid: communityItem.id
-                    displayName: communityItem.displayName
+                    roomid: model.id
+                    displayName: model.displayName
                     color: communityItem.backgroundColor
 
                     NotificationBubble {
-                        notificationCount: communityItem.unreadMessages
-                        hasLoudNotification: communityItem.hasLoudNotification
+                        notificationCount: model.unreadMessages
+                        hasLoudNotification: model.hasLoudNotification
                         bubbleBackgroundColor: communityItem.bubbleBackground
                         bubbleTextColor: communityItem.bubbleText
                         font.pixelSize: fontMetrics.font.pixelSize * 0.6
-                        mayBeVisible: communitySidebar.collapsed && !communityItem.muted && Settings.spaceNotifications
+                        mayBeVisible: communitySidebar.collapsed && !model.muted && Settings.spaceNotifications
                         anchors.right: avatar.right
                         anchors.bottom: avatar.bottom
                         anchors.margins: -Nheko.paddingSmall
@@ -199,7 +189,7 @@ Page {
                     color: communityItem.importantText
                     Layout.fillWidth: true
                     elideWidth: width
-                    fullText: communityItem.displayName
+                    fullText: model.displayName
                     textFormat: Text.PlainText
                 }
 
@@ -208,11 +198,11 @@ Page {
                 }
 
                 NotificationBubble {
-                    notificationCount: communityItem.unreadMessages
-                    hasLoudNotification: communityItem.hasLoudNotification
+                    notificationCount: model.unreadMessages
+                    hasLoudNotification: model.hasLoudNotification
                     bubbleBackgroundColor: communityItem.bubbleBackground
                     bubbleTextColor: communityItem.bubbleText
-                    mayBeVisible: !communitySidebar.collapsed && !communityItem.muted && Settings.spaceNotifications
+                    mayBeVisible: !communitySidebar.collapsed && !model.muted && Settings.spaceNotifications
                     Layout.alignment: Qt.AlignRight
                     Layout.leftMargin: Nheko.paddingSmall
                 }
diff --git a/src/Utils.cpp b/src/Utils.cpp
index 3a90bd505..d98669e08 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -881,21 +881,3 @@ utils::markRoomAsDirect(QString roomid, std::vector<RoomMember> members)
           });
       });
 }
-
-QPair<int, int>
-utils::getChildNotificationsForSpace(const QString &spaceId)
-{
-    auto children = cache::getRoomInfo(cache::client()->getChildRoomIds(spaceId.toStdString()));
-    QPair<int, int> retVal;
-    for (const auto &[childId, child] : children) {
-        if (child.is_space) {
-            auto temp{utils::getChildNotificationsForSpace(childId)};
-            retVal.first += temp.first;
-            retVal.second += temp.second;
-        } else {
-            retVal.first += child.notification_count;
-            retVal.second += child.highlight_count;
-        }
-    }
-    return retVal;
-}
diff --git a/src/Utils.h b/src/Utils.h
index bdd56d55f..0b6034acf 100644
--- a/src/Utils.h
+++ b/src/Utils.h
@@ -311,9 +311,4 @@ removeDirectFromRoom(QString roomid);
 
 void
 markRoomAsDirect(QString roomid, std::vector<RoomMember> members);
-
-//! Returns a pair of integers representing the unread notifications in a space and how many of them
-//! are loud notifications, respectively.
-QPair<int, int>
-getChildNotificationsForSpace(const QString &spaceId);
 }
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
index 0d47c64de..c75f4265b 100644
--- a/src/timeline/CommunitiesModel.cpp
+++ b/src/timeline/CommunitiesModel.cpp
@@ -208,9 +208,15 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
         case CommunitiesModel::Roles::Id:
             return "tag:" + tag;
         case CommunitiesModel::Roles::UnreadMessages:
-            return (int)tagNotificationCache.at(tag).notification_count;
+            if (auto it = tagNotificationCache.find(tag); it != tagNotificationCache.end())
+                return (int)it->second.notification_count;
+            else
+                return 0;
         case CommunitiesModel::Roles::HasLoudNotification:
-            return (int)tagNotificationCache.at(tag).highlight_count > 0;
+            if (auto it = tagNotificationCache.find(tag); it != tagNotificationCache.end())
+                return it->second.highlight_count > 0;
+            else
+                return 0;
         }
     }
     return QVariant();
@@ -265,6 +271,21 @@ CommunitiesModel::initializeSidebar()
     tags_.clear();
     spaceOrder_.tree.clear();
     spaces_.clear();
+    tagNotificationCache.clear();
+    globalUnreads.notification_count = {};
+    dmUnreads.notification_count     = {};
+
+    auto e = cache::client()->getAccountData(mtx::events::EventType::Direct);
+    if (e) {
+        if (auto event =
+              std::get_if<mtx::events::AccountDataEvent<mtx::events::account_data::Direct>>(
+                &e.value())) {
+            directMessages_.clear();
+            for (const auto &[userId, roomIds] : event->content.user_to_rooms)
+                for (const auto &roomId : roomIds)
+                    directMessages_.push_back(roomId);
+        }
+    }
 
     std::set<std::string> ts;
 
@@ -284,6 +305,19 @@ CommunitiesModel::initializeSidebar()
                 }
             }
         }
+
+        for (const auto &t : it->tags) {
+            auto tagId = QString::fromStdString(t);
+            auto &tNs  = tagNotificationCache[tagId];
+            tNs.notification_count += it->notification_count;
+            tNs.highlight_count += it->highlight_count;
+        }
+
+        auto &e              = roomNotificationCache[it.key()];
+        e.highlight_count    = it->highlight_count;
+        e.notification_count = it->notification_count;
+        globalUnreads.notification_count += it->notification_count;
+        globalUnreads.highlight_count += it->highlight_count;
     }
 
     // NOTE(Nico): We build a forrest from the Directed Cyclic(!) Graph of spaces. To do that we
@@ -319,6 +353,14 @@ CommunitiesModel::initializeSidebar()
 
     spaceOrder_.restoreCollapsed();
 
+    for (auto &space : spaceOrder_.tree) {
+        for (const auto &c : cache::client()->getChildRoomIds(space.id.toStdString())) {
+            const auto &counts = roomNotificationCache[QString::fromStdString(c)];
+            space.notificationCounts.highlight_count += counts.highlight_count;
+            space.notificationCounts.notification_count += counts.notification_count;
+        }
+    }
+
     endResetModel();
 
     emit tagsChanged();
@@ -413,16 +455,21 @@ CommunitiesModel::sync(const mtx::responses::Sync &sync_)
                 tagsUpdated = true;
             }
 
-        auto roomId           = QString::fromStdString(roomid);
-        auto oldUnreads       = roomNotificationCache[roomId];
-        int notificationCDiff = -static_cast<int64_t>(oldUnreads.highlight_count) +
-                                static_cast<int64_t>(room.unread_notifications.highlight_count);
-        int highlightCDiff = -static_cast<int64_t>(oldUnreads.highlight_count) +
-                             static_cast<int64_t>(room.unread_notifications.highlight_count);
+        auto roomId            = QString::fromStdString(roomid);
+        auto &oldUnreads       = roomNotificationCache[roomId];
+        auto notificationCDiff = -static_cast<int64_t>(oldUnreads.notification_count) +
+                                 static_cast<int64_t>(room.unread_notifications.notification_count);
+        auto highlightCDiff = -static_cast<int64_t>(oldUnreads.highlight_count) +
+                              static_cast<int64_t>(room.unread_notifications.highlight_count);
+
+        auto applyDiff = [notificationCDiff,
+                          highlightCDiff](mtx::responses::UnreadNotifications &n) {
+            n.highlight_count    = static_cast<int64_t>(n.highlight_count) + highlightCDiff;
+            n.notification_count = static_cast<int64_t>(n.notification_count) + notificationCDiff;
+        };
         if (highlightCDiff || notificationCDiff) {
             // bool hidden = hiddenTagIds_.contains(roomId);
-            globalUnreads.notification_count += notificationCDiff;
-            globalUnreads.highlight_count += highlightCDiff;
+            applyDiff(globalUnreads);
             emit dataChanged(index(0),
                              index(0),
                              {
@@ -431,8 +478,7 @@ CommunitiesModel::sync(const mtx::responses::Sync &sync_)
                              });
             if (std::find(begin(directMessages_), end(directMessages_), roomid) !=
                 end(directMessages_)) {
-                dmUnreads.notification_count += notificationCDiff;
-                dmUnreads.highlight_count += highlightCDiff;
+                applyDiff(dmUnreads);
                 emit dataChanged(index(1),
                                  index(1),
                                  {
@@ -446,11 +492,8 @@ CommunitiesModel::sync(const mtx::responses::Sync &sync_)
 
             for (const auto &t : tags) {
                 auto tagId = QString::fromStdString(t);
-                auto &tNs  = tagNotificationCache[tagId];
-                tNs.notification_count += notificationCDiff;
-                tNs.highlight_count += highlightCDiff;
+                applyDiff(tagNotificationCache[tagId]);
                 int idx = tags_.indexOf(tagId) + 2 + spaceOrder_.size();
-                ;
                 emit dataChanged(index(idx),
                                  index(idx),
                                  {
@@ -463,8 +506,10 @@ CommunitiesModel::sync(const mtx::responses::Sync &sync_)
                 auto spaceId = QString::fromStdString(s);
 
                 for (int i = 0; i < spaceOrder_.size(); i++) {
-                    spaceOrder_.tree[i].notificationCounts.notification_count += notificationCDiff;
-                    spaceOrder_.tree[i].notificationCounts.highlight_count += highlightCDiff;
+                    if (spaceOrder_.tree[i].id != spaceId)
+                        continue;
+
+                    applyDiff(spaceOrder_.tree[i].notificationCounts);
 
                     int idx = i;
                     do {
@@ -474,10 +519,13 @@ CommunitiesModel::sync(const mtx::responses::Sync &sync_)
                                            UnreadMessages,
                                            HasLoudNotification,
                                          });
+                        idx = spaceOrder_.parent(idx);
                     } while (idx != -1);
                 }
             }
         }
+
+        roomNotificationCache[roomId] = room.unread_notifications;
     }
     for (const auto &[roomid, room] : sync_.rooms.leave) {
         (void)room;
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 3b46c0535..1869d2e0c 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -642,15 +642,18 @@ RoomlistModel::clear()
 }
 
 void
-RoomlistModel::joinPreview(QString roomid, QString parentSpace)
+RoomlistModel::joinPreview(QString roomid)
 {
     if (previewedRooms.contains(roomid)) {
-        auto child = cache::client()->getStateEvent<mtx::events::state::space::Child>(
-          parentSpace.toStdString(), roomid.toStdString());
-        ChatPage::instance()->joinRoomVia(
-          roomid.toStdString(),
-          (child && child->content.via) ? child->content.via.value() : std::vector<std::string>{},
-          false);
+        std::vector<std::string> vias;
+        auto parents = cache::client()->getParentRoomIds(roomid.toStdString());
+        for (const auto &p : parents) {
+            auto child = cache::client()->getStateEvent<mtx::events::state::space::Child>(
+              p, roomid.toStdString());
+            if (child && child->content.via)
+                vias.insert(vias.end(), child->content.via->begin(), child->content.via->end());
+        }
+        ChatPage::instance()->joinRoomVia(roomid.toStdString(), vias, false);
     }
 }
 void
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index cf2b45d85..61bf2e7cf 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -105,7 +105,7 @@ public slots:
 
         return -1;
     }
-    void joinPreview(QString roomid, QString parentSpace);
+    void joinPreview(QString roomid);
     void acceptInvite(QString roomid);
     void declineInvite(QString roomid);
     void leave(QString roomid, QString reason = "");
@@ -169,11 +169,7 @@ public slots:
     {
         return mapFromSource(roomlistmodel->index(roomlistmodel->roomidToIndex(roomid))).row();
     }
-    void joinPreview(QString roomid)
-    {
-        roomlistmodel->joinPreview(roomid,
-                                   filterType == FilterBy::Space ? filterStr : QLatin1String(""));
-    }
+    void joinPreview(QString roomid) { roomlistmodel->joinPreview(roomid); }
     void acceptInvite(QString roomid) { roomlistmodel->acceptInvite(roomid); }
     void declineInvite(QString roomid) { roomlistmodel->declineInvite(roomid); }
     void leave(QString roomid, QString reason = "") { roomlistmodel->leave(roomid, reason); }
-- 
GitLab