diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml index 61287789..ec9ef940 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,19 +71,30 @@ 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 height: avatarSize + 2 * Nheko.paddingMedium width: ListView.view.width state: "normal" ToolTip.visible: hovered && collapsed - ToolTip.text: model.tooltip + ToolTip.text: communityItem.tooltip ToolTip.delay: Nheko.tooltipDelay - onClicked: Communities.setCurrentTagId(model.id) - onPressAndHold: communityContextMenu.show(model.id) + onClicked: Communities.setCurrentTagId(communityItem.id) + onPressAndHold: communityContextMenu.show(communityItem.id, communityItem.hidden, communityItem.muted) states: [ State { name: "highlight" - when: (communityItem.hovered || model.hidden) && !(Communities.currentTagId == model.id) + when: (communityItem.hovered || communityItem.hidden) && !(Communities.currentTagId === communityItem.id) PropertyChanges { target: communityItem @@ -83,7 +108,7 @@ Page { }, State { name: "selected" - when: Communities.currentTagId == model.id + when: Communities.currentTagId == communityItem.id PropertyChanges { target: communityItem @@ -102,7 +127,7 @@ Page { TapHandler { acceptedButtons: Qt.RightButton - onSingleTapped: communityContextMenu.show(model.id) + onSingleTapped: communityContextMenu.show(communityItem.id, communityItem.hidden, communityItem.muted) gesturePolicy: TapHandler.ReleaseWithinBounds acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad } @@ -114,27 +139,27 @@ Page { spacing: Nheko.paddingMedium anchors.fill: parent anchors.margins: Nheko.paddingMedium - anchors.leftMargin: Nheko.paddingMedium + (communitySidebar.collapsed ? 0 : (fontMetrics.lineSpacing * model.depth)) + anchors.leftMargin: Nheko.paddingMedium + (communitySidebar.collapsed ? 0 : (fontMetrics.lineSpacing * communityItem.depth)) ImageButton { - visible: !communitySidebar.collapsed && model.collapsible + visible: !communitySidebar.collapsed && communityItem.collapsible Layout.preferredHeight: fontMetrics.lineSpacing Layout.preferredWidth: fontMetrics.lineSpacing Layout.alignment: Qt.AlignVCenter height: fontMetrics.lineSpacing width: fontMetrics.lineSpacing - image: model.collapsed ? ":/icons/icons/ui/collapsed.svg" : ":/icons/icons/ui/expanded.svg" + image: communityItem.collapsed ? ":/icons/icons/ui/collapsed.svg" : ":/icons/icons/ui/expanded.svg" ToolTip.visible: hovered ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: model.collapsed ? qsTr("Expand") : qsTr("Collapse") + ToolTip.text: communityItem.collapsed ? qsTr("Expand") : qsTr("Collapse") hoverEnabled: true - onClicked: model.collapsed = !model.collapsed + onClicked: communityItem.collapsed = !communityItem.collapsed } Item { Layout.preferredWidth: fontMetrics.lineSpacing - visible: !communitySidebar.collapsed && !model.collapsible && Communities.containsSubspaces + visible: !communitySidebar.collapsed && !communityItem.collapsible && Communities.containsSubspaces } Avatar { @@ -145,14 +170,27 @@ Page { height: avatarSize width: avatarSize url: { - if (model.avatarUrl.startsWith("mxc://")) - return model.avatarUrl.replace("mxc://", "image://MxcImage/"); + if (communityItem.avatarUrl.startsWith("mxc://")) + return communityItem.avatarUrl.replace("mxc://", "image://MxcImage/"); else - return "image://colorimage/" + model.avatarUrl + "?" + communityItem.unimportantText; + return "image://colorimage/" + communityItem.avatarUrl + "?" + communityItem.unimportantText; } - roomid: model.id - displayName: model.displayName + roomid: communityItem.id + displayName: communityItem.displayName color: communityItem.backgroundColor + + NotificationBubble { + notificationCount: communityItem.unreadMessages + hasLoudNotification: communityItem.hasLoudNotification + bubbleBackgroundColor: communityItem.bubbleBackground + bubbleTextColor: communityItem.bubbleText + font.pixelSize: fontMetrics.font.pixelSize * 0.6 + mayBeVisible: communitySidebar.collapsed && !communityItem.muted && Settings.spaceNotifications + anchors.right: avatar.right + anchors.bottom: avatar.bottom + anchors.margins: -Nheko.paddingSmall + } + } ElidedLabel { @@ -161,7 +199,7 @@ Page { color: communityItem.importantText Layout.fillWidth: true elideWidth: width - fullText: model.displayName + fullText: communityItem.displayName textFormat: Text.PlainText } @@ -169,10 +207,20 @@ Page { Layout.fillWidth: true } + NotificationBubble { + notificationCount: communityItem.unreadMessages + hasLoudNotification: communityItem.hasLoudNotification + bubbleBackgroundColor: communityItem.bubbleBackground + bubbleTextColor: communityItem.bubbleText + mayBeVisible: !communitySidebar.collapsed && !communityItem.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 @@ qml/voip/PlaceCall.qml qml/voip/ScreenShare.qml qml/voip/VideoCall.qml + qml/components/NotificationBubble.qml media/ring.ogg 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 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 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(); @@ -416,6 +425,16 @@ UserSettings::setDecryptSidebar(bool state) save(); } +void +UserSettings::setSpaceNotifications(bool state) +{ + if (state == spaceNotifications_) + return; + spaceNotifications_ = state; + emit spaceNotificationsChanged(state); + save(); +} + void UserSettings::setPrivacyScreen(bool state) { @@ -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 0ac37d8e..3a90bd50 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -27,6 +27,7 @@ #include #include "Cache.h" +#include "Cache_p.h" #include "Config.h" #include "EventAccessors.h" #include "Logging.h" @@ -880,3 +881,21 @@ utils::markRoomAsDirect(QString roomid, std::vector members) }); }); } + +QPair +utils::getChildNotificationsForSpace(const QString &spaceId) +{ + auto children = cache::getRoomInfo(cache::client()->getChildRoomIds(spaceId.toStdString())); + QPair 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 0b6034ac..bdd56d55 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -311,4 +311,9 @@ removeDirectFromRoom(QString roomid); void markRoomAsDirect(QString roomid, std::vector members); + +//! Returns a pair of integers representing the unread notifications in a space and how many of them +//! are loud notifications, respectively. +QPair +getChildNotificationsForSpace(const QString &spaceId); } diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp index 4f650f49..91f7d555 100644 --- a/src/timeline/CommunitiesModel.cpp +++ b/src/timeline/CommunitiesModel.cpp @@ -9,12 +9,24 @@ #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()} +{ + connect(ChatPage::instance(), &ChatPage::unreadMessages, this, [this](int) { + // Simply updating every space is easier than tracking which ones need updated. + if (!spaces_.empty()) + emit dataChanged(index(0, 0), + index(spaces_.size() + tags_.size() + 1, 0), + {Roles::UnreadMessages, Roles::HasLoudNotification}); + }); +} QHash CommunitiesModel::roleNames() const @@ -28,6 +40,9 @@ CommunitiesModel::roleNames() const {Hidden, "hidden"}, {Depth, "depth"}, {Id, "id"}, + {UnreadMessages, "unreadMessages"}, + {HasLoudNotification, "hasLoudNotification"}, + {Muted, "muted"}, }; } @@ -50,6 +65,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 +92,17 @@ CommunitiesModel::data(const QModelIndex &index, int role) const return 0; case CommunitiesModel::Roles::Id: return ""; + case CommunitiesModel::Roles::UnreadMessages: { + int total{0}; + for (const auto &[id, info] : cache::getRoomInfo(cache::joinedRooms())) + total += info.notification_count; + return total; + } + case CommunitiesModel::Roles::HasLoudNotification: + for (const auto &[id, info] : cache::getRoomInfo(cache::joinedRooms())) + if (info.highlight_count > 0) + return true; + return false; } } else if (index.row() == 1) { switch (role) { @@ -84,16 +117,27 @@ 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: { + int total{0}; + for (const auto &[id, info] : cache::getRoomInfo(directMessages_)) + total += info.notification_count; + return total; + } + case CommunitiesModel::Roles::HasLoudNotification: + for (const auto &[id, info] : cache::getRoomInfo(directMessages_)) + if (info.highlight_count > 0) + return true; + return false; } } 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 +151,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 +162,10 @@ 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: + return utils::getChildNotificationsForSpace(id).first; + case CommunitiesModel::Roles::HasLoudNotification: + return utils::getChildNotificationsForSpace(id).second > 0; } } else if (index.row() - 2 < tags_.size() + spaceOrder_.size()) { auto tag = tags_.at(index.row() - 2 - spaceOrder_.size()); @@ -160,7 +208,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 +219,24 @@ CommunitiesModel::data(const QModelIndex &index, int role) const return 0; case CommunitiesModel::Roles::Id: return "tag:" + tag; + case CommunitiesModel::Roles::UnreadMessages: { + int total{0}; + auto rooms{cache::joinedRooms()}; + for (const auto &[roomid, info] : cache::getRoomInfo(rooms)) + if (std::find(std::begin(info.tags), std::end(info.tags), tag.toStdString()) != + std::end(info.tags)) + total += info.notification_count; + return total; + } + case CommunitiesModel::Roles::HasLoudNotification: { + auto rooms{cache::joinedRooms()}; + for (const auto &[roomid, info] : cache::getRoomInfo(rooms)) + if (std::find(std::begin(info.tags), std::end(info.tags), tag.toStdString()) != + std::end(info.tags)) + if (info.highlight_count > 0) + return true; + return false; + } } } return QVariant(); @@ -277,8 +343,8 @@ CommunitiesModel::initializeSidebar() for (const auto &t : ts) tags_.push_back(QString::fromStdString(t)); - hiddentTagIds_ = UserSettings::instance()->hiddenTags(); spaceOrder_.restoreCollapsed(); + endResetModel(); emit tagsChanged(); @@ -298,12 +364,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 +389,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 +419,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>(e)) { @@ -380,8 +445,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>(e)) { + if (auto event = + std::get_if>(&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 +461,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 +475,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 +494,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 +516,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..5a659751 100644 --- a/src/timeline/CommunitiesModel.h +++ b/src/timeline/CommunitiesModel.h @@ -48,13 +48,17 @@ public: Parent, Depth, Id, + UnreadMessages, + HasLoudNotification, + Muted, + IsDirect, }; struct FlatTree { struct Elem { - QString name; + QString id; int depth = 0; bool collapsed = false; }; @@ -65,7 +69,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 +125,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 +142,7 @@ public slots: return tagsWD; } void toggleTagId(QString tagId); + void toggleTagMute(QString tagId); FilteredCommunitiesModel *filtered() { return new FilteredCommunitiesModel(this, this); } signals: @@ -149,9 +154,11 @@ signals: private: QStringList tags_; QString currentTagId_; - QStringList hiddentTagIds_; + QStringList hiddenTagIds_; + QStringList mutedTagIds_; FlatTree spaceOrder_; std::map spaces_; + std::vector directMessages_; friend class FilteredCommunitiesModel; }; diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index 1cf16243..3b46c053 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(); } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 767fdaa2..db56ac52 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -364,16 +364,26 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj { this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString()); - auto roomInfo = cache::singleRoomInfo(room_id_.toStdString()); - this->isSpace_ = roomInfo.is_space; - this->notification_count = roomInfo.notification_count; - this->highlight_count = roomInfo.highlight_count; - lastMessage_.timestamp = roomInfo.approximate_last_modification_ts; + auto roomInfo = cache::singleRoomInfo(room_id_.toStdString()); + this->isSpace_ = roomInfo.is_space; + this->notification_count = + isSpace_ ? utils::getChildNotificationsForSpace(room_id_).first : roomInfo.notification_count; + this->highlight_count = + isSpace_ ? utils::getChildNotificationsForSpace(room_id_).second : 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); + if (isSpace_) + connect(ChatPage::instance(), &ChatPage::unreadMessages, this, [this](int) { + auto temp{utils::getChildNotificationsForSpace(room_id_)}; + notification_count = temp.first; + highlight_count = temp.second; + emit notificationsChanged(); + }); + connect( this, &TimelineModel::redactionFailed,