summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--resources/qml/CommunitiesList.qml48
-rw-r--r--resources/qml/RoomList.qml85
-rw-r--r--resources/qml/components/NotificationBubble.qml46
-rw-r--r--resources/res.qrc1
-rw-r--r--src/UserSettingsPage.cpp53
-rw-r--r--src/UserSettingsPage.h16
-rw-r--r--src/Utils.cpp1
-rw-r--r--src/timeline/CommunitiesModel.cpp231
-rw-r--r--src/timeline/CommunitiesModel.h27
-rw-r--r--src/timeline/RoomlistModel.cpp22
-rw-r--r--src/timeline/RoomlistModel.h8
-rw-r--r--src/timeline/TimelineModel.cpp4
-rw-r--r--src/timeline/TimelineModel.h3
13 files changed, 421 insertions, 124 deletions
diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml
index 61287789..ca63bffd 100644
--- a/resources/qml/CommunitiesList.qml
+++ b/resources/qml/CommunitiesList.qml
@@ -3,6 +3,7 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
+import "./components"
 import "./dialogs"
 import Qt.labs.platform 1.1 as Platform
 import QtQml 2.12
@@ -36,14 +37,27 @@ Page {
             id: communityContextMenu
 
             property string tagId
+            property bool hidden
+            property bool muted
 
-            function show(id_, tags_) {
+            function show(id_, hidden_, muted_) {
                 tagId = id_;
+                hidden = hidden_;
+                muted = muted_;
                 open();
             }
 
             Platform.MenuItem {
+                text: qsTr("Do not show notification counts for this space or tag.")
+                checkable: true
+                checked: communityContextMenu.muted
+                onTriggered: Communities.toggleTagMute(communityContextMenu.tagId)
+            }
+
+            Platform.MenuItem {
                 text: qsTr("Hide rooms with this tag or from this space by default.")
+                checkable: true
+                checked: communityContextMenu.hidden
                 onTriggered: Communities.toggleTagId(communityContextMenu.tagId)
             }
 
@@ -57,6 +71,7 @@ Page {
             property color unimportantText: Nheko.colors.buttonText
             property color bubbleBackground: Nheko.colors.highlight
             property color bubbleText: Nheko.colors.highlightedText
+            required property var model
 
             height: avatarSize + 2 * Nheko.paddingMedium
             width: ListView.view.width
@@ -65,11 +80,11 @@ Page {
             ToolTip.text: model.tooltip
             ToolTip.delay: Nheko.tooltipDelay
             onClicked: Communities.setCurrentTagId(model.id)
-            onPressAndHold: communityContextMenu.show(model.id)
+            onPressAndHold: communityContextMenu.show(model.id, model.hidden, model.muted)
             states: [
                 State {
                     name: "highlight"
-                    when: (communityItem.hovered || model.hidden) && !(Communities.currentTagId == model.id)
+                    when: (communityItem.hovered || model.hidden) && !(Communities.currentTagId === model.id)
 
                     PropertyChanges {
                         target: communityItem
@@ -102,7 +117,7 @@ Page {
 
                 TapHandler {
                     acceptedButtons: Qt.RightButton
-                    onSingleTapped: communityContextMenu.show(model.id)
+                    onSingleTapped: communityContextMenu.show(model.id, model.hidden, model.muted)
                     gesturePolicy: TapHandler.ReleaseWithinBounds
                     acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
                 }
@@ -153,6 +168,19 @@ Page {
                     roomid: model.id
                     displayName: model.displayName
                     color: communityItem.backgroundColor
+
+                    NotificationBubble {
+                        notificationCount: model.unreadMessages
+                        hasLoudNotification: model.hasLoudNotification
+                        bubbleBackgroundColor: communityItem.bubbleBackground
+                        bubbleTextColor: communityItem.bubbleText
+                        font.pixelSize: fontMetrics.font.pixelSize * 0.6
+                        mayBeVisible: communitySidebar.collapsed && !model.muted && Settings.spaceNotifications
+                        anchors.right: avatar.right
+                        anchors.bottom: avatar.bottom
+                        anchors.margins: -Nheko.paddingSmall
+                    }
+
                 }
 
                 ElidedLabel {
@@ -169,10 +197,20 @@ Page {
                     Layout.fillWidth: true
                 }
 
+                NotificationBubble {
+                    notificationCount: model.unreadMessages
+                    hasLoudNotification: model.hasLoudNotification
+                    bubbleBackgroundColor: communityItem.bubbleBackground
+                    bubbleTextColor: communityItem.bubbleText
+                    mayBeVisible: !communitySidebar.collapsed && !model.muted && Settings.spaceNotifications
+                    Layout.alignment: Qt.AlignRight
+                    Layout.leftMargin: Nheko.paddingSmall
+                }
+
             }
 
             background: Rectangle {
-                color: backgroundColor
+                color: communityItem.backgroundColor
             }
 
         }
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index a86ca725..1e61b68b 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -3,6 +3,7 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
+import "./components"
 import "./dialogs"
 import "./ui"
 import Qt.labs.platform 1.1 as Platform
@@ -294,9 +295,6 @@ Page {
                 anchors.margins: Nheko.paddingMedium
 
                 Avatar {
-                    // In the future we could show an online indicator by setting the userid for the avatar
-                    //userid: Nheko.currentUser.userid
-
                     id: avatar
 
                     enabled: false
@@ -308,33 +306,17 @@ Page {
                     userid: isDirect ? directChatOtherUserId : ""
                     roomid: roomId
 
-                    Rectangle {
+                    NotificationBubble {
                         id: collapsedNotificationBubble
 
+                        notificationCount: roomItem.notificationCount
+                        hasLoudNotification: roomItem.hasLoudNotification
+                        bubbleBackgroundColor: roomItem.bubbleBackground
+                        bubbleTextColor: roomItem.bubbleText
                         anchors.right: parent.right
                         anchors.bottom: parent.bottom
                         anchors.margins: -Nheko.paddingSmall
-                        visible: collapsed && notificationCount > 0
-                        enabled: false
-                        Layout.alignment: Qt.AlignRight
-                        height: fontMetrics.averageCharacterWidth * 3
-                        width: Math.max(collapsedBubbleText.width, height)
-                        radius: height / 2
-                        color: hasLoudNotification ? Nheko.theme.red : roomItem.bubbleBackground
-
-                        Label {
-                            id: collapsedBubbleText
-
-                            anchors.centerIn: parent
-                            horizontalAlignment: Text.AlignHCenter
-                            verticalAlignment: Text.AlignVCenter
-                            width: Math.max(implicitWidth + Nheko.paddingMedium, parent.height)
-                            font.bold: true
-                            font.pixelSize: fontMetrics.font.pixelSize * 0.8
-                            color: hasLoudNotification ? "white" : roomItem.bubbleText
-                            text: notificationCount > 9999 ? "9999+" : notificationCount
-                        }
-
+                        mayBeVisible: collapsed && (isSpace ? Settings.spaceNotifications : true)
                     }
 
                 }
@@ -351,7 +333,24 @@ Page {
                     height: avatar.height
                     spacing: Nheko.paddingSmall
 
+                    NotificationBubble {
+                        id: notificationBubble
+
+                        parent: isSpace ? titleRow : subtextRow
+                        notificationCount: roomItem.notificationCount
+                        hasLoudNotification: roomItem.hasLoudNotification
+                        bubbleBackgroundColor: roomItem.bubbleBackground
+                        bubbleTextColor: roomItem.bubbleText
+                        Layout.alignment: Qt.AlignRight
+                        Layout.leftMargin: Nheko.paddingSmall
+                        Layout.preferredWidth: implicitWidth
+                        Layout.preferredHeight: implicitHeight
+                        mayBeVisible: !collapsed && (isSpace ? Settings.spaceNotifications : true)
+                    }
+
                     RowLayout {
+                        id: titleRow
+
                         Layout.alignment: Qt.AlignTop
                         Layout.fillWidth: true
                         spacing: Nheko.paddingSmall
@@ -380,6 +379,8 @@ Page {
                     }
 
                     RowLayout {
+                        id: subtextRow
+
                         Layout.fillWidth: true
                         spacing: 0
                         visible: !isSpace
@@ -395,40 +396,6 @@ Page {
                             Layout.fillWidth: true
                         }
 
-                        Rectangle {
-                            id: notificationBubble
-
-                            visible: notificationCount > 0
-                            Layout.alignment: Qt.AlignRight
-                            Layout.leftMargin: Nheko.paddingSmall
-                            height: notificationBubbleText.height + Nheko.paddingMedium
-                            Layout.preferredWidth: Math.max(notificationBubbleText.width, height)
-                            radius: height / 2
-                            color: hasLoudNotification ? Nheko.theme.red : roomItem.bubbleBackground
-                            ToolTip.text: notificationCount
-                            ToolTip.delay: Nheko.tooltipDelay
-                            ToolTip.visible: notificationBubbleHover.hovered && (notificationCount > 9999)
-
-                            Label {
-                                id: notificationBubbleText
-
-                                anchors.centerIn: parent
-                                horizontalAlignment: Text.AlignHCenter
-                                verticalAlignment: Text.AlignVCenter
-                                width: Math.max(implicitWidth + Nheko.paddingMedium, parent.height)
-                                font.bold: true
-                                font.pixelSize: fontMetrics.font.pixelSize * 0.8
-                                color: hasLoudNotification ? "white" : roomItem.bubbleText
-                                text: notificationCount > 9999 ? "9999+" : notificationCount
-
-                                HoverHandler {
-                                    id: notificationBubbleHover
-                                }
-
-                            }
-
-                        }
-
                     }
 
                 }
diff --git a/resources/qml/components/NotificationBubble.qml b/resources/qml/components/NotificationBubble.qml
new file mode 100644
index 00000000..ca0ae6cb
--- /dev/null
+++ b/resources/qml/components/NotificationBubble.qml
@@ -0,0 +1,46 @@
+// SPDX-FileCopyrightText: 2022 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import im.nheko 1.0
+
+Rectangle {
+    id: bubbleRoot
+
+    required property int notificationCount
+    required property bool hasLoudNotification
+    required property color bubbleBackgroundColor
+    required property color bubbleTextColor
+    property bool mayBeVisible: true
+    property alias font: notificationBubbleText.font
+
+    visible: mayBeVisible && notificationCount > 0
+    implicitHeight: notificationBubbleText.height + Nheko.paddingMedium
+    implicitWidth: Math.max(notificationBubbleText.width, height)
+    radius: height / 2
+    color: hasLoudNotification ? Nheko.theme.red : bubbleBackgroundColor
+    ToolTip.text: notificationCount
+    ToolTip.delay: Nheko.tooltipDelay
+    ToolTip.visible: notificationBubbleHover.hovered && (notificationCount > 9999)
+
+    Label {
+        id: notificationBubbleText
+
+        anchors.centerIn: bubbleRoot
+        horizontalAlignment: Text.AlignHCenter
+        verticalAlignment: Text.AlignVCenter
+        width: Math.max(implicitWidth + Nheko.paddingMedium, bubbleRoot.height)
+        font.bold: true
+        font.pixelSize: fontMetrics.font.pixelSize * 0.8
+        color: bubbleRoot.hasLoudNotification ? "white" : bubbleRoot.bubbleTextColor
+        text: bubbleRoot.notificationCount > 9999 ? "9999+" : bubbleRoot.notificationCount
+
+        HoverHandler {
+            id: notificationBubbleHover
+        }
+
+    }
+
+}
diff --git a/resources/res.qrc b/resources/res.qrc
index 3ec24238..7f08c29d 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -185,6 +185,7 @@
         <file>qml/voip/PlaceCall.qml</file>
         <file>qml/voip/ScreenShare.qml</file>
         <file>qml/voip/VideoCall.qml</file>
+        <file>qml/components/NotificationBubble.qml</file>
     </qresource>
     <qresource prefix="/media">
         <file>media/ring.ogg</file>
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 666a03b4..b850d2e5 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -88,7 +88,8 @@ UserSettings::load(std::optional<QString> profile)
     openImageExternal_ = settings.value(QStringLiteral("user/open_image_external"), false).toBool();
     openVideoExternal_ = settings.value(QStringLiteral("user/open_video_external"), false).toBool();
     decryptSidebar_    = settings.value(QStringLiteral("user/decrypt_sidebar"), true).toBool();
-    privacyScreen_     = settings.value(QStringLiteral("user/privacy_screen"), false).toBool();
+    spaceNotifications_ = settings.value(QStringLiteral("user/space_notifications"), true).toBool();
+    privacyScreen_      = settings.value(QStringLiteral("user/privacy_screen"), false).toBool();
     privacyScreenTimeout_ =
       settings.value(QStringLiteral("user/privacy_screen_timeout"), 0).toInt();
     exposeDBusApi_ = settings.value(QStringLiteral("user/expose_dbus_api"), false).toBool();
@@ -132,7 +133,8 @@ UserSettings::load(std::optional<QString> profile)
     userId_        = settings.value(prefix + "auth/user_id", "").toString();
     deviceId_      = settings.value(prefix + "auth/device_id", "").toString();
     hiddenTags_    = settings.value(prefix + "user/hidden_tags", QStringList{}).toStringList();
-    hiddenPins_    = settings.value(prefix + "user/hidden_pins", QStringList{}).toStringList();
+    mutedTags_  = settings.value(prefix + "user/muted_tags", QStringList{"global"}).toStringList();
+    hiddenPins_ = settings.value(prefix + "user/hidden_pins", QStringList{}).toStringList();
     hiddenWidgets_ = settings.value(prefix + "user/hidden_widgets", QStringList{}).toStringList();
     recentReactions_ =
       settings.value(prefix + "user/recent_reactions", QStringList{}).toStringList();
@@ -220,14 +222,21 @@ UserSettings::setGroupView(bool state)
 }
 
 void
-UserSettings::setHiddenTags(QStringList hiddenTags)
+UserSettings::setHiddenTags(const QStringList &hiddenTags)
 {
     hiddenTags_ = hiddenTags;
     save();
 }
 
 void
-UserSettings::setHiddenPins(QStringList hiddenTags)
+UserSettings::setMutedTags(const QStringList &mutedTags)
+{
+    mutedTags_ = mutedTags;
+    save();
+}
+
+void
+UserSettings::setHiddenPins(const QStringList &hiddenTags)
 {
     hiddenPins_ = hiddenTags;
     save();
@@ -235,7 +244,7 @@ UserSettings::setHiddenPins(QStringList hiddenTags)
 }
 
 void
-UserSettings::setHiddenWidgets(QStringList hiddenTags)
+UserSettings::setHiddenWidgets(const QStringList &hiddenTags)
 {
     hiddenWidgets_ = hiddenTags;
     save();
@@ -417,6 +426,16 @@ UserSettings::setDecryptSidebar(bool state)
 }
 
 void
+UserSettings::setSpaceNotifications(bool state)
+{
+    if (state == spaceNotifications_)
+        return;
+    spaceNotifications_ = state;
+    emit spaceNotificationsChanged(state);
+    save();
+}
+
+void
 UserSettings::setPrivacyScreen(bool state)
 {
     if (state == privacyScreen_) {
@@ -777,6 +796,7 @@ UserSettings::save()
 
     settings.setValue(QStringLiteral("avatar_circles"), avatarCircles_);
     settings.setValue(QStringLiteral("decrypt_sidebar"), decryptSidebar_);
+    settings.setValue(QStringLiteral("space_notifications"), spaceNotifications_);
     settings.setValue(QStringLiteral("privacy_screen"), privacyScreen_);
     settings.setValue(QStringLiteral("privacy_screen_timeout"), privacyScreenTimeout_);
     settings.setValue(QStringLiteral("mobile_mode"), mobileMode_);
@@ -830,6 +850,7 @@ UserSettings::save()
                       onlyShareKeysWithVerifiedUsers_);
     settings.setValue(prefix + "user/online_key_backup", useOnlineKeyBackup_);
     settings.setValue(prefix + "user/hidden_tags", hiddenTags_);
+    settings.setValue(prefix + "user/muted_tags", mutedTags_);
     settings.setValue(prefix + "user/hidden_pins", hiddenPins_);
     settings.setValue(prefix + "user/hidden_widgets", hiddenWidgets_);
     settings.setValue(prefix + "user/recent_reactions", recentReactions_);
@@ -923,6 +944,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
             return tr("Open videos with external program");
         case DecryptSidebar:
             return tr("Decrypt messages in sidebar");
+        case SpaceNotifications:
+            return tr("Show message counts for spaces");
         case PrivacyScreen:
             return tr("Privacy Screen");
         case PrivacyScreenTimeout:
@@ -1053,6 +1076,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
             return i->openVideoExternal();
         case DecryptSidebar:
             return i->decryptSidebar();
+        case SpaceNotifications:
+            return i->spaceNotifications();
         case PrivacyScreen:
             return i->privacyScreen();
         case PrivacyScreenTimeout:
@@ -1208,6 +1233,9 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
         case DecryptSidebar:
             return tr("Decrypt the messages shown in the sidebar.\nOnly affects messages in "
                       "encrypted chats.");
+        case SpaceNotifications:
+            return tr(
+              "Choose where to show the total number of notifications contained within a space.");
         case PrivacyScreen:
             return tr("When the window loses focus, the timeline will\nbe blurred.");
         case MobileMode:
@@ -1317,6 +1345,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
         case ShareKeysWithTrustedUsers:
         case UseOnlineKeyBackup:
         case ExposeDBusApi:
+        case SpaceNotifications:
             return Toggle;
         case Profile:
         case UserId:
@@ -1409,7 +1438,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
             return fontDb.families();
         case EmojiFont:
             return fontDb.families(QFontDatabase::WritingSystem::Symbol);
-        case Ringtone:
+        case Ringtone: {
             QStringList l{
               QStringLiteral("Mute"),
               QStringLiteral("Default"),
@@ -1419,6 +1448,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
                 l.push_back(i->ringtone());
             return l;
         }
+        }
     } else if (role == Good) {
         switch (index.row()) {
         case OnlineBackupKey:
@@ -1624,6 +1654,13 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int
                 return false;
         }
             return i->decryptSidebar();
+        case SpaceNotifications: {
+            if (value.userType() == QMetaType::Bool) {
+                i->setSpaceNotifications(value.toBool());
+                return true;
+            } else
+                return false;
+        }
         case PrivacyScreen: {
             if (value.userType() == QMetaType::Bool) {
                 i->setPrivacyScreen(value.toBool());
@@ -1936,7 +1973,9 @@ UserSettingsModel::UserSettingsModel(QObject *p)
     connect(s.get(), &UserSettings::decryptSidebarChanged, this, [this]() {
         emit dataChanged(index(DecryptSidebar), index(DecryptSidebar), {Value});
     });
-
+    connect(s.get(), &UserSettings::spaceNotificationsChanged, this, [this] {
+        emit dataChanged(index(SpaceNotifications), index(SpaceNotifications), {Value});
+    });
     connect(s.get(), &UserSettings::trayChanged, this, [this]() {
         emit dataChanged(index(Tray), index(Tray), {Value});
         emit dataChanged(index(StartInTray), index(StartInTray), {Enabled});
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index 1fb3ddcf..34a792eb 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -58,6 +58,8 @@ class UserSettings : public QObject
       bool avatarCircles READ avatarCircles WRITE setAvatarCircles NOTIFY avatarCirclesChanged)
     Q_PROPERTY(
       bool decryptSidebar READ decryptSidebar WRITE setDecryptSidebar NOTIFY decryptSidebarChanged)
+    Q_PROPERTY(bool spaceNotifications READ spaceNotifications WRITE setSpaceNotifications NOTIFY
+                 spaceNotificationsChanged)
     Q_PROPERTY(
       bool privacyScreen READ privacyScreen WRITE setPrivacyScreen NOTIFY privacyScreenChanged)
     Q_PROPERTY(int privacyScreenTimeout READ privacyScreenTimeout WRITE setPrivacyScreenTimeout
@@ -162,6 +164,7 @@ public:
     void setAlertOnNotification(bool state);
     void setAvatarCircles(bool state);
     void setDecryptSidebar(bool state);
+    void setSpaceNotifications(bool state);
     void setPrivacyScreen(bool state);
     void setPrivacyScreenTimeout(int state);
     void setPresence(Presence state);
@@ -184,9 +187,10 @@ public:
     void setDeviceId(QString deviceId);
     void setHomeserver(QString homeserver);
     void setDisableCertificateValidation(bool disabled);
-    void setHiddenTags(QStringList hiddenTags);
-    void setHiddenPins(QStringList hiddenTags);
-    void setHiddenWidgets(QStringList hiddenTags);
+    void setHiddenTags(const QStringList &hiddenTags);
+    void setMutedTags(const QStringList &mutedTags);
+    void setHiddenPins(const QStringList &hiddenTags);
+    void setHiddenWidgets(const QStringList &hiddenTags);
     void setRecentReactions(QStringList recent);
     void setUseIdenticon(bool state);
     void setOpenImageExternal(bool state);
@@ -202,6 +206,7 @@ public:
     bool groupView() const { return groupView_; }
     bool avatarCircles() const { return avatarCircles_; }
     bool decryptSidebar() const { return decryptSidebar_; }
+    bool spaceNotifications() const { return spaceNotifications_; }
     bool privacyScreen() const { return privacyScreen_; }
     int privacyScreenTimeout() const { return privacyScreenTimeout_; }
     bool markdown() const { return markdown_; }
@@ -250,6 +255,7 @@ public:
     QString homeserver() const { return homeserver_; }
     bool disableCertificateValidation() const { return disableCertificateValidation_; }
     QStringList hiddenTags() const { return hiddenTags_; }
+    QStringList mutedTags() const { return mutedTags_; }
     QStringList hiddenPins() const { return hiddenPins_; }
     QStringList hiddenWidgets() const { return hiddenWidgets_; }
     QStringList recentReactions() const { return recentReactions_; }
@@ -278,6 +284,7 @@ signals:
     void alertOnNotificationChanged(bool state);
     void avatarCirclesChanged(bool state);
     void decryptSidebarChanged(bool state);
+    void spaceNotificationsChanged(bool state);
     void privacyScreenChanged(bool state);
     void privacyScreenTimeoutChanged(int state);
     void timelineMaxWidthChanged(int state);
@@ -340,6 +347,7 @@ private:
     bool hasAlertOnNotification_;
     bool avatarCircles_;
     bool decryptSidebar_;
+    bool spaceNotifications_;
     bool privacyScreen_;
     int privacyScreenTimeout_;
     bool shareKeysWithTrustedUsers_;
@@ -370,6 +378,7 @@ private:
     QString deviceId_;
     QString homeserver_;
     QStringList hiddenTags_;
+    QStringList mutedTags_;
     QStringList hiddenPins_;
     QStringList hiddenWidgets_;
     QStringList recentReactions_;
@@ -424,6 +433,7 @@ class UserSettingsModel : public QAbstractListModel
         GroupView,
         SortByImportance,
         DecryptSidebar,
+        SpaceNotifications,
 
         TraySection,
         Tray,
diff --git a/src/Utils.cpp b/src/Utils.cpp
index bdc1a411..b85d7916 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -27,6 +27,7 @@
 #include <cmark.h>
 
 #include "Cache.h"
+#include "Cache_p.h"
 #include "Config.h"
 #include "EventAccessors.h"
 #include "Logging.h"
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
index 4f650f49..c75f4265 100644
--- a/src/timeline/CommunitiesModel.cpp
+++ b/src/timeline/CommunitiesModel.cpp
@@ -9,11 +9,15 @@
 
 #include "Cache.h"
 #include "Cache_p.h"
+#include "ChatPage.h"
 #include "Logging.h"
 #include "UserSettingsPage.h"
+#include "Utils.h"
 
 CommunitiesModel::CommunitiesModel(QObject *parent)
   : QAbstractListModel(parent)
+  , hiddenTagIds_{UserSettings::instance()->hiddenTags()}
+  , mutedTagIds_{UserSettings::instance()->mutedTags()}
 {}
 
 QHash<int, QByteArray>
@@ -28,6 +32,9 @@ CommunitiesModel::roleNames() const
       {Hidden, "hidden"},
       {Depth, "depth"},
       {Id, "id"},
+      {UnreadMessages, "unreadMessages"},
+      {HasLoudNotification, "hasLoudNotification"},
+      {Muted, "muted"},
     };
 }
 
@@ -50,6 +57,13 @@ CommunitiesModel::setData(const QModelIndex &index, const QVariant &value, int r
 QVariant
 CommunitiesModel::data(const QModelIndex &index, int role) const
 {
+    if (role == CommunitiesModel::Roles::Muted) {
+        if (index.row() == 0)
+            return mutedTagIds_.contains(QStringLiteral("global"));
+        else
+            return mutedTagIds_.contains(data(index, CommunitiesModel::Roles::Id).toString());
+    }
+
     if (index.row() == 0) {
         switch (role) {
         case CommunitiesModel::Roles::AvatarUrl:
@@ -70,6 +84,10 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
             return 0;
         case CommunitiesModel::Roles::Id:
             return "";
+        case CommunitiesModel::Roles::UnreadMessages:
+            return (int)globalUnreads.notification_count;
+        case CommunitiesModel::Roles::HasLoudNotification:
+            return globalUnreads.highlight_count > 0;
         }
     } else if (index.row() == 1) {
         switch (role) {
@@ -84,16 +102,20 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
         case CommunitiesModel::Roles::Collapsible:
             return false;
         case CommunitiesModel::Roles::Hidden:
-            return hiddentTagIds_.contains(QStringLiteral("dm"));
+            return hiddenTagIds_.contains(QStringLiteral("dm"));
         case CommunitiesModel::Roles::Parent:
             return "";
         case CommunitiesModel::Roles::Depth:
             return 0;
         case CommunitiesModel::Roles::Id:
             return "dm";
+        case CommunitiesModel::Roles::UnreadMessages:
+            return (int)dmUnreads.notification_count;
+        case CommunitiesModel::Roles::HasLoudNotification:
+            return dmUnreads.highlight_count > 0;
         }
     } else if (index.row() - 2 < spaceOrder_.size()) {
-        auto id = spaceOrder_.tree.at(index.row() - 2).name;
+        auto id = spaceOrder_.tree.at(index.row() - 2).id;
         switch (role) {
         case CommunitiesModel::Roles::AvatarUrl:
             return QString::fromStdString(spaces_.at(id).avatar_url);
@@ -107,10 +129,10 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
             return idx != spaceOrder_.lastChild(idx);
         }
         case CommunitiesModel::Roles::Hidden:
-            return hiddentTagIds_.contains("space:" + id);
+            return hiddenTagIds_.contains("space:" + id);
         case CommunitiesModel::Roles::Parent: {
             if (auto p = spaceOrder_.parent(index.row() - 2); p >= 0)
-                return spaceOrder_.tree[p].name;
+                return spaceOrder_.tree[p].id;
 
             return "";
         }
@@ -118,6 +140,20 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
             return spaceOrder_.tree.at(index.row() - 2).depth;
         case CommunitiesModel::Roles::Id:
             return "space:" + id;
+        case CommunitiesModel::Roles::UnreadMessages: {
+            int count = 0;
+            auto end  = spaceOrder_.lastChild(index.row() - 2);
+            for (int i = index.row() - 2; i <= end; i++)
+                count += spaceOrder_.tree[i].notificationCounts.notification_count;
+            return count;
+        }
+        case CommunitiesModel::Roles::HasLoudNotification: {
+            auto end = spaceOrder_.lastChild(index.row() - 2);
+            for (int i = index.row() - 2; i <= end; i++)
+                if (spaceOrder_.tree[i].notificationCounts.highlight_count > 0)
+                    return true;
+            return false;
+        }
         }
     } else if (index.row() - 2 < tags_.size() + spaceOrder_.size()) {
         auto tag = tags_.at(index.row() - 2 - spaceOrder_.size());
@@ -160,7 +196,7 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
 
         switch (role) {
         case CommunitiesModel::Roles::Hidden:
-            return hiddentTagIds_.contains("tag:" + tag);
+            return hiddenTagIds_.contains("tag:" + tag);
         case CommunitiesModel::Roles::Collapsed:
             return true;
         case CommunitiesModel::Roles::Collapsible:
@@ -171,6 +207,16 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
             return 0;
         case CommunitiesModel::Roles::Id:
             return "tag:" + tag;
+        case CommunitiesModel::Roles::UnreadMessages:
+            if (auto it = tagNotificationCache.find(tag); it != tagNotificationCache.end())
+                return (int)it->second.notification_count;
+            else
+                return 0;
+        case CommunitiesModel::Roles::HasLoudNotification:
+            if (auto it = tagNotificationCache.find(tag); it != tagNotificationCache.end())
+                return it->second.highlight_count > 0;
+            else
+                return 0;
         }
     }
     return QVariant();
@@ -225,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;
 
@@ -244,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
@@ -277,8 +351,16 @@ CommunitiesModel::initializeSidebar()
     for (const auto &t : ts)
         tags_.push_back(QString::fromStdString(t));
 
-    hiddentTagIds_ = UserSettings::instance()->hiddenTags();
     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();
@@ -298,12 +380,12 @@ CommunitiesModel::FlatTree::storeCollapsed()
 
     for (const auto &e : tree) {
         if (e.depth > depth) {
-            current.push_back(e.name);
+            current.push_back(e.id);
         } else if (e.depth == depth) {
-            current.back() = e.name;
+            current.back() = e.id;
         } else {
             current.pop_back();
-            current.back() = e.name;
+            current.back() = e.id;
         }
 
         if (e.collapsed)
@@ -323,12 +405,12 @@ CommunitiesModel::FlatTree::restoreCollapsed()
 
     for (auto &e : tree) {
         if (e.depth > depth) {
-            current.push_back(e.name);
+            current.push_back(e.id);
         } else if (e.depth == depth) {
-            current.back() = e.name;
+            current.back() = e.id;
         } else {
             current.pop_back();
-            current.back() = e.name;
+            current.back() = e.id;
         }
 
         if (elements.contains(current))
@@ -353,7 +435,6 @@ CommunitiesModel::sync(const mtx::responses::Sync &sync_)
     bool tagsUpdated = false;
 
     for (const auto &[roomid, room] : sync_.rooms.join) {
-        (void)roomid;
         for (const auto &e : room.account_data.events)
             if (std::holds_alternative<
                   mtx::events::AccountDataEvent<mtx::events::account_data::Tags>>(e)) {
@@ -373,6 +454,78 @@ CommunitiesModel::sync(const mtx::responses::Sync &sync_)
                   e)) {
                 tagsUpdated = true;
             }
+
+        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);
+            applyDiff(globalUnreads);
+            emit dataChanged(index(0),
+                             index(0),
+                             {
+                               UnreadMessages,
+                               HasLoudNotification,
+                             });
+            if (std::find(begin(directMessages_), end(directMessages_), roomid) !=
+                end(directMessages_)) {
+                applyDiff(dmUnreads);
+                emit dataChanged(index(1),
+                                 index(1),
+                                 {
+                                   UnreadMessages,
+                                   HasLoudNotification,
+                                 });
+            }
+
+            auto spaces = cache::client()->getParentRoomIds(roomid);
+            auto tags   = cache::singleRoomInfo(roomid).tags;
+
+            for (const auto &t : tags) {
+                auto tagId = QString::fromStdString(t);
+                applyDiff(tagNotificationCache[tagId]);
+                int idx = tags_.indexOf(tagId) + 2 + spaceOrder_.size();
+                emit dataChanged(index(idx),
+                                 index(idx),
+                                 {
+                                   UnreadMessages,
+                                   HasLoudNotification,
+                                 });
+            }
+
+            for (const auto &s : spaces) {
+                auto spaceId = QString::fromStdString(s);
+
+                for (int i = 0; i < spaceOrder_.size(); i++) {
+                    if (spaceOrder_.tree[i].id != spaceId)
+                        continue;
+
+                    applyDiff(spaceOrder_.tree[i].notificationCounts);
+
+                    int idx = i;
+                    do {
+                        emit dataChanged(index(idx + 2),
+                                         index(idx + 2),
+                                         {
+                                           UnreadMessages,
+                                           HasLoudNotification,
+                                         });
+                        idx = spaceOrder_.parent(idx);
+                    } while (idx != -1);
+                }
+            }
+        }
+
+        roomNotificationCache[roomId] = room.unread_notifications;
     }
     for (const auto &[roomid, room] : sync_.rooms.leave) {
         (void)room;
@@ -380,8 +533,12 @@ CommunitiesModel::sync(const mtx::responses::Sync &sync_)
             tagsUpdated = true;
     }
     for (const auto &e : sync_.account_data.events) {
-        if (std::holds_alternative<
-              mtx::events::AccountDataEvent<mtx::events::account_data::Direct>>(e)) {
+        if (auto event =
+              std::get_if<mtx::events::AccountDataEvent<mtx::events::account_data::Direct>>(&e)) {
+            directMessages_.clear();
+            for (const auto &[userId, roomIds] : event->content.user_to_rooms)
+                for (const auto &roomId : roomIds)
+                    directMessages_.push_back(roomId);
             tagsUpdated = true;
             break;
         }
@@ -392,7 +549,7 @@ CommunitiesModel::sync(const mtx::responses::Sync &sync_)
 }
 
 void
-CommunitiesModel::setCurrentTagId(QString tagId)
+CommunitiesModel::setCurrentTagId(const QString &tagId)
 {
     if (tagId.startsWith(QLatin1String("tag:"))) {
         auto tag = tagId.mid(4);
@@ -406,7 +563,7 @@ CommunitiesModel::setCurrentTagId(QString tagId)
     } else if (tagId.startsWith(QLatin1String("space:"))) {
         auto tag = tagId.mid(6);
         for (const auto &t : spaceOrder_.tree) {
-            if (t.name == tag) {
+            if (t.id == tag) {
                 this->currentTagId_ = tagId;
                 emit currentTagIdChanged(currentTagId_);
                 return;
@@ -425,13 +582,11 @@ CommunitiesModel::setCurrentTagId(QString tagId)
 void
 CommunitiesModel::toggleTagId(QString tagId)
 {
-    if (hiddentTagIds_.contains(tagId)) {
-        hiddentTagIds_.removeOne(tagId);
-        UserSettings::instance()->setHiddenTags(hiddentTagIds_);
-    } else {
-        hiddentTagIds_.push_back(tagId);
-        UserSettings::instance()->setHiddenTags(hiddentTagIds_);
-    }
+    if (hiddenTagIds_.contains(tagId))
+        hiddenTagIds_.removeOne(tagId);
+    else
+        hiddenTagIds_.push_back(tagId);
+    UserSettings::instance()->setHiddenTags(hiddenTagIds_);
 
     if (tagId.startsWith(QLatin1String("tag:"))) {
         auto idx = tags_.indexOf(tagId.mid(4));
@@ -449,6 +604,34 @@ CommunitiesModel::toggleTagId(QString tagId)
     emit hiddenTagsChanged();
 }
 
+void
+CommunitiesModel::toggleTagMute(QString tagId)
+{
+    if (tagId.isEmpty())
+        tagId = QStringLiteral("global");
+
+    if (mutedTagIds_.contains(tagId))
+        mutedTagIds_.removeOne(tagId);
+    else
+        mutedTagIds_.push_back(tagId);
+    UserSettings::instance()->setMutedTags(mutedTagIds_);
+
+    if (tagId.startsWith(QLatin1String("tag:"))) {
+        auto idx = tags_.indexOf(tagId.mid(4));
+        if (idx != -1)
+            emit dataChanged(index(idx + 1 + spaceOrder_.size()),
+                             index(idx + 1 + spaceOrder_.size()));
+    } else if (tagId.startsWith(QLatin1String("space:"))) {
+        auto idx = spaceOrder_.indexOf(tagId.mid(6));
+        if (idx != -1)
+            emit dataChanged(index(idx + 1), index(idx + 1));
+    } else if (tagId == QLatin1String("dm")) {
+        emit dataChanged(index(1), index(1));
+    } else if (tagId == QLatin1String("global")) {
+        emit dataChanged(index(0), index(0));
+    }
+}
+
 FilteredCommunitiesModel::FilteredCommunitiesModel(CommunitiesModel *model, QObject *parent)
   : QSortFilterProxyModel(parent)
 {
diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h
index 5da7d1bd..08269e21 100644
--- a/src/timeline/CommunitiesModel.h
+++ b/src/timeline/CommunitiesModel.h
@@ -22,7 +22,7 @@ class FilteredCommunitiesModel : public QSortFilterProxyModel
     Q_OBJECT
 
 public:
-    FilteredCommunitiesModel(CommunitiesModel *model, QObject *parent = nullptr);
+    explicit FilteredCommunitiesModel(CommunitiesModel *model, QObject *parent = nullptr);
     bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
     bool filterAcceptsRow(int sourceRow, const QModelIndex &) const override;
 };
@@ -48,14 +48,21 @@ public:
         Parent,
         Depth,
         Id,
+        UnreadMessages,
+        HasLoudNotification,
+        Muted,
+        IsDirect,
     };
 
     struct FlatTree
     {
         struct Elem
         {
-            QString name;
-            int depth      = 0;
+            QString id;
+            int depth = 0;
+
+            mtx::responses::UnreadNotifications notificationCounts = {0, 0};
+
             bool collapsed = false;
         };
 
@@ -65,7 +72,7 @@ public:
         int indexOf(const QString &s) const
         {
             for (int i = 0; i < size(); i++)
-                if (tree[i].name == s)
+                if (tree[i].id == s)
                     return i;
             return -1;
         }
@@ -121,7 +128,7 @@ public slots:
     void sync(const mtx::responses::Sync &sync_);
     void clear();
     QString currentTagId() const { return currentTagId_; }
-    void setCurrentTagId(QString tagId);
+    void setCurrentTagId(const QString &tagId);
     void resetCurrentTagId()
     {
         currentTagId_.clear();
@@ -138,6 +145,7 @@ public slots:
         return tagsWD;
     }
     void toggleTagId(QString tagId);
+    void toggleTagMute(QString tagId);
     FilteredCommunitiesModel *filtered() { return new FilteredCommunitiesModel(this, this); }
 
 signals:
@@ -149,9 +157,16 @@ signals:
 private:
     QStringList tags_;
     QString currentTagId_;
-    QStringList hiddentTagIds_;
+    QStringList hiddenTagIds_;
+    QStringList mutedTagIds_;
     FlatTree spaceOrder_;
     std::map<QString, RoomInfo> spaces_;
+    std::vector<std::string> directMessages_;
+
+    std::unordered_map<QString, mtx::responses::UnreadNotifications> roomNotificationCache;
+    std::unordered_map<QString, mtx::responses::UnreadNotifications> tagNotificationCache;
+    mtx::responses::UnreadNotifications globalUnreads{};
+    mtx::responses::UnreadNotifications dmUnreads{};
 
     friend class FilteredCommunitiesModel;
 };
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 1cf16243..1869d2e0 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -330,10 +330,13 @@ RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
                                Qt::DisplayRole,
                              });
 
+            if (getRoomById(room_id)->isSpace())
+                return; // no need to update space notifications
+
             int total_unread_msgs = 0;
 
             for (const auto &room : qAsConst(models)) {
-                if (!room.isNull())
+                if (!room.isNull() && !room->isSpace())
                     total_unread_msgs += room->notificationCount();
             }
 
@@ -639,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 cf2b45d8..61bf2e7c 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); }
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 9ada2afd..b21fb091 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -370,10 +370,6 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
     this->highlight_count    = roomInfo.highlight_count;
     lastMessage_.timestamp   = roomInfo.approximate_last_modification_ts;
 
-    // this connection will simplify adding the plainRoomNameChanged() signal everywhere that it
-    // needs to be
-    connect(this, &TimelineModel::roomNameChanged, this, &TimelineModel::plainRoomNameChanged);
-
     connect(
       this,
       &TimelineModel::redactionFailed,
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 6d424981..47fd27f1 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -182,7 +182,7 @@ class TimelineModel : public QAbstractListModel
       bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged)
     Q_PROPERTY(QString roomId READ roomId CONSTANT)
     Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
-    Q_PROPERTY(QString plainRoomName READ plainRoomName NOTIFY plainRoomNameChanged)
+    Q_PROPERTY(QString plainRoomName READ plainRoomName NOTIFY roomNameChanged)
     Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged)
     Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
     Q_PROPERTY(QStringList pinnedMessages READ pinnedMessages NOTIFY pinnedMessagesChanged)
@@ -429,7 +429,6 @@ signals:
     void encryptionChanged();
     void trustlevelChanged();
     void roomNameChanged();
-    void plainRoomNameChanged();
     void roomTopicChanged();
     void pinnedMessagesChanged();
     void widgetLinksChanged();